hammoc 1.8.4 → 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.
- package/README.md +9 -6
- package/package.json +1 -1
- package/packages/client/dist/assets/MermaidViewer-B-LwclIl.js +7 -0
- package/packages/client/dist/assets/PdfViewer-C3Nymwn3.js +12 -0
- package/packages/client/dist/assets/PdfViewer-QCQEGvBO.css +1 -0
- package/packages/client/dist/assets/abnfDiagram-VRR7QNED-GfhY_hnV.js +1 -0
- package/packages/client/dist/assets/{agentExampleHighlight-Dws80em9.js → agentExampleHighlight-CyhVLy0_.js} +1 -1
- package/packages/client/dist/assets/arc-8nyjXGC3.js +1 -0
- package/packages/client/dist/assets/architectureDiagram-ZJ3FMSHR-B4onXLm4.js +36 -0
- package/packages/client/dist/assets/blockDiagram-677ZJIJ3-D0D6ncaN.js +132 -0
- package/packages/client/dist/assets/c4Diagram-LMCZKHZV-CE7Tip-_.js +10 -0
- package/packages/client/dist/assets/channel-CPjlbdOo.js +1 -0
- package/packages/client/dist/assets/chunk-2Q5K7J3B-4rW1da1Z.js +1 -0
- package/packages/client/dist/assets/chunk-32BRIVSS-JuzxS4QA.js +1 -0
- package/packages/client/dist/assets/chunk-5VM5RSS4-WRNf-xsi.js +15 -0
- package/packages/client/dist/assets/chunk-EX3LRPZG-BDWPlBKO.js +231 -0
- package/packages/client/dist/assets/chunk-JWPE2WC7-B8nCyQAc.js +1 -0
- package/packages/client/dist/assets/chunk-MOJQB5TN-7c-yriHe.js +88 -0
- package/packages/client/dist/assets/chunk-RYQCIY6F-LTDywRs7.js +1 -0
- package/packages/client/dist/assets/chunk-V7JOEXUC-DjVgQpjl.js +206 -0
- package/packages/client/dist/assets/chunk-VR4S4FIN-7jDdkUpx.js +1 -0
- package/packages/client/dist/assets/chunk-XXDRQBXY-ENKV2O8a.js +1 -0
- package/packages/client/dist/assets/classDiagram-OUVF2IWQ-C0PbC7Pi.js +1 -0
- package/packages/client/dist/assets/classDiagram-v2-EOCWNBFH-C0PbC7Pi.js +1 -0
- package/packages/client/dist/assets/{commandTokenHighlight-aR071CNy.js → commandTokenHighlight-DGP0RKx0.js} +1 -1
- package/packages/client/dist/assets/cose-bilkent-JH36ORCC-CQTNdQ-g.js +1 -0
- package/packages/client/dist/assets/cynefin-VYW2F7L2-BYlAJRcO.js +178 -0
- package/packages/client/dist/assets/cynefinDiagram-TSTJHNR4-7zyUTflT.js +62 -0
- package/packages/client/dist/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/packages/client/dist/assets/dagre-VKFMJZFB-DyZir42W.js +4 -0
- package/packages/client/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/packages/client/dist/assets/diagram-FQU43EPY-CmNAU8Nw.js +3 -0
- package/packages/client/dist/assets/diagram-G47NLZAW-B0EIk03o.js +24 -0
- package/packages/client/dist/assets/diagram-NH7WQ7WH-Dulv0f1N.js +24 -0
- package/packages/client/dist/assets/diagram-OA4YK3LP-DEEcMLgi.js +30 -0
- package/packages/client/dist/assets/diagram-WEI45ONY-ZYebAZ93.js +41 -0
- package/packages/client/dist/assets/ebnfDiagram-CCIWWBDH-DCHpt3qn.js +1 -0
- package/packages/client/dist/assets/erDiagram-Q63AITRT-B6T87-48.js +85 -0
- package/packages/client/dist/assets/flowDiagram-23GEKE2U-DAcW6mKJ.js +156 -0
- package/packages/client/dist/assets/ganttDiagram-NO4QXBWP-Ct_eFFcM.js +292 -0
- package/packages/client/dist/assets/gitGraphDiagram-IHSO6WYX-BGi8Fdm2.js +106 -0
- package/packages/client/dist/assets/graph-C9eacEi8.js +1 -0
- package/packages/client/dist/assets/index-CtMiUbMJ.js +1671 -0
- package/packages/client/dist/assets/index-hTu6ZG2N.js +2 -0
- package/packages/client/dist/assets/index-oBa2uNlv.css +32 -0
- package/packages/client/dist/assets/infoDiagram-FWYZ7A6U-CqvwbXJn.js +2 -0
- package/packages/client/dist/assets/init-Gi6I4Gst.js +1 -0
- package/packages/client/dist/assets/ishikawaDiagram-FXEZZL3T-CHhr5ioQ.js +70 -0
- package/packages/client/dist/assets/journeyDiagram-5HDEW3XC-d20NzZAd.js +139 -0
- package/packages/client/dist/assets/kanban-definition-HUTT4EX6-CHx3wOer.js +89 -0
- package/packages/client/dist/assets/katex-C5jXJg4s.js +257 -0
- package/packages/client/dist/assets/layout-DEXfKzaS.js +1 -0
- package/packages/client/dist/assets/linear-Cnp1E9M4.js +1 -0
- package/packages/client/dist/assets/map-Czzmt4hB.js +1 -0
- package/packages/client/dist/assets/mermaid.core-jAhLhPYr.js +314 -0
- package/packages/client/dist/assets/mindmap-definition-LN4V7U3C-zi30RPoJ.js +96 -0
- package/packages/client/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/packages/client/dist/assets/pdf.worker.min-qwK7q_zL.mjs +28 -0
- package/packages/client/dist/assets/pegDiagram-2B236MQR-4gKLzTqu.js +1 -0
- package/packages/client/dist/assets/pieDiagram-ENE6RG2P-DRDhpjxP.js +39 -0
- package/packages/client/dist/assets/quadrantDiagram-ABIIQ3AL-FmTxRIlW.js +7 -0
- package/packages/client/dist/assets/railroadDiagram-RFXS5EU6-Bc-Q7iPs.js +1 -0
- package/packages/client/dist/assets/requirementDiagram-TGXJPOKE-D209tlqv.js +84 -0
- package/packages/client/dist/assets/sankeyDiagram-HTMAVEWB--I_lSbCV.js +40 -0
- package/packages/client/dist/assets/sequenceDiagram-DBY2YBRQ-Da5YJcDJ.js +162 -0
- package/packages/client/dist/assets/sizeCapture-X5ZJPWSS-DYuc-rzB.js +1 -0
- package/packages/client/dist/assets/{snippetTokenHighlight-ot6u3hW4.js → snippetTokenHighlight-qXHxvlCE.js} +1 -1
- package/packages/client/dist/assets/stateDiagram-2N3HPSRC-DoOeZoRP.js +1 -0
- package/packages/client/dist/assets/stateDiagram-v2-6OUMAXLB-DLkUyWs6.js +1 -0
- package/packages/client/dist/assets/swimlanes-5IMT3BWC-BcR7uCTf.js +2 -0
- package/packages/client/dist/assets/swimlanesDiagram-G3AALYLV-C0FDo-hf.js +8 -0
- package/packages/client/dist/assets/timeline-definition-FHXFAJF6-CLzKR6_i.js +120 -0
- package/packages/client/dist/assets/vennDiagram-L72KCM5P-TajEacPR.js +34 -0
- package/packages/client/dist/assets/wardleyDiagram-EHGQE667-DKvVEb3V.js +78 -0
- package/packages/client/dist/assets/xychartDiagram-FW5EYKEG-CaJG8r0j.js +7 -0
- package/packages/client/dist/index.html +2 -2
- package/packages/client/dist/sw.js +2 -2
- package/packages/server/dist/controllers/accountController.d.ts.map +1 -1
- package/packages/server/dist/controllers/accountController.js +31 -11
- package/packages/server/dist/controllers/accountController.js.map +1 -1
- package/packages/server/dist/controllers/fileSystemController.d.ts +6 -0
- package/packages/server/dist/controllers/fileSystemController.d.ts.map +1 -1
- package/packages/server/dist/controllers/fileSystemController.js +34 -0
- package/packages/server/dist/controllers/fileSystemController.js.map +1 -1
- package/packages/server/dist/controllers/projectController.d.ts.map +1 -1
- package/packages/server/dist/controllers/projectController.js +4 -2
- package/packages/server/dist/controllers/projectController.js.map +1 -1
- package/packages/server/dist/controllers/sessionController.d.ts +5 -0
- package/packages/server/dist/controllers/sessionController.d.ts.map +1 -1
- package/packages/server/dist/controllers/sessionController.js +95 -3
- package/packages/server/dist/controllers/sessionController.js.map +1 -1
- package/packages/server/dist/handlers/hookBridge.d.ts +131 -0
- package/packages/server/dist/handlers/hookBridge.d.ts.map +1 -0
- package/packages/server/dist/handlers/hookBridge.js +178 -0
- package/packages/server/dist/handlers/hookBridge.js.map +1 -0
- package/packages/server/dist/handlers/streamCallbacks.d.ts.map +1 -1
- package/packages/server/dist/handlers/streamCallbacks.js +30 -4
- package/packages/server/dist/handlers/streamCallbacks.js.map +1 -1
- package/packages/server/dist/handlers/websocket.d.ts +44 -2
- package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
- package/packages/server/dist/handlers/websocket.js +666 -64
- package/packages/server/dist/handlers/websocket.js.map +1 -1
- package/packages/server/dist/index.js +4 -1
- package/packages/server/dist/index.js.map +1 -1
- package/packages/server/dist/locales/en/server.json +9 -3
- package/packages/server/dist/locales/es/server.json +9 -3
- package/packages/server/dist/locales/ja/server.json +9 -3
- package/packages/server/dist/locales/ko/server.json +9 -3
- package/packages/server/dist/locales/pt/server.json +9 -3
- package/packages/server/dist/locales/zh-CN/server.json +9 -3
- package/packages/server/dist/routes/account.d.ts.map +1 -1
- package/packages/server/dist/routes/account.js +4 -2
- package/packages/server/dist/routes/account.js.map +1 -1
- package/packages/server/dist/routes/fileSystem.d.ts.map +1 -1
- package/packages/server/dist/routes/fileSystem.js +2 -0
- package/packages/server/dist/routes/fileSystem.js.map +1 -1
- package/packages/server/dist/routes/sessions.d.ts.map +1 -1
- package/packages/server/dist/routes/sessions.js +2 -0
- package/packages/server/dist/routes/sessions.js.map +1 -1
- package/packages/server/dist/services/accountInfoService.d.ts +36 -1
- package/packages/server/dist/services/accountInfoService.d.ts.map +1 -1
- package/packages/server/dist/services/accountInfoService.js +90 -4
- package/packages/server/dist/services/accountInfoService.js.map +1 -1
- package/packages/server/dist/services/accountStorageService.d.ts +55 -9
- package/packages/server/dist/services/accountStorageService.d.ts.map +1 -1
- package/packages/server/dist/services/accountStorageService.js +244 -12
- package/packages/server/dist/services/accountStorageService.js.map +1 -1
- package/packages/server/dist/services/chatEngine.d.ts +59 -3
- package/packages/server/dist/services/chatEngine.d.ts.map +1 -1
- package/packages/server/dist/services/chatService.d.ts +112 -3
- package/packages/server/dist/services/chatService.d.ts.map +1 -1
- package/packages/server/dist/services/chatService.js +353 -98
- package/packages/server/dist/services/chatService.js.map +1 -1
- package/packages/server/dist/services/claudeLoginService.d.ts +14 -3
- package/packages/server/dist/services/claudeLoginService.d.ts.map +1 -1
- package/packages/server/dist/services/claudeLoginService.js +18 -4
- package/packages/server/dist/services/claudeLoginService.js.map +1 -1
- package/packages/server/dist/services/cliChatEngine.d.ts.map +1 -1
- package/packages/server/dist/services/cliChatEngine.js +124 -22
- package/packages/server/dist/services/cliChatEngine.js.map +1 -1
- package/packages/server/dist/services/cliModalDetect.d.ts +14 -0
- package/packages/server/dist/services/cliModalDetect.d.ts.map +1 -1
- package/packages/server/dist/services/cliModalDetect.js +12 -3
- package/packages/server/dist/services/cliModalDetect.js.map +1 -1
- package/packages/server/dist/services/cliSessionPool.js +1 -1
- package/packages/server/dist/services/cliSessionPool.js.map +1 -1
- package/packages/server/dist/services/commandService.d.ts.map +1 -1
- package/packages/server/dist/services/commandService.js +19 -9
- package/packages/server/dist/services/commandService.js.map +1 -1
- package/packages/server/dist/services/engineResidencyRegistry.d.ts +112 -0
- package/packages/server/dist/services/engineResidencyRegistry.d.ts.map +1 -0
- package/packages/server/dist/services/engineResidencyRegistry.js +195 -0
- package/packages/server/dist/services/engineResidencyRegistry.js.map +1 -0
- package/packages/server/dist/services/harnessAgentService.d.ts.map +1 -1
- package/packages/server/dist/services/harnessAgentService.js +12 -2
- package/packages/server/dist/services/harnessAgentService.js.map +1 -1
- package/packages/server/dist/services/harnessLintService.d.ts.map +1 -1
- package/packages/server/dist/services/harnessLintService.js +25 -114
- package/packages/server/dist/services/harnessLintService.js.map +1 -1
- package/packages/server/dist/services/historyParser.d.ts.map +1 -1
- package/packages/server/dist/services/historyParser.js +23 -6
- package/packages/server/dist/services/historyParser.js.map +1 -1
- package/packages/server/dist/services/imageStorageService.d.ts.map +1 -1
- package/packages/server/dist/services/imageStorageService.js +7 -0
- package/packages/server/dist/services/imageStorageService.js.map +1 -1
- package/packages/server/dist/services/projectService.d.ts +4 -1
- package/packages/server/dist/services/projectService.d.ts.map +1 -1
- package/packages/server/dist/services/projectService.js +14 -1
- package/packages/server/dist/services/projectService.js.map +1 -1
- package/packages/server/dist/services/promptQueue.d.ts +34 -0
- package/packages/server/dist/services/promptQueue.d.ts.map +1 -0
- package/packages/server/dist/services/promptQueue.js +80 -0
- package/packages/server/dist/services/promptQueue.js.map +1 -0
- package/packages/server/dist/services/ptyService.d.ts.map +1 -1
- package/packages/server/dist/services/ptyService.js +4 -1
- package/packages/server/dist/services/ptyService.js.map +1 -1
- package/packages/server/dist/services/queueService.d.ts.map +1 -1
- package/packages/server/dist/services/queueService.js +62 -4
- package/packages/server/dist/services/queueService.js.map +1 -1
- package/packages/server/dist/services/rateLimitProbeService.d.ts +73 -3
- package/packages/server/dist/services/rateLimitProbeService.d.ts.map +1 -1
- package/packages/server/dist/services/rateLimitProbeService.js +397 -30
- package/packages/server/dist/services/rateLimitProbeService.js.map +1 -1
- package/packages/server/dist/services/sessionBufferManager.d.ts.map +1 -1
- package/packages/server/dist/services/sessionBufferManager.js +22 -4
- package/packages/server/dist/services/sessionBufferManager.js.map +1 -1
- package/packages/server/dist/services/sessionExportService.d.ts +34 -0
- package/packages/server/dist/services/sessionExportService.d.ts.map +1 -0
- package/packages/server/dist/services/sessionExportService.js +269 -0
- package/packages/server/dist/services/sessionExportService.js.map +1 -0
- package/packages/server/dist/services/sessionService.d.ts +6 -0
- package/packages/server/dist/services/sessionService.d.ts.map +1 -1
- package/packages/server/dist/services/sessionService.js +30 -9
- package/packages/server/dist/services/sessionService.js.map +1 -1
- package/packages/server/dist/services/streamHandler.d.ts +177 -1
- package/packages/server/dist/services/streamHandler.d.ts.map +1 -1
- package/packages/server/dist/services/streamHandler.js +425 -39
- package/packages/server/dist/services/streamHandler.js.map +1 -1
- package/packages/server/dist/services/workspaceContext.d.ts.map +1 -1
- package/packages/server/dist/services/workspaceContext.js +6 -0
- package/packages/server/dist/services/workspaceContext.js.map +1 -1
- package/packages/server/dist/snippets/fix-lint-command-not-on-path +13 -0
- package/packages/server/dist/snippets/fix-lint-duplicate-name +12 -0
- package/packages/server/dist/snippets/fix-lint-env-var-undefined +13 -0
- package/packages/server/dist/snippets/fix-lint-regex-invalid +14 -0
- package/packages/server/dist/snippets/fix-lint-tools-non-standard +14 -0
- package/packages/server/dist/snippets/fix-lint-url-invalid +13 -0
- package/packages/server/dist/snippets/fix-lint-yaml-parse-error +14 -0
- package/packages/server/dist/snippets/fix-malformed-skill +17 -0
- package/packages/server/dist/utils/backgroundHookSettings.d.ts +31 -0
- package/packages/server/dist/utils/backgroundHookSettings.d.ts.map +1 -0
- package/packages/server/dist/utils/backgroundHookSettings.js +41 -0
- package/packages/server/dist/utils/backgroundHookSettings.js.map +1 -0
- package/packages/server/dist/utils/backgroundTaskPoller.d.ts +69 -0
- package/packages/server/dist/utils/backgroundTaskPoller.d.ts.map +1 -0
- package/packages/server/dist/utils/backgroundTaskPoller.js +356 -0
- package/packages/server/dist/utils/backgroundTaskPoller.js.map +1 -0
- package/packages/server/dist/utils/backgroundTaskTracker.d.ts +51 -3
- package/packages/server/dist/utils/backgroundTaskTracker.d.ts.map +1 -1
- package/packages/server/dist/utils/backgroundTaskTracker.js +117 -9
- package/packages/server/dist/utils/backgroundTaskTracker.js.map +1 -1
- package/packages/server/dist/utils/backgroundToolGuard.d.ts +26 -0
- package/packages/server/dist/utils/backgroundToolGuard.d.ts.map +1 -0
- package/packages/server/dist/utils/backgroundToolGuard.js +40 -0
- package/packages/server/dist/utils/backgroundToolGuard.js.map +1 -0
- package/packages/server/dist/utils/boundedRing.d.ts +44 -0
- package/packages/server/dist/utils/boundedRing.d.ts.map +1 -0
- package/packages/server/dist/utils/boundedRing.js +77 -0
- package/packages/server/dist/utils/boundedRing.js.map +1 -0
- package/packages/server/dist/utils/effortUtils.d.ts +7 -6
- package/packages/server/dist/utils/effortUtils.d.ts.map +1 -1
- package/packages/server/dist/utils/effortUtils.js +10 -9
- package/packages/server/dist/utils/effortUtils.js.map +1 -1
- package/packages/server/dist/utils/messageTree.d.ts +25 -0
- package/packages/server/dist/utils/messageTree.d.ts.map +1 -1
- package/packages/server/dist/utils/messageTree.js +167 -3
- package/packages/server/dist/utils/messageTree.js.map +1 -1
- package/packages/server/dist/utils/pathUtils.d.ts.map +1 -1
- package/packages/server/dist/utils/pathUtils.js +1 -0
- package/packages/server/dist/utils/pathUtils.js.map +1 -1
- package/packages/server/dist/utils/residencyActivation.d.ts +61 -0
- package/packages/server/dist/utils/residencyActivation.d.ts.map +1 -0
- package/packages/server/dist/utils/residencyActivation.js +60 -0
- package/packages/server/dist/utils/residencyActivation.js.map +1 -0
- package/packages/server/dist/utils/residencyPolicy.d.ts +62 -0
- package/packages/server/dist/utils/residencyPolicy.d.ts.map +1 -0
- package/packages/server/dist/utils/residencyPolicy.js +82 -0
- package/packages/server/dist/utils/residencyPolicy.js.map +1 -0
- package/packages/server/dist/utils/runtimeParamStrategy.d.ts +51 -0
- package/packages/server/dist/utils/runtimeParamStrategy.d.ts.map +1 -0
- package/packages/server/dist/utils/runtimeParamStrategy.js +60 -0
- package/packages/server/dist/utils/runtimeParamStrategy.js.map +1 -0
- package/packages/server/dist/utils/stopStrategy.d.ts +62 -0
- package/packages/server/dist/utils/stopStrategy.d.ts.map +1 -0
- package/packages/server/dist/utils/stopStrategy.js +65 -0
- package/packages/server/dist/utils/stopStrategy.js.map +1 -0
- package/packages/server/resources/internals/INDEX.md +1 -1
- package/packages/server/resources/internals/image-storage.md +14 -2
- package/packages/server/resources/manual/02-chat.md +30 -13
- package/packages/server/resources/manual/03-sessions.md +22 -1
- package/packages/server/resources/manual/04-slash-commands-favorites.md +2 -1
- package/packages/server/resources/manual/05-projects.md +1 -0
- package/packages/server/resources/manual/06-file-explorer-editor.md +21 -2
- package/packages/server/resources/manual/07-git.md +1 -1
- package/packages/server/resources/manual/08-terminal.md +1 -1
- package/packages/server/resources/manual/12-harness-workbench.md +21 -16
- package/packages/server/resources/manual/13-settings.md +22 -6
- package/packages/server/resources/manual/14-keyboard-shortcuts.md +8 -1
- package/packages/shared/dist/constants/messageTree.d.ts +2 -0
- package/packages/shared/dist/constants/messageTree.d.ts.map +1 -1
- package/packages/shared/dist/constants/messageTree.js +6 -0
- package/packages/shared/dist/constants/messageTree.js.map +1 -1
- package/packages/shared/dist/index.d.ts +1 -1
- package/packages/shared/dist/index.d.ts.map +1 -1
- package/packages/shared/dist/index.js.map +1 -1
- package/packages/shared/dist/types/accounts.d.ts +13 -0
- package/packages/shared/dist/types/accounts.d.ts.map +1 -1
- package/packages/shared/dist/types/history.d.ts +14 -1
- package/packages/shared/dist/types/history.d.ts.map +1 -1
- package/packages/shared/dist/types/message.d.ts +26 -3
- package/packages/shared/dist/types/message.d.ts.map +1 -1
- package/packages/shared/dist/types/message.js +21 -1
- package/packages/shared/dist/types/message.js.map +1 -1
- package/packages/shared/dist/types/preferences.d.ts +5 -1
- package/packages/shared/dist/types/preferences.d.ts.map +1 -1
- package/packages/shared/dist/types/preferences.js +3 -0
- package/packages/shared/dist/types/preferences.js.map +1 -1
- package/packages/shared/dist/types/sdk.d.ts +68 -10
- package/packages/shared/dist/types/sdk.d.ts.map +1 -1
- package/packages/shared/dist/types/sdk.js +56 -16
- package/packages/shared/dist/types/sdk.js.map +1 -1
- package/packages/shared/dist/types/session.d.ts +6 -0
- package/packages/shared/dist/types/session.d.ts.map +1 -1
- package/packages/shared/dist/types/session.js +6 -0
- package/packages/shared/dist/types/session.js.map +1 -1
- package/packages/shared/dist/types/streaming.d.ts +32 -2
- package/packages/shared/dist/types/streaming.d.ts.map +1 -1
- package/packages/shared/dist/types/streaming.js.map +1 -1
- package/packages/shared/dist/types/websocket.d.ts +4 -0
- package/packages/shared/dist/types/websocket.d.ts.map +1 -1
- package/scripts/probe-persistent-hooks.mjs +795 -0
- package/packages/client/dist/assets/index-BHbx57gc.js +0 -2
- package/packages/client/dist/assets/index-BX7y_Qt3.css +0 -32
- package/packages/client/dist/assets/index-DWIOf7rA.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]
|
|
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
|
-
|
|
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
|
|
83
|
-
if (
|
|
84
|
-
result.old_string = remaining.slice(0,
|
|
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
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|