clinkx 0.3.0 → 1.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 (555) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +10 -0
  3. package/README.md +82 -26
  4. package/clinkx-workflows/dist/allowed-roots.d.ts +1 -0
  5. package/clinkx-workflows/dist/allowed-roots.js +5 -0
  6. package/clinkx-workflows/dist/allowed-roots.js.map +1 -0
  7. package/clinkx-workflows/dist/artifacts.d.ts +20 -1
  8. package/clinkx-workflows/dist/artifacts.js +38 -9
  9. package/clinkx-workflows/dist/artifacts.js.map +1 -1
  10. package/clinkx-workflows/dist/backend.d.ts +4 -0
  11. package/clinkx-workflows/dist/citation-check.d.ts +40 -0
  12. package/clinkx-workflows/dist/citation-check.js +348 -0
  13. package/clinkx-workflows/dist/citation-check.js.map +1 -0
  14. package/clinkx-workflows/dist/clink-client.d.ts +2 -1
  15. package/clinkx-workflows/dist/clink-client.js +17 -4
  16. package/clinkx-workflows/dist/clink-client.js.map +1 -1
  17. package/clinkx-workflows/dist/config.d.ts +145 -16
  18. package/clinkx-workflows/dist/config.js +384 -26
  19. package/clinkx-workflows/dist/config.js.map +1 -1
  20. package/clinkx-workflows/dist/contained-file.d.ts +172 -0
  21. package/clinkx-workflows/dist/contained-file.js +483 -0
  22. package/clinkx-workflows/dist/contained-file.js.map +1 -0
  23. package/clinkx-workflows/dist/context-builder-lifecycle-contract.d.ts +416 -0
  24. package/clinkx-workflows/dist/context-builder-lifecycle-contract.js +328 -0
  25. package/clinkx-workflows/dist/context-builder-lifecycle-contract.js.map +1 -0
  26. package/clinkx-workflows/dist/context-builder-lifecycle-router.d.ts +24 -0
  27. package/clinkx-workflows/dist/context-builder-lifecycle-router.js +39 -0
  28. package/clinkx-workflows/dist/context-builder-lifecycle-router.js.map +1 -0
  29. package/clinkx-workflows/dist/context-cache-key.d.ts +53 -0
  30. package/clinkx-workflows/dist/context-cache-key.js +91 -0
  31. package/clinkx-workflows/dist/context-cache-key.js.map +1 -0
  32. package/clinkx-workflows/dist/context-mode-resolver.d.ts +27 -0
  33. package/clinkx-workflows/dist/context-mode-resolver.js +126 -0
  34. package/clinkx-workflows/dist/context-mode-resolver.js.map +1 -0
  35. package/clinkx-workflows/dist/context-preflight.d.ts +184 -0
  36. package/clinkx-workflows/dist/context-preflight.js +91 -0
  37. package/clinkx-workflows/dist/context-preflight.js.map +1 -0
  38. package/clinkx-workflows/dist/council-expander.d.ts +1 -1
  39. package/clinkx-workflows/dist/council-expander.js +2 -1
  40. package/clinkx-workflows/dist/council-expander.js.map +1 -1
  41. package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +165 -0
  42. package/clinkx-workflows/dist/dashboard/activity-contract.js +105 -0
  43. package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -0
  44. package/clinkx-workflows/dist/dashboard/activity-lease.d.ts +25 -0
  45. package/clinkx-workflows/dist/dashboard/activity-lease.js +34 -0
  46. package/clinkx-workflows/dist/dashboard/activity-lease.js.map +1 -0
  47. package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +86 -0
  48. package/clinkx-workflows/dist/dashboard/activity-reader.js +480 -0
  49. package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -0
  50. package/clinkx-workflows/dist/dashboard/api-routes.d.ts +36 -0
  51. package/clinkx-workflows/dist/dashboard/api-routes.js +1047 -0
  52. package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -0
  53. package/clinkx-workflows/dist/dashboard/artifact-index.d.ts +37 -0
  54. package/clinkx-workflows/dist/dashboard/artifact-index.js +145 -0
  55. package/clinkx-workflows/dist/dashboard/artifact-index.js.map +1 -0
  56. package/clinkx-workflows/dist/dashboard/auth/challenge-store.d.ts +31 -0
  57. package/clinkx-workflows/dist/dashboard/auth/challenge-store.js +65 -0
  58. package/clinkx-workflows/dist/dashboard/auth/challenge-store.js.map +1 -0
  59. package/clinkx-workflows/dist/dashboard/auth/controller.d.ts +26 -0
  60. package/clinkx-workflows/dist/dashboard/auth/controller.js +596 -0
  61. package/clinkx-workflows/dist/dashboard/auth/controller.js.map +1 -0
  62. package/clinkx-workflows/dist/dashboard/auth/credential-store.d.ts +60 -0
  63. package/clinkx-workflows/dist/dashboard/auth/credential-store.js +776 -0
  64. package/clinkx-workflows/dist/dashboard/auth/credential-store.js.map +1 -0
  65. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.d.ts +9 -0
  66. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js +44 -0
  67. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js.map +1 -0
  68. package/clinkx-workflows/dist/dashboard/auth/session-store.d.ts +17 -0
  69. package/clinkx-workflows/dist/dashboard/auth/session-store.js +77 -0
  70. package/clinkx-workflows/dist/dashboard/auth/session-store.js.map +1 -0
  71. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.d.ts +25 -0
  72. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js +71 -0
  73. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js.map +1 -0
  74. package/clinkx-workflows/dist/dashboard/context-run-liveness.d.ts +37 -0
  75. package/clinkx-workflows/dist/dashboard/context-run-liveness.js +49 -0
  76. package/clinkx-workflows/dist/dashboard/context-run-liveness.js.map +1 -0
  77. package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +222 -0
  78. package/clinkx-workflows/dist/dashboard/context-run-reader.js +1178 -0
  79. package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -0
  80. package/clinkx-workflows/dist/dashboard/contracts.d.ts +8936 -0
  81. package/clinkx-workflows/dist/dashboard/contracts.js +1157 -0
  82. package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -0
  83. package/clinkx-workflows/dist/dashboard/generation.d.ts +14 -0
  84. package/clinkx-workflows/dist/dashboard/generation.js +41 -0
  85. package/clinkx-workflows/dist/dashboard/generation.js.map +1 -0
  86. package/clinkx-workflows/dist/dashboard/index.d.ts +28 -0
  87. package/clinkx-workflows/dist/dashboard/index.js +16 -0
  88. package/clinkx-workflows/dist/dashboard/index.js.map +1 -0
  89. package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +101 -0
  90. package/clinkx-workflows/dist/dashboard/lazy-content.js +748 -0
  91. package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -0
  92. package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +303 -0
  93. package/clinkx-workflows/dist/dashboard/progress-report-contract.js +221 -0
  94. package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -0
  95. package/clinkx-workflows/dist/dashboard/progress-report-reader.d.ts +50 -0
  96. package/clinkx-workflows/dist/dashboard/progress-report-reader.js +581 -0
  97. package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -0
  98. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.d.ts +89 -0
  99. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js +71 -0
  100. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js.map +1 -0
  101. package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +28 -0
  102. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +110 -0
  103. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -0
  104. package/clinkx-workflows/dist/dashboard/read-model.d.ts +86 -0
  105. package/clinkx-workflows/dist/dashboard/read-model.js +440 -0
  106. package/clinkx-workflows/dist/dashboard/read-model.js.map +1 -0
  107. package/clinkx-workflows/dist/dashboard/request-admission.d.ts +31 -0
  108. package/clinkx-workflows/dist/dashboard/request-admission.js +126 -0
  109. package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -0
  110. package/clinkx-workflows/dist/dashboard/scanner.d.ts +75 -0
  111. package/clinkx-workflows/dist/dashboard/scanner.js +1110 -0
  112. package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -0
  113. package/clinkx-workflows/dist/dashboard/server-factory.d.ts +50 -0
  114. package/clinkx-workflows/dist/dashboard/server-factory.js +431 -0
  115. package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -0
  116. package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +2611 -0
  117. package/clinkx-workflows/dist/dashboard/session-log-contract.js +480 -0
  118. package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -0
  119. package/clinkx-workflows/dist/dashboard/session-log-identity.d.ts +21 -0
  120. package/clinkx-workflows/dist/dashboard/session-log-identity.js +101 -0
  121. package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -0
  122. package/clinkx-workflows/dist/dashboard/session-log-journal.d.ts +43 -0
  123. package/clinkx-workflows/dist/dashboard/session-log-journal.js +522 -0
  124. package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -0
  125. package/clinkx-workflows/dist/dashboard/session-log-reader.d.ts +61 -0
  126. package/clinkx-workflows/dist/dashboard/session-log-reader.js +378 -0
  127. package/clinkx-workflows/dist/dashboard/session-log-reader.js.map +1 -0
  128. package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +6 -0
  129. package/clinkx-workflows/dist/dashboard/session-log-redaction.js +367 -0
  130. package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -0
  131. package/clinkx-workflows/dist/dashboard/session-log-retention.d.ts +23 -0
  132. package/clinkx-workflows/dist/dashboard/session-log-retention.js +212 -0
  133. package/clinkx-workflows/dist/dashboard/session-log-retention.js.map +1 -0
  134. package/clinkx-workflows/dist/dashboard/static-responses.d.ts +10 -0
  135. package/clinkx-workflows/dist/dashboard/static-responses.js +94 -0
  136. package/clinkx-workflows/dist/dashboard/static-responses.js.map +1 -0
  137. package/clinkx-workflows/dist/dashboard/worker-lock-observer.d.ts +28 -0
  138. package/clinkx-workflows/dist/dashboard/worker-lock-observer.js +241 -0
  139. package/clinkx-workflows/dist/dashboard/worker-lock-observer.js.map +1 -0
  140. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  141. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  142. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  143. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  144. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  145. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  146. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  147. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  148. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  149. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  150. package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +25 -0
  151. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  152. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  153. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  154. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  155. package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +1 -0
  156. package/clinkx-workflows/dist/dashboard-ui/contracts/council-plan-review-input-v1.json +158 -0
  157. package/clinkx-workflows/dist/dashboard-ui/index.html +15 -0
  158. package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Mono-OFL.txt +93 -0
  159. package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Sans-OFL.txt +93 -0
  160. package/clinkx-workflows/dist/dashboard-ui/licenses/JetBrains-Mono-OFL.txt +93 -0
  161. package/clinkx-workflows/dist/dashboard-ui/licenses/NOTICE.md +32 -0
  162. package/clinkx-workflows/dist/dashboard-ui/theme-init.js +8 -0
  163. package/clinkx-workflows/dist/definition-normalizer.d.ts +19 -0
  164. package/clinkx-workflows/dist/definition-normalizer.js.map +1 -1
  165. package/clinkx-workflows/dist/delegate-validate.d.ts +78 -0
  166. package/clinkx-workflows/dist/delegate-validate.js +97 -0
  167. package/clinkx-workflows/dist/delegate-validate.js.map +1 -0
  168. package/clinkx-workflows/dist/engine.d.ts +141 -0
  169. package/clinkx-workflows/dist/engine.js +882 -28
  170. package/clinkx-workflows/dist/engine.js.map +1 -1
  171. package/clinkx-workflows/dist/errors.d.ts +2 -0
  172. package/clinkx-workflows/dist/errors.js.map +1 -1
  173. package/clinkx-workflows/dist/fingerprint.d.ts +1 -1
  174. package/clinkx-workflows/dist/fingerprint.js +1 -1
  175. package/clinkx-workflows/dist/index.d.ts +19 -0
  176. package/clinkx-workflows/dist/index.js +15 -0
  177. package/clinkx-workflows/dist/index.js.map +1 -1
  178. package/clinkx-workflows/dist/loader.d.ts +9 -3
  179. package/clinkx-workflows/dist/loader.js +92 -21
  180. package/clinkx-workflows/dist/loader.js.map +1 -1
  181. package/clinkx-workflows/dist/plan-review/admission.d.ts +160 -0
  182. package/clinkx-workflows/dist/plan-review/admission.js +290 -0
  183. package/clinkx-workflows/dist/plan-review/admission.js.map +1 -0
  184. package/clinkx-workflows/dist/plan-review/context-policy.d.ts +55 -0
  185. package/clinkx-workflows/dist/plan-review/context-policy.js +146 -0
  186. package/clinkx-workflows/dist/plan-review/context-policy.js.map +1 -0
  187. package/clinkx-workflows/dist/plan-review/contract.d.ts +763 -0
  188. package/clinkx-workflows/dist/plan-review/contract.js +289 -0
  189. package/clinkx-workflows/dist/plan-review/contract.js.map +1 -0
  190. package/clinkx-workflows/dist/plan-review/digest.d.ts +68 -0
  191. package/clinkx-workflows/dist/plan-review/digest.js +241 -0
  192. package/clinkx-workflows/dist/plan-review/digest.js.map +1 -0
  193. package/clinkx-workflows/dist/plan-review/discovery.d.ts +45 -0
  194. package/clinkx-workflows/dist/plan-review/discovery.js +36 -0
  195. package/clinkx-workflows/dist/plan-review/discovery.js.map +1 -0
  196. package/clinkx-workflows/dist/plan-review/execution-contract.d.ts +298 -0
  197. package/clinkx-workflows/dist/plan-review/execution-contract.js +194 -0
  198. package/clinkx-workflows/dist/plan-review/execution-contract.js.map +1 -0
  199. package/clinkx-workflows/dist/plan-review/expander.d.ts +48 -0
  200. package/clinkx-workflows/dist/plan-review/expander.js +243 -0
  201. package/clinkx-workflows/dist/plan-review/expander.js.map +1 -0
  202. package/clinkx-workflows/dist/plan-review/projector.d.ts +126 -0
  203. package/clinkx-workflows/dist/plan-review/projector.js +437 -0
  204. package/clinkx-workflows/dist/plan-review/projector.js.map +1 -0
  205. package/clinkx-workflows/dist/plan-review/publication.d.ts +95 -0
  206. package/clinkx-workflows/dist/plan-review/publication.js +182 -0
  207. package/clinkx-workflows/dist/plan-review/publication.js.map +1 -0
  208. package/clinkx-workflows/dist/plan-review/ranking-reducer.d.ts +34 -0
  209. package/clinkx-workflows/dist/plan-review/ranking-reducer.js +139 -0
  210. package/clinkx-workflows/dist/plan-review/ranking-reducer.js.map +1 -0
  211. package/clinkx-workflows/dist/plan-review/resume.d.ts +38 -0
  212. package/clinkx-workflows/dist/plan-review/resume.js +122 -0
  213. package/clinkx-workflows/dist/plan-review/resume.js.map +1 -0
  214. package/clinkx-workflows/dist/plan-review/rubric.d.ts +7 -0
  215. package/clinkx-workflows/dist/plan-review/rubric.js +24 -0
  216. package/clinkx-workflows/dist/plan-review/rubric.js.map +1 -0
  217. package/clinkx-workflows/dist/plan-review/run-state.d.ts +398 -0
  218. package/clinkx-workflows/dist/plan-review/run-state.js +183 -0
  219. package/clinkx-workflows/dist/plan-review/run-state.js.map +1 -0
  220. package/clinkx-workflows/dist/plan-review/runtime-policy.d.ts +74 -0
  221. package/clinkx-workflows/dist/plan-review/runtime-policy.js +186 -0
  222. package/clinkx-workflows/dist/plan-review/runtime-policy.js.map +1 -0
  223. package/clinkx-workflows/dist/plan-review/snapshot.d.ts +81 -0
  224. package/clinkx-workflows/dist/plan-review/snapshot.js +170 -0
  225. package/clinkx-workflows/dist/plan-review/snapshot.js.map +1 -0
  226. package/clinkx-workflows/dist/plan-review/stage-error.d.ts +19 -0
  227. package/clinkx-workflows/dist/plan-review/stage-error.js +20 -0
  228. package/clinkx-workflows/dist/plan-review/stage-error.js.map +1 -0
  229. package/clinkx-workflows/dist/post-processors/index.d.ts +20 -0
  230. package/clinkx-workflows/dist/post-processors/index.js +21 -0
  231. package/clinkx-workflows/dist/post-processors/index.js.map +1 -0
  232. package/clinkx-workflows/dist/prompt-budget.d.ts +7 -0
  233. package/clinkx-workflows/dist/prompt-budget.js +7 -1
  234. package/clinkx-workflows/dist/prompt-budget.js.map +1 -1
  235. package/clinkx-workflows/dist/reducers/index.js +2 -0
  236. package/clinkx-workflows/dist/reducers/index.js.map +1 -1
  237. package/clinkx-workflows/dist/run-lock-heartbeat.d.ts +2 -0
  238. package/clinkx-workflows/dist/run-lock-heartbeat.js +3 -0
  239. package/clinkx-workflows/dist/run-lock-heartbeat.js.map +1 -0
  240. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.d.ts +34 -0
  241. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js +74 -0
  242. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js.map +1 -0
  243. package/clinkx-workflows/dist/run-summary/cards/index.js +5 -0
  244. package/clinkx-workflows/dist/run-summary/cards/index.js.map +1 -1
  245. package/clinkx-workflows/dist/run-summary.d.ts +13 -0
  246. package/clinkx-workflows/dist/run-summary.js +29 -1
  247. package/clinkx-workflows/dist/run-summary.js.map +1 -1
  248. package/clinkx-workflows/dist/schema.d.ts +76 -21
  249. package/clinkx-workflows/dist/schema.js +10 -1
  250. package/clinkx-workflows/dist/schema.js.map +1 -1
  251. package/clinkx-workflows/dist/seed-terms.d.ts +30 -0
  252. package/clinkx-workflows/dist/seed-terms.js +176 -0
  253. package/clinkx-workflows/dist/seed-terms.js.map +1 -0
  254. package/clinkx-workflows/dist/server.d.ts +5 -2
  255. package/clinkx-workflows/dist/server.js +82 -52
  256. package/clinkx-workflows/dist/server.js.map +1 -1
  257. package/clinkx-workflows/dist/shutdown.d.ts +18 -20
  258. package/clinkx-workflows/dist/shutdown.js +43 -61
  259. package/clinkx-workflows/dist/shutdown.js.map +1 -1
  260. package/clinkx-workflows/dist/state-schema.d.ts +38 -0
  261. package/clinkx-workflows/dist/state.d.ts +170 -3
  262. package/clinkx-workflows/dist/state.js +1275 -28
  263. package/clinkx-workflows/dist/state.js.map +1 -1
  264. package/clinkx-workflows/dist/transport.d.ts +15 -0
  265. package/clinkx-workflows/dist/transport.js +19 -10
  266. package/clinkx-workflows/dist/transport.js.map +1 -1
  267. package/clinkx-workflows/dist/types.d.ts +14 -2
  268. package/clinkx-workflows/dist/validators/council.d.ts +318 -85
  269. package/clinkx-workflows/dist/validators/council.js +52 -13
  270. package/clinkx-workflows/dist/validators/council.js.map +1 -1
  271. package/clinkx-workflows/dist/validators/index.d.ts +1 -0
  272. package/clinkx-workflows/dist/validators/index.js +3 -0
  273. package/clinkx-workflows/dist/validators/index.js.map +1 -1
  274. package/clinkx-workflows/dist/validators/plan-review.d.ts +398 -0
  275. package/clinkx-workflows/dist/validators/plan-review.js +316 -0
  276. package/clinkx-workflows/dist/validators/plan-review.js.map +1 -0
  277. package/clinkx-workflows/dist/workflow-receipt.js +188 -4
  278. package/clinkx-workflows/dist/workflow-receipt.js.map +1 -1
  279. package/clinkx-workflows/dist/workflow-tools.d.ts +48 -3
  280. package/clinkx-workflows/dist/workflow-tools.js +1402 -59
  281. package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
  282. package/clinkx-workflows/dist/workspace-signature.d.ts +16 -0
  283. package/clinkx-workflows/dist/workspace-signature.js +388 -0
  284. package/clinkx-workflows/dist/workspace-signature.js.map +1 -0
  285. package/clinkx-workflows/templates/council/answer-specialist.txt +2 -1
  286. package/clinkx-workflows/templates/council/code-review-chairman.txt +13 -0
  287. package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +4 -1
  288. package/clinkx-workflows/templates/council/code-review-reviewer.txt +19 -3
  289. package/clinkx-workflows/templates/council/debug-debugger.txt +2 -1
  290. package/clinkx-workflows/templates/council/discover-analyst.txt +2 -1
  291. package/clinkx-workflows/templates/council/member.txt +2 -1
  292. package/clinkx-workflows/templates/council/plan-review-chairman.txt +68 -0
  293. package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +51 -0
  294. package/clinkx-workflows/templates/council/plan-review-reviewer.txt +60 -0
  295. package/clinkx-workflows/templates/plan-review/chairman.txt +48 -0
  296. package/clinkx-workflows/templates/plan-review/cross-review.txt +52 -0
  297. package/clinkx-workflows/templates/plan-review/ranking.txt +16 -0
  298. package/clinkx-workflows/templates/plan-review/reviewer.txt +57 -0
  299. package/clinkx-workflows/workflows/council-answer.yaml +97 -8
  300. package/clinkx-workflows/workflows/council-code-review.yaml +103 -8
  301. package/clinkx-workflows/workflows/council-debug.yaml +97 -8
  302. package/clinkx-workflows/workflows/council-default.yaml +97 -8
  303. package/clinkx-workflows/workflows/council-discover.yaml +97 -8
  304. package/clinkx-workflows/workflows/council-plan-review.yaml +250 -0
  305. package/clinkx-workflows/workflows/plan-review-receipts.yaml +230 -0
  306. package/conf/adapters/agy.json +49 -0
  307. package/conf/adapters/claude.json +5 -0
  308. package/conf/adapters/codex-app-server.json +44 -0
  309. package/conf/adapters/codex.json +7 -2
  310. package/conf/adapters/glm.json +5 -0
  311. package/conf/adapters/grok.json +60 -0
  312. package/conf/adapters/hapi/agy.json +52 -0
  313. package/conf/adapters/hapi/claude.json +5 -0
  314. package/conf/adapters/hapi/codex.json +7 -2
  315. package/conf/adapters/hapi/glm.json +5 -0
  316. package/dist/activity-journal.d.ts +42 -0
  317. package/dist/activity-journal.js +381 -0
  318. package/dist/activity-journal.js.map +1 -0
  319. package/dist/allowed-roots.d.ts +41 -0
  320. package/dist/allowed-roots.js +125 -3
  321. package/dist/allowed-roots.js.map +1 -1
  322. package/dist/codex/activity-session-log.d.ts +14 -0
  323. package/dist/codex/activity-session-log.js +206 -0
  324. package/dist/codex/activity-session-log.js.map +1 -0
  325. package/dist/codex/agent-tools.d.ts +23 -0
  326. package/dist/codex/agent-tools.js +1270 -0
  327. package/dist/codex/agent-tools.js.map +1 -0
  328. package/dist/codex/app-server-client.d.ts +76 -0
  329. package/dist/codex/app-server-client.js +582 -0
  330. package/dist/codex/app-server-client.js.map +1 -0
  331. package/dist/codex/app-server-registry.d.ts +389 -0
  332. package/dist/codex/app-server-registry.js +1407 -0
  333. package/dist/codex/app-server-registry.js.map +1 -0
  334. package/dist/codex/client-types.d.ts +109 -0
  335. package/dist/codex/client-types.js +2 -0
  336. package/dist/codex/client-types.js.map +1 -0
  337. package/dist/codex/protocol.d.ts +5375 -0
  338. package/dist/codex/protocol.js +614 -0
  339. package/dist/codex/protocol.js.map +1 -0
  340. package/dist/codex/session.d.ts +263 -0
  341. package/dist/codex/session.js +466 -0
  342. package/dist/codex/session.js.map +1 -0
  343. package/dist/config.d.ts +185 -19
  344. package/dist/config.js +82 -16
  345. package/dist/config.js.map +1 -1
  346. package/dist/context-builder/anchors.d.ts +1 -0
  347. package/dist/context-builder/anchors.js +3 -0
  348. package/dist/context-builder/anchors.js.map +1 -1
  349. package/dist/context-builder/bundle.d.ts +35 -2
  350. package/dist/context-builder/bundle.js +342 -2417
  351. package/dist/context-builder/bundle.js.map +1 -1
  352. package/dist/context-builder/codemap.d.ts +5 -0
  353. package/dist/context-builder/codemap.js +387 -13
  354. package/dist/context-builder/codemap.js.map +1 -1
  355. package/dist/context-builder/config.d.ts +1 -0
  356. package/dist/context-builder/config.js +12 -0
  357. package/dist/context-builder/config.js.map +1 -1
  358. package/dist/context-builder/context-run-catalog.d.ts +17 -0
  359. package/dist/context-builder/context-run-catalog.js +99 -0
  360. package/dist/context-builder/context-run-catalog.js.map +1 -0
  361. package/dist/context-builder/delegate-packet.d.ts +37 -0
  362. package/dist/context-builder/delegate-packet.js +359 -67
  363. package/dist/context-builder/delegate-packet.js.map +1 -1
  364. package/dist/context-builder/delegate-validation.d.ts +10 -1
  365. package/dist/context-builder/delegate-validation.js +153 -41
  366. package/dist/context-builder/delegate-validation.js.map +1 -1
  367. package/dist/context-builder/delegate.d.ts +2 -0
  368. package/dist/context-builder/delegate.js +1 -0
  369. package/dist/context-builder/delegate.js.map +1 -1
  370. package/dist/context-builder/diagnostics.d.ts +11 -2
  371. package/dist/context-builder/diagnostics.js +39 -6
  372. package/dist/context-builder/diagnostics.js.map +1 -1
  373. package/dist/context-builder/discovery.d.ts +29 -1
  374. package/dist/context-builder/discovery.js +475 -175
  375. package/dist/context-builder/discovery.js.map +1 -1
  376. package/dist/context-builder/globs.js +10 -10
  377. package/dist/context-builder/globs.js.map +1 -1
  378. package/dist/context-builder/handler.d.ts +26 -2
  379. package/dist/context-builder/handler.js +1158 -2416
  380. package/dist/context-builder/handler.js.map +1 -1
  381. package/dist/context-builder/lifecycle-contract.d.ts +424 -0
  382. package/dist/context-builder/lifecycle-contract.js +523 -0
  383. package/dist/context-builder/lifecycle-contract.js.map +1 -0
  384. package/dist/context-builder/lifecycle-journal.d.ts +62 -0
  385. package/dist/context-builder/lifecycle-journal.js +88 -0
  386. package/dist/context-builder/lifecycle-journal.js.map +1 -0
  387. package/dist/context-builder/lifecycle-observer.d.ts +102 -0
  388. package/dist/context-builder/lifecycle-observer.js +258 -0
  389. package/dist/context-builder/lifecycle-observer.js.map +1 -0
  390. package/dist/context-builder/lifecycle-reconcile.d.ts +32 -0
  391. package/dist/context-builder/lifecycle-reconcile.js +191 -0
  392. package/dist/context-builder/lifecycle-reconcile.js.map +1 -0
  393. package/dist/context-builder/lifecycle-recovery.d.ts +2 -0
  394. package/dist/context-builder/lifecycle-recovery.js +14 -0
  395. package/dist/context-builder/lifecycle-recovery.js.map +1 -0
  396. package/dist/context-builder/path-signals.js +9 -5
  397. package/dist/context-builder/path-signals.js.map +1 -1
  398. package/dist/context-builder/progress.d.ts +3 -1
  399. package/dist/context-builder/progress.js +23 -0
  400. package/dist/context-builder/progress.js.map +1 -1
  401. package/dist/context-builder/receipt.d.ts +6 -4
  402. package/dist/context-builder/receipt.js +12 -1
  403. package/dist/context-builder/receipt.js.map +1 -1
  404. package/dist/context-builder/refine-handler.d.ts +14 -0
  405. package/dist/context-builder/refine-handler.js +790 -0
  406. package/dist/context-builder/refine-handler.js.map +1 -0
  407. package/dist/context-builder/review-coverage.d.ts +84 -0
  408. package/dist/context-builder/review-coverage.js +437 -0
  409. package/dist/context-builder/review-coverage.js.map +1 -0
  410. package/dist/context-builder/schema.d.ts +239 -1
  411. package/dist/context-builder/schema.js +112 -6
  412. package/dist/context-builder/schema.js.map +1 -1
  413. package/dist/context-builder/secrets.d.ts +28 -0
  414. package/dist/context-builder/secrets.js +1473 -44
  415. package/dist/context-builder/secrets.js.map +1 -1
  416. package/dist/context-builder/selection.d.ts +7 -3
  417. package/dist/context-builder/selection.js +200 -738
  418. package/dist/context-builder/selection.js.map +1 -1
  419. package/dist/context-builder/session-log-identity.d.ts +1 -0
  420. package/dist/context-builder/session-log-identity.js +66 -0
  421. package/dist/context-builder/session-log-identity.js.map +1 -0
  422. package/dist/context-builder/slice-range-math.d.ts +16 -0
  423. package/dist/context-builder/slice-range-math.js +137 -0
  424. package/dist/context-builder/slice-range-math.js.map +1 -0
  425. package/dist/context-builder/status.d.ts +22 -1
  426. package/dist/context-builder/status.js +59 -4
  427. package/dist/context-builder/status.js.map +1 -1
  428. package/dist/context-builder/storage.d.ts +35 -2
  429. package/dist/context-builder/storage.js +271 -14
  430. package/dist/context-builder/storage.js.map +1 -1
  431. package/dist/context-builder/token-estimate.d.ts +13 -2
  432. package/dist/context-builder/token-estimate.js +77 -4
  433. package/dist/context-builder/token-estimate.js.map +1 -1
  434. package/dist/context-builder/versions.d.ts +29 -0
  435. package/dist/context-builder/versions.js +30 -0
  436. package/dist/context-builder/versions.js.map +1 -0
  437. package/dist/context-builder/workflow-runner.d.ts +69 -0
  438. package/dist/context-builder/workflow-runner.js +1051 -0
  439. package/dist/context-builder/workflow-runner.js.map +1 -0
  440. package/dist/context-builder/write-watchdog.d.ts +14 -0
  441. package/dist/context-builder/write-watchdog.js +38 -0
  442. package/dist/context-builder/write-watchdog.js.map +1 -0
  443. package/dist/dashboard-adapter-catalog.d.ts +9 -0
  444. package/dist/dashboard-adapter-catalog.js +46 -0
  445. package/dist/dashboard-adapter-catalog.js.map +1 -0
  446. package/dist/dashboard-cli.d.ts +79 -0
  447. package/dist/dashboard-cli.js +585 -0
  448. package/dist/dashboard-cli.js.map +1 -0
  449. package/dist/dashboard-companion/client.d.ts +36 -0
  450. package/dist/dashboard-companion/client.js +210 -0
  451. package/dist/dashboard-companion/client.js.map +1 -0
  452. package/dist/dashboard-companion/config.d.ts +8 -0
  453. package/dist/dashboard-companion/config.js +52 -0
  454. package/dist/dashboard-companion/config.js.map +1 -0
  455. package/dist/dashboard-companion/http-proxy.d.ts +10 -0
  456. package/dist/dashboard-companion/http-proxy.js +234 -0
  457. package/dist/dashboard-companion/http-proxy.js.map +1 -0
  458. package/dist/dashboard-companion/protocol.d.ts +80 -0
  459. package/dist/dashboard-companion/protocol.js +455 -0
  460. package/dist/dashboard-companion/protocol.js.map +1 -0
  461. package/dist/dashboard-companion/standalone-assets.d.ts +12 -0
  462. package/dist/dashboard-companion/standalone-assets.js +72 -0
  463. package/dist/dashboard-companion/standalone-assets.js.map +1 -0
  464. package/dist/dashboard-companion/standalone-entry.d.ts +1 -0
  465. package/dist/dashboard-companion/standalone-entry.js +17 -0
  466. package/dist/dashboard-companion/standalone-entry.js.map +1 -0
  467. package/dist/dashboard-gateway/config.d.ts +9 -0
  468. package/dist/dashboard-gateway/config.js +44 -0
  469. package/dist/dashboard-gateway/config.js.map +1 -0
  470. package/dist/dashboard-gateway/index.d.ts +11 -0
  471. package/dist/dashboard-gateway/index.js +755 -0
  472. package/dist/dashboard-gateway/index.js.map +1 -0
  473. package/dist/dashboard-gateway/main.d.ts +1 -0
  474. package/dist/dashboard-gateway/main.js +25 -0
  475. package/dist/dashboard-gateway/main.js.map +1 -0
  476. package/dist/dashboard-pairing-token.d.ts +1 -0
  477. package/dist/dashboard-pairing-token.js +5 -0
  478. package/dist/dashboard-pairing-token.js.map +1 -0
  479. package/dist/elaris/client.d.ts +73 -0
  480. package/dist/elaris/client.js +825 -0
  481. package/dist/elaris/client.js.map +1 -0
  482. package/dist/elaris/config.d.ts +18 -0
  483. package/dist/elaris/config.js +75 -0
  484. package/dist/elaris/config.js.map +1 -0
  485. package/dist/elaris/errors.d.ts +41 -0
  486. package/dist/elaris/errors.js +205 -0
  487. package/dist/elaris/errors.js.map +1 -0
  488. package/dist/elaris/github-repo.d.ts +18 -0
  489. package/dist/elaris/github-repo.js +89 -0
  490. package/dist/elaris/github-repo.js.map +1 -0
  491. package/dist/elaris/index.d.ts +10 -0
  492. package/dist/elaris/index.js +11 -0
  493. package/dist/elaris/index.js.map +1 -0
  494. package/dist/elaris/redact.d.ts +89 -0
  495. package/dist/elaris/redact.js +423 -0
  496. package/dist/elaris/redact.js.map +1 -0
  497. package/dist/elaris/run-id.d.ts +5 -0
  498. package/dist/elaris/run-id.js +20 -0
  499. package/dist/elaris/run-id.js.map +1 -0
  500. package/dist/elaris/status.d.ts +45 -0
  501. package/dist/elaris/status.js +122 -0
  502. package/dist/elaris/status.js.map +1 -0
  503. package/dist/elaris/tool-schemas.d.ts +314 -0
  504. package/dist/elaris/tool-schemas.js +208 -0
  505. package/dist/elaris/tool-schemas.js.map +1 -0
  506. package/dist/elaris/tools.d.ts +73 -0
  507. package/dist/elaris/tools.js +1065 -0
  508. package/dist/elaris/tools.js.map +1 -0
  509. package/dist/elaris/types.d.ts +1541 -0
  510. package/dist/elaris/types.js +252 -0
  511. package/dist/elaris/types.js.map +1 -0
  512. package/dist/errors.d.ts +16 -0
  513. package/dist/errors.js +19 -0
  514. package/dist/errors.js.map +1 -1
  515. package/dist/handler.d.ts +4 -0
  516. package/dist/handler.js +10 -1
  517. package/dist/handler.js.map +1 -1
  518. package/dist/index.d.ts +2 -1
  519. package/dist/index.js +13 -3
  520. package/dist/index.js.map +1 -1
  521. package/dist/local-clink-backend.d.ts +3 -0
  522. package/dist/local-clink-backend.js +4 -0
  523. package/dist/local-clink-backend.js.map +1 -1
  524. package/dist/package-info.d.ts +2 -0
  525. package/dist/package-info.js +18 -0
  526. package/dist/package-info.js.map +1 -0
  527. package/dist/parsers/codex-jsonl.js +48 -3
  528. package/dist/parsers/codex-jsonl.js.map +1 -1
  529. package/dist/parsers/extract.d.ts +10 -1
  530. package/dist/parsers/extract.js +11 -3
  531. package/dist/parsers/extract.js.map +1 -1
  532. package/dist/pipeline.d.ts +4 -0
  533. package/dist/pipeline.js +129 -8
  534. package/dist/pipeline.js.map +1 -1
  535. package/dist/run-dir.d.ts +24 -0
  536. package/dist/run-dir.js +201 -2
  537. package/dist/run-dir.js.map +1 -1
  538. package/dist/runner.d.ts +13 -0
  539. package/dist/runner.js +184 -15
  540. package/dist/runner.js.map +1 -1
  541. package/dist/safety.d.ts +4 -4
  542. package/dist/safety.js +4 -4
  543. package/dist/schema.d.ts +7 -7
  544. package/dist/server.d.ts +1 -0
  545. package/dist/server.js +359 -17
  546. package/dist/server.js.map +1 -1
  547. package/dist/session-log-stream.d.ts +18 -0
  548. package/dist/session-log-stream.js +319 -0
  549. package/dist/session-log-stream.js.map +1 -0
  550. package/package.json +46 -5
  551. package/conf/adapters/gemini.json +0 -44
  552. package/conf/adapters/hapi/gemini.json +0 -47
  553. package/dist/parsers/gemini-json.d.ts +0 -2
  554. package/dist/parsers/gemini-json.js +0 -72
  555. package/dist/parsers/gemini-json.js.map +0 -1
@@ -0,0 +1,1407 @@
1
+ import { ExecutionError, InvalidParamsError } from "../errors.js";
2
+ import { Semaphore } from "../concurrency.js";
3
+ import { logger } from "../logger.js";
4
+ import { CodexAppServerClient as RealCodexAppServerClient, sandboxPolicyFromMode, } from "./app-server-client.js";
5
+ import { CODEX_EVENT_AGENT_MESSAGE_DELTA, CODEX_EVENT_ITEM_COMPLETED, CODEX_EVENT_ITEM_PLAN_DELTA, CODEX_EVENT_ITEM_STARTED, CODEX_EVENT_THREAD_TOKEN_USAGE_UPDATED, CODEX_EVENT_TURN_COMPLETED, CODEX_EVENT_TURN_DIFF_UPDATED, CODEX_EVENT_TURN_PLAN_UPDATED, CODEX_EVENT_TURN_STARTED, CODEX_REQUEST_COMMAND_EXECUTION_APPROVAL, CODEX_REQUEST_FILE_CHANGE_APPROVAL, CODEX_REQUEST_PERMISSIONS_APPROVAL, } from "./protocol.js";
6
+ import { SessionRecord, } from "./session.js";
7
+ /** Typed sentinel for a withBudget timeout so callers can gate on the type,
8
+ * not a fragile message substring (review fix). */
9
+ class BudgetExceededError extends ExecutionError {
10
+ constructor(message = "app-server RPC budget exceeded") {
11
+ super(message);
12
+ this.name = "BudgetExceededError";
13
+ }
14
+ }
15
+ const DEFAULTS = {
16
+ maxActiveTurns: 5,
17
+ maxRetainedSessions: 20,
18
+ sessionTtlSeconds: 1800,
19
+ eventBuffer: 500,
20
+ turnTimeoutSeconds: 600,
21
+ approvalTimeoutSeconds: 120,
22
+ maxAgentDepth: 2,
23
+ maxResponseChars: 20_000,
24
+ maxEventTextChars: 2_000,
25
+ interruptBudgetSeconds: 10,
26
+ terminalWaitBudgetSeconds: 10,
27
+ startBudgetSeconds: 60,
28
+ };
29
+ function safeParseInt(val, fallback) {
30
+ if (val == null || val === "")
31
+ return fallback;
32
+ const n = Number(val);
33
+ return Number.isFinite(n) && n > 0 ? n : fallback;
34
+ }
35
+ // Allow 0 (disables a timeout) but reject negatives / non-numbers.
36
+ function safeParseNonNegInt(val, fallback) {
37
+ if (val == null || val === "")
38
+ return fallback;
39
+ const n = Number(val);
40
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
41
+ }
42
+ function createDeferred() {
43
+ let resolve;
44
+ const promise = new Promise((res) => {
45
+ resolve = res;
46
+ });
47
+ return { promise, resolve };
48
+ }
49
+ function extractThreadId(result) {
50
+ // The result schemas are .passthrough() (additive vendor fields), whose index
51
+ // signature defeats union narrowing — read the id fields via a typed view.
52
+ const r = result;
53
+ if (typeof r.thread?.id === "string")
54
+ return r.thread.id;
55
+ if (typeof r.threadId === "string")
56
+ return r.threadId;
57
+ throw new ExecutionError("Codex app-server returned a thread result without a thread id.");
58
+ }
59
+ function extractTurnId(result) {
60
+ const r = result;
61
+ if (typeof r.turn?.id === "string")
62
+ return r.turn.id;
63
+ if (typeof r.turnId === "string")
64
+ return r.turnId;
65
+ throw new ExecutionError("Codex app-server returned a turn result without a turn id.");
66
+ }
67
+ function approvalTypeFromMethod(method) {
68
+ switch (method) {
69
+ case CODEX_REQUEST_COMMAND_EXECUTION_APPROVAL:
70
+ return "commandExecution";
71
+ case CODEX_REQUEST_FILE_CHANGE_APPROVAL:
72
+ return "fileChange";
73
+ case CODEX_REQUEST_PERMISSIONS_APPROVAL:
74
+ return "permissions";
75
+ default:
76
+ return null;
77
+ }
78
+ }
79
+ function registryHealthFromClient(activeLeases, retainedSessionCount, clientHealth) {
80
+ return {
81
+ activeLeases,
82
+ retainedSessionCount,
83
+ appServerProtocol: {
84
+ acceptedVersion: clientHealth.acceptedVersion,
85
+ minVersion: clientHealth.minVersion,
86
+ maxVersion: clientHealth.maxVersion,
87
+ strictVersion: clientHealth.strictVersion,
88
+ },
89
+ childRespawnCount: clientHealth.childRespawnCount,
90
+ recentFailures: {
91
+ count: clientHealth.recentFailures.length,
92
+ samples: [...clientHealth.recentFailures],
93
+ },
94
+ };
95
+ }
96
+ function pendingApprovalFromEvent(event, type, requestedAtMs) {
97
+ const base = {
98
+ id: event.id,
99
+ type,
100
+ requestedAtMs,
101
+ };
102
+ switch (event.method) {
103
+ case CODEX_REQUEST_COMMAND_EXECUTION_APPROVAL: {
104
+ const params = event.params;
105
+ return {
106
+ ...base,
107
+ reason: params.reason ?? null,
108
+ ...(hasOwn(params, "command") ? { command: params.command ?? null } : {}),
109
+ ...(hasOwn(params, "cwd") ? { cwd: params.cwd ?? null } : {}),
110
+ ...(hasOwn(params, "networkApprovalContext")
111
+ ? { networkApprovalContext: params.networkApprovalContext ?? null }
112
+ : {}),
113
+ };
114
+ }
115
+ case CODEX_REQUEST_FILE_CHANGE_APPROVAL: {
116
+ const params = event.params;
117
+ return {
118
+ ...base,
119
+ reason: params.reason ?? null,
120
+ ...(hasOwn(params, "grantRoot")
121
+ ? { grantRoot: params.grantRoot ?? null }
122
+ : {}),
123
+ };
124
+ }
125
+ case CODEX_REQUEST_PERMISSIONS_APPROVAL: {
126
+ const params = event.params;
127
+ return {
128
+ ...base,
129
+ reason: params.reason ?? null,
130
+ cwd: params.cwd,
131
+ };
132
+ }
133
+ default:
134
+ return {
135
+ ...base,
136
+ reason: null,
137
+ };
138
+ }
139
+ }
140
+ function hasOwn(value, key) {
141
+ return Object.prototype.hasOwnProperty.call(value, key);
142
+ }
143
+ function asRecord(value) {
144
+ return typeof value === "object" && value !== null && !Array.isArray(value)
145
+ ? value
146
+ : null;
147
+ }
148
+ function stringField(value, key) {
149
+ return typeof value[key] === "string" ? value[key] : null;
150
+ }
151
+ function joinedStringField(value, key) {
152
+ const field = value[key];
153
+ if (typeof field === "string")
154
+ return field;
155
+ if (!Array.isArray(field))
156
+ return null;
157
+ const strings = field.filter((entry) => typeof entry === "string");
158
+ return strings.length > 0 ? strings.join("\n") : null;
159
+ }
160
+ function turnInputText(input) {
161
+ const parts = [];
162
+ for (const entry of input) {
163
+ const item = asRecord(entry);
164
+ if (item != null &&
165
+ item["type"] === "text" &&
166
+ typeof item["text"] === "string") {
167
+ parts.push(item["text"]);
168
+ }
169
+ }
170
+ return parts.length > 0 ? parts.join("\n") : null;
171
+ }
172
+ function toolIdentity(item) {
173
+ const type = stringField(item, "type");
174
+ switch (type) {
175
+ case "commandExecution":
176
+ return {
177
+ toolName: type,
178
+ summary: stringField(item, "command") ?? type,
179
+ };
180
+ case "fileChange": {
181
+ const count = Array.isArray(item["changes"]) ? item["changes"].length : 0;
182
+ return {
183
+ toolName: type,
184
+ summary: count > 0
185
+ ? `${count} file change${count === 1 ? "" : "s"}`
186
+ : type,
187
+ };
188
+ }
189
+ case "mcpToolCall": {
190
+ const server = stringField(item, "server");
191
+ const tool = stringField(item, "tool");
192
+ const toolName = server != null && tool != null ? `${server}/${tool}` : tool ?? type;
193
+ return { toolName, summary: toolName };
194
+ }
195
+ case "dynamicToolCall": {
196
+ const namespace = stringField(item, "namespace");
197
+ const tool = stringField(item, "tool");
198
+ const toolName = namespace != null && tool != null
199
+ ? `${namespace}/${tool}`
200
+ : tool ?? type;
201
+ return { toolName, summary: toolName };
202
+ }
203
+ case "collabAgentToolCall": {
204
+ const toolName = stringField(item, "tool") ?? type;
205
+ return { toolName, summary: stringField(item, "prompt") ?? toolName };
206
+ }
207
+ case "webSearch":
208
+ return { toolName: type, summary: stringField(item, "query") ?? type };
209
+ case "imageView":
210
+ return { toolName: type, summary: stringField(item, "path") ?? type };
211
+ default:
212
+ return null;
213
+ }
214
+ }
215
+ function toolOutcome(item) {
216
+ const status = stringField(item, "status");
217
+ if (status === "failed")
218
+ return "failed";
219
+ if (status === "declined" ||
220
+ status === "cancelled" ||
221
+ status === "interrupted") {
222
+ return "cancelled";
223
+ }
224
+ const exitCode = item["exitCode"];
225
+ if (typeof exitCode === "number" && exitCode !== 0)
226
+ return "failed";
227
+ return "succeeded";
228
+ }
229
+ function itemObservation(itemValue, phase) {
230
+ const item = asRecord(itemValue);
231
+ if (item == null)
232
+ return null;
233
+ const itemId = stringField(item, "id");
234
+ const type = stringField(item, "type");
235
+ if (itemId == null || type == null)
236
+ return null;
237
+ if (phase === "completed" && type === "agentMessage") {
238
+ const content = stringField(item, "text");
239
+ if (content == null)
240
+ return null;
241
+ return {
242
+ kind: "assistant",
243
+ itemId,
244
+ content,
245
+ phase: stringField(item, "phase"),
246
+ };
247
+ }
248
+ if (phase === "completed" && type === "reasoning") {
249
+ const summary = joinedStringField(item, "summary");
250
+ if (summary == null)
251
+ return null;
252
+ return {
253
+ kind: "reasoning",
254
+ itemId,
255
+ summary,
256
+ content: joinedStringField(item, "content"),
257
+ };
258
+ }
259
+ const tool = toolIdentity(item);
260
+ if (tool == null)
261
+ return null;
262
+ if (phase === "started") {
263
+ return {
264
+ kind: "tool_call",
265
+ toolName: tool.toolName,
266
+ callId: itemId,
267
+ summary: tool.summary,
268
+ };
269
+ }
270
+ const resultSummary = stringField(item, "aggregatedOutput") ??
271
+ stringField(asRecord(item["error"]) ?? {}, "message") ??
272
+ tool.summary;
273
+ return {
274
+ kind: "tool_result",
275
+ toolName: tool.toolName,
276
+ callId: itemId,
277
+ outcome: toolOutcome(item),
278
+ summary: resultSummary,
279
+ };
280
+ }
281
+ function approvalAction(approval) {
282
+ return (approval.command ?? approval.grantRoot ?? approval.reason ?? approval.type);
283
+ }
284
+ function optionalTokenCount(value) {
285
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
286
+ }
287
+ function mapTurnStatus(status) {
288
+ switch (status) {
289
+ case "completed":
290
+ return "completed";
291
+ case "interrupted":
292
+ case "cancelled":
293
+ return "interrupted";
294
+ case "failed":
295
+ return "failed";
296
+ default:
297
+ return "failed";
298
+ }
299
+ }
300
+ export function resolveRegistryConfig(override) {
301
+ const env = process.env;
302
+ const base = {
303
+ maxActiveTurns: safeParseInt(env["CLINKX_MAX_ACTIVE_TURNS"], DEFAULTS.maxActiveTurns),
304
+ maxRetainedSessions: safeParseInt(env["CLINKX_MAX_RETAINED_SESSIONS"], DEFAULTS.maxRetainedSessions),
305
+ sessionTtlMs: safeParseNonNegInt(env["CLINKX_SESSION_TTL_SECONDS"], DEFAULTS.sessionTtlSeconds) * 1000,
306
+ eventBuffer: safeParseInt(env["CLINKX_SESSION_EVENT_BUFFER"], DEFAULTS.eventBuffer),
307
+ turnTimeoutMs: safeParseNonNegInt(env["CLINKX_TURN_TIMEOUT_SECONDS"], DEFAULTS.turnTimeoutSeconds) * 1000,
308
+ approvalTimeoutMs: safeParseNonNegInt(env["CLINKX_APPROVAL_TIMEOUT_SECONDS"], DEFAULTS.approvalTimeoutSeconds) * 1000,
309
+ maxAgentDepth: safeParseInt(env["CLINKX_MAX_AGENT_DEPTH"], DEFAULTS.maxAgentDepth),
310
+ currentDepth: safeParseNonNegInt(env["CLINKX_AGENT_DEPTH"], 0),
311
+ maxResponseChars: safeParseInt(env["CLINKX_MAX_RESPONSE_CHARS"], DEFAULTS.maxResponseChars),
312
+ maxEventTextChars: safeParseInt(env["CLINKX_SESSION_EVENT_TEXT_MAX"], DEFAULTS.maxEventTextChars),
313
+ interruptBudgetMs: safeParseInt(env["CLINKX_TURN_INTERRUPT_BUDGET_SECONDS"], DEFAULTS.interruptBudgetSeconds) * 1000,
314
+ terminalWaitBudgetMs: safeParseInt(env["CLINKX_TERMINAL_WAIT_BUDGET_SECONDS"], DEFAULTS.terminalWaitBudgetSeconds) * 1000,
315
+ startBudgetMs: safeParseInt(env["CLINKX_APP_SERVER_START_BUDGET_SECONDS"], DEFAULTS.startBudgetSeconds) * 1000,
316
+ };
317
+ return { ...base, ...override };
318
+ }
319
+ /**
320
+ * Env marker to ADD to the spawned app-server child so the recursion guard
321
+ * survives the env sanitizer (2-6). Single source for both the registry's
322
+ * {@link AppServerRegistry.childEnvMarker} and the default-singleton spawn.
323
+ */
324
+ function childEnvMarkerFor(currentDepth) {
325
+ return { CLINKX_AGENT_DEPTH: String(currentDepth + 1) };
326
+ }
327
+ const defaultSetTimer = (cb, ms) => {
328
+ const handle = setTimeout(cb, ms);
329
+ // Don't keep the STDIO server alive on dangling turn timers.
330
+ handle.unref?.();
331
+ return handle;
332
+ };
333
+ const defaultClearTimer = (handle) => {
334
+ clearTimeout(handle);
335
+ };
336
+ const DEFAULT_APP_SERVER_SPAWN_CONFIG = {
337
+ command: "codex",
338
+ args: ["app-server"],
339
+ };
340
+ /**
341
+ * Registry & lifecycle for long-lived Codex app-server sessions.
342
+ *
343
+ * CORE responsibilities (Phase 2): the active-turn lease (the SOLE concurrency
344
+ * gate — the request {@link Semaphore} is never acquired here), session record
345
+ * accounting, the bounded event ring, retained-cap + TTL reaping, turn and
346
+ * pending-approval timeouts with the shared interrupt → terminal-wait →
347
+ * process-group-kill + respawn escalation path, crash semantics, the recursion
348
+ * depth guard, and a standalone {@link shutdown}.
349
+ *
350
+ * Respawn ownership belongs to the client: the registry only calls
351
+ * {@link CodexAppServerClient.ensureReady} and the client's guarded
352
+ * {@link CodexAppServerClient.killProcessGroup}.
353
+ */
354
+ export class AppServerRegistry {
355
+ client;
356
+ observer;
357
+ clock;
358
+ setTimer;
359
+ clearTimer;
360
+ config;
361
+ lease;
362
+ sessions = new Map();
363
+ /**
364
+ * Thread ids with an in-flight resume reservation. Held synchronously across
365
+ * startSession's awaited RPCs so two concurrent resumes of the same thread
366
+ * cannot both pass the in-flight check before either becomes visible as a
367
+ * running session (review fix A — closes the deferred-insertion race).
368
+ */
369
+ reservedResumeThreads = new Set();
370
+ unsubscribeProcessDied;
371
+ constructor(deps) {
372
+ this.client = deps.client;
373
+ this.observer = deps.observer;
374
+ this.clock = deps.clock ?? { now: () => Date.now() };
375
+ this.setTimer = deps.setTimer ?? defaultSetTimer;
376
+ this.clearTimer = deps.clearTimer ?? defaultClearTimer;
377
+ this.config = resolveRegistryConfig(deps.config);
378
+ this.lease = new Semaphore(this.config.maxActiveTurns);
379
+ this.unsubscribeProcessDied = this.client.onProcessDied((event) => this.handleProcessDied(event));
380
+ }
381
+ now() {
382
+ return this.clock.now();
383
+ }
384
+ get activeLeases() {
385
+ return this.lease.active;
386
+ }
387
+ get sessionCount() {
388
+ return this.sessions.size;
389
+ }
390
+ health() {
391
+ return registryHealthFromClient(this.activeLeases, this.sessionCount, this.client.getHealth());
392
+ }
393
+ /**
394
+ * Env marker to ADD to the spawned app-server child so the recursion guard
395
+ * survives the env sanitizer (2-6). The client owns actual env injection
396
+ * into the AppServerSpawnConfig; the registry only supplies the marker.
397
+ */
398
+ childEnvMarker() {
399
+ return childEnvMarkerFor(this.config.currentDepth);
400
+ }
401
+ require(sessionId) {
402
+ const runtime = this.sessions.get(sessionId);
403
+ if (runtime == null) {
404
+ throw new InvalidParamsError(`Unknown session: ${sessionId}`);
405
+ }
406
+ return runtime;
407
+ }
408
+ /**
409
+ * Start a new (or resumed) session and its first turn. Returns once the turn
410
+ * is RUNNING (lease held); the turn proceeds asynchronously and the lease is
411
+ * released only on provable terminality — held even if the MCP call detaches
412
+ * (2-3). Use {@link waitForTurn} to await completion without consuming a lease.
413
+ */
414
+ async startSession(opts) {
415
+ // Opportunistic reap before admitting new work.
416
+ this.reapIdle();
417
+ // Recursion guard (2-6): refuse beyond the depth ceiling.
418
+ if (this.config.currentDepth >= this.config.maxAgentDepth) {
419
+ throw new ExecutionError(`Agent recursion depth limit reached (${this.config.currentDepth}/${this.config.maxAgentDepth}). ` +
420
+ "Refusing to start a nested Codex app-server session.");
421
+ }
422
+ if (this.sessions.has(opts.sessionId)) {
423
+ throw new InvalidParamsError(`Session already exists: ${opts.sessionId}`);
424
+ }
425
+ // Resume reservation (review fix A): claim the resumed thread SYNCHRONOUSLY
426
+ // — before any await — so two concurrent resumes of the same thread cannot
427
+ // both pass this check during startSession's awaited RPC sequence. The
428
+ // authoritative guard lives here, not in the caller's preflight.
429
+ const resumeThreadId = opts.resumeThreadId;
430
+ if (resumeThreadId != null) {
431
+ const heldByRunning = [...this.sessions.values()].some((rt) => rt.record.threadId === resumeThreadId && !rt.record.isIdle());
432
+ if (this.reservedResumeThreads.has(resumeThreadId) || heldByRunning) {
433
+ throw new InvalidParamsError(`Cannot resume thread ${resumeThreadId}: a turn is already in flight on it. ` +
434
+ "Wait for that turn to complete before resuming.");
435
+ }
436
+ this.reservedResumeThreads.add(resumeThreadId);
437
+ }
438
+ let leaseHeld = false;
439
+ let runtime;
440
+ try {
441
+ // Active-turn lease: the SOLE gate, fail-fast when full (2-3). Acquired
442
+ // INSIDE the try — and after the synchronous reservation-add with no await
443
+ // between — so a pool-full throw here is caught below and releases the
444
+ // reservation (otherwise a saturated resume would leak it permanently).
445
+ this.lease.acquire();
446
+ leaseHeld = true;
447
+ // A silent child must not hang startSession forever holding the lease
448
+ // (review fix 3): bound the whole pre-turn RPC sequence with a budget.
449
+ const established = await this.withBudget((async () => {
450
+ await this.client.ensureReady();
451
+ let threadId;
452
+ if (opts.resumeThreadId != null) {
453
+ const resumed = await this.client.threadResume({
454
+ threadId: opts.resumeThreadId,
455
+ });
456
+ threadId = extractThreadId(resumed);
457
+ }
458
+ else {
459
+ const started = await this.client.threadStart(this.buildThreadStartParams(opts));
460
+ threadId = extractThreadId(started);
461
+ }
462
+ const origin = this.config.currentDepth === 0 ? "root" : "nested";
463
+ const record = new SessionRecord({
464
+ sessionId: opts.sessionId,
465
+ threadId,
466
+ sandbox: opts.sandbox,
467
+ approval: opts.approval,
468
+ origin,
469
+ depth: this.config.currentDepth,
470
+ createdAtMs: this.now(),
471
+ eventBuffer: this.config.eventBuffer,
472
+ maxEventTextChars: this.config.maxEventTextChars,
473
+ maxFinalTextChars: this.config.maxResponseChars,
474
+ cwd: opts.cwd,
475
+ model: opts.model,
476
+ });
477
+ const rt = {
478
+ record,
479
+ hasLease: true,
480
+ finalized: false,
481
+ unsubscribe: undefined,
482
+ turnTimer: undefined,
483
+ approvalTimer: undefined,
484
+ terminal: createDeferred(),
485
+ attention: createDeferred(),
486
+ turnTimeoutMs: opts.turnTimeoutMs,
487
+ activityObservation: opts.activityObservation,
488
+ sessionObserver: opts.sessionObserver,
489
+ observerTail: Promise.resolve(),
490
+ nextObservationSequence: 0,
491
+ };
492
+ runtime = rt;
493
+ this.sessions.set(opts.sessionId, rt);
494
+ this.observeUserInput(rt, opts.input, this.now());
495
+ // Subscribe before turn/start so early deltas are not lost.
496
+ rt.unsubscribe = this.client.on({ threadId }, (event) => {
497
+ this.handleEvent(rt, event);
498
+ });
499
+ const turnResult = await this.client.turnStart(this.buildTurnStartParams(threadId, opts));
500
+ return { runtime: rt, turnId: extractTurnId(turnResult) };
501
+ })(), this.config.startBudgetMs);
502
+ const { runtime: rt, turnId } = established;
503
+ this.bindSessionObserverTurn(rt, turnId);
504
+ // turnStart flushes buffered provisional events before returning; a fast
505
+ // turn can fully finalize during that flush. Only establish the running
506
+ // turn if it has not already gone terminal (review fix 2).
507
+ if (!rt.finalized && rt.record.activeTurnId == null) {
508
+ rt.record.beginTurn(turnId, this.now());
509
+ this.scheduleTurnTimeout(rt);
510
+ this.observeRunning(rt);
511
+ this.observeLifecycle(rt, "running");
512
+ // Lease intentionally retained — the turn is now running.
513
+ leaseHeld = false;
514
+ }
515
+ else {
516
+ // Already finalized via turn/completed during the flush: the lease was
517
+ // released by finalizeTurn. Do not release it again.
518
+ leaseHeld = false;
519
+ }
520
+ // The session is now in the sessions map (running, or terminal if it
521
+ // finished during the flush). Subsequent resumes are gated by the
522
+ // in-flight session check, so the synchronous reservation can be released.
523
+ if (resumeThreadId != null) {
524
+ this.reservedResumeThreads.delete(resumeThreadId);
525
+ }
526
+ return rt.record.snapshot();
527
+ }
528
+ catch (err) {
529
+ // Release the resume reservation on every failure path so it never leaks.
530
+ if (resumeThreadId != null) {
531
+ this.reservedResumeThreads.delete(resumeThreadId);
532
+ }
533
+ // Failed before the turn was established: tear down, unregister, and
534
+ // release the lease (the session never reached a running turn). Guard the
535
+ // rare race where finalizeTurn already ran during the turnStart
536
+ // provisional flush (a fast turn went terminal) and the IIFE then threw
537
+ // afterwards (e.g. extractTurnId on a malformed turn result): that path
538
+ // already completed the turn and released the lease via finalizeTurn, so
539
+ // neither relabel the completed turn as "failed" nor double-release the
540
+ // lease. Once the runtime exists, runtime.hasLease/finalized are the
541
+ // source of truth (finalizeTurn flips them together); the local leaseHeld
542
+ // only covers a throw before the runtime was created.
543
+ if (runtime != null) {
544
+ if (!runtime.finalized) {
545
+ runtime.unsubscribe?.();
546
+ this.clearTimers(runtime);
547
+ runtime.finalized = true;
548
+ const failureReason = err instanceof Error ? err.message : String(err);
549
+ const now = this.now();
550
+ runtime.record.completeTurn("failed", { failureReason }, now);
551
+ this.observeTerminal(runtime, "failed");
552
+ this.observeError(runtime, failureReason, now, null);
553
+ this.observeLifecycle(runtime, "failed", now, null);
554
+ runtime.terminal.resolve();
555
+ this.sessions.delete(opts.sessionId);
556
+ if (runtime.hasLease) {
557
+ this.lease.release();
558
+ runtime.hasLease = false;
559
+ }
560
+ }
561
+ }
562
+ else if (leaseHeld) {
563
+ // Acquired the lease but threw before the runtime existed
564
+ // (ensureReady/threadResume/threadStart, or a pre-runtime budget
565
+ // timeout): release the lease taken at the top of the try.
566
+ this.lease.release();
567
+ }
568
+ // On a startup-budget timeout the child is presumed silent/hung: recycle
569
+ // it so the leaked pending request + provisional buffer in the client get
570
+ // cleared by handleChildClose (review fix 3).
571
+ if (err instanceof BudgetExceededError) {
572
+ await this.client
573
+ .killProcessGroup("startSession budget exceeded")
574
+ .catch(() => { });
575
+ // The kill terminated the SHARED child, so every OTHER running session
576
+ // is dead too. The client suppresses onProcessDied for intentional
577
+ // kills, so fan out here or their leases leak (review fix). The
578
+ // triggering session was already torn down + removed above.
579
+ this.finalizeCoActiveAfterSharedKill(undefined, "startSession budget exceeded");
580
+ }
581
+ throw err;
582
+ }
583
+ }
584
+ buildThreadStartParams(opts) {
585
+ const params = { sandbox: opts.sandbox };
586
+ if (opts.model != null)
587
+ params.model = opts.model;
588
+ if (opts.cwd != null)
589
+ params.cwd = opts.cwd;
590
+ if (opts.approvalPolicy != null)
591
+ params.approvalPolicy = opts.approvalPolicy;
592
+ return params;
593
+ }
594
+ buildTurnStartParams(threadId, opts) {
595
+ const params = { threadId, input: opts.input };
596
+ if (opts.cwd != null)
597
+ params.cwd = opts.cwd;
598
+ if (opts.model != null)
599
+ params.model = opts.model;
600
+ if (opts.approvalPolicy != null)
601
+ params.approvalPolicy = opts.approvalPolicy;
602
+ // turn/start requires the TAGGED-OBJECT sandboxPolicy, never the string.
603
+ // When the caller omits it, derive from the thread's sandbox mode so the
604
+ // client does not silently downgrade to read-only (review fix 8).
605
+ params.sandboxPolicy =
606
+ opts.sandboxPolicy ??
607
+ sandboxPolicyFromMode(opts.sandbox, opts.cwd != null ? { writableRoots: [opts.cwd] } : undefined);
608
+ if (opts.outputSchema != null)
609
+ params.outputSchema = opts.outputSchema;
610
+ return params;
611
+ }
612
+ /** Snapshot without consuming an active-turn lease (2-4). */
613
+ poll(sessionId) {
614
+ return this.require(sessionId).record.snapshot();
615
+ }
616
+ list(filter) {
617
+ const all = [...this.sessions.values()].map((rt) => rt.record.snapshot());
618
+ if (filter?.status != null) {
619
+ return all.filter((s) => s.status === filter.status);
620
+ }
621
+ return all;
622
+ }
623
+ /** Bounded event-log read with a monotonic cursor; never consumes a lease (2-4,2-5). */
624
+ getLog(sessionId, cursor, limit) {
625
+ return this.require(sessionId).record.events.read(cursor, limit);
626
+ }
627
+ /**
628
+ * Discovery passthrough for `model/list`. Spawns/initializes the app-server
629
+ * child if needed (via the client) but never starts a session or consumes a
630
+ * lease. Bounded by the start budget like other pre-turn RPCs.
631
+ */
632
+ async listModels(params) {
633
+ return this.withBudget(this.client.modelList(params), this.config.startBudgetMs, "model/list RPC budget exceeded");
634
+ }
635
+ /**
636
+ * Await the active turn's terminality WITHOUT consuming a lease. An optional
637
+ * `timeoutMs` bounds the wait only — the underlying turn keeps running and
638
+ * retains its lease on wait-timeout.
639
+ */
640
+ async waitForTurn(sessionId, opts) {
641
+ const runtime = this.require(sessionId);
642
+ if (runtime.finalized || runtime.record.status !== "running") {
643
+ return runtime.record.snapshot();
644
+ }
645
+ if (runtime.record.pendingApprovals.size > 0) {
646
+ return runtime.record.snapshot();
647
+ }
648
+ await this.raceTurnWait(runtime, opts?.timeoutMs, opts?.signal);
649
+ return runtime.record.snapshot();
650
+ }
651
+ /** Reply to a pending approval. The turn continues; the lease is NOT released. */
652
+ async respondApproval(sessionId, approvalId, decision) {
653
+ const runtime = this.require(sessionId);
654
+ const pending = runtime.record.getPendingApproval(approvalId);
655
+ if (pending == null) {
656
+ throw new InvalidParamsError(`stale or unknown approval: ${String(approvalId)}`);
657
+ }
658
+ this.clearApprovalTimer(runtime);
659
+ try {
660
+ // Defensive: permissions approvals are auto-handled on arrival and never
661
+ // surfaced, so this path normally only sees command/file. If a permissions
662
+ // pending somehow reaches here, use the empty-grant shape (not {decision}).
663
+ await this.withBudget(pending.type === "permissions"
664
+ ? this.client.respondPermissionsApproval(approvalId)
665
+ : this.client.respondApproval({ id: approvalId, decision }), this.config.interruptBudgetMs);
666
+ }
667
+ catch (err) {
668
+ // Restore the auto-decline/escalation backstop the timer provides before
669
+ // surfacing the failure (review fix 4).
670
+ if (runtime.record.pendingApprovals.size > 0) {
671
+ this.scheduleApprovalTimeout(runtime);
672
+ }
673
+ throw err;
674
+ }
675
+ const now = this.now();
676
+ runtime.record.clearPendingApproval(approvalId, now);
677
+ this.observeApprovalResolution(runtime, approvalId, decision, now);
678
+ if (runtime.record.pendingApprovals.size > 0) {
679
+ this.scheduleApprovalTimeout(runtime);
680
+ }
681
+ return runtime.record.snapshot();
682
+ }
683
+ /**
684
+ * Steer the currently active turn. This never starts a new turn and never
685
+ * acquires a new lease; it targets the active turn id captured in the record.
686
+ */
687
+ async steer(sessionId, input) {
688
+ const runtime = this.require(sessionId);
689
+ if (runtime.finalized || runtime.record.status !== "running") {
690
+ throw new InvalidParamsError(`No active turn for session: ${sessionId}`);
691
+ }
692
+ const turnId = runtime.record.activeTurnId;
693
+ if (turnId == null) {
694
+ throw new InvalidParamsError(`No active turn for session: ${sessionId}`);
695
+ }
696
+ await this.withBudget(this.client.turnSteer({
697
+ threadId: runtime.record.threadId,
698
+ expectedTurnId: turnId,
699
+ input,
700
+ }), this.config.interruptBudgetMs, "turnSteer RPC budget exceeded");
701
+ this.observeUserInput(runtime, input, this.now());
702
+ return runtime.record.snapshot();
703
+ }
704
+ /** Explicit cancel → interrupted snapshot (decision 4). */
705
+ async interrupt(sessionId) {
706
+ const runtime = this.require(sessionId);
707
+ if (runtime.finalized || runtime.record.status !== "running") {
708
+ return runtime.record.snapshot();
709
+ }
710
+ await this.terminateTurn(runtime, "interrupted", "explicit interrupt");
711
+ return runtime.record.snapshot();
712
+ }
713
+ /** Remove a session. Running sessions are skipped unless force:true is supplied. */
714
+ async cleanup(sessionId, opts = {}) {
715
+ const runtime = this.require(sessionId);
716
+ if (!runtime.finalized && runtime.record.status === "running") {
717
+ if (opts.force !== true) {
718
+ return { sessionId, cleanedUp: false, skippedActive: true };
719
+ }
720
+ await this.terminateTurn(runtime, "interrupted", "session cleanup");
721
+ }
722
+ this.evict(sessionId, runtime, "cleanup");
723
+ return { sessionId, cleanedUp: true, skippedActive: false };
724
+ }
725
+ /**
726
+ * Standalone shutdown: interrupt every active turn, then kill the app-server
727
+ * child by process group via the client's guarded kill.
728
+ */
729
+ async shutdown(reason = "shutdown") {
730
+ const running = [...this.sessions.values()].filter((rt) => !rt.finalized && rt.record.status === "running");
731
+ for (const runtime of running) {
732
+ await this.terminateTurn(runtime, "interrupted", reason);
733
+ }
734
+ try {
735
+ await this.client.killProcessGroup(reason);
736
+ }
737
+ catch (err) {
738
+ logger.error({ err, reason }, "app-server-registry: killProcessGroup during shutdown failed");
739
+ }
740
+ for (const [id, runtime] of [...this.sessions]) {
741
+ this.clearTimers(runtime);
742
+ runtime.unsubscribe?.();
743
+ runtime.unsubscribe = undefined;
744
+ this.sessions.delete(id);
745
+ }
746
+ this.unsubscribeProcessDied();
747
+ }
748
+ /** TTL + retained-cap reaping of idle sessions (2-4). Running sessions are never reaped. */
749
+ reapIdle() {
750
+ const now = this.now();
751
+ if (this.config.sessionTtlMs > 0) {
752
+ for (const [id, runtime] of [...this.sessions]) {
753
+ if (runtime.record.status === "running")
754
+ continue;
755
+ if (now - runtime.record.lastActivityAtMs >= this.config.sessionTtlMs) {
756
+ this.evict(id, runtime, "ttl");
757
+ }
758
+ }
759
+ }
760
+ const idle = [...this.sessions.entries()].filter(([, rt]) => rt.record.status !== "running");
761
+ const overflow = idle.length - this.config.maxRetainedSessions;
762
+ if (overflow > 0) {
763
+ idle.sort((a, b) => a[1].record.lastActivityAtMs - b[1].record.lastActivityAtMs);
764
+ for (let i = 0; i < overflow; i++) {
765
+ const entry = idle[i];
766
+ if (entry == null)
767
+ break;
768
+ this.evict(entry[0], entry[1], "retained-cap");
769
+ }
770
+ }
771
+ }
772
+ // --- internal lifecycle --------------------------------------------------
773
+ handleEvent(runtime, event) {
774
+ if (runtime.finalized)
775
+ return;
776
+ const now = this.now();
777
+ runtime.record.appendEvent(event, now);
778
+ switch (event.method) {
779
+ case CODEX_EVENT_TURN_STARTED: {
780
+ const turnId = event.params.turn.id;
781
+ if (runtime.record.activeTurnId == null) {
782
+ runtime.record.beginTurn(turnId, now);
783
+ this.scheduleTurnTimeout(runtime);
784
+ this.observeRunning(runtime);
785
+ this.observeLifecycle(runtime, "running", now);
786
+ }
787
+ break;
788
+ }
789
+ case CODEX_EVENT_AGENT_MESSAGE_DELTA: {
790
+ // Streaming assistant text. Accumulate into the bounded running preview
791
+ // so a poller can watch a turn mid-stream without a second get_log; the
792
+ // delta is also retained verbatim (capped) in the ring via appendEvent.
793
+ runtime.record.appendAssistantDelta(event.params.delta, event.params.turnId, now);
794
+ break;
795
+ }
796
+ case CODEX_EVENT_ITEM_STARTED: {
797
+ const observation = itemObservation(event.params.item, "started");
798
+ if (observation != null) {
799
+ this.runSessionObservation(runtime, observation, now);
800
+ }
801
+ break;
802
+ }
803
+ case CODEX_EVENT_ITEM_COMPLETED: {
804
+ const item = event.params.item;
805
+ if (item.type === "agentMessage" && typeof item.text === "string") {
806
+ // Cache the latest final-answer text; capped at assignment so any
807
+ // terminal path exposes a bounded value (review fix 7).
808
+ runtime.record.setFinalText(item.text);
809
+ }
810
+ const observation = itemObservation(event.params.item, "completed");
811
+ if (observation != null) {
812
+ this.runSessionObservation(runtime, observation, now);
813
+ }
814
+ break;
815
+ }
816
+ case CODEX_EVENT_THREAD_TOKEN_USAGE_UPDATED: {
817
+ runtime.record.recordTokenUsage(event.params.tokenUsage, now);
818
+ const last = event.params.tokenUsage.last;
819
+ this.runSessionObservation(runtime, {
820
+ kind: "usage",
821
+ inputTokens: optionalTokenCount(last.inputTokens),
822
+ cachedInputTokens: optionalTokenCount(last.cachedInputTokens),
823
+ outputTokens: optionalTokenCount(last.outputTokens),
824
+ reasoningTokens: optionalTokenCount(last.reasoningOutputTokens),
825
+ incomplete: runtime.record.tokenUsage.incomplete,
826
+ }, now);
827
+ break;
828
+ }
829
+ case CODEX_EVENT_TURN_COMPLETED: {
830
+ const status = mapTurnStatus(event.params.turn.status);
831
+ // finalText already cached from item/completed (if any).
832
+ const finalText = runtime.record.finalText;
833
+ this.finalizeTurn(runtime, status, { finalText });
834
+ break;
835
+ }
836
+ case CODEX_EVENT_TURN_DIFF_UPDATED: {
837
+ runtime.record.setLatestDiff(event.params.diff, now);
838
+ break;
839
+ }
840
+ case CODEX_EVENT_TURN_PLAN_UPDATED: {
841
+ runtime.record.setLatestPlan(event.params.plan, event.params.explanation ?? null, now);
842
+ break;
843
+ }
844
+ case CODEX_EVENT_ITEM_PLAN_DELTA:
845
+ // Streaming plan text; no snapshot field — it is already retained in the
846
+ // event ring via appendEvent above (get_log surfaces it).
847
+ break;
848
+ case CODEX_REQUEST_PERMISSIONS_APPROVAL: {
849
+ // Permissions (sandbox-escalation) approvals are never surfaced: under
850
+ // the read-only / approval:never posture we auto-decline on arrival with
851
+ // a protocol-valid empty grant (grants nothing). Best-effort and bounded;
852
+ // a failure here cannot stall the turn beyond the wall-clock timeout.
853
+ const pending = pendingApprovalFromEvent(event, "permissions", now);
854
+ this.observeApprovalRequest(runtime, pending, now);
855
+ void this.autoDeclinePermissionsApproval(runtime, event.id);
856
+ break;
857
+ }
858
+ case CODEX_REQUEST_COMMAND_EXECUTION_APPROVAL:
859
+ case CODEX_REQUEST_FILE_CHANGE_APPROVAL: {
860
+ const type = approvalTypeFromMethod(event.method);
861
+ if (type == null)
862
+ break;
863
+ const pending = pendingApprovalFromEvent(event, type, now);
864
+ runtime.record.setPendingApproval(pending, now);
865
+ this.notifyAttention(runtime);
866
+ this.scheduleApprovalTimeout(runtime);
867
+ this.observeApprovalRequest(runtime, pending, now);
868
+ break;
869
+ }
870
+ default:
871
+ break;
872
+ }
873
+ }
874
+ /**
875
+ * Finalize a turn on PROVABLE terminality. Idempotent. Clears timers,
876
+ * unsubscribes, releases the lease, and resolves the terminal deferred.
877
+ */
878
+ finalizeTurn(runtime, status, opts) {
879
+ if (runtime.finalized)
880
+ return;
881
+ runtime.finalized = true;
882
+ this.clearTimers(runtime);
883
+ runtime.unsubscribe?.();
884
+ runtime.unsubscribe = undefined;
885
+ const turnId = runtime.record.activeTurnId ?? null;
886
+ const now = this.now();
887
+ runtime.record.completeTurn(status, opts, now);
888
+ this.observeTerminal(runtime, status);
889
+ if (status === "failed" || status === "timeout") {
890
+ this.observeError(runtime, runtime.record.failureReason ??
891
+ (status === "timeout"
892
+ ? "Codex app-server turn timed out."
893
+ : "Codex app-server turn failed."), now, turnId);
894
+ }
895
+ this.observeLifecycle(runtime, status, now, turnId);
896
+ if (runtime.hasLease) {
897
+ this.lease.release();
898
+ runtime.hasLease = false;
899
+ }
900
+ runtime.terminal.resolve();
901
+ }
902
+ runSessionObservation(runtime, semantic, atMs = this.now(), turnId = runtime.record.activeTurnId ?? null) {
903
+ const observer = runtime.sessionObserver ?? this.observer;
904
+ if (observer == null)
905
+ return;
906
+ // Capture sequence and identity synchronously at the semantic reduction
907
+ // point. The async continuation receives only this immutable snapshot.
908
+ const observation = {
909
+ ...semantic,
910
+ sequence: runtime.nextObservationSequence++,
911
+ sessionId: runtime.record.sessionId,
912
+ threadId: runtime.record.threadId,
913
+ turnId,
914
+ atMs,
915
+ };
916
+ const deliver = async () => {
917
+ try {
918
+ await observer(observation);
919
+ }
920
+ catch (err) {
921
+ logger.debug({
922
+ err: err instanceof Error ? err.message : String(err),
923
+ sessionId: observation.sessionId,
924
+ observationKind: observation.kind,
925
+ sequence: observation.sequence,
926
+ }, "app-server-registry: session observation failed open");
927
+ }
928
+ };
929
+ runtime.observerTail = runtime.observerTail.then(deliver, deliver);
930
+ }
931
+ bindSessionObserverTurn(runtime, turnId) {
932
+ const observer = runtime.sessionObserver ?? this.observer;
933
+ if (observer?.bindTurn == null)
934
+ return;
935
+ const binding = Object.freeze({
936
+ sessionId: runtime.record.sessionId,
937
+ threadId: runtime.record.threadId,
938
+ turnId,
939
+ });
940
+ const deliver = async () => {
941
+ try {
942
+ await observer.bindTurn(binding);
943
+ }
944
+ catch {
945
+ logger.debug({ sessionId: binding.sessionId }, "app-server-registry: session turn binding failed open");
946
+ }
947
+ };
948
+ runtime.observerTail = runtime.observerTail.then(deliver, deliver);
949
+ }
950
+ observeUserInput(runtime, input, atMs = this.now()) {
951
+ const content = turnInputText(input);
952
+ if (content == null)
953
+ return;
954
+ this.runSessionObservation(runtime, { kind: "user", content }, atMs, runtime.record.activeTurnId ?? null);
955
+ }
956
+ observeApprovalRequest(runtime, approval, atMs = this.now()) {
957
+ this.runSessionObservation(runtime, {
958
+ kind: "approval_request",
959
+ approvalId: approval.id,
960
+ approvalType: approval.type,
961
+ action: approvalAction(approval),
962
+ }, atMs);
963
+ }
964
+ observeApprovalResolution(runtime, approvalId, decision, atMs = this.now()) {
965
+ this.runSessionObservation(runtime, {
966
+ kind: "approval_resolution",
967
+ approvalId,
968
+ decision,
969
+ }, atMs);
970
+ }
971
+ observeError(runtime, message, atMs = this.now(), turnId = runtime.record.activeTurnId ?? null) {
972
+ this.runSessionObservation(runtime, {
973
+ kind: "error",
974
+ code: "app_server_session_failed",
975
+ message,
976
+ retryable: false,
977
+ }, atMs, turnId);
978
+ }
979
+ observeLifecycle(runtime, status, atMs = this.now(), turnId = runtime.record.activeTurnId ?? null) {
980
+ this.runSessionObservation(runtime, {
981
+ kind: "lifecycle",
982
+ status,
983
+ failureReason: runtime.record.failureReason ?? null,
984
+ }, atMs, turnId);
985
+ }
986
+ observeRunning(runtime) {
987
+ const observation = runtime.activityObservation;
988
+ if (observation == null)
989
+ return;
990
+ this.runActivityObservation(runtime, "running", () => observation.transition("running"));
991
+ }
992
+ observeTerminal(runtime, status) {
993
+ const observation = runtime.activityObservation;
994
+ if (observation == null)
995
+ return;
996
+ const completion = status === "completed"
997
+ ? { status: "succeeded", outcomeKind: "success" }
998
+ : status === "interrupted"
999
+ ? { status: "cancelled", outcomeKind: "cancelled" }
1000
+ : { status: "failed", outcomeKind: "error" };
1001
+ this.runActivityObservation(runtime, "terminal", () => observation.complete(completion));
1002
+ }
1003
+ runActivityObservation(runtime, boundary, operation) {
1004
+ const report = (err) => {
1005
+ logger.debug({
1006
+ err: err instanceof Error ? err.message : String(err),
1007
+ sessionId: runtime.record.sessionId,
1008
+ }, `app-server-registry: dashboard ${boundary} observation failed open`);
1009
+ };
1010
+ try {
1011
+ void operation().catch(report);
1012
+ }
1013
+ catch (err) {
1014
+ report(err);
1015
+ }
1016
+ }
1017
+ /**
1018
+ * Shared terminality/escalation path (2-8 / 2-9):
1019
+ * 1. bounded interrupt RPC
1020
+ * 2. bounded wait for terminal turn/completed
1021
+ * 3. on hang/no-terminal → process-group kill + respawn + mark terminal
1022
+ *
1023
+ * The lease is released only via {@link finalizeTurn}, i.e. only once the
1024
+ * turn is provably terminal.
1025
+ */
1026
+ async terminateTurn(runtime, statusOnKill, reason) {
1027
+ if (runtime.finalized)
1028
+ return;
1029
+ const threadId = runtime.record.threadId;
1030
+ const turnId = runtime.record.activeTurnId;
1031
+ if (turnId != null) {
1032
+ try {
1033
+ await this.withBudget(this.client.turnInterrupt({ threadId, turnId }), this.config.interruptBudgetMs);
1034
+ const reachedTerminal = await this.waitTerminalWithin(runtime, this.config.terminalWaitBudgetMs);
1035
+ if (reachedTerminal)
1036
+ return; // finalized via turn/completed event
1037
+ }
1038
+ catch (err) {
1039
+ logger.warn({ err, reason, sessionId: runtime.record.sessionId }, "app-server-registry: interrupt RPC failed or timed out — escalating");
1040
+ }
1041
+ }
1042
+ if (runtime.finalized)
1043
+ return;
1044
+ await this.forceKill(runtime, statusOnKill, reason);
1045
+ }
1046
+ async forceKill(runtime, status, reason) {
1047
+ if (runtime.finalized)
1048
+ return;
1049
+ try {
1050
+ await this.client.killProcessGroup(reason);
1051
+ }
1052
+ catch (err) {
1053
+ logger.error({ err, reason, sessionId: runtime.record.sessionId }, "app-server-registry: killProcessGroup failed during escalation");
1054
+ }
1055
+ // Release leases FIRST — the kill terminated the SHARED child, so every
1056
+ // in-flight turn is dead. The client suppresses onProcessDied for
1057
+ // intentional kills, so we must finalize here or leases leak forever
1058
+ // (review fix). This must NOT depend on the respawn: a silent respawned
1059
+ // child would otherwise block escalation before any lease is released.
1060
+ // Future startSession calls re-ensureReady themselves.
1061
+ this.finalizeTurn(runtime, status, { failureReason: reason });
1062
+ this.finalizeCoActiveAfterSharedKill(runtime, reason);
1063
+ try {
1064
+ // Best-effort respawn ownership is the client's — registry only nudges
1065
+ // it, and bounds it so a silent child cannot hang escalation.
1066
+ await this.withBudget(this.client.ensureReady(), this.config.startBudgetMs);
1067
+ }
1068
+ catch (err) {
1069
+ logger.error({ err, sessionId: runtime.record.sessionId }, "app-server-registry: ensureReady (respawn) failed during escalation");
1070
+ }
1071
+ }
1072
+ /**
1073
+ * Finalize every co-active running session after the SHARED child was killed.
1074
+ * The client suppresses onProcessDied for intentional kills, so without this
1075
+ * their leases leak (review fix). finalizeTurn is idempotent; `except` is the
1076
+ * triggering runtime, handled by the caller.
1077
+ */
1078
+ finalizeCoActiveAfterSharedKill(except, reason) {
1079
+ for (const other of this.sessions.values()) {
1080
+ if (other === except)
1081
+ continue;
1082
+ if (other.finalized || other.record.status !== "running")
1083
+ continue;
1084
+ this.finalizeTurn(other, "failed", {
1085
+ failureReason: `shared app-server killed: ${reason}`,
1086
+ });
1087
+ }
1088
+ }
1089
+ async onTurnTimeout(runtime) {
1090
+ if (runtime.finalized)
1091
+ return;
1092
+ logger.warn({ sessionId: runtime.record.sessionId }, "app-server-registry: turn wall-clock timeout — attempting interrupt");
1093
+ await this.terminateTurn(runtime, "timeout", "turn wall-clock timeout");
1094
+ }
1095
+ async onApprovalTimeout(runtime) {
1096
+ if (runtime.finalized)
1097
+ return;
1098
+ const pendingApprovals = [...runtime.record.pendingApprovals.values()];
1099
+ if (pendingApprovals.length === 0)
1100
+ return;
1101
+ // Auto-decline is NOT terminal (2-9): the turn may continue, and the
1102
+ // decline RPC can hang. Bound it, then await terminality, then escalate.
1103
+ try {
1104
+ for (const pending of pendingApprovals) {
1105
+ if (runtime.finalized)
1106
+ return;
1107
+ // Defensive: permissions approvals are auto-handled on arrival, so this
1108
+ // path normally only sees command/file. Route any stray permissions
1109
+ // pending to the empty-grant shape rather than {decision}.
1110
+ await this.withBudget(pending.type === "permissions"
1111
+ ? this.client.respondPermissionsApproval(pending.id)
1112
+ : this.client.respondApproval({
1113
+ id: pending.id,
1114
+ decision: "decline",
1115
+ }), this.config.interruptBudgetMs);
1116
+ const now = this.now();
1117
+ runtime.record.clearPendingApproval(pending.id, now);
1118
+ this.observeApprovalResolution(runtime, pending.id, "decline", now);
1119
+ }
1120
+ const reachedTerminal = await this.waitTerminalWithin(runtime, this.config.terminalWaitBudgetMs);
1121
+ if (reachedTerminal)
1122
+ return; // finalized via turn/completed event
1123
+ }
1124
+ catch (err) {
1125
+ logger.warn({ err, sessionId: runtime.record.sessionId }, "app-server-registry: auto-decline RPC failed or timed out — escalating");
1126
+ }
1127
+ if (runtime.finalized)
1128
+ return;
1129
+ await this.forceKill(runtime, "failed", "pending-approval timeout");
1130
+ }
1131
+ /**
1132
+ * Auto-decline a permissions (sandbox-escalation) approval the moment it
1133
+ * arrives, with a protocol-valid empty grant. Never surfaced to the user
1134
+ * (read-only / approval:never posture). Bounded: the decline RPC is capped by
1135
+ * `interruptBudgetMs`.
1136
+ *
1137
+ * On decline failure/timeout we do NOT merely log: Codex's child is blocked on
1138
+ * a response that never lands, and if the operator disabled the wall-clock
1139
+ * turn timeout (`CLINKX_TURN_TIMEOUT_SECONDS=0`) there is no backstop until
1140
+ * idle reap. So we escalate the turn directly under a bounded budget,
1141
+ * independent of the global turn timeout, mirroring the command/file
1142
+ * decline-failure recovery in {@link onApprovalTimeout}. terminateTurn is
1143
+ * idempotent and no-ops if the turn already finalized, so this never
1144
+ * double-finalizes.
1145
+ */
1146
+ async autoDeclinePermissionsApproval(runtime, id) {
1147
+ try {
1148
+ await this.withBudget(this.client.respondPermissionsApproval(id), this.config.interruptBudgetMs);
1149
+ this.observeApprovalResolution(runtime, id, "decline", this.now());
1150
+ logger.debug({ sessionId: runtime.record.sessionId, approvalId: id }, "app-server-registry: auto-declined permissions approval (empty grant)");
1151
+ }
1152
+ catch (err) {
1153
+ logger.warn({ err, sessionId: runtime.record.sessionId, approvalId: id }, "app-server-registry: permissions auto-decline failed — escalating turn");
1154
+ // Failure-safe backstop: the child is wedged awaiting a response we could
1155
+ // not deliver. Escalate independently of the (possibly disabled) turn
1156
+ // wall-clock timeout. Idempotent; bounded by interrupt/terminal budgets.
1157
+ await this.terminateTurn(runtime, "failed", "permissions auto-decline failed").catch((escalationErr) => {
1158
+ logger.error({ err: escalationErr, sessionId: runtime.record.sessionId, approvalId: id }, "app-server-registry: escalation after permissions auto-decline failure errored");
1159
+ });
1160
+ }
1161
+ }
1162
+ handleProcessDied(event) {
1163
+ const reason = "app-server process died" +
1164
+ (event.signal != null ? ` (signal ${event.signal})` : "") +
1165
+ (event.exitCode != null ? ` (exit ${event.exitCode})` : "");
1166
+ for (const runtime of this.sessions.values()) {
1167
+ // Crash split (2-10): only active/in-flight turns are unrecoverable.
1168
+ // Idle/completed sessions keep their resumable threadId untouched.
1169
+ if (!runtime.finalized && runtime.record.status === "running") {
1170
+ this.finalizeTurn(runtime, "failed", { failureReason: reason });
1171
+ }
1172
+ }
1173
+ }
1174
+ // --- timers --------------------------------------------------------------
1175
+ scheduleTurnTimeout(runtime) {
1176
+ // Honor an explicit per-session override (review fix B); fall back to the
1177
+ // global default/backstop. A value of 0 disables the timeout, preserving
1178
+ // the global's disabled semantics exactly.
1179
+ const turnTimeoutMs = runtime.turnTimeoutMs ?? this.config.turnTimeoutMs;
1180
+ if (turnTimeoutMs <= 0)
1181
+ return;
1182
+ if (runtime.turnTimer != null)
1183
+ this.clearTimer(runtime.turnTimer);
1184
+ runtime.turnTimer = this.setTimer(() => {
1185
+ void this.onTurnTimeout(runtime);
1186
+ }, turnTimeoutMs);
1187
+ }
1188
+ scheduleApprovalTimeout(runtime) {
1189
+ if (this.config.approvalTimeoutMs <= 0)
1190
+ return;
1191
+ this.clearApprovalTimer(runtime);
1192
+ runtime.approvalTimer = this.setTimer(() => {
1193
+ void this.onApprovalTimeout(runtime);
1194
+ }, this.config.approvalTimeoutMs);
1195
+ }
1196
+ clearApprovalTimer(runtime) {
1197
+ if (runtime.approvalTimer != null) {
1198
+ this.clearTimer(runtime.approvalTimer);
1199
+ runtime.approvalTimer = undefined;
1200
+ }
1201
+ }
1202
+ clearTimers(runtime) {
1203
+ if (runtime.turnTimer != null) {
1204
+ this.clearTimer(runtime.turnTimer);
1205
+ runtime.turnTimer = undefined;
1206
+ }
1207
+ this.clearApprovalTimer(runtime);
1208
+ }
1209
+ evict(sessionId, runtime, why) {
1210
+ this.clearTimers(runtime);
1211
+ runtime.unsubscribe?.();
1212
+ runtime.unsubscribe = undefined;
1213
+ this.sessions.delete(sessionId);
1214
+ logger.debug({ sessionId, why }, "app-server-registry: evicted session");
1215
+ }
1216
+ // --- bounded-wait helpers ------------------------------------------------
1217
+ /** Reject if `promise` does not settle within `ms`. */
1218
+ withBudget(promise, ms, message) {
1219
+ if (ms <= 0)
1220
+ return promise;
1221
+ return new Promise((resolve, reject) => {
1222
+ const handle = this.setTimer(() => {
1223
+ reject(new BudgetExceededError(message));
1224
+ }, ms);
1225
+ promise.then((value) => {
1226
+ this.clearTimer(handle);
1227
+ resolve(value);
1228
+ }, (err) => {
1229
+ this.clearTimer(handle);
1230
+ reject(err instanceof Error ? err : new Error(String(err)));
1231
+ });
1232
+ });
1233
+ }
1234
+ /** Resolve `true` if the terminal deferred settles within `ms`, else `false`. */
1235
+ waitTerminalWithin(runtime, ms) {
1236
+ if (runtime.finalized)
1237
+ return Promise.resolve(true);
1238
+ if (ms <= 0)
1239
+ return Promise.resolve(false);
1240
+ return new Promise((resolve) => {
1241
+ let settled = false;
1242
+ const handle = this.setTimer(() => {
1243
+ if (settled)
1244
+ return;
1245
+ settled = true;
1246
+ resolve(false);
1247
+ }, ms);
1248
+ void runtime.terminal.promise.then(() => {
1249
+ if (settled)
1250
+ return;
1251
+ settled = true;
1252
+ this.clearTimer(handle);
1253
+ resolve(true);
1254
+ });
1255
+ });
1256
+ }
1257
+ notifyAttention(runtime) {
1258
+ const attention = runtime.attention;
1259
+ runtime.attention = createDeferred();
1260
+ attention.resolve();
1261
+ }
1262
+ /**
1263
+ * Await terminality, a wait-local timeout, or newly requested human attention.
1264
+ * All outcomes are lease-free: a pending approval returns a running snapshot.
1265
+ */
1266
+ raceTurnWait(runtime, timeoutMs, signal) {
1267
+ return new Promise((resolve) => {
1268
+ let settled = false;
1269
+ let handle;
1270
+ let onAbort;
1271
+ const done = () => {
1272
+ if (settled)
1273
+ return;
1274
+ settled = true;
1275
+ if (handle != null)
1276
+ this.clearTimer(handle);
1277
+ if (onAbort != null && signal != null) {
1278
+ signal.removeEventListener("abort", onAbort);
1279
+ }
1280
+ resolve();
1281
+ };
1282
+ if (signal?.aborted) {
1283
+ // Already aborted: resolve immediately without arming a timer.
1284
+ resolve();
1285
+ return;
1286
+ }
1287
+ if (timeoutMs != null && timeoutMs > 0) {
1288
+ handle = this.setTimer(done, timeoutMs);
1289
+ }
1290
+ if (signal != null) {
1291
+ onAbort = done;
1292
+ signal.addEventListener("abort", onAbort, { once: true });
1293
+ }
1294
+ void runtime.terminal.promise.then(done);
1295
+ void runtime.attention.promise.then(done);
1296
+ });
1297
+ }
1298
+ }
1299
+ let appServerRegistrySingleton;
1300
+ let appServerRegistrySingletonSpawn;
1301
+ let appServerRegistryTestOverrides;
1302
+ /**
1303
+ * Derive the registry spawn request from a resolved app_server adapter:
1304
+ * `app_server.command` (falling back to the adapter's top-level `command`) and
1305
+ * `app_server.args`.
1306
+ */
1307
+ export function spawnRequestFor(adapter) {
1308
+ return {
1309
+ command: adapter.app_server?.command ?? adapter.command,
1310
+ args: adapter.app_server?.args,
1311
+ };
1312
+ }
1313
+ function resolveSpawnRequest(request) {
1314
+ return {
1315
+ command: request?.command ?? DEFAULT_APP_SERVER_SPAWN_CONFIG.command,
1316
+ args: request?.args ?? [...DEFAULT_APP_SERVER_SPAWN_CONFIG.args],
1317
+ };
1318
+ }
1319
+ function defaultAppServerSpawnConfig(spawn, env) {
1320
+ return {
1321
+ command: spawn.command,
1322
+ args: [...spawn.args],
1323
+ env,
1324
+ };
1325
+ }
1326
+ function createDefaultAppServerRegistry(spawn) {
1327
+ // Resolve config first so the spawned child and the registry share the same
1328
+ // depth: the child must receive CLINKX_AGENT_DEPTH=currentDepth+1 so the
1329
+ // recursion guard survives the env sanitizer (review fix 5).
1330
+ const config = resolveRegistryConfig();
1331
+ const childEnv = childEnvMarkerFor(config.currentDepth);
1332
+ const client = appServerRegistryTestOverrides?.client ??
1333
+ new RealCodexAppServerClient(defaultAppServerSpawnConfig(spawn, childEnv));
1334
+ const registryFactory = appServerRegistryTestOverrides?.registryFactory ??
1335
+ ((deps) => new AppServerRegistry(deps));
1336
+ return registryFactory({ client, config });
1337
+ }
1338
+ /**
1339
+ * Get the process-wide app-server registry, creating it on first call.
1340
+ *
1341
+ * The registry is a singleton: one long-lived `codex app-server` process
1342
+ * multiplexes every session. The adapter's `app_server.command/args` therefore
1343
+ * take effect only at FIRST creation. A later adapter requesting a spawn config
1344
+ * that conflicts with the live one is rejected with {@link InvalidParamsError}
1345
+ * (-32602) rather than silently ignored — building a registry-per-config map
1346
+ * would be over-engineering for a singleton-by-design subsystem.
1347
+ */
1348
+ export function getAppServerRegistry(spawn) {
1349
+ const resolved = resolveSpawnRequest(spawn);
1350
+ if (appServerRegistrySingleton == null) {
1351
+ appServerRegistrySingleton = createDefaultAppServerRegistry(resolved);
1352
+ appServerRegistrySingletonSpawn = resolved;
1353
+ return appServerRegistrySingleton;
1354
+ }
1355
+ // A no-arg call (e.g. clink_agent_manage) is a pure accessor — it never
1356
+ // imposes a spawn config, so it cannot conflict with the live one.
1357
+ if (spawn == null) {
1358
+ return appServerRegistrySingleton;
1359
+ }
1360
+ const live = appServerRegistrySingletonSpawn;
1361
+ if (live != null &&
1362
+ (live.command !== resolved.command ||
1363
+ live.args.length !== resolved.args.length ||
1364
+ live.args.some((arg, i) => arg !== resolved.args[i]))) {
1365
+ throw new InvalidParamsError(`Codex app-server spawn config conflict: the app-server process is already ` +
1366
+ `running as "${live.command} ${live.args.join(" ")}" but this adapter requests ` +
1367
+ `"${resolved.command} ${resolved.args.join(" ")}". The app-server registry is a ` +
1368
+ `single shared process; all app_server adapters must agree on command/args.`);
1369
+ }
1370
+ return appServerRegistrySingleton;
1371
+ }
1372
+ /**
1373
+ * Return the live process-wide registry WITHOUT creating it. Read-only manage
1374
+ * ops (clink_agent_manage) must not lock in a default spawn config before a run
1375
+ * has chosen one — otherwise a later run with a custom app_server.command would
1376
+ * spuriously hit the spawn-config conflict (-32602). See {@link getAppServerRegistry}.
1377
+ */
1378
+ export function peekAppServerRegistry() {
1379
+ return appServerRegistrySingleton;
1380
+ }
1381
+ export async function shutdownAppServerRegistry(reason) {
1382
+ const registry = appServerRegistrySingleton;
1383
+ if (registry == null)
1384
+ return;
1385
+ await registry.shutdown(reason);
1386
+ appServerRegistrySingleton = undefined;
1387
+ appServerRegistrySingletonSpawn = undefined;
1388
+ }
1389
+ /** The spawn command/args the live singleton was created with (test inspection). */
1390
+ export function appServerRegistrySpawnForTest() {
1391
+ return appServerRegistrySingletonSpawn;
1392
+ }
1393
+ export function configureAppServerRegistryForTest(overrides) {
1394
+ appServerRegistryTestOverrides = overrides;
1395
+ appServerRegistrySingleton = undefined;
1396
+ appServerRegistrySingletonSpawn = undefined;
1397
+ }
1398
+ export async function resetAppServerRegistryForTest(reason = "test reset") {
1399
+ const registry = appServerRegistrySingleton;
1400
+ appServerRegistrySingleton = undefined;
1401
+ appServerRegistrySingletonSpawn = undefined;
1402
+ appServerRegistryTestOverrides = undefined;
1403
+ if (registry != null) {
1404
+ await registry.shutdown(reason);
1405
+ }
1406
+ }
1407
+ //# sourceMappingURL=app-server-registry.js.map