@jungjaehoon/mama-core 2.4.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +76 -239
  2. package/db/migrations/030-case-first-memory-substrate.sql +13 -98
  3. package/db/migrations/034-add-connector-event-scope-columns.sql +9 -13
  4. package/db/migrations/039-add-connector-event-operator-ingest-seq.sql +9 -95
  5. package/db/migrations/062-refresh-connector-event-sequences.sql +9 -113
  6. package/db/migrations/063-refresh-legacy-connector-event-updates.sql +9 -32
  7. package/db/migrations/067-add-connector-event-source-entity-id.sql +9 -13
  8. package/db/migrations/069-create-registry-nodes.sql +52 -0
  9. package/db/migrations/070-record-identity.sql +35 -0
  10. package/db/migrations/071-service-operation-origins.sql +187 -0
  11. package/db/migrations/072-observation-versions.sql +29 -0
  12. package/db/migrations/073-registry-corrections.sql +45 -0
  13. package/db/migrations/074-work-graph-ref-kinds.sql +39 -0
  14. package/db/migrations/075-agent-judgments.sql +58 -0
  15. package/db/migrations/076-scoped-checkpoint-bindings.sql +15 -0
  16. package/db/migrations/077-legacy-record-kind.sql +6 -0
  17. package/db/migrations/078-source-ingest-commands.sql +18 -0
  18. package/db/migrations/079-twin-edge-relations.sql +43 -0
  19. package/db/migrations/080-restore-duration-days.sql +7 -0
  20. package/db/migrations/081-mailbox.sql +61 -0
  21. package/db/migrations/082-commitments-row-id.sql +12 -0
  22. package/db/migrations/083-observation-is-the-evidence.sql +49 -0
  23. package/db/migrations/084-scope-kind-is-not-the-cores-to-list.sql +34 -0
  24. package/db/migrations/085-a-migration-has-a-source.sql +37 -0
  25. package/db/migrations/086-the-mailbox-is-generic.sql +110 -0
  26. package/db/migrations/087-mailbox-payload.sql +5 -0
  27. package/db/migrations/088-native-input-journal.sql +14 -0
  28. package/db/migrations/089-native-turn-receipt-lookup.sql +3 -0
  29. package/db/migrations/090-model-run-native-inputs.sql +29 -0
  30. package/db/migrations/091-native-turn-results.sql +11 -0
  31. package/db/migrations/092-restore-decision-fts.sql +34 -0
  32. package/db/migrations/093-native-turn-primary-kind.sql +14 -0
  33. package/db/migrations/094-commitment-run-provenance.sql +9 -0
  34. package/db/migrations/095-model-run-native-input-identity.sql +39 -0
  35. package/db/migrations/096-drop-connector-event-index-fts.sql +9 -0
  36. package/db/migrations/097-native-input-json-validity.sql +37 -0
  37. package/db/migrations/098-workflow-memory-kind.sql +105 -0
  38. package/dist/action-contracts.d.ts +212 -0
  39. package/dist/action-contracts.js +10 -0
  40. package/dist/api/catalog.d.ts +118 -0
  41. package/dist/api/catalog.js +1756 -0
  42. package/dist/api/dispatch.d.ts +76 -0
  43. package/dist/api/dispatch.js +372 -0
  44. package/dist/canonicalize.d.ts +2 -2
  45. package/dist/canonicalize.js +3 -3
  46. package/dist/client/client.d.ts +33 -0
  47. package/dist/client/client.js +123 -0
  48. package/dist/client/ipc.d.ts +81 -0
  49. package/dist/client/ipc.js +314 -0
  50. package/dist/db-adapter/base-adapter.d.ts +6 -66
  51. package/dist/db-adapter/base-adapter.js +6 -6
  52. package/dist/db-adapter/index.d.ts +5 -5
  53. package/dist/db-adapter/index.js +2 -5
  54. package/dist/db-adapter/node-sqlite-adapter.d.ts +85 -15
  55. package/dist/db-adapter/node-sqlite-adapter.js +1968 -570
  56. package/dist/db-manager.d.ts +53 -97
  57. package/dist/db-manager.js +121 -603
  58. package/dist/embedding/embedder.d.ts +132 -0
  59. package/dist/embedding/embedder.js +299 -0
  60. package/dist/identity/principal-repository.d.ts +2 -2
  61. package/dist/index.d.ts +43 -58
  62. package/dist/index.js +97 -145
  63. package/dist/{context-compile/channel-grant.d.ts → knowledge/access.d.ts} +12 -17
  64. package/dist/knowledge/access.js +663 -0
  65. package/dist/knowledge/case-errors.d.ts +31 -0
  66. package/dist/knowledge/case-errors.js +40 -0
  67. package/dist/{cases/search-rollup.d.ts → knowledge/case-search-rollup.d.ts} +5 -4
  68. package/dist/{cases/search-rollup.js → knowledge/case-search-rollup.js} +6 -8
  69. package/dist/{cases/store.d.ts → knowledge/case-store.d.ts} +3 -6
  70. package/dist/{cases/store.js → knowledge/case-store.js} +7 -42
  71. package/dist/{cases/timeline-range.d.ts → knowledge/case-timeline-range.d.ts} +3 -3
  72. package/dist/{cases/timeline-range.js → knowledge/case-timeline-range.js} +47 -25
  73. package/dist/{cases/types.d.ts → knowledge/case-types.d.ts} +8 -2
  74. package/dist/{cases/types.js → knowledge/case-types.js} +1 -1
  75. package/dist/knowledge/commitments.d.ts +188 -0
  76. package/dist/knowledge/commitments.js +370 -0
  77. package/dist/knowledge/decision-edges.d.ts +70 -0
  78. package/dist/knowledge/decision-edges.js +123 -0
  79. package/dist/{agent-graph/types.d.ts → knowledge/graph-query.d.ts} +80 -70
  80. package/dist/knowledge/graph-query.js +1585 -0
  81. package/dist/knowledge/identity.d.ts +16 -0
  82. package/dist/knowledge/identity.js +22 -0
  83. package/dist/knowledge/index.d.ts +47 -0
  84. package/dist/knowledge/index.js +90 -0
  85. package/dist/knowledge/judgments.d.ts +118 -0
  86. package/dist/knowledge/judgments.js +808 -0
  87. package/dist/knowledge/observations.d.ts +160 -0
  88. package/dist/knowledge/observations.js +449 -0
  89. package/dist/knowledge/question-type.d.ts +34 -0
  90. package/dist/knowledge/question-type.js +70 -0
  91. package/dist/{search → knowledge}/ranker-features.js +1 -1
  92. package/dist/{search → knowledge}/search-quality.d.ts +0 -1
  93. package/dist/knowledge/search.d.ts +38 -0
  94. package/dist/knowledge/search.js +91 -0
  95. package/dist/knowledge/source-ingest.d.ts +65 -0
  96. package/dist/knowledge/source-ingest.js +174 -0
  97. package/dist/{edges/types.d.ts → knowledge/twin-edge-types.d.ts} +31 -4
  98. package/dist/{edges/types.js → knowledge/twin-edge-types.js} +13 -2
  99. package/dist/knowledge/work-dates.d.ts +49 -0
  100. package/dist/knowledge/work-dates.js +120 -0
  101. package/dist/mama-api.d.ts +114 -778
  102. package/dist/mama-api.js +313 -2461
  103. package/dist/memory/api.d.ts +264 -24
  104. package/dist/memory/api.js +1633 -843
  105. package/dist/memory/bootstrap-builder.d.ts +2 -1
  106. package/dist/memory/bootstrap-builder.js +5 -5
  107. package/dist/memory/channel-summary-state-store.d.ts +3 -2
  108. package/dist/memory/channel-summary-state-store.js +53 -51
  109. package/dist/memory/channel-summary-store.d.ts +5 -3
  110. package/dist/memory/channel-summary-store.js +3 -8
  111. package/dist/memory/dashboard-read.d.ts +46 -0
  112. package/dist/memory/dashboard-read.js +94 -0
  113. package/dist/memory/event-store.d.ts +3 -3
  114. package/dist/memory/event-store.js +3 -10
  115. package/dist/memory/finding-store.d.ts +3 -2
  116. package/dist/memory/finding-store.js +2 -7
  117. package/dist/memory/graph-read.d.ts +52 -0
  118. package/dist/memory/graph-read.js +130 -0
  119. package/dist/memory/judgment-types.d.ts +394 -0
  120. package/dist/memory/judgment-types.js +16 -0
  121. package/dist/memory/provenance-audit.d.ts +5 -2
  122. package/dist/memory/provenance-audit.js +8 -14
  123. package/dist/memory/provenance-live.d.ts +141 -0
  124. package/dist/memory/provenance-live.js +409 -0
  125. package/dist/memory/provenance-query.d.ts +5 -4
  126. package/dist/memory/provenance-query.js +21 -29
  127. package/dist/memory/provenance-resolver.d.ts +191 -0
  128. package/dist/memory/provenance-resolver.js +155 -0
  129. package/dist/memory/provenance.d.ts +1 -11
  130. package/dist/memory/provenance.js +2 -33
  131. package/dist/memory/recall-sanitize.d.ts +55 -0
  132. package/dist/memory/recall-sanitize.js +144 -0
  133. package/dist/memory/secret-filter.d.ts +32 -0
  134. package/dist/memory/secret-filter.js +126 -0
  135. package/dist/memory/truth-store.d.ts +2 -1
  136. package/dist/memory/truth-store.js +1 -4
  137. package/dist/memory/types.d.ts +74 -24
  138. package/dist/memory/types.js +75 -5
  139. package/dist/memory/write-adapters.d.ts +98 -0
  140. package/dist/memory/write-adapters.js +242 -0
  141. package/dist/operations/owner-action-effects.d.ts +53 -0
  142. package/dist/operations/owner-action-effects.js +96 -0
  143. package/dist/provenance/source-ref.d.ts +1 -1
  144. package/dist/provenance/source-ref.js +0 -2
  145. package/dist/registry/corrections.d.ts +31 -0
  146. package/dist/registry/corrections.js +332 -0
  147. package/dist/registry/record-identity.d.ts +49 -0
  148. package/dist/registry/record-identity.js +133 -0
  149. package/dist/registry/store.d.ts +101 -0
  150. package/dist/registry/store.js +395 -0
  151. package/dist/registry/types.d.ts +28 -0
  152. package/dist/registry/types.js +5 -0
  153. package/dist/relevance-scorer.js +2 -2
  154. package/dist/runtime/agent-event-bus.d.ts +50 -0
  155. package/dist/runtime/agent-event-bus.js +101 -0
  156. package/dist/runtime/drivers/claude-cli-wrapper.d.ts +155 -0
  157. package/dist/runtime/drivers/claude-cli-wrapper.js +392 -0
  158. package/dist/runtime/drivers/cli-arg-redaction.d.ts +2 -0
  159. package/dist/runtime/drivers/cli-arg-redaction.js +18 -0
  160. package/dist/runtime/drivers/cli-secret-redaction.d.ts +17 -0
  161. package/dist/runtime/drivers/cli-secret-redaction.js +126 -0
  162. package/dist/runtime/drivers/codex-app-server-process.d.ts +277 -0
  163. package/dist/runtime/drivers/codex-app-server-process.js +2342 -0
  164. package/dist/runtime/drivers/codex-auxiliary-tools.d.ts +17 -0
  165. package/dist/runtime/drivers/codex-auxiliary-tools.js +267 -0
  166. package/dist/runtime/drivers/codex-home.d.ts +20 -0
  167. package/dist/runtime/drivers/codex-home.js +674 -0
  168. package/dist/runtime/drivers/codex-thread-registry.d.ts +63 -0
  169. package/dist/runtime/drivers/codex-thread-registry.js +433 -0
  170. package/dist/runtime/drivers/persistent-cli-adapter.d.ts +160 -0
  171. package/dist/runtime/drivers/persistent-cli-adapter.js +396 -0
  172. package/dist/runtime/drivers/persistent-cli-process.d.ts +424 -0
  173. package/dist/runtime/drivers/persistent-cli-process.js +1543 -0
  174. package/dist/runtime/drivers/types.d.ts +456 -0
  175. package/dist/runtime/drivers/types.js +110 -0
  176. package/dist/runtime/mailbox.d.ts +223 -0
  177. package/dist/runtime/mailbox.js +582 -0
  178. package/dist/runtime/model-run-store.d.ts +28 -0
  179. package/dist/{model-runs/store.js → runtime/model-run-store.js} +81 -41
  180. package/dist/{model-runs/types.d.ts → runtime/model-run-types.d.ts} +33 -4
  181. package/dist/{model-runs/types.js → runtime/model-run-types.js} +1 -1
  182. package/dist/runtime/native-effect-observer.d.ts +22 -0
  183. package/dist/runtime/native-effect-observer.js +103 -0
  184. package/dist/runtime/native-input-journal.d.ts +46 -0
  185. package/dist/runtime/native-input-journal.js +308 -0
  186. package/dist/runtime/native-prompt.d.ts +118 -0
  187. package/dist/runtime/native-prompt.js +496 -0
  188. package/dist/runtime/native-session.d.ts +10 -0
  189. package/dist/{agent-graph/index.js → runtime/native-session.js} +9 -6
  190. package/dist/runtime/native-tool-trace-observer.d.ts +5 -0
  191. package/dist/runtime/native-tool-trace-observer.js +65 -0
  192. package/dist/runtime/native-turn.d.ts +272 -0
  193. package/dist/runtime/native-turn.js +1041 -0
  194. package/dist/runtime/operations.d.ts +236 -0
  195. package/dist/runtime/operations.js +565 -0
  196. package/dist/runtime/post-tool-handler.d.ts +74 -0
  197. package/dist/runtime/post-tool-handler.js +137 -0
  198. package/dist/runtime/prompt-layers.d.ts +126 -0
  199. package/dist/runtime/prompt-layers.js +239 -0
  200. package/dist/runtime/runtime-process.d.ts +102 -0
  201. package/dist/runtime/runtime-process.js +206 -0
  202. package/dist/runtime/runtime.d.ts +278 -0
  203. package/dist/runtime/runtime.js +563 -0
  204. package/dist/runtime/session-pool.d.ts +176 -0
  205. package/dist/runtime/session-pool.js +369 -0
  206. package/dist/runtime/stimulus-payload.d.ts +5 -0
  207. package/dist/runtime/stimulus-payload.js +50 -0
  208. package/dist/runtime/subagent-bridge.d.ts +24 -0
  209. package/dist/{agent-graph/types.js → runtime/subagent-bridge.js} +1 -1
  210. package/dist/runtime/text-completion.d.ts +22 -0
  211. package/dist/{agent-situation/types.js → runtime/text-completion.js} +1 -1
  212. package/dist/runtime/token-estimator.d.ts +25 -0
  213. package/dist/runtime/token-estimator.js +75 -0
  214. package/dist/runtime/tool-trace-store.d.ts +22 -0
  215. package/dist/{model-runs → runtime}/tool-trace-store.js +46 -27
  216. package/dist/runtime/trace-summary.d.ts +5 -0
  217. package/dist/runtime/trace-summary.js +74 -0
  218. package/dist/runtime/turn-text.d.ts +23 -0
  219. package/dist/runtime/turn-text.js +41 -0
  220. package/dist/storage/database.d.ts +67 -0
  221. package/dist/storage/database.js +158 -0
  222. package/package.json +20 -34
  223. package/dist/agent-graph/alias-write.d.ts +0 -3
  224. package/dist/agent-graph/alias-write.js +0 -234
  225. package/dist/agent-graph/entity-resolve.d.ts +0 -3
  226. package/dist/agent-graph/entity-resolve.js +0 -271
  227. package/dist/agent-graph/errors.d.ts +0 -4
  228. package/dist/agent-graph/errors.js +0 -11
  229. package/dist/agent-graph/graph-query.d.ts +0 -5
  230. package/dist/agent-graph/graph-query.js +0 -360
  231. package/dist/agent-graph/index.d.ts +0 -6
  232. package/dist/agent-situation/builder.d.ts +0 -8
  233. package/dist/agent-situation/builder.js +0 -239
  234. package/dist/agent-situation/cache-key.d.ts +0 -4
  235. package/dist/agent-situation/cache-key.js +0 -64
  236. package/dist/agent-situation/index.d.ts +0 -7
  237. package/dist/agent-situation/index.js +0 -28
  238. package/dist/agent-situation/packet-store.d.ts +0 -27
  239. package/dist/agent-situation/packet-store.js +0 -228
  240. package/dist/agent-situation/ranking-policy.d.ts +0 -18
  241. package/dist/agent-situation/ranking-policy.js +0 -75
  242. package/dist/agent-situation/source-readers.d.ts +0 -52
  243. package/dist/agent-situation/source-readers.js +0 -300
  244. package/dist/agent-situation/types.d.ts +0 -195
  245. package/dist/cases/wiki-page-index.d.ts +0 -37
  246. package/dist/cases/wiki-page-index.js +0 -201
  247. package/dist/config-loader.d.ts +0 -68
  248. package/dist/config-loader.js +0 -239
  249. package/dist/connectors/event-index.d.ts +0 -24
  250. package/dist/connectors/event-index.js +0 -245
  251. package/dist/connectors/raw-query.d.ts +0 -40
  252. package/dist/connectors/raw-query.js +0 -412
  253. package/dist/connectors/types.d.ts +0 -117
  254. package/dist/connectors/types.js +0 -3
  255. package/dist/context-compile/boundary-defaults.d.ts +0 -12
  256. package/dist/context-compile/boundary-defaults.js +0 -105
  257. package/dist/context-compile/channel-grant.js +0 -66
  258. package/dist/context-compile/compiler-policy.d.ts +0 -39
  259. package/dist/context-compile/compiler-policy.js +0 -147
  260. package/dist/context-compile/compiler.d.ts +0 -18
  261. package/dist/context-compile/compiler.js +0 -404
  262. package/dist/context-compile/index.d.ts +0 -10
  263. package/dist/context-compile/index.js +0 -26
  264. package/dist/context-compile/packet-store.d.ts +0 -8
  265. package/dist/context-compile/packet-store.js +0 -239
  266. package/dist/context-compile/ref.d.ts +0 -7
  267. package/dist/context-compile/ref.js +0 -102
  268. package/dist/context-compile/source-readers.d.ts +0 -70
  269. package/dist/context-compile/source-readers.js +0 -840
  270. package/dist/context-compile/types.d.ts +0 -155
  271. package/dist/context-compile/types.js +0 -11
  272. package/dist/context-compile/visibility.d.ts +0 -21
  273. package/dist/context-compile/visibility.js +0 -224
  274. package/dist/decision-tracker.d.ts +0 -206
  275. package/dist/decision-tracker.js +0 -481
  276. package/dist/edges/ref-validation.d.ts +0 -7
  277. package/dist/edges/ref-validation.js +0 -321
  278. package/dist/edges/store.d.ts +0 -8
  279. package/dist/edges/store.js +0 -129
  280. package/dist/embedding-client.d.ts +0 -28
  281. package/dist/embedding-client.js +0 -53
  282. package/dist/embedding-server/index.d.ts +0 -67
  283. package/dist/embedding-server/index.js +0 -405
  284. package/dist/embedding-server/mobile/auth.d.ts +0 -44
  285. package/dist/embedding-server/mobile/auth.js +0 -150
  286. package/dist/embedding-server/mobile/daemon.d.ts +0 -129
  287. package/dist/embedding-server/mobile/daemon.js +0 -313
  288. package/dist/embedding-server/mobile/output-parser.d.ts +0 -115
  289. package/dist/embedding-server/mobile/output-parser.js +0 -241
  290. package/dist/embedding-server/mobile/session-api.d.ts +0 -57
  291. package/dist/embedding-server/mobile/session-api.js +0 -261
  292. package/dist/embedding-server/mobile/session-manager.d.ts +0 -138
  293. package/dist/embedding-server/mobile/session-manager.js +0 -378
  294. package/dist/embedding-server/mobile/websocket-handler.d.ts +0 -137
  295. package/dist/embedding-server/mobile/websocket-handler.js +0 -486
  296. package/dist/embeddings.d.ts +0 -68
  297. package/dist/embeddings.js +0 -244
  298. package/dist/entities/audit-metrics.d.ts +0 -68
  299. package/dist/entities/audit-metrics.js +0 -124
  300. package/dist/entities/candidate-generator.d.ts +0 -8
  301. package/dist/entities/candidate-generator.js +0 -24
  302. package/dist/entities/entity-linked-decision-counts.d.ts +0 -2
  303. package/dist/entities/entity-linked-decision-counts.js +0 -29
  304. package/dist/entities/entity-list.d.ts +0 -22
  305. package/dist/entities/entity-list.js +0 -3
  306. package/dist/entities/entity-orphan-list.d.ts +0 -17
  307. package/dist/entities/entity-orphan-list.js +0 -3
  308. package/dist/entities/entity-search.d.ts +0 -20
  309. package/dist/entities/entity-search.js +0 -3
  310. package/dist/entities/errors.d.ts +0 -55
  311. package/dist/entities/errors.js +0 -100
  312. package/dist/entities/lineage-store.d.ts +0 -28
  313. package/dist/entities/lineage-store.js +0 -204
  314. package/dist/entities/normalization.d.ts +0 -14
  315. package/dist/entities/normalization.js +0 -63
  316. package/dist/entities/policy-store.d.ts +0 -8
  317. package/dist/entities/policy-store.js +0 -75
  318. package/dist/entities/policy-types.d.ts +0 -72
  319. package/dist/entities/policy-types.js +0 -59
  320. package/dist/entities/projection.d.ts +0 -8
  321. package/dist/entities/projection.js +0 -82
  322. package/dist/entities/provenance-query.d.ts +0 -32
  323. package/dist/entities/provenance-query.js +0 -3
  324. package/dist/entities/read-identity.d.ts +0 -31
  325. package/dist/entities/read-identity.js +0 -264
  326. package/dist/entities/recall-bridge.d.ts +0 -5
  327. package/dist/entities/recall-bridge.js +0 -171
  328. package/dist/entities/resolution-engine.d.ts +0 -8
  329. package/dist/entities/resolution-engine.js +0 -3
  330. package/dist/entities/rollback-preview.d.ts +0 -35
  331. package/dist/entities/rollback-preview.js +0 -3
  332. package/dist/entities/source-locator.d.ts +0 -3
  333. package/dist/entities/source-locator.js +0 -32
  334. package/dist/entities/store.d.ts +0 -69
  335. package/dist/entities/store.js +0 -544
  336. package/dist/entities/types.d.ts +0 -166
  337. package/dist/entities/types.js +0 -30
  338. package/dist/memory/extraction-prompt.d.ts +0 -4
  339. package/dist/memory/extraction-prompt.js +0 -89
  340. package/dist/memory/scope-backfill.d.ts +0 -14
  341. package/dist/memory/scope-backfill.js +0 -3
  342. package/dist/memory/scope-store.d.ts +0 -4
  343. package/dist/memory/scope-store.js +0 -9
  344. package/dist/memory-store.d.ts +0 -87
  345. package/dist/memory-store.js +0 -78
  346. package/dist/model-runs/store.d.ts +0 -13
  347. package/dist/model-runs/tool-trace-store.d.ts +0 -8
  348. package/dist/notification-manager.d.ts +0 -7
  349. package/dist/notification-manager.js +0 -12
  350. package/dist/outcome-tracker.d.ts +0 -151
  351. package/dist/outcome-tracker.js +0 -271
  352. package/dist/query-intent.d.ts +0 -23
  353. package/dist/query-intent.js +0 -114
  354. package/dist/runtime/trusted-provenance.d.ts +0 -3
  355. package/dist/runtime/trusted-provenance.js +0 -6
  356. package/dist/search/question-type.d.ts +0 -5
  357. package/dist/search/question-type.js +0 -41
  358. package/dist/test-utils.d.ts +0 -80
  359. package/dist/test-utils.js +0 -218
  360. package/dist/tier-validator.d.ts +0 -43
  361. package/dist/tier-validator.js +0 -159
  362. package/dist/time-formatter.d.ts +0 -25
  363. package/dist/time-formatter.js +0 -93
  364. /package/dist/{search → knowledge}/feedback-store.d.ts +0 -0
  365. /package/dist/{search → knowledge}/feedback-store.js +0 -0
  366. /package/dist/{search → knowledge}/ranker-features.d.ts +0 -0
  367. /package/dist/{search → knowledge}/ranker-rescore.d.ts +0 -0
  368. /package/dist/{search → knowledge}/ranker-rescore.js +0 -0
  369. /package/dist/{search → knowledge}/ranker-trainer.d.ts +0 -0
  370. /package/dist/{search → knowledge}/ranker-trainer.js +0 -0
  371. /package/dist/{search → knowledge}/search-quality.js +0 -0
@@ -0,0 +1,2342 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CodexAppServerProcess = void 0;
4
+ const cli_secret_redaction_js_1 = require("./cli-secret-redaction.js");
5
+ const node_child_process_1 = require("node:child_process");
6
+ const node_crypto_1 = require("node:crypto");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ const node_readline_1 = require("node:readline");
10
+ const types_js_1 = require("./types.js");
11
+ const types_js_2 = require("./types.js");
12
+ const codex_home_js_1 = require("./codex-home.js");
13
+ const codex_thread_registry_js_1 = require("./codex-thread-registry.js");
14
+ const DEFAULT_TIMEOUT = 300_000;
15
+ const STOP_GRACE_MS = 200;
16
+ const STDERR_LIMIT = 4_000;
17
+ const CLIENT_INFO = { name: 'mama-codex-app-server', version: '1.0.0' };
18
+ const TURN_STATUSES = new Set(['completed', 'interrupted', 'failed', 'inProgress']);
19
+ const TURN_ITEM_VIEWS = new Set(['notLoaded', 'summary', 'full']);
20
+ const APPROVAL_REVIEWERS = new Set(['user', 'auto_review', 'guardian_subagent']);
21
+ const OVERLOADED_ERROR_CODE = -32001;
22
+ const OVERLOAD_RETRY_LIMIT = 4;
23
+ const OVERLOAD_RETRY_BASE_MS = 25;
24
+ const TURN_START_RECONCILE_GRACE_MS = 250;
25
+ const SUBAGENT_FINAL_TEXT_LIMIT = 4_000;
26
+ /** Wait for the child's own `turn/completed` after the parent announced it finished. */
27
+ const SUBAGENT_COMPLETION_GRACE_MS = 5_000;
28
+ /** A registered child that never completes is reported failed rather than leaked. */
29
+ const SUBAGENT_TTL_MS = 45 * 60_000;
30
+ /** Bound on remembered finished child threads; identity, not history. */
31
+ const MAX_FINISHED_SUBAGENTS = 200;
32
+ const SUBAGENT_AUTHORITY_UNAVAILABLE = 'subagent authority unavailable';
33
+ // `SUBAGENT_AUTHORITY_EXPIRED` and the `[expired]` marker stood here. They read
34
+ // a refusal the envelope enforcer emitted once a grant passed its wall; a
35
+ // principal's access has no wall, so nothing emits it and nothing may pretend
36
+ // to detect it.
37
+ /** turn/start was explicitly rejected by the app-server, so no turn exists on the thread. */
38
+ class CodexTurnRejectedError extends Error {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = 'CodexTurnRejectedError';
42
+ }
43
+ }
44
+ class CodexAppServerRpcError extends Error {
45
+ code;
46
+ constructor(code, message) {
47
+ super(message);
48
+ this.name = 'CodexAppServerRpcError';
49
+ this.code = code;
50
+ }
51
+ }
52
+ function object(value) {
53
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
54
+ ? value
55
+ : undefined;
56
+ }
57
+ function stableJson(value) {
58
+ if (Array.isArray(value)) {
59
+ return value.map(stableJson);
60
+ }
61
+ const record = object(value);
62
+ if (!record) {
63
+ return value;
64
+ }
65
+ return Object.fromEntries(Object.keys(record)
66
+ .sort()
67
+ .map((key) => [key, stableJson(record[key])]));
68
+ }
69
+ function deepFreeze(value) {
70
+ if (Array.isArray(value)) {
71
+ for (const item of value) {
72
+ deepFreeze(item);
73
+ }
74
+ Object.freeze(value);
75
+ return;
76
+ }
77
+ const record = object(value);
78
+ if (record) {
79
+ for (const item of Object.values(record)) {
80
+ deepFreeze(item);
81
+ }
82
+ Object.freeze(record);
83
+ }
84
+ }
85
+ function snapshotHostToolBridge(bridge) {
86
+ if (!bridge) {
87
+ return undefined;
88
+ }
89
+ // Codex's dynamic-tool protocol accepts only ASCII letters, digits, `_` and `-`.
90
+ // Product actions keep their canonical dotted names; translate only at this wire
91
+ // boundary and restore the original name before the host executes it.
92
+ const canonicalByWire = new Map();
93
+ const tools = bridge.tools
94
+ .map((tool) => {
95
+ const wireName = tool.name.replace(/[^a-zA-Z0-9_-]/g, '_');
96
+ if (!wireName || canonicalByWire.has(wireName)) {
97
+ throw new Error(`Codex dynamic tool names collide at ${wireName || '<empty>'}`);
98
+ }
99
+ canonicalByWire.set(wireName, tool.name);
100
+ return stableJson({ ...tool, name: wireName });
101
+ })
102
+ .sort((left, right) => left.name.localeCompare(right.name));
103
+ deepFreeze(tools);
104
+ const execute = bridge.execute.bind(bridge);
105
+ return Object.freeze({
106
+ tools,
107
+ execute: (call) => {
108
+ const canonical = canonicalByWire.get(call.name);
109
+ if (!canonical) {
110
+ throw new Error(`Codex dynamic tool ${call.name} was not advertised`);
111
+ }
112
+ return execute({ ...call, name: canonical });
113
+ },
114
+ });
115
+ }
116
+ function errorMessage(value, fallback) {
117
+ const record = object(value);
118
+ return typeof record?.message === 'string' && record.message ? record.message : fallback;
119
+ }
120
+ function stringField(record, name, context) {
121
+ const value = record[name];
122
+ if (typeof value !== 'string') {
123
+ throw new Error(`Codex app-server returned malformed ${context}.${name}`);
124
+ }
125
+ return value;
126
+ }
127
+ function nullableStringField(record, name, context) {
128
+ const value = record[name];
129
+ if (value !== null && typeof value !== 'string') {
130
+ throw new Error(`Codex app-server returned malformed ${context}.${name}`);
131
+ }
132
+ }
133
+ function nullableNumberField(record, name, context) {
134
+ const value = record[name];
135
+ if (value !== null && (typeof value !== 'number' || !Number.isFinite(value))) {
136
+ throw new Error(`Codex app-server returned malformed ${context}.${name}`);
137
+ }
138
+ }
139
+ function validateTurn(value, context) {
140
+ const turn = object(value);
141
+ if (!turn) {
142
+ throw new Error(`Codex app-server returned malformed ${context}`);
143
+ }
144
+ stringField(turn, 'id', context);
145
+ if (!Array.isArray(turn.items) || !TURN_ITEM_VIEWS.has(String(turn.itemsView))) {
146
+ throw new Error(`Codex app-server returned malformed ${context} items`);
147
+ }
148
+ if (!TURN_STATUSES.has(String(turn.status))) {
149
+ throw new Error(`Codex app-server returned malformed ${context}.status`);
150
+ }
151
+ if (turn.error !== null) {
152
+ const error = object(turn.error);
153
+ if (!error ||
154
+ typeof error.message !== 'string' ||
155
+ error.codexErrorInfo === undefined ||
156
+ (error.additionalDetails !== null && typeof error.additionalDetails !== 'string')) {
157
+ throw new Error(`Codex app-server returned malformed ${context}.error`);
158
+ }
159
+ }
160
+ nullableNumberField(turn, 'startedAt', context);
161
+ nullableNumberField(turn, 'completedAt', context);
162
+ nullableNumberField(turn, 'durationMs', context);
163
+ return turn;
164
+ }
165
+ function validateThread(value) {
166
+ const thread = object(value);
167
+ if (!thread) {
168
+ throw new Error('Codex app-server returned malformed thread');
169
+ }
170
+ for (const field of ['id', 'sessionId', 'preview', 'modelProvider', 'cwd', 'cliVersion']) {
171
+ stringField(thread, field, 'thread');
172
+ }
173
+ for (const field of [
174
+ 'forkedFromId',
175
+ 'parentThreadId',
176
+ 'path',
177
+ 'threadSource',
178
+ 'agentNickname',
179
+ 'agentRole',
180
+ 'name',
181
+ ]) {
182
+ nullableStringField(thread, field, 'thread');
183
+ }
184
+ if (typeof thread.ephemeral !== 'boolean') {
185
+ throw new Error('Codex app-server returned malformed thread.ephemeral');
186
+ }
187
+ for (const field of ['createdAt', 'updatedAt']) {
188
+ if (typeof thread[field] !== 'number' || !Number.isFinite(thread[field])) {
189
+ throw new Error(`Codex app-server returned malformed thread.${field}`);
190
+ }
191
+ }
192
+ nullableNumberField(thread, 'recencyAt', 'thread');
193
+ const status = object(thread.status);
194
+ if (!status ||
195
+ !['notLoaded', 'idle', 'systemError', 'active'].includes(String(status.type)) ||
196
+ (status.type === 'active' && !Array.isArray(status.activeFlags))) {
197
+ throw new Error('Codex app-server returned malformed thread.status');
198
+ }
199
+ const sourceValid = typeof thread.source === 'string' || object(thread.source) !== undefined;
200
+ if (!sourceValid ||
201
+ (thread.gitInfo !== null && !object(thread.gitInfo)) ||
202
+ !Array.isArray(thread.turns)) {
203
+ throw new Error('Codex app-server returned malformed thread metadata');
204
+ }
205
+ for (const turn of thread.turns) {
206
+ validateTurn(turn, 'thread.turns[]');
207
+ }
208
+ return thread;
209
+ }
210
+ function shaFile(path) {
211
+ return (0, node_crypto_1.createHash)('sha256').update((0, node_fs_1.readFileSync)(path)).digest('hex');
212
+ }
213
+ function managedFileSignature(path) {
214
+ if (!(0, node_fs_1.existsSync)(path)) {
215
+ return undefined;
216
+ }
217
+ const stat = (0, node_fs_1.statSync)(path);
218
+ return `${stat.dev}:${stat.ino}:${stat.mode}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
219
+ }
220
+ function ensurePrivateDirectory(path) {
221
+ (0, node_fs_1.mkdirSync)(path, { recursive: true, mode: 0o700 });
222
+ (0, node_fs_1.chmodSync)(path, 0o700);
223
+ }
224
+ function atomicPrivateWrite(path, value) {
225
+ ensurePrivateDirectory((0, node_path_1.dirname)(path));
226
+ const temporary = `${path}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`;
227
+ (0, node_fs_1.writeFileSync)(temporary, value, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
228
+ (0, node_fs_1.chmodSync)(temporary, 0o600);
229
+ (0, node_fs_1.renameSync)(temporary, path);
230
+ (0, node_fs_1.chmodSync)(path, 0o600);
231
+ }
232
+ function copyAuthAtomically(source, destination) {
233
+ if (!(0, node_fs_1.existsSync)(source) ||
234
+ (0, node_fs_1.statSync)(source).size === 0 ||
235
+ (0, node_path_1.resolve)(source) === (0, node_path_1.resolve)(destination)) {
236
+ return;
237
+ }
238
+ ensurePrivateDirectory((0, node_path_1.dirname)(destination));
239
+ const temporary = `${destination}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`;
240
+ (0, node_fs_1.copyFileSync)(source, temporary);
241
+ (0, node_fs_1.chmodSync)(temporary, 0o600);
242
+ (0, node_fs_1.renameSync)(temporary, destination);
243
+ (0, node_fs_1.chmodSync)(destination, 0o600);
244
+ }
245
+ /** Item IDs are provider-local; this identifier is only for host observations. */
246
+ function nativeObservationId(threadId, itemId) {
247
+ return JSON.stringify([threadId, itemId]);
248
+ }
249
+ class CodexAppServerProcess {
250
+ options;
251
+ registry;
252
+ child;
253
+ stdout;
254
+ stderr;
255
+ nextId = 0;
256
+ pending = new Map();
257
+ lateTurnStarts = new Map();
258
+ turnStartReconciliations = new Map();
259
+ turns = new Map();
260
+ subagents = new Map();
261
+ /**
262
+ * Child threads that already reported a terminal status. The SAME `started`
263
+ * announcement arrives as both an item/started and an item/completed view, and the
264
+ * second view can land after the child's own turn/completed - `subagents.has` no longer
265
+ * dedupes it then, so it would mint a second authority and a second completion for a
266
+ * dead child. Bounded FIFO: identity of recent children, not a growing ledger.
267
+ */
268
+ finishedSubagents = new Set();
269
+ threadContexts = new Map();
270
+ sessions = new Map();
271
+ sessionQueues = new Map();
272
+ connectionQueue = Promise.resolve();
273
+ startPromise;
274
+ shutdownPromise;
275
+ stopped = false;
276
+ stderrTail = '';
277
+ secrets = new Set();
278
+ authFingerprint;
279
+ authFingerprintInitialized = false;
280
+ authSourceSignature;
281
+ managedConfigFingerprint;
282
+ managedConfigSignature;
283
+ secretLaunchFingerprint;
284
+ secretLaunchFingerprintInitialized = false;
285
+ killTimer;
286
+ finalKillTimer;
287
+ constructor(options) {
288
+ const mamaRoot = options.hostRootDir;
289
+ this.options = {
290
+ ...options,
291
+ command: options.command ?? 'codex',
292
+ requestTimeout: options.requestTimeout ?? DEFAULT_TIMEOUT,
293
+ codexHome: (0, node_path_1.resolve)(options.codexHome ?? (0, node_path_1.join)(mamaRoot, '.codex')),
294
+ isolatedHome: (0, node_path_1.resolve)(options.isolatedHome ?? (0, node_path_1.join)(mamaRoot, 'codex-runtime', 'home')),
295
+ registryRoot: (0, node_path_1.resolve)(options.registryRoot ?? (0, node_path_1.join)(mamaRoot, 'codex-runtime', 'threads')),
296
+ cwd: (0, node_path_1.resolve)(options.cwd),
297
+ };
298
+ ensurePrivateDirectory((0, node_path_1.dirname)(this.options.registryRoot));
299
+ this.registry = new codex_thread_registry_js_1.CodexThreadRegistry({ rootDir: this.options.registryRoot });
300
+ }
301
+ async prompt(text, callbacks, overrides = {}) {
302
+ if (this.stopped) {
303
+ throw new Error('Codex app-server process is stopped');
304
+ }
305
+ const session = this.resolveSessionPolicy(overrides);
306
+ return this.enqueueSession(session.sessionKey, async () => {
307
+ try {
308
+ if (this.stopped) {
309
+ throw new Error('Codex app-server process is stopped');
310
+ }
311
+ const launch = (0, codex_home_js_1.buildCodexAppServerLaunchConfig)(this.options.mcpConfigPath, this.options.processEnv ?? process.env);
312
+ if (overrides.resumeSession === false) {
313
+ this.discardSessionThreadState(session.sessionKey, 'session was restarted');
314
+ this.registry.remove(session.sessionKey);
315
+ this.sessions.delete(session.sessionKey);
316
+ }
317
+ this.assertRegistryPolicy(session, launch);
318
+ await this.prepareConnection(launch, session.requestTimeout);
319
+ let state = this.sessions.get(session.sessionKey);
320
+ if (!state?.threadId) {
321
+ try {
322
+ state = await this.openThread(session, launch);
323
+ }
324
+ catch (error) {
325
+ if (this.child && !this.shutdownPromise) {
326
+ await this.shutdown(this.toError(error));
327
+ }
328
+ throw error;
329
+ }
330
+ this.sessions.set(session.sessionKey, state);
331
+ }
332
+ const reconciliation = this.turnStartReconciliations.get(state.threadId);
333
+ if (reconciliation) {
334
+ await reconciliation.promise;
335
+ await this.prepareConnection(launch, session.requestTimeout);
336
+ state = this.sessions.get(session.sessionKey);
337
+ if (!state?.threadId) {
338
+ state = await this.openThread(session, launch);
339
+ this.sessions.set(session.sessionKey, state);
340
+ }
341
+ }
342
+ let prompt = text;
343
+ if (overrides.preparePrompt) {
344
+ try {
345
+ prompt = await overrides.preparePrompt({
346
+ sessionId: state.threadId,
347
+ isNewSession: state.startupPending === true,
348
+ });
349
+ }
350
+ catch (error) {
351
+ // No turn/start has been sent. Do not persist a never-populated thread
352
+ // as a retained session if startup context failed to assemble.
353
+ if (state.startupPending) {
354
+ this.registry.remove(session.sessionKey);
355
+ this.sessions.delete(session.sessionKey);
356
+ }
357
+ throw error;
358
+ }
359
+ }
360
+ const replayReminder = Boolean(state.bootstrapPending && session.systemPrompt);
361
+ const turnText = replayReminder
362
+ ? `<system-reminder>\nFresh MAMA runtime context after resuming this durable thread:\n${session.systemPrompt.replace(/<\/system-reminder>/gi, '')}\n</system-reminder>\n\n${prompt}`
363
+ : prompt;
364
+ // One line per turn, where the text that actually reaches the model is known.
365
+ // This is what makes "fixed things once, turns carry only deltas" measurable
366
+ // from daemon.log instead of asserted.
367
+ console.log(`[prompt] thread=${state.threadId} kind=${session.promptTelemetry?.kind ?? 'chat'} ` +
368
+ `chars=${turnText.length} brief=${session.promptTelemetry?.brief ?? 'omitted'} ` +
369
+ `reminder=${replayReminder ? 'sent' : 'omitted'}`);
370
+ // A Codex-native child can be announced after this turn resolved, so remember
371
+ // which session the thread belongs to outside the PendingTurn lifetime. The
372
+ // parent's BRIDGE is deliberately not kept: a child gets its own authority.
373
+ // A session that rotated its thread leaves the previous thread's context behind,
374
+ // and a stale entry both leaks and lets a dead thread announce children.
375
+ this.pruneThreadContexts(session.sessionKey, state.threadId);
376
+ this.threadContexts.set(state.threadId, {
377
+ sessionKey: session.sessionKey,
378
+ onToolUse: callbacks?.onToolUse,
379
+ onToolComplete: callbacks?.onToolComplete,
380
+ });
381
+ const result = await this.startTurn(state.threadId, turnText, {
382
+ ...callbacks,
383
+ onAccepted: (receipt) => {
384
+ state.startupPending = false;
385
+ callbacks?.onAccepted?.(receipt);
386
+ },
387
+ }, session.requestTimeout, session.hostToolBridge, overrides.runTokenBudget, overrides.nativeInputId, session.restrictedReadRoots);
388
+ state.bootstrapPending = false;
389
+ state.startupPending = false;
390
+ state.unproven = false;
391
+ return result;
392
+ }
393
+ catch (error) {
394
+ const opened = this.sessions.get(session.sessionKey);
395
+ if (opened?.unproven && error instanceof CodexTurnRejectedError) {
396
+ // Live 2026-09-25: a first turn rejected for input length left the thread saved
397
+ // although Codex kept no rollout for it, and every later turn failed with "no
398
+ // rollout found". Only an explicit rejection proves no turn exists; a timeout or a
399
+ // late acknowledgement may have started one, and that retry keeps the thread.
400
+ console.warn(`[CodexAppServer] forgetting thread ${opened.threadId}: its first turn was rejected`);
401
+ this.registry.remove(session.sessionKey);
402
+ this.sessions.delete(session.sessionKey);
403
+ }
404
+ if (error instanceof CodexAppServerRpcError &&
405
+ error.code !== OVERLOADED_ERROR_CODE &&
406
+ this.child &&
407
+ !this.shutdownPromise) {
408
+ await this.shutdown(error);
409
+ }
410
+ if (this.shutdownPromise) {
411
+ await this.shutdownPromise;
412
+ }
413
+ throw error;
414
+ }
415
+ });
416
+ }
417
+ /** Append input to this exact native turn; never queue behind it or start a replacement. */
418
+ async steer(text, target, sessionKey = this.options.sessionKey, beforeSend) {
419
+ const session = this.sessions.get(sessionKey);
420
+ const active = session && this.turns.get(session.threadId);
421
+ if (this.stopped ||
422
+ target.backend !== 'codex' ||
423
+ !session ||
424
+ session.threadId !== target.sessionId ||
425
+ !active ||
426
+ active.turnId !== target.turnId ||
427
+ active.abortController.signal.aborted) {
428
+ throw new types_js_1.NativeSteeringTargetUnavailableError();
429
+ }
430
+ // The core journals this input only after the exact target is known active,
431
+ // but before the RPC can write anything to the native transport.
432
+ beforeSend?.();
433
+ let response;
434
+ try {
435
+ response = await this.request('turn/steer', {
436
+ threadId: target.sessionId,
437
+ expectedTurnId: target.turnId,
438
+ input: [{ type: 'text', text, text_elements: [] }],
439
+ }, active.requestTimeout);
440
+ }
441
+ catch (error) {
442
+ if (error instanceof CodexAppServerRpcError)
443
+ throw error;
444
+ throw new types_js_1.NativeInputUncertainError('Native steer acceptance is uncertain', target, error);
445
+ }
446
+ if (object(response)?.turnId !== target.turnId) {
447
+ throw new types_js_1.NativeInputUncertainError('Native steer acknowledgement is uncertain', target);
448
+ }
449
+ return { ...target };
450
+ }
451
+ async reset(sessionKey = this.options.sessionKey) {
452
+ await this.enqueueSession(sessionKey, async () => {
453
+ this.discardSessionThreadState(sessionKey, 'session was reset');
454
+ this.registry.remove(sessionKey);
455
+ this.sessions.delete(sessionKey);
456
+ });
457
+ }
458
+ getSessionPolicyStatus(overrides = {}) {
459
+ const session = this.resolveSessionPolicy(overrides);
460
+ const record = this.registry.load(session.sessionKey);
461
+ if (!record) {
462
+ return 'missing';
463
+ }
464
+ const launch = (0, codex_home_js_1.buildCodexAppServerLaunchConfig)(this.options.mcpConfigPath, this.options.processEnv ?? process.env);
465
+ return this.registryPolicyMatches(record, session, launch) ? 'compatible' : 'mismatch';
466
+ }
467
+ async stop() {
468
+ this.stopped = true;
469
+ await this.shutdown(new Error('Codex app-server process stopped'));
470
+ this.terminateSubagents(new Error('Codex app-server process stopped'));
471
+ this.threadContexts.clear();
472
+ }
473
+ async executeSandboxedCommand(command, cwd, roots, signal) {
474
+ signal?.throwIfAborted();
475
+ if (!this.child)
476
+ throw new Error('Codex app-server is not connected');
477
+ const shellCommand = process.platform === 'win32'
478
+ ? ['cmd.exe', '/d', '/s', '/c', command]
479
+ : ['/bin/sh', '-c', command];
480
+ const result = object(await this.request('command/exec', {
481
+ command: shellCommand,
482
+ cwd,
483
+ sandboxPolicy: {
484
+ type: 'workspaceWrite',
485
+ writableRoots: [...roots],
486
+ readOnlyAccess: {
487
+ type: 'restricted',
488
+ includePlatformDefaults: true,
489
+ readableRoots: [...roots],
490
+ },
491
+ networkAccess: false,
492
+ },
493
+ timeoutMs: this.options.requestTimeout,
494
+ }, this.options.requestTimeout));
495
+ signal?.throwIfAborted();
496
+ if (typeof result?.exitCode !== 'number' ||
497
+ typeof result.stdout !== 'string' ||
498
+ typeof result.stderr !== 'string') {
499
+ throw new Error('Codex app-server returned a malformed command result');
500
+ }
501
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
502
+ }
503
+ getThreadId(sessionKey = this.options.sessionKey) {
504
+ return this.sessions.get(sessionKey)?.threadId || undefined;
505
+ }
506
+ getStatus() {
507
+ return {
508
+ running: this.child !== undefined,
509
+ childPid: this.child?.pid,
510
+ pendingRequestCount: this.pending.size,
511
+ hasActiveTurn: this.turns.size > 0,
512
+ stdoutListenerCount: this.stdout?.listenerCount('line') ?? 0,
513
+ stderrListenerCount: this.stderr?.listenerCount('line') ?? 0,
514
+ shutdownTimerActive: this.killTimer !== undefined || this.finalKillTimer !== undefined,
515
+ };
516
+ }
517
+ resolveSessionPolicy(overrides) {
518
+ const hostToolBridge = snapshotHostToolBridge(overrides.hostToolBridge);
519
+ const restrictedReadRoots = overrides.restrictedReadRoots
520
+ ? [
521
+ ...new Set(overrides.restrictedReadRoots.map((root) => (0, node_fs_1.realpathSync)((0, node_path_1.resolve)(root)))),
522
+ ].sort()
523
+ : undefined;
524
+ if (restrictedReadRoots?.length === 0) {
525
+ throw new Error('Restricted native read policy requires at least one real root');
526
+ }
527
+ const cwd = restrictedReadRoots
528
+ ? (0, node_fs_1.realpathSync)((0, node_path_1.resolve)(overrides.cwd ?? this.options.cwd))
529
+ : (0, node_path_1.resolve)(overrides.cwd ?? this.options.cwd);
530
+ if (restrictedReadRoots &&
531
+ !restrictedReadRoots.some((root) => {
532
+ const childPath = (0, node_path_1.relative)(root, cwd);
533
+ return (childPath === '' ||
534
+ (childPath !== '..' && !childPath.startsWith(`..${node_path_1.sep}`) && !(0, node_path_1.isAbsolute)(childPath)));
535
+ })) {
536
+ throw new Error('Restricted native working directory must be inside a readable root');
537
+ }
538
+ return {
539
+ sessionKey: overrides.sessionKey ?? this.options.sessionKey,
540
+ model: overrides.model ?? this.options.model,
541
+ systemPrompt: overrides.systemPrompt ?? this.options.systemPrompt,
542
+ cwd,
543
+ sandbox: restrictedReadRoots ? 'read-only' : (overrides.sandbox ?? this.options.sandbox),
544
+ ...(restrictedReadRoots ? { restrictedReadRoots } : {}),
545
+ requestTimeout: overrides.requestTimeout ?? this.options.requestTimeout,
546
+ policyFingerprint: overrides.policyFingerprint ?? this.options.policyFingerprint,
547
+ hostToolBridge,
548
+ resumeInstructions: overrides.resumeInstructions,
549
+ promptTelemetry: overrides.promptTelemetry,
550
+ };
551
+ }
552
+ enqueueSession(sessionKey, operation) {
553
+ const previous = this.sessionQueues.get(sessionKey) ?? Promise.resolve();
554
+ const result = previous.catch(() => undefined).then(operation);
555
+ const tail = result.then(() => undefined, () => undefined);
556
+ this.sessionQueues.set(sessionKey, tail);
557
+ void tail.finally(() => {
558
+ if (this.sessionQueues.get(sessionKey) === tail) {
559
+ this.sessionQueues.delete(sessionKey);
560
+ }
561
+ });
562
+ return result;
563
+ }
564
+ async prepareConnection(launch, requestTimeout) {
565
+ const operation = this.connectionQueue.then(async () => {
566
+ if (this.stopped) {
567
+ throw new Error('Codex app-server process is stopped');
568
+ }
569
+ if (this.shutdownPromise) {
570
+ await this.shutdownPromise;
571
+ }
572
+ if (this.stopped) {
573
+ throw new Error('Codex app-server process is stopped');
574
+ }
575
+ const refreshed = this.prepareManagedFiles(launch);
576
+ if (refreshed && this.child) {
577
+ await this.restart();
578
+ }
579
+ try {
580
+ await this.ensureStarted(launch, requestTimeout);
581
+ }
582
+ catch (error) {
583
+ if (this.child && !this.shutdownPromise) {
584
+ await this.shutdown(this.toError(error));
585
+ }
586
+ throw error;
587
+ }
588
+ });
589
+ this.connectionQueue = operation.catch(() => undefined);
590
+ await operation;
591
+ }
592
+ assertRegistryPolicy(session, launch) {
593
+ const record = this.registry.load(session.sessionKey);
594
+ if (!record) {
595
+ return;
596
+ }
597
+ if (!this.registryPolicyMatches(record, session, launch)) {
598
+ throw new Error('Codex app-server thread policy mismatch; reset the session explicitly');
599
+ }
600
+ }
601
+ registryPolicyMatches(record, session, launch) {
602
+ return (record.model === session.model &&
603
+ record.cwd === session.cwd &&
604
+ record.systemPromptFingerprint === this.policyFingerprint(session) &&
605
+ record.mcpConfigFingerprint === launch.fingerprint);
606
+ }
607
+ prepareManagedFiles(launch) {
608
+ ensurePrivateDirectory(this.options.codexHome);
609
+ ensurePrivateDirectory(this.options.isolatedHome);
610
+ const configPath = (0, node_path_1.join)(this.options.codexHome, 'config.toml');
611
+ const config = (0, codex_home_js_1.buildMAMACodexAppServerConfig)(this.options.effort, undefined, {
612
+ shellTool: this.options.shellTool,
613
+ webSearch: this.options.webSearch,
614
+ shellEnvironment: this.options.shellEnvironment,
615
+ deniedReadPaths: this.options.deniedReadPaths,
616
+ allowLoginShell: this.options.allowLoginShell,
617
+ });
618
+ const configFingerprint = (0, codex_thread_registry_js_1.fingerprintText)(config);
619
+ const configSignature = managedFileSignature(configPath);
620
+ if (this.managedConfigFingerprint !== configFingerprint ||
621
+ this.managedConfigSignature !== configSignature) {
622
+ if (configSignature === undefined || shaFile(configPath) !== configFingerprint) {
623
+ atomicPrivateWrite(configPath, config);
624
+ }
625
+ this.managedConfigFingerprint = configFingerprint;
626
+ this.managedConfigSignature = managedFileSignature(configPath);
627
+ }
628
+ const sourceAuth = this.options.authSourcePath;
629
+ const destinationAuth = (0, node_path_1.join)(this.options.codexHome, 'auth.json');
630
+ const sourceStat = sourceAuth !== undefined && (0, node_fs_1.existsSync)(sourceAuth) ? (0, node_fs_1.statSync)(sourceAuth) : undefined;
631
+ const sourceSignature = sourceAuth !== undefined && sourceStat && sourceStat.size > 0
632
+ ? `${(0, node_fs_1.realpathSync)(sourceAuth)}:${sourceStat.size}:${sourceStat.mtimeMs}:${sourceStat.ctimeMs}`
633
+ : undefined;
634
+ let sourceFingerprint = this.authFingerprint;
635
+ if (!this.authFingerprintInitialized ||
636
+ this.authSourceSignature !== sourceSignature ||
637
+ (sourceSignature !== undefined && !(0, node_fs_1.existsSync)(destinationAuth))) {
638
+ sourceFingerprint =
639
+ sourceAuth !== undefined && sourceSignature ? shaFile(sourceAuth) : undefined;
640
+ if (sourceAuth !== undefined &&
641
+ sourceFingerprint &&
642
+ (!(0, node_fs_1.existsSync)(destinationAuth) || shaFile(destinationAuth) !== sourceFingerprint)) {
643
+ copyAuthAtomically(sourceAuth, destinationAuth);
644
+ }
645
+ }
646
+ const changed = this.authFingerprintInitialized && this.authFingerprint !== sourceFingerprint;
647
+ this.authFingerprint = sourceFingerprint;
648
+ this.authFingerprintInitialized = true;
649
+ this.authSourceSignature = sourceSignature;
650
+ const secretChanged = this.secretLaunchFingerprintInitialized &&
651
+ this.secretLaunchFingerprint !== launch.secretFingerprint;
652
+ if (!this.secretLaunchFingerprintInitialized ||
653
+ this.secretLaunchFingerprint !== launch.secretFingerprint) {
654
+ this.secrets = (0, cli_secret_redaction_js_1.configuredSecretValues)(launch);
655
+ this.secretLaunchFingerprint = launch.secretFingerprint;
656
+ this.secretLaunchFingerprintInitialized = true;
657
+ }
658
+ return changed || secretChanged;
659
+ }
660
+ async ensureStarted(launch, requestTimeout) {
661
+ if (this.child) {
662
+ return;
663
+ }
664
+ if (!this.startPromise) {
665
+ this.startPromise = this.start(launch, requestTimeout);
666
+ }
667
+ try {
668
+ await this.startPromise;
669
+ }
670
+ finally {
671
+ this.startPromise = undefined;
672
+ }
673
+ }
674
+ async start(launch, requestTimeout) {
675
+ const child = (0, node_child_process_1.spawn)(this.options.command, ['app-server', '--strict-config', '--stdio', ...launch.args], {
676
+ cwd: this.options.cwd,
677
+ env: { ...launch.env, HOME: this.options.isolatedHome, CODEX_HOME: this.options.codexHome },
678
+ stdio: ['pipe', 'pipe', 'pipe'],
679
+ });
680
+ this.child = child;
681
+ this.stdout = (0, node_readline_1.createInterface)({ input: child.stdout, crlfDelay: Infinity });
682
+ this.stderr = (0, node_readline_1.createInterface)({ input: child.stderr, crlfDelay: Infinity });
683
+ this.stdout.on('line', (line) => this.handleLine(child, line));
684
+ this.stderr.on('line', (line) => this.handleStderr(line));
685
+ child.once('error', (error) => this.failProcess(child, error));
686
+ child.once('exit', (code, signal) => this.failProcess(child, new Error(`Codex app-server exited (${code ?? signal ?? 'unknown'})`)));
687
+ const initialized = await this.request('initialize', {
688
+ clientInfo: CLIENT_INFO,
689
+ capabilities: { experimentalApi: true },
690
+ }, requestTimeout);
691
+ const initializeResponse = object(initialized);
692
+ if (!initializeResponse ||
693
+ typeof initializeResponse.userAgent !== 'string' ||
694
+ typeof initializeResponse.codexHome !== 'string' ||
695
+ typeof initializeResponse.platformFamily !== 'string' ||
696
+ typeof initializeResponse.platformOs !== 'string') {
697
+ throw new Error('Codex app-server initialize returned a malformed response');
698
+ }
699
+ if ((0, node_fs_1.realpathSync)(initializeResponse.codexHome) !== (0, node_fs_1.realpathSync)(this.options.codexHome)) {
700
+ throw new Error('Codex app-server initialize returned an unexpected CODEX_HOME');
701
+ }
702
+ this.notify('initialized');
703
+ }
704
+ async openThread(session, launch) {
705
+ const record = this.registry.load(session.sessionKey);
706
+ if (record) {
707
+ // A rehydrated thread carries its original instructions only in the model's own
708
+ // history, where compaction eventually erodes them. ThreadResumeParams accepts
709
+ // baseInstructions, so re-anchor through the protocol when the caller can supply
710
+ // the full text. assertRegistryPolicy already proved the stored policy fingerprint
711
+ // matches this session, and the caller builds these instructions from the same
712
+ // composed-prompt sources that produced that fingerprint - so re-supplying them
713
+ // restates the agreed policy rather than switching to a new one.
714
+ const resumeInstructions = session.resumeInstructions
715
+ ? await session.resumeInstructions()
716
+ : undefined;
717
+ if (session.resumeInstructions && !resumeInstructions?.trim()) {
718
+ // No-fallback: silently resuming without instructions is the very failure this
719
+ // path exists to remove.
720
+ throw new Error('Codex app-server resume instructions resolved to empty text');
721
+ }
722
+ const resumeParams = {
723
+ threadId: record.threadId,
724
+ model: session.model,
725
+ cwd: session.cwd,
726
+ approvalPolicy: 'never',
727
+ ...(this.options.deniedReadPaths?.length
728
+ ? { permissions: 'host-workspace' }
729
+ : { sandbox: session.sandbox }),
730
+ config: this.threadConfig(session),
731
+ };
732
+ if (resumeInstructions) {
733
+ resumeParams.baseInstructions = resumeInstructions;
734
+ }
735
+ let result;
736
+ try {
737
+ result = object(await this.request('thread/resume', resumeParams, session.requestTimeout));
738
+ }
739
+ catch (error) {
740
+ // Only an explicit missing-context rejection permits replacement. Transport,
741
+ // authentication and overloaded-server failures retain the durable thread.
742
+ if (error instanceof CodexAppServerRpcError &&
743
+ /no rollout found|thread (?:[^\n]* )?not found/i.test(error.message)) {
744
+ throw new types_js_2.NativeSessionUnavailableError(error.message, { cause: error });
745
+ }
746
+ throw error;
747
+ }
748
+ this.validateResponsePolicy(result, session);
749
+ this.validateInstructionMetadata(result, session);
750
+ const resumed = validateThread(result?.thread);
751
+ if (typeof resumed?.id !== 'string' || resumed.id !== record.threadId) {
752
+ throw new Error('Codex app-server resumed an unexpected thread');
753
+ }
754
+ // Instructions delivered through the protocol make the turn-text bootstrap
755
+ // redundant; without them the legacy <system-reminder> replay still applies.
756
+ return { threadId: record.threadId, bootstrapPending: resumeInstructions === undefined };
757
+ }
758
+ const threadStartParams = {
759
+ model: session.model,
760
+ cwd: session.cwd,
761
+ approvalPolicy: 'never',
762
+ ...(this.options.deniedReadPaths?.length
763
+ ? { permissions: 'host-workspace' }
764
+ : { sandbox: session.sandbox }),
765
+ baseInstructions: session.systemPrompt,
766
+ config: this.threadConfig(session),
767
+ };
768
+ if (session.hostToolBridge) {
769
+ threadStartParams.dynamicTools = session.hostToolBridge.tools;
770
+ }
771
+ const result = object(await this.request('thread/start', threadStartParams, session.requestTimeout));
772
+ this.validateResponsePolicy(result, session);
773
+ this.validateInstructionMetadata(result, session);
774
+ const thread = validateThread(result?.thread);
775
+ if (typeof thread?.id !== 'string' || !thread.id) {
776
+ throw new Error('Codex app-server thread/start returned no thread id');
777
+ }
778
+ this.registry.save({
779
+ sessionKey: session.sessionKey,
780
+ threadId: thread.id,
781
+ model: session.model,
782
+ cwd: session.cwd,
783
+ systemPromptFingerprint: this.policyFingerprint(session),
784
+ mcpConfigFingerprint: launch.fingerprint,
785
+ });
786
+ return { threadId: thread.id, bootstrapPending: false, unproven: true, startupPending: true };
787
+ }
788
+ policyFingerprint(session) {
789
+ const original = session.policyFingerprint ?? (0, codex_thread_registry_js_1.fingerprintText)(session.systemPrompt);
790
+ const base = this.options.deniedReadPaths?.length
791
+ ? (0, codex_thread_registry_js_1.fingerprintText)(JSON.stringify([original, this.options.deniedReadPaths]))
792
+ : original;
793
+ const tools = session.hostToolBridge?.tools;
794
+ const roots = session.restrictedReadRoots;
795
+ if (!tools?.length && !roots) {
796
+ return base;
797
+ }
798
+ return (0, codex_thread_registry_js_1.fingerprintText)(`${base}${tools?.length ? `\n${JSON.stringify(tools)}` : ''}` +
799
+ `${roots ? `\nrestrictedReadRoots=${JSON.stringify(roots)}` : ''}`);
800
+ }
801
+ /** Restrict Codex-native child creation only for a thread with a stated read boundary. */
802
+ threadConfig(session) {
803
+ return session.restrictedReadRoots
804
+ ? { features: { multi_agent: false, multi_agent_v2: false } }
805
+ : {};
806
+ }
807
+ validateInstructionMetadata(result, session) {
808
+ const sources = result?.instructionSources;
809
+ if (!Array.isArray(sources)) {
810
+ throw new Error('Codex app-server returned malformed instruction sources');
811
+ }
812
+ const managedRoots = [session.cwd, this.options.codexHome].map((root) => (0, node_fs_1.realpathSync)(root));
813
+ for (const source of sources) {
814
+ if (typeof source !== 'string') {
815
+ throw new Error('Codex app-server returned malformed instruction source');
816
+ }
817
+ let path;
818
+ try {
819
+ path = (0, node_fs_1.realpathSync)((0, node_path_1.resolve)(session.cwd, source));
820
+ }
821
+ catch (error) {
822
+ throw new Error('Codex app-server loaded an instruction source outside managed roots', {
823
+ cause: error,
824
+ });
825
+ }
826
+ const allowed = managedRoots.some((root) => {
827
+ const childPath = (0, node_path_1.relative)(root, path);
828
+ return (childPath === '' ||
829
+ (childPath !== '..' && !childPath.startsWith(`..${node_path_1.sep}`) && !(0, node_path_1.isAbsolute)(childPath)));
830
+ });
831
+ if (!allowed) {
832
+ throw new Error('Codex app-server loaded an instruction source outside managed roots');
833
+ }
834
+ }
835
+ }
836
+ validateResponsePolicy(result, session) {
837
+ if (!result) {
838
+ throw new Error('Codex app-server returned a malformed thread response');
839
+ }
840
+ if (typeof result.modelProvider !== 'string' ||
841
+ (result.serviceTier !== null && typeof result.serviceTier !== 'string') ||
842
+ !APPROVAL_REVIEWERS.has(String(result.approvalsReviewer)) ||
843
+ (result.reasoningEffort !== null && typeof result.reasoningEffort !== 'string')) {
844
+ throw new Error('Codex app-server returned malformed thread response metadata');
845
+ }
846
+ if (typeof result.model !== 'string' || result.model !== session.model) {
847
+ throw new Error('Codex app-server response model did not match the requested policy');
848
+ }
849
+ if (typeof result.cwd !== 'string' || (0, node_path_1.resolve)(result.cwd) !== session.cwd) {
850
+ throw new Error('Codex app-server response cwd did not match the requested policy');
851
+ }
852
+ if (result.approvalPolicy !== 'never') {
853
+ throw new Error('Codex app-server response approval policy was not never');
854
+ }
855
+ const sandbox = object(result.sandbox);
856
+ const expectedSandboxType = {
857
+ 'read-only': 'readOnly',
858
+ 'workspace-write': 'workspaceWrite',
859
+ 'danger-full-access': 'dangerFullAccess',
860
+ }[session.sandbox];
861
+ if (typeof sandbox?.type !== 'string' || sandbox.type !== expectedSandboxType) {
862
+ throw new Error('Codex app-server response sandbox did not match the requested policy');
863
+ }
864
+ if (sandbox.type === 'readOnly' && typeof sandbox.networkAccess !== 'boolean') {
865
+ throw new Error('Codex app-server returned a malformed read-only sandbox policy');
866
+ }
867
+ if (sandbox.type === 'workspaceWrite' &&
868
+ (!Array.isArray(sandbox.writableRoots) ||
869
+ typeof sandbox.networkAccess !== 'boolean' ||
870
+ typeof sandbox.excludeTmpdirEnvVar !== 'boolean' ||
871
+ typeof sandbox.excludeSlashTmp !== 'boolean')) {
872
+ throw new Error('Codex app-server returned a malformed workspace-write sandbox policy');
873
+ }
874
+ }
875
+ startTurn(threadId, text, callbacks, requestTimeout, hostToolBridge, runTokenBudget, nativeInputId = (0, node_crypto_1.randomUUID)(), restrictedReadRoots) {
876
+ return new Promise((resolveTurn, rejectTurn) => {
877
+ const abortController = new AbortController();
878
+ const timer = setTimeout(() => {
879
+ const error = new Error(`Codex app-server turn timed out after ${requestTimeout}ms`);
880
+ this.timeoutTurn(threadId, error, requestTimeout);
881
+ }, requestTimeout);
882
+ timer.unref();
883
+ const pendingTurn = {
884
+ threadId,
885
+ agentMessagePhases: new Map(),
886
+ usage: { input_tokens: 0, output_tokens: 0 },
887
+ runTokenBudget,
888
+ timer,
889
+ requestTimeout,
890
+ queuedNotifications: [],
891
+ queuedToolRequests: [],
892
+ toolCallQueue: Promise.resolve(),
893
+ toolCallResults: new Map(),
894
+ stoppingCallIds: new Set(),
895
+ hostToolBridge,
896
+ abortController,
897
+ intentionalStop: false,
898
+ onDelta: callbacks?.onDelta,
899
+ onToolUse: callbacks?.onToolUse,
900
+ onToolComplete: callbacks?.onToolComplete,
901
+ onSubagentStart: callbacks?.onSubagentStart,
902
+ nativeItems: new Map(),
903
+ resolve: resolveTurn,
904
+ reject: rejectTurn,
905
+ };
906
+ this.turns.set(threadId, pendingTurn);
907
+ try {
908
+ callbacks?.onInputDispatch?.({
909
+ backend: 'codex',
910
+ sessionId: threadId,
911
+ inputId: nativeInputId,
912
+ });
913
+ }
914
+ catch (error) {
915
+ this.failTurn(threadId, this.toError(error));
916
+ return;
917
+ }
918
+ this.request('turn/start', {
919
+ threadId,
920
+ input: [{ type: 'text', text, text_elements: [] }],
921
+ ...(restrictedReadRoots
922
+ ? {
923
+ sandboxPolicy: {
924
+ type: 'readOnly',
925
+ access: {
926
+ type: 'restricted',
927
+ includePlatformDefaults: true,
928
+ readableRoots: [...restrictedReadRoots],
929
+ },
930
+ },
931
+ }
932
+ : {}),
933
+ }, requestTimeout)
934
+ .then((value) => {
935
+ const turn = validateTurn(object(value)?.turn, 'turn/start turn');
936
+ if (typeof turn?.id !== 'string' || !turn.id) {
937
+ this.failTurn(threadId, new Error('Codex app-server turn/start returned no turn id'));
938
+ return;
939
+ }
940
+ const activeTurn = this.turns.get(threadId);
941
+ if (activeTurn === pendingTurn) {
942
+ activeTurn.turnId = turn.id;
943
+ callbacks?.onAccepted?.({ backend: 'codex', sessionId: threadId, turnId: turn.id });
944
+ const queued = activeTurn.queuedNotifications.splice(0);
945
+ for (const notification of queued) {
946
+ this.handleNotification(notification.method, notification.params);
947
+ }
948
+ const queuedToolRequests = activeTurn.queuedToolRequests.splice(0);
949
+ for (const request of queuedToolRequests) {
950
+ this.handleDynamicToolRequest(request, activeTurn);
951
+ }
952
+ }
953
+ else if (this.turnStartReconciliations.has(threadId)) {
954
+ void this.reconcileAcknowledgedTurn(threadId, turn.id, requestTimeout);
955
+ }
956
+ })
957
+ .catch((error) => {
958
+ if (this.turns.get(threadId) === pendingTurn) {
959
+ this.failTurn(threadId, error instanceof CodexAppServerRpcError && error.code !== OVERLOADED_ERROR_CODE
960
+ ? new CodexTurnRejectedError(this.redact(error.message))
961
+ : this.toError(error));
962
+ return;
963
+ }
964
+ const reconciliation = this.turnStartReconciliations.get(threadId);
965
+ if (!reconciliation) {
966
+ return;
967
+ }
968
+ if (error instanceof CodexAppServerRpcError) {
969
+ this.completeTurnStartReconciliation(threadId, reconciliation);
970
+ }
971
+ else if (!reconciliation.recoveryTimer) {
972
+ void this.shutdown(this.toError(error)).finally(() => {
973
+ this.completeTurnStartReconciliation(threadId, reconciliation);
974
+ });
975
+ }
976
+ });
977
+ });
978
+ }
979
+ async request(method, params, requestTimeout = this.options.requestTimeout) {
980
+ for (let attempt = 0;; attempt++) {
981
+ try {
982
+ return await this.requestOnce(method, params, requestTimeout);
983
+ }
984
+ catch (error) {
985
+ if (!(error instanceof CodexAppServerRpcError) ||
986
+ error.code !== OVERLOADED_ERROR_CODE ||
987
+ attempt >= OVERLOAD_RETRY_LIMIT - 1) {
988
+ throw error;
989
+ }
990
+ const delay = OVERLOAD_RETRY_BASE_MS * 2 ** attempt + Math.floor(Math.random() * 10);
991
+ await new Promise((resolveDelay) => {
992
+ const timer = setTimeout(resolveDelay, delay);
993
+ timer.unref();
994
+ });
995
+ }
996
+ }
997
+ }
998
+ requestOnce(method, params, requestTimeout) {
999
+ const child = this.child;
1000
+ if (!child?.stdin.writable) {
1001
+ return Promise.reject(new Error('Codex app-server stdin is not writable'));
1002
+ }
1003
+ const id = ++this.nextId;
1004
+ return new Promise((resolveRequest, rejectRequest) => {
1005
+ const timer = setTimeout(() => {
1006
+ this.pending.delete(id);
1007
+ const error = new Error(`Codex app-server ${method} timed out after ${requestTimeout}ms`);
1008
+ const threadId = method === 'turn/start' ? object(params)?.threadId : undefined;
1009
+ if (typeof threadId === 'string' && threadId) {
1010
+ const reconciliation = this.getOrCreateTurnStartReconciliation(threadId);
1011
+ reconciliation.recoveryTimer = setTimeout(() => {
1012
+ this.lateTurnStarts.delete(id);
1013
+ void this.shutdown(error).finally(() => {
1014
+ this.completeTurnStartReconciliation(threadId, reconciliation);
1015
+ });
1016
+ }, Math.max(requestTimeout, TURN_START_RECONCILE_GRACE_MS));
1017
+ reconciliation.recoveryTimer.unref();
1018
+ this.lateTurnStarts.set(id, { threadId, requestTimeout, reconciliation });
1019
+ rejectRequest(error);
1020
+ return;
1021
+ }
1022
+ rejectRequest(error);
1023
+ void this.shutdown(error);
1024
+ }, requestTimeout);
1025
+ timer.unref();
1026
+ this.pending.set(id, { method, timer, resolve: resolveRequest, reject: rejectRequest });
1027
+ child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
1028
+ });
1029
+ }
1030
+ notify(method, params) {
1031
+ if (!this.child?.stdin.writable) {
1032
+ throw new Error('Codex app-server stdin is not writable');
1033
+ }
1034
+ const message = params === undefined ? { jsonrpc: '2.0', method } : { jsonrpc: '2.0', method, params };
1035
+ this.child.stdin.write(`${JSON.stringify(message)}\n`);
1036
+ }
1037
+ handleLine(child, line) {
1038
+ if (child !== this.child || !line.trim()) {
1039
+ return;
1040
+ }
1041
+ let message;
1042
+ try {
1043
+ const parsed = JSON.parse(line);
1044
+ const record = object(parsed);
1045
+ if (!record) {
1046
+ throw new Error('not an object');
1047
+ }
1048
+ message = record;
1049
+ }
1050
+ catch {
1051
+ const error = new Error('Codex app-server emitted malformed JSON');
1052
+ this.failAll(error);
1053
+ void this.shutdown(error);
1054
+ return;
1055
+ }
1056
+ // Codex 0.144 accepts JSON-RPC 2.0 requests but omits the `jsonrpc`
1057
+ // member from its responses and notifications on the stdio wire.
1058
+ if (message.jsonrpc !== undefined && message.jsonrpc !== '2.0') {
1059
+ const error = new Error('Codex app-server emitted a malformed protocol message');
1060
+ this.failAll(error);
1061
+ void this.shutdown(error);
1062
+ return;
1063
+ }
1064
+ const hasMethod = typeof message.method === 'string';
1065
+ const hasResult = Object.hasOwn(message, 'result');
1066
+ const hasError = Object.hasOwn(message, 'error');
1067
+ const hasRequestId = typeof message.id === 'number' || typeof message.id === 'string';
1068
+ const serverRequest = hasMethod && hasRequestId && !hasResult && !hasError;
1069
+ const notification = hasMethod && !hasRequestId && !hasResult && !hasError;
1070
+ const response = !hasMethod && typeof message.id === 'number' && hasResult !== hasError;
1071
+ if ([serverRequest, notification, response].filter(Boolean).length !== 1) {
1072
+ const error = new Error('Codex app-server emitted a malformed protocol message');
1073
+ this.failAll(error);
1074
+ void this.shutdown(error);
1075
+ return;
1076
+ }
1077
+ if (typeof message.method === 'string' &&
1078
+ (typeof message.id === 'number' || typeof message.id === 'string')) {
1079
+ this.handleServerRequest(child, message.id, message.method, message.params);
1080
+ return;
1081
+ }
1082
+ if (typeof message.id === 'number') {
1083
+ const lateTurnStart = this.lateTurnStarts.get(message.id);
1084
+ if (lateTurnStart) {
1085
+ this.handleLateTurnStartResponse(message.id, lateTurnStart, message);
1086
+ return;
1087
+ }
1088
+ const pending = this.pending.get(message.id);
1089
+ if (!pending) {
1090
+ const error = new Error('Codex app-server response id did not match a pending request');
1091
+ this.failAll(error);
1092
+ void this.shutdown(error);
1093
+ return;
1094
+ }
1095
+ clearTimeout(pending.timer);
1096
+ this.pending.delete(message.id);
1097
+ if (message.error !== undefined) {
1098
+ const rpcError = object(message.error);
1099
+ const code = typeof rpcError?.code === 'number' ? rpcError.code : 0;
1100
+ pending.reject(new CodexAppServerRpcError(code, this.redact(errorMessage(message.error, `${pending.method} failed`))));
1101
+ }
1102
+ else if (!Object.hasOwn(message, 'result')) {
1103
+ pending.reject(new Error(`Codex app-server ${pending.method} returned a malformed response`));
1104
+ }
1105
+ else {
1106
+ pending.resolve(message.result);
1107
+ }
1108
+ return;
1109
+ }
1110
+ if (typeof message.method === 'string') {
1111
+ this.handleNotification(message.method, message.params);
1112
+ }
1113
+ }
1114
+ handleNotification(method, params) {
1115
+ const data = object(params);
1116
+ if (!data || typeof data.threadId !== 'string') {
1117
+ return;
1118
+ }
1119
+ if (this.handleSubagentNotification(method, data.threadId, data)) {
1120
+ return;
1121
+ }
1122
+ const turn = this.turns.get(data.threadId);
1123
+ if (!turn) {
1124
+ return;
1125
+ }
1126
+ const completedTurn = method === 'turn/completed' ? object(data.turn) : undefined;
1127
+ const eventTurnId = typeof data.turnId === 'string'
1128
+ ? data.turnId
1129
+ : typeof completedTurn?.id === 'string'
1130
+ ? completedTurn.id
1131
+ : undefined;
1132
+ if (eventTurnId && !turn.turnId) {
1133
+ turn.queuedNotifications.push({ method, params });
1134
+ return;
1135
+ }
1136
+ if (eventTurnId && eventTurnId !== turn.turnId) {
1137
+ return;
1138
+ }
1139
+ // TG-03/04/05/06: these notifications observe native effects; they are not
1140
+ // pre-execution hooks. Dynamic/MCP calls retain their existing host bridge.
1141
+ if (method === 'item/started' || method === 'item/completed') {
1142
+ if (typeof data.turnId !== 'string' || data.turnId !== turn.turnId) {
1143
+ return;
1144
+ }
1145
+ const item = object(data.item);
1146
+ if (!item || typeof item.id !== 'string' || typeof item.type !== 'string') {
1147
+ return;
1148
+ }
1149
+ if (item.type === 'agentMessage') {
1150
+ this.refreshTurnIdleTimeout(turn);
1151
+ const phase = item.phase === 'commentary' || item.phase === 'final_answer' ? item.phase : null;
1152
+ turn.agentMessagePhases.set(item.id, phase);
1153
+ if (method === 'item/completed' &&
1154
+ phase === 'final_answer' &&
1155
+ typeof item.text === 'string') {
1156
+ turn.finalText = item.text;
1157
+ }
1158
+ else if (method === 'item/completed' && phase === null && typeof item.text === 'string') {
1159
+ turn.lastUnphasedText = item.text;
1160
+ }
1161
+ else if (method === 'item/completed' && phase === 'commentary') {
1162
+ turn.lastUnphasedText = undefined;
1163
+ }
1164
+ return;
1165
+ }
1166
+ if (item.type === 'reasoning') {
1167
+ // A completed reasoning item is the model making progress; at high effort a
1168
+ // thinking phase can outlast the idle timeout between tool calls.
1169
+ this.refreshTurnIdleTimeout(turn);
1170
+ return;
1171
+ }
1172
+ if (!['commandExecution', 'fileChange', 'webSearch', 'collabAgentToolCall'].includes(item.type)) {
1173
+ return;
1174
+ }
1175
+ this.refreshTurnIdleTimeout(turn);
1176
+ this.observeNativeItem(turn, method, item, turn.threadId);
1177
+ return;
1178
+ }
1179
+ if (method === 'item/agentMessage/delta') {
1180
+ if (typeof data.turnId !== 'string' || data.turnId !== turn.turnId) {
1181
+ return;
1182
+ }
1183
+ if (typeof data.delta !== 'string' || typeof data.itemId !== 'string') {
1184
+ this.failTurn(turn.threadId, new Error('Codex app-server returned malformed agent message delta'));
1185
+ return;
1186
+ }
1187
+ this.refreshTurnIdleTimeout(turn);
1188
+ if (!turn.agentMessagePhases.has(data.itemId)) {
1189
+ this.failTurn(turn.threadId, new Error('Codex app-server delta preceded its agent message item'));
1190
+ return;
1191
+ }
1192
+ if (turn.agentMessagePhases.get(data.itemId) === 'final_answer') {
1193
+ try {
1194
+ turn.onDelta?.(data.delta);
1195
+ }
1196
+ catch (error) {
1197
+ this.failTurn(turn.threadId, this.toError(error));
1198
+ }
1199
+ }
1200
+ return;
1201
+ }
1202
+ if (method === 'thread/tokenUsage/updated') {
1203
+ if (typeof data.turnId !== 'string' || data.turnId !== turn.turnId) {
1204
+ return;
1205
+ }
1206
+ const tokenUsage = object(data.tokenUsage);
1207
+ const last = object(tokenUsage?.last);
1208
+ const total = object(tokenUsage?.total);
1209
+ this.refreshTurnIdleTimeout(turn);
1210
+ const num = (value) => (typeof value === 'number' ? value : 0);
1211
+ if (total) {
1212
+ // Turn usage = cumulative-thread delta. The baseline derives from the
1213
+ // first total-bearing event (total minus its own call, minus any usage
1214
+ // already accumulated from total-less events this turn), so a resumed
1215
+ // thread's unseen history is never attributed to this turn, and a
1216
+ // multi-call tool-loop turn keeps its earlier calls (recording only
1217
+ // `last` undercounted ~5x against the rollout ground truth).
1218
+ turn.usageBaseline ??= {
1219
+ input: num(total.inputTokens) - num(last?.inputTokens) - turn.usage.input_tokens,
1220
+ output: num(total.outputTokens) - num(last?.outputTokens) - turn.usage.output_tokens,
1221
+ cached: num(total.cachedInputTokens) -
1222
+ num(last?.cachedInputTokens) -
1223
+ (turn.usage.cache_read_input_tokens ?? 0),
1224
+ };
1225
+ // A total below the baseline (compaction/rollback resetting the thread
1226
+ // counter) must never write negative usage into the metrics DB.
1227
+ const input = num(total.inputTokens) - turn.usageBaseline.input;
1228
+ const output = num(total.outputTokens) - turn.usageBaseline.output;
1229
+ const cached = num(total.cachedInputTokens) - turn.usageBaseline.cached;
1230
+ if ((input < 0 || output < 0 || cached < 0) && !turn.usageShrinkWarned) {
1231
+ turn.usageShrinkWarned = true;
1232
+ console.warn('[CodexAppServer] cumulative token total shrank below the turn baseline; clamping usage to 0');
1233
+ }
1234
+ turn.usage = {
1235
+ input_tokens: Math.max(0, input),
1236
+ output_tokens: Math.max(0, output),
1237
+ cache_read_input_tokens: Math.max(0, cached),
1238
+ };
1239
+ this.enforceTurnBudget(turn.threadId, turn);
1240
+ }
1241
+ else {
1242
+ // No cumulative total on this event: accumulate per-call usage.
1243
+ turn.usage = {
1244
+ input_tokens: turn.usage.input_tokens + num(last?.inputTokens),
1245
+ output_tokens: turn.usage.output_tokens + num(last?.outputTokens),
1246
+ cache_read_input_tokens: (turn.usage.cache_read_input_tokens ?? 0) + num(last?.cachedInputTokens),
1247
+ };
1248
+ this.enforceTurnBudget(turn.threadId, turn);
1249
+ }
1250
+ return;
1251
+ }
1252
+ if (method !== 'turn/completed') {
1253
+ return;
1254
+ }
1255
+ const completed = completedTurn;
1256
+ if (!completed ||
1257
+ typeof completed.id !== 'string' ||
1258
+ (turn.turnId && completed.id !== turn.turnId))
1259
+ return;
1260
+ const status = completed.status;
1261
+ if (status === 'inProgress') {
1262
+ return;
1263
+ }
1264
+ try {
1265
+ validateTurn(completed, 'turn/completed turn');
1266
+ }
1267
+ catch (error) {
1268
+ this.failTurn(turn.threadId, this.toError(error));
1269
+ return;
1270
+ }
1271
+ if (turn.abortError) {
1272
+ this.failTurn(turn.threadId, turn.abortError);
1273
+ return;
1274
+ }
1275
+ if (status === 'failed') {
1276
+ this.failTurn(turn.threadId, new Error(this.redact(errorMessage(completed.error, 'Codex app-server turn failed'))));
1277
+ return;
1278
+ }
1279
+ if (status === 'interrupted') {
1280
+ if (turn.abortError) {
1281
+ this.failTurn(turn.threadId, turn.abortError);
1282
+ }
1283
+ else if (turn.intentionalStop) {
1284
+ this.resolveTurn(turn);
1285
+ }
1286
+ else {
1287
+ this.failTurn(turn.threadId, new Error('Codex app-server turn was interrupted'));
1288
+ }
1289
+ return;
1290
+ }
1291
+ if (status !== 'completed') {
1292
+ this.failTurn(turn.threadId, new Error(`Codex app-server returned unknown turn status: ${String(status)}`));
1293
+ return;
1294
+ }
1295
+ if (turn.finalText === undefined) {
1296
+ for (const entry of completed.items) {
1297
+ const item = object(entry);
1298
+ if (item?.type === 'agentMessage' &&
1299
+ item.phase === 'final_answer' &&
1300
+ typeof item.text === 'string') {
1301
+ turn.finalText = item.text;
1302
+ }
1303
+ else if (item?.type === 'agentMessage' && typeof item.text === 'string') {
1304
+ if (item.phase === undefined || item.phase === null) {
1305
+ turn.lastUnphasedText = item.text;
1306
+ }
1307
+ else if (item.phase === 'commentary') {
1308
+ turn.lastUnphasedText = undefined;
1309
+ }
1310
+ }
1311
+ }
1312
+ }
1313
+ if (turn.finalText === undefined && turn.lastUnphasedText !== undefined) {
1314
+ // Some supported Codex providers omit phase. Use their terminal item,
1315
+ // never accumulated deltas, so known commentary cannot become the reply.
1316
+ turn.finalText = turn.lastUnphasedText;
1317
+ }
1318
+ if (turn.finalText === undefined) {
1319
+ this.failTurn(turn.threadId, new Error('Codex app-server completed without final_answer'));
1320
+ return;
1321
+ }
1322
+ this.resolveTurn(turn);
1323
+ }
1324
+ resolveTurn(turn) {
1325
+ if (this.turns.get(turn.threadId) !== turn) {
1326
+ return;
1327
+ }
1328
+ this.turns.delete(turn.threadId);
1329
+ clearTimeout(turn.timer);
1330
+ this.clearTurnCallbacks(turn);
1331
+ turn.resolve({
1332
+ response: turn.finalText ?? '',
1333
+ usage: turn.usage,
1334
+ session_id: turn.threadId,
1335
+ toolUseBlocks: undefined,
1336
+ hasToolUse: false,
1337
+ });
1338
+ }
1339
+ // ─── Codex-native subagents ───────────────────────────────────────────────
1340
+ //
1341
+ // Codex announces a child on the PARENT thread as a `subAgentActivity` item and then
1342
+ // drives the child on its OWN thread id, which outlives the parent turn. Nothing here
1343
+ // blocks a run. A child NEVER inherits the parent turn's bridge: the parent's envelope
1344
+ // expires with the parent's wall and the child cannot renew it, so a child that
1345
+ // inherited it would lose every tool mid-run and still report "done". The host issues
1346
+ // the child its own authority instead (`createSubagentBridge`), or the child has none
1347
+ // and its calls are refused loudly.
1348
+ /** Returns true when this notification belongs to the subagent surface and was consumed. */
1349
+ observeNativeItem(target, method, item, threadId, subagent = false) {
1350
+ if (typeof item.id !== 'string' ||
1351
+ typeof item.type !== 'string' ||
1352
+ !['commandExecution', 'fileChange', 'webSearch', 'collabAgentToolCall'].includes(item.type))
1353
+ return;
1354
+ const observationId = nativeObservationId(threadId, item.id);
1355
+ let observed = target.nativeItems.get(observationId);
1356
+ if (!observed) {
1357
+ observed = { name: item.type, completed: false };
1358
+ target.nativeItems.set(observationId, observed);
1359
+ const input = { nativeToolUseId: observationId, providerToolUseId: item.id };
1360
+ if (subagent)
1361
+ input.subagentThreadId = threadId;
1362
+ // Input only: stdout, results and file diffs are not diagnostic inputs.
1363
+ for (const key of ['command', 'cwd', 'action', 'query', 'queries', 'url']) {
1364
+ if (item[key] !== undefined)
1365
+ input[key] = item[key];
1366
+ }
1367
+ if (Array.isArray(item.changes))
1368
+ input.paths = item.changes.map((change) => object(change)?.path);
1369
+ try {
1370
+ target.onToolUse?.(item.type, input);
1371
+ }
1372
+ catch {
1373
+ console.warn('[CodexAppServer] native observation failed');
1374
+ }
1375
+ }
1376
+ if (method === 'item/completed' && !observed.completed) {
1377
+ observed.completed = true;
1378
+ const isError = (item.status !== undefined && item.status !== 'completed') ||
1379
+ (typeof item.exitCode === 'number' && item.exitCode !== 0);
1380
+ try {
1381
+ target.onToolComplete?.(observed.name, observationId, isError);
1382
+ }
1383
+ catch {
1384
+ console.warn('[CodexAppServer] native observation failed');
1385
+ }
1386
+ }
1387
+ }
1388
+ handleSubagentNotification(method, threadId, data) {
1389
+ if (method === 'item/started' || method === 'item/completed') {
1390
+ const item = object(data.item);
1391
+ if (item?.type === 'subAgentActivity') {
1392
+ // Only a thread this process actually drives may announce children.
1393
+ if (!this.subagents.has(threadId) && !this.threadContexts.has(threadId)) {
1394
+ return false;
1395
+ }
1396
+ if (item.kind === 'started') {
1397
+ this.registerSubagent(threadId, item);
1398
+ }
1399
+ else if (item.kind === 'completed') {
1400
+ this.observeParentSideCompletion(threadId, item);
1401
+ }
1402
+ return true;
1403
+ }
1404
+ const child = this.subagents.get(threadId);
1405
+ if (!child) {
1406
+ return false;
1407
+ }
1408
+ this.refreshParentTurnIdleTimeout(child);
1409
+ if (item)
1410
+ this.observeNativeItem(child, method, item, threadId, true);
1411
+ if (method === 'item/completed' &&
1412
+ item?.type === 'agentMessage' &&
1413
+ item.phase === 'final_answer' &&
1414
+ typeof item.text === 'string') {
1415
+ child.finalText = item.text.slice(0, SUBAGENT_FINAL_TEXT_LIMIT);
1416
+ }
1417
+ return true;
1418
+ }
1419
+ const child = this.subagents.get(threadId);
1420
+ if (!child) {
1421
+ return false;
1422
+ }
1423
+ this.refreshParentTurnIdleTimeout(child);
1424
+ if (method !== 'turn/completed') {
1425
+ return true;
1426
+ }
1427
+ const completed = object(data.turn);
1428
+ const status = completed?.status;
1429
+ if (status === 'inProgress') {
1430
+ return true;
1431
+ }
1432
+ if (status !== 'completed' && status !== 'failed' && status !== 'interrupted') {
1433
+ return true;
1434
+ }
1435
+ if (!child.finalText) {
1436
+ child.finalText = this.subagentFinalText(completed?.items);
1437
+ }
1438
+ // The child's OWN turn/completed is the only completing event: the parent-side
1439
+ // announcement carries neither status nor result.
1440
+ this.finishSubagent(threadId, status, status === 'failed'
1441
+ ? this.redact(errorMessage(completed?.error, 'Codex app-server subagent turn failed'))
1442
+ : undefined);
1443
+ return true;
1444
+ }
1445
+ subagentFinalText(items) {
1446
+ if (!Array.isArray(items)) {
1447
+ return '';
1448
+ }
1449
+ let text = '';
1450
+ for (const entry of items) {
1451
+ const item = object(entry);
1452
+ if (item?.type === 'agentMessage' && typeof item.text === 'string') {
1453
+ text = item.text;
1454
+ }
1455
+ }
1456
+ return text.slice(0, SUBAGENT_FINAL_TEXT_LIMIT);
1457
+ }
1458
+ /** A working child keeps the parent's idle timeout alive (the parent may be in wait_agent). */
1459
+ refreshParentTurnIdleTimeout(child) {
1460
+ const parentTurn = this.turns.get(child.parentThreadId);
1461
+ if (parentTurn) {
1462
+ this.refreshTurnIdleTimeout(parentTurn);
1463
+ }
1464
+ }
1465
+ /** Drop every thread context this session left behind on a previous thread id. */
1466
+ pruneThreadContexts(sessionKey, currentThreadId) {
1467
+ for (const [threadId, context] of this.threadContexts) {
1468
+ if (context.sessionKey === sessionKey && threadId !== currentThreadId) {
1469
+ this.threadContexts.delete(threadId);
1470
+ }
1471
+ }
1472
+ }
1473
+ knownThreadIds() {
1474
+ const ids = new Set(this.threadContexts.keys());
1475
+ for (const state of this.sessions.values()) {
1476
+ if (state.threadId) {
1477
+ ids.add(state.threadId);
1478
+ }
1479
+ }
1480
+ for (const threadId of this.turns.keys()) {
1481
+ ids.add(threadId);
1482
+ }
1483
+ return ids;
1484
+ }
1485
+ registerSubagent(parentThreadId, item) {
1486
+ const agentThreadId = typeof item.agentThreadId === 'string' ? item.agentThreadId : '';
1487
+ if (!agentThreadId || this.subagents.has(agentThreadId)) {
1488
+ // The same `started` announcement arrives twice (item/started + item/completed views).
1489
+ return;
1490
+ }
1491
+ if (this.finishedSubagents.has(agentThreadId)) {
1492
+ // The second view of that announcement lost the race with the child's own
1493
+ // turn/completed. Re-registering would resurrect a finished child.
1494
+ console.warn(`[CodexAppServer] subagent announcement ignored: thread=${agentThreadId} already finished`);
1495
+ return;
1496
+ }
1497
+ // A child may never claim a thread this process already drives: that would let one
1498
+ // session's announcement capture another session's live parent thread.
1499
+ if (this.knownThreadIds().has(agentThreadId)) {
1500
+ console.warn(`[CodexAppServer] subagent announcement refused: thread=${agentThreadId} is a live thread`);
1501
+ return;
1502
+ }
1503
+ // A grandchild's session comes from the child that announced it.
1504
+ const parent = this.subagents.get(parentThreadId) ?? this.threadContexts.get(parentThreadId);
1505
+ if (!parent) {
1506
+ return;
1507
+ }
1508
+ const agentPath = typeof item.agentPath === 'string' ? item.agentPath : '';
1509
+ // Observed admission: tell the PARENT'S live turn that this run handed work to a
1510
+ // child. Nothing else on the item stream carries that fact - on codex-cli 0.153.4
1511
+ // a spawn surfaces ONLY as `subAgentActivity`, which this handler consumes before
1512
+ // the native-item path. Never routed through onToolUse (effect ledger, see
1513
+ // PromptCallbacks.onSubagentStart).
1514
+ const parentTurn = this.turns.get(parentThreadId);
1515
+ if (parentTurn?.onSubagentStart) {
1516
+ const itemId = typeof item.id === 'string' ? item.id : '';
1517
+ try {
1518
+ parentTurn.onSubagentStart({ agentThreadId, agentPath, itemId });
1519
+ }
1520
+ catch (error) {
1521
+ console.warn(`[CodexAppServer] onSubagentStart callback failed: ${this.toError(error).message}`);
1522
+ }
1523
+ }
1524
+ const factory = this.options.createSubagentBridge;
1525
+ const child = {
1526
+ parentThreadId,
1527
+ sessionKey: parent.sessionKey,
1528
+ agentPath,
1529
+ onToolUse: parent.onToolUse,
1530
+ onToolComplete: parent.onToolComplete,
1531
+ nativeItems: new Map(),
1532
+ finalText: '',
1533
+ startedAt: Date.now(),
1534
+ toolCallQueue: Promise.resolve(),
1535
+ toolCallResults: new Map(),
1536
+ stoppingCallIds: new Set(),
1537
+ abortController: new AbortController(),
1538
+ authorityWarned: false,
1539
+ authority: factory
1540
+ ? factory({
1541
+ sessionKey: parent.sessionKey,
1542
+ parentThreadId,
1543
+ agentThreadId,
1544
+ agentPath,
1545
+ })
1546
+ .then((authority) => authority ? { ...authority, bridge: snapshotHostToolBridge(authority.bridge) } : null)
1547
+ .catch((error) => {
1548
+ console.warn(`[CodexAppServer] subagent authority factory failed thread=${agentThreadId}: ${this.toError(error).message}`);
1549
+ return null;
1550
+ })
1551
+ : Promise.resolve(null),
1552
+ };
1553
+ child.ttlTimer = setTimeout(() => {
1554
+ child.ttlTimer = undefined;
1555
+ this.finishSubagent(agentThreadId, 'failed', 'subagent produced no completion');
1556
+ }, this.options.subagentTtlMs ?? SUBAGENT_TTL_MS);
1557
+ child.ttlTimer.unref();
1558
+ this.subagents.set(agentThreadId, child);
1559
+ console.log(`[CodexAppServer] subagent started path=${agentPath} thread=${agentThreadId}`);
1560
+ this.emitSubagentEvent({
1561
+ kind: 'started',
1562
+ sessionKey: parent.sessionKey,
1563
+ parentThreadId,
1564
+ agentThreadId,
1565
+ agentPath,
1566
+ });
1567
+ }
1568
+ /**
1569
+ * The parent says a child finished. That is an announcement, not a result: it can win
1570
+ * the race against the child's own `turn/completed`, which is what carries status and
1571
+ * final text. Wait a bounded grace for the real event, then report `unknown`.
1572
+ */
1573
+ observeParentSideCompletion(announcingThreadId, item) {
1574
+ const agentThreadId = typeof item.agentThreadId === 'string' ? item.agentThreadId : '';
1575
+ const child = agentThreadId ? this.subagents.get(agentThreadId) : undefined;
1576
+ if (!child) {
1577
+ return;
1578
+ }
1579
+ if (child.parentThreadId !== announcingThreadId) {
1580
+ console.warn(`[CodexAppServer] subagent completion refused: thread=${announcingThreadId} does not own ${agentThreadId}`);
1581
+ return;
1582
+ }
1583
+ if (child.graceTimer) {
1584
+ return;
1585
+ }
1586
+ child.graceTimer = setTimeout(() => {
1587
+ child.graceTimer = undefined;
1588
+ console.warn(`[CodexAppServer] subagent completion unconfirmed path=${child.agentPath} thread=${agentThreadId}`);
1589
+ this.finishSubagent(agentThreadId, 'unknown');
1590
+ }, this.options.subagentGraceMs ?? SUBAGENT_COMPLETION_GRACE_MS);
1591
+ child.graceTimer.unref();
1592
+ }
1593
+ /**
1594
+ * Emits exactly one completion per child thread and releases its authority with the
1595
+ * same terminal status. Grandchildren keep their own entries.
1596
+ */
1597
+ finishSubagent(agentThreadId, status, error) {
1598
+ const child = this.subagents.get(agentThreadId);
1599
+ if (!child) {
1600
+ return;
1601
+ }
1602
+ if (!child.completionEvent) {
1603
+ for (const [callId, item] of child.nativeItems) {
1604
+ if (item.completed)
1605
+ continue;
1606
+ item.completed = true;
1607
+ try {
1608
+ child.onToolComplete?.(item.name, callId, true, 'unknown');
1609
+ }
1610
+ catch {
1611
+ console.warn('[CodexAppServer] native observation failed');
1612
+ }
1613
+ }
1614
+ this.clearSubagentTimers(child);
1615
+ const finalText = child.finalText ? this.redact(child.finalText) : '';
1616
+ child.completionEvent = {
1617
+ kind: 'completed',
1618
+ sessionKey: child.sessionKey,
1619
+ parentThreadId: child.parentThreadId,
1620
+ agentThreadId,
1621
+ agentPath: child.agentPath,
1622
+ status,
1623
+ ...(finalText ? { finalText } : {}),
1624
+ ...(error ? { error } : {}),
1625
+ };
1626
+ }
1627
+ this.deliverSubagentCompletion(child);
1628
+ }
1629
+ deliverSubagentCompletion(child) {
1630
+ const event = child.completionEvent;
1631
+ if (!event || this.subagents.get(event.agentThreadId) !== child)
1632
+ return;
1633
+ if (!this.emitSubagentEvent(event)) {
1634
+ if (!child.completionRetryTimer) {
1635
+ child.completionRetryTimer = setTimeout(() => {
1636
+ child.completionRetryTimer = undefined;
1637
+ this.deliverSubagentCompletion(child);
1638
+ }, 1_000);
1639
+ child.completionRetryTimer.unref();
1640
+ }
1641
+ return;
1642
+ }
1643
+ this.subagents.delete(event.agentThreadId);
1644
+ this.rememberFinishedSubagent(event.agentThreadId);
1645
+ this.clearSubagentTimers(child);
1646
+ console.log(`[CodexAppServer] subagent completed status=${event.status} path=${child.agentPath} thread=${event.agentThreadId}`);
1647
+ void child.authority
1648
+ .then(async (authority) => {
1649
+ if (!authority) {
1650
+ return;
1651
+ }
1652
+ await authority.release({
1653
+ status: event.status ?? 'unknown',
1654
+ ...(event.error ? { error: event.error } : {}),
1655
+ });
1656
+ })
1657
+ .catch((releaseError) => {
1658
+ console.warn(`[CodexAppServer] subagent authority release failed thread=${event.agentThreadId}: ${this.toError(releaseError).message}`);
1659
+ });
1660
+ }
1661
+ /** FIFO-bounded identity of finished children; the oldest id is forgotten first. */
1662
+ rememberFinishedSubagent(agentThreadId) {
1663
+ this.finishedSubagents.add(agentThreadId);
1664
+ while (this.finishedSubagents.size > MAX_FINISHED_SUBAGENTS) {
1665
+ const oldest = this.finishedSubagents.values().next().value;
1666
+ if (oldest === undefined) {
1667
+ return;
1668
+ }
1669
+ this.finishedSubagents.delete(oldest);
1670
+ }
1671
+ }
1672
+ clearSubagentTimers(child) {
1673
+ if (child.ttlTimer) {
1674
+ clearTimeout(child.ttlTimer);
1675
+ child.ttlTimer = undefined;
1676
+ }
1677
+ if (child.graceTimer) {
1678
+ clearTimeout(child.graceTimer);
1679
+ child.graceTimer = undefined;
1680
+ }
1681
+ if (child.completionRetryTimer) {
1682
+ clearTimeout(child.completionRetryTimer);
1683
+ child.completionRetryTimer = undefined;
1684
+ }
1685
+ }
1686
+ emitSubagentEvent(event) {
1687
+ const emit = this.options.onSubagentEvent;
1688
+ if (!emit) {
1689
+ return true;
1690
+ }
1691
+ try {
1692
+ emit(event);
1693
+ return true;
1694
+ }
1695
+ catch (error) {
1696
+ console.warn(`[CodexAppServer] subagent event listener failed: ${this.toError(error).message}`);
1697
+ return false;
1698
+ }
1699
+ }
1700
+ /** Every live child is reported interrupted and its authority released. */
1701
+ terminateSubagents(reason) {
1702
+ const safe = this.toError(reason);
1703
+ for (const agentThreadId of [...this.subagents.keys()]) {
1704
+ const child = this.subagents.get(agentThreadId);
1705
+ if (!child) {
1706
+ continue;
1707
+ }
1708
+ child.abortController.abort(safe);
1709
+ this.finishSubagent(agentThreadId, 'interrupted', safe.message);
1710
+ }
1711
+ }
1712
+ /** A reset/restart drops the thread's context and interrupts its children. */
1713
+ discardSessionThreadState(sessionKey, reason) {
1714
+ const threadId = this.sessions.get(sessionKey)?.threadId;
1715
+ if (threadId) {
1716
+ this.threadContexts.delete(threadId);
1717
+ }
1718
+ for (const agentThreadId of [...this.subagents.keys()]) {
1719
+ const child = this.subagents.get(agentThreadId);
1720
+ if (!child || child.sessionKey !== sessionKey) {
1721
+ continue;
1722
+ }
1723
+ child.abortController.abort(new Error(`Codex app-server ${reason}`));
1724
+ this.finishSubagent(agentThreadId, 'interrupted', reason);
1725
+ }
1726
+ }
1727
+ handleSubagentToolRequest(request, agentThreadId, child, data) {
1728
+ this.refreshParentTurnIdleTimeout(child);
1729
+ // The child's authority is host-issued and asynchronous; its calls queue until it
1730
+ // resolves rather than falling back to anything the parent held.
1731
+ void child.authority.then((authority) => {
1732
+ try {
1733
+ const turnId = this.requiredToolString(data, 'turnId');
1734
+ const callId = this.requiredToolString(data, 'callId');
1735
+ const tool = this.requiredToolString(data, 'tool');
1736
+ if (data.namespace !== null && typeof data.namespace !== 'string') {
1737
+ throw new Error('Codex app-server tool call namespace must be null or a string');
1738
+ }
1739
+ const input = object(data.arguments);
1740
+ if (!input) {
1741
+ throw new Error('Codex app-server tool call arguments must be an object');
1742
+ }
1743
+ if (!authority) {
1744
+ if (!child.authorityWarned) {
1745
+ child.authorityWarned = true;
1746
+ console.warn(`[CodexAppServer] ${SUBAGENT_AUTHORITY_UNAVAILABLE} path=${child.agentPath} thread=${agentThreadId}`);
1747
+ }
1748
+ this.replyToolError(request, `${SUBAGENT_AUTHORITY_UNAVAILABLE}: ${tool} was refused`);
1749
+ return;
1750
+ }
1751
+ const bridge = authority.bridge;
1752
+ if (!bridge.tools.some((definition) => definition.name === tool)) {
1753
+ throw new Error(`Codex app-server tool call ${tool} was not advertised`);
1754
+ }
1755
+ this.dispatchSubagentToolCall(request, agentThreadId, child, bridge, {
1756
+ turnId,
1757
+ callId,
1758
+ tool,
1759
+ namespace: data.namespace ?? null,
1760
+ input,
1761
+ });
1762
+ }
1763
+ catch (error) {
1764
+ // A child's protocol error is reported to the child only: the parent turn it was
1765
+ // spawned from may already have resolved, and there is nothing here to fail.
1766
+ this.replyToolError(request, this.toError(error).message);
1767
+ }
1768
+ });
1769
+ }
1770
+ dispatchSubagentToolCall(request, agentThreadId, child, bridge, call) {
1771
+ const { turnId, callId, tool, namespace, input } = call;
1772
+ const identity = JSON.stringify(stableJson({ threadId: agentThreadId, turnId, tool, namespace, arguments: input }));
1773
+ const existing = child.toolCallResults.get(callId);
1774
+ if (existing && existing.identity !== identity) {
1775
+ throw new Error(`Codex app-server callId ${callId} had a conflicting request`);
1776
+ }
1777
+ let execution = existing?.execution;
1778
+ if (!execution) {
1779
+ execution = child.toolCallQueue
1780
+ .then(async () => {
1781
+ if (!this.isSubagentToolActive(request, agentThreadId, child)) {
1782
+ return {
1783
+ result: this.toolResult(false, 'Codex app-server tool call is no longer active'),
1784
+ stop: false,
1785
+ abortError: undefined,
1786
+ };
1787
+ }
1788
+ try {
1789
+ child.onToolUse?.(tool, {
1790
+ nativeToolUseId: nativeObservationId(agentThreadId, callId),
1791
+ providerToolUseId: callId,
1792
+ subagentThreadId: agentThreadId,
1793
+ agentPath: child.agentPath,
1794
+ });
1795
+ }
1796
+ catch (error) {
1797
+ console.warn(`[CodexAppServer] subagent onToolUse failed: ${this.toError(error).message}`);
1798
+ }
1799
+ let result;
1800
+ try {
1801
+ result = await bridge.execute({
1802
+ callId,
1803
+ name: tool,
1804
+ input,
1805
+ signal: child.abortController.signal,
1806
+ });
1807
+ }
1808
+ catch (error) {
1809
+ result = { content: this.toError(error).message, isError: true };
1810
+ }
1811
+ const resultData = object(result);
1812
+ if (!resultData ||
1813
+ typeof resultData.content !== 'string' ||
1814
+ typeof resultData.isError !== 'boolean') {
1815
+ this.reportSubagentToolComplete(child, tool, nativeObservationId(agentThreadId, callId), true);
1816
+ return {
1817
+ result: this.toolResult(false, 'Host tool returned a malformed result'),
1818
+ stop: false,
1819
+ abortError: undefined,
1820
+ };
1821
+ }
1822
+ this.reportSubagentToolComplete(child, tool, nativeObservationId(agentThreadId, callId), resultData.isError);
1823
+ return {
1824
+ result: this.toolResult(!resultData.isError, resultData.content),
1825
+ stop: resultData.stop === true || resultData.abort === true,
1826
+ abortError: undefined,
1827
+ };
1828
+ })
1829
+ .catch((error) => ({
1830
+ result: this.toolResult(false, this.toError(error).message),
1831
+ stop: false,
1832
+ abortError: undefined,
1833
+ }));
1834
+ child.toolCallResults.set(callId, { identity, execution });
1835
+ child.toolCallQueue = execution.then(() => undefined, () => undefined);
1836
+ }
1837
+ void execution.then(({ result, stop }) => {
1838
+ if (!this.isSubagentToolActive(request, agentThreadId, child)) {
1839
+ if (this.child === request.child) {
1840
+ this.replyToolError(request, 'Codex app-server tool call is no longer active');
1841
+ }
1842
+ return;
1843
+ }
1844
+ this.reply(request.child, { jsonrpc: '2.0', id: request.id, result });
1845
+ if (stop && !child.stoppingCallIds.has(callId)) {
1846
+ child.stoppingCallIds.add(callId);
1847
+ child.abortController.abort(new Error('Codex app-server subagent tool stopped the run'));
1848
+ void this.request('turn/interrupt', { threadId: agentThreadId, turnId }).catch((error) => console.warn(`[CodexAppServer] subagent interrupt failed: ${this.toError(error).message}`));
1849
+ }
1850
+ });
1851
+ }
1852
+ reportSubagentToolComplete(child, tool, callId, isError) {
1853
+ try {
1854
+ child.onToolComplete?.(tool, callId, isError);
1855
+ }
1856
+ catch (error) {
1857
+ console.warn(`[CodexAppServer] subagent onToolComplete failed: ${this.toError(error).message}`);
1858
+ }
1859
+ }
1860
+ isSubagentToolActive(request, agentThreadId, child) {
1861
+ return (this.child === request.child &&
1862
+ this.subagents.get(agentThreadId) === child &&
1863
+ !child.abortController.signal.aborted);
1864
+ }
1865
+ handleServerRequest(child, id, method, params) {
1866
+ if (method === 'item/tool/call') {
1867
+ this.handleDynamicToolRequest({ child, id, params });
1868
+ return;
1869
+ }
1870
+ const bodies = {
1871
+ 'item/tool/requestUserInput': { answers: {} },
1872
+ 'mcpServer/elicitation/request': { action: 'decline', content: null, _meta: null },
1873
+ 'item/commandExecution/requestApproval': { decision: 'decline' },
1874
+ 'item/fileChange/requestApproval': { decision: 'decline' },
1875
+ 'item/permissions/requestApproval': {
1876
+ permissions: {},
1877
+ scope: 'turn',
1878
+ strictAutoReview: true,
1879
+ },
1880
+ applyPatchApproval: { decision: 'denied' },
1881
+ execCommandApproval: { decision: 'denied' },
1882
+ };
1883
+ if (Object.hasOwn(bodies, method)) {
1884
+ this.reply(child, { jsonrpc: '2.0', id, result: bodies[method] });
1885
+ }
1886
+ else {
1887
+ this.reply(child, {
1888
+ jsonrpc: '2.0',
1889
+ id,
1890
+ error: { code: -32601, message: `Unsupported app-server request: ${method}` },
1891
+ });
1892
+ }
1893
+ }
1894
+ handleDynamicToolRequest(request, expectedTurn) {
1895
+ const data = object(request.params);
1896
+ const threadId = typeof data?.threadId === 'string' ? data.threadId : undefined;
1897
+ const turn = threadId ? this.turns.get(threadId) : undefined;
1898
+ if (data && threadId && !turn && !expectedTurn) {
1899
+ // A Codex-native child calls on ITS OWN thread id, which never holds a PendingTurn.
1900
+ const child = this.subagents.get(threadId);
1901
+ if (child) {
1902
+ this.handleSubagentToolRequest(request, threadId, child, data);
1903
+ return;
1904
+ }
1905
+ }
1906
+ if (!data || !turn || !turn.hostToolBridge || (expectedTurn && turn !== expectedTurn)) {
1907
+ this.replyDisabledTool(request);
1908
+ return;
1909
+ }
1910
+ try {
1911
+ const turnId = this.requiredToolString(data, 'turnId');
1912
+ const callId = this.requiredToolString(data, 'callId');
1913
+ const tool = this.requiredToolString(data, 'tool');
1914
+ if (data.namespace !== null && typeof data.namespace !== 'string') {
1915
+ throw new Error('Codex app-server tool call namespace must be null or a string');
1916
+ }
1917
+ const input = object(data.arguments);
1918
+ if (!input) {
1919
+ throw new Error('Codex app-server tool call arguments must be an object');
1920
+ }
1921
+ if (!turn.hostToolBridge.tools.some((definition) => definition.name === tool)) {
1922
+ throw new Error(`Codex app-server tool call ${tool} was not advertised`);
1923
+ }
1924
+ if (!turn.turnId) {
1925
+ turn.queuedToolRequests.push(request);
1926
+ return;
1927
+ }
1928
+ if (turnId !== turn.turnId) {
1929
+ this.failToolProtocol(request, turn, new Error('Codex app-server tool call did not match the active turn'));
1930
+ return;
1931
+ }
1932
+ this.refreshTurnIdleTimeout(turn);
1933
+ this.dispatchDynamicToolCall(request, turn, callId, tool, data.namespace, input);
1934
+ }
1935
+ catch (error) {
1936
+ this.failToolProtocol(request, turn, this.toError(error));
1937
+ }
1938
+ }
1939
+ failToolProtocol(request, turn, error) {
1940
+ const failure = this.toError(error);
1941
+ this.replyToolError(request, failure.message);
1942
+ this.failTurn(turn.threadId, failure);
1943
+ if (this.child === request.child && !this.shutdownPromise) {
1944
+ void this.shutdown(failure);
1945
+ }
1946
+ }
1947
+ requiredToolString(data, field) {
1948
+ const value = data[field];
1949
+ if (typeof value !== 'string' || !value) {
1950
+ throw new Error(`Codex app-server tool call ${field} must be a string`);
1951
+ }
1952
+ return value;
1953
+ }
1954
+ dispatchDynamicToolCall(request, turn, callId, tool, namespace, input) {
1955
+ const identity = JSON.stringify(stableJson({
1956
+ threadId: turn.threadId,
1957
+ turnId: turn.turnId,
1958
+ tool,
1959
+ namespace,
1960
+ arguments: input,
1961
+ }));
1962
+ const existing = turn.toolCallResults.get(callId);
1963
+ if (existing && existing.identity !== identity) {
1964
+ throw new Error(`Codex app-server callId ${callId} had a conflicting request`);
1965
+ }
1966
+ let execution = existing?.execution;
1967
+ if (!execution) {
1968
+ const bridge = turn.hostToolBridge;
1969
+ if (!bridge) {
1970
+ this.replyDisabledTool(request);
1971
+ return;
1972
+ }
1973
+ execution = turn.toolCallQueue
1974
+ .then(async () => {
1975
+ if (!this.isToolTurnActive(request, turn)) {
1976
+ return {
1977
+ result: this.toolResult(false, 'Codex app-server tool call is no longer active'),
1978
+ stop: false,
1979
+ abortError: undefined,
1980
+ };
1981
+ }
1982
+ let result;
1983
+ try {
1984
+ result = await bridge.execute({
1985
+ callId,
1986
+ name: tool,
1987
+ input,
1988
+ signal: turn.abortController.signal,
1989
+ });
1990
+ }
1991
+ catch (error) {
1992
+ result = { content: this.toError(error).message, isError: true };
1993
+ }
1994
+ const resultData = object(result);
1995
+ if (!resultData ||
1996
+ typeof resultData.content !== 'string' ||
1997
+ typeof resultData.isError !== 'boolean') {
1998
+ return {
1999
+ result: this.toolResult(false, 'Host tool returned a malformed result'),
2000
+ stop: false,
2001
+ abortError: undefined,
2002
+ };
2003
+ }
2004
+ const abortError = resultData.abort === true && resultData.isError
2005
+ ? (0, types_js_1.isHostToolTerminalCode)(resultData.terminalCode)
2006
+ ? new types_js_1.HostToolTerminalError(resultData.terminalCode, resultData.content)
2007
+ : new Error(resultData.content)
2008
+ : undefined;
2009
+ if (abortError instanceof types_js_1.HostToolTerminalError) {
2010
+ turn.settledTerminalError ??= abortError;
2011
+ }
2012
+ return {
2013
+ result: this.toolResult(!resultData.isError, resultData.content),
2014
+ stop: resultData.stop === true && !resultData.isError,
2015
+ abortError,
2016
+ };
2017
+ })
2018
+ .catch((error) => ({
2019
+ result: this.toolResult(false, this.toError(error).message),
2020
+ stop: false,
2021
+ abortError: undefined,
2022
+ }));
2023
+ turn.toolCallResults.set(callId, { identity, execution });
2024
+ turn.toolCallQueue = execution.then(() => undefined, () => undefined);
2025
+ }
2026
+ void execution.then(({ result, stop, abortError }) => {
2027
+ if (!this.isToolTurnActive(request, turn)) {
2028
+ if (this.child === request.child) {
2029
+ this.replyToolError(request, 'Codex app-server tool call is no longer active');
2030
+ }
2031
+ return;
2032
+ }
2033
+ this.refreshTurnIdleTimeout(turn);
2034
+ this.reply(request.child, { jsonrpc: '2.0', id: request.id, result });
2035
+ if (abortError && !turn.stoppingCallIds.has(callId)) {
2036
+ turn.stoppingCallIds.add(callId);
2037
+ turn.abortError = abortError;
2038
+ void this.request('turn/interrupt', { threadId: turn.threadId, turnId: turn.turnId }).catch((error) => this.failTurn(turn.threadId, this.toError(error)));
2039
+ }
2040
+ else if (stop && !turn.stoppingCallIds.has(callId)) {
2041
+ turn.stoppingCallIds.add(callId);
2042
+ turn.intentionalStop = true;
2043
+ void this.request('turn/interrupt', { threadId: turn.threadId, turnId: turn.turnId }).catch((error) => this.failTurn(turn.threadId, this.toError(error)));
2044
+ }
2045
+ });
2046
+ }
2047
+ isToolTurnActive(request, turn) {
2048
+ return (this.child === request.child &&
2049
+ this.turns.get(turn.threadId) === turn &&
2050
+ !turn.intentionalStop &&
2051
+ !turn.abortError &&
2052
+ !turn.abortController.signal.aborted);
2053
+ }
2054
+ toolResult(success, content) {
2055
+ return { success, contentItems: [{ type: 'inputText', text: content }] };
2056
+ }
2057
+ replyDisabledTool(request) {
2058
+ this.reply(request.child, {
2059
+ jsonrpc: '2.0',
2060
+ id: request.id,
2061
+ result: this.toolResult(false, 'Native app-server tools are disabled by MAMA'),
2062
+ });
2063
+ }
2064
+ replyToolError(request, message) {
2065
+ this.reply(request.child, {
2066
+ jsonrpc: '2.0',
2067
+ id: request.id,
2068
+ error: { code: -32602, message },
2069
+ });
2070
+ }
2071
+ reply(child, message) {
2072
+ if (child.stdin.writable) {
2073
+ child.stdin.write(`${JSON.stringify(message)}\n`);
2074
+ }
2075
+ }
2076
+ handleStderr(line) {
2077
+ this.stderrTail = `${this.stderrTail}${this.redact(line)}\n`.slice(-STDERR_LIMIT);
2078
+ }
2079
+ redact(value) {
2080
+ return (0, cli_secret_redaction_js_1.redactConfiguredSecrets)(value, this.secrets);
2081
+ }
2082
+ toError(error) {
2083
+ if (error instanceof types_js_1.HostToolTerminalError) {
2084
+ return new types_js_1.HostToolTerminalError(error.terminalCode, this.redact(error.message));
2085
+ }
2086
+ if (error instanceof CodexTurnRejectedError) {
2087
+ return new CodexTurnRejectedError(this.redact(error.message));
2088
+ }
2089
+ return error instanceof Error
2090
+ ? new Error(this.redact(error.message))
2091
+ : new Error(this.redact(String(error)));
2092
+ }
2093
+ refreshTurnIdleTimeout(turn) {
2094
+ if (this.turns.get(turn.threadId) !== turn) {
2095
+ return;
2096
+ }
2097
+ clearTimeout(turn.timer);
2098
+ turn.timer = setTimeout(() => {
2099
+ const error = new Error(`Codex app-server turn timed out after ${turn.requestTimeout}ms without progress`);
2100
+ this.timeoutTurn(turn.threadId, error, turn.requestTimeout);
2101
+ }, turn.requestTimeout);
2102
+ turn.timer.unref();
2103
+ }
2104
+ failTurn(threadId, error) {
2105
+ const turn = this.turns.get(threadId);
2106
+ if (!turn) {
2107
+ return;
2108
+ }
2109
+ this.turns.delete(threadId);
2110
+ clearTimeout(turn.timer);
2111
+ turn.abortController.abort(error);
2112
+ turn.queuedNotifications.length = 0;
2113
+ turn.queuedToolRequests.length = 0;
2114
+ const safe = this.toError(error);
2115
+ // A gateway mutation that cannot be interrupted must settle before the
2116
+ // caller sees failure. This prevents retries from racing a late mutation.
2117
+ void turn.toolCallQueue.finally(() => {
2118
+ this.clearTurnCallbacks(turn);
2119
+ const terminalError = turn.settledTerminalError ??
2120
+ (turn.abortError instanceof types_js_1.HostToolTerminalError ? turn.abortError : undefined);
2121
+ turn.reject(this.toError(terminalError ?? safe));
2122
+ });
2123
+ }
2124
+ /** Interrupt a turn whose counted usage crossed its run budget; the loop reports stoppedBy. */
2125
+ enforceTurnBudget(threadId, turn) {
2126
+ const budget = turn.runTokenBudget ?? 0;
2127
+ if (budget <= 0 || turn.budgetStopped) {
2128
+ return;
2129
+ }
2130
+ // Codex reports inputTokens INCLUSIVE of cachedInputTokens (OpenAI usage semantics),
2131
+ // so cached is not added again: 0.44.0 counted a 2.8M wiki turn as 5.5M.
2132
+ const counted = turn.usage.input_tokens + turn.usage.output_tokens;
2133
+ if (counted < budget) {
2134
+ return;
2135
+ }
2136
+ turn.budgetStopped = true;
2137
+ const error = new Error(`run budget stop: ${counted} >= ${budget} counted tokens inside one turn`);
2138
+ error.code = 'RUN_BUDGET_STOP';
2139
+ // The interrupted turn's usage rides on the error: the loop's success path never
2140
+ // runs for it, and the most expensive run of the day must not count as zero.
2141
+ error.usage = { ...turn.usage };
2142
+ this.timeoutTurn(threadId, error, DEFAULT_TIMEOUT);
2143
+ }
2144
+ timeoutTurn(threadId, error, requestTimeout) {
2145
+ const turn = this.turns.get(threadId);
2146
+ if (!turn) {
2147
+ return;
2148
+ }
2149
+ const turnId = turn.turnId;
2150
+ if (!turnId) {
2151
+ this.getOrCreateTurnStartReconciliation(threadId);
2152
+ }
2153
+ this.failTurn(threadId, error);
2154
+ if (!turnId) {
2155
+ return;
2156
+ }
2157
+ void this.request('turn/interrupt', { threadId, turnId }, requestTimeout).catch((interruptError) => {
2158
+ if (this.child && !this.shutdownPromise) {
2159
+ void this.shutdown(this.toError(interruptError));
2160
+ }
2161
+ });
2162
+ }
2163
+ getOrCreateTurnStartReconciliation(threadId) {
2164
+ const existing = this.turnStartReconciliations.get(threadId);
2165
+ if (existing) {
2166
+ return existing;
2167
+ }
2168
+ let resolveReconciliation;
2169
+ const promise = new Promise((resolvePromise) => {
2170
+ resolveReconciliation = resolvePromise;
2171
+ });
2172
+ const reconciliation = {
2173
+ promise,
2174
+ resolve: () => resolveReconciliation?.(),
2175
+ };
2176
+ this.turnStartReconciliations.set(threadId, reconciliation);
2177
+ return reconciliation;
2178
+ }
2179
+ completeTurnStartReconciliation(threadId, reconciliation) {
2180
+ if (this.turnStartReconciliations.get(threadId) !== reconciliation) {
2181
+ return;
2182
+ }
2183
+ if (reconciliation.recoveryTimer) {
2184
+ clearTimeout(reconciliation.recoveryTimer);
2185
+ reconciliation.recoveryTimer = undefined;
2186
+ }
2187
+ this.turnStartReconciliations.delete(threadId);
2188
+ reconciliation.resolve();
2189
+ }
2190
+ async reconcileAcknowledgedTurn(threadId, turnId, requestTimeout) {
2191
+ const reconciliation = this.turnStartReconciliations.get(threadId);
2192
+ if (!reconciliation) {
2193
+ return;
2194
+ }
2195
+ try {
2196
+ await this.request('turn/interrupt', { threadId, turnId }, requestTimeout);
2197
+ this.completeTurnStartReconciliation(threadId, reconciliation);
2198
+ }
2199
+ catch (error) {
2200
+ await this.shutdown(this.toError(error));
2201
+ this.completeTurnStartReconciliation(threadId, reconciliation);
2202
+ }
2203
+ }
2204
+ handleLateTurnStartResponse(id, late, message) {
2205
+ this.lateTurnStarts.delete(id);
2206
+ if (late.reconciliation.recoveryTimer) {
2207
+ clearTimeout(late.reconciliation.recoveryTimer);
2208
+ late.reconciliation.recoveryTimer = undefined;
2209
+ }
2210
+ if (message.error !== undefined) {
2211
+ this.completeTurnStartReconciliation(late.threadId, late.reconciliation);
2212
+ return;
2213
+ }
2214
+ try {
2215
+ const turn = validateTurn(object(message.result)?.turn, 'late turn/start turn');
2216
+ if (typeof turn?.id !== 'string' || !turn.id) {
2217
+ throw new Error('Codex app-server late turn/start returned no turn id');
2218
+ }
2219
+ void this.reconcileAcknowledgedTurn(late.threadId, turn.id, late.requestTimeout);
2220
+ }
2221
+ catch (error) {
2222
+ void this.shutdown(this.toError(error)).finally(() => {
2223
+ this.completeTurnStartReconciliation(late.threadId, late.reconciliation);
2224
+ });
2225
+ }
2226
+ }
2227
+ clearTurnCallbacks(turn) {
2228
+ turn.queuedNotifications.length = 0;
2229
+ turn.queuedToolRequests.length = 0;
2230
+ turn.toolCallResults.clear();
2231
+ turn.stoppingCallIds.clear();
2232
+ turn.hostToolBridge = undefined;
2233
+ turn.onDelta = undefined;
2234
+ }
2235
+ failAll(error) {
2236
+ const safe = this.toError(error);
2237
+ // Pending child tool calls abort exactly like a parent turn's.
2238
+ for (const child of this.subagents.values()) {
2239
+ child.abortController.abort(safe);
2240
+ }
2241
+ for (const threadId of [...this.turns.keys()]) {
2242
+ this.failTurn(threadId, safe);
2243
+ }
2244
+ for (const [id, pending] of this.pending) {
2245
+ clearTimeout(pending.timer);
2246
+ this.pending.delete(id);
2247
+ pending.reject(safe);
2248
+ }
2249
+ }
2250
+ failProcess(child, error) {
2251
+ if (child !== this.child) {
2252
+ return;
2253
+ }
2254
+ const suffix = this.stderrTail ? `: ${this.stderrTail}` : '';
2255
+ this.failAll(new Error(this.redact(`${error.message}${suffix}`)));
2256
+ this.detach(child);
2257
+ }
2258
+ async restart() {
2259
+ await this.shutdown(new Error('Codex app-server restarting after auth refresh'));
2260
+ this.sessions.clear();
2261
+ }
2262
+ async shutdown(reason) {
2263
+ if (this.shutdownPromise) {
2264
+ return this.shutdownPromise;
2265
+ }
2266
+ const operation = this.performShutdown(reason);
2267
+ this.shutdownPromise = operation;
2268
+ try {
2269
+ await operation;
2270
+ }
2271
+ finally {
2272
+ if (this.shutdownPromise === operation) {
2273
+ this.shutdownPromise = undefined;
2274
+ }
2275
+ }
2276
+ }
2277
+ async performShutdown(reason) {
2278
+ const child = this.child;
2279
+ if (!child) {
2280
+ return;
2281
+ }
2282
+ this.failAll(reason);
2283
+ await new Promise((resolveStop) => {
2284
+ let settled = false;
2285
+ const finish = () => {
2286
+ if (settled) {
2287
+ return;
2288
+ }
2289
+ settled = true;
2290
+ if (this.killTimer) {
2291
+ clearTimeout(this.killTimer);
2292
+ this.killTimer = undefined;
2293
+ }
2294
+ if (this.finalKillTimer) {
2295
+ clearTimeout(this.finalKillTimer);
2296
+ this.finalKillTimer = undefined;
2297
+ }
2298
+ this.detach(child);
2299
+ resolveStop();
2300
+ };
2301
+ child.once('exit', finish);
2302
+ child.kill('SIGTERM');
2303
+ this.killTimer = setTimeout(() => {
2304
+ if (child.exitCode === null && child.signalCode === null) {
2305
+ child.kill('SIGKILL');
2306
+ }
2307
+ this.killTimer = undefined;
2308
+ this.finalKillTimer = setTimeout(finish, STOP_GRACE_MS);
2309
+ this.finalKillTimer.unref();
2310
+ }, STOP_GRACE_MS);
2311
+ this.killTimer.unref();
2312
+ if (child.exitCode !== null || child.signalCode !== null) {
2313
+ finish();
2314
+ }
2315
+ });
2316
+ }
2317
+ detach(child) {
2318
+ if (this.child !== child) {
2319
+ return;
2320
+ }
2321
+ this.stdout?.close();
2322
+ this.stderr?.close();
2323
+ child.removeAllListeners();
2324
+ child.stdin.removeAllListeners();
2325
+ child.stdout.removeAllListeners();
2326
+ child.stderr.removeAllListeners();
2327
+ this.stdout = undefined;
2328
+ this.stderr = undefined;
2329
+ this.child = undefined;
2330
+ this.sessions.clear();
2331
+ // The connection is gone, so every child is gone with it: say so once per child and
2332
+ // release its authority instead of dropping the registry silently.
2333
+ this.terminateSubagents(new Error('Codex app-server connection closed'));
2334
+ this.threadContexts.clear();
2335
+ this.lateTurnStarts.clear();
2336
+ for (const [threadId, reconciliation] of this.turnStartReconciliations) {
2337
+ this.completeTurnStartReconciliation(threadId, reconciliation);
2338
+ }
2339
+ }
2340
+ }
2341
+ exports.CodexAppServerProcess = CodexAppServerProcess;
2342
+ //# sourceMappingURL=codex-app-server-process.js.map