@xyagent/cli 0.0.1

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 (385) hide show
  1. package/README.md +860 -0
  2. package/bin/agentlink +8585 -0
  3. package/bin/agentlink-agent +7 -0
  4. package/bin/agentlink-mcp-stdio +173 -0
  5. package/docs/LOGGING.md +159 -0
  6. package/package.json +38 -0
  7. package/src/core.mjs +320 -0
  8. package/src/tunnel.mjs +291 -0
  9. package/src/tunnel_proxy.mjs +913 -0
  10. package/src/tunnel_service.mjs +699 -0
  11. package/src/tunnel_state.mjs +301 -0
  12. package/src/unified_producer.mjs +259 -0
  13. package/src-ext/README.md +75 -0
  14. package/src-ext/bin.mjs +128 -0
  15. package/src-ext/commands/agent.mjs +66 -0
  16. package/src-ext/commands/help.mjs +31 -0
  17. package/src-ext/commands/pair.mjs +455 -0
  18. package/src-ext/commands/reload.mjs +78 -0
  19. package/src-ext/commands/service.mjs +64 -0
  20. package/src-ext/core/accountResolver.mjs +137 -0
  21. package/src-ext/core/activeRuns.mjs +76 -0
  22. package/src-ext/core/agentlinkToolsBootstrap.mjs +75 -0
  23. package/src-ext/core/agentlinkToolsRegistry.mjs +323 -0
  24. package/src-ext/core/agentlinkToolsServer.mjs +109 -0
  25. package/src-ext/core/approvalGateway.mjs +132 -0
  26. package/src-ext/core/attachAuditLog.mjs +79 -0
  27. package/src-ext/core/attachDenylist.mjs +128 -0
  28. package/src-ext/core/autoTunnelDetector.mjs +98 -0
  29. package/src-ext/core/autoTunnelGateway.mjs +393 -0
  30. package/src-ext/core/autoTunnelWiring.mjs +184 -0
  31. package/src-ext/core/boundedMap.mjs +98 -0
  32. package/src-ext/core/bridgeAttachAutoScanner.mjs +237 -0
  33. package/src-ext/core/bridgeHooks.mjs +168 -0
  34. package/src-ext/core/bridgeSelfUninstall.mjs +133 -0
  35. package/src-ext/core/claudeTrust.mjs +74 -0
  36. package/src-ext/core/codexTrust.mjs +77 -0
  37. package/src-ext/core/cosUploadClient.mjs +326 -0
  38. package/src-ext/core/createWorkspace.mjs +170 -0
  39. package/src-ext/core/daemonRestart.mjs +49 -0
  40. package/src-ext/core/defaultWorkspace.mjs +65 -0
  41. package/src-ext/core/ensureGitRepo.mjs +57 -0
  42. package/src-ext/core/hermesProfile.mjs +75 -0
  43. package/src-ext/core/interruptGateway.mjs +119 -0
  44. package/src-ext/core/interruptRunListener.mjs +106 -0
  45. package/src-ext/core/mcpRuntimeFanout.mjs +236 -0
  46. package/src-ext/core/mcpStdioClient.mjs +316 -0
  47. package/src-ext/core/openclawGatewayClient.mjs +230 -0
  48. package/src-ext/core/pairCodeClient.mjs +331 -0
  49. package/src-ext/core/pairInventory.mjs +59 -0
  50. package/src-ext/core/pathExpand.mjs +34 -0
  51. package/src-ext/core/preprocessAttachments.mjs +113 -0
  52. package/src-ext/core/relayWorker.mjs +284 -0
  53. package/src-ext/core/relayWorkerHttp.mjs +58 -0
  54. package/src-ext/core/scanWorkspaces.mjs +842 -0
  55. package/src-ext/core/sessionSlot.mjs +70 -0
  56. package/src-ext/core/stateMachine.mjs +85 -0
  57. package/src-ext/core/toolsServerLifecycle.mjs +169 -0
  58. package/src-ext/core/unifiedDispatchHandler.mjs +439 -0
  59. package/src-ext/core/usageReporter.mjs +207 -0
  60. package/src-ext/openclaw-plugin/envelope-builder.cjs +619 -0
  61. package/src-ext/openclaw-plugin/index.cjs +1919 -0
  62. package/src-ext/openclaw-plugin/openclaw.plugin.json +20 -0
  63. package/src-ext/openclaw-plugin/package.json +12 -0
  64. package/src-ext/openclaw-plugin/relay-defaults.json +3 -0
  65. package/src-ext/openclaw-plugin/todoTranslatorUtils.cjs +101 -0
  66. package/src-ext/runtime/_shared/cliExec.mjs +138 -0
  67. package/src-ext/runtime/_shared/cosTextRefs.mjs +120 -0
  68. package/src-ext/runtime/_shared/fetchAndBase64Encode.mjs +52 -0
  69. package/src-ext/runtime/_shared/relayObjectToBlock.mjs +307 -0
  70. package/src-ext/runtime/_shared/sessionPicker.mjs +418 -0
  71. package/src-ext/runtime/_shared/slashCommandRouter.mjs +1346 -0
  72. package/src-ext/runtime/_shared/todoTranslatorUtils.mjs +94 -0
  73. package/src-ext/runtime/claude/askUserQuestionTranslator.mjs +241 -0
  74. package/src-ext/runtime/claude/customCommandsCollector.mjs +132 -0
  75. package/src-ext/runtime/claude/handleRequest.mjs +1418 -0
  76. package/src-ext/runtime/claude/index.mjs +160 -0
  77. package/src-ext/runtime/claude/internalCommandNames.mjs +58 -0
  78. package/src-ext/runtime/claude/launcher.mjs +313 -0
  79. package/src-ext/runtime/claude/mcpConfigAdapter.mjs +174 -0
  80. package/src-ext/runtime/claude/mcpConfigWriter.mjs +57 -0
  81. package/src-ext/runtime/claude/modelContextWindow.mjs +30 -0
  82. package/src-ext/runtime/claude/modelScanner.mjs +257 -0
  83. package/src-ext/runtime/claude/permissionInterruptBlock.mjs +86 -0
  84. package/src-ext/runtime/claude/permissionPromptServer.mjs +170 -0
  85. package/src-ext/runtime/claude/permissionPromptServerWorker.mjs +214 -0
  86. package/src-ext/runtime/claude/preflight.mjs +62 -0
  87. package/src-ext/runtime/claude/stdoutParser.mjs +365 -0
  88. package/src-ext/runtime/claude/todoTranslator.mjs +66 -0
  89. package/src-ext/runtime/claude/usageProbe.mjs +576 -0
  90. package/src-ext/runtime/claude/usageProbeStrip.mjs +14 -0
  91. package/src-ext/runtime/codex/codexCustomCommandsCollector.mjs +87 -0
  92. package/src-ext/runtime/codex/elicitationHandler.mjs +91 -0
  93. package/src-ext/runtime/codex/eventMapper.mjs +154 -0
  94. package/src-ext/runtime/codex/handleRequest.mjs +1152 -0
  95. package/src-ext/runtime/codex/index.mjs +142 -0
  96. package/src-ext/runtime/codex/mcpConfigAdapter.mjs +155 -0
  97. package/src-ext/runtime/codex/modelScanner.mjs +305 -0
  98. package/src-ext/runtime/codex/planNotificationHandler.mjs +61 -0
  99. package/src-ext/runtime/codex/preflight.mjs +67 -0
  100. package/src-ext/runtime/hermes/commandBackend.mjs +117 -0
  101. package/src-ext/runtime/hermes/envSetup.mjs +229 -0
  102. package/src-ext/runtime/hermes/gatewayManager.mjs +625 -0
  103. package/src-ext/runtime/hermes/handleRequest.mjs +1257 -0
  104. package/src-ext/runtime/hermes/httpBackend.mjs +219 -0
  105. package/src-ext/runtime/hermes/index.mjs +196 -0
  106. package/src-ext/runtime/hermes/mcpConfigAdapter.mjs +189 -0
  107. package/src-ext/runtime/hermes/preflight.mjs +359 -0
  108. package/src-ext/runtime/hermes/responsesBackend.mjs +276 -0
  109. package/src-ext/runtime/hermes/sessionStore.mjs +78 -0
  110. package/src-ext/runtime/hermes/todoTranslator.mjs +90 -0
  111. package/src-ext/runtime/hermes/workspaceRpc.mjs +124 -0
  112. package/src-ext/runtime/openclaw/buildOpenclawDaemonInput.mjs +143 -0
  113. package/src-ext/runtime/openclaw/mcpConfigAdapter.mjs +91 -0
  114. package/src-ext/runtime/openclaw/openclawConfig.mjs +145 -0
  115. package/src-ext/runtime/picoclaw/constants.mjs +39 -0
  116. package/src-ext/runtime/picoclaw/handleRequest.mjs +289 -0
  117. package/src-ext/runtime/picoclaw/index.mjs +314 -0
  118. package/src-ext/runtime/picoclaw/pairFlow.mjs +224 -0
  119. package/src-ext/runtime/picoclaw/state.mjs +78 -0
  120. package/src-ext/runtime/picoclaw/todoTranslator.mjs +67 -0
  121. package/src-ext/runtime/picoclaw/translator.mjs +272 -0
  122. package/src-ext/runtime/picoclaw/wsClient.mjs +129 -0
  123. package/src-ext/service/serviceManager.mjs +521 -0
  124. package/src-shared/README.md +44 -0
  125. package/src-shared/chat_push_normalizer.mjs +113 -0
  126. package/src-shared/correlation.mjs +25 -0
  127. package/src-shared/envelope_builder.mjs +612 -0
  128. package/src-shared/logger.mjs +167 -0
  129. package/src-shared/relay-defaults.json +3 -0
  130. package/src-shared/relay_revoke.mjs +46 -0
  131. package/src-shared/vendor/README.md +60 -0
  132. package/src-shared/vendor/smol-toml/LICENSE +24 -0
  133. package/src-shared/vendor/smol-toml/README.md +236 -0
  134. package/src-shared/vendor/smol-toml/VENDORED.md +37 -0
  135. package/src-shared/vendor/smol-toml/dist/date.d.ts +41 -0
  136. package/src-shared/vendor/smol-toml/dist/date.js +127 -0
  137. package/src-shared/vendor/smol-toml/dist/error.d.ts +38 -0
  138. package/src-shared/vendor/smol-toml/dist/error.js +63 -0
  139. package/src-shared/vendor/smol-toml/dist/extract.d.ts +30 -0
  140. package/src-shared/vendor/smol-toml/dist/extract.js +109 -0
  141. package/src-shared/vendor/smol-toml/dist/index.cjs +900 -0
  142. package/src-shared/vendor/smol-toml/dist/index.d.ts +43 -0
  143. package/src-shared/vendor/smol-toml/dist/index.js +33 -0
  144. package/src-shared/vendor/smol-toml/dist/parse.d.ts +37 -0
  145. package/src-shared/vendor/smol-toml/dist/parse.js +148 -0
  146. package/src-shared/vendor/smol-toml/dist/primitive.d.ts +31 -0
  147. package/src-shared/vendor/smol-toml/dist/primitive.js +178 -0
  148. package/src-shared/vendor/smol-toml/dist/stringify.d.ts +31 -0
  149. package/src-shared/vendor/smol-toml/dist/stringify.js +163 -0
  150. package/src-shared/vendor/smol-toml/dist/struct.d.ts +32 -0
  151. package/src-shared/vendor/smol-toml/dist/struct.js +194 -0
  152. package/src-shared/vendor/smol-toml/dist/util.d.ts +42 -0
  153. package/src-shared/vendor/smol-toml/dist/util.js +100 -0
  154. package/src-shared/vendor/smol-toml/package.json +54 -0
  155. package/src-shared/vendor/yaml/LICENSE +13 -0
  156. package/src-shared/vendor/yaml/README.md +155 -0
  157. package/src-shared/vendor/yaml/VENDORED.md +29 -0
  158. package/src-shared/vendor/yaml/bin.mjs +11 -0
  159. package/src-shared/vendor/yaml/browser/dist/compose/compose-collection.js +88 -0
  160. package/src-shared/vendor/yaml/browser/dist/compose/compose-doc.js +42 -0
  161. package/src-shared/vendor/yaml/browser/dist/compose/compose-node.js +92 -0
  162. package/src-shared/vendor/yaml/browser/dist/compose/compose-scalar.js +80 -0
  163. package/src-shared/vendor/yaml/browser/dist/compose/composer.js +217 -0
  164. package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-map.js +113 -0
  165. package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
  166. package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-seq.js +47 -0
  167. package/src-shared/vendor/yaml/browser/dist/compose/resolve-end.js +37 -0
  168. package/src-shared/vendor/yaml/browser/dist/compose/resolve-flow-collection.js +203 -0
  169. package/src-shared/vendor/yaml/browser/dist/compose/resolve-flow-scalar.js +223 -0
  170. package/src-shared/vendor/yaml/browser/dist/compose/resolve-props.js +148 -0
  171. package/src-shared/vendor/yaml/browser/dist/compose/util-contains-newline.js +34 -0
  172. package/src-shared/vendor/yaml/browser/dist/compose/util-empty-scalar-position.js +27 -0
  173. package/src-shared/vendor/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
  174. package/src-shared/vendor/yaml/browser/dist/compose/util-map-includes.js +17 -0
  175. package/src-shared/vendor/yaml/browser/dist/doc/Document.js +334 -0
  176. package/src-shared/vendor/yaml/browser/dist/doc/anchors.js +72 -0
  177. package/src-shared/vendor/yaml/browser/dist/doc/applyReviver.js +55 -0
  178. package/src-shared/vendor/yaml/browser/dist/doc/createNode.js +89 -0
  179. package/src-shared/vendor/yaml/browser/dist/doc/directives.js +176 -0
  180. package/src-shared/vendor/yaml/browser/dist/errors.js +57 -0
  181. package/src-shared/vendor/yaml/browser/dist/index.js +17 -0
  182. package/src-shared/vendor/yaml/browser/dist/log.js +14 -0
  183. package/src-shared/vendor/yaml/browser/dist/nodes/Alias.js +101 -0
  184. package/src-shared/vendor/yaml/browser/dist/nodes/Collection.js +147 -0
  185. package/src-shared/vendor/yaml/browser/dist/nodes/Node.js +38 -0
  186. package/src-shared/vendor/yaml/browser/dist/nodes/Pair.js +36 -0
  187. package/src-shared/vendor/yaml/browser/dist/nodes/Scalar.js +24 -0
  188. package/src-shared/vendor/yaml/browser/dist/nodes/YAMLMap.js +144 -0
  189. package/src-shared/vendor/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
  190. package/src-shared/vendor/yaml/browser/dist/nodes/addPairToJSMap.js +104 -0
  191. package/src-shared/vendor/yaml/browser/dist/nodes/identity.js +36 -0
  192. package/src-shared/vendor/yaml/browser/dist/nodes/toJS.js +37 -0
  193. package/src-shared/vendor/yaml/browser/dist/parse/cst-scalar.js +214 -0
  194. package/src-shared/vendor/yaml/browser/dist/parse/cst-stringify.js +61 -0
  195. package/src-shared/vendor/yaml/browser/dist/parse/cst-visit.js +97 -0
  196. package/src-shared/vendor/yaml/browser/dist/parse/cst.js +98 -0
  197. package/src-shared/vendor/yaml/browser/dist/parse/lexer.js +717 -0
  198. package/src-shared/vendor/yaml/browser/dist/parse/line-counter.js +39 -0
  199. package/src-shared/vendor/yaml/browser/dist/parse/parser.js +954 -0
  200. package/src-shared/vendor/yaml/browser/dist/public-api.js +99 -0
  201. package/src-shared/vendor/yaml/browser/dist/schema/Schema.js +38 -0
  202. package/src-shared/vendor/yaml/browser/dist/schema/common/map.js +17 -0
  203. package/src-shared/vendor/yaml/browser/dist/schema/common/null.js +15 -0
  204. package/src-shared/vendor/yaml/browser/dist/schema/common/seq.js +17 -0
  205. package/src-shared/vendor/yaml/browser/dist/schema/common/string.js +14 -0
  206. package/src-shared/vendor/yaml/browser/dist/schema/core/bool.js +19 -0
  207. package/src-shared/vendor/yaml/browser/dist/schema/core/float.js +43 -0
  208. package/src-shared/vendor/yaml/browser/dist/schema/core/int.js +38 -0
  209. package/src-shared/vendor/yaml/browser/dist/schema/core/schema.js +23 -0
  210. package/src-shared/vendor/yaml/browser/dist/schema/json/schema.js +62 -0
  211. package/src-shared/vendor/yaml/browser/dist/schema/tags.js +83 -0
  212. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/binary.js +66 -0
  213. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
  214. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
  215. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
  216. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
  217. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
  218. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/schema.js +37 -0
  219. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
  220. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
  221. package/src-shared/vendor/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
  222. package/src-shared/vendor/yaml/browser/dist/stringify/stringify.js +124 -0
  223. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyCollection.js +143 -0
  224. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyComment.js +20 -0
  225. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
  226. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyNumber.js +24 -0
  227. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyPair.js +150 -0
  228. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyString.js +328 -0
  229. package/src-shared/vendor/yaml/browser/dist/util.js +11 -0
  230. package/src-shared/vendor/yaml/browser/dist/visit.js +233 -0
  231. package/src-shared/vendor/yaml/browser/index.js +5 -0
  232. package/src-shared/vendor/yaml/browser/package.json +3 -0
  233. package/src-shared/vendor/yaml/dist/cli.d.ts +8 -0
  234. package/src-shared/vendor/yaml/dist/cli.mjs +199 -0
  235. package/src-shared/vendor/yaml/dist/compose/compose-collection.d.ts +11 -0
  236. package/src-shared/vendor/yaml/dist/compose/compose-collection.js +90 -0
  237. package/src-shared/vendor/yaml/dist/compose/compose-doc.d.ts +7 -0
  238. package/src-shared/vendor/yaml/dist/compose/compose-doc.js +44 -0
  239. package/src-shared/vendor/yaml/dist/compose/compose-node.d.ts +28 -0
  240. package/src-shared/vendor/yaml/dist/compose/compose-node.js +95 -0
  241. package/src-shared/vendor/yaml/dist/compose/compose-scalar.d.ts +5 -0
  242. package/src-shared/vendor/yaml/dist/compose/compose-scalar.js +82 -0
  243. package/src-shared/vendor/yaml/dist/compose/composer.d.ts +62 -0
  244. package/src-shared/vendor/yaml/dist/compose/composer.js +221 -0
  245. package/src-shared/vendor/yaml/dist/compose/resolve-block-map.d.ts +6 -0
  246. package/src-shared/vendor/yaml/dist/compose/resolve-block-map.js +115 -0
  247. package/src-shared/vendor/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
  248. package/src-shared/vendor/yaml/dist/compose/resolve-block-scalar.js +200 -0
  249. package/src-shared/vendor/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
  250. package/src-shared/vendor/yaml/dist/compose/resolve-block-seq.js +49 -0
  251. package/src-shared/vendor/yaml/dist/compose/resolve-end.d.ts +6 -0
  252. package/src-shared/vendor/yaml/dist/compose/resolve-end.js +39 -0
  253. package/src-shared/vendor/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
  254. package/src-shared/vendor/yaml/dist/compose/resolve-flow-collection.js +205 -0
  255. package/src-shared/vendor/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
  256. package/src-shared/vendor/yaml/dist/compose/resolve-flow-scalar.js +225 -0
  257. package/src-shared/vendor/yaml/dist/compose/resolve-props.d.ts +23 -0
  258. package/src-shared/vendor/yaml/dist/compose/resolve-props.js +150 -0
  259. package/src-shared/vendor/yaml/dist/compose/util-contains-newline.d.ts +2 -0
  260. package/src-shared/vendor/yaml/dist/compose/util-contains-newline.js +36 -0
  261. package/src-shared/vendor/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
  262. package/src-shared/vendor/yaml/dist/compose/util-empty-scalar-position.js +29 -0
  263. package/src-shared/vendor/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
  264. package/src-shared/vendor/yaml/dist/compose/util-flow-indent-check.js +17 -0
  265. package/src-shared/vendor/yaml/dist/compose/util-map-includes.d.ts +4 -0
  266. package/src-shared/vendor/yaml/dist/compose/util-map-includes.js +19 -0
  267. package/src-shared/vendor/yaml/dist/doc/Document.d.ts +141 -0
  268. package/src-shared/vendor/yaml/dist/doc/Document.js +336 -0
  269. package/src-shared/vendor/yaml/dist/doc/anchors.d.ts +24 -0
  270. package/src-shared/vendor/yaml/dist/doc/anchors.js +77 -0
  271. package/src-shared/vendor/yaml/dist/doc/applyReviver.d.ts +9 -0
  272. package/src-shared/vendor/yaml/dist/doc/applyReviver.js +57 -0
  273. package/src-shared/vendor/yaml/dist/doc/createNode.d.ts +17 -0
  274. package/src-shared/vendor/yaml/dist/doc/createNode.js +91 -0
  275. package/src-shared/vendor/yaml/dist/doc/directives.d.ts +49 -0
  276. package/src-shared/vendor/yaml/dist/doc/directives.js +178 -0
  277. package/src-shared/vendor/yaml/dist/errors.d.ts +21 -0
  278. package/src-shared/vendor/yaml/dist/errors.js +62 -0
  279. package/src-shared/vendor/yaml/dist/index.d.ts +22 -0
  280. package/src-shared/vendor/yaml/dist/index.js +50 -0
  281. package/src-shared/vendor/yaml/dist/log.d.ts +3 -0
  282. package/src-shared/vendor/yaml/dist/log.js +17 -0
  283. package/src-shared/vendor/yaml/dist/nodes/Alias.d.ts +28 -0
  284. package/src-shared/vendor/yaml/dist/nodes/Alias.js +103 -0
  285. package/src-shared/vendor/yaml/dist/nodes/Collection.d.ts +73 -0
  286. package/src-shared/vendor/yaml/dist/nodes/Collection.js +151 -0
  287. package/src-shared/vendor/yaml/dist/nodes/Node.d.ts +47 -0
  288. package/src-shared/vendor/yaml/dist/nodes/Node.js +40 -0
  289. package/src-shared/vendor/yaml/dist/nodes/Pair.d.ts +21 -0
  290. package/src-shared/vendor/yaml/dist/nodes/Pair.js +39 -0
  291. package/src-shared/vendor/yaml/dist/nodes/Scalar.d.ts +42 -0
  292. package/src-shared/vendor/yaml/dist/nodes/Scalar.js +27 -0
  293. package/src-shared/vendor/yaml/dist/nodes/YAMLMap.d.ts +53 -0
  294. package/src-shared/vendor/yaml/dist/nodes/YAMLMap.js +147 -0
  295. package/src-shared/vendor/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
  296. package/src-shared/vendor/yaml/dist/nodes/YAMLSeq.js +115 -0
  297. package/src-shared/vendor/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
  298. package/src-shared/vendor/yaml/dist/nodes/addPairToJSMap.js +106 -0
  299. package/src-shared/vendor/yaml/dist/nodes/identity.d.ts +23 -0
  300. package/src-shared/vendor/yaml/dist/nodes/identity.js +53 -0
  301. package/src-shared/vendor/yaml/dist/nodes/toJS.d.ts +27 -0
  302. package/src-shared/vendor/yaml/dist/nodes/toJS.js +39 -0
  303. package/src-shared/vendor/yaml/dist/options.d.ts +338 -0
  304. package/src-shared/vendor/yaml/dist/parse/cst-scalar.d.ts +64 -0
  305. package/src-shared/vendor/yaml/dist/parse/cst-scalar.js +218 -0
  306. package/src-shared/vendor/yaml/dist/parse/cst-stringify.d.ts +8 -0
  307. package/src-shared/vendor/yaml/dist/parse/cst-stringify.js +63 -0
  308. package/src-shared/vendor/yaml/dist/parse/cst-visit.d.ts +39 -0
  309. package/src-shared/vendor/yaml/dist/parse/cst-visit.js +99 -0
  310. package/src-shared/vendor/yaml/dist/parse/cst.d.ts +108 -0
  311. package/src-shared/vendor/yaml/dist/parse/cst.js +112 -0
  312. package/src-shared/vendor/yaml/dist/parse/lexer.d.ts +87 -0
  313. package/src-shared/vendor/yaml/dist/parse/lexer.js +719 -0
  314. package/src-shared/vendor/yaml/dist/parse/line-counter.d.ts +22 -0
  315. package/src-shared/vendor/yaml/dist/parse/line-counter.js +41 -0
  316. package/src-shared/vendor/yaml/dist/parse/parser.d.ts +84 -0
  317. package/src-shared/vendor/yaml/dist/parse/parser.js +958 -0
  318. package/src-shared/vendor/yaml/dist/public-api.d.ts +43 -0
  319. package/src-shared/vendor/yaml/dist/public-api.js +104 -0
  320. package/src-shared/vendor/yaml/dist/schema/Schema.d.ts +18 -0
  321. package/src-shared/vendor/yaml/dist/schema/Schema.js +40 -0
  322. package/src-shared/vendor/yaml/dist/schema/common/map.d.ts +2 -0
  323. package/src-shared/vendor/yaml/dist/schema/common/map.js +19 -0
  324. package/src-shared/vendor/yaml/dist/schema/common/null.d.ts +4 -0
  325. package/src-shared/vendor/yaml/dist/schema/common/null.js +17 -0
  326. package/src-shared/vendor/yaml/dist/schema/common/seq.d.ts +2 -0
  327. package/src-shared/vendor/yaml/dist/schema/common/seq.js +19 -0
  328. package/src-shared/vendor/yaml/dist/schema/common/string.d.ts +2 -0
  329. package/src-shared/vendor/yaml/dist/schema/common/string.js +16 -0
  330. package/src-shared/vendor/yaml/dist/schema/core/bool.d.ts +4 -0
  331. package/src-shared/vendor/yaml/dist/schema/core/bool.js +21 -0
  332. package/src-shared/vendor/yaml/dist/schema/core/float.d.ts +4 -0
  333. package/src-shared/vendor/yaml/dist/schema/core/float.js +47 -0
  334. package/src-shared/vendor/yaml/dist/schema/core/int.d.ts +4 -0
  335. package/src-shared/vendor/yaml/dist/schema/core/int.js +42 -0
  336. package/src-shared/vendor/yaml/dist/schema/core/schema.d.ts +1 -0
  337. package/src-shared/vendor/yaml/dist/schema/core/schema.js +25 -0
  338. package/src-shared/vendor/yaml/dist/schema/json/schema.d.ts +2 -0
  339. package/src-shared/vendor/yaml/dist/schema/json/schema.js +64 -0
  340. package/src-shared/vendor/yaml/dist/schema/json-schema.d.ts +69 -0
  341. package/src-shared/vendor/yaml/dist/schema/tags.d.ts +40 -0
  342. package/src-shared/vendor/yaml/dist/schema/tags.js +86 -0
  343. package/src-shared/vendor/yaml/dist/schema/types.d.ts +90 -0
  344. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
  345. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/binary.js +68 -0
  346. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
  347. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/bool.js +29 -0
  348. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
  349. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/float.js +50 -0
  350. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
  351. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/int.js +76 -0
  352. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/omap.d.ts +28 -0
  353. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/omap.js +77 -0
  354. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
  355. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
  356. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
  357. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/schema.js +39 -0
  358. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
  359. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/set.js +96 -0
  360. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
  361. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
  362. package/src-shared/vendor/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
  363. package/src-shared/vendor/yaml/dist/stringify/foldFlowLines.js +151 -0
  364. package/src-shared/vendor/yaml/dist/stringify/stringify.d.ts +21 -0
  365. package/src-shared/vendor/yaml/dist/stringify/stringify.js +127 -0
  366. package/src-shared/vendor/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
  367. package/src-shared/vendor/yaml/dist/stringify/stringifyCollection.js +145 -0
  368. package/src-shared/vendor/yaml/dist/stringify/stringifyComment.d.ts +10 -0
  369. package/src-shared/vendor/yaml/dist/stringify/stringifyComment.js +24 -0
  370. package/src-shared/vendor/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
  371. package/src-shared/vendor/yaml/dist/stringify/stringifyDocument.js +87 -0
  372. package/src-shared/vendor/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
  373. package/src-shared/vendor/yaml/dist/stringify/stringifyNumber.js +26 -0
  374. package/src-shared/vendor/yaml/dist/stringify/stringifyPair.d.ts +3 -0
  375. package/src-shared/vendor/yaml/dist/stringify/stringifyPair.js +152 -0
  376. package/src-shared/vendor/yaml/dist/stringify/stringifyString.d.ts +9 -0
  377. package/src-shared/vendor/yaml/dist/stringify/stringifyString.js +330 -0
  378. package/src-shared/vendor/yaml/dist/test-events.d.ts +4 -0
  379. package/src-shared/vendor/yaml/dist/test-events.js +134 -0
  380. package/src-shared/vendor/yaml/dist/util.d.ts +12 -0
  381. package/src-shared/vendor/yaml/dist/util.js +28 -0
  382. package/src-shared/vendor/yaml/dist/visit.d.ts +102 -0
  383. package/src-shared/vendor/yaml/dist/visit.js +236 -0
  384. package/src-shared/vendor/yaml/package.json +96 -0
  385. package/src-shared/vendor/yaml/util.js +2 -0
@@ -0,0 +1,1152 @@
1
+ /**
2
+ * handleRequest.mjs — codex runtime D-C2.2
3
+ *
4
+ * 把 codex handleRequest 逻辑提取为独立模块,以便单元测试。
5
+ *
6
+ * createCodexHandleRequest({ pre, cwd, log, usage, shuttingDownRef, dispatchHandlerOptions })
7
+ * → async (request, ctx) => void
8
+ *
9
+ * 路由:
10
+ * 1. request.__kind == "agent.run.dispatch" → unifiedDispatchHandler(返回 true)→ 结束
11
+ * 2. 否则 → 原有 MCP stdio 旧路径
12
+ *
13
+ * dispatchHandlerOptions:
14
+ * relayUrl string relay 服务地址
15
+ * bridgeToken string bridge 鉴权 token
16
+ * producerFactory? Function 注入 producer(测试用)
17
+ * openclawInvoke? Function 注入 openclawInvoke(测试用;若未注入则使用默认)
18
+ * _mcpClientFactory? Function 注入 MCP 客户端工厂(测试用,覆盖旧路径)
19
+ */
20
+
21
+ import { createUnifiedDispatchHandler } from "../../core/unifiedDispatchHandler.mjs";
22
+ import { collectCodexCustomCommands as _defaultCollectCodexCustomCommands } from "./codexCustomCommandsCollector.mjs";
23
+ import { scanWorkspacesByRuntime } from "../../core/scanWorkspaces.mjs";
24
+ import { createMcpStdioClient } from "../../core/mcpStdioClient.mjs";
25
+ import { createActiveRunRegistry } from "../../core/activeRuns.mjs";
26
+ import { BoundedMap } from "../../core/boundedMap.mjs";
27
+ import { expandHome } from "../../core/pathExpand.mjs";
28
+ import { createWorkspace as _defaultCreateWorkspace } from "../../core/createWorkspace.mjs";
29
+ import { ensureDefaultWorkspace } from "../../core/defaultWorkspace.mjs";
30
+ import { mapCodexMsg } from "./eventMapper.mjs";
31
+ import { createCodexPlanHandler } from "./planNotificationHandler.mjs";
32
+ import {
33
+ classifySlashCommand,
34
+ emitAssistantText,
35
+ } from "../_shared/slashCommandRouter.mjs";
36
+ import { createInterruptGateway } from "../../core/interruptGateway.mjs";
37
+ import { createApprovalGateway } from "../../core/approvalGateway.mjs";
38
+ import { attachElicitationHandler } from "./elicitationHandler.mjs";
39
+ import { writeCodexTrust } from "../../core/codexTrust.mjs";
40
+ import { waitForRunResumed } from "../../core/interruptRunListener.mjs";
41
+ import { listSessionsForRuntime, buildSessionLabel, buildSessionSubtitle } from "../_shared/sessionPicker.mjs";
42
+ import {
43
+ buildRunStarted,
44
+ buildRunCompleted,
45
+ buildRunPaused,
46
+ buildBlockStarted,
47
+ buildBlockCompleted,
48
+ newBlockId,
49
+ } from "../../../src-shared/envelope_builder.mjs";
50
+
51
+ const TURN_HARD_TIMEOUT_MS = 10 * 60_000;
52
+
53
+ /**
54
+ * codex notification msg 翻译为 onChunk 期望格式。
55
+ * 翻译规则:
56
+ * agent_message_delta → response.output_text.delta
57
+ * agent_reasoning(_delta|_section_break|_raw_content)
58
+ * → response.reasoning_summary_text.delta
59
+ * exec_command_begin → response.tool_use.start + completed (一次性,因 codex
60
+ * 的 command 在 begin 事件就有完整 input)
61
+ * exec_command_end → response.tool_result
62
+ * task_complete/task_completed/... → response.completed
63
+ * error → response.failed
64
+ * 其他 → null (忽略)
65
+ *
66
+ * codex 没有 subagent 概念 (单一 agent + tool calls),所以无 parent_tool_use_id。
67
+ *
68
+ * 返回单个 chunk 或一组 chunk 数组——为支持 exec_command_begin 一次产 start+completed
69
+ * 两个事件,签名兼容数组返回。
70
+ */
71
+ export function translateCodexMsgToChunk(msg) {
72
+ if (!msg || typeof msg.type !== "string") return null;
73
+ switch (msg.type) {
74
+ case "agent_message_content_delta":
75
+ // codex 0.130.0+: streaming text fragments arrive under msg.delta (single-token chars).
76
+ // 53 fragments per ~10s turn (实测), 必须接;否则用户看不到流式打字。
77
+ return { event: "response.output_text.delta", delta: String(msg.delta ?? "") };
78
+ case "agent_message_delta":
79
+ // Legacy event name (codex <0.130). msg.content 是完整文本。
80
+ return { event: "response.output_text.delta", delta: String(msg.content || "") };
81
+ case "agent_message":
82
+ // 完成态汇总 — content_delta 已流过整段, 此处不重复发, 否则 app 会看到双倍文本。
83
+ // 仅在 phase 标识为 "final" 时再考虑 (codex 默认 phase="commentary")
84
+ return null;
85
+ case "agent_reasoning":
86
+ case "agent_reasoning_delta":
87
+ case "agent_reasoning_section_break":
88
+ case "agent_reasoning_raw_content": {
89
+ // 字段在 codex 协议里可能是 msg.text 或 msg.content(参考 eventMapper.mjs:44-46)
90
+ const text = typeof msg.text === "string"
91
+ ? msg.text
92
+ : (typeof msg.content === "string" ? msg.content : "");
93
+ if (!text) return null;
94
+ return { event: "response.reasoning_summary_text.delta", delta: text };
95
+ }
96
+ case "exec_command_begin": {
97
+ const callId = String(msg.call_id || "");
98
+ const input = {
99
+ command: msg.command || "",
100
+ ...(msg.cwd ? { cwd: msg.cwd } : {}),
101
+ };
102
+ return [
103
+ { event: "response.tool_use.start", call_id: callId, name: "bash", input },
104
+ { event: "response.tool_use.completed", call_id: callId, name: "bash", input },
105
+ ];
106
+ }
107
+ case "exec_command_end": {
108
+ const callId = String(msg.call_id || "");
109
+ const exitCode = typeof msg.exit_code === "number" ? msg.exit_code : null;
110
+ const stdout = typeof msg.stdout === "string" ? msg.stdout : "";
111
+ const stderr = typeof msg.stderr === "string" ? msg.stderr : "";
112
+ const isError = exitCode != null && exitCode !== 0;
113
+ const summaryParts = [];
114
+ if (stdout) summaryParts.push(stdout.slice(-1500));
115
+ if (stderr) summaryParts.push(`STDERR:\n${stderr.slice(-500)}`);
116
+ return {
117
+ event: "response.tool_result",
118
+ call_id: callId,
119
+ output: [{ type: "text", format: "plain", text: summaryParts.join("\n") }],
120
+ is_error: isError,
121
+ summary: exitCode != null ? `exit=${exitCode}` : undefined,
122
+ };
123
+ }
124
+ case "task_complete":
125
+ case "task_completed":
126
+ case "session_ended":
127
+ case "shutdown":
128
+ return { event: "response.completed" };
129
+ case "error":
130
+ return {
131
+ event: "response.failed",
132
+ error: { message: String(msg.message || msg.error || "codex error") },
133
+ };
134
+ default:
135
+ return null;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * @param {{
141
+ * pre: { ok: boolean, mcpSubcommand: string },
142
+ * cwd: string,
143
+ * log: { info, warn, error, child? },
144
+ * usage: { addCost, schedulePublish },
145
+ * shuttingDownRef: { current: boolean },
146
+ * dispatchHandlerOptions: {
147
+ * relayUrl: string,
148
+ * bridgeToken: string,
149
+ * producerFactory?: Function,
150
+ * openclawInvoke?: Function,
151
+ * _mcpClientFactory?: Function, // 仅测试用
152
+ * },
153
+ * }} opts
154
+ * @returns {(request: any, ctx: any) => Promise<void>}
155
+ */
156
+ export function createCodexHandleRequest({ pre, cwd, log, usage, shuttingDownRef, bridgeCtx, dispatchHandlerOptions, scanner, _createWorkspace, _collectCodexCustomCommands }) {
157
+ const {
158
+ relayUrl,
159
+ bridgeToken,
160
+ producerFactory,
161
+ openclawInvoke: injectedOpenclawInvoke,
162
+ _mcpClientFactory,
163
+ } = dispatchHandlerOptions ?? {};
164
+ // bridge 状态(codex CLI 版本 / gw_id / model), 供 slash 路由器 bridge-synth
165
+ // 分支引用(/help /status /cost /model — codex MCP 全不识别 slash, 必须 bridge 合成)。
166
+ const codexCliVersion = bridgeCtx?.version || pre?.version || null;
167
+ const codexGwId = bridgeCtx?.gwId || null;
168
+ const codexModel = bridgeCtx?.model || null;
169
+
170
+ // MCP 客户端工厂(可被测试注入)
171
+ const mkMcpClient = _mcpClientFactory ?? (({ command, args, cwd: mcpCwd, log: mcpLog }) =>
172
+ createMcpStdioClient({ command, args, cwd: mcpCwd, log: mcpLog })
173
+ );
174
+
175
+ // agentlink thread_id → codex internal threadId 缓存(结构化输出抽出来的)。
176
+ // Codex MCP 暴露两个 tool:
177
+ // - `codex {prompt}` 开新会话,返回 `{threadId, content}`
178
+ // - `codex-reply {threadId, prompt}` 用现成的 threadId 续接 —— **但 codex
179
+ // MCP server 要求 session 还在同一个 MCP 进程的内存里**(实测:从另一个
180
+ // MCP 进程发 codex-reply 直接返回 isError + "Session not found")。
181
+ // 所以续接必须配合 **long-lived MCP per agentlink-thread**:第一次给某
182
+ // thread dispatch 时 spawn MCP + 调 codex 拿 threadId 缓存;后续同 thread
183
+ // 复用这个 MCP client 调 codex-reply。
184
+ const threadCodexIdMap = new BoundedMap({ maxSize: 5_000, ttlMs: 7 * 24 * 60 * 60 * 1000 }); // audit: LRU+TTL
185
+ // interrupt 闸门 — /resume picker 挂起/恢复
186
+ const interruptGateway = createInterruptGateway({ log });
187
+ // 审批闸门 — codex elicitation/create(工具/命令需用户确认)经此收敛到 App
188
+ // (兼容无 child 方法的注入 log,如部分测试桩)
189
+ const approvalGateway = createApprovalGateway({
190
+ log: typeof log?.child === "function" ? log : { ...log, child: () => log },
191
+ });
192
+ // activeRuns 注册表:per-turn 登记 in-flight requestId,供 sessions.abort 精确 cancel。
193
+ // kill 语义 = 发 notifications/cancelled,不杀 MCP 子进程(会话保留)。
194
+ const activeRuns = createActiveRunRegistry({ log });
195
+
196
+ // agentlink thread_id → 长寿命 MCP client(per-thread 复用),死了从 map 里清掉。
197
+ const threadMcpMap = new Map();
198
+ // agentlink thread_id → 最近一次 session_configured payload(model / permission_profile /
199
+ // approval_policy / mcp_servers / cwd / sandbox / reasoning_effort 等)。供 /permissions
200
+ // /approvals /mcp /status 等 slash 合成响应时引用真实状态(而不是空模板)。
201
+ // 在 MCP onNotification("codex/event") 里捕获(msg.type === "session_configured")。
202
+ const threadSessionConfigured = new BoundedMap({ maxSize: 5_000, ttlMs: 7 * 24 * 60 * 60 * 1000 }); // audit: LRU+TTL
203
+ // agentlink thread_id → 最近一次 token_count.rate_limits payload。Codex MCP 每轮
204
+ // 跑完会发 `event_msg.token_count` 携带 5h(primary) + weekly(secondary) 额度,
205
+ // 结构跟 claude 的 rate_limits 一致 — primary.{used_percent, window_minutes:300,
206
+ // resets_at(unix秒)} / secondary.{used_percent, window_minutes:10080, resets_at} /
207
+ // credits / plan_type。/usage 合成时用这份, 同时也写盘到本地最近一次 jsonl
208
+ // (~/.codex/sessions/...)做 fallback(bridge 重启后还能读到上次结果)。
209
+ const threadRateLimits = new BoundedMap({ maxSize: 5_000, ttlMs: 7 * 24 * 60 * 60 * 1000 }); // audit: LRU+TTL
210
+
211
+ function evictMcp(threadId, mcpRef, reason) {
212
+ const cur = threadMcpMap.get(threadId);
213
+ if (cur === mcpRef) {
214
+ // audit 修复:仅删 map 引用时旧 mcp 对象的 pending/notifHandlers 仍被
215
+ // 闭包持有;显式 close 释放(进程已退场景 close 幂等无害)。
216
+ try { mcpRef.close?.(reason); } catch { /* already closed */ }
217
+ threadMcpMap.delete(threadId);
218
+ threadCodexIdMap.delete(threadId);
219
+ threadSessionConfigured.delete(threadId);
220
+ threadRateLimits.delete(threadId);
221
+ log.warn?.("codex.mcp.evicted", "long-lived MCP removed from cache", {
222
+ thread_id: threadId, reason,
223
+ });
224
+ }
225
+ }
226
+
227
+ // 默认 openclawInvoke:起 MCP stdio 子进程,监听 codex/event notification
228
+ // Task 5.1: 加入 blocks 参数(默认 []),支持 image + PDF attachment inline URL
229
+ // align-runtime-default-workspace: 加入 workspace_path 参数,解析链同 claude
230
+ const defaultOpenclawInvoke = async ({ thread_id, run_id, input, blocks = [], workspace_path: reqWorkspacePath, onChunk: _rawOnChunk }) => {
231
+ // per-request cwd 解析(align-runtime-default-workspace):
232
+ // 1. workspace_path 非空 → expandHome + existsSync → 用它
233
+ // 2. workspace_path 给定但不存在 → fallback 到 defaultWorkspace,emit cwd_fallback
234
+ // 3. workspace_path 未给 → 直接用 defaultWorkspace(修 service 模式 "/" bug)
235
+ let spawnCwd;
236
+ let cwdFallback = null;
237
+
238
+ const { existsSync: fsExistsSync } = await import("node:fs");
239
+
240
+ if (reqWorkspacePath && typeof reqWorkspacePath === "string" && reqWorkspacePath.trim()) {
241
+ const resolved = expandHome(reqWorkspacePath.trim());
242
+ if (fsExistsSync(resolved)) {
243
+ spawnCwd = resolved;
244
+ log.info?.("codex.spawn.cwd.workspace", "using workspace_path as cwd", {
245
+ cwd: resolved, source: "workspace_path",
246
+ });
247
+ } else {
248
+ spawnCwd = await ensureDefaultWorkspace("codex");
249
+ cwdFallback = {
250
+ actual_cwd: spawnCwd,
251
+ requested_cwd: reqWorkspacePath,
252
+ reason: `workspace_path '${reqWorkspacePath}' does not exist after expand, falling back to default workspace`,
253
+ };
254
+ log.warn?.("codex.spawn.cwd.not_found", "workspace_path does not exist, falling back to default workspace", {
255
+ workspace_path: reqWorkspacePath, fallback_cwd: spawnCwd,
256
+ });
257
+ }
258
+ } else {
259
+ spawnCwd = await ensureDefaultWorkspace("codex");
260
+ log.info?.("codex.spawn.cwd.default", "no workspace_path, using default workspace", {
261
+ cwd: spawnCwd, source: "default_workspace",
262
+ });
263
+ }
264
+
265
+ // 自动 attach 扫描器: 拦 response.output_text.delta 喂 accumulator, fire-and-forget。
266
+ // codex 是 single-token streaming, accumulator 按 \n 切完整行触发 scan, 路径
267
+ // 自然落在完整行里被识别。scanner 由工厂注入。
268
+ const _acc = scanner?.createTurnAccumulator({ requestId: run_id });
269
+ const onChunk = _acc
270
+ ? async (event) => {
271
+ if (event && event.event === "response.output_text.delta" && typeof event.delta === "string") {
272
+ try { _acc.append(event.delta); } catch { /* swallow */ }
273
+ }
274
+ return _rawOnChunk(event);
275
+ }
276
+ : _rawOnChunk;
277
+
278
+ // fallback 可观测:workspace_path 给定但不存在时,emit run.started 含 cwd_fallback
279
+ if (cwdFallback) {
280
+ await onChunk({
281
+ event: "__envelopes",
282
+ envelopes: [buildRunStarted(run_id, { options: { cwd_fallback: cwdFallback } })],
283
+ });
284
+ }
285
+
286
+ // audit 修复:unsubEvent/mcp 提升声明——turn 中任何异常此前会跳过
287
+ // try 尾部的清理(listener 累积 → 下一轮事件翻倍;__currentRun 悬空 →
288
+ // elicitation 发往旧 run)。清理统一挪进 finally。
289
+ let mcp = null;
290
+ let unsubEvent = null;
291
+ try {
292
+ // ── Slash command 拦截(bridge 层)─────────────────────────────────────
293
+ // codex MCP `tools/call codex {prompt:"/help"}` **不识别任何 slash 命令**——
294
+ // 全部当 prompt 喂给 LLM,LLM 用自然语言"假装"完成命令(最危险的是
295
+ // `/clear` 答"已清除上下文"但 codex 内部 threadId 一字未动,用户被误导)。
296
+ //
297
+ // 因此 codex 这边 bridge 必须截下所有 slash:
298
+ // - /clear → 删 threadCodexIdMap + 关 MCP 进程, 下一条消息 fresh session
299
+ // - /help /status /cost /model /usage /version → bridge 用已有信息合成
300
+ // 等价输出(不调 MCP, 避免 LLM 介入)
301
+ // - 其它 unknown slash → 友好提示
302
+ const cachedSessionCfg = thread_id ? threadSessionConfigured.get(thread_id) || null : null;
303
+ const slash = await classifySlashCommand(input, "codex", {
304
+ version: codexCliVersion,
305
+ cwd,
306
+ gwId: codexGwId,
307
+ threadId: thread_id || null,
308
+ sessionId: thread_id ? threadCodexIdMap.get(thread_id) || null : null,
309
+ // 优先用 session_configured 里的真实 model(codex MCP 启动时返回),
310
+ // 退回到 bridgeCtx 里的 fallback model(目前是 null)。
311
+ model: cachedSessionCfg?.model || codexModel,
312
+ // 整段 session_configured payload, 给 /permissions /approvals /mcp 用
313
+ sessionConfigured: cachedSessionCfg,
314
+ // 最近一次 token_count.rate_limits, 给 /usage 用
315
+ rateLimits: thread_id ? threadRateLimits.get(thread_id) || null : null,
316
+ });
317
+ if (slash.kind === "client-reset") {
318
+ if (thread_id) {
319
+ threadCodexIdMap.delete(thread_id);
320
+ const mcpRef = threadMcpMap.get(thread_id);
321
+ if (mcpRef) {
322
+ // 关掉当前 MCP 进程,下一轮 spawn 新的 = 新 codex session,
323
+ // 跟用户对"清除上下文"的期望一致。
324
+ try { mcpRef.close?.("slash-clear"); } catch {}
325
+ evictMcp(thread_id, mcpRef, "slash-clear");
326
+ }
327
+ log.info?.("codex.slash.clear",
328
+ "client-reset slash command, dropped codex threadId + closed MCP",
329
+ { thread_id, command: slash.command });
330
+ }
331
+ await emitAssistantText(onChunk, slash.ackText);
332
+ return;
333
+ }
334
+ if (slash.kind === "bridge-synth") {
335
+ log.info?.("codex.slash.synth",
336
+ "bridge-synthesized slash response (codex MCP can't dispatch slash)",
337
+ { thread_id, command: slash.command });
338
+ await emitAssistantText(onChunk, slash.responseText);
339
+ return;
340
+ }
341
+
342
+ // ── /resume interrupt session picker (codex) ────────────────────────────
343
+ if (slash.kind === "interrupt-session-pick") {
344
+ log.info?.("codex.slash.resume_picker", "/resume: starting interrupt session picker", { thread_id, run_id });
345
+ // 默认只列「当前工作区」(spawnCwd) 下的历史会话;跨项目会话不再混入。
346
+ const sessions = await listSessionsForRuntime("codex", { filterCwd: spawnCwd });
347
+ if (sessions.length === 0) {
348
+ log.info?.("codex.slash.resume_picker.empty", "/resume: no sessions in current workspace", { thread_id, cwd: spawnCwd });
349
+ await emitAssistantText(onChunk, "本工作区暂无历史会话,直接开始新对话吧");
350
+ return;
351
+ }
352
+ const blockId = newBlockId();
353
+ const options = sessions.map((s) => ({ id: s.session_id, label: buildSessionLabel(s), description: buildSessionSubtitle(s) }));
354
+ await onChunk({
355
+ event: "__envelopes",
356
+ envelopes: [
357
+ buildRunStarted(run_id),
358
+ buildBlockStarted(run_id, {
359
+ id: blockId, type: "interrupt", subtype: "question",
360
+ prompt: "选择要续接的历史会话", options, selection_mode: "single",
361
+ }),
362
+ buildRunPaused(run_id, [blockId]),
363
+ ],
364
+ });
365
+ // 非阻塞:发完 interrupt + run.paused 立即 return 释放 worker;
366
+ // resume 由 relay stream 监听器异步处理(见 claude handleRequest 同款)。
367
+ if (relayUrl && bridgeToken && thread_id) {
368
+ waitForRunResumed({ relayUrl, bridgeToken, threadId: thread_id, runId: run_id, interruptId: blockId, log })
369
+ .then(async ({ value }) => {
370
+ const decisionValue = value;
371
+ await onChunk({
372
+ event: "__envelopes",
373
+ envelopes: [
374
+ buildBlockCompleted(run_id, { id: blockId, type: "interrupt", subtype: "question", prompt: "选择要续接的历史会话", status: "completed", value: decisionValue }),
375
+ buildRunCompleted(run_id, "end_turn"),
376
+ ],
377
+ });
378
+ // codex 的 session resume 与 claude 不同(走 codex-reply threadId),
379
+ // 磁盘 session resume 暂无等价路径,仅记录供后续集成。
380
+ const selectedSessionId = decisionValue?.option_id ?? null;
381
+ if (selectedSessionId) {
382
+ log.info?.("codex.slash.resume_picker.done", "codex session selected (pending resume integration)", {
383
+ thread_id, session_id: selectedSessionId,
384
+ });
385
+ }
386
+ })
387
+ .catch((err) => log.warn?.("codex.slash.resume_picker.listener_err", err?.message));
388
+ }
389
+ return;
390
+ }
391
+
392
+ if (slash.kind === "unsupported") {
393
+ log.info?.("codex.slash.unsupported",
394
+ "unsupported slash command, replying with hint",
395
+ { thread_id, command: slash.command });
396
+ await emitAssistantText(onChunk, slash.hintText);
397
+ return;
398
+ }
399
+ // not-slash:继续走 MCP 路径
400
+
401
+ // Task 5.3: 使用 buildCodexPromptWithAttachments 构造包含 image/PDF 引导语的 prompt
402
+ // P0#29: codex MCP 工具只收纯文本 prompt,远程 URL 模型"看"不到(fetch 拿到的
403
+ // 是二进制,进不了多模态上下文)→ 先把 image URL 下载成本地临时文件,
404
+ // prompt 引导 codex 用 view_image 工具查看本地路径。下载失败回退 URL 行为。
405
+ const localizedBlocks = await localizeImageBlocksForCodex(blocks, { log });
406
+ const promptWithAttachments = buildCodexPromptWithAttachments(input, localizedBlocks);
407
+ let priorCodexThreadId = thread_id ? threadCodexIdMap.get(thread_id) : null;
408
+ mcp = thread_id ? threadMcpMap.get(thread_id) : null;
409
+ let mcpIsNew = false;
410
+ if (!mcp) {
411
+ // P0#27: codex 按 ~/.codex/config.toml 的 projects 信任决定沙箱模式;
412
+ // 默认工作区(ensureDefaultWorkspace 建出)从未写过信任 → untrusted →
413
+ // read-only 沙箱 + 命令被拒。配对进来的工作区都是用户显式导入/创建的,
414
+ // spawn 前幂等补写 trust_level=trusted。
415
+ // 测试注入 _mcpClientFactory 时跳过(不碰真实 ~/.codex/config.toml)。
416
+ try {
417
+ if (!_mcpClientFactory) await writeCodexTrust(spawnCwd);
418
+ } catch (trustErr) {
419
+ log.warn?.("codex.trust.write_failed", "writeCodexTrust failed (continuing)", {
420
+ cwd: spawnCwd, err: trustErr?.message || String(trustErr),
421
+ });
422
+ }
423
+ mcp = mkMcpClient({
424
+ command: "codex",
425
+ args: [pre.mcpSubcommand],
426
+ cwd: spawnCwd, // align-runtime-default-workspace: 用解析后的 spawnCwd
427
+ log,
428
+ });
429
+ mcpIsNew = true;
430
+ if (thread_id) {
431
+ threadMcpMap.set(thread_id, mcp);
432
+ // MCP 进程意外死亡 → 从 cache 清掉,下次 dispatch 会重 spawn
433
+ try {
434
+ mcp.setExitHandler(({ code, signal }) => {
435
+ evictMcp(thread_id, mcp, `mcp.exit code=${code} signal=${signal}`);
436
+ });
437
+ } catch { /* setExitHandler 不存在的话忽略 */ }
438
+ }
439
+ }
440
+
441
+ let turnComplete = false;
442
+ let lastChunkError = null;
443
+
444
+ // plan notification handler:处理 turn/plan/updated → todo_list block 翻译
445
+ // 每次 openclawInvoke 独立实例(每个 run 独立),符合 D3 的 block.id 复用策略
446
+ const planHandler = createCodexPlanHandler({
447
+ runId: run_id,
448
+ onEnvelopes: async (envs) => {
449
+ try { await onChunk({ event: "__envelopes", envelopes: envs }); } catch { /* ignore */ }
450
+ },
451
+ });
452
+
453
+ // 每轮重新订阅 codex/event —— 闭包里捕获本轮的 turnComplete/onChunk。
454
+ // 关键:turn 结束时 unsubscribe 这个 listener,否则下一轮 listener 会叠加,
455
+ // 一个事件触发多次 chunk → App 端文本翻倍。
456
+ unsubEvent = mcp.onNotification("codex/event", async (params) => {
457
+ const msg = params?.msg;
458
+ if (!msg || typeof msg !== "object") return;
459
+
460
+ // 捕获 session_configured: 含 model / permission_profile / approval_policy /
461
+ // mcp_servers / sandbox / reasoning_effort / cwd / session_id, 后续 slash
462
+ // 合成(/permissions /approvals /mcp /sandbox 等)从这里读真实状态。
463
+ // 捕获 token_count → rate_limits(5h + weekly 配额),供 /usage 用
464
+ if (msg.type === "token_count" && msg.rate_limits && thread_id) {
465
+ threadRateLimits.set(thread_id, {
466
+ ...msg.rate_limits,
467
+ captured_at: new Date().toISOString(),
468
+ });
469
+ }
470
+
471
+ if (msg.type === "session_configured" && thread_id) {
472
+ threadSessionConfigured.set(thread_id, {
473
+ session_id: msg.session_id || null,
474
+ thread_id_codex: msg.thread_id || null,
475
+ model: msg.model || null,
476
+ model_provider_id: msg.model_provider_id || null,
477
+ approval_policy: msg.approval_policy || null,
478
+ approvals_reviewer: msg.approvals_reviewer || null,
479
+ permission_profile: msg.permission_profile || null,
480
+ active_permission_profile: msg.active_permission_profile || null,
481
+ cwd: msg.cwd || null,
482
+ reasoning_effort: msg.reasoning_effort || null,
483
+ mcp_servers: msg.mcp_servers || null,
484
+ rollout_path: msg.rollout_path || null,
485
+ captured_at: new Date().toISOString(),
486
+ });
487
+ }
488
+
489
+ // 先检测 turn 结束信号
490
+ if (msg.type === "task_complete" || msg.type === "task_completed" ||
491
+ msg.type === "session_ended" || msg.type === "shutdown") {
492
+ turnComplete = true;
493
+ }
494
+ // turn_aborted: codex 收到 notifications/cancelled 后发此事件,表示 turn 已真停。
495
+ // 置 turnComplete=true 使主循环退出;同时配合下方 `if (toolError && !turnComplete)`
496
+ // 护栏,确保被 cancel 的 tools/call 迟到 reject 不触发 response.failed。
497
+ if (msg.type === "turn_aborted") {
498
+ turnComplete = true;
499
+ abortedByTurnAborted = true;
500
+ }
501
+ if (msg.type === "error") {
502
+ lastChunkError = String(msg.message || msg.error || "codex error");
503
+ }
504
+
505
+ // turn/plan/updated 通知:交给 planHandler 处理(不透传为普通 chunk)
506
+ if (msg.type === "turn/plan/updated") {
507
+ const mapped = mapCodexMsg(msg);
508
+ if (mapped?.kind === "plan_updated") {
509
+ planHandler.handlePlanUpdated(mapped.payload);
510
+ }
511
+ return;
512
+ }
513
+
514
+ const chunk = translateCodexMsgToChunk(msg);
515
+ if (chunk) {
516
+ const chunks = Array.isArray(chunk) ? chunk : [chunk];
517
+ for (const c of chunks) {
518
+ try { await onChunk(c); } catch { /* ignore */ }
519
+ }
520
+ }
521
+ });
522
+
523
+ // long-lived MCP:只在第一次 spawn 时 initialize,后续 turn 复用已经
524
+ // initialized 的 client。mcp.initialize 自己有 `if (initialized) return`
525
+ // 兜底,这里加 if(mcpIsNew) 主要是为了不在 reuse 路径上多打 init log。
526
+ if (mcpIsNew) {
527
+ try {
528
+ await mcp.initialize({ capabilities: {} });
529
+ } catch (err) {
530
+ const msg = err?.message || String(err);
531
+ log.error?.("mcp.initialize.failed", "handshake failed", { err: msg });
532
+ await onChunk({ event: "response.failed", error: { message: `mcp init failed: ${msg}` } });
533
+ try { unsubEvent(); } catch {}
534
+ try { mcp.close("init-failed"); } catch {}
535
+ if (thread_id) evictMcp(thread_id, mcp, "init-failed");
536
+ return;
537
+ }
538
+ }
539
+
540
+ // P0#27/#16: codex elicitation/create(命令/工具需用户确认)→ App 审批弹窗。
541
+ // 此前 handler 从未注册,codex 的确认请求悬空超时 → 工具直接失败。
542
+ // 编排与 /resume picker 一致:interrupt(approval) block + run.paused →
543
+ // 等 relay run.resumed(App 决策)→ blockCompleted + settle 闸门。
544
+ if (mcpIsNew) {
545
+ try {
546
+ attachElicitationHandler({
547
+ mcp,
548
+ approvalGateway,
549
+ log,
550
+ onApprovalRequest: (info) => {
551
+ const cur = mcp.__currentRun;
552
+ if (!cur || !relayUrl || !bridgeToken || !cur.thread_id) {
553
+ log.warn?.("codex.approval.no_context",
554
+ "elicitation arrived without run context; auto-reject", {
555
+ request_id: info?.request_id || "",
556
+ });
557
+ approvalGateway.settle(info.request_id, "reject", "no-run-context");
558
+ return;
559
+ }
560
+ const blockId = newBlockId();
561
+ const message =
562
+ (typeof info?.tool_input?.message === "string" && info.tool_input.message.trim())
563
+ ? info.tool_input.message.trim()
564
+ : "Codex 请求执行需要确认的操作";
565
+ void cur.onChunk({
566
+ event: "__envelopes",
567
+ envelopes: [
568
+ buildBlockStarted(cur.run_id, {
569
+ id: blockId,
570
+ type: "interrupt",
571
+ subtype: "approval",
572
+ prompt: message,
573
+ }),
574
+ buildRunPaused(cur.run_id, [blockId]),
575
+ ],
576
+ });
577
+ waitForRunResumed({
578
+ relayUrl,
579
+ bridgeToken,
580
+ threadId: cur.thread_id,
581
+ runId: cur.run_id,
582
+ interruptId: blockId,
583
+ log,
584
+ }).then(async ({ value }) => {
585
+ const approved =
586
+ value?.approved === true ||
587
+ (typeof value?.option_id === "string" &&
588
+ value.option_id !== "" && value.option_id !== "reject");
589
+ await cur.onChunk({
590
+ event: "__envelopes",
591
+ envelopes: [
592
+ buildBlockCompleted(cur.run_id, {
593
+ id: blockId,
594
+ type: "interrupt",
595
+ subtype: "approval",
596
+ prompt: message,
597
+ status: "completed",
598
+ value,
599
+ }),
600
+ ],
601
+ });
602
+ approvalGateway.settle(
603
+ info.request_id,
604
+ approved ? "approve" : "reject",
605
+ typeof value?.reason === "string" ? value.reason : "",
606
+ );
607
+ }).catch((err) => {
608
+ log.warn?.("codex.approval.listener_err", "run.resumed listener error", {
609
+ request_id: info?.request_id || "",
610
+ err: err?.message || String(err),
611
+ });
612
+ approvalGateway.settle(info.request_id, "reject", "listener-error");
613
+ });
614
+ },
615
+ });
616
+ } catch (attachErr) {
617
+ log.warn?.("codex.approval.attach_failed", "attachElicitationHandler failed", {
618
+ err: attachErr?.message || String(attachErr),
619
+ });
620
+ }
621
+ }
622
+
623
+ let toolError = null;
624
+ let toolResult = null;
625
+ // turn_aborted 标志:收到 codex/event turn_aborted 时置 true,用于 turn 结束后
626
+ // emit「已停止本次回复」文案(类比 claude runtime 的 abortedByRpc 收尾行为)。
627
+ let abortedByTurnAborted = false;
628
+ // 续接:缓存命中 → codex-reply(带 threadId 续会话),否则 codex(开新会话)。
629
+ // 第一次会 cache codex 返回的 threadId;同一 MCP 进程后续轮直接 reply。
630
+ let callName = priorCodexThreadId ? "codex-reply" : "codex";
631
+ let callArgs = priorCodexThreadId
632
+ ? { threadId: priorCodexThreadId, prompt: promptWithAttachments }
633
+ : { prompt: promptWithAttachments };
634
+ if (priorCodexThreadId) {
635
+ log.info?.("codex.session.resume", "resuming codex thread", {
636
+ agentlink_thread: thread_id, codex_thread: priorCodexThreadId,
637
+ });
638
+ }
639
+
640
+ // executeCall 包装:codex-reply 在 MCP server 还活着但 session 失效时
641
+ // (理论上不该发生,但兜底)返回 isError + "Session not found",
642
+ // 自动回退到全新 `codex` 调用。
643
+ //
644
+ // 改用 callWithId 以持有 requestId,供 activeRuns.register 的 kill 函数
645
+ // 调用 mcp.cancel(requestId) 发 notifications/cancelled 真停当前 turn。
646
+ async function executeCall() {
647
+ let lastErr = null;
648
+ for (let attempt = 0; attempt < 2; attempt++) {
649
+ let res;
650
+ try {
651
+ // callWithId 暴露 requestId,供 cancel(requestId) 精确取消当前 turn。
652
+ // mcp 由 _mcpClientFactory 注入,真实路径已是 mcpStdioClient(有 callWithId);
653
+ // 若注入的 mock 未实现 callWithId,降级到 call() 并注册空 kill(无 cancel 能力)。
654
+ let promise;
655
+ let unregisterRun;
656
+ if (typeof mcp.callWithId === "function") {
657
+ const callResult = mcp.callWithId("tools/call", { name: callName, arguments: callArgs });
658
+ promise = callResult.promise;
659
+ const requestId = callResult.requestId;
660
+ // turn 开始前登记 activeRun:
661
+ // kill = () => mcp.cancel(requestId)(发 cancelled,不杀子进程)
662
+ // turn 结束(turnComplete 或 turn_aborted)时 unregister。
663
+ unregisterRun = activeRuns.register({
664
+ sessionKey: thread_id,
665
+ threadId: thread_id,
666
+ runId: run_id,
667
+ kill: () => mcp.cancel(requestId),
668
+ });
669
+ } else {
670
+ // 降级路径:旧 mock 或不支持 callWithId 的客户端
671
+ promise = mcp.call("tools/call", { name: callName, arguments: callArgs });
672
+ unregisterRun = activeRuns.register({
673
+ sessionKey: thread_id,
674
+ threadId: thread_id,
675
+ runId: run_id,
676
+ kill: () => { /* no cancel capability in fallback mode */ },
677
+ });
678
+ }
679
+ try {
680
+ res = await promise;
681
+ } catch (err) {
682
+ lastErr = err;
683
+ unregisterRun();
684
+ return { res: null, err };
685
+ }
686
+ unregisterRun();
687
+ } catch (err) {
688
+ lastErr = err;
689
+ return { res: null, err };
690
+ }
691
+ if (res?.isError === true) {
692
+ const txt = Array.isArray(res.content)
693
+ ? res.content.filter((b) => b?.type === "text").map((b) => b.text).join("\n")
694
+ : "tool returned isError";
695
+ // Session not found → 清缓存,重试 codex(开新会话)
696
+ if (callName === "codex-reply" && /session not found/i.test(txt) && attempt === 0) {
697
+ log.warn?.("codex.session.invalid",
698
+ "codex-reply rejected with Session not found; falling back to fresh codex",
699
+ { agentlink_thread: thread_id, codex_thread: callArgs.threadId, err: txt });
700
+ if (thread_id) threadCodexIdMap.delete(thread_id);
701
+ callName = "codex";
702
+ callArgs = { prompt: promptWithAttachments };
703
+ priorCodexThreadId = null;
704
+ continue;
705
+ }
706
+ return { res, err: new Error(`codex tool error: ${txt}`) };
707
+ }
708
+ return { res, err: null };
709
+ }
710
+ return { res: null, err: lastErr || new Error("codex retry exhausted") };
711
+ }
712
+
713
+ // elicitation handler 是 per-MCP 一次性挂载的,审批要回到「当前 turn」的
714
+ // run 上下文 —— 每轮 tools/call 前登记,turn 结束清除。
715
+ mcp.__currentRun = { thread_id, run_id, onChunk };
716
+ const callPromise = executeCall()
717
+ .then(({ res, err }) => {
718
+ toolResult = res;
719
+ if (err) toolError = err;
720
+ });
721
+
722
+ // 等 turn 完成(task_complete notification / tools/call 返回 / 硬超时 / shutdown)
723
+ const startedAt = Date.now();
724
+ while (!turnComplete && !shuttingDownRef?.current && !toolError) {
725
+ if (Date.now() - startedAt > TURN_HARD_TIMEOUT_MS) {
726
+ log.warn?.("codex.turn.hard_timeout", "turn exceeded hard timeout", {
727
+ elapsed_ms: Date.now() - startedAt,
728
+ });
729
+ await onChunk({ event: "response.failed", error: { message: "codex turn hard timeout" } });
730
+ break;
731
+ }
732
+ await sleep(200);
733
+ }
734
+ await callPromise;
735
+
736
+ if (toolError && !turnComplete) {
737
+ const errMsg = toolError?.message || String(toolError);
738
+ await onChunk({ event: "response.failed", error: { message: `tools/call failed: ${errMsg}` } });
739
+ }
740
+
741
+ // turn_aborted 收尾:emit 「已停止本次回复」 assistant text chunk
742
+ // 类比 claude runtime 的 abortedByRpc 收尾(让 App 看到明确的停止文案)。
743
+ if (abortedByTurnAborted) {
744
+ await onChunk({ event: "response.output_text.delta", delta: "已停止本次回复" });
745
+ log.info?.("codex.abort.finalized", "turn aborted by cancel, emitted stop notice", {
746
+ thread_id, run_id,
747
+ });
748
+ }
749
+
750
+ // 从 tool result 抽取 codex threadId 缓存起来,下次同 agentlink thread
751
+ // 来 dispatch 时走 codex-reply。结构化输出按 MCP outputSchema 形状走
752
+ // structuredContent;老版本 codex 可能放在 content[].text 的 JSON 里,
753
+ // 这里两种都兜一下。已有缓存(同 MCP 复用)时跳过,避免每轮覆盖。
754
+ if (thread_id && toolResult && !toolError && !threadCodexIdMap.has(thread_id)) {
755
+ const sc = toolResult.structuredContent;
756
+ let codexThreadId = sc && typeof sc.threadId === "string" ? sc.threadId : null;
757
+ if (!codexThreadId && Array.isArray(toolResult.content)) {
758
+ for (const item of toolResult.content) {
759
+ if (item?.type === "text" && typeof item.text === "string") {
760
+ try {
761
+ const parsed = JSON.parse(item.text);
762
+ if (parsed && typeof parsed.threadId === "string") {
763
+ codexThreadId = parsed.threadId;
764
+ break;
765
+ }
766
+ } catch { /* not JSON, ignore */ }
767
+ }
768
+ }
769
+ }
770
+ if (codexThreadId) {
771
+ threadCodexIdMap.set(thread_id, codexThreadId);
772
+ log.info?.("codex.session.captured",
773
+ "captured codex threadId for resume", {
774
+ agentlink_thread: thread_id, codex_thread: codexThreadId,
775
+ });
776
+ } else {
777
+ log.warn?.("codex.session.no_thread_id",
778
+ "tools/call returned no threadId — next turn will start a new session",
779
+ { agentlink_thread: thread_id });
780
+ }
781
+ }
782
+
783
+ // plan handler complete():发 content_block.completed(若有未关闭的 plan block)
784
+ // 在 response.completed 之前调用(unifiedDispatchHandler 会在我们返回后追加 run.completed)
785
+ planHandler.complete();
786
+
787
+ // **关键**:long-lived MCP 模式下 turn 结束 **只 unsubscribe listener**,
788
+ // 不能 close MCP —— 否则同 agentlink thread 下一轮还得重 spawn,
789
+ // codex 内存 session 又丢了,跟没续接一样。
790
+ // MCP 子进程的真正 cleanup 走 setExitHandler / bridge shutdown。
791
+ } finally {
792
+ try { _acc?.flush(); } catch { /* swallow */ }
793
+ // 清理统一在 finally:异常路径同样生效
794
+ if (mcp?.__currentRun?.run_id === run_id) mcp.__currentRun = null;
795
+ try { unsubEvent?.(); } catch { /* already unsubscribed */ }
796
+ }
797
+ };
798
+
799
+ // 注册 unifiedDispatchHandler
800
+ const unifiedHandler = createUnifiedDispatchHandler({
801
+ relayUrl,
802
+ bridgeToken,
803
+ log,
804
+ producerFactory,
805
+ openclawInvoke: injectedOpenclawInvoke ?? defaultOpenclawInvoke,
806
+ gatewayId: codexGwId, // 正文 agentlink-cos:// 图片引用解析(sign-get)
807
+ });
808
+
809
+ // workspaces.create 实现(可注入用于测试)
810
+ const createWorkspaceImpl = _createWorkspace ?? _defaultCreateWorkspace;
811
+ // codex 自定义命令收集器(可注入用于测试)
812
+ const collectCodexCustomCommandsImpl = _collectCodexCustomCommands ?? _defaultCollectCodexCustomCommands;
813
+
814
+ return async function handleRequest(request, ctx) {
815
+ // 步骤 0(最优先):relay-rpc 分支。
816
+ if (request?.request?.__relayKind === "rpc") {
817
+ const method = request.request.method;
818
+ const params = request.request.params ?? {};
819
+ let result;
820
+ try {
821
+ if (method === "__runtime.customCommands") {
822
+ // codex 自定义命令来自 ~/.codex/prompts/*.md,无需 probe
823
+ const commands = await collectCodexCustomCommandsImpl();
824
+ result = { commands };
825
+ } else if (method === "workspaces.scan") {
826
+ // workspaces.scan 走通用 scanWorkspacesByRuntime(codex runtime 读
827
+ // ~/.codex/sessions/*.jsonl 首行 cwd field,dedupe 后返回)。
828
+ result = await scanWorkspacesByRuntime({
829
+ runtimeKind: "codex",
830
+ gatewayId: codexGwId,
831
+ params,
832
+ });
833
+ } else if (method === "workspaces.create") {
834
+ result = await createWorkspaceImpl({
835
+ runtime: "codex",
836
+ name: params.name,
837
+ });
838
+ } else if (method === "sessions.abort" || method === "chat.abort") {
839
+ // codex abort 真实实现(codex-abort-via-cancelled):
840
+ // 1. activeRuns.abort 找到匹配的 in-flight run,调用 kill()
841
+ // 2. kill() = mcp.cancel(requestId),发 notifications/cancelled 给 codex MCP server
842
+ // 3. codex 发 turn_aborted 事件,主循环 turnComplete 置位,turn 自然收尾
843
+ // 4. MCP 子进程不被 kill,threadCodexIdMap 保留,下条消息可续接
844
+ // aborted=0 时返回 no-active-run(不报错,方便 App 幂等重试)。
845
+ const { aborted, matched } = activeRuns.abort({
846
+ key: params.key ?? params.sessionKey ?? "",
847
+ runId: params.runId ?? "",
848
+ });
849
+ if (aborted >= 1) {
850
+ result = { ok: true, aborted, matched };
851
+ } else {
852
+ result = { ok: true, aborted: 0, matched: [], status: "no-active-run" };
853
+ }
854
+ } else {
855
+ result = { ok: false, error: `unknown rpc method: ${method}` };
856
+ }
857
+ } catch (err) {
858
+ result = { ok: false, error: err?.message ?? String(err) };
859
+ }
860
+ await ctx.publishEvent({
861
+ event: "response",
862
+ statusCode: 200,
863
+ contentType: "application/json; charset=utf-8",
864
+ body: JSON.stringify(result),
865
+ });
866
+ await ctx.publishEvent({ event: "end" });
867
+ return;
868
+ }
869
+
870
+ // 步骤 1:优先走 unified dispatch
871
+ const handled = await unifiedHandler(request, ctx);
872
+ if (handled) return;
873
+
874
+ // 步骤 2:旧路径 — 与 index.mjs 原有逻辑一致
875
+ const userText = extractUserText(request);
876
+ if (!userText) {
877
+ log.warn?.("turn.skip", "no user text in request", { requestId: ctx.requestId });
878
+ await ctx.publishEvent({ event: "error", error: "empty user input" });
879
+ await ctx.publishEvent({ event: "end" });
880
+ return;
881
+ }
882
+
883
+ await ctx.publishEvent({ event: "start" });
884
+
885
+ const mcp = mkMcpClient({
886
+ command: "codex",
887
+ args: [pre.mcpSubcommand],
888
+ cwd,
889
+ log,
890
+ });
891
+
892
+ let turnComplete = false;
893
+ let lastError = null;
894
+
895
+ mcp.onNotification("codex/event", (params) => {
896
+ const msg = params?.msg;
897
+ if (!msg || typeof msg !== "object" || typeof msg.type !== "string") return;
898
+ try {
899
+ const data = JSON.stringify(msg);
900
+ void ctx.publishEvent({ event: "chunk", data });
901
+ } catch (err) {
902
+ log.warn?.("codex.notify.serialize", "failed to stringify msg", {
903
+ err: err?.message || String(err), type: msg.type,
904
+ });
905
+ }
906
+ if (msg.type === "task_complete" || msg.type === "task_completed" ||
907
+ msg.type === "session_ended" || msg.type === "shutdown") {
908
+ turnComplete = true;
909
+ }
910
+ if (msg.type === "error") {
911
+ lastError = String(msg.message || msg.error || "codex error");
912
+ }
913
+ });
914
+
915
+ try {
916
+ await mcp.initialize({ capabilities: {} });
917
+ } catch (err) {
918
+ const msg = err?.message || String(err);
919
+ log.error?.("mcp.initialize.failed", "handshake failed", { err: msg });
920
+ await ctx.publishEvent({ event: "error", error: `mcp init failed: ${msg}` });
921
+ await ctx.publishEvent({ event: "end" });
922
+ try { mcp.close("init-failed"); } catch {}
923
+ return;
924
+ }
925
+
926
+ let toolError = null;
927
+ const callPromise = mcp
928
+ .call("tools/call", { name: "codex", arguments: { prompt: userText } })
929
+ .catch((err) => { toolError = err; });
930
+
931
+ const startedAt = Date.now();
932
+ while (!turnComplete && !shuttingDownRef?.current && !toolError) {
933
+ if (Date.now() - startedAt > TURN_HARD_TIMEOUT_MS) {
934
+ log.warn?.("codex.turn.hard_timeout", "turn exceeded hard timeout", {
935
+ requestId: ctx.requestId, elapsed_ms: Date.now() - startedAt,
936
+ });
937
+ lastError = "turn hard timeout";
938
+ break;
939
+ }
940
+ await sleep(200);
941
+ }
942
+ await callPromise;
943
+
944
+ if (toolError) {
945
+ const msg = toolError?.message || String(toolError);
946
+ log.warn?.("codex.tool.fail", "tools/call codex errored", { err: msg });
947
+ await ctx.publishEvent({ event: "error", error: `tools/call failed: ${msg}` });
948
+ } else if (lastError) {
949
+ await ctx.publishEvent({ event: "error", error: lastError });
950
+ }
951
+ await ctx.publishEvent({ event: "end" });
952
+
953
+ try { mcp.close("turn-end"); } catch {}
954
+
955
+ usage.addCost({ turns: 1, duration_ms: Date.now() - startedAt });
956
+ usage.schedulePublish("codex.turn_end");
957
+ };
958
+ }
959
+
960
+ // ── helpers ──────────────────────────────────────────────────────────────────
961
+
962
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
963
+
964
+ /**
965
+ * buildCodexPromptWithAttachments — Phase 5 (multimodal-attachment-llm-dispatch)
966
+ *
967
+ * codex MCP prompt 参数为字符串类型(无多模态 content array)。
968
+ * N/A for base64 fetch path — codex uses string prompt only; attachment content
969
+ * is represented as URL references or descriptions inline in the prompt text.
970
+ *
971
+ * attachment_kind ベースの4種 dispatch(relayObjectToBlock 相当のロジックを string に inline):
972
+ * - image → [image] <URL>(base64 fetch は不可。codex は fetch tool で取得する)
973
+ * - document → [file: <name>] <URL>(PDF: URL inline)
974
+ * - text → [file: <name>] <URL>(text-like: URL inline)
975
+ * - binary → [file: <name>] (type: <mime>, size: <size>) — binary content
976
+ *
977
+ * 後方互換:attachment_kind がない場合は block.type + mime から推断。
978
+ *
979
+ * @param {string} input - 用户原始文字 prompt
980
+ * @param {any[]} blocks - dispatch payload blocks 数组
981
+ * @returns {string} 构造后的 prompt 字符串
982
+ */
983
+ // P0#29: image URL block → 本地临时文件。codex 的 view_image 工具只认本地路径,
984
+ // 这是 codex 路径上让模型真正"看到"图片的唯一通道(MCP prompt 无多模态入参)。
985
+ // 单图上限 15MB、15s 超时;任何失败都不阻塞——保留原 block,prompt 回退 URL 行。
986
+ const CODEX_IMG_MAX_BYTES = 15 * 1024 * 1024;
987
+ export async function localizeImageBlocksForCodex(blocks = [], { log, _fetchImpl } = {}) {
988
+ const { mkdtemp, writeFile } = await import("node:fs/promises");
989
+ const { tmpdir } = await import("node:os");
990
+ const path = await import("node:path");
991
+ const doFetch = _fetchImpl || fetch;
992
+
993
+ const imageIdx = (blocks ?? [])
994
+ .map((b, i) => ({ b, i }))
995
+ .filter(({ b }) => b?.type === "image" && b?.source?.kind === "url" && b?.source?.url);
996
+ if (imageIdx.length === 0) return blocks;
997
+
998
+ let dir = null;
999
+ const out = [...blocks];
1000
+ for (const { b, i } of imageIdx) {
1001
+ try {
1002
+ const controller = new AbortController();
1003
+ const timer = setTimeout(() => controller.abort(), 15000);
1004
+ const resp = await doFetch(b.source.url, { signal: controller.signal });
1005
+ clearTimeout(timer);
1006
+ if (!resp.ok) throw new Error(`http_${resp.status}`);
1007
+ const buf = Buffer.from(await resp.arrayBuffer());
1008
+ if (buf.length > CODEX_IMG_MAX_BYTES) throw new Error(`too_large_${buf.length}`);
1009
+ if (!dir) dir = await mkdtemp(path.join(tmpdir(), "agentlink-codex-img-"));
1010
+ const mime = String(b.source?.media_type || "").toLowerCase();
1011
+ const ext = mime.includes("jpeg") || mime.includes("jpg") ? ".jpg"
1012
+ : mime.includes("webp") ? ".webp"
1013
+ : mime.includes("gif") ? ".gif"
1014
+ : ".png";
1015
+ const file = path.join(dir, `img-${i}${ext}`);
1016
+ await writeFile(file, buf);
1017
+ out[i] = { ...b, source: { ...b.source, local_path: file } };
1018
+ log?.info?.("codex.image.localized", "image downloaded for view_image", {
1019
+ index: i, bytes: buf.length, path: file,
1020
+ });
1021
+ } catch (err) {
1022
+ log?.warn?.("codex.image.localize_failed", "image download failed; falling back to URL line", {
1023
+ index: i, err: err?.message || String(err),
1024
+ });
1025
+ }
1026
+ }
1027
+ return out;
1028
+ }
1029
+
1030
+ export function buildCodexPromptWithAttachments(input, blocks = []) {
1031
+ const relayBlocks = (blocks ?? []).filter(
1032
+ (b) =>
1033
+ (b?.type === "image" || b?.type === "attachment") &&
1034
+ b?.source?.kind === "url" &&
1035
+ b?.source?.url,
1036
+ );
1037
+
1038
+ if (relayBlocks.length === 0) return input;
1039
+
1040
+ const imageParts = [];
1041
+ const fileParts = [];
1042
+
1043
+ for (const block of relayBlocks) {
1044
+ const kind = _resolveAttachmentKind(block);
1045
+ const url = block.source.url;
1046
+ const name = block.name ?? "file";
1047
+ const mime = block.source?.media_type ?? block.mime ?? "application/octet-stream";
1048
+ const size = block.size ?? null;
1049
+
1050
+ if (kind === "image") {
1051
+ // P0#29: 本地化成功 → 给本地路径(配合 view_image 引导语);失败回退 URL。
1052
+ const localPath = block.source?.local_path;
1053
+ imageParts.push(`[image] ${localPath || url}`);
1054
+ } else if (kind === "binary") {
1055
+ const sizeDesc = size != null ? `${size} bytes` : "unknown size";
1056
+ fileParts.push(`[file: ${name}] (type: ${mime}, size: ${sizeDesc}) — binary content`);
1057
+ } else {
1058
+ // document / text → URL inline
1059
+ fileParts.push(`[file: ${name}] ${url}`);
1060
+ }
1061
+ }
1062
+
1063
+ const parts = [];
1064
+
1065
+ // 引导语。图片若已本地化(local_path),必须明确指示用 view_image 查看——
1066
+ // 这是 codex 把图片载入多模态上下文的唯一方式;不引导的话模型会拿 shell
1067
+ // 工具去 cat/curl 二进制然后开始幻觉 (P0#29)。
1068
+ const hasLocalImage = relayBlocks.some(
1069
+ (b) => _resolveAttachmentKind(b) === "image" && b.source?.local_path,
1070
+ );
1071
+ if (imageParts.length > 0 && fileParts.length > 0) {
1072
+ parts.push(hasLocalImage
1073
+ ? "你收到了以下图片和文件。图片是本地文件路径,请先用 view_image 工具逐张查看,再结合内容回答:"
1074
+ : "你收到了以下图片和文件,请结合内容回答:");
1075
+ } else if (imageParts.length > 0) {
1076
+ parts.push(hasLocalImage
1077
+ ? "你收到了以下图片(本地文件路径)。请先用 view_image 工具逐张查看,再结合图片内容回答:"
1078
+ : "你收到了以下图片,请结合图片内容回答:");
1079
+ } else {
1080
+ parts.push("你收到了以下文件,请结合内容回答:");
1081
+ }
1082
+
1083
+ parts.push(...imageParts, ...fileParts);
1084
+
1085
+ if (input) {
1086
+ parts.push("---");
1087
+ parts.push(input);
1088
+ }
1089
+
1090
+ return parts.join("\n");
1091
+ }
1092
+
1093
+ /**
1094
+ * attachment_kind を推断(relayObjectToBlock.mjs の resolveAttachmentKind と同じロジック)。
1095
+ * codex は relayObjectToBlock を async でインポートせず同期で動く必要があるため、
1096
+ * ここでは同期コピーを保持する。
1097
+ */
1098
+ // keep in sync with relayObjectToBlock.mjs:resolveAttachmentKind (lines 50-65)
1099
+ function _resolveAttachmentKind(block) {
1100
+ const kind = block?.source?.attachment_kind;
1101
+ if (kind && kind !== "file") return kind;
1102
+
1103
+ const mime = block?.source?.media_type ?? block?.mime ?? "";
1104
+ if (block?.type === "image" || mime.startsWith("image/")) return "image";
1105
+ if (mime === "application/pdf") return "document";
1106
+ if (
1107
+ mime.startsWith("text/") ||
1108
+ mime === "application/json" ||
1109
+ mime === "application/yaml" ||
1110
+ mime === "application/x-yaml" || // CR-005: align with relayObjectToBlock.mjs
1111
+ mime === "application/xml" ||
1112
+ mime.startsWith("application/typescript") ||
1113
+ /^text\/x-/.test(mime)
1114
+ ) return "text";
1115
+ return "binary";
1116
+ }
1117
+
1118
+ function extractUserText(request) {
1119
+ const input = request?.request?.input;
1120
+ if (typeof input === "string") return input.trim();
1121
+ if (Array.isArray(input)) return collectTextFromItems(input).trim();
1122
+ if (input && typeof input === "object") {
1123
+ if (typeof input.text === "string") return input.text.trim();
1124
+ if (input.content !== undefined) return collectTextFromAny(input.content).trim();
1125
+ }
1126
+ if (typeof request?.clientPreviewText === "string") {
1127
+ return request.clientPreviewText.trim();
1128
+ }
1129
+ return "";
1130
+ }
1131
+
1132
+ function collectTextFromAny(value) {
1133
+ if (typeof value === "string") return value;
1134
+ if (Array.isArray(value)) return collectTextFromItems(value);
1135
+ if (value && typeof value === "object" && typeof value.text === "string") return value.text;
1136
+ return "";
1137
+ }
1138
+
1139
+ function collectTextFromItems(items) {
1140
+ const parts = [];
1141
+ for (const item of items) {
1142
+ if (typeof item === "string") { parts.push(item); continue; }
1143
+ if (!item || typeof item !== "object") continue;
1144
+ if (item.role && String(item.role).toLowerCase() !== "user") continue;
1145
+ if (typeof item.text === "string") { parts.push(item.text); continue; }
1146
+ if (item.content !== undefined) {
1147
+ const t = collectTextFromAny(item.content);
1148
+ if (t) parts.push(t);
1149
+ }
1150
+ }
1151
+ return parts.join("\n\n");
1152
+ }