hammoc 1.8.3 → 1.9.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 (304) hide show
  1. package/README.md +9 -6
  2. package/package.json +1 -1
  3. package/packages/client/dist/assets/MermaidViewer-B-LwclIl.js +7 -0
  4. package/packages/client/dist/assets/PdfViewer-C3Nymwn3.js +12 -0
  5. package/packages/client/dist/assets/PdfViewer-QCQEGvBO.css +1 -0
  6. package/packages/client/dist/assets/abnfDiagram-VRR7QNED-GfhY_hnV.js +1 -0
  7. package/packages/client/dist/assets/{agentExampleHighlight-B0EHAvjx.js → agentExampleHighlight-CyhVLy0_.js} +1 -1
  8. package/packages/client/dist/assets/arc-8nyjXGC3.js +1 -0
  9. package/packages/client/dist/assets/architectureDiagram-ZJ3FMSHR-B4onXLm4.js +36 -0
  10. package/packages/client/dist/assets/blockDiagram-677ZJIJ3-D0D6ncaN.js +132 -0
  11. package/packages/client/dist/assets/c4Diagram-LMCZKHZV-CE7Tip-_.js +10 -0
  12. package/packages/client/dist/assets/channel-CPjlbdOo.js +1 -0
  13. package/packages/client/dist/assets/chunk-2Q5K7J3B-4rW1da1Z.js +1 -0
  14. package/packages/client/dist/assets/chunk-32BRIVSS-JuzxS4QA.js +1 -0
  15. package/packages/client/dist/assets/chunk-5VM5RSS4-WRNf-xsi.js +15 -0
  16. package/packages/client/dist/assets/chunk-EX3LRPZG-BDWPlBKO.js +231 -0
  17. package/packages/client/dist/assets/chunk-JWPE2WC7-B8nCyQAc.js +1 -0
  18. package/packages/client/dist/assets/chunk-MOJQB5TN-7c-yriHe.js +88 -0
  19. package/packages/client/dist/assets/chunk-RYQCIY6F-LTDywRs7.js +1 -0
  20. package/packages/client/dist/assets/chunk-V7JOEXUC-DjVgQpjl.js +206 -0
  21. package/packages/client/dist/assets/chunk-VR4S4FIN-7jDdkUpx.js +1 -0
  22. package/packages/client/dist/assets/chunk-XXDRQBXY-ENKV2O8a.js +1 -0
  23. package/packages/client/dist/assets/classDiagram-OUVF2IWQ-C0PbC7Pi.js +1 -0
  24. package/packages/client/dist/assets/classDiagram-v2-EOCWNBFH-C0PbC7Pi.js +1 -0
  25. package/packages/client/dist/assets/{commandTokenHighlight-BFQeQmwm.js → commandTokenHighlight-DGP0RKx0.js} +1 -1
  26. package/packages/client/dist/assets/cose-bilkent-JH36ORCC-CQTNdQ-g.js +1 -0
  27. package/packages/client/dist/assets/cynefin-VYW2F7L2-BYlAJRcO.js +178 -0
  28. package/packages/client/dist/assets/cynefinDiagram-TSTJHNR4-7zyUTflT.js +62 -0
  29. package/packages/client/dist/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  30. package/packages/client/dist/assets/dagre-VKFMJZFB-DyZir42W.js +4 -0
  31. package/packages/client/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  32. package/packages/client/dist/assets/diagram-FQU43EPY-CmNAU8Nw.js +3 -0
  33. package/packages/client/dist/assets/diagram-G47NLZAW-B0EIk03o.js +24 -0
  34. package/packages/client/dist/assets/diagram-NH7WQ7WH-Dulv0f1N.js +24 -0
  35. package/packages/client/dist/assets/diagram-OA4YK3LP-DEEcMLgi.js +30 -0
  36. package/packages/client/dist/assets/diagram-WEI45ONY-ZYebAZ93.js +41 -0
  37. package/packages/client/dist/assets/ebnfDiagram-CCIWWBDH-DCHpt3qn.js +1 -0
  38. package/packages/client/dist/assets/erDiagram-Q63AITRT-B6T87-48.js +85 -0
  39. package/packages/client/dist/assets/flowDiagram-23GEKE2U-DAcW6mKJ.js +156 -0
  40. package/packages/client/dist/assets/ganttDiagram-NO4QXBWP-Ct_eFFcM.js +292 -0
  41. package/packages/client/dist/assets/gitGraphDiagram-IHSO6WYX-BGi8Fdm2.js +106 -0
  42. package/packages/client/dist/assets/graph-C9eacEi8.js +1 -0
  43. package/packages/client/dist/assets/index-CtMiUbMJ.js +1671 -0
  44. package/packages/client/dist/assets/index-hTu6ZG2N.js +2 -0
  45. package/packages/client/dist/assets/index-oBa2uNlv.css +32 -0
  46. package/packages/client/dist/assets/infoDiagram-FWYZ7A6U-CqvwbXJn.js +2 -0
  47. package/packages/client/dist/assets/init-Gi6I4Gst.js +1 -0
  48. package/packages/client/dist/assets/ishikawaDiagram-FXEZZL3T-CHhr5ioQ.js +70 -0
  49. package/packages/client/dist/assets/journeyDiagram-5HDEW3XC-d20NzZAd.js +139 -0
  50. package/packages/client/dist/assets/kanban-definition-HUTT4EX6-CHx3wOer.js +89 -0
  51. package/packages/client/dist/assets/katex-C5jXJg4s.js +257 -0
  52. package/packages/client/dist/assets/layout-DEXfKzaS.js +1 -0
  53. package/packages/client/dist/assets/linear-Cnp1E9M4.js +1 -0
  54. package/packages/client/dist/assets/map-Czzmt4hB.js +1 -0
  55. package/packages/client/dist/assets/mermaid.core-jAhLhPYr.js +314 -0
  56. package/packages/client/dist/assets/mindmap-definition-LN4V7U3C-zi30RPoJ.js +96 -0
  57. package/packages/client/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  58. package/packages/client/dist/assets/pdf.worker.min-qwK7q_zL.mjs +28 -0
  59. package/packages/client/dist/assets/pegDiagram-2B236MQR-4gKLzTqu.js +1 -0
  60. package/packages/client/dist/assets/pieDiagram-ENE6RG2P-DRDhpjxP.js +39 -0
  61. package/packages/client/dist/assets/quadrantDiagram-ABIIQ3AL-FmTxRIlW.js +7 -0
  62. package/packages/client/dist/assets/railroadDiagram-RFXS5EU6-Bc-Q7iPs.js +1 -0
  63. package/packages/client/dist/assets/requirementDiagram-TGXJPOKE-D209tlqv.js +84 -0
  64. package/packages/client/dist/assets/sankeyDiagram-HTMAVEWB--I_lSbCV.js +40 -0
  65. package/packages/client/dist/assets/sequenceDiagram-DBY2YBRQ-Da5YJcDJ.js +162 -0
  66. package/packages/client/dist/assets/sizeCapture-X5ZJPWSS-DYuc-rzB.js +1 -0
  67. package/packages/client/dist/assets/{snippetTokenHighlight-DVju7SBc.js → snippetTokenHighlight-qXHxvlCE.js} +1 -1
  68. package/packages/client/dist/assets/stateDiagram-2N3HPSRC-DoOeZoRP.js +1 -0
  69. package/packages/client/dist/assets/stateDiagram-v2-6OUMAXLB-DLkUyWs6.js +1 -0
  70. package/packages/client/dist/assets/swimlanes-5IMT3BWC-BcR7uCTf.js +2 -0
  71. package/packages/client/dist/assets/swimlanesDiagram-G3AALYLV-C0FDo-hf.js +8 -0
  72. package/packages/client/dist/assets/timeline-definition-FHXFAJF6-CLzKR6_i.js +120 -0
  73. package/packages/client/dist/assets/vennDiagram-L72KCM5P-TajEacPR.js +34 -0
  74. package/packages/client/dist/assets/wardleyDiagram-EHGQE667-DKvVEb3V.js +78 -0
  75. package/packages/client/dist/assets/xychartDiagram-FW5EYKEG-CaJG8r0j.js +7 -0
  76. package/packages/client/dist/index.html +2 -2
  77. package/packages/client/dist/sw.js +2 -2
  78. package/packages/server/dist/controllers/accountController.d.ts.map +1 -1
  79. package/packages/server/dist/controllers/accountController.js +31 -11
  80. package/packages/server/dist/controllers/accountController.js.map +1 -1
  81. package/packages/server/dist/controllers/fileSystemController.d.ts +6 -0
  82. package/packages/server/dist/controllers/fileSystemController.d.ts.map +1 -1
  83. package/packages/server/dist/controllers/fileSystemController.js +34 -0
  84. package/packages/server/dist/controllers/fileSystemController.js.map +1 -1
  85. package/packages/server/dist/controllers/projectController.d.ts.map +1 -1
  86. package/packages/server/dist/controllers/projectController.js +4 -2
  87. package/packages/server/dist/controllers/projectController.js.map +1 -1
  88. package/packages/server/dist/controllers/sessionController.d.ts +5 -0
  89. package/packages/server/dist/controllers/sessionController.d.ts.map +1 -1
  90. package/packages/server/dist/controllers/sessionController.js +95 -3
  91. package/packages/server/dist/controllers/sessionController.js.map +1 -1
  92. package/packages/server/dist/handlers/hookBridge.d.ts +131 -0
  93. package/packages/server/dist/handlers/hookBridge.d.ts.map +1 -0
  94. package/packages/server/dist/handlers/hookBridge.js +178 -0
  95. package/packages/server/dist/handlers/hookBridge.js.map +1 -0
  96. package/packages/server/dist/handlers/streamCallbacks.d.ts.map +1 -1
  97. package/packages/server/dist/handlers/streamCallbacks.js +30 -4
  98. package/packages/server/dist/handlers/streamCallbacks.js.map +1 -1
  99. package/packages/server/dist/handlers/websocket.d.ts +44 -2
  100. package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
  101. package/packages/server/dist/handlers/websocket.js +666 -64
  102. package/packages/server/dist/handlers/websocket.js.map +1 -1
  103. package/packages/server/dist/index.js +4 -1
  104. package/packages/server/dist/index.js.map +1 -1
  105. package/packages/server/dist/locales/en/server.json +9 -3
  106. package/packages/server/dist/locales/es/server.json +9 -3
  107. package/packages/server/dist/locales/ja/server.json +9 -3
  108. package/packages/server/dist/locales/ko/server.json +9 -3
  109. package/packages/server/dist/locales/pt/server.json +9 -3
  110. package/packages/server/dist/locales/zh-CN/server.json +9 -3
  111. package/packages/server/dist/routes/account.d.ts.map +1 -1
  112. package/packages/server/dist/routes/account.js +4 -2
  113. package/packages/server/dist/routes/account.js.map +1 -1
  114. package/packages/server/dist/routes/fileSystem.d.ts.map +1 -1
  115. package/packages/server/dist/routes/fileSystem.js +2 -0
  116. package/packages/server/dist/routes/fileSystem.js.map +1 -1
  117. package/packages/server/dist/routes/sessions.d.ts.map +1 -1
  118. package/packages/server/dist/routes/sessions.js +2 -0
  119. package/packages/server/dist/routes/sessions.js.map +1 -1
  120. package/packages/server/dist/services/accountInfoService.d.ts +36 -1
  121. package/packages/server/dist/services/accountInfoService.d.ts.map +1 -1
  122. package/packages/server/dist/services/accountInfoService.js +90 -4
  123. package/packages/server/dist/services/accountInfoService.js.map +1 -1
  124. package/packages/server/dist/services/accountStorageService.d.ts +55 -9
  125. package/packages/server/dist/services/accountStorageService.d.ts.map +1 -1
  126. package/packages/server/dist/services/accountStorageService.js +244 -12
  127. package/packages/server/dist/services/accountStorageService.js.map +1 -1
  128. package/packages/server/dist/services/chatEngine.d.ts +59 -3
  129. package/packages/server/dist/services/chatEngine.d.ts.map +1 -1
  130. package/packages/server/dist/services/chatService.d.ts +112 -3
  131. package/packages/server/dist/services/chatService.d.ts.map +1 -1
  132. package/packages/server/dist/services/chatService.js +353 -98
  133. package/packages/server/dist/services/chatService.js.map +1 -1
  134. package/packages/server/dist/services/claudeLoginService.d.ts +14 -3
  135. package/packages/server/dist/services/claudeLoginService.d.ts.map +1 -1
  136. package/packages/server/dist/services/claudeLoginService.js +18 -4
  137. package/packages/server/dist/services/claudeLoginService.js.map +1 -1
  138. package/packages/server/dist/services/cliChatEngine.d.ts.map +1 -1
  139. package/packages/server/dist/services/cliChatEngine.js +124 -22
  140. package/packages/server/dist/services/cliChatEngine.js.map +1 -1
  141. package/packages/server/dist/services/cliModalDetect.d.ts +14 -0
  142. package/packages/server/dist/services/cliModalDetect.d.ts.map +1 -1
  143. package/packages/server/dist/services/cliModalDetect.js +12 -3
  144. package/packages/server/dist/services/cliModalDetect.js.map +1 -1
  145. package/packages/server/dist/services/cliSessionPool.js +1 -1
  146. package/packages/server/dist/services/cliSessionPool.js.map +1 -1
  147. package/packages/server/dist/services/commandService.d.ts.map +1 -1
  148. package/packages/server/dist/services/commandService.js +19 -9
  149. package/packages/server/dist/services/commandService.js.map +1 -1
  150. package/packages/server/dist/services/engineResidencyRegistry.d.ts +112 -0
  151. package/packages/server/dist/services/engineResidencyRegistry.d.ts.map +1 -0
  152. package/packages/server/dist/services/engineResidencyRegistry.js +195 -0
  153. package/packages/server/dist/services/engineResidencyRegistry.js.map +1 -0
  154. package/packages/server/dist/services/harnessAgentService.d.ts.map +1 -1
  155. package/packages/server/dist/services/harnessAgentService.js +12 -2
  156. package/packages/server/dist/services/harnessAgentService.js.map +1 -1
  157. package/packages/server/dist/services/harnessLintService.d.ts.map +1 -1
  158. package/packages/server/dist/services/harnessLintService.js +25 -114
  159. package/packages/server/dist/services/harnessLintService.js.map +1 -1
  160. package/packages/server/dist/services/historyParser.d.ts.map +1 -1
  161. package/packages/server/dist/services/historyParser.js +23 -6
  162. package/packages/server/dist/services/historyParser.js.map +1 -1
  163. package/packages/server/dist/services/imageStorageService.d.ts.map +1 -1
  164. package/packages/server/dist/services/imageStorageService.js +7 -0
  165. package/packages/server/dist/services/imageStorageService.js.map +1 -1
  166. package/packages/server/dist/services/projectService.d.ts +4 -1
  167. package/packages/server/dist/services/projectService.d.ts.map +1 -1
  168. package/packages/server/dist/services/projectService.js +14 -1
  169. package/packages/server/dist/services/projectService.js.map +1 -1
  170. package/packages/server/dist/services/promptQueue.d.ts +34 -0
  171. package/packages/server/dist/services/promptQueue.d.ts.map +1 -0
  172. package/packages/server/dist/services/promptQueue.js +80 -0
  173. package/packages/server/dist/services/promptQueue.js.map +1 -0
  174. package/packages/server/dist/services/ptyService.d.ts.map +1 -1
  175. package/packages/server/dist/services/ptyService.js +4 -1
  176. package/packages/server/dist/services/ptyService.js.map +1 -1
  177. package/packages/server/dist/services/queueService.d.ts.map +1 -1
  178. package/packages/server/dist/services/queueService.js +62 -4
  179. package/packages/server/dist/services/queueService.js.map +1 -1
  180. package/packages/server/dist/services/rateLimitProbeService.d.ts +73 -3
  181. package/packages/server/dist/services/rateLimitProbeService.d.ts.map +1 -1
  182. package/packages/server/dist/services/rateLimitProbeService.js +397 -30
  183. package/packages/server/dist/services/rateLimitProbeService.js.map +1 -1
  184. package/packages/server/dist/services/sessionBufferManager.d.ts.map +1 -1
  185. package/packages/server/dist/services/sessionBufferManager.js +22 -4
  186. package/packages/server/dist/services/sessionBufferManager.js.map +1 -1
  187. package/packages/server/dist/services/sessionExportService.d.ts +34 -0
  188. package/packages/server/dist/services/sessionExportService.d.ts.map +1 -0
  189. package/packages/server/dist/services/sessionExportService.js +269 -0
  190. package/packages/server/dist/services/sessionExportService.js.map +1 -0
  191. package/packages/server/dist/services/sessionService.d.ts +6 -0
  192. package/packages/server/dist/services/sessionService.d.ts.map +1 -1
  193. package/packages/server/dist/services/sessionService.js +30 -9
  194. package/packages/server/dist/services/sessionService.js.map +1 -1
  195. package/packages/server/dist/services/streamHandler.d.ts +177 -1
  196. package/packages/server/dist/services/streamHandler.d.ts.map +1 -1
  197. package/packages/server/dist/services/streamHandler.js +425 -39
  198. package/packages/server/dist/services/streamHandler.js.map +1 -1
  199. package/packages/server/dist/services/workspaceContext.d.ts.map +1 -1
  200. package/packages/server/dist/services/workspaceContext.js +6 -0
  201. package/packages/server/dist/services/workspaceContext.js.map +1 -1
  202. package/packages/server/dist/snippets/fix-lint-command-not-on-path +13 -0
  203. package/packages/server/dist/snippets/fix-lint-duplicate-name +12 -0
  204. package/packages/server/dist/snippets/fix-lint-env-var-undefined +13 -0
  205. package/packages/server/dist/snippets/fix-lint-regex-invalid +14 -0
  206. package/packages/server/dist/snippets/fix-lint-tools-non-standard +14 -0
  207. package/packages/server/dist/snippets/fix-lint-url-invalid +13 -0
  208. package/packages/server/dist/snippets/fix-lint-yaml-parse-error +14 -0
  209. package/packages/server/dist/snippets/fix-malformed-skill +17 -0
  210. package/packages/server/dist/utils/backgroundHookSettings.d.ts +31 -0
  211. package/packages/server/dist/utils/backgroundHookSettings.d.ts.map +1 -0
  212. package/packages/server/dist/utils/backgroundHookSettings.js +41 -0
  213. package/packages/server/dist/utils/backgroundHookSettings.js.map +1 -0
  214. package/packages/server/dist/utils/backgroundTaskPoller.d.ts +69 -0
  215. package/packages/server/dist/utils/backgroundTaskPoller.d.ts.map +1 -0
  216. package/packages/server/dist/utils/backgroundTaskPoller.js +356 -0
  217. package/packages/server/dist/utils/backgroundTaskPoller.js.map +1 -0
  218. package/packages/server/dist/utils/backgroundTaskTracker.d.ts +51 -3
  219. package/packages/server/dist/utils/backgroundTaskTracker.d.ts.map +1 -1
  220. package/packages/server/dist/utils/backgroundTaskTracker.js +117 -9
  221. package/packages/server/dist/utils/backgroundTaskTracker.js.map +1 -1
  222. package/packages/server/dist/utils/backgroundToolGuard.d.ts +26 -0
  223. package/packages/server/dist/utils/backgroundToolGuard.d.ts.map +1 -0
  224. package/packages/server/dist/utils/backgroundToolGuard.js +40 -0
  225. package/packages/server/dist/utils/backgroundToolGuard.js.map +1 -0
  226. package/packages/server/dist/utils/boundedRing.d.ts +44 -0
  227. package/packages/server/dist/utils/boundedRing.d.ts.map +1 -0
  228. package/packages/server/dist/utils/boundedRing.js +77 -0
  229. package/packages/server/dist/utils/boundedRing.js.map +1 -0
  230. package/packages/server/dist/utils/effortUtils.d.ts +7 -6
  231. package/packages/server/dist/utils/effortUtils.d.ts.map +1 -1
  232. package/packages/server/dist/utils/effortUtils.js +10 -9
  233. package/packages/server/dist/utils/effortUtils.js.map +1 -1
  234. package/packages/server/dist/utils/messageTree.d.ts +25 -0
  235. package/packages/server/dist/utils/messageTree.d.ts.map +1 -1
  236. package/packages/server/dist/utils/messageTree.js +167 -3
  237. package/packages/server/dist/utils/messageTree.js.map +1 -1
  238. package/packages/server/dist/utils/pathUtils.d.ts.map +1 -1
  239. package/packages/server/dist/utils/pathUtils.js +1 -0
  240. package/packages/server/dist/utils/pathUtils.js.map +1 -1
  241. package/packages/server/dist/utils/residencyActivation.d.ts +61 -0
  242. package/packages/server/dist/utils/residencyActivation.d.ts.map +1 -0
  243. package/packages/server/dist/utils/residencyActivation.js +60 -0
  244. package/packages/server/dist/utils/residencyActivation.js.map +1 -0
  245. package/packages/server/dist/utils/residencyPolicy.d.ts +62 -0
  246. package/packages/server/dist/utils/residencyPolicy.d.ts.map +1 -0
  247. package/packages/server/dist/utils/residencyPolicy.js +82 -0
  248. package/packages/server/dist/utils/residencyPolicy.js.map +1 -0
  249. package/packages/server/dist/utils/runtimeParamStrategy.d.ts +51 -0
  250. package/packages/server/dist/utils/runtimeParamStrategy.d.ts.map +1 -0
  251. package/packages/server/dist/utils/runtimeParamStrategy.js +60 -0
  252. package/packages/server/dist/utils/runtimeParamStrategy.js.map +1 -0
  253. package/packages/server/dist/utils/stopStrategy.d.ts +62 -0
  254. package/packages/server/dist/utils/stopStrategy.d.ts.map +1 -0
  255. package/packages/server/dist/utils/stopStrategy.js +65 -0
  256. package/packages/server/dist/utils/stopStrategy.js.map +1 -0
  257. package/packages/server/resources/internals/INDEX.md +1 -1
  258. package/packages/server/resources/internals/image-storage.md +14 -2
  259. package/packages/server/resources/manual/02-chat.md +30 -13
  260. package/packages/server/resources/manual/03-sessions.md +22 -1
  261. package/packages/server/resources/manual/04-slash-commands-favorites.md +2 -1
  262. package/packages/server/resources/manual/05-projects.md +1 -0
  263. package/packages/server/resources/manual/06-file-explorer-editor.md +21 -2
  264. package/packages/server/resources/manual/07-git.md +1 -1
  265. package/packages/server/resources/manual/08-terminal.md +1 -1
  266. package/packages/server/resources/manual/12-harness-workbench.md +21 -16
  267. package/packages/server/resources/manual/13-settings.md +22 -6
  268. package/packages/server/resources/manual/14-keyboard-shortcuts.md +8 -1
  269. package/packages/shared/dist/constants/messageTree.d.ts +2 -0
  270. package/packages/shared/dist/constants/messageTree.d.ts.map +1 -1
  271. package/packages/shared/dist/constants/messageTree.js +6 -0
  272. package/packages/shared/dist/constants/messageTree.js.map +1 -1
  273. package/packages/shared/dist/index.d.ts +1 -1
  274. package/packages/shared/dist/index.d.ts.map +1 -1
  275. package/packages/shared/dist/index.js.map +1 -1
  276. package/packages/shared/dist/types/accounts.d.ts +13 -0
  277. package/packages/shared/dist/types/accounts.d.ts.map +1 -1
  278. package/packages/shared/dist/types/history.d.ts +14 -1
  279. package/packages/shared/dist/types/history.d.ts.map +1 -1
  280. package/packages/shared/dist/types/message.d.ts +26 -3
  281. package/packages/shared/dist/types/message.d.ts.map +1 -1
  282. package/packages/shared/dist/types/message.js +21 -1
  283. package/packages/shared/dist/types/message.js.map +1 -1
  284. package/packages/shared/dist/types/preferences.d.ts +5 -1
  285. package/packages/shared/dist/types/preferences.d.ts.map +1 -1
  286. package/packages/shared/dist/types/preferences.js +3 -0
  287. package/packages/shared/dist/types/preferences.js.map +1 -1
  288. package/packages/shared/dist/types/sdk.d.ts +68 -10
  289. package/packages/shared/dist/types/sdk.d.ts.map +1 -1
  290. package/packages/shared/dist/types/sdk.js +56 -16
  291. package/packages/shared/dist/types/sdk.js.map +1 -1
  292. package/packages/shared/dist/types/session.d.ts +6 -0
  293. package/packages/shared/dist/types/session.d.ts.map +1 -1
  294. package/packages/shared/dist/types/session.js +6 -0
  295. package/packages/shared/dist/types/session.js.map +1 -1
  296. package/packages/shared/dist/types/streaming.d.ts +32 -2
  297. package/packages/shared/dist/types/streaming.d.ts.map +1 -1
  298. package/packages/shared/dist/types/streaming.js.map +1 -1
  299. package/packages/shared/dist/types/websocket.d.ts +4 -0
  300. package/packages/shared/dist/types/websocket.d.ts.map +1 -1
  301. package/scripts/probe-persistent-hooks.mjs +795 -0
  302. package/packages/client/dist/assets/index-BX7y_Qt3.css +0 -32
  303. package/packages/client/dist/assets/index-Bav7AMMy.js +0 -2
  304. package/packages/client/dist/assets/index-DgRQoBIg.js +0 -1657
@@ -4,8 +4,31 @@
4
4
  */
5
5
  import { correctContextWindow, sanitizeToolResultContent } from '@hammoc/shared';
6
6
  import { createLogger } from '../utils/logger.js';
7
+ import { parseTaskNotification } from './historyParser.js';
7
8
  import { SDKMessageType, ContentBlockType, createInitialStreamingState, } from '@hammoc/shared';
8
9
  const log = createLogger('streamHandler');
10
+ /**
11
+ * Bounded micro-grace (Story 38.2 AC7) for the rare case where the main Stop
12
+ * snapshot has not landed at the instant the result is consumed. The order probe
13
+ * (2026-07-08) showed the main Stop reliably LEADS the result by ~2-5ms, so this
14
+ * is a safety margin against the control-channel/main-stream race flipping — NOT
15
+ * the retired 45s follow-up grace. If Stop still never arrives (engine crash /
16
+ * lost control channel) the turn ends and the poller safety net covers survivors.
17
+ */
18
+ export const STOP_SNAPSHOT_GRACE_MS = 250;
19
+ /**
20
+ * Grace bound for a hold justified ONLY by the tracker (Stop ledger empty).
21
+ * Unlike a ledger hold — where the engine itself promises more work — a
22
+ * tracker-only hold waits for a queued notification to inject and its follow-up
23
+ * generation to start streaming. Injection lands within milliseconds of the
24
+ * Stop; the follow-up's first stream event needs an API roundtrip (TTFT, which
25
+ * can stretch on heavy contexts/retries). Each notification sighting re-arms the
26
+ * window; the first main-chain generation activity cancels it (normal hold until
27
+ * the next result takes over). Expiry ends the turn — covers stopped/failed
28
+ * notifications that never produce a follow-up, and tracker entries whose
29
+ * notification never arrives (engine stall → poller net territory).
30
+ */
31
+ export const TRACKER_HOLD_GRACE_MS = 30_000;
9
32
  // Intentionally duplicated in chatService.ts for file independence
10
33
  function extractContextWindow(modelUsage) {
11
34
  if (!modelUsage)
@@ -28,12 +51,79 @@ export class StreamHandler {
28
51
  receivedStreamTextDelta = false;
29
52
  /** Whether stream_event thinking deltas have been received (to avoid duplicate thinking from assistant messages) */
30
53
  receivedStreamThinkingDelta = false;
54
+ /**
55
+ * Stable per-text-block chunk id. Every delta of ONE text block must carry the
56
+ * SAME messageId: the client re-attaches a delta to its block's segment by id
57
+ * when another card (e.g. a background subagent's tool activity) lands between
58
+ * two deltas — with a fresh id per delta the split is unrecoverable. The block
59
+ * key is (main-chain API message generation, content block index): indexes
60
+ * restart per API message, so message_start advances the generation.
61
+ */
62
+ apiMessageGeneration = 0;
63
+ currentTextBlockKey = null;
64
+ currentTextBlockMessageId = null;
65
+ /** Same contract for thinking blocks (kept separate — thinking and text never share a block index). */
66
+ currentThinkingBlockKey = null;
67
+ currentThinkingBlockMessageId = null;
31
68
  /** Throttle tracker: last emit time per tool ID for partial input updates */
32
69
  lastPartialEmitByTool = new Map();
33
70
  /** Running estimate of context window token consumption (updated from assistant usage + tool results) */
34
71
  estimatedContextTokens = 0;
35
72
  /** Known context window size from modelUsage (0 until first result arrives) */
36
73
  contextWindowSize = 0;
74
+ /**
75
+ * Turn-boundary source of truth for background work (Story 38.2). When set, a
76
+ * `result` message only ENDS THE TURN if the engine's Stop hook reports nothing
77
+ * in flight (`hadInFlight === false`) — otherwise the stream is held open so the
78
+ * completion notification, the engine's auto-resume follow-up generation, and the
79
+ * further result(s) arrive LIVE (probe-verified on bundled SDK 0.3.191: the
80
+ * generator keeps delivering and ends naturally after the follow-up's final
81
+ * result). Replaces the retired ack-marker `BackgroundTaskTracker.pending` guess.
82
+ */
83
+ stopSource;
84
+ /** AC7 micro-grace bound (overridable for deterministic tests). */
85
+ stopSnapshotGraceMs = STOP_SNAPSHOT_GRACE_MS;
86
+ /**
87
+ * Second turn-boundary gate (see {@link PendingTaskSource}): tracked background
88
+ * tasks not yet resolved by a completion notification. A result only ends the
89
+ * turn when the Stop ledger is empty AND this count is 0 — the tracker covers
90
+ * the queued-notification window the ledger cannot see.
91
+ */
92
+ pendingSource;
93
+ /** Tracker-only hold grace bound (overridable for deterministic tests). */
94
+ trackerHoldGraceMs = TRACKER_HOLD_GRACE_MS;
95
+ /**
96
+ * Deadline (epoch ms) of an active tracker-only hold, or null. Set at an
97
+ * intermediate result held on the tracker gate alone; extended by each task
98
+ * notification (injection progressing); cancelled by main-chain generation
99
+ * activity (a follow-up started — the next result re-evaluates); expiry ends
100
+ * the turn with the held result.
101
+ */
102
+ trackerHoldDeadline = null;
103
+ /**
104
+ * Task-notification dedupe (toolUseId, or taskId fallback): the same
105
+ * completion can surface BOTH as a stream `system/task_notification` event and
106
+ * as the injected `<task-notification>` user message — onTaskNotification must
107
+ * fire once per task (the tracker resolution is idempotent, but UI emits are not).
108
+ */
109
+ notifiedTaskKeys = new Set();
110
+ /**
111
+ * Set when this turn ended via tracker-hold grace EXPIRY (not a clean result).
112
+ * The expiry path abandons an in-flight `iterator.next()` — on a RESIDENT
113
+ * engine that parked read sits on the SHARED session iterator and would steal
114
+ * the NEXT turn's first message (async-generator next() calls are FIFO), and a
115
+ * late follow-up could still generate invisibly. The caller (websocket
116
+ * residency reconciliation) MUST therefore evict the resident engine when this
117
+ * is set: close() settles the parked read, kills any not-yet-started follow-up
118
+ * (its notification is already in the JSONL — the next resumed turn sees it),
119
+ * and the next send resumes fresh. Non-resident engines are closed by the
120
+ * chatService finally anyway.
121
+ */
122
+ _trackerHoldExpired = false;
123
+ /** Whether this turn ended via tracker-hold grace expiry (see field docs). */
124
+ get trackerHoldExpired() {
125
+ return this._trackerHoldExpired;
126
+ }
37
127
  /**
38
128
  * @param is1M Whether the request runs at a 1M context window (Opus auto or an
39
129
  * explicit Sonnet `[1m]` opt-in). Drives the contextWindow meter correction —
@@ -44,6 +134,45 @@ export class StreamHandler {
44
134
  this.is1M = is1M;
45
135
  this.state = createInitialStreamingState();
46
136
  }
137
+ /**
138
+ * Unescape a JSON string body in ONE left-to-right pass. Sequential chained
139
+ * `.replace()` calls consume backslashes more than once — a literal `\\n` in
140
+ * the source (backslash + n as DATA) collapsed into a real newline because the
141
+ * `\n` pass ran before the `\\` pass. Each escape must be decoded exactly once.
142
+ * A trailing lone backslash / truncated `\uXX` (partial stream) stays as-is.
143
+ */
144
+ static unescapeJsonString(raw) {
145
+ return raw.replace(/\\(u[0-9a-fA-F]{4}|.)/g, (whole, esc) => {
146
+ switch (esc[0]) {
147
+ case 'n': return '\n';
148
+ case 't': return '\t';
149
+ case 'r': return '\r';
150
+ case 'b': return '\b';
151
+ case 'f': return '\f';
152
+ case '"': return '"';
153
+ case '\\': return '\\';
154
+ case '/': return '/';
155
+ case 'u': return esc.length === 5 ? String.fromCharCode(parseInt(esc.slice(1), 16)) : whole;
156
+ default: return whole; // unknown escape — keep verbatim (preview only)
157
+ }
158
+ });
159
+ }
160
+ /**
161
+ * Index of the first UNESCAPED double quote in `raw`, or -1. Walks backslash
162
+ * pairs so an escaped-backslash-then-quote (source `\\"` = data `\` + CLOSING
163
+ * quote) is detected — a single-character lookbehind cannot distinguish it.
164
+ */
165
+ static findUnescapedQuote(raw) {
166
+ for (let i = 0; i < raw.length; i++) {
167
+ if (raw[i] === '\\') {
168
+ i++;
169
+ continue;
170
+ }
171
+ if (raw[i] === '"')
172
+ return i;
173
+ }
174
+ return -1;
175
+ }
47
176
  /**
48
177
  * Extract partial fields from incomplete JSON for streaming preview.
49
178
  * Called when JSON.parse fails during input_json_delta accumulation.
@@ -54,7 +183,7 @@ export class StreamHandler {
54
183
  // Extract file_path (should be complete early in the stream)
55
184
  const pathMatch = rawJson.match(/"file_path"\s*:\s*"((?:[^"\\]|\\.)*)"/);
56
185
  if (pathMatch)
57
- result.file_path = pathMatch[1].replace(/\\\\/g, '\\').replace(/\\"/g, '"');
186
+ result.file_path = StreamHandler.unescapeJsonString(pathMatch[1]);
58
187
  // Extract partial content (may be incomplete — no closing quote)
59
188
  const contentStart = rawJson.match(/"content"\s*:\s*"/);
60
189
  if (contentStart) {
@@ -65,24 +194,16 @@ export class StreamHandler {
65
194
  partial = partial.slice(0, -2);
66
195
  else if (partial.endsWith('"'))
67
196
  partial = partial.slice(0, -1);
68
- // Unescape JSON string escapes
69
- result.content = partial
70
- .replace(/\\n/g, '\n')
71
- .replace(/\\t/g, '\t')
72
- .replace(/\\r/g, '\r')
73
- .replace(/\\"/g, '"')
74
- .replace(/\\\\/g, '\\');
197
+ result.content = StreamHandler.unescapeJsonString(partial);
75
198
  }
76
199
  // Extract old_string for Edit tool
77
200
  const oldStart = rawJson.match(/"old_string"\s*:\s*"/);
78
201
  if (oldStart) {
79
202
  const start = oldStart.index + oldStart[0].length;
80
- // Find the closing quote (not escaped)
81
203
  const remaining = rawJson.slice(start);
82
- const closeMatch = remaining.match(/(?<!\\)"/);
83
- if (closeMatch) {
84
- result.old_string = remaining.slice(0, closeMatch.index)
85
- .replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
204
+ const closeIdx = StreamHandler.findUnescapedQuote(remaining);
205
+ if (closeIdx !== -1) {
206
+ result.old_string = StreamHandler.unescapeJsonString(remaining.slice(0, closeIdx));
86
207
  }
87
208
  }
88
209
  // Extract new_string for Edit tool
@@ -94,8 +215,7 @@ export class StreamHandler {
94
215
  partial = partial.slice(0, -2);
95
216
  else if (partial.endsWith('"'))
96
217
  partial = partial.slice(0, -1);
97
- result.new_string = partial
98
- .replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
218
+ result.new_string = StreamHandler.unescapeJsonString(partial);
99
219
  }
100
220
  return Object.keys(result).length > 0 ? result : null;
101
221
  }
@@ -116,8 +236,16 @@ export class StreamHandler {
116
236
  this.lastPartialEmitByTool.clear();
117
237
  this.receivedStreamTextDelta = false;
118
238
  this.receivedStreamThinkingDelta = false;
239
+ this.apiMessageGeneration = 0;
240
+ this.currentTextBlockKey = null;
241
+ this.currentTextBlockMessageId = null;
242
+ this.currentThinkingBlockKey = null;
243
+ this.currentThinkingBlockMessageId = null;
119
244
  this.estimatedContextTokens = 0;
120
245
  this.contextWindowSize = 0;
246
+ this.trackerHoldDeadline = null;
247
+ this.notifiedTaskKeys.clear();
248
+ this._trackerHoldExpired = false;
121
249
  }
122
250
  /**
123
251
  * Generate a unique message ID for stream chunks
@@ -276,6 +404,16 @@ export class StreamHandler {
276
404
  toolUseId: taskMsg.tool_use_id,
277
405
  };
278
406
  }
407
+ if (msg.subtype === 'task_started') {
408
+ const taskMsg = message;
409
+ parsed.subtype = 'task_started';
410
+ parsed.taskStarted = {
411
+ taskId: taskMsg.task_id ?? '',
412
+ toolUseId: taskMsg.tool_use_id,
413
+ description: taskMsg.description,
414
+ taskType: taskMsg.task_type,
415
+ };
416
+ }
279
417
  return parsed;
280
418
  }
281
419
  /**
@@ -388,8 +526,14 @@ export class StreamHandler {
388
526
  /**
389
527
  * Process a stream of SDK messages and invoke callbacks
390
528
  */
391
- async processStream(generator, callbacks) {
529
+ async processStream(generator, callbacks, opts) {
392
530
  this.reset();
531
+ this.stopSource = opts?.stopSource;
532
+ if (opts?.stopSnapshotGraceMs !== undefined)
533
+ this.stopSnapshotGraceMs = opts.stopSnapshotGraceMs;
534
+ this.pendingSource = opts?.pendingSource;
535
+ if (opts?.trackerHoldGraceMs !== undefined)
536
+ this.trackerHoldGraceMs = opts.trackerHoldGraceMs;
393
537
  let finalResponse = {
394
538
  id: '',
395
539
  sessionId: '',
@@ -397,14 +541,40 @@ export class StreamHandler {
397
541
  done: false,
398
542
  isError: false,
399
543
  };
544
+ // Whether a TURN-ENDING result was processed (its onComplete already fired
545
+ // inside handleResult). When the loop exits without one (generator ended
546
+ // mid-hold — e.g. engine death), the post-loop fallback fires onComplete.
547
+ let finalResultSeen = false;
548
+ const iterator = generator[Symbol.asyncIterator]();
400
549
  try {
401
- for await (const message of generator) {
402
- const parsed = this.parseMessage(message);
550
+ for (;;) {
551
+ let step;
552
+ if (this.trackerHoldDeadline === null) {
553
+ step = await iterator.next();
554
+ }
555
+ else {
556
+ // Tracker-only hold: bound the wait. The abandoned next() (on expiry)
557
+ // is unwound by the finally's fire-and-forget iterator.return().
558
+ const raced = await StreamHandler.raceDeadline(iterator.next(), this.trackerHoldDeadline);
559
+ if (raced === 'expired') {
560
+ // The queued notification never turned into a live follow-up within
561
+ // the grace (stopped/failed notification with no auto-resume, or an
562
+ // engine stall). End the turn with the held result — the post-loop
563
+ // fallback fires onComplete; the poller net covers true survivors.
564
+ // `trackerHoldExpired` tells the caller to EVICT a resident engine:
565
+ // the abandoned iterator.next() above is parked on the shared session
566
+ // iterator and would steal the next turn's first message otherwise.
567
+ this._trackerHoldExpired = true;
568
+ log.warn(`tracker-hold grace expired (${this.trackerHoldGraceMs}ms, ${this.pendingSource?.pending ?? 0} task(s) still tracked) — ending the turn with the held result (resident engine will be evicted)`);
569
+ callbacks.onBackgroundState?.({ waiting: false, pendingCount: 0 });
570
+ break;
571
+ }
572
+ step = raced;
573
+ }
574
+ if (step.done)
575
+ break;
576
+ const parsed = this.parseMessage(step.value);
403
577
  this.processMessage(parsed, callbacks);
404
- // Capture final response from result message and exit immediately.
405
- // The SDK generator may not close promptly after RESULT, so waiting
406
- // for the next generator.next() can hang indefinitely, leaving the
407
- // queue item stuck in "pending" state even though the response is done.
408
578
  if (parsed.type === SDKMessageType.RESULT) {
409
579
  const resultMsg = parsed;
410
580
  finalResponse = {
@@ -415,7 +585,39 @@ export class StreamHandler {
415
585
  isError: resultMsg.isError,
416
586
  usage: resultMsg.usage,
417
587
  };
418
- break;
588
+ // Dual-gate turn boundary: the engine's Stop-hook ledger (gate 1) AND
589
+ // the tracked-task pending set (gate 2). The ledger answers "is work
590
+ // RUNNING"; the tracker additionally covers the queued-notification
591
+ // window — a completed task whose notification has not been injected
592
+ // yet is invisible to the ledger but guarantees a follow-up.
593
+ const inFlight = await this.resolveInFlight(resultMsg);
594
+ // Single source for the amber "background:waiting" card + inactivity
595
+ // timer (AC6): derived from the SAME verdict that gates the turn.
596
+ callbacks.onBackgroundState?.({ waiting: inFlight.hadInFlight, pendingCount: inFlight.pendingCount });
597
+ // Exit at the final result rather than waiting for generator close —
598
+ // the SDK generator may not close promptly after it, and waiting
599
+ // for the next generator.next() could hang indefinitely. Fire onComplete
600
+ // HERE (moved out of handleResult) now that the turn-ending decision is
601
+ // async: only the turn-ending result completes; intermediate holds.
602
+ if (!inFlight.hadInFlight) {
603
+ finalResultSeen = true;
604
+ this.state.isComplete = true;
605
+ callbacks.onComplete?.(finalResponse);
606
+ break;
607
+ }
608
+ if (inFlight.trackerOnly) {
609
+ // Ledger empty but tracked tasks unresolved — the queued-notification
610
+ // window. Hold, but grace-bounded (see TRACKER_HOLD_GRACE_MS).
611
+ this.trackerHoldDeadline = Date.now() + this.trackerHoldGraceMs;
612
+ log.info(`intermediate result: Stop ledger empty but ${inFlight.trackerPending} tracked task(s) unresolved (queued-notification window) — holding with ${this.trackerHoldGraceMs}ms grace`);
613
+ }
614
+ else {
615
+ // The engine's own ledger promises more work — unbounded hold, the
616
+ // live stream delivers the follow-up (Story 38.2 behavior).
617
+ this.trackerHoldDeadline = null;
618
+ log.info(`intermediate result: ${inFlight.pendingCount} background task(s) in flight (Stop hook) — holding the turn open for the live follow-up`);
619
+ }
620
+ continue;
419
621
  }
420
622
  }
421
623
  }
@@ -426,16 +628,118 @@ export class StreamHandler {
426
628
  throw err;
427
629
  }
428
630
  finally {
631
+ // Fire-and-forget close. At a yield-suspended generator this unwinds it
632
+ // immediately (same as for-await break did); mid-await (grace abandon)
633
+ // the close queues behind the in-flight next() — awaiting it here could
634
+ // block turn finalization on a hung engine.
635
+ void iterator.return?.(undefined)?.catch(() => { });
429
636
  this.cleanup();
430
637
  }
638
+ // Loop exited without a turn-ending result (the generator ended mid-hold —
639
+ // e.g. engine death). Complete the turn with the last captured result so
640
+ // callers finalize normally; the JSONL poller safety net (websocket/queue
641
+ // layer) picks up surviving tasks.
642
+ if (!finalResultSeen && finalResponse.done) {
643
+ this.state.isComplete = true;
644
+ callbacks.onComplete?.(finalResponse);
645
+ }
431
646
  return finalResponse;
432
647
  }
648
+ /**
649
+ * Whether background work is still in flight at this result — the turn-boundary
650
+ * verdict. Two gates, both must clear (Story 38.2 + the 2026-07-12 fix):
651
+ *
652
+ * - **Gate 1 — the engine's Stop-hook `background_tasks[]` ledger**: work the
653
+ * engine reports as running/pending. Consumed per turn; if the snapshot has
654
+ * not landed yet, wait a bounded micro-grace (AC7). Never arrives → gate open
655
+ * (crash → poller net).
656
+ * - **Gate 2 — the tracked-task pending set** (task_started / launch acks minus
657
+ * completion notifications): covers the ledger's deterministic blind spot —
658
+ * a task that COMPLETED while a generation was running has already left the
659
+ * ledger, but its `<task-notification>` is still queued and will trigger an
660
+ * auto-resume follow-up the instant the engine is free (production-verified:
661
+ * injection +5ms after the Stop). A gate-2-only hold is grace-bounded by the
662
+ * caller (trackerHoldDeadline) since a stopped/failed notification may never
663
+ * produce a follow-up.
664
+ *
665
+ * Errors always end the turn (`hadInFlight=false`): a failed engine cannot be
666
+ * trusted to deliver task notifications, so holding would only hang (the
667
+ * poller safety net covers any survivors). No sources at all (unit tests
668
+ * without a bridge/tracker) → end the turn, matching pre-38.2 behavior.
669
+ */
670
+ async resolveInFlight(message) {
671
+ if (message.isError) {
672
+ return { hadInFlight: false, pendingCount: 0, trackerOnly: false, trackerPending: 0 };
673
+ }
674
+ let snapshotInFlight = false;
675
+ let snapshotCount = 0;
676
+ let snapshotAnswered = false;
677
+ if (this.stopSource) {
678
+ let snap = this.stopSource.consumeMainStopSnapshot();
679
+ if (!snap) {
680
+ const deadline = Date.now() + this.stopSnapshotGraceMs;
681
+ while (!snap && Date.now() < deadline) {
682
+ await new Promise((resolve) => setTimeout(resolve, 5));
683
+ snap = this.stopSource.consumeMainStopSnapshot();
684
+ }
685
+ if (!snap) {
686
+ log.warn(`no main Stop snapshot within ${this.stopSnapshotGraceMs}ms of result — ending turn (crash fallback; poller net covers survivors)`);
687
+ }
688
+ }
689
+ snapshotAnswered = snap !== undefined;
690
+ snapshotInFlight = snap?.hadInFlight ?? false;
691
+ snapshotCount = snap?.backgroundTasks.length ?? 0;
692
+ }
693
+ const trackerPending = this.pendingSource?.pending ?? 0;
694
+ // Gate 2 applies only when gate 1 AFFIRMATIVELY answered empty: a MISSING
695
+ // snapshot means the control channel is suspect (engine crash), and holding
696
+ // on the tracker would wait for notifications a dead engine cannot deliver.
697
+ // End the turn instead (the pre-fix crash fallback) — the tracker still has
698
+ // its pendings, so the poller net engages and watches the JSONL.
699
+ const trackerHolds = snapshotAnswered && !snapshotInFlight && trackerPending > 0;
700
+ const hadInFlight = snapshotInFlight || trackerHolds;
701
+ return {
702
+ hadInFlight,
703
+ pendingCount: hadInFlight ? Math.max(snapshotCount, trackerPending) : snapshotCount,
704
+ trackerOnly: trackerHolds,
705
+ trackerPending,
706
+ };
707
+ }
708
+ /**
709
+ * Race a generator step against a tracker-hold deadline. Resolves 'expired'
710
+ * when the deadline passes first; the pending step promise is abandoned to the
711
+ * caller's teardown (iterator.return queues behind it — the documented
712
+ * "grace abandon" shape in processStream's finally).
713
+ */
714
+ static raceDeadline(step, deadline) {
715
+ const ms = deadline - Date.now();
716
+ if (ms <= 0)
717
+ return Promise.resolve('expired');
718
+ let timer;
719
+ const expiry = new Promise((resolve) => {
720
+ timer = setTimeout(() => resolve('expired'), ms);
721
+ });
722
+ return Promise.race([step, expiry]).finally(() => {
723
+ if (timer)
724
+ clearTimeout(timer);
725
+ });
726
+ }
433
727
  /**
434
728
  * Process a single parsed message and invoke appropriate callbacks
435
729
  */
436
730
  processMessage(message, callbacks) {
437
731
  // Fire activity callback for every SDK message (timeout reset)
438
732
  callbacks.onActivity?.(message.type);
733
+ // An active tracker-only hold waits for the queued notification's follow-up
734
+ // to START. Main-chain generation activity IS that start — cancel the grace;
735
+ // the follow-up's own result re-evaluates the boundary (normal hold).
736
+ // Sidechain records (a still-running subagent's activity) must not cancel.
737
+ if (this.trackerHoldDeadline !== null &&
738
+ (message.type === SDKMessageType.ASSISTANT || message.type === SDKMessageType.STREAM_EVENT) &&
739
+ !message.rawMessage.parent_tool_use_id) {
740
+ this.trackerHoldDeadline = null;
741
+ log.info('tracker-hold: main-chain generation activity detected — grace cancelled, holding until its result');
742
+ }
439
743
  switch (message.type) {
440
744
  case SDKMessageType.INIT:
441
745
  this.handleInit(message, callbacks);
@@ -497,15 +801,25 @@ export class StreamHandler {
497
801
  this.state.metadata = this.state.metadata || {};
498
802
  callbacks.onSessionInit?.(rawSessionId, this.state.metadata);
499
803
  }
804
+ // Sidechain (subagent) assistant messages arrive on the parent stream as COMPLETED
805
+ // records (probe-verified: `assistant [text] parent_tool_use_id=…` lands mid-turn;
806
+ // sidechains never emit stream_event deltas). Their tool_use blocks stay visible as
807
+ // live activity cards, but their TEXT/THINKING must not masquerade as main-chain
808
+ // prose: outside the delta-dedup window (a background agent finishing before the new
809
+ // turn's first main text delta) handleTextBlock would emit subagent text as a main
810
+ // assistant card and pollute accumulatedText.
811
+ const isSidechainMsg = Boolean(message.rawMessage.parent_tool_use_id);
500
812
  for (const block of message.contentBlocks) {
501
813
  if (block.type === ContentBlockType.THINKING) {
502
814
  // Skip if stream_event thinking deltas already handled this (avoids double emission)
503
- if (!this.receivedStreamThinkingDelta) {
815
+ if (!isSidechainMsg && !this.receivedStreamThinkingDelta) {
504
816
  callbacks.onThinking?.(block.thinking);
505
817
  }
506
818
  }
507
819
  else if (block.type === ContentBlockType.TEXT) {
508
- this.handleTextBlock(block, callbacks);
820
+ if (!isSidechainMsg) {
821
+ this.handleTextBlock(block, callbacks);
822
+ }
509
823
  }
510
824
  else if (block.type === ContentBlockType.TOOL_USE) {
511
825
  this.handleToolUseBlock(block, callbacks);
@@ -584,6 +898,22 @@ export class StreamHandler {
584
898
  this.estimatedContextTokens += Math.ceil(contentLength / 4);
585
899
  }
586
900
  }
901
+ else if (isMainChain && block.type === ContentBlockType.TEXT) {
902
+ // The engine injects a background task's completion as a
903
+ // `<task-notification>` user message (queued while a generation was
904
+ // running, injected the moment the engine is free). Surface it through
905
+ // the same callback as the stream's system/task_notification event —
906
+ // it resolves the tracker's pending entry (turn-boundary gate 2) and
907
+ // feeds the same UI emit, deduped against the system-event form.
908
+ // ID-less notifications are NOT emitted (same guard as the poller): the
909
+ // callback's no-id fallback removes an ARBITRARY tracker entry, which
910
+ // would punch a hole in gate 2. Requiring the tool-use-id also shrinks
911
+ // the pasted-notification-lookalike surface to exact live-id collisions.
912
+ const notif = parseTaskNotification(block.text);
913
+ if (notif?.toolUseId) {
914
+ this.emitTaskNotification({ taskId: notif.taskId, status: notif.status, summary: notif.summary, toolUseId: notif.toolUseId }, callbacks);
915
+ }
916
+ }
587
917
  }
588
918
  // Emit updated context estimate after processing main-chain tool results
589
919
  if (isMainChain && this.estimatedContextTokens > 0) {
@@ -623,19 +953,53 @@ export class StreamHandler {
623
953
  this.state.metadata = this.state.metadata || {};
624
954
  callbacks.onSessionInit?.(rawSessionId, this.state.metadata);
625
955
  }
626
- // Process thinking deltas
956
+ // Sidechain (subagent) stream events must not disturb the main chain's
957
+ // text-block key — their message_start/indexes belong to a different stream.
958
+ const rawEvent = message.rawMessage;
959
+ const isMainChain = !rawEvent.parent_tool_use_id;
960
+ // A new API message begins: content block indexes restart at 0, so advance
961
+ // the generation to keep text-block keys unique across messages.
962
+ if (message.eventType === 'message_start' && isMainChain) {
963
+ this.apiMessageGeneration++;
964
+ }
965
+ // Process thinking deltas — same block-stable id contract as text deltas, so an
966
+ // interleaved card (background subagent activity) can't split a thinking card either.
627
967
  if (message.thinkingDelta) {
628
968
  this.receivedStreamThinkingDelta = true;
629
- callbacks.onThinking?.(message.thinkingDelta);
969
+ let thinkingMessageId;
970
+ if (isMainChain) {
971
+ const blockKey = `${this.apiMessageGeneration}:${rawEvent.event?.index ?? 0}`;
972
+ if (this.currentThinkingBlockKey !== blockKey || !this.currentThinkingBlockMessageId) {
973
+ this.currentThinkingBlockKey = blockKey;
974
+ this.currentThinkingBlockMessageId = this.generateMessageId();
975
+ }
976
+ thinkingMessageId = this.currentThinkingBlockMessageId;
977
+ }
978
+ callbacks.onThinking?.(message.thinkingDelta, undefined, thinkingMessageId);
630
979
  }
631
980
  // Process text deltas
632
981
  if (message.textDelta) {
633
982
  this.receivedStreamTextDelta = true;
634
983
  this.state.accumulatedText += message.textDelta;
984
+ // One messageId per text BLOCK (not per delta): the client merges chunks
985
+ // by id, so deltas re-attach to their segment even when a tool/thinking
986
+ // card (e.g. background subagent activity) interleaves mid-block.
987
+ let messageId;
988
+ if (isMainChain) {
989
+ const blockKey = `${this.apiMessageGeneration}:${rawEvent.event?.index ?? 0}`;
990
+ if (this.currentTextBlockKey !== blockKey || !this.currentTextBlockMessageId) {
991
+ this.currentTextBlockKey = blockKey;
992
+ this.currentTextBlockMessageId = this.generateMessageId();
993
+ }
994
+ messageId = this.currentTextBlockMessageId;
995
+ }
996
+ else {
997
+ messageId = this.generateMessageId();
998
+ }
635
999
  // Always emit - websocket.ts uses actualSessionId fallback
636
1000
  callbacks.onTextChunk?.({
637
1001
  sessionId: this.state.sessionId ?? '',
638
- messageId: this.generateMessageId(),
1002
+ messageId,
639
1003
  content: message.textDelta,
640
1004
  done: false,
641
1005
  });
@@ -722,7 +1086,10 @@ export class StreamHandler {
722
1086
  callbacks.onCompact?.(message.compactMetadata);
723
1087
  }
724
1088
  if (message.subtype === 'task_notification' && message.taskNotification) {
725
- callbacks.onTaskNotification?.(message.taskNotification);
1089
+ this.emitTaskNotification(message.taskNotification, callbacks);
1090
+ }
1091
+ if (message.subtype === 'task_started' && message.taskStarted) {
1092
+ callbacks.onTaskStarted?.(message.taskStarted);
726
1093
  }
727
1094
  }
728
1095
  /**
@@ -741,7 +1108,6 @@ export class StreamHandler {
741
1108
  * Handle result message - stream completion
742
1109
  */
743
1110
  handleResult(message, callbacks) {
744
- this.state.isComplete = true;
745
1111
  // Update known context window size from result's modelUsage
746
1112
  if (message.usage?.contextWindow && message.usage.contextWindow > 0) {
747
1113
  this.contextWindowSize = message.usage.contextWindow;
@@ -756,15 +1122,35 @@ export class StreamHandler {
756
1122
  result: message.result,
757
1123
  });
758
1124
  }
759
- const response = {
760
- id: message.uuid,
761
- sessionId: message.sessionId,
762
- content: this.state.accumulatedText,
763
- done: true,
764
- isError: message.isError,
765
- usage: message.usage,
766
- };
767
- callbacks.onComplete?.(response);
1125
+ // Completion (message:complete / deferred notifyComplete / queue advance)
1126
+ // is NOT fired here anymore (Story 38.2): whether this result ENDS the turn
1127
+ // is decided asynchronously in processStream from the engine's Stop hook,
1128
+ // AFTER this synchronous per-message dispatch. processStream fires onComplete
1129
+ // at the turn-ending result; an intermediate result (background_tasks[] still
1130
+ // in flight) fires nothing — the auto-resume follow-up is still coming.
1131
+ // The context-window update + onResultError dispatch above still run for
1132
+ // every result, unchanged.
1133
+ }
1134
+ /**
1135
+ * Fire onTaskNotification exactly once per task. The same completion can
1136
+ * surface twice — as a stream `system/task_notification` event AND as the
1137
+ * injected `<task-notification>` user message — and both must resolve the
1138
+ * tracker (idempotent there), but the UI emit must not double. While a
1139
+ * tracker-only hold is active, a sighting proves the injection chain is
1140
+ * progressing — re-arm the grace for the follow-up generation to start.
1141
+ */
1142
+ emitTaskNotification(data, callbacks) {
1143
+ const key = data.toolUseId || data.taskId;
1144
+ if (key) {
1145
+ if (this.notifiedTaskKeys.has(key))
1146
+ return;
1147
+ this.notifiedTaskKeys.add(key);
1148
+ }
1149
+ if (this.trackerHoldDeadline !== null) {
1150
+ this.trackerHoldDeadline = Date.now() + this.trackerHoldGraceMs;
1151
+ log.info(`tracker-hold: task notification sighted (${key || 'no-id'}) — grace re-armed for the follow-up start`);
1152
+ }
1153
+ callbacks.onTaskNotification?.(data);
768
1154
  }
769
1155
  /**
770
1156
  * Cleanup resources after stream processing