chainlesschain 0.162.159 → 0.162.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-ByIiF5bT.js → AIOps-DMn528q-.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-NlgSFZq5.js → ActionButton-D9ZcDCJL.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-D17cOUcb.js → Analytics-A_K-8PX0.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-B4mEZcbl.js → AppLayout-D4pFW3X6.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-CASsmyQw.js → Artifacts-Dz46COAd.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-DYAia2e2.js → Audit-cbMjbyg0.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-C1lvYUSu.js → BackgroundAgents-Dtf88GAj.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-B6agK6bA.js → Backup-C5KRxn52.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-DM43R2vy.js → BaseInput-Dd2UK3BN.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-Dtdp3eXp.js → Chat-lptKTFdG.js} +6 -6
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BL5c_OQ1.js → ChatBubbleRenderer-C2KLtwHz.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-D2xf7uZt.js → Checkbox-_yspG6rG.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-D3j45jJL.js → Codegen-BGq_GAha.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-voPJ9fUZ.js → Col-CePor71g.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-DEsyTESF.js → Community-CLOWhIT7.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-OHV_QXLD.js → Compact-CivJt2vT.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-eDTJWC-J.js → Compliance-Btmo1GGE.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-BBvy-6d1.js → Cowork-Ct35OlG4.js} +2 -2
  21. package/src/assets/web-panel/assets/{Cron-Dw2eoJxu.js → Cron-CTc2GmhB.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-DQHms_Bh.js → Crosschain-ZtiBETk1.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-Cyu7Id1c.js → DID-BNqMQQXw.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-sIDzkG92.js → Dashboard-BmzZd-9V.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-DUW3T2ey.js → Dropdown-CMVgvP2M.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-CECELXW8.js → EmailListRenderer-CcqQFAfT.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BIZR37ov.js → FamilyGuardDashboard-CTqB1_4V.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-BEZIQX84.js → Federation-CiKOOzlb.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-BcdUMznL.js → FormItemContext-Wk-OT0gr.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-DsN_fr5H.js → GenericCardRenderer-DbEQT6ue.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-63LhjBx2.js → Git-0ZvCPSD9.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CYtQtIcH.js → Governance-CmVuKkPb.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-BwR6UUox.js → Inference-CTWE3Jwg.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-D9lVNTUA.js → KnowledgeGraph-CBEIF00L.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-C4q5h7Ut.js → Logs-DLhxDwVG.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-Plksliuw.js → Marketplace-BgsCBrnc.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DZUFfeuG.js → McpTools-ClBUGDS2.js} +3 -3
  39. package/src/assets/web-panel/assets/{Memory-NoDYt29V.js → Memory-CnG_ZnYw.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-CEXGTkdh.js → MobileBridge-BUOAbR1Z.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-DnbJkbZo.js → MobileProjects-Dnt_9-WB.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-DTAa0cwy.js → Mtc-CLjt4kCD.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-CVV7LnNS.js → MtcAudit-f24U9Aes.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-BqqGrjt_.js → Multisig-GrmGQbSq.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-C4WP8DRp.js → NLProgramming-DADaWn-n.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-Zo4DJUVI.js → Notes-xtf5RX6C.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-DBcjRaz2.js → NotificationSettings-Cd27Rgtx.js} +1 -1
  48. package/src/assets/web-panel/assets/OrderTableRenderer-fXRxlDjl.js +1 -0
  49. package/src/assets/web-panel/assets/{Organization-BT9WPBOX.js → Organization-joCFlNB0.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-D6b4bZ3q.js → Overflow-B1HoURq0.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-Cw3OsYx-.js → P2P-DiwsKbT7.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-C0nyiK2b.js → PdhVaultBrowser-Xe-BsW6S.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-zVBZEB_C.js → Permissions-C23qi8kl.js} +3 -3
  54. package/src/assets/web-panel/assets/{PersonalDataHub-BOspsFew.js → PersonalDataHub-BS1syX2f.js} +2 -2
  55. package/src/assets/web-panel/assets/{Pipeline-CQWfZ4XH.js → Pipeline-DRYwwDyh.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-C2oGJ_WL.js → Privacy-BQKaDHPH.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-BZMR4qY1.js → ProjectInit-BC6F9xiG.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-DAujoP63.js → ProjectSettings-ClFPc1Nd.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-Dn-f5aYe.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-D_RGHM-v.js → Providers-CLWuOUNt.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-Mf6KfL2J.js → QrScannerModal-DiqULTxb.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-5D8URoI_.js → QuickAsk-9A03P-E_.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-Df4kC8Gy.js → Recommend-CJZAnbq_.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-DGi3f3vO.js → RemoteSession-C9A1R77h.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-jmkGFNWb.js → Reputation-BIZc6Rky.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-DP6Pcu3m.js → Row-DfIPNtuG.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-D60hSuPO.js → RssFeed-C5znFcQ1.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-0mf1BQUK.js → Search-DSs2z3OX.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-CfZtLFYV.js → Security-CV6LcrLY.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-D9zpZLWz.js → Services-cmhoX0Xj.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-D1_44eMD.js → Skeleton-tCsg2po7.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-cVFa8zkz.js → Skills-BK1KUDiy.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-Cinpzc3L.js → Sla-yRgyeA82.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-bqdiJE_P.js → SpeechSettings-c1mo1mlp.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-Dgl_SLgj.js → SyncSettings-koIAfuT3.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-DawWAEhy.js → Tasks-DkkreAaB.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-Ca0nY651.js → Templates-fy5Ctf9M.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-CgaGEhLm.js → Tenant-Bw70Jf6S.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-D-9EnpRx.js → Terminal-BKh6Rx-4.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-CSOxDHZ6.js → TimelineRenderer-Dg54WP0c.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-BpwLZpL3.js → Tokens-BvvD5SxJ.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-DgowwBK5.js → Trigger-B7Ug9x4e.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-BZutapjE.js → Trust-BjvuMORY.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-QZPt8_Sp.js → UkeySign-CZqhkx8p.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-DlQx5UDJ.js → VideoEditing-ClR50lhX.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-ZfdTgtWA.js → Wallet-DlfppYjx.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-CAp0y_Cr.js → WebAuthn-BirgVr8B.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-BVITZEmr.js → WorkflowEditor-B8DP_bax.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-CC2tPaNg.js → chat-DA8AUZzL.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-31brc9CA.js → colors-C38pLGp1.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-DLMkC9C-.js → compact-item-81rtr98o.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-BMtveI6F.js → createContext-D34ixq8t.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-DbD6WHW2.js → hasIn-D4bzqTrZ.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BoO4WEJB.js → index-9qoVpPt6.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CJhREOlG.js → index-B0QArPQt.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BnFLzejT.js → index-B8mkLM4x.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Bbc2JeQG.js → index-BIP62a6r.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CFXKbOek.js → index-BP-zkXK8.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-C-cGlCDW.js → index-BS3M5H9V.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-AaIKG60c.js → index-BXd2_LmH.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CEXa-4zd.js → index-BlxEphl7.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DZl-UXB6.js → index-BqIhLAbA.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BOM-VFlv.js → index-BtGxuQrO.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CcVszn2b.js → index-BvD7b2IK.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-C21pdBqs.js → index-BvL_3J_n.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DJvxFyIz.js → index-COe_cOLJ.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-ALvoNTRF.js → index-CP5alL4t.js} +1 -1
  109. package/src/assets/web-panel/assets/index-CY1NURWZ.js +1 -0
  110. package/src/assets/web-panel/assets/{index-Dm0byjm1.js → index-CbhE3j5j.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-WQUO2vvK.js → index-Cd3KuNK5.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-bdaJwTXJ.js → index-CoVgZw_b.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-COHHQ9cU.js → index-CuS-UPoj.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-C7LHNw_N.js → index-D0Ih7Hwn.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Cod97Urs.js → index-D3zCCZRS.js} +3 -3
  116. package/src/assets/web-panel/assets/{index-Cwbqh0KZ.js → index-D6GfvEQG.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BKAB6-6V.js → index-DBFSo1NF.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CEwjBA9q.js → index-DFHa1HIg.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DndNnYc0.js → index-DFyWKOBD.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-3oBsXZmF.js → index-DMgnRwrK.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-C_eUKGYP.js → index-DUQLXLQM.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CUmLMFsy.js → index-D_2VYPr1.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BPpS-lGb.js → index-DakfFZ-k.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-ep6JchNG.js → index-Ddk0e_Pi.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-D7cV0tu0.js → index-DkHqMnn3.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-jG6h22Qg.js → index-Dmo3dFHb.js} +1 -1
  127. package/src/assets/web-panel/assets/index-DokUYqpq.js +1 -0
  128. package/src/assets/web-panel/assets/{index-C1PfeI88.js → index-DvfCEJG-.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-ntVQqSML.js → index-E1OJRXs4.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CWgfipcC.js → index-F76w1NiL.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BiRQUoyE.js → index-ZRJRItm0.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-BFRLmCR3.js → index-fI5sXMLg.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-D67kIagl.js → index-tKiC8T-4.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-D5UoBRhN.js → initDefaultProps-ByyvflIR.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-DltOw0CR.js → motion-B3FQm2By.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-_oPkk5zj.js → move-BSfns6Ef.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-EES8ROCS.js → mtc-parser-CTG1Ieri.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-B-_39x_M.js → omit-DObM_2j6.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-l6D0yJHz.js → pickAttrs-DOfelQ1t.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-DIeUK31L.js → placementArrow-CHZILds_.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-dG4EzUQo.js → responsiveObserve-REbucKSx.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-DWFzYGdH.js → slide-C6EZzD7o.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-CECdTA5H.js → statusUtils-BxkWS-g3.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-D6kP-mMK.js → styleChecker-Chklyoq6.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-BWfna_rd.js → useFlexGapSupport-CRVwKrrI.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-Bb-j6rzo.js → useFs-DV24ik_J.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-D5OIsYM4.js → usePersonalDataHub-C7vIo4Pg.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-DNpwzFTM.js → vnode-By-v5GlF.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-vNLpFQBW.js → zoom-B1w9Am3r.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/agenda.js +15 -1
  152. package/src/commands/agent.js +72 -6
  153. package/src/commands/background-session.js +46 -2
  154. package/src/commands/mcp.js +46 -0
  155. package/src/commands/session.js +70 -0
  156. package/src/data/changelog.json +12 -2
  157. package/src/gateways/ws/background-agent-protocol.js +88 -14
  158. package/src/harness/jsonl-session-store.js +70 -1
  159. package/src/harness/mcp-client.js +50 -0
  160. package/src/lib/agent-authority.js +251 -0
  161. package/src/lib/agent-sandbox.js +68 -0
  162. package/src/lib/agents.js +12 -4
  163. package/src/lib/api-key-helper.js +62 -0
  164. package/src/lib/background-agent-phase.js +100 -0
  165. package/src/lib/background-agent-supervisor.js +320 -8
  166. package/src/lib/backpressure-policy.js +105 -0
  167. package/src/lib/capability-negotiation.js +212 -0
  168. package/src/lib/credential-guard.js +11 -0
  169. package/src/lib/credential-proxy.js +182 -0
  170. package/src/lib/dependency-install-policy.js +130 -0
  171. package/src/lib/event-seq-replay.js +167 -0
  172. package/src/lib/exit-codes.cjs +77 -0
  173. package/src/lib/goal-condition-engine.js +369 -0
  174. package/src/lib/headless-manifest.js +188 -0
  175. package/src/lib/hook-runner.cjs +70 -4
  176. package/src/lib/ide-context-redaction.js +130 -0
  177. package/src/lib/ide-context.js +128 -13
  178. package/src/lib/jsonl-session-store.js +2 -0
  179. package/src/lib/llm-config-defaults.js +24 -8
  180. package/src/lib/project-instructions.js +98 -1
  181. package/src/lib/project-mcp-trust.js +87 -0
  182. package/src/lib/remote-path-mapping.js +229 -0
  183. package/src/lib/schedule-planner.js +136 -0
  184. package/src/lib/sensitive-file-guard.js +51 -0
  185. package/src/lib/sub-agent-context.js +7 -3
  186. package/src/repl/bg-dashboard.js +13 -12
  187. package/src/runtime/agent-core.js +113 -10
  188. package/src/runtime/coding-agent-contract-shared.cjs +19 -2
  189. package/src/runtime/headless-runner.js +66 -7
  190. package/src/runtime/headless-stream.js +224 -12
  191. package/src/runtime/mcp-config.js +32 -1
  192. package/src/runtime/system-prompt.js +3 -0
  193. package/src/workers/background-agent-worker.js +37 -2
  194. package/src/assets/web-panel/assets/MarkdownRenderer-DexLlXfN.js +0 -1
  195. package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +0 -1
  196. package/src/assets/web-panel/assets/Projects-DvH_P2Uq.js +0 -1
  197. package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +0 -1
  198. package/src/assets/web-panel/assets/index-BhXiaiev.js +0 -1
  199. package/src/assets/web-panel/assets/index-WpaYaE3D.js +0 -1
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Event-stream gap-detection + replay primitives — the server→client (event)
3
+ * counterpart to the client→server RemoteCommandLedger (harness/
4
+ * remote-command-ledger.js). Both give a reconnecting peer an at-most-once,
5
+ * totally-ordered view over a lossy transport; the ledger orders *commands*,
6
+ * this orders *events*.
7
+ *
8
+ * A producer keeps a bounded buffer of recently emitted, seq-stamped frames
9
+ * (EventReplayBuffer); a reconnecting consumer reports its last-seen seq and
10
+ * gets the tail re-sent, or is told to full-resync when the missed range has
11
+ * already been evicted. A consumer detects holes with SeqGapTracker.
12
+ *
13
+ * Pure + deterministic: no I/O, no timers, no globals. The `seq` it stamps is
14
+ * the same additive protocol-v1 notion used on the stream-json surface
15
+ * (agent-sdk docs/PROTOCOL.md §1.2.1) — 1-based, monotonic, per producer
16
+ * instance; consumers MUST tolerate its absence and MUST NOT require gap-free
17
+ * numbering across a producer restart (a fresh instance rewinds to 1).
18
+ */
19
+
20
+ const DEFAULT_MAX_EVENTS = 512;
21
+ const DEFAULT_MAX_BYTES = 1 << 20; // 1 MiB — bound memory on a chatty session
22
+
23
+ /** Best-effort byte size of a frame (overridable for deterministic tests). */
24
+ function approxBytes(frame) {
25
+ try {
26
+ return Buffer.byteLength(JSON.stringify(frame));
27
+ } catch {
28
+ return 0; // circular / unserializable — don't let sizing throw
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Producer-side bounded replay buffer. `record(frame)` stamps the next `seq`
34
+ * onto a shallow copy, retains it (evicting the oldest beyond count/byte
35
+ * bounds), and returns the stamped frame ready to send. `replaySince(cursor)`
36
+ * returns the frames a consumer with last-seen seq `cursor` still needs.
37
+ */
38
+ export class EventReplayBuffer {
39
+ constructor({
40
+ maxEvents = DEFAULT_MAX_EVENTS,
41
+ maxBytes = DEFAULT_MAX_BYTES,
42
+ sizeOf,
43
+ } = {}) {
44
+ this._max = Math.max(1, Math.floor(maxEvents) || DEFAULT_MAX_EVENTS);
45
+ this._maxBytes = Math.max(0, Math.floor(maxBytes) || 0);
46
+ this._sizeOf = typeof sizeOf === "function" ? sizeOf : approxBytes;
47
+ this._buf = []; // [{ seq, frame, bytes }] oldest → newest
48
+ this._seq = 0;
49
+ this._bytes = 0;
50
+ }
51
+
52
+ /** Highest seq stamped so far (0 before anything is recorded). */
53
+ get latestSeq() {
54
+ return this._seq;
55
+ }
56
+
57
+ /** Oldest seq still retained (== latestSeq when the buffer is empty). */
58
+ get oldestSeq() {
59
+ return this._buf.length ? this._buf[0].seq : this._seq;
60
+ }
61
+
62
+ /** Number of frames currently retained. */
63
+ get size() {
64
+ return this._buf.length;
65
+ }
66
+
67
+ /**
68
+ * Stamp the next seq onto `frame` (shallow copy), retain it, evict overflow.
69
+ * @returns the stamped frame (`{ ...frame, seq }`) to hand straight to send.
70
+ */
71
+ record(frame) {
72
+ const seq = ++this._seq;
73
+ const stamped = { ...frame, seq };
74
+ const bytes = this._sizeOf(stamped) || 0;
75
+ this._buf.push({ seq, frame: stamped, bytes });
76
+ this._bytes += bytes;
77
+ this._evict();
78
+ return stamped;
79
+ }
80
+
81
+ _evict() {
82
+ // Always keep at least the newest frame even if it alone exceeds maxBytes.
83
+ while (
84
+ this._buf.length > this._max ||
85
+ (this._maxBytes > 0 &&
86
+ this._bytes > this._maxBytes &&
87
+ this._buf.length > 1)
88
+ ) {
89
+ const dropped = this._buf.shift();
90
+ this._bytes -= dropped.bytes;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Frames a consumer whose last-seen seq is `cursor` still needs.
96
+ * - `cursor >= latestSeq` → nothing missed → `{ frames: [], truncated: false }`.
97
+ * - `cursor` still within the retained window → the exact tail after it.
98
+ * - `cursor` older than what's retained → the gap was evicted →
99
+ * `truncated: true` (consumer MUST full-resync; the returned frames are
100
+ * only the retained suffix, not the whole hole).
101
+ * @param {number} cursor last seq the consumer acknowledged (default 0 = all)
102
+ */
103
+ replaySince(cursor = 0) {
104
+ const c = Number.isFinite(cursor) ? cursor : 0;
105
+ if (c >= this._seq) {
106
+ return { frames: [], truncated: false, from: c, to: this._seq };
107
+ }
108
+ // The lowest cursor we can honor without a hole is (oldest retained − 1).
109
+ const floor = this._buf.length ? this._buf[0].seq - 1 : this._seq;
110
+ const truncated = c < floor;
111
+ const frames = this._buf.filter((e) => e.seq > c).map((e) => e.frame);
112
+ return { frames, truncated, from: c, to: this._seq };
113
+ }
114
+ }
115
+
116
+ const OK = Object.freeze({ status: "ok" });
117
+
118
+ /**
119
+ * Consumer-side sequence tracker. Feed it each received frame's `seq`; it
120
+ * reports contiguity so the consumer can request a replay on a hole and drop
121
+ * duplicates a reconnect re-sent. One tracker per producer epoch — reset it
122
+ * when the transport hands you a fresh producer instance (e.g. a non-reattach
123
+ * bg-attach reply), since a fresh producer rewinds seq to 1.
124
+ */
125
+ export class SeqGapTracker {
126
+ constructor() {
127
+ this._last = 0;
128
+ this._started = false;
129
+ }
130
+
131
+ /** Last contiguous seq accepted (0 before the first frame). */
132
+ get lastSeq() {
133
+ return this._last;
134
+ }
135
+
136
+ /**
137
+ * @param {number} seq the received frame's seq (absent/NaN on older
138
+ * producers that don't stamp — treated as always-ok, never a gap).
139
+ * @returns {{status:"ok"}|{status:"duplicate"}|{status:"gap",from:number,to:number}}
140
+ * - `ok`: contiguous (or the first frame, or an unstamped frame).
141
+ * - `duplicate`: `seq <= lastSeq` — already seen; drop it.
142
+ * - `gap`: `seq > lastSeq + 1` — frames `(from, to)` were missed; ask the
143
+ * producer for `replaySince(from)`. The tracker adopts `to` as current.
144
+ */
145
+ observe(seq) {
146
+ if (!Number.isFinite(seq)) return OK; // unstamped producer — tolerate
147
+ if (!this._started) {
148
+ this._started = true;
149
+ this._last = seq;
150
+ return OK;
151
+ }
152
+ if (seq === this._last + 1) {
153
+ this._last = seq;
154
+ return OK;
155
+ }
156
+ if (seq <= this._last) return { status: "duplicate" };
157
+ const from = this._last;
158
+ this._last = seq; // adopt new position; caller replays the (from, to) hole
159
+ return { status: "gap", from, to: seq };
160
+ }
161
+
162
+ /** Forget all history (call when attaching to a fresh producer instance). */
163
+ reset() {
164
+ this._last = 0;
165
+ this._started = false;
166
+ }
167
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Headless exit-code taxonomy (gap-analysis 2026-07-11, P0 "确定性 Headless"):
3
+ * scripted callers can tell apart WHY a run ended without parsing stderr.
4
+ *
5
+ * Backward compatibility: every failure is still non-zero, 0 stays success,
6
+ * 2 stays "settings hook blocked the prompt" (pre-existing), 130/143 stay the
7
+ * signal-interrupt codes. The new codes carve budget/turn exhaustion and
8
+ * config errors out of the generic 1.
9
+ */
10
+
11
+ const HEADLESS_EXIT_CODES = Object.freeze({
12
+ OK: 0,
13
+ /** generic failure (tool/loop/unclassified error) */
14
+ ERROR: 1,
15
+ /** a settings hook blocked the prompt (pre-existing semantic) */
16
+ HOOK_BLOCKED: 2,
17
+ /** the turn budget (--max-turns / loop iteration cap) ran out first */
18
+ MAX_TURNS: 3,
19
+ /** the --max-budget-usd cost cap stopped the run */
20
+ MAX_BUDGET: 4,
21
+ /** the LLM call itself failed (provider/model/network error) */
22
+ MODEL_ERROR: 5,
23
+ /** bad run configuration (--mcp-config / --permission-prompt-tool / settings) */
24
+ CONFIG_ERROR: 6,
25
+ /** user interrupt (SIGINT) — 128+2, shell convention */
26
+ INTERRUPT_SIGINT: 130,
27
+ /** termination (SIGTERM) — 128+15, shell convention */
28
+ INTERRUPT_SIGTERM: 143,
29
+ });
30
+
31
+ /**
32
+ * Best-effort classifier for errors thrown out of the agent loop: provider /
33
+ * transport failures → MODEL_ERROR, everything else → generic ERROR. Checks
34
+ * structural markers (HTTP status, network error codes) before falling back —
35
+ * never message-text guessing beyond the explicit LLM markers.
36
+ */
37
+ function classifyLoopError(err) {
38
+ if (!err) return HEADLESS_EXIT_CODES.ERROR;
39
+ const status = err.status ?? err.statusCode ?? err.response?.status;
40
+ if (Number.isFinite(Number(status))) return HEADLESS_EXIT_CODES.MODEL_ERROR;
41
+ const code = err.code || err.cause?.code;
42
+ if (
43
+ typeof code === "string" &&
44
+ /^(ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|UND_ERR)/.test(
45
+ code,
46
+ )
47
+ ) {
48
+ return HEADLESS_EXIT_CODES.MODEL_ERROR;
49
+ }
50
+ if (typeof err.name === "string" && /LLM|Provider|Api/i.test(err.name)) {
51
+ return HEADLESS_EXIT_CODES.MODEL_ERROR;
52
+ }
53
+ return HEADLESS_EXIT_CODES.ERROR;
54
+ }
55
+
56
+ /**
57
+ * Map a finished run's endReason (+ error flag) to an exit code.
58
+ * @param {string} endReason "complete" | "max_turns" | "budget-exhausted" |
59
+ * "cost-budget-exhausted" | "no-response" | ...
60
+ * @param {boolean} isError
61
+ */
62
+ function exitCodeForEndReason(endReason, isError) {
63
+ if (!isError) return HEADLESS_EXIT_CODES.OK;
64
+ if (endReason === "max_turns" || endReason === "budget-exhausted") {
65
+ return HEADLESS_EXIT_CODES.MAX_TURNS;
66
+ }
67
+ if (endReason === "cost-budget-exhausted") {
68
+ return HEADLESS_EXIT_CODES.MAX_BUDGET;
69
+ }
70
+ return HEADLESS_EXIT_CODES.ERROR;
71
+ }
72
+
73
+ module.exports = {
74
+ HEADLESS_EXIT_CODES,
75
+ classifyLoopError,
76
+ exitCodeForEndReason,
77
+ };
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Session-level completion-condition engine (P1 §"会话级完成条件引擎").
3
+ *
4
+ * Distinct from `cc goal` (long-term, cross-session OKR progress) and from
5
+ * `cc loop` (a timer that re-runs ONE command): this drives the agent's OUTER
6
+ * turns. After each outer turn an independent evaluator judges a completion
7
+ * CONDITION; if unmet, the runner starts the next outer turn — until the
8
+ * condition is met (`completed`) or a budget is exhausted (`exhausted`).
9
+ *
10
+ * The engine is a pure reducer over usage + per-turn evaluations, so it is
11
+ * deterministically testable without a model or the real agent loop. It:
12
+ * - supports model-judged conditions AND deterministic checkers
13
+ * (exit-zero / file-exists / contains / regex),
14
+ * - enforces max_outer_turns and token / cost / time budgets,
15
+ * - emits the SDK event taxonomy (goal_started / goal_evaluated /
16
+ * goal_completed / goal_exhausted) with structured reason, evidence and
17
+ * turn/token/cost/time, and
18
+ * - snapshots/restores so a condition survives `--resume`.
19
+ *
20
+ * The clock is injected (`now`) and NEVER read implicitly, so snapshots replay
21
+ * identically and tests are deterministic.
22
+ */
23
+
24
+ /** SDK event taxonomy for the completion-condition loop. */
25
+ export const GOAL_EVENTS = Object.freeze({
26
+ STARTED: "goal_started",
27
+ EVALUATED: "goal_evaluated",
28
+ COMPLETED: "goal_completed",
29
+ EXHAUSTED: "goal_exhausted",
30
+ });
31
+
32
+ /** Terminal decisions the reducer can return alongside `continue`. */
33
+ export const GOAL_DECISION = Object.freeze({
34
+ CONTINUE: "continue",
35
+ COMPLETE: "complete",
36
+ EXHAUSTED: "exhausted",
37
+ });
38
+
39
+ /** Default budget — a bounded outer-turn count; other axes unbounded (null). */
40
+ export const DEFAULT_GOAL_BUDGET = Object.freeze({
41
+ maxOuterTurns: 10,
42
+ maxTokens: null,
43
+ maxCostUsd: null,
44
+ maxTimeMs: null,
45
+ });
46
+
47
+ // A hard ceiling so a misconfigured condition can never spin forever.
48
+ const OUTER_TURN_HARD_CAP = 100;
49
+
50
+ function positiveIntOrNull(value, cap) {
51
+ const n = Number(value);
52
+ if (!Number.isFinite(n) || n <= 0) return null;
53
+ const floored = Math.floor(n);
54
+ return cap ? Math.min(floored, cap) : floored;
55
+ }
56
+
57
+ function positiveNumOrNull(value) {
58
+ const n = Number(value);
59
+ return Number.isFinite(n) && n > 0 ? n : null;
60
+ }
61
+
62
+ /** Normalize a budget: sane defaults, positive values, hard outer-turn cap. */
63
+ export function normalizeGoalBudget(input = {}) {
64
+ const maxOuterTurns =
65
+ positiveIntOrNull(input.maxOuterTurns, OUTER_TURN_HARD_CAP) ??
66
+ DEFAULT_GOAL_BUDGET.maxOuterTurns;
67
+ return {
68
+ maxOuterTurns,
69
+ maxTokens: positiveIntOrNull(input.maxTokens),
70
+ maxCostUsd: positiveNumOrNull(input.maxCostUsd),
71
+ maxTimeMs: positiveIntOrNull(input.maxTimeMs),
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Parse a `--goal-condition` spec into a condition descriptor. A `kind:` prefix
77
+ * selects a deterministic checker; anything else (or an explicit `model:`) is
78
+ * model-judged natural language.
79
+ *
80
+ * exit-zero:npm test → run the command, met iff exit 0
81
+ * file-exists:dist/app.js → met iff the path exists
82
+ * contains:ALL GREEN → met iff the last output contains the text
83
+ * regex:^BUILD OK$ → met iff the last output matches
84
+ * model:the tests pass → a model judges the transcript
85
+ * the tests pass → (bare) model-judged
86
+ */
87
+ export function parseGoalCondition(spec) {
88
+ const raw = String(spec ?? "").trim();
89
+ if (!raw) throw new Error("goal condition is required");
90
+ const idx = raw.indexOf(":");
91
+ const prefix = idx === -1 ? "" : raw.slice(0, idx).trim().toLowerCase();
92
+ const rest = idx === -1 ? "" : raw.slice(idx + 1).trim();
93
+ switch (prefix) {
94
+ case "exit-zero":
95
+ case "exit0":
96
+ if (!rest) throw new Error("exit-zero condition needs a command");
97
+ return { kind: "exit-zero", command: rest, source: raw };
98
+ case "file-exists":
99
+ case "file":
100
+ if (!rest) throw new Error("file-exists condition needs a path");
101
+ return { kind: "file-exists", path: rest, source: raw };
102
+ case "contains":
103
+ if (!rest) throw new Error("contains condition needs text");
104
+ return { kind: "contains", text: rest, source: raw };
105
+ case "regex":
106
+ if (!rest) throw new Error("regex condition needs a pattern");
107
+ return { kind: "regex", pattern: rest, source: raw };
108
+ case "model":
109
+ if (!rest) throw new Error("model condition needs a description");
110
+ return { kind: "model", text: rest, source: raw };
111
+ default:
112
+ return { kind: "model", text: raw, source: raw };
113
+ }
114
+ }
115
+
116
+ /** True when a condition is a deterministic checker (not model-judged). */
117
+ export function isDeterministicCondition(condition) {
118
+ return Boolean(condition) && condition.kind !== "model";
119
+ }
120
+
121
+ /**
122
+ * Evaluate a deterministic condition against gathered evidence — PURE.
123
+ * `evidence` supplies `output` (last turn text), `exitCode`, and a
124
+ * `fileExists(path) => boolean` predicate. Returns `{met, reason, evidence}`.
125
+ */
126
+ export function checkDeterministicCondition(condition, evidence = {}) {
127
+ const output = String(evidence.output ?? "");
128
+ switch (condition?.kind) {
129
+ case "contains": {
130
+ const met = output.includes(condition.text);
131
+ return {
132
+ met,
133
+ reason: met
134
+ ? `output contains "${condition.text}"`
135
+ : `output does not contain "${condition.text}"`,
136
+ evidence: { kind: "contains", matched: met },
137
+ };
138
+ }
139
+ case "regex": {
140
+ let re;
141
+ try {
142
+ re = new RegExp(condition.pattern, condition.flags || "m");
143
+ } catch (err) {
144
+ return {
145
+ met: false,
146
+ reason: `invalid regex: ${err.message}`,
147
+ evidence: { kind: "regex", error: true },
148
+ };
149
+ }
150
+ const met = re.test(output);
151
+ return {
152
+ met,
153
+ reason: met
154
+ ? `output matches /${condition.pattern}/`
155
+ : `output does not match /${condition.pattern}/`,
156
+ evidence: { kind: "regex", matched: met },
157
+ };
158
+ }
159
+ case "exit-zero": {
160
+ const met = Number(evidence.exitCode) === 0;
161
+ return {
162
+ met,
163
+ reason: `\`${condition.command}\` exited ${evidence.exitCode}`,
164
+ evidence: { kind: "exit-zero", exitCode: evidence.exitCode ?? null },
165
+ };
166
+ }
167
+ case "file-exists": {
168
+ const fx = typeof evidence.fileExists === "function";
169
+ const met = fx ? evidence.fileExists(condition.path) === true : false;
170
+ return {
171
+ met,
172
+ reason: met
173
+ ? `${condition.path} exists`
174
+ : `${condition.path} does not exist`,
175
+ evidence: { kind: "file-exists", path: condition.path, met },
176
+ };
177
+ }
178
+ default:
179
+ return {
180
+ met: false,
181
+ reason: `not a deterministic condition (${condition?.kind})`,
182
+ evidence: { kind: condition?.kind ?? "unknown" },
183
+ };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Gather evidence and run a deterministic check. Deps (`spawnSync`,
189
+ * `existsSync`) are injected for tests; `lastOutput` feeds contains/regex.
190
+ */
191
+ export function runDeterministicCheck(condition, deps = {}) {
192
+ const evidence = { output: deps.lastOutput ?? "" };
193
+ if (condition.kind === "exit-zero") {
194
+ const spawnSync = deps.spawnSync;
195
+ if (typeof spawnSync !== "function") {
196
+ return {
197
+ met: false,
198
+ reason: "no spawnSync available for exit-zero",
199
+ evidence: { kind: "exit-zero", error: true },
200
+ };
201
+ }
202
+ const res = spawnSync(condition.command, {
203
+ cwd: deps.cwd,
204
+ shell: true,
205
+ encoding: "utf8",
206
+ windowsHide: true,
207
+ });
208
+ evidence.exitCode = res?.status ?? (res?.error ? 1 : null);
209
+ } else if (condition.kind === "file-exists") {
210
+ const existsSync = deps.existsSync;
211
+ evidence.fileExists = (p) =>
212
+ typeof existsSync === "function" ? existsSync(p) === true : false;
213
+ }
214
+ return checkDeterministicCondition(condition, evidence);
215
+ }
216
+
217
+ function usageSnapshot(state, nowMs) {
218
+ return {
219
+ outerTurns: state.outerTurns,
220
+ tokens: state.tokens,
221
+ costUsd: state.costUsd,
222
+ elapsedMs: Math.max(0, nowMs - state.startedAtMs),
223
+ };
224
+ }
225
+
226
+ function whichLimitHit(state, budget, nowMs) {
227
+ if (budget.maxOuterTurns != null && state.outerTurns >= budget.maxOuterTurns)
228
+ return "max_outer_turns";
229
+ if (budget.maxTokens != null && state.tokens >= budget.maxTokens)
230
+ return "max_tokens";
231
+ if (budget.maxCostUsd != null && state.costUsd >= budget.maxCostUsd)
232
+ return "max_cost";
233
+ if (
234
+ budget.maxTimeMs != null &&
235
+ Math.max(0, nowMs - state.startedAtMs) >= budget.maxTimeMs
236
+ )
237
+ return "max_time";
238
+ return null;
239
+ }
240
+
241
+ /**
242
+ * The pure decision step. Given the accumulated `state`, this turn's
243
+ * `evaluation` ({met, reason, evidence}), the `budget`, and the current time,
244
+ * decide `complete | continue | exhausted` and produce events. Always emits a
245
+ * `goal_evaluated`; adds a terminal `goal_completed` / `goal_exhausted` when
246
+ * the loop stops. Does NOT mutate `state` — returns the next one.
247
+ */
248
+ export function evaluateGoalStep(state, evaluation, budget, opts = {}) {
249
+ const nowMs = Number(opts.now ?? state.startedAtMs);
250
+ const next = {
251
+ ...state,
252
+ outerTurns: state.outerTurns + 1,
253
+ };
254
+ const usage = usageSnapshot(next, nowMs);
255
+ const met = evaluation?.met === true;
256
+ const evaluated = {
257
+ type: GOAL_EVENTS.EVALUATED,
258
+ met,
259
+ reason: evaluation?.reason ?? null,
260
+ evidence: evaluation?.evidence ?? null,
261
+ ...usage,
262
+ };
263
+ if (met) {
264
+ return {
265
+ state: { ...next, done: true, outcome: "completed" },
266
+ decision: GOAL_DECISION.COMPLETE,
267
+ events: [
268
+ evaluated,
269
+ {
270
+ type: GOAL_EVENTS.COMPLETED,
271
+ reason: evaluation?.reason ?? "condition met",
272
+ evidence: evaluation?.evidence ?? null,
273
+ ...usage,
274
+ },
275
+ ],
276
+ };
277
+ }
278
+ const limit = whichLimitHit(next, budget, nowMs);
279
+ if (limit) {
280
+ return {
281
+ state: { ...next, done: true, outcome: "exhausted", limit },
282
+ decision: GOAL_DECISION.EXHAUSTED,
283
+ events: [
284
+ evaluated,
285
+ {
286
+ type: GOAL_EVENTS.EXHAUSTED,
287
+ limit,
288
+ reason: `budget exhausted (${limit})`,
289
+ ...usage,
290
+ },
291
+ ],
292
+ };
293
+ }
294
+ return { state: next, decision: GOAL_DECISION.CONTINUE, events: [evaluated] };
295
+ }
296
+
297
+ /**
298
+ * Stateful wrapper over the reducer for the runner. Inject `now` (a `() => ms`
299
+ * clock) so behavior is deterministic and snapshot-replayable.
300
+ */
301
+ export class GoalConditionEngine {
302
+ constructor({ condition, budget, now } = {}) {
303
+ this.condition =
304
+ typeof condition === "string" ? parseGoalCondition(condition) : condition;
305
+ this.budget = normalizeGoalBudget(budget);
306
+ this._now = typeof now === "function" ? now : () => 0;
307
+ this.state = {
308
+ outerTurns: 0,
309
+ tokens: 0,
310
+ costUsd: 0,
311
+ startedAtMs: this._now(),
312
+ done: false,
313
+ outcome: null,
314
+ };
315
+ }
316
+
317
+ /** Emit the opening event; call once before the first outer turn. */
318
+ start() {
319
+ return {
320
+ type: GOAL_EVENTS.STARTED,
321
+ condition: this.condition,
322
+ budget: this.budget,
323
+ startedAtMs: this.state.startedAtMs,
324
+ };
325
+ }
326
+
327
+ /** Accumulate a turn's usage before evaluating it. */
328
+ recordTurnUsage({ tokens = 0, costUsd = 0 } = {}) {
329
+ this.state = {
330
+ ...this.state,
331
+ tokens: this.state.tokens + (Number(tokens) || 0),
332
+ costUsd: this.state.costUsd + (Number(costUsd) || 0),
333
+ };
334
+ return this.state;
335
+ }
336
+
337
+ /** Evaluate this outer turn; returns `{decision, events}`. */
338
+ evaluate(evaluation) {
339
+ const result = evaluateGoalStep(this.state, evaluation, this.budget, {
340
+ now: this._now(),
341
+ });
342
+ this.state = result.state;
343
+ return { decision: result.decision, events: result.events };
344
+ }
345
+
346
+ get done() {
347
+ return this.state.done === true;
348
+ }
349
+
350
+ /** Serialize for `--resume`. startedAtMs is preserved so time keeps counting. */
351
+ snapshot() {
352
+ return {
353
+ condition: this.condition,
354
+ budget: this.budget,
355
+ state: { ...this.state },
356
+ };
357
+ }
358
+
359
+ /** Restore a prior run (same clock injection contract). */
360
+ static fromSnapshot(snap, { now } = {}) {
361
+ const engine = new GoalConditionEngine({
362
+ condition: snap.condition,
363
+ budget: snap.budget,
364
+ now,
365
+ });
366
+ engine.state = { ...snap.state };
367
+ return engine;
368
+ }
369
+ }