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
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
// Story 38.1 — live probe (feedback_sdk_verify): does the PERSISTENT stream
|
|
2
|
+
// (AsyncIterable prompt kept open) actually deliver first-class hooks in-process,
|
|
3
|
+
// and does Stop's background_tasks[] track a SUBAGENT-type child (Task 4 GO/NO-GO)?
|
|
4
|
+
//
|
|
5
|
+
// Story 38.2 — `order` mode adds the AC7 GO/NO-GO gate: at the moment the stream
|
|
6
|
+
// consumer dequeues the turn-ending `result`, has THIS turn's main Stop hook
|
|
7
|
+
// (fromSubagent === false) already fired into the ledger? If yes for both a
|
|
8
|
+
// simple turn AND a background turn → GO (processStream reads the snapshot
|
|
9
|
+
// directly at result). If Stop lands after result → NO-GO (bounded micro-grace).
|
|
10
|
+
//
|
|
11
|
+
// Story 38.4 — `runtime` mode: on the persistent stream, does each runtime
|
|
12
|
+
// parameter setter actually take effect on SUBSEQUENT responses?
|
|
13
|
+
// (a) setModel(<other>) — do later assistant messages report the new model
|
|
14
|
+
// id (message.model / result.modelUsage keys)? GO.
|
|
15
|
+
// (b) setMaxThinkingTokens — is a thinking budget change reflected (thinking
|
|
16
|
+
// blocks appear/disappear with the display param)?
|
|
17
|
+
// (c) setPermissionMode — does the 37.5 runtime apply still hold in the
|
|
18
|
+
// persistent stream (no error, follow-up result)?
|
|
19
|
+
// (d) setEffort — confirm the Query interface has NO such setter
|
|
20
|
+
// (effort is a query() creation option only → NO-GO).
|
|
21
|
+
//
|
|
22
|
+
// Story 38.5 — `residency` mode: the CORE GO/NO-GO gate for turn-to-turn process
|
|
23
|
+
// residency. On ONE persistent stream, does a single query() process sustain
|
|
24
|
+
// MULTIPLE turns with NO close() and NO restart between them?
|
|
25
|
+
// - Turn 1 consumed to `result`, then HELD (no close).
|
|
26
|
+
// - Turn 2..N pushed via the SAME promptQueue onto the SAME iterator; each turn's
|
|
27
|
+
// result must land on the same process.
|
|
28
|
+
// - Inter-turn gap is timestamped: a fresh query() spawn costs ~3s of CLI/auth
|
|
29
|
+
// warmup, so residency shows up as a SECOND+ turn whose time-to-first-token /
|
|
30
|
+
// total is well under a cold spawn (no restart). GO ⇒ Story 38.5 adopts the
|
|
31
|
+
// residency registry (turn-end close replaced by an idle-timeout policy).
|
|
32
|
+
// - Also confirms resume/forkSession have NO live-query runtime setter (they are
|
|
33
|
+
// query() creation options only) → resume/fork require a NEW query, wired into
|
|
34
|
+
// the residency registry rather than mutating a live query.
|
|
35
|
+
//
|
|
36
|
+
// Story 38.3 — TWO gates on the persistent control channel:
|
|
37
|
+
// `interact` — does the permission/AskUserQuestion gate (canUseTool) still fire
|
|
38
|
+
// in the POST-RESULT follow-up generation, and does the injected
|
|
39
|
+
// answer round-trip over the still-open control channel back to the
|
|
40
|
+
// SDK (no "Stream closed" — the c4ec9acf failure)? GO if a
|
|
41
|
+
// post-result AskUserQuestion is answered and a follow-up result
|
|
42
|
+
// lands.
|
|
43
|
+
// `bashkill` — a background Bash (`sleep 20`) launched then the turn ended: does
|
|
44
|
+
// it survive past the ~5s mark (completion notification lands near
|
|
45
|
+
// the full sleep) or is it killed at ~5s? GO = survives (no kill).
|
|
46
|
+
//
|
|
47
|
+
// Usage:
|
|
48
|
+
// node scripts/probe-persistent-hooks.mjs smoke # auth + hook-firing check (fast)
|
|
49
|
+
// node scripts/probe-persistent-hooks.mjs subagent # GO/NO-GO: subagent tracking (~90s)
|
|
50
|
+
// node scripts/probe-persistent-hooks.mjs order # 38.2 AC7: Stop-vs-result ordering (~90s)
|
|
51
|
+
// node scripts/probe-persistent-hooks.mjs interact # 38.3 AC2/AC6: post-result canUseTool round-trip (~40s)
|
|
52
|
+
// node scripts/probe-persistent-hooks.mjs bashkill # 38.3 AC3/AC6: background Bash ~5s kill survival (~40s)
|
|
53
|
+
// node scripts/probe-persistent-hooks.mjs interrupt # 38.3 AC1: interrupt() ends the turn but keeps the session (~40s)
|
|
54
|
+
// node scripts/probe-persistent-hooks.mjs runtime # 38.4 AC1: setModel/setMaxThinkingTokens/setPermissionMode reflection + setEffort absence (~120s)
|
|
55
|
+
// node scripts/probe-persistent-hooks.mjs residency # 38.5 AC1: N-turn process residency (no close, no restart between turns) + resume/fork runtime-setter absence (~120s)
|
|
56
|
+
// node scripts/probe-persistent-hooks.mjs longhold # 38.6 AC1: vitest-grade (>1min) long child — hold→terminate + reuse turn (~120s)
|
|
57
|
+
//
|
|
58
|
+
// Runs against the real bundled SDK + owner subscription (standing-approved).
|
|
59
|
+
|
|
60
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
61
|
+
|
|
62
|
+
const ARG = process.argv[2];
|
|
63
|
+
const KNOWN_MODES = ['smoke', 'subagent', 'order', 'hold', 'interact', 'bashkill', 'interrupt', 'runtime', 'residency', 'longhold'];
|
|
64
|
+
const MODE = KNOWN_MODES.includes(ARG) ? ARG : 'smoke';
|
|
65
|
+
|
|
66
|
+
// --- Persistent prompt queue (mirrors packages/server/src/services/promptQueue.ts) ---
|
|
67
|
+
class PromptQueue {
|
|
68
|
+
#queued = [];
|
|
69
|
+
#waiter = null;
|
|
70
|
+
#closed = false;
|
|
71
|
+
push(m) {
|
|
72
|
+
if (this.#closed) return;
|
|
73
|
+
if (this.#waiter) { const r = this.#waiter; this.#waiter = null; r({ value: m, done: false }); }
|
|
74
|
+
else this.#queued.push(m);
|
|
75
|
+
}
|
|
76
|
+
close() {
|
|
77
|
+
if (this.#closed) return;
|
|
78
|
+
this.#closed = true;
|
|
79
|
+
if (this.#waiter) { const r = this.#waiter; this.#waiter = null; r({ value: undefined, done: true }); }
|
|
80
|
+
}
|
|
81
|
+
[Symbol.asyncIterator]() {
|
|
82
|
+
return {
|
|
83
|
+
next: () => {
|
|
84
|
+
if (this.#queued.length) return Promise.resolve({ value: this.#queued.shift(), done: false });
|
|
85
|
+
if (this.#closed) return Promise.resolve({ value: undefined, done: true });
|
|
86
|
+
return new Promise((resolve) => { this.#waiter = resolve; });
|
|
87
|
+
},
|
|
88
|
+
return: () => { this.close(); return Promise.resolve({ value: undefined, done: true }); },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const userMsg = (text) => ({ type: 'user', session_id: '', message: { role: 'user', content: text }, parent_tool_use_id: null });
|
|
94
|
+
|
|
95
|
+
// --- Hook capture ---
|
|
96
|
+
const stopSnapshots = []; // { at, tasks: [{id,type,status,agent_type}] }
|
|
97
|
+
const subagentStops = []; // { at, agent_type, tasks }
|
|
98
|
+
const taskEvents = []; // ['created'|'completed'|'subagent-start', ...]
|
|
99
|
+
|
|
100
|
+
const pass = async () => ({});
|
|
101
|
+
const mk = (fn) => ({ hooks: [async (input) => { try { fn(input); } catch (e) { console.error('hook err', e); } return pass(); }] });
|
|
102
|
+
const hooks = {
|
|
103
|
+
TaskCreated: [mk((i) => taskEvents.push(['created', i.task_id, i.task_subject]))],
|
|
104
|
+
TaskCompleted: [mk((i) => taskEvents.push(['completed', i.task_id, i.task_subject]))],
|
|
105
|
+
SubagentStart: [mk((i) => taskEvents.push(['subagent-start', i.agent_id, i.agent_type]))],
|
|
106
|
+
Stop: [mk((i) => {
|
|
107
|
+
const tasks = (i.background_tasks ?? []).map((t) => ({ id: t.id, type: t.type, status: t.status, agent_type: t.agent_type }));
|
|
108
|
+
stopSnapshots.push({ at: Date.now(), tasks });
|
|
109
|
+
console.log(`[Stop] background_tasks=${tasks.length} ${JSON.stringify(tasks)}`);
|
|
110
|
+
})],
|
|
111
|
+
SubagentStop: [mk((i) => {
|
|
112
|
+
const tasks = (i.background_tasks ?? []).map((t) => ({ id: t.id, type: t.type, status: t.status, agent_type: t.agent_type }));
|
|
113
|
+
subagentStops.push({ at: Date.now(), agent_type: i.agent_type, tasks });
|
|
114
|
+
console.log(`[SubagentStop] agent_type=${i.agent_type} background_tasks=${tasks.length} ${JSON.stringify(tasks)}`);
|
|
115
|
+
})],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// --- canUseTool (interact mode, Story 38.3) -----------------------------------
|
|
119
|
+
// Records whether the gate is invoked AFTER the first turn-ending result (i.e. in
|
|
120
|
+
// the post-result follow-up generation) and, for AskUserQuestion, injects an answer
|
|
121
|
+
// whose return value must round-trip over the still-open control channel.
|
|
122
|
+
const PROBE_T0 = Date.now();
|
|
123
|
+
let firstResultAt = null; // set when the first result is dequeued (interact)
|
|
124
|
+
const canUseToolEvents = []; // { at, tool, postResult }
|
|
125
|
+
const canUseTool = async (toolName, input) => {
|
|
126
|
+
const postResult = firstResultAt !== null;
|
|
127
|
+
canUseToolEvents.push({ at: Date.now(), tool: toolName, postResult });
|
|
128
|
+
console.log(`[canUseTool] tool=${toolName} @t+${Date.now() - PROBE_T0}ms postResult=${postResult}`);
|
|
129
|
+
if (toolName === 'AskUserQuestion') {
|
|
130
|
+
const questions = Array.isArray(input?.questions) ? input.questions : [];
|
|
131
|
+
const answers = {};
|
|
132
|
+
for (const question of questions) {
|
|
133
|
+
const opts = Array.isArray(question?.options) ? question.options : [];
|
|
134
|
+
const firstOpt = opts.length ? (opts[0]?.label ?? String(opts[0])) : 'yes';
|
|
135
|
+
answers[question?.question ?? 'q'] = firstOpt;
|
|
136
|
+
}
|
|
137
|
+
await new Promise((r) => setTimeout(r, 300)); // simulate human think-time
|
|
138
|
+
console.log(`[canUseTool] injecting AskUserQuestion answer (postResult=${postResult}) → return over control channel`);
|
|
139
|
+
return { behavior: 'allow', updatedInput: { questions, answers } };
|
|
140
|
+
}
|
|
141
|
+
return { behavior: 'allow', updatedInput: input };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// interact needs canUseTool exercised, so run it under the production-faithful
|
|
145
|
+
// `default` permission mode (bypass+skip may short-circuit the gate). All other
|
|
146
|
+
// modes keep the fast bypass path.
|
|
147
|
+
const NEEDS_CANUSE = MODE === 'interact';
|
|
148
|
+
|
|
149
|
+
const pq = new PromptQueue();
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
const q = query({
|
|
152
|
+
prompt: pq,
|
|
153
|
+
options: {
|
|
154
|
+
cwd: process.cwd(),
|
|
155
|
+
permissionMode: NEEDS_CANUSE ? 'default' : 'bypassPermissions',
|
|
156
|
+
allowDangerouslySkipPermissions: !NEEDS_CANUSE,
|
|
157
|
+
includePartialMessages: false,
|
|
158
|
+
settingSources: ['user', 'project', 'local'],
|
|
159
|
+
hooks,
|
|
160
|
+
...(NEEDS_CANUSE ? { canUseTool } : {}),
|
|
161
|
+
abortController: controller,
|
|
162
|
+
stderr: (d) => process.stderr.write(`[cli] ${d}`),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ========================= interrupt mode (38.3 AC1) ==========================
|
|
167
|
+
// The Stop-button primitive: does q.interrupt() (a) END the current turn's
|
|
168
|
+
// generation (a `result` arrives shortly after), and (b) KEEP the session alive so
|
|
169
|
+
// a SUBSEQUENT prompt on the SAME persistent stream produces a fresh result? GO for
|
|
170
|
+
// both ⇒ mapping the Stop button to interrupt() (turn-only) is safe; the generator
|
|
171
|
+
// is NOT torn down (that is close()/abort).
|
|
172
|
+
if (MODE === 'interrupt') {
|
|
173
|
+
const iterator = q[Symbol.asyncIterator]();
|
|
174
|
+
let firstTurnEndedAfterInterrupt = false;
|
|
175
|
+
let secondTurnResult = false;
|
|
176
|
+
let interruptAt = null;
|
|
177
|
+
let firstResultAfterInterruptAt = null;
|
|
178
|
+
const t0 = Date.now();
|
|
179
|
+
|
|
180
|
+
// Turn 1: a long, slow generation we can interrupt mid-flight.
|
|
181
|
+
pq.push(userMsg(
|
|
182
|
+
'Count slowly from 1 to 60. Output ONE number per line, and think briefly before each so it takes a while. Do not stop early.'));
|
|
183
|
+
|
|
184
|
+
(async () => {
|
|
185
|
+
try {
|
|
186
|
+
// Fire the interrupt ~4s into the first turn's streaming.
|
|
187
|
+
const interruptTimer = setTimeout(async () => {
|
|
188
|
+
interruptAt = Date.now() - t0;
|
|
189
|
+
console.log(`[interrupt] calling q.interrupt() @t+${interruptAt}ms`);
|
|
190
|
+
try { await q.interrupt(); } catch (e) { console.error('[interrupt] interrupt() threw', e?.message || e); }
|
|
191
|
+
}, 4000);
|
|
192
|
+
|
|
193
|
+
let pushedSecond = false;
|
|
194
|
+
for (;;) {
|
|
195
|
+
const step = await iterator.next();
|
|
196
|
+
if (step.done) { console.log('[interrupt] stream ended (generator done)'); break; }
|
|
197
|
+
const m = step.value;
|
|
198
|
+
if (m.type !== 'result') continue;
|
|
199
|
+
const at = Date.now() - t0;
|
|
200
|
+
if (interruptAt !== null && !firstTurnEndedAfterInterrupt) {
|
|
201
|
+
firstTurnEndedAfterInterrupt = true;
|
|
202
|
+
firstResultAfterInterruptAt = at;
|
|
203
|
+
console.log(`[interrupt] turn-1 result arrived @t+${at}ms (${at - interruptAt}ms after interrupt) subtype=${m.subtype}`);
|
|
204
|
+
// Session-survival check: push a SECOND prompt on the SAME stream.
|
|
205
|
+
if (!pushedSecond) {
|
|
206
|
+
pushedSecond = true;
|
|
207
|
+
clearTimeout(interruptTimer);
|
|
208
|
+
console.log('[interrupt] pushing turn-2 prompt on the SAME persistent stream…');
|
|
209
|
+
pq.push(userMsg('Reply with the single word: RESUMED'));
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (firstTurnEndedAfterInterrupt) {
|
|
214
|
+
secondTurnResult = true;
|
|
215
|
+
console.log(`[interrupt] turn-2 result arrived @t+${at}ms subtype=${m.subtype} — session survived the interrupt`);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (e) { console.error('[interrupt error]', e?.message || e); }
|
|
220
|
+
|
|
221
|
+
console.log(`\n=== verdict (mode=interrupt, AC1 gate) ===`);
|
|
222
|
+
console.log(`interrupt fired: ${interruptAt !== null ? `@t+${interruptAt}ms` : 'NO'}`);
|
|
223
|
+
console.log(`turn-1 ended after interrupt: ${firstTurnEndedAfterInterrupt}${firstResultAfterInterruptAt !== null ? ` (@t+${firstResultAfterInterruptAt}ms)` : ''}`);
|
|
224
|
+
console.log(`turn-2 (same stream) produced a result: ${secondTurnResult}`);
|
|
225
|
+
const GO = firstTurnEndedAfterInterrupt && secondTurnResult;
|
|
226
|
+
console.log(`GO/NO-GO: ${GO
|
|
227
|
+
? 'GO — interrupt() ended the current turn AND the session survived for a follow-up turn (turn-only stop, generator preserved)'
|
|
228
|
+
: firstTurnEndedAfterInterrupt
|
|
229
|
+
? 'PARTIAL — interrupt ended the turn but the follow-up turn did not complete (session-survival unproven)'
|
|
230
|
+
: 'NO-GO — interrupt did not end the turn (Stop would hang)'}`);
|
|
231
|
+
pq.close();
|
|
232
|
+
controller.abort();
|
|
233
|
+
setTimeout(() => process.exit(0), 500);
|
|
234
|
+
})();
|
|
235
|
+
|
|
236
|
+
setTimeout(() => { if (!secondTurnResult) { console.log('[interrupt] TIMEOUT — did not observe both turns within 45s'); pq.close(); controller.abort(); setTimeout(() => process.exit(0), 500); } }, 45_000);
|
|
237
|
+
}
|
|
238
|
+
// =========================== runtime mode (38.4 AC1) ==========================
|
|
239
|
+
// Drives sequential turns on ONE persistent stream, calling each runtime setter
|
|
240
|
+
// BETWEEN turns, and checks the NEXT turn's response for the effect. The authority
|
|
241
|
+
// for "which model answered" is the response meta (assistant message.model /
|
|
242
|
+
// result.modelUsage keys) — NOT supportedModels() (a static catalog).
|
|
243
|
+
else if (MODE === 'runtime') {
|
|
244
|
+
const iterator = q[Symbol.asyncIterator]();
|
|
245
|
+
const t0 = Date.now();
|
|
246
|
+
|
|
247
|
+
// Run one turn to its result, collecting the model ids that answered and
|
|
248
|
+
// whether any thinking content block was emitted.
|
|
249
|
+
const runTurn = async (text) => {
|
|
250
|
+
const models = new Set();
|
|
251
|
+
let thinking = false;
|
|
252
|
+
pq.push(userMsg(text));
|
|
253
|
+
for (;;) {
|
|
254
|
+
const step = await iterator.next();
|
|
255
|
+
if (step.done) return { done: true, models: [], modelUsage: null, thinking };
|
|
256
|
+
const m = step.value;
|
|
257
|
+
if (m.type === 'assistant') {
|
|
258
|
+
if (m.message?.model) models.add(m.message.model);
|
|
259
|
+
const content = m.message?.content;
|
|
260
|
+
if (Array.isArray(content) && content.some((b) => b?.type === 'thinking' || b?.type === 'redacted_thinking')) thinking = true;
|
|
261
|
+
}
|
|
262
|
+
if (m.type === 'result') {
|
|
263
|
+
const usage = m.modelUsage && typeof m.modelUsage === 'object' ? Object.keys(m.modelUsage) : null;
|
|
264
|
+
return { done: false, subtype: m.subtype, models: [...models], modelUsage: usage, thinking };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
(async () => {
|
|
270
|
+
const presence = {
|
|
271
|
+
setModel: typeof q.setModel === 'function',
|
|
272
|
+
setMaxThinkingTokens: typeof q.setMaxThinkingTokens === 'function',
|
|
273
|
+
setPermissionMode: typeof q.setPermissionMode === 'function',
|
|
274
|
+
setEffort: typeof q.setEffort === 'function',
|
|
275
|
+
};
|
|
276
|
+
console.log(`[runtime] setter presence ${JSON.stringify(presence)}`);
|
|
277
|
+
|
|
278
|
+
// (a) setModel — baseline model, then switch to a clearly-different model.
|
|
279
|
+
const r1 = await runTurn('Reply with exactly one word: ONE');
|
|
280
|
+
const ids1 = [...new Set([...(r1.models || []), ...(r1.modelUsage || [])])];
|
|
281
|
+
console.log(`[runtime] turn1 (baseline) @t+${Date.now() - t0}ms modelIds=${JSON.stringify(ids1)}`);
|
|
282
|
+
|
|
283
|
+
const TARGET = 'haiku';
|
|
284
|
+
let setModelErr = null;
|
|
285
|
+
if (presence.setModel) {
|
|
286
|
+
try { console.log(`[runtime] q.setModel('${TARGET}') @t+${Date.now() - t0}ms`); await q.setModel(TARGET); }
|
|
287
|
+
catch (e) { setModelErr = e?.message || String(e); console.error('[runtime] setModel threw', setModelErr); }
|
|
288
|
+
}
|
|
289
|
+
const r2 = await runTurn('Reply with exactly one word: TWO');
|
|
290
|
+
const ids2 = [...new Set([...(r2.models || []), ...(r2.modelUsage || [])])];
|
|
291
|
+
console.log(`[runtime] turn2 (after setModel) @t+${Date.now() - t0}ms modelIds=${JSON.stringify(ids2)}`);
|
|
292
|
+
const modelChanged = ids2.some((id) => id.includes('haiku')) && !ids1.some((id) => id.includes('haiku'));
|
|
293
|
+
const modelGo = presence.setModel && !setModelErr && modelChanged;
|
|
294
|
+
|
|
295
|
+
// (b) setMaxThinkingTokens — reset model to default, budget OFF then ON (with
|
|
296
|
+
// an explicit summarized display so thinking blocks are observable when on).
|
|
297
|
+
if (presence.setModel) { try { await q.setModel(undefined); } catch (e) { console.error('[runtime] setModel(undefined) threw', e?.message || e); } }
|
|
298
|
+
let thinkErr = null;
|
|
299
|
+
if (presence.setMaxThinkingTokens) { try { await q.setMaxThinkingTokens(0); } catch (e) { thinkErr = e?.message || String(e); } }
|
|
300
|
+
const rOff = await runTurn('Think step by step, then give the answer: what is 17 * 23?');
|
|
301
|
+
console.log(`[runtime] thinking-off turn @t+${Date.now() - t0}ms thinkingBlocks=${rOff.thinking}`);
|
|
302
|
+
if (presence.setMaxThinkingTokens) { try { await q.setMaxThinkingTokens(8000, 'summarized'); } catch (e) { thinkErr = thinkErr || (e?.message || String(e)); } }
|
|
303
|
+
const rOn = await runTurn('Think step by step, then give the answer: what is 29 * 31?');
|
|
304
|
+
console.log(`[runtime] thinking-on turn @t+${Date.now() - t0}ms thinkingBlocks=${rOn.thinking}`);
|
|
305
|
+
const thinkingCallable = presence.setMaxThinkingTokens && !thinkErr;
|
|
306
|
+
const thinkingObservable = thinkingCallable && rOff.thinking === false && rOn.thinking === true;
|
|
307
|
+
|
|
308
|
+
// (c) setPermissionMode — runtime apply must still hold on the persistent
|
|
309
|
+
// stream (37.5 regression): no error, and a follow-up turn still produces a result.
|
|
310
|
+
let pmErr = null;
|
|
311
|
+
if (presence.setPermissionMode) {
|
|
312
|
+
try { await q.setPermissionMode('plan'); await q.setPermissionMode('default'); }
|
|
313
|
+
catch (e) { pmErr = e?.message || String(e); }
|
|
314
|
+
}
|
|
315
|
+
const rPm = await runTurn('Reply with exactly one word: THREE');
|
|
316
|
+
const pmGo = presence.setPermissionMode && !pmErr && !rPm.done;
|
|
317
|
+
console.log(`[runtime] setPermissionMode runtime @t+${Date.now() - t0}ms err=${pmErr} followUpResult=${!rPm.done}`);
|
|
318
|
+
|
|
319
|
+
// (d) setEffort — expected ABSENT on the Query interface.
|
|
320
|
+
const effortRuntimePresent = presence.setEffort;
|
|
321
|
+
|
|
322
|
+
console.log(`\n=== verdict (mode=runtime, 38.4 AC1 gate) ===`);
|
|
323
|
+
console.log(`(a) setModel: ${modelGo ? 'GO' : 'NO-GO'} — before=${JSON.stringify(ids1)} after=${JSON.stringify(ids2)}${setModelErr ? ` err=${setModelErr}` : ''}`);
|
|
324
|
+
console.log(`(b) setMaxThinkingTokens: ${thinkingObservable ? 'GO (callable + observable thinking diff)' : thinkingCallable ? 'GO-callable (no error; thinking-block diff inconclusive — display may be omitted)' : 'NO-GO'} off=${rOff.thinking} on=${rOn.thinking}${thinkErr ? ` err=${thinkErr}` : ''}`);
|
|
325
|
+
console.log(`(c) setPermissionMode (persistent-stream 37.5 regression): ${pmGo ? 'GO' : 'NO-GO'}${pmErr ? ` err=${pmErr}` : ''}`);
|
|
326
|
+
console.log(`(d) setEffort runtime API: ${effortRuntimePresent ? 'PRESENT (unexpected — re-evaluate)' : 'ABSENT → NO-GO by design (fallback: generator restart)'}`);
|
|
327
|
+
pq.close();
|
|
328
|
+
controller.abort();
|
|
329
|
+
setTimeout(() => process.exit(0), 500);
|
|
330
|
+
})();
|
|
331
|
+
|
|
332
|
+
setTimeout(() => { console.log('[runtime] TIMEOUT — did not finish within 120s'); pq.close(); controller.abort(); setTimeout(() => process.exit(0), 500); }, 120_000);
|
|
333
|
+
}
|
|
334
|
+
// ========================= residency mode (38.5 AC1) ==========================
|
|
335
|
+
// The CORE Story 38.5 gate. One persistent stream, N sequential turns, NO close()
|
|
336
|
+
// and NO restart between turns. We time each turn end-to-end; a cold query() spawn
|
|
337
|
+
// pays ~3s CLI/auth warmup, so turns 2..N landing on the SAME iterator with the
|
|
338
|
+
// gap between "prompt pushed" and "result" free of that warmup = residency holds.
|
|
339
|
+
else if (MODE === 'residency') {
|
|
340
|
+
const iterator = q[Symbol.asyncIterator]();
|
|
341
|
+
const t0 = Date.now();
|
|
342
|
+
const N = 4; // ≥3 turns to prove repeated reuse
|
|
343
|
+
const turns = []; // { i, elapsedMs, gapMs, subtype }
|
|
344
|
+
let lastResultAt = null; // wall-clock of the previous turn's result
|
|
345
|
+
|
|
346
|
+
// Drive one turn on the shared iterator; measure prompt→result AND the idle gap
|
|
347
|
+
// between the previous result and this turn's first observed message.
|
|
348
|
+
const runTurn = async (i, text) => {
|
|
349
|
+
const pushedAt = Date.now();
|
|
350
|
+
pq.push(userMsg(text));
|
|
351
|
+
let firstMsgAt = null;
|
|
352
|
+
for (;;) {
|
|
353
|
+
const step = await iterator.next();
|
|
354
|
+
if (step.done) { console.log(`[residency] turn ${i}: stream ended UNEXPECTEDLY (process died)`); return { i, done: true }; }
|
|
355
|
+
const m = step.value;
|
|
356
|
+
if (firstMsgAt === null) firstMsgAt = Date.now();
|
|
357
|
+
if (m.type !== 'result') continue;
|
|
358
|
+
const resultAt = Date.now();
|
|
359
|
+
const rec = {
|
|
360
|
+
i,
|
|
361
|
+
subtype: m.subtype,
|
|
362
|
+
elapsedMs: resultAt - pushedAt, // prompt → result for this turn
|
|
363
|
+
ttfmMs: firstMsgAt - pushedAt, // prompt → first message (warmup proxy)
|
|
364
|
+
gapMs: lastResultAt !== null ? pushedAt - lastResultAt : null,
|
|
365
|
+
};
|
|
366
|
+
lastResultAt = resultAt;
|
|
367
|
+
turns.push(rec);
|
|
368
|
+
console.log(`[residency] turn ${i} result subtype=${m.subtype} elapsed=${rec.elapsedMs}ms ttfm=${rec.ttfmMs}ms @t+${resultAt - t0}ms`);
|
|
369
|
+
return rec;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
(async () => {
|
|
374
|
+
// Live-query runtime setter presence: resume/fork must be ABSENT (creation
|
|
375
|
+
// options only), model/permission present (38.4 baseline) for contrast.
|
|
376
|
+
const setterPresence = {
|
|
377
|
+
setModel: typeof q.setModel === 'function',
|
|
378
|
+
setPermissionMode: typeof q.setPermissionMode === 'function',
|
|
379
|
+
setResume: typeof q.setResume === 'function', // expected ABSENT
|
|
380
|
+
setForkSession: typeof q.setForkSession === 'function', // expected ABSENT
|
|
381
|
+
resume: typeof q.resume === 'function', // expected ABSENT
|
|
382
|
+
fork: typeof q.fork === 'function', // expected ABSENT
|
|
383
|
+
};
|
|
384
|
+
console.log(`[residency] live-query setter presence ${JSON.stringify(setterPresence)}`);
|
|
385
|
+
|
|
386
|
+
let processDied = false;
|
|
387
|
+
for (let i = 1; i <= N && !processDied; i++) {
|
|
388
|
+
const r = await runTurn(i, `Reply with exactly one word: TURN${i}`);
|
|
389
|
+
if (r.done) { processDied = true; break; }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log(`\n=== verdict (mode=residency, 38.5 AC1 gate) ===`);
|
|
393
|
+
console.log(`turns completed: ${turns.length}/${N}`);
|
|
394
|
+
for (const t of turns) {
|
|
395
|
+
console.log(` turn ${t.i}: elapsed=${t.elapsedMs}ms ttfm=${t.ttfmMs}ms${t.gapMs !== null ? ` idleGapBeforePush=${t.gapMs}ms` : ''} subtype=${t.subtype}`);
|
|
396
|
+
}
|
|
397
|
+
// Warmup asymmetry: turn 1 pays the cold spawn; turns 2..N should be markedly
|
|
398
|
+
// faster to first message (no ~3s restart). Report the ratio for the record.
|
|
399
|
+
const t1 = turns.find((t) => t.i === 1);
|
|
400
|
+
const later = turns.filter((t) => t.i > 1);
|
|
401
|
+
const laterMaxTtfm = later.reduce((mx, t) => Math.max(mx, t.ttfmMs), 0);
|
|
402
|
+
const allTurnsLanded = turns.length === N && !processDied;
|
|
403
|
+
// No-restart signal: every turn 2..N reached first message without paying a
|
|
404
|
+
// fresh cold-spawn warmup (well under turn 1's ttfm, and under a 3s restart).
|
|
405
|
+
const noRestart = allTurnsLanded && later.every((t) => t.ttfmMs < 3000 && (t1 ? t.ttfmMs <= t1.ttfmMs + 500 : true));
|
|
406
|
+
const resumeForkRuntimeAbsent = !setterPresence.setResume && !setterPresence.setForkSession && !setterPresence.resume && !setterPresence.fork;
|
|
407
|
+
console.log(`turn1 ttfm=${t1 ? t1.ttfmMs : 'n/a'}ms; later turns max ttfm=${laterMaxTtfm}ms`);
|
|
408
|
+
console.log(`resume/fork live-query runtime setters absent: ${resumeForkRuntimeAbsent} (creation-option-only confirmed) ${JSON.stringify({ setResume: setterPresence.setResume, setForkSession: setterPresence.setForkSession, resume: setterPresence.resume, fork: setterPresence.fork })}`);
|
|
409
|
+
console.log(`GO/NO-GO: ${allTurnsLanded && noRestart
|
|
410
|
+
? `GO — one process sustained ${N} turns with no close and no ~3s restart between them (later ttfm max ${laterMaxTtfm}ms) → adopt residency registry`
|
|
411
|
+
: allTurnsLanded
|
|
412
|
+
? `PARTIAL — all ${N} turns landed on one process but a later turn showed restart-scale warmup (max ttfm ${laterMaxTtfm}ms); inspect gaps before adopting residency`
|
|
413
|
+
: `NO-GO — process did not sustain ${N} turns (completed ${turns.length}); residency not viable, keep turn-boundary recreation`}`);
|
|
414
|
+
pq.close();
|
|
415
|
+
controller.abort();
|
|
416
|
+
setTimeout(() => process.exit(0), 500);
|
|
417
|
+
})();
|
|
418
|
+
|
|
419
|
+
setTimeout(() => { console.log('[residency] TIMEOUT — did not finish within 120s'); pq.close(); controller.abort(); setTimeout(() => process.exit(0), 500); }, 120_000);
|
|
420
|
+
}
|
|
421
|
+
// ========================== interact mode (38.3 AC2) ==========================
|
|
422
|
+
// Post-result control-channel round-trip. Turn 1 launches a background bash and
|
|
423
|
+
// ends; the engine holds, gets the completion notification, auto-resumes, and in
|
|
424
|
+
// THAT follow-up generation calls AskUserQuestion. We verify the gate (canUseTool)
|
|
425
|
+
// fires post-result AND the injected answer round-trips (a follow-up result lands,
|
|
426
|
+
// no "Stream closed"). GO ⇒ wire the production canUseTool path onto the follow-up.
|
|
427
|
+
else if (MODE === 'interact') {
|
|
428
|
+
const iterator = q[Symbol.asyncIterator]();
|
|
429
|
+
const results = [];
|
|
430
|
+
let sawFollowupResult = false;
|
|
431
|
+
let streamError = null;
|
|
432
|
+
pq.push(userMsg(
|
|
433
|
+
'Do these steps strictly in order:\n' +
|
|
434
|
+
'1. Run a Bash command in the BACKGROUND (set run_in_background: true): `sleep 6; echo probe-done`.\n' +
|
|
435
|
+
"2. As soon as it is launched, reply with the single word 'launched' and END YOUR TURN — do NOT wait for it.\n" +
|
|
436
|
+
'3. Later, when you receive the background-task completion notification, use the AskUserQuestion tool to ask exactly one question: "Proceed?" with two options "yes" and "no".\n' +
|
|
437
|
+
"4. After I answer, reply with the single word 'FOLLOWUP_OK' and end."));
|
|
438
|
+
(async () => {
|
|
439
|
+
try {
|
|
440
|
+
for (;;) {
|
|
441
|
+
const step = await iterator.next();
|
|
442
|
+
if (step.done) { console.log('[interact] stream ended'); break; }
|
|
443
|
+
const m = step.value;
|
|
444
|
+
if (m.type !== 'result') continue;
|
|
445
|
+
if (firstResultAt === null) firstResultAt = Date.now();
|
|
446
|
+
const latest = stopSnapshots[stopSnapshots.length - 1];
|
|
447
|
+
const tasks = latest ? latest.tasks.length : -1;
|
|
448
|
+
results.push({ at: Date.now() - PROBE_T0, subtype: m.subtype, bg: tasks });
|
|
449
|
+
console.log(`[interact] result #${results.length} subtype=${m.subtype} @t+${Date.now() - PROBE_T0}ms bg=${tasks}`);
|
|
450
|
+
// Turn ends only once the follow-up (which carried the AskUserQuestion)
|
|
451
|
+
// has produced its own result with no background task still in flight.
|
|
452
|
+
const answeredPostResult = canUseToolEvents.some((e) => e.tool === 'AskUserQuestion' && e.postResult);
|
|
453
|
+
if (answeredPostResult && tasks === 0) { sawFollowupResult = true; break; }
|
|
454
|
+
}
|
|
455
|
+
} catch (e) { streamError = e?.message || String(e); console.error('[interact error]', streamError); }
|
|
456
|
+
|
|
457
|
+
const askPost = canUseToolEvents.filter((e) => e.tool === 'AskUserQuestion' && e.postResult);
|
|
458
|
+
const askPre = canUseToolEvents.filter((e) => e.tool === 'AskUserQuestion' && !e.postResult);
|
|
459
|
+
console.log(`\n=== verdict (mode=interact, AC2 gate) ===`);
|
|
460
|
+
console.log(`results: ${results.map((r) => `#${results.indexOf(r) + 1}:${r.subtype}/bg=${r.bg}@t+${r.at}ms`).join(' → ') || '(none)'}`);
|
|
461
|
+
console.log(`canUseTool calls: ${JSON.stringify(canUseToolEvents.map((e) => ({ tool: e.tool, postResult: e.postResult, at: e.at - PROBE_T0 })))}`);
|
|
462
|
+
console.log(`AskUserQuestion post-result: ${askPost.length}, pre-result: ${askPre.length}`);
|
|
463
|
+
console.log(`stream error: ${streamError ?? 'none'}`);
|
|
464
|
+
const GO = askPost.length > 0 && sawFollowupResult && !streamError;
|
|
465
|
+
console.log(`GO/NO-GO: ${GO
|
|
466
|
+
? 'GO — post-result AskUserQuestion answered, injection round-tripped, follow-up result landed (control channel survives result)'
|
|
467
|
+
: askPost.length === 0
|
|
468
|
+
? 'INCONCLUSIVE — no post-result AskUserQuestion observed (model may not have deferred it to the follow-up)'
|
|
469
|
+
: `NO-GO — ${streamError ? `control channel error on injection: ${streamError}` : 'no follow-up result after injection'}`}`);
|
|
470
|
+
pq.close();
|
|
471
|
+
controller.abort();
|
|
472
|
+
setTimeout(() => process.exit(0), 500);
|
|
473
|
+
})();
|
|
474
|
+
|
|
475
|
+
setTimeout(() => { if (!sawFollowupResult) { console.log('[interact] TIMEOUT — no follow-up result within 60s'); pq.close(); controller.abort(); setTimeout(() => process.exit(0), 500); } }, 60_000);
|
|
476
|
+
}
|
|
477
|
+
// ========================== bashkill mode (38.3 AC3) ==========================
|
|
478
|
+
// A background Bash launched with the input channel kept OPEN (persistent stream):
|
|
479
|
+
// does it survive past the historical ~5s kill point? We watch for the completion
|
|
480
|
+
// notification landing near the full sleep (survived) vs an early stop/vanish (~5s).
|
|
481
|
+
else if (MODE === 'bashkill') {
|
|
482
|
+
const iterator = q[Symbol.asyncIterator]();
|
|
483
|
+
const SLEEP_S = 20;
|
|
484
|
+
const t0 = Date.now();
|
|
485
|
+
let launchedAt = null;
|
|
486
|
+
let notif = null; // { at, status }
|
|
487
|
+
let survivedResult = false;
|
|
488
|
+
const snapshotPolls = []; // { at, bg } — Stop snapshot bg count sampled over time
|
|
489
|
+
const poll = setInterval(() => {
|
|
490
|
+
const latest = stopSnapshots[stopSnapshots.length - 1];
|
|
491
|
+
snapshotPolls.push({ at: Math.round((Date.now() - t0) / 1000), bg: latest ? latest.tasks.length : -1 });
|
|
492
|
+
}, 1000);
|
|
493
|
+
pq.push(userMsg(
|
|
494
|
+
`Run a Bash command in the BACKGROUND (set run_in_background: true): \`sleep ${SLEEP_S}; echo probe-done\`. ` +
|
|
495
|
+
"As soon as it is launched, reply with the single word 'launched' and END YOUR TURN — do NOT wait for it. " +
|
|
496
|
+
"Later, when you receive the completion notification, reply with the single word 'SURVIVED' and end."));
|
|
497
|
+
(async () => {
|
|
498
|
+
try {
|
|
499
|
+
for (;;) {
|
|
500
|
+
const step = await iterator.next();
|
|
501
|
+
if (step.done) { console.log('[bashkill] stream ended'); break; }
|
|
502
|
+
const m = step.value;
|
|
503
|
+
if (m.type === 'system' && m.subtype === 'task_notification') {
|
|
504
|
+
notif = { at: Math.round((Date.now() - t0) / 1000), status: m.status };
|
|
505
|
+
console.log(`[bashkill] task_notification @t+${notif.at}s status=${notif.status}`);
|
|
506
|
+
}
|
|
507
|
+
if (m.type !== 'result') continue;
|
|
508
|
+
const latest = stopSnapshots[stopSnapshots.length - 1];
|
|
509
|
+
const tasks = latest ? latest.tasks.length : -1;
|
|
510
|
+
const at = Math.round((Date.now() - t0) / 1000);
|
|
511
|
+
console.log(`[bashkill] result @t+${at}s subtype=${m.subtype} bg=${tasks}`);
|
|
512
|
+
if (launchedAt === null) launchedAt = at;
|
|
513
|
+
// The follow-up result (after the completion notification) closes the turn.
|
|
514
|
+
if (launchedAt !== null && at > 6 && tasks === 0) { survivedResult = true; break; }
|
|
515
|
+
}
|
|
516
|
+
} catch (e) { console.error('[bashkill error]', e?.message || e); }
|
|
517
|
+
clearInterval(poll);
|
|
518
|
+
|
|
519
|
+
console.log(`\n=== verdict (mode=bashkill, AC3 gate) ===`);
|
|
520
|
+
console.log(`launched result @t+${launchedAt}s; Stop-snapshot bg over time: ${snapshotPolls.map((p) => `${p.at}s:${p.bg}`).join(' ')}`);
|
|
521
|
+
console.log(`completion notification: ${notif ? `@t+${notif.at}s status=${notif.status}` : 'NONE'}`);
|
|
522
|
+
// Survival = the sleep 20 ran to completion (notification near the full sleep,
|
|
523
|
+
// status completed) → no ~5s kill. A stop/failed near 5s = kill reproduced.
|
|
524
|
+
const survivedByNotif = !!notif && notif.status === 'completed' && notif.at >= 15;
|
|
525
|
+
const killedEarly = !!notif && notif.status !== 'completed' && notif.at <= 8;
|
|
526
|
+
const GO = (survivedByNotif || survivedResult) && !killedEarly;
|
|
527
|
+
console.log(`GO/NO-GO: ${GO
|
|
528
|
+
? `GO — background Bash survived past 5s (${survivedByNotif ? `completion @t+${notif.at}s` : 'follow-up result reached'}); no ~5s kill with the input channel open`
|
|
529
|
+
: killedEarly
|
|
530
|
+
? `NO-GO — background task terminated early (@t+${notif.at}s status=${notif.status}) → ~5s kill reproduced, needs root-cause`
|
|
531
|
+
: 'INCONCLUSIVE — no completion notification nor follow-up result within the window'}`);
|
|
532
|
+
pq.close();
|
|
533
|
+
controller.abort();
|
|
534
|
+
setTimeout(() => process.exit(0), 500);
|
|
535
|
+
})();
|
|
536
|
+
|
|
537
|
+
setTimeout(() => { if (!survivedResult) { console.log(`[bashkill] TIMEOUT — no closing follow-up result within ${SLEEP_S + 20}s`); clearInterval(poll); pq.close(); controller.abort(); setTimeout(() => process.exit(0), 500); } }, (SLEEP_S + 20) * 1000);
|
|
538
|
+
}
|
|
539
|
+
// ========================= longhold mode (38.6 AC1) ============================
|
|
540
|
+
// Vitest-grade verification: a >1 minute background child must be tracked correctly
|
|
541
|
+
// AND the same process must sustain a follow-up reuse turn after the hold→terminate.
|
|
542
|
+
// Combines the hold (background hold→terminate) and residency (reuse) patterns.
|
|
543
|
+
else if (MODE === 'longhold') {
|
|
544
|
+
const iterator = q[Symbol.asyncIterator]();
|
|
545
|
+
const SLEEP_S = 65;
|
|
546
|
+
const results = [];
|
|
547
|
+
const t0 = Date.now();
|
|
548
|
+
let holdDone = false;
|
|
549
|
+
let reuseDone = false;
|
|
550
|
+
pq.push(userMsg(
|
|
551
|
+
`Run a Bash command in the BACKGROUND (set run_in_background: true): \`sleep ${SLEEP_S}; echo probe-done\`. ` +
|
|
552
|
+
"As soon as it is launched, reply 'launched' and END YOUR TURN — do NOT wait."));
|
|
553
|
+
(async () => {
|
|
554
|
+
try {
|
|
555
|
+
// Phase 1: hold→terminate (>1 min child)
|
|
556
|
+
for (;;) {
|
|
557
|
+
const step = await iterator.next();
|
|
558
|
+
if (step.done) { console.log('[longhold] stream ended during phase 1'); break; }
|
|
559
|
+
const m = step.value;
|
|
560
|
+
if (m.type !== 'result') continue;
|
|
561
|
+
const latest = stopSnapshots[stopSnapshots.length - 1];
|
|
562
|
+
const tasks = latest ? latest.tasks.length : -1;
|
|
563
|
+
const at = Date.now() - t0;
|
|
564
|
+
results.push({ phase: 1, at, tasks });
|
|
565
|
+
console.log(`[longhold] phase1 result #${results.length} @t+${at}ms — bg=${tasks}`);
|
|
566
|
+
if (tasks === 0) { holdDone = true; console.log('[longhold] hold→terminate complete'); break; }
|
|
567
|
+
console.log('[longhold] holding (bg in flight)...');
|
|
568
|
+
}
|
|
569
|
+
// Phase 2: reuse turn on the same process (residency survival after hold)
|
|
570
|
+
if (holdDone) {
|
|
571
|
+
const pushedAt = Date.now();
|
|
572
|
+
pq.push(userMsg('Reply with exactly one word: REUSED'));
|
|
573
|
+
for (;;) {
|
|
574
|
+
const step = await iterator.next();
|
|
575
|
+
if (step.done) { console.log('[longhold] stream ended during phase 2'); break; }
|
|
576
|
+
if (step.value.type !== 'result') continue;
|
|
577
|
+
const at = Date.now() - t0;
|
|
578
|
+
const ttfm = Date.now() - pushedAt;
|
|
579
|
+
results.push({ phase: 2, at, ttfm });
|
|
580
|
+
console.log(`[longhold] phase2 result @t+${at}ms ttfm=${ttfm}ms — reuse turn confirmed`);
|
|
581
|
+
reuseDone = true;
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
} catch (e) { console.error('[longhold error]', e?.message || e); }
|
|
586
|
+
|
|
587
|
+
const holdResults = results.filter((r) => r.phase === 1);
|
|
588
|
+
const firstHold = holdResults[0];
|
|
589
|
+
const lastHold = holdResults[holdResults.length - 1];
|
|
590
|
+
const holdDurationMs = firstHold && lastHold ? lastHold.at - firstHold.at : 0;
|
|
591
|
+
const reuseResult = results.find((r) => r.phase === 2);
|
|
592
|
+
console.log(`\n=== verdict (mode=longhold, 38.6 AC1 gate) ===`);
|
|
593
|
+
console.log(`hold phase: ${holdResults.length} result(s), duration=${holdDurationMs}ms (${Math.round(holdDurationMs / 1000)}s)`);
|
|
594
|
+
for (const r of holdResults) console.log(` phase1 @t+${r.at}ms bg=${r.tasks}`);
|
|
595
|
+
if (reuseResult) console.log(`reuse phase: @t+${reuseResult.at}ms ttfm=${reuseResult.ttfm}ms`);
|
|
596
|
+
const childExceeded60s = holdDurationMs > 55_000;
|
|
597
|
+
console.log(`(a) running period ≥1 task captured: ${firstHold && firstHold.tasks > 0}`);
|
|
598
|
+
console.log(`(b) completion bg=0: ${holdDone && lastHold?.tasks === 0}`);
|
|
599
|
+
console.log(`(c) hold→terminate correct: ${holdDone}`);
|
|
600
|
+
console.log(`(d) reuse turn same process: ${reuseDone}${reuseResult ? ` (ttfm=${reuseResult.ttfm}ms — no cold-spawn warmup)` : ''}`);
|
|
601
|
+
console.log(`child exceeded 60s: ${childExceeded60s} (hold duration=${Math.round(holdDurationMs / 1000)}s)`);
|
|
602
|
+
const GO = holdDone && reuseDone && firstHold?.tasks > 0 && childExceeded60s;
|
|
603
|
+
console.log(`GO/NO-GO: ${GO
|
|
604
|
+
? `GO — vitest-grade (${Math.round(holdDurationMs / 1000)}s) background child tracked, hold→terminate correct, reuse turn confirmed`
|
|
605
|
+
: !holdDone ? 'NO-GO — hold did not terminate'
|
|
606
|
+
: !childExceeded60s ? `PARTIAL — mechanism correct but child only ran ${Math.round(holdDurationMs / 1000)}s (<60s threshold)`
|
|
607
|
+
: 'PARTIAL — hold OK but reuse turn did not complete'}`);
|
|
608
|
+
pq.close();
|
|
609
|
+
controller.abort();
|
|
610
|
+
setTimeout(() => process.exit(0), 500);
|
|
611
|
+
})();
|
|
612
|
+
|
|
613
|
+
setTimeout(() => {
|
|
614
|
+
if (!holdDone || !reuseDone) {
|
|
615
|
+
console.log(`[longhold] TIMEOUT — holdDone=${holdDone} reuseDone=${reuseDone}`);
|
|
616
|
+
pq.close(); controller.abort(); setTimeout(() => process.exit(0), 500);
|
|
617
|
+
}
|
|
618
|
+
}, (SLEEP_S + 30) * 1000);
|
|
619
|
+
}
|
|
620
|
+
// ============================ hold mode (38.2 AC5) =============================
|
|
621
|
+
// Validates that with the 45s follow-up grace REMOVED, a turn that holds on an
|
|
622
|
+
// in-flight background task still terminates on its own: launch a background bash,
|
|
623
|
+
// keep consuming, and confirm a follow-up result eventually arrives whose main Stop
|
|
624
|
+
// reports background_tasks EMPTY (hadInFlight=false) — the turn's true end. No hang.
|
|
625
|
+
else if (MODE === 'hold') {
|
|
626
|
+
const iterator = q[Symbol.asyncIterator]();
|
|
627
|
+
const results = []; // { at, mainStopTasks }
|
|
628
|
+
pq.push(userMsg(
|
|
629
|
+
"Run a Bash command in the BACKGROUND (set run_in_background: true): `sleep 6; echo probe-done`. " +
|
|
630
|
+
"As soon as it is launched, reply 'launched' and END YOUR TURN — do NOT wait. " +
|
|
631
|
+
"Later, when you get the completion notification, reply 'FOLLOWUP_OK'."));
|
|
632
|
+
const startedAt = Date.now();
|
|
633
|
+
let done = false;
|
|
634
|
+
(async () => {
|
|
635
|
+
try {
|
|
636
|
+
for (;;) {
|
|
637
|
+
const step = await iterator.next();
|
|
638
|
+
if (step.done) { console.log('[hold] stream ended'); break; }
|
|
639
|
+
const m = step.value;
|
|
640
|
+
if (m.type !== 'result') { continue; }
|
|
641
|
+
// Latest main Stop snapshot at this result (mirrors processStream consume).
|
|
642
|
+
const latest = stopSnapshots[stopSnapshots.length - 1];
|
|
643
|
+
const tasks = latest ? latest.tasks.length : -1;
|
|
644
|
+
results.push({ at: Date.now() - startedAt, tasks });
|
|
645
|
+
console.log(`[hold] result #${results.length} @t+${Date.now() - startedAt}ms — main Stop background_tasks=${tasks}`);
|
|
646
|
+
// Turn boundary per 38.2: end when the main Stop reports nothing in flight.
|
|
647
|
+
if (tasks === 0) { done = true; console.log('[hold] hadInFlight=false → turn ends (no grace needed)'); break; }
|
|
648
|
+
console.log('[hold] hadInFlight=true → HOLD (keep consuming for the live follow-up)');
|
|
649
|
+
}
|
|
650
|
+
} catch (e) { console.error('[hold error]', e?.message || e); }
|
|
651
|
+
|
|
652
|
+
console.log(`\n=== verdict (mode=hold, AC5 gate) ===`);
|
|
653
|
+
console.log(`results seen: ${results.map((r) => `t+${r.at}ms/tasks=${r.tasks}`).join(' → ')}`);
|
|
654
|
+
console.log(`GO/NO-GO: ${done
|
|
655
|
+
? 'GO — hold terminated at an empty-Stop follow-up result WITHOUT any 45s grace (no hang)'
|
|
656
|
+
: 'NO-GO — turn did not reach an empty-Stop result within the window (would hang without grace)'}`);
|
|
657
|
+
pq.close();
|
|
658
|
+
controller.abort();
|
|
659
|
+
setTimeout(() => process.exit(0), 500);
|
|
660
|
+
})();
|
|
661
|
+
|
|
662
|
+
// Safety: hard stop if it hangs (proves NO-GO rather than blocking forever).
|
|
663
|
+
setTimeout(() => { if (!done) { console.log('[hold] TIMEOUT — no empty-Stop result within 40s'); pq.close(); controller.abort(); setTimeout(() => process.exit(0), 500); } }, 40_000);
|
|
664
|
+
}
|
|
665
|
+
// ============================ order mode (38.2 AC7) ============================
|
|
666
|
+
// Drives TWO sequential turns on ONE persistent stream and measures, at the moment
|
|
667
|
+
// each turn-ending `result` is dequeued, whether that turn's main Stop already fired.
|
|
668
|
+
else if (MODE === 'order') {
|
|
669
|
+
const iterator = q[Symbol.asyncIterator]();
|
|
670
|
+
const turns = [];
|
|
671
|
+
|
|
672
|
+
// Await the next turn-ending `result`, recording the ordering vs main Stop.
|
|
673
|
+
const runTurn = async (label, text) => {
|
|
674
|
+
const mainStopsBefore = stopSnapshots.length;
|
|
675
|
+
const pushedAt = Date.now();
|
|
676
|
+
pq.push(userMsg(text));
|
|
677
|
+
for (;;) {
|
|
678
|
+
const step = await iterator.next();
|
|
679
|
+
if (step.done) { console.log(`[${label}] stream ended unexpectedly`); break; }
|
|
680
|
+
const m = step.value;
|
|
681
|
+
if (m.type !== 'result') continue;
|
|
682
|
+
const resultAt = Date.now();
|
|
683
|
+
// Main Stop snapshots that arrived DURING this turn (after the push).
|
|
684
|
+
const turnStops = stopSnapshots.slice(mainStopsBefore);
|
|
685
|
+
const stopBeforeResult = turnStops.length > 0;
|
|
686
|
+
const stopAt = turnStops.length ? turnStops[turnStops.length - 1].at : null;
|
|
687
|
+
const rec = {
|
|
688
|
+
label,
|
|
689
|
+
subtype: m.subtype,
|
|
690
|
+
deltaMs: stopAt !== null ? resultAt - stopAt : null, // >0 = Stop before result
|
|
691
|
+
stopBeforeResult,
|
|
692
|
+
hadInFlight: turnStops.length ? turnStops[turnStops.length - 1].tasks.length > 0 : null,
|
|
693
|
+
elapsedMs: resultAt - pushedAt,
|
|
694
|
+
};
|
|
695
|
+
turns.push(rec);
|
|
696
|
+
console.log(`[${label}] result subtype=${m.subtype} @t+${rec.elapsedMs}ms — mainStopBeforeResult=${stopBeforeResult}` +
|
|
697
|
+
(stopAt !== null ? ` (Stop ${rec.deltaMs >= 0 ? 'led result by' : 'trailed result by'} ${Math.abs(rec.deltaMs)}ms)` : ' (Stop NOT yet fired at result)'));
|
|
698
|
+
// If Stop had not fired at result, poll briefly to see how late it lands.
|
|
699
|
+
if (!stopBeforeResult) {
|
|
700
|
+
const waitStart = Date.now();
|
|
701
|
+
while (stopSnapshots.length === mainStopsBefore && Date.now() - waitStart < 2000) {
|
|
702
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
703
|
+
}
|
|
704
|
+
if (stopSnapshots.length > mainStopsBefore) {
|
|
705
|
+
rec.stopLagMs = Date.now() - resultAt;
|
|
706
|
+
console.log(`[${label}] main Stop landed ${rec.stopLagMs}ms AFTER result (NO-GO signal)`);
|
|
707
|
+
} else {
|
|
708
|
+
console.log(`[${label}] main Stop still not fired 2s after result`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
(async () => {
|
|
716
|
+
try {
|
|
717
|
+
await runTurn('simple', 'Reply with the single word: OK');
|
|
718
|
+
await runTurn('background',
|
|
719
|
+
"Run a Bash command in the BACKGROUND (set run_in_background: true): `sleep 30; echo probe-done`. " +
|
|
720
|
+
"As soon as it is launched in the background, reply with the single word 'launched' and END YOUR TURN — do NOT wait for it to finish.");
|
|
721
|
+
} catch (e) {
|
|
722
|
+
console.error('[order driver error]', e?.message || e);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
console.log(`\n=== verdict (mode=order, AC7 gate) ===`);
|
|
726
|
+
for (const t of turns) {
|
|
727
|
+
console.log(` ${t.label}: stopBeforeResult=${t.stopBeforeResult}` +
|
|
728
|
+
(t.deltaMs !== null ? `, Stop→result delta=${t.deltaMs}ms` : '') +
|
|
729
|
+
(t.stopLagMs !== undefined ? `, stopLagAfterResult=${t.stopLagMs}ms` : '') +
|
|
730
|
+
`, hadInFlight=${t.hadInFlight}`);
|
|
731
|
+
}
|
|
732
|
+
const allBefore = turns.length > 0 && turns.every((t) => t.stopBeforeResult);
|
|
733
|
+
const maxLag = turns.reduce((mx, t) => Math.max(mx, t.stopLagMs ?? 0), 0);
|
|
734
|
+
console.log(`GO/NO-GO: ${allBefore
|
|
735
|
+
? 'GO — main Stop present in ledger at every result (read snapshot directly at result)'
|
|
736
|
+
: `NO-GO — Stop trailed result (max observed lag ${maxLag}ms) → bounded micro-grace needed`}`);
|
|
737
|
+
|
|
738
|
+
pq.close();
|
|
739
|
+
controller.abort();
|
|
740
|
+
setTimeout(() => process.exit(0), 500);
|
|
741
|
+
})();
|
|
742
|
+
} else {
|
|
743
|
+
// ========================== smoke / subagent modes ============================
|
|
744
|
+
const prompt = MODE === 'subagent'
|
|
745
|
+
? "Use the Agent tool to launch a subagent in the BACKGROUND (set run_in_background: true) with subagent_type 'general-purpose'. Its task prompt: run a Bash command `sleep 45; echo probe-done`. As soon as you have launched it in the background, reply with the single word 'launched' and END YOUR TURN — do NOT wait for the subagent to finish."
|
|
746
|
+
: "Reply with the single word: OK";
|
|
747
|
+
|
|
748
|
+
console.log(`=== probe mode=${MODE} — pushing prompt, streaming... ===`);
|
|
749
|
+
pq.push(userMsg(prompt));
|
|
750
|
+
|
|
751
|
+
let idleProbeDone = false;
|
|
752
|
+
const startedAt = Date.now();
|
|
753
|
+
|
|
754
|
+
// Consume the persistent message stream.
|
|
755
|
+
(async () => {
|
|
756
|
+
try {
|
|
757
|
+
for await (const m of q) {
|
|
758
|
+
if (m.type === 'result') {
|
|
759
|
+
console.log(`[result] subtype=${m.subtype} (t+${Date.now() - startedAt}ms)`);
|
|
760
|
+
// Idle-next() blocking probe: after the first result the stream should NOT
|
|
761
|
+
// end; the next iteration blocks awaiting either a follow-up or close().
|
|
762
|
+
if (!idleProbeDone) { idleProbeDone = true; console.log('[idle] stream held open past result (next() awaiting follow-up/close)'); }
|
|
763
|
+
} else {
|
|
764
|
+
console.log(`[msg] ${m.type}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
console.log('[stream] generator ended (query closed)');
|
|
768
|
+
} catch (e) {
|
|
769
|
+
console.error('[stream error]', e?.message || e);
|
|
770
|
+
}
|
|
771
|
+
})();
|
|
772
|
+
|
|
773
|
+
const DURATION = MODE === 'subagent' ? 95_000 : 25_000;
|
|
774
|
+
setTimeout(() => {
|
|
775
|
+
console.log(`\n=== verdict (mode=${MODE}) ===`);
|
|
776
|
+
console.log(`Stop snapshots: ${stopSnapshots.length}`);
|
|
777
|
+
console.log(`SubagentStop hooks: ${subagentStops.length}`);
|
|
778
|
+
console.log(`Task events: ${JSON.stringify(taskEvents)}`);
|
|
779
|
+
|
|
780
|
+
const anyStopFired = stopSnapshots.length > 0;
|
|
781
|
+
console.log(`HOOKS-IN-PERSISTENT-STREAM: ${anyStopFired ? 'CONFIRMED (Stop fired in-process)' : 'NOT OBSERVED'}`);
|
|
782
|
+
|
|
783
|
+
if (MODE === 'subagent') {
|
|
784
|
+
const subagentInStop = stopSnapshots.some((s) => s.tasks.some((t) => t.type === 'subagent' || t.agent_type));
|
|
785
|
+
const subagentInSubagentStop = subagentStops.some((s) => s.tasks.length > 0 || s.agent_type);
|
|
786
|
+
console.log(`Subagent seen in Stop.background_tasks[]: ${subagentInStop}`);
|
|
787
|
+
console.log(`Subagent seen via SubagentStop: ${subagentInSubagentStop}`);
|
|
788
|
+
console.log(`GO/NO-GO: ${subagentInStop ? 'GO (Stop tracks subagent)' : subagentInSubagentStop ? 'NO-GO for Stop / SubagentStop is a viable fallback' : 'INCONCLUSIVE (subagent never observed — model may not have backgrounded it)'}`);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
pq.close();
|
|
792
|
+
controller.abort();
|
|
793
|
+
setTimeout(() => process.exit(0), 500);
|
|
794
|
+
}, DURATION);
|
|
795
|
+
}
|