@yansigit/opencodex 2.31.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 (826) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +587 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
  11. package/gui/dist/assets/index-CGoDO3uO.css +1 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/icons.svg +24 -0
  14. package/gui/dist/index.html +25 -0
  15. package/gui/dist/logo.png +0 -0
  16. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/cline-color.svg +16 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  25. package/gui/dist/provider-icons/discord.svg +1 -0
  26. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  27. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  28. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  29. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  30. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  31. package/gui/dist/provider-icons/grok.svg +1 -0
  32. package/gui/dist/provider-icons/groq-color.svg +1 -0
  33. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  34. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  35. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  36. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  37. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  38. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  39. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  40. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  41. package/gui/dist/provider-icons/openai.svg +1 -0
  42. package/gui/dist/provider-icons/opencode.svg +2 -0
  43. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  44. package/gui/dist/provider-icons/pi.svg +21 -0
  45. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  46. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  47. package/gui/dist/provider-icons/telegram.svg +1 -0
  48. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  49. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  50. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  51. package/package.json +108 -0
  52. package/src/AGENTS.md +28 -0
  53. package/src/adapters/anthropic-image-guard.ts +251 -0
  54. package/src/adapters/anthropic-image-normalize.ts +518 -0
  55. package/src/adapters/anthropic-output-schema.ts +137 -0
  56. package/src/adapters/anthropic.ts +1327 -0
  57. package/src/adapters/azure.ts +36 -0
  58. package/src/adapters/base.ts +121 -0
  59. package/src/adapters/client-fingerprint.ts +65 -0
  60. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  61. package/src/adapters/command-code.ts +601 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  65. package/src/adapters/cursor/cursor-errors.ts +288 -0
  66. package/src/adapters/cursor/discovery.ts +333 -0
  67. package/src/adapters/cursor/effort-map.ts +151 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +250 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/h2-pool.ts +123 -0
  72. package/src/adapters/cursor/http1-bidi.ts +361 -0
  73. package/src/adapters/cursor/images.ts +704 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +269 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1653 -0
  78. package/src/adapters/cursor/mcp-config.ts +42 -0
  79. package/src/adapters/cursor/mcp-manager.ts +333 -0
  80. package/src/adapters/cursor/message-mapper.ts +49 -0
  81. package/src/adapters/cursor/native-exec-common.ts +76 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  84. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  85. package/src/adapters/cursor/native-exec-network.ts +43 -0
  86. package/src/adapters/cursor/native-exec-shell.ts +547 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +663 -0
  89. package/src/adapters/cursor/protobuf-events.ts +1381 -0
  90. package/src/adapters/cursor/protobuf-request.ts +1032 -0
  91. package/src/adapters/cursor/request-builder.ts +461 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +735 -0
  94. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  95. package/src/adapters/cursor/transport-retry.ts +132 -0
  96. package/src/adapters/cursor/transport.ts +79 -0
  97. package/src/adapters/cursor/types.ts +79 -0
  98. package/src/adapters/cursor.ts +322 -0
  99. package/src/adapters/google-antigravity-hosts.ts +48 -0
  100. package/src/adapters/google-antigravity-replay.ts +827 -0
  101. package/src/adapters/google-antigravity-tools.ts +105 -0
  102. package/src/adapters/google-antigravity-wire.ts +141 -0
  103. package/src/adapters/google-errors.ts +92 -0
  104. package/src/adapters/google-http.ts +460 -0
  105. package/src/adapters/google-tool-schema.ts +238 -0
  106. package/src/adapters/google-truncation.ts +24 -0
  107. package/src/adapters/google-wire-compiler.ts +232 -0
  108. package/src/adapters/google.ts +1377 -0
  109. package/src/adapters/identity.ts +77 -0
  110. package/src/adapters/image.ts +23 -0
  111. package/src/adapters/kiro-constants.ts +16 -0
  112. package/src/adapters/kiro-errors.ts +208 -0
  113. package/src/adapters/kiro-events.ts +197 -0
  114. package/src/adapters/kiro-images.ts +129 -0
  115. package/src/adapters/kiro-retry.ts +312 -0
  116. package/src/adapters/kiro-thinking.ts +112 -0
  117. package/src/adapters/kiro-tool-fallback.ts +36 -0
  118. package/src/adapters/kiro-tools.ts +224 -0
  119. package/src/adapters/kiro-truncation.ts +33 -0
  120. package/src/adapters/kiro-wire.ts +129 -0
  121. package/src/adapters/kiro.ts +1936 -0
  122. package/src/adapters/mimo-free.ts +280 -0
  123. package/src/adapters/openai-chat-url.ts +11 -0
  124. package/src/adapters/openai-chat.ts +1980 -0
  125. package/src/adapters/openai-responses-url.ts +16 -0
  126. package/src/adapters/openai-responses.ts +1911 -0
  127. package/src/adapters/registry.ts +175 -0
  128. package/src/adapters/responses-tool-schema.ts +67 -0
  129. package/src/adapters/run-turn-queue.ts +114 -0
  130. package/src/adapters/tool-call-id.ts +119 -0
  131. package/src/adapters/tool-catalog-nudge.ts +154 -0
  132. package/src/adapters/upstream-http-error.ts +48 -0
  133. package/src/adapters/xai-web-search.ts +185 -0
  134. package/src/bridge.ts +1986 -0
  135. package/src/chat/inbound.ts +319 -0
  136. package/src/chat/outbound.ts +821 -0
  137. package/src/claude/agents-inject.ts +266 -0
  138. package/src/claude/alias.ts +149 -0
  139. package/src/claude/auth-detect.ts +229 -0
  140. package/src/claude/auth-mode-migration.ts +32 -0
  141. package/src/claude/auth-mode.ts +62 -0
  142. package/src/claude/context-windows.ts +205 -0
  143. package/src/claude/desktop-3p-guard.ts +35 -0
  144. package/src/claude/desktop-3p-paths.ts +84 -0
  145. package/src/claude/desktop-3p.ts +615 -0
  146. package/src/claude/desktop-health.ts +26 -0
  147. package/src/claude/desktop-profile.ts +263 -0
  148. package/src/claude/gateway-cache.ts +107 -0
  149. package/src/claude/inbound-debug.ts +163 -0
  150. package/src/claude/inbound.ts +578 -0
  151. package/src/claude/model-info.ts +174 -0
  152. package/src/claude/outbound.ts +926 -0
  153. package/src/cli/access.ts +108 -0
  154. package/src/cli/account-api.ts +302 -0
  155. package/src/cli/account-auth.ts +250 -0
  156. package/src/cli/account-catalog-refresh.ts +14 -0
  157. package/src/cli/account-extended.ts +737 -0
  158. package/src/cli/account-main.ts +317 -0
  159. package/src/cli/account.ts +299 -0
  160. package/src/cli/agent-driven.ts +70 -0
  161. package/src/cli/agent.ts +290 -0
  162. package/src/cli/catalog-prewarm.ts +27 -0
  163. package/src/cli/claude-agent-startup-sync.ts +73 -0
  164. package/src/cli/claude-desktop.ts +213 -0
  165. package/src/cli/claude.ts +355 -0
  166. package/src/cli/codex-log-guard-doctor.ts +103 -0
  167. package/src/cli/codex-shim-autorestore.ts +47 -0
  168. package/src/cli/codex-shim-readiness.ts +76 -0
  169. package/src/cli/combo.ts +127 -0
  170. package/src/cli/config-command.ts +209 -0
  171. package/src/cli/debug.ts +228 -0
  172. package/src/cli/dispatch.ts +585 -0
  173. package/src/cli/doctor.ts +1202 -0
  174. package/src/cli/ensure-desired-integrations.ts +152 -0
  175. package/src/cli/export-command.ts +213 -0
  176. package/src/cli/help.ts +101 -0
  177. package/src/cli/index.ts +973 -0
  178. package/src/cli/init.ts +211 -0
  179. package/src/cli/integrations.ts +260 -0
  180. package/src/cli/interactive-confirm.ts +133 -0
  181. package/src/cli/lab.ts +607 -0
  182. package/src/cli/launcher-context.ts +77 -0
  183. package/src/cli/minimax.ts +497 -0
  184. package/src/cli/models-runtime.ts +245 -0
  185. package/src/cli/models.ts +422 -0
  186. package/src/cli/observe.ts +206 -0
  187. package/src/cli/opencode.ts +588 -0
  188. package/src/cli/provider-replit.ts +232 -0
  189. package/src/cli/provider-runtime.ts +179 -0
  190. package/src/cli/provider.ts +492 -0
  191. package/src/cli/ready.ts +301 -0
  192. package/src/cli/registry.ts +422 -0
  193. package/src/cli/replit-gateway-key-input.ts +138 -0
  194. package/src/cli/root.ts +86 -0
  195. package/src/cli/route-policy.ts +92 -0
  196. package/src/cli/runtime-api.ts +328 -0
  197. package/src/cli/star-prompt.ts +211 -0
  198. package/src/cli/status-oauth.ts +78 -0
  199. package/src/cli/status.ts +328 -0
  200. package/src/cli/system-command.ts +112 -0
  201. package/src/cli/system-restart-client.ts +146 -0
  202. package/src/cli/tray-proxy.ts +199 -0
  203. package/src/cli/v2.ts +268 -0
  204. package/src/cli.ts +10 -0
  205. package/src/clients/config-export.ts +1704 -0
  206. package/src/codex/account-id.ts +34 -0
  207. package/src/codex/account-label.ts +47 -0
  208. package/src/codex/account-lifecycle.ts +172 -0
  209. package/src/codex/account-namespace-match.ts +63 -0
  210. package/src/codex/account-namespaces.ts +195 -0
  211. package/src/codex/account-pause.ts +20 -0
  212. package/src/codex/account-priority.ts +83 -0
  213. package/src/codex/account-runtime-state.ts +31 -0
  214. package/src/codex/account-store.ts +544 -0
  215. package/src/codex/account-usability.ts +43 -0
  216. package/src/codex/admission.ts +256 -0
  217. package/src/codex/affinity-debug.ts +162 -0
  218. package/src/codex/agent-roles-sync.ts +225 -0
  219. package/src/codex/agent-roles.ts +238 -0
  220. package/src/codex/app-server-processes.ts +1143 -0
  221. package/src/codex/app-server-restart-service.ts +232 -0
  222. package/src/codex/auth-api.ts +2147 -0
  223. package/src/codex/auth-collision.ts +109 -0
  224. package/src/codex/auth-context.ts +665 -0
  225. package/src/codex/autostart-health.ts +156 -0
  226. package/src/codex/catalog/account-models.ts +67 -0
  227. package/src/codex/catalog/aggregation.ts +436 -0
  228. package/src/codex/catalog/bundled.ts +549 -0
  229. package/src/codex/catalog/effort.ts +446 -0
  230. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  231. package/src/codex/catalog/kinds.ts +2 -0
  232. package/src/codex/catalog/metadata.ts +664 -0
  233. package/src/codex/catalog/native-models.ts +72 -0
  234. package/src/codex/catalog/parsing.ts +650 -0
  235. package/src/codex/catalog/provider-fetch.ts +2064 -0
  236. package/src/codex/catalog/sync.ts +1883 -0
  237. package/src/codex/catalog-admission.ts +199 -0
  238. package/src/codex/catalog-refresh-status.ts +105 -0
  239. package/src/codex/catalog-write-serialization.ts +242 -0
  240. package/src/codex/catalog.ts +14 -0
  241. package/src/codex/codex-write-lock.ts +384 -0
  242. package/src/codex/convergence-types.ts +614 -0
  243. package/src/codex/convergence.ts +651 -0
  244. package/src/codex/coordinator-doctor.ts +332 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/exec-invocation.ts +22 -0
  249. package/src/codex/features.ts +1566 -0
  250. package/src/codex/generation.ts +202 -0
  251. package/src/codex/history-job.ts +407 -0
  252. package/src/codex/history-lock.ts +242 -0
  253. package/src/codex/history-migration-guardian.ts +108 -0
  254. package/src/codex/history-provider.ts +979 -0
  255. package/src/codex/history-transition.ts +105 -0
  256. package/src/codex/history-worker.ts +220 -0
  257. package/src/codex/home.ts +206 -0
  258. package/src/codex/inject-coordination.ts +290 -0
  259. package/src/codex/inject.ts +1733 -0
  260. package/src/codex/injected-marker.ts +106 -0
  261. package/src/codex/integration-record.ts +266 -0
  262. package/src/codex/internal/catalog-writer.ts +203 -0
  263. package/src/codex/internal/history-writer.ts +80 -0
  264. package/src/codex/journal.ts +225 -0
  265. package/src/codex/log-guard/inspect.ts +506 -0
  266. package/src/codex/log-guard/lock.ts +150 -0
  267. package/src/codex/log-guard/maintenance.ts +403 -0
  268. package/src/codex/log-guard/path-safety.ts +88 -0
  269. package/src/codex/log-guard/policy.ts +44 -0
  270. package/src/codex/log-guard/processes.ts +205 -0
  271. package/src/codex/log-guard/protection.ts +489 -0
  272. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  273. package/src/codex/main-account-cache.ts +56 -0
  274. package/src/codex/main-account.ts +68 -0
  275. package/src/codex/management-convergence.ts +167 -0
  276. package/src/codex/model-cache.ts +273 -0
  277. package/src/codex/model-entitlements.ts +353 -0
  278. package/src/codex/native-main-admission.ts +47 -0
  279. package/src/codex/native-main-auth-temp.ts +187 -0
  280. package/src/codex/native-main-claim.ts +178 -0
  281. package/src/codex/native-main-lock-file.ts +162 -0
  282. package/src/codex/native-main-owner.ts +329 -0
  283. package/src/codex/native-profile-api.ts +247 -0
  284. package/src/codex/native-profile-manager.ts +1531 -0
  285. package/src/codex/native-profile-processes.ts +121 -0
  286. package/src/codex/native-profile-recovery.ts +99 -0
  287. package/src/codex/native-profile-stage-store.ts +387 -0
  288. package/src/codex/native-profile-startup.ts +492 -0
  289. package/src/codex/native-profile-store.ts +855 -0
  290. package/src/codex/native-profile-types.ts +120 -0
  291. package/src/codex/native-residue.ts +682 -0
  292. package/src/codex/paths.ts +144 -0
  293. package/src/codex/plan-from-token.ts +140 -0
  294. package/src/codex/plan.ts +40 -0
  295. package/src/codex/plugins-doctor.ts +242 -0
  296. package/src/codex/pool-rotation.ts +295 -0
  297. package/src/codex/project-config-warnings.ts +425 -0
  298. package/src/codex/prompt-journal.ts +352 -0
  299. package/src/codex/prompt-layers.ts +967 -0
  300. package/src/codex/prompt-lock.ts +143 -0
  301. package/src/codex/quota-rejection.ts +298 -0
  302. package/src/codex/quota.ts +573 -0
  303. package/src/codex/refresh.ts +62 -0
  304. package/src/codex/reset-credit-recovery.ts +1044 -0
  305. package/src/codex/routing.ts +1888 -0
  306. package/src/codex/runtime.ts +659 -0
  307. package/src/codex/shim.ts +2170 -0
  308. package/src/codex/subagent-defaults.ts +550 -0
  309. package/src/codex/subagent-model-fallback.ts +784 -0
  310. package/src/codex/sync.ts +319 -0
  311. package/src/codex/transition-state.ts +612 -0
  312. package/src/codex/upstream-host-health.ts +368 -0
  313. package/src/codex/user-identity.ts +557 -0
  314. package/src/codex/warmup.ts +298 -0
  315. package/src/codex/websocket-registry.ts +100 -0
  316. package/src/codex/write-coordination.ts +114 -0
  317. package/src/combos/failover.ts +160 -0
  318. package/src/combos/index.ts +45 -0
  319. package/src/combos/request.ts +94 -0
  320. package/src/combos/resolve.ts +232 -0
  321. package/src/combos/types.ts +398 -0
  322. package/src/config/provider-name.ts +24 -0
  323. package/src/config.ts +4041 -0
  324. package/src/fork/register.ts +3 -0
  325. package/src/generated/compatibility-version.json +3116 -0
  326. package/src/generated/model-metadata.ts +106 -0
  327. package/src/github/star-state.ts +203 -0
  328. package/src/grok/inject.ts +530 -0
  329. package/src/grok/inspect.ts +45 -0
  330. package/src/grok/status.ts +121 -0
  331. package/src/grok/sync.ts +66 -0
  332. package/src/images/artifacts.ts +516 -0
  333. package/src/images/fulfill-video.ts +163 -0
  334. package/src/images/fulfill.ts +149 -0
  335. package/src/images/index.ts +4 -0
  336. package/src/images/loop.ts +955 -0
  337. package/src/images/plan.ts +143 -0
  338. package/src/images/synthetic-tool.ts +133 -0
  339. package/src/images/types.ts +41 -0
  340. package/src/images/xai-client.ts +141 -0
  341. package/src/images/xai-video-client.ts +163 -0
  342. package/src/index.ts +22 -0
  343. package/src/integrations/config-io.ts +269 -0
  344. package/src/integrations/journal.ts +315 -0
  345. package/src/integrations/merge.ts +135 -0
  346. package/src/integrations/mutation-flight.ts +71 -0
  347. package/src/integrations/native/ownership-preflight.ts +202 -0
  348. package/src/integrations/omp-yaml-source.ts +358 -0
  349. package/src/integrations/owned-refresh.ts +74 -0
  350. package/src/integrations/ownership.ts +111 -0
  351. package/src/integrations/registry.ts +159 -0
  352. package/src/integrations/serialize.ts +314 -0
  353. package/src/integrations/state.ts +361 -0
  354. package/src/integrations/store.ts +103 -0
  355. package/src/integrations/writer-lock.ts +98 -0
  356. package/src/integrations/writer.ts +691 -0
  357. package/src/lab/artifacts/sanitize.ts +586 -0
  358. package/src/lab/artifacts/secure-fs.ts +475 -0
  359. package/src/lab/artifacts/store.ts +310 -0
  360. package/src/lab/automation/budgets.ts +78 -0
  361. package/src/lab/automation/config-persistence.ts +256 -0
  362. package/src/lab/automation/constants.ts +39 -0
  363. package/src/lab/automation/cooldown.ts +103 -0
  364. package/src/lab/automation/dispatch.ts +211 -0
  365. package/src/lab/automation/index.ts +13 -0
  366. package/src/lab/automation/orchestrator.ts +499 -0
  367. package/src/lab/automation/persistence.ts +512 -0
  368. package/src/lab/automation/planner.ts +371 -0
  369. package/src/lab/automation/policy.ts +136 -0
  370. package/src/lab/automation/queue.ts +191 -0
  371. package/src/lab/automation/recovery.ts +24 -0
  372. package/src/lab/automation/route-context.ts +21 -0
  373. package/src/lab/automation/run-key.ts +44 -0
  374. package/src/lab/automation/runs-query.ts +34 -0
  375. package/src/lab/automation/types.ts +160 -0
  376. package/src/lab/conformance/assertion.ts +325 -0
  377. package/src/lab/conformance/digest.ts +22 -0
  378. package/src/lab/conformance/executor.ts +741 -0
  379. package/src/lab/conformance/fixture-provider.ts +27 -0
  380. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  381. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  382. package/src/lab/conformance/harness-budget.ts +47 -0
  383. package/src/lab/conformance/index.ts +5 -0
  384. package/src/lab/conformance/jcs.ts +64 -0
  385. package/src/lab/conformance/json-pointer.ts +39 -0
  386. package/src/lab/conformance/manifest.ts +180 -0
  387. package/src/lab/conformance/mcp-stub.ts +179 -0
  388. package/src/lab/conformance/negative-controls.ts +164 -0
  389. package/src/lab/conformance/observation.ts +355 -0
  390. package/src/lab/conformance/runner.ts +68 -0
  391. package/src/lab/conformance/sse-normalize.ts +59 -0
  392. package/src/lab/conformance/suite-manifest.ts +78 -0
  393. package/src/lab/conformance/types.ts +214 -0
  394. package/src/lab/constants.ts +126 -0
  395. package/src/lab/digest.ts +64 -0
  396. package/src/lab/events/errors.ts +9 -0
  397. package/src/lab/events/limits.ts +117 -0
  398. package/src/lab/events/types.ts +229 -0
  399. package/src/lab/events/validate.ts +781 -0
  400. package/src/lab/fabric/constants.ts +40 -0
  401. package/src/lab/fabric/executor.ts +492 -0
  402. package/src/lab/fabric/index.ts +80 -0
  403. package/src/lab/fabric/manifest.ts +222 -0
  404. package/src/lab/fabric/observe.ts +489 -0
  405. package/src/lab/fabric/patch.ts +79 -0
  406. package/src/lab/fabric/producer-child.ts +139 -0
  407. package/src/lab/fabric/producer-isolate.ts +276 -0
  408. package/src/lab/fabric/producer-protocol.ts +61 -0
  409. package/src/lab/fabric/scratch.ts +439 -0
  410. package/src/lab/fabric/subject.ts +106 -0
  411. package/src/lab/fabric/types.ts +134 -0
  412. package/src/lab/fabric/verifier.ts +98 -0
  413. package/src/lab/index.ts +54 -0
  414. package/src/lab/ledger/artifact-refs.ts +127 -0
  415. package/src/lab/ledger/invalidation.ts +136 -0
  416. package/src/lab/ledger/purge.ts +310 -0
  417. package/src/lab/ledger/store.ts +532 -0
  418. package/src/lab/live/credential-lease.ts +53 -0
  419. package/src/lab/live/destination.ts +155 -0
  420. package/src/lab/live/executor.ts +336 -0
  421. package/src/lab/live/inert-tools.ts +56 -0
  422. package/src/lab/live/manifest.ts +85 -0
  423. package/src/lab/live/mcp-loopback.ts +57 -0
  424. package/src/lab/live/runner.ts +19 -0
  425. package/src/lab/live/sandbox.ts +61 -0
  426. package/src/lab/live/suite-manifest.ts +41 -0
  427. package/src/lab/live/transport.ts +118 -0
  428. package/src/lab/live/types.ts +197 -0
  429. package/src/lab/observe/from-conformance.ts +301 -0
  430. package/src/lab/observe/from-live.ts +117 -0
  431. package/src/lab/paths.ts +153 -0
  432. package/src/lab/projection/rebuild.ts +495 -0
  433. package/src/lab/projection/schema.ts +135 -0
  434. package/src/lab/projection/verdicts.ts +474 -0
  435. package/src/lab/projection/verification.ts +412 -0
  436. package/src/lab/public/bundle.ts +217 -0
  437. package/src/lab/public/community-authority.ts +175 -0
  438. package/src/lab/public/community-files.ts +29 -0
  439. package/src/lab/public/community.ts +479 -0
  440. package/src/lab/public/file-safety.ts +155 -0
  441. package/src/lab/public/ids.ts +26 -0
  442. package/src/lab/public/index.ts +16 -0
  443. package/src/lab/public/mutation-lock.ts +424 -0
  444. package/src/lab/public/operator.ts +353 -0
  445. package/src/lab/public/origin-purge.ts +79 -0
  446. package/src/lab/public/origin.ts +203 -0
  447. package/src/lab/public/privacy.ts +143 -0
  448. package/src/lab/public/private-file.ts +261 -0
  449. package/src/lab/public/project.ts +124 -0
  450. package/src/lab/public/purge-test-fault.ts +21 -0
  451. package/src/lab/public/purge.ts +223 -0
  452. package/src/lab/public/registry.ts +44 -0
  453. package/src/lab/public/revocation.ts +252 -0
  454. package/src/lab/public/signature.ts +243 -0
  455. package/src/lab/public/storage.ts +105 -0
  456. package/src/lab/public/strict-json.ts +206 -0
  457. package/src/lab/public/time.ts +26 -0
  458. package/src/lab/public/types.ts +172 -0
  459. package/src/lab/public/validate.ts +391 -0
  460. package/src/lab/query/catalog.ts +101 -0
  461. package/src/lab/query/connection.ts +107 -0
  462. package/src/lab/query/constants.ts +4 -0
  463. package/src/lab/query/cursor.ts +132 -0
  464. package/src/lab/query/dto-map.ts +277 -0
  465. package/src/lab/query/errors.ts +22 -0
  466. package/src/lab/query/freshness.ts +53 -0
  467. package/src/lab/query/index.ts +45 -0
  468. package/src/lab/query/latest-observation.ts +59 -0
  469. package/src/lab/query/passive-production.ts +159 -0
  470. package/src/lab/query/queries.ts +444 -0
  471. package/src/lab/query/types.ts +266 -0
  472. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  473. package/src/lab/subject/installation-salt.ts +112 -0
  474. package/src/lab/subject/protocol-subject.ts +80 -0
  475. package/src/lab/subject/route-subject.ts +74 -0
  476. package/src/lib/abort.ts +146 -0
  477. package/src/lib/admin-secrets.ts +25 -0
  478. package/src/lib/admission.ts +83 -0
  479. package/src/lib/app-owned-memory-stores.ts +195 -0
  480. package/src/lib/app-owned-memory.ts +265 -0
  481. package/src/lib/bounded-body.ts +346 -0
  482. package/src/lib/bun-binary-validator.d.mts +3 -0
  483. package/src/lib/bun-binary-validator.mjs +18 -0
  484. package/src/lib/bun-runtime.ts +184 -0
  485. package/src/lib/bun-stream-caps.ts +130 -0
  486. package/src/lib/codex-restart-contract.ts +120 -0
  487. package/src/lib/config-ownership.ts +364 -0
  488. package/src/lib/crash-guard.ts +344 -0
  489. package/src/lib/debug-log-buffer.ts +83 -0
  490. package/src/lib/debug-settings.ts +108 -0
  491. package/src/lib/debug.ts +31 -0
  492. package/src/lib/destination-policy.ts +380 -0
  493. package/src/lib/errors.ts +406 -0
  494. package/src/lib/eventstream-decoder.ts +253 -0
  495. package/src/lib/fabric-task-execution-authority.ts +7 -0
  496. package/src/lib/fabric-task-host.ts +29 -0
  497. package/src/lib/gcp-adc.ts +341 -0
  498. package/src/lib/injection-debug-log.ts +58 -0
  499. package/src/lib/lab-activation.ts +223 -0
  500. package/src/lib/lab-live-execution-authority.ts +13 -0
  501. package/src/lib/lab-live-host.ts +30 -0
  502. package/src/lib/lab-live-pinned-sender.ts +56 -0
  503. package/src/lib/lab-live-route-production.ts +130 -0
  504. package/src/lib/lab-passive-linker-registration.ts +26 -0
  505. package/src/lib/local-management-attestation.ts +51 -0
  506. package/src/lib/local-management-capability.ts +100 -0
  507. package/src/lib/local-provider-reload-contract.ts +100 -0
  508. package/src/lib/open-url.ts +25 -0
  509. package/src/lib/optional-shutdown-hooks.ts +57 -0
  510. package/src/lib/pinned-http.ts +270 -0
  511. package/src/lib/privacy.ts +20 -0
  512. package/src/lib/process-control.ts +168 -0
  513. package/src/lib/provider-outbound.ts +210 -0
  514. package/src/lib/provider-url.ts +14 -0
  515. package/src/lib/proxy-env.ts +18 -0
  516. package/src/lib/redact.ts +521 -0
  517. package/src/lib/retry-after.ts +55 -0
  518. package/src/lib/self-launch-argv.ts +15 -0
  519. package/src/lib/server-resource-ownership.ts +71 -0
  520. package/src/lib/service-secrets.ts +25 -0
  521. package/src/lib/shadow-call.ts +61 -0
  522. package/src/lib/sidecar-tracker.ts +52 -0
  523. package/src/lib/sse-decoder.ts +364 -0
  524. package/src/lib/state-store-registrations.ts +119 -0
  525. package/src/lib/state-store-sweeper.ts +184 -0
  526. package/src/lib/system-restart-contract.ts +73 -0
  527. package/src/lib/test-home-guard.ts +90 -0
  528. package/src/lib/token-estimate.ts +86 -0
  529. package/src/lib/tool-argument-integers.ts +202 -0
  530. package/src/lib/translator-budget.ts +400 -0
  531. package/src/lib/upstream-http-version.ts +57 -0
  532. package/src/lib/upstream-reachability.ts +95 -0
  533. package/src/lib/upstream-retry.ts +392 -0
  534. package/src/lib/win-exec.ts +115 -0
  535. package/src/lib/win-paths.ts +68 -0
  536. package/src/lib/windows-atomic-replace.ts +156 -0
  537. package/src/lib/windows-elevation.ts +773 -0
  538. package/src/lib/windows-secret-acl.ts +854 -0
  539. package/src/lib/windows-service-wrappers.ts +72 -0
  540. package/src/lib/windows-text.ts +106 -0
  541. package/src/lib/windows-user-principal.ts +341 -0
  542. package/src/lib/winsw.ts +403 -0
  543. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  544. package/src/oauth/account-import/index.ts +15 -0
  545. package/src/oauth/account-import/parser.ts +83 -0
  546. package/src/oauth/account-import/registry.ts +18 -0
  547. package/src/oauth/account-import/service.ts +75 -0
  548. package/src/oauth/account-import/types.ts +91 -0
  549. package/src/oauth/anthropic-routing.ts +594 -0
  550. package/src/oauth/anthropic.ts +188 -0
  551. package/src/oauth/antigravity-routing.ts +151 -0
  552. package/src/oauth/callback-server.ts +300 -0
  553. package/src/oauth/chatgpt.ts +161 -0
  554. package/src/oauth/command-code.ts +239 -0
  555. package/src/oauth/cursor.ts +252 -0
  556. package/src/oauth/github-copilot.ts +428 -0
  557. package/src/oauth/google-antigravity.ts +262 -0
  558. package/src/oauth/health.ts +407 -0
  559. package/src/oauth/index.ts +1504 -0
  560. package/src/oauth/key-providers.ts +124 -0
  561. package/src/oauth/kimi.ts +227 -0
  562. package/src/oauth/kiro-credentials.ts +726 -0
  563. package/src/oauth/kiro.ts +621 -0
  564. package/src/oauth/local-token-detect.ts +130 -0
  565. package/src/oauth/log.ts +50 -0
  566. package/src/oauth/login-cli.ts +223 -0
  567. package/src/oauth/nous.ts +798 -0
  568. package/src/oauth/pkce.ts +15 -0
  569. package/src/oauth/store.ts +728 -0
  570. package/src/oauth/token-guardian.ts +309 -0
  571. package/src/oauth/types.ts +62 -0
  572. package/src/oauth/xai.ts +241 -0
  573. package/src/providers/alibaba-region-backup.ts +75 -0
  574. package/src/providers/alibaba-region-migration.ts +156 -0
  575. package/src/providers/alibaba-region-startup.ts +36 -0
  576. package/src/providers/antigravity-models.ts +695 -0
  577. package/src/providers/antigravity-quota.ts +216 -0
  578. package/src/providers/api-keys.ts +140 -0
  579. package/src/providers/base-url-choices.ts +74 -0
  580. package/src/providers/codex-capacity.ts +292 -0
  581. package/src/providers/command-code-efforts.ts +144 -0
  582. package/src/providers/context-cap.ts +82 -0
  583. package/src/providers/cursor-pool.ts +72 -0
  584. package/src/providers/derive.ts +586 -0
  585. package/src/providers/fastwire.ts +501 -0
  586. package/src/providers/free-directory.ts +187 -0
  587. package/src/providers/github-copilot-transport.ts +56 -0
  588. package/src/providers/google-vertex-location.ts +14 -0
  589. package/src/providers/key-failover.ts +271 -0
  590. package/src/providers/kiro-models.ts +67 -0
  591. package/src/providers/label.ts +19 -0
  592. package/src/providers/model-discovery-limits.ts +16 -0
  593. package/src/providers/model-discovery.ts +449 -0
  594. package/src/providers/model-rename-migration.ts +255 -0
  595. package/src/providers/model-rename-startup.ts +28 -0
  596. package/src/providers/openai-sidecar.ts +243 -0
  597. package/src/providers/openai-tier-startup.ts +56 -0
  598. package/src/providers/openai-tiers.ts +423 -0
  599. package/src/providers/openai-virtual-models.ts +83 -0
  600. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  601. package/src/providers/openrouter-routing.ts +102 -0
  602. package/src/providers/provider-id-rewrite.ts +185 -0
  603. package/src/providers/quota.ts +2345 -0
  604. package/src/providers/registry.ts +2918 -0
  605. package/src/providers/replit/constants.ts +27 -0
  606. package/src/providers/replit/derive.ts +85 -0
  607. package/src/providers/replit/headers.ts +28 -0
  608. package/src/providers/replit/origin.ts +55 -0
  609. package/src/providers/replit/pair-install-response.ts +72 -0
  610. package/src/providers/replit/probe.ts +199 -0
  611. package/src/providers/replit/setup.ts +350 -0
  612. package/src/providers/request-pacing.ts +310 -0
  613. package/src/providers/service-tier.ts +277 -0
  614. package/src/providers/slug-codec.ts +103 -0
  615. package/src/providers/static-model-discovery.ts +86 -0
  616. package/src/providers/xai-responses-opt-in.ts +15 -0
  617. package/src/providers/xai-transport.ts +148 -0
  618. package/src/reasoning-effort.ts +183 -0
  619. package/src/responses/compaction.ts +142 -0
  620. package/src/responses/custom-tool-compat.ts +266 -0
  621. package/src/responses/hosted-tool-policy.ts +9 -0
  622. package/src/responses/namespace-tool-compat.ts +355 -0
  623. package/src/responses/parser.ts +838 -0
  624. package/src/responses/provider-continuation.ts +98 -0
  625. package/src/responses/provider-opaque-metadata.ts +73 -0
  626. package/src/responses/reasoning-envelope.ts +60 -0
  627. package/src/responses/reasoning-replay-cache.ts +426 -0
  628. package/src/responses/schema.ts +165 -0
  629. package/src/responses/spill-store.ts +459 -0
  630. package/src/responses/state.ts +1433 -0
  631. package/src/responses/thought-signature-replay.ts +347 -0
  632. package/src/responses/tool-groups.ts +19 -0
  633. package/src/responses/tool-search-compat.ts +301 -0
  634. package/src/responses/truncated-stop-reason.ts +60 -0
  635. package/src/router.ts +761 -0
  636. package/src/routing/analytics.ts +378 -0
  637. package/src/routing/capability.ts +244 -0
  638. package/src/routing/compatibility/assemble.ts +73 -0
  639. package/src/routing/compatibility/behavior.ts +278 -0
  640. package/src/routing/compatibility/catalog.ts +99 -0
  641. package/src/routing/compatibility/endpoint.ts +52 -0
  642. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  643. package/src/routing/compatibility/policy.ts +181 -0
  644. package/src/routing/compatibility/provider-slot.ts +56 -0
  645. package/src/routing/compatibility/reader.ts +110 -0
  646. package/src/routing/compatibility/subject.ts +191 -0
  647. package/src/routing/compatibility/types.ts +64 -0
  648. package/src/routing/compatibility/version.ts +104 -0
  649. package/src/routing/cost.ts +77 -0
  650. package/src/routing/evaluator.ts +495 -0
  651. package/src/routing/health.ts +412 -0
  652. package/src/routing/history/cursor.ts +43 -0
  653. package/src/routing/history/indexer.ts +605 -0
  654. package/src/routing/history/schema.ts +72 -0
  655. package/src/routing/profile-namespace.ts +15 -0
  656. package/src/routing/profile.ts +547 -0
  657. package/src/routing/quota.ts +145 -0
  658. package/src/routing/request-evidence.ts +45 -0
  659. package/src/routing/trace.ts +776 -0
  660. package/src/server/adapter-resolve.ts +53 -0
  661. package/src/server/auth-cors.ts +751 -0
  662. package/src/server/background-lifecycle.ts +182 -0
  663. package/src/server/chat-completions.ts +442 -0
  664. package/src/server/chat-native-sse.ts +331 -0
  665. package/src/server/chat-native.ts +426 -0
  666. package/src/server/claude-messages.ts +1030 -0
  667. package/src/server/direct-local-http.ts +347 -0
  668. package/src/server/effort-policy.ts +190 -0
  669. package/src/server/github-copilot-responses-repair.ts +338 -0
  670. package/src/server/gui-static.ts +152 -0
  671. package/src/server/image-retry.ts +42 -0
  672. package/src/server/images.ts +568 -0
  673. package/src/server/index.ts +1813 -0
  674. package/src/server/lifecycle.ts +498 -0
  675. package/src/server/live.ts +717 -0
  676. package/src/server/local-management-read-client.ts +90 -0
  677. package/src/server/local-provider-reload-client.ts +137 -0
  678. package/src/server/management/agent-settings-routes.ts +1433 -0
  679. package/src/server/management/api-access.ts +141 -0
  680. package/src/server/management/api-key-usage.ts +193 -0
  681. package/src/server/management/body.ts +41 -0
  682. package/src/server/management/combo-routes.ts +263 -0
  683. package/src/server/management/config-routes.ts +835 -0
  684. package/src/server/management/context.ts +113 -0
  685. package/src/server/management/integration-routes.ts +498 -0
  686. package/src/server/management/lab-automation-routes.ts +206 -0
  687. package/src/server/management/lab-routes.ts +563 -0
  688. package/src/server/management/logs-usage-routes.ts +586 -0
  689. package/src/server/management/model-routes.ts +560 -0
  690. package/src/server/management/model-rows.ts +163 -0
  691. package/src/server/management/native-integration-routes.ts +769 -0
  692. package/src/server/management/oauth-account-routes.ts +637 -0
  693. package/src/server/management/provider-capability-config.ts +48 -0
  694. package/src/server/management/provider-routes.ts +1033 -0
  695. package/src/server/management/replit-provider-routes.ts +86 -0
  696. package/src/server/management/request-history-routes.ts +191 -0
  697. package/src/server/management/routing-analytics-routes.ts +74 -0
  698. package/src/server/management/routing-profile-routes.ts +385 -0
  699. package/src/server/management/shared.ts +286 -0
  700. package/src/server/management/sidebar-routes.ts +106 -0
  701. package/src/server/management/storage-log-guard-routes.ts +186 -0
  702. package/src/server/management/sync-response.ts +69 -0
  703. package/src/server/management/system-restart.ts +435 -0
  704. package/src/server/management/system-routes.ts +194 -0
  705. package/src/server/management/usage-summary-cache.ts +94 -0
  706. package/src/server/management/vision-sidecar-options.ts +167 -0
  707. package/src/server/management/web-search-sidecar-options.ts +120 -0
  708. package/src/server/management-api.ts +314 -0
  709. package/src/server/management-auth.ts +482 -0
  710. package/src/server/memory-watchdog.ts +156 -0
  711. package/src/server/passive-route-linker.ts +66 -0
  712. package/src/server/port-reclaim.ts +307 -0
  713. package/src/server/ports.ts +156 -0
  714. package/src/server/proxy-liveness.ts +328 -0
  715. package/src/server/readiness.ts +99 -0
  716. package/src/server/relay-eager.ts +353 -0
  717. package/src/server/relay.ts +1209 -0
  718. package/src/server/request-decompress.ts +239 -0
  719. package/src/server/request-log-conversation.ts +168 -0
  720. package/src/server/request-log.ts +1259 -0
  721. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  722. package/src/server/responses/agent-task-recovery.ts +465 -0
  723. package/src/server/responses/collaboration.ts +551 -0
  724. package/src/server/responses/compact.ts +771 -0
  725. package/src/server/responses/core.ts +5389 -0
  726. package/src/server/responses/empty-completion-guard.ts +276 -0
  727. package/src/server/responses/encrypted-payload.ts +331 -0
  728. package/src/server/responses/fetch-helpers.ts +232 -0
  729. package/src/server/responses/input-admission.ts +185 -0
  730. package/src/server/responses/pacing-overload.ts +13 -0
  731. package/src/server/responses/passthrough-error.ts +78 -0
  732. package/src/server/responses/policy-fallback.ts +178 -0
  733. package/src/server/responses/responses-field-backfill.ts +251 -0
  734. package/src/server/responses/terminal-guard.ts +251 -0
  735. package/src/server/responses/upstream-error.ts +53 -0
  736. package/src/server/responses/ws-upstream.ts +308 -0
  737. package/src/server/responses-custom-tool-repair.ts +282 -0
  738. package/src/server/responses-image-gen-repair.ts +132 -0
  739. package/src/server/responses-item-id-repair.ts +272 -0
  740. package/src/server/responses-json-events.ts +90 -0
  741. package/src/server/responses-model-rewrite.ts +29 -0
  742. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  743. package/src/server/responses-snapshot-repair.ts +621 -0
  744. package/src/server/responses-terminal-repair.ts +342 -0
  745. package/src/server/responses-tool-search-repair.ts +267 -0
  746. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  747. package/src/server/responses.ts +25 -0
  748. package/src/server/search.ts +201 -0
  749. package/src/server/sse-frame-buffer.ts +292 -0
  750. package/src/server/sse-payload-rewrite.ts +263 -0
  751. package/src/server/startup-action-control.ts +315 -0
  752. package/src/server/startup-health-cache.ts +131 -0
  753. package/src/server/system-env.ts +484 -0
  754. package/src/server/windows-tcp-drop.ts +184 -0
  755. package/src/server/windows-tray-control.ts +41 -0
  756. package/src/server/ws-bridge.ts +472 -0
  757. package/src/service-manager-probe.ts +892 -0
  758. package/src/service.ts +3575 -0
  759. package/src/sidecar/auth.ts +92 -0
  760. package/src/sidecar/candidates.ts +83 -0
  761. package/src/stall-timeout.ts +20 -0
  762. package/src/storage/cleanup-job.ts +57 -0
  763. package/src/storage/cleanup.ts +3085 -0
  764. package/src/storage/policy-job.ts +457 -0
  765. package/src/storage/policy-scheduler.ts +40 -0
  766. package/src/storage/policy-worker.ts +59 -0
  767. package/src/storage/policy.ts +527 -0
  768. package/src/storage/restore-job.ts +299 -0
  769. package/src/storage/restore-worker.ts +58 -0
  770. package/src/storage/scanner.ts +238 -0
  771. package/src/storage/storage-mutation-coordinator.ts +139 -0
  772. package/src/storage/worker-lifecycle.ts +215 -0
  773. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  774. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  775. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  776. package/src/tray/assets/opencodex-tray.png +0 -0
  777. package/src/tray/windows-tray.ps1 +364 -0
  778. package/src/tray/windows.ts +757 -0
  779. package/src/types/accounts.ts +37 -0
  780. package/src/types/config.ts +876 -0
  781. package/src/types/provider.ts +545 -0
  782. package/src/types/request.ts +384 -0
  783. package/src/types/tools.ts +131 -0
  784. package/src/types/wire.ts +80 -0
  785. package/src/types.ts +106 -0
  786. package/src/update/badge.ts +72 -0
  787. package/src/update/index.ts +415 -0
  788. package/src/update/job.ts +1887 -0
  789. package/src/update/notify.ts +263 -0
  790. package/src/update/npm-cache-preflight.d.mts +47 -0
  791. package/src/update/npm-cache-preflight.mjs +201 -0
  792. package/src/update/npm-invocation.d.mts +23 -0
  793. package/src/update/npm-invocation.mjs +94 -0
  794. package/src/update/transactional-install.d.mts +22 -0
  795. package/src/update/transactional-install.mjs +259 -0
  796. package/src/update/tray-update-plan.d.mts +18 -0
  797. package/src/update/tray-update-plan.mjs +38 -0
  798. package/src/usage/cost.ts +625 -0
  799. package/src/usage/debug.ts +97 -0
  800. package/src/usage/expected-prices.ts +416 -0
  801. package/src/usage/log.ts +1223 -0
  802. package/src/usage/summary.ts +753 -0
  803. package/src/usage/totals.ts +14 -0
  804. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  805. package/src/usage/user-cost-overlays.ts +314 -0
  806. package/src/vision/anthropic-describe.ts +189 -0
  807. package/src/vision/backends.ts +97 -0
  808. package/src/vision/describe.ts +131 -0
  809. package/src/vision/eligibility.ts +250 -0
  810. package/src/vision/index.ts +681 -0
  811. package/src/vision/reasoning.ts +55 -0
  812. package/src/vision/routed-describe.ts +175 -0
  813. package/src/vision/timeout-bounds.ts +9 -0
  814. package/src/web-search/anthropic-executor.ts +195 -0
  815. package/src/web-search/backends.ts +108 -0
  816. package/src/web-search/exa-executor.ts +88 -0
  817. package/src/web-search/executor.ts +113 -0
  818. package/src/web-search/format-result.ts +89 -0
  819. package/src/web-search/gemini-executor.ts +141 -0
  820. package/src/web-search/index.ts +331 -0
  821. package/src/web-search/loop.ts +896 -0
  822. package/src/web-search/parse.ts +315 -0
  823. package/src/web-search/progress-stream.ts +342 -0
  824. package/src/web-search/sources.ts +60 -0
  825. package/src/web-search/synthetic-tool.ts +47 -0
  826. package/src/web-search/xai-executor.ts +219 -0
@@ -0,0 +1,1143 @@
1
+ /**
2
+ * Detect / optionally terminate long-lived Codex app-server processes that keep an
3
+ * in-memory model catalog after `ocx sync` rewrites on-disk files (#476).
4
+ *
5
+ * Matching is intentionally narrow: require `app-server` as the Codex subcommand
6
+ * (not merely as a substring in some later argument) or `codex-code-mode-host`.
7
+ * Never match broad `*codex*` patterns that hit unrelated tools such as
8
+ * `hermes-codex-bridge-mcp`.
9
+ */
10
+ import { execFile, execFileSync, type ExecFileException } from "node:child_process";
11
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
12
+ import { isProcessAlive, waitForExit } from "../lib/process-control";
13
+ import {
14
+ resolveTrustedWindowsPowerShellExe,
15
+ resolveTrustedWindowsTaskkillExe,
16
+ } from "../lib/windows-elevation";
17
+ import { readCodexCatalogPath } from "./catalog/parsing";
18
+
19
+ export const STALE_CODEX_APP_SERVER_HINT =
20
+ "If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex).";
21
+
22
+ /** Attach the shared dashboard hint only after a catalog or models_cache write. */
23
+ export function attachStaleAppServerHint<T extends {
24
+ catalogWritten: boolean;
25
+ cacheSynced: boolean;
26
+ }>(result: T): T & { staleAppServerHint?: string } {
27
+ if (result.catalogWritten || result.cacheSynced) {
28
+ return { ...result, staleAppServerHint: STALE_CODEX_APP_SERVER_HINT };
29
+ }
30
+ return { ...result };
31
+ }
32
+ /**
33
+ * Rust-style target-triple body on official platform-baked Codex binaries
34
+ * (e.g. `x86_64-unknown-linux-musl`, `aarch64-apple-darwin`,
35
+ * `x86_64-pc-windows-msvc`). Requires arch-vendor-os with an optional env
36
+ * segment — not a broad `codex-*` wildcard.
37
+ */
38
+ const CODEX_TARGET_TRIPLE_BODY = "[a-z0-9_]+-[a-z0-9_]+-[a-z0-9_]+(?:-[a-z0-9_]+)?";
39
+
40
+ /**
41
+ * Narrow Win32_Process CommandLine pre-filter (JS + .NET compatible).
42
+ * Allows an optional closing quote after the executable basename so paths like
43
+ * `"C:\Program Files\...\codex.exe" app-server` still reach GetOwner.
44
+ * Also admits official target-triple basenames such as
45
+ * `codex-x86_64-pc-windows-msvc.exe`.
46
+ */
47
+ export const WINDOWS_CODEX_BASENAME_CANDIDATE_RE = new RegExp(
48
+ `(^|[/\\\\\\s'"=])codex(-${CODEX_TARGET_TRIPLE_BODY})?([.]exe|[.]cmd)?['"]?(\\s|$)`,
49
+ "i",
50
+ );
51
+
52
+ export const WINDOWS_CODEX_CODE_MODE_HOST_CANDIDATE_RE = /codex-code-mode-host/i;
53
+
54
+ /** Basename of an official Codex release binary (plain or target-triple). */
55
+ const CODEX_TARGET_TRIPLE_BASENAME_RE = new RegExp(
56
+ `^codex-${CODEX_TARGET_TRIPLE_BODY}(?:\\.exe|\\.cmd)?$`,
57
+ );
58
+
59
+ /** True when a Windows CommandLine is worth paying GetOwner for (current-user scoped later). */
60
+ export function isWindowsCodexCandidateCommandLine(commandLine: string): boolean {
61
+ return WINDOWS_CODEX_BASENAME_CANDIDATE_RE.test(commandLine)
62
+ || WINDOWS_CODEX_CODE_MODE_HOST_CANDIDATE_RE.test(commandLine);
63
+ }
64
+
65
+ /** Embed a regex source in a PowerShell single-quoted -match operand (`''` escapes `'`). */
66
+ function powerShellSingleQuotedIgnoreCaseMatch(patternSource: string): string {
67
+ return `'(?i)${patternSource.replace(/'/g, "''")}'`;
68
+ }
69
+
70
+ export interface CodexAppServerProcess {
71
+ pid: number;
72
+ commandLine: string;
73
+ }
74
+
75
+ export interface ProcessSnapshot {
76
+ pid: number;
77
+ commandLine: string;
78
+ executable?: string;
79
+ uid?: number;
80
+ owner?: string;
81
+ startedAtMs?: number;
82
+ }
83
+
84
+ export interface CodexAppServerProcessIo {
85
+ platform?: NodeJS.Platform;
86
+ getuid?: () => number | undefined;
87
+ listSnapshots?: () => ProcessSnapshot[];
88
+ isAlive?: (pid: number) => boolean;
89
+ kill?: (pid: number, signal: NodeJS.Signals) => void;
90
+ /** Windows termination seam: drives the taskkill branch without a real exec. */
91
+ execFile?: (file: string, args: readonly string[]) => void;
92
+ /**
93
+ * Signal seam for the Unix branch and the taskkill fallback. Without it,
94
+ * injecting `kill` bypasses defaultKillCodexAppServer entirely, so the
95
+ * fallback could never be observed.
96
+ */
97
+ processKill?: (pid: number, signal: NodeJS.Signals) => void;
98
+ waitExit?: (pid: number, timeoutMs: number) => boolean;
99
+ now?: () => number;
100
+ readStartMs?: (pid: number) => number | null;
101
+ /** Async process-list seam used by the request-path Windows collector. */
102
+ listSnapshotsAsync?: () => Promise<ProcessSnapshot[]>;
103
+ /** Async batch start-time seam used by the request-path Windows collector. */
104
+ readStartMsBatchAsync?: (pids: readonly number[]) => Promise<Map<number, number | null>>;
105
+ catalogMtimeMs?: () => number | null;
106
+ }
107
+
108
+ function execFileTextAsync(
109
+ file: string,
110
+ args: readonly string[],
111
+ timeoutMs: number,
112
+ ): Promise<string> {
113
+ return new Promise((resolve, reject) => {
114
+ execFile(file, [...args], {
115
+ encoding: "utf-8",
116
+ timeout: timeoutMs,
117
+ windowsHide: true,
118
+ }, (error: ExecFileException | null, stdout: string) => {
119
+ if (error) reject(error);
120
+ else resolve(stdout);
121
+ });
122
+ });
123
+ }
124
+
125
+ /** Split a process command line into argv-like tokens (handles simple quotes). */
126
+ export function tokenizeCommandLine(commandLine: string): string[] {
127
+ const tokens: string[] = [];
128
+ let current = "";
129
+ let quote: '"' | "'" | null = null;
130
+ for (let i = 0; i < commandLine.length; i++) {
131
+ const ch = commandLine[i]!;
132
+ if (quote) {
133
+ if (ch === quote) quote = null;
134
+ else current += ch;
135
+ continue;
136
+ }
137
+ if (ch === '"' || ch === "'") {
138
+ quote = ch;
139
+ continue;
140
+ }
141
+ if (/\s/.test(ch)) {
142
+ if (current) {
143
+ tokens.push(current);
144
+ current = "";
145
+ }
146
+ continue;
147
+ }
148
+ current += ch;
149
+ }
150
+ if (current) tokens.push(current);
151
+ return tokens;
152
+ }
153
+
154
+ function tokenBasename(token: string): string {
155
+ return token.toLowerCase().replace(/\\/g, "/").split("/").pop() ?? "";
156
+ }
157
+
158
+ function isCodexExecutableToken(token: string): boolean {
159
+ const base = tokenBasename(token);
160
+ return base === "codex" || base === "codex.exe" || base === "codex.cmd"
161
+ || CODEX_TARGET_TRIPLE_BASENAME_RE.test(base);
162
+ }
163
+
164
+ function isCodeModeHostToken(token: string): boolean {
165
+ const base = tokenBasename(token);
166
+ return base === "codex-code-mode-host" || base === "codex-code-mode-host.exe";
167
+ }
168
+
169
+ function isInterpreterToken(token: string): boolean {
170
+ const base = tokenBasename(token);
171
+ return base === "node" || base === "node.exe"
172
+ || base === "bun" || base === "bun.exe"
173
+ || base === "deno" || base === "deno.exe";
174
+ }
175
+
176
+ /**
177
+ * Codex global options that take a following value when written without `=`.
178
+ * Keep this list explicit so unknown flags stay boolean (narrow matching).
179
+ */
180
+ const CODEX_GLOBAL_OPTIONS_WITH_VALUE = new Set([
181
+ "--enable",
182
+ "--disable",
183
+ "--config",
184
+ "-c",
185
+ "--profile",
186
+ "-p",
187
+ "--model",
188
+ "-m",
189
+ "--sandbox",
190
+ "-s",
191
+ "--ask-for-approval",
192
+ "-a",
193
+ "--local-provider",
194
+ "--add-dir",
195
+ "--cd",
196
+ "-C",
197
+ "--color",
198
+ "--image",
199
+ "-i",
200
+ "--output-schema",
201
+ "--output-last-message",
202
+ "-o",
203
+ ]);
204
+
205
+ /** Parse a CLI option token into its flag name and whether a value is inline (`--opt=value`). */
206
+ function splitCliOptionToken(token: string): { name: string; hasInlineValue: boolean } | null {
207
+ if (!token.startsWith("-") || token === "-" || token === "--") return null;
208
+ if (token.startsWith("--")) {
209
+ const eq = token.indexOf("=");
210
+ if (eq >= 0) return { name: token.slice(0, eq).toLowerCase(), hasInlineValue: true };
211
+ return { name: token.toLowerCase(), hasInlineValue: false };
212
+ }
213
+ // Preserve short-option case: `-c` (config) vs `-C` (cd).
214
+ const eq = token.indexOf("=");
215
+ if (eq >= 0) return { name: token.slice(0, eq), hasInlineValue: true };
216
+ return { name: token, hasInlineValue: false };
217
+ }
218
+
219
+ /** Advance past one argv token, consuming a value for known Codex global options. */
220
+ function advancePastCodexGlobalOption(tokens: readonly string[], index: number): number {
221
+ const option = splitCliOptionToken(tokens[index]!);
222
+ if (!option) return index + 1;
223
+ let next = index + 1;
224
+ if (
225
+ !option.hasInlineValue
226
+ && CODEX_GLOBAL_OPTIONS_WITH_VALUE.has(option.name)
227
+ && next < tokens.length
228
+ && !tokens[next]!.startsWith("-")
229
+ ) {
230
+ next += 1; // skip the option value
231
+ }
232
+ return next;
233
+ }
234
+
235
+ /** True when code-mode-host is the process executable or interpreter entrypoint, not a later arg. */
236
+ function isCodeModeHostProcess(tokens: readonly string[]): boolean {
237
+ if (tokens.length === 0) return false;
238
+ if (isCodeModeHostToken(tokens[0]!)) return true;
239
+ return isInterpreterToken(tokens[0]!) && tokens.length > 1 && isCodeModeHostToken(tokens[1]!);
240
+ }
241
+
242
+ /** Stable identity for PID reuse checks: pid + normalized command line. */
243
+ export function codexAppServerProcessIdentity(proc: Pick<CodexAppServerProcess, "pid" | "commandLine">): string {
244
+ return `${proc.pid}\0${proc.commandLine.trim().replace(/\s+/g, " ")}`;
245
+ }
246
+
247
+ /** True when the command line is a Codex app-server (or code-mode host) worth restarting. */
248
+ export function isCodexAppServerCommandLine(commandLine: string, executable?: string): boolean {
249
+ const trimmed = commandLine.trim();
250
+ let tokens = tokenizeCommandLine(trimmed);
251
+ if (executable) {
252
+ if (isCodeModeHostToken(executable)) return true;
253
+ if (isCodexExecutableToken(executable)) {
254
+ let remainder = trimmed;
255
+ if (remainder.startsWith(executable)) remainder = remainder.slice(executable.length).trimStart();
256
+ else if (remainder.startsWith(`"${executable}"`)) remainder = remainder.slice(executable.length + 2).trimStart();
257
+ tokens = [executable, ...tokenizeCommandLine(remainder)];
258
+ }
259
+ }
260
+ if (tokens.length === 0) return false;
261
+ if (isCodeModeHostProcess(tokens)) return true;
262
+ if (!isCodexExecutableToken(tokens[0]!)) return false;
263
+
264
+ let i = 1;
265
+ while (i < tokens.length) {
266
+ const token = tokens[i]!;
267
+ if (token.startsWith("-")) {
268
+ i = advancePastCodexGlobalOption(tokens, i);
269
+ continue;
270
+ }
271
+ return token.toLowerCase() === "app-server";
272
+ }
273
+ return false;
274
+ }
275
+
276
+ function parseUnixProcStatusUid(status: string): number | undefined {
277
+ const match = /^Uid:\s+(\d+)/m.exec(status);
278
+ if (!match) return undefined;
279
+ const uid = Number(match[1]);
280
+ return Number.isSafeInteger(uid) ? uid : undefined;
281
+ }
282
+
283
+ function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
284
+ // procfs missing on a Linux-shaped platform is an enumeration failure, not
285
+ // "no processes" — the staleness collector must not read it as not_running.
286
+ if (!existsSync("/proc")) throw new Error("procfs_unavailable");
287
+ const out: ProcessSnapshot[] = [];
288
+ for (const ent of readdirSync("/proc")) {
289
+ if (!/^\d+$/.test(ent)) continue;
290
+ const pid = Number(ent);
291
+ if (!Number.isSafeInteger(pid) || pid <= 1) continue;
292
+ try {
293
+ const status = readFileSync(`/proc/${pid}/status`, "utf8");
294
+ const processUid = parseUnixProcStatusUid(status);
295
+ if (uid !== undefined && processUid !== undefined && processUid !== uid) continue;
296
+ const argv = readFileSync(`/proc/${pid}/cmdline`)
297
+ .toString("utf8")
298
+ .split("\0")
299
+ .filter(Boolean);
300
+ const commandLine = argv.join(" ").trim();
301
+ if (!commandLine) continue;
302
+ out.push({ pid, commandLine, executable: argv[0], uid: processUid });
303
+ } catch {
304
+ /* process exited mid-scan */
305
+ }
306
+ }
307
+ return out;
308
+ }
309
+
310
+ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
311
+ const out: ProcessSnapshot[] = [];
312
+ const commandOutput = uid !== undefined
313
+ ? execFileSync("/bin/ps", ["-u", String(uid), "-o", "pid=,command="], {
314
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
315
+ })
316
+ : execFileSync("/bin/ps", ["-axo", "pid=,uid=,command="], {
317
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
318
+ });
319
+ const executableOutput = uid !== undefined
320
+ ? execFileSync("/bin/ps", ["-u", String(uid), "-o", "pid=,comm="], {
321
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
322
+ })
323
+ : execFileSync("/bin/ps", ["-axo", "pid=,comm="], {
324
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
325
+ });
326
+ const executableByPid = new Map<number, string>();
327
+ for (const raw of executableOutput.split(/\r?\n/)) {
328
+ const match = /^\s*(\d+)\s+(.+)$/.exec(raw);
329
+ if (!match) continue;
330
+ const pid = Number(match[1]);
331
+ const executable = match[2]?.trim() ?? "";
332
+ if (Number.isSafeInteger(pid) && pid > 0 && executable) executableByPid.set(pid, executable);
333
+ }
334
+ for (const raw of commandOutput.split(/\r?\n/)) {
335
+ const line = raw.trim();
336
+ if (!line) continue;
337
+ if (uid !== undefined) {
338
+ const match = /^(\d+)\s+(.*)$/.exec(line);
339
+ if (!match) continue;
340
+ const pid = Number(match[1]);
341
+ const commandLine = match[2]?.trim() ?? "";
342
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !commandLine) continue;
343
+ out.push({ pid, commandLine, executable: executableByPid.get(pid), uid });
344
+ continue;
345
+ }
346
+ const match = /^(\d+)\s+(\d+)\s+(.*)$/.exec(line);
347
+ if (!match) continue;
348
+ const pid = Number(match[1]);
349
+ const processUid = Number(match[2]);
350
+ const commandLine = match[3]?.trim() ?? "";
351
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !commandLine) continue;
352
+ out.push({
353
+ pid, commandLine, executable: executableByPid.get(pid),
354
+ uid: Number.isSafeInteger(processUid) ? processUid : undefined,
355
+ });
356
+ }
357
+ return out;
358
+ }
359
+
360
+ /**
361
+ * Windows snapshots scoped to the invoking user via Win32_Process GetOwner.
362
+ * PowerShell is the sole path: WMIC lacks reliable owner data and is disabled on
363
+ * many Windows 11 installs; returning unscoped rows would contradict the
364
+ * current-user restart contract.
365
+ *
366
+ * CIM instance methods must use Invoke-CimMethod (direct .GetOwner() calls fail).
367
+ * Candidates are pre-filtered to Codex basename / code-mode-host command lines
368
+ * so we do not pay GetOwner per every process on the machine.
369
+ * Exported for the Windows integration regression that exercises the real
370
+ * PowerShell enumeration.
371
+ */
372
+ /**
373
+ * Turn one PowerShell enumeration's stdout into snapshots.
374
+ *
375
+ * Split out from the spawn so the failure contract is testable off-Windows: the
376
+ * sentinel path is the difference between "no Codex process is running" and "we could
377
+ * not read the process list", and only one of those is safe to act on.
378
+ */
379
+ export function parseWindowsSnapshotOutput(output: string): ProcessSnapshot[] {
380
+ const out: ProcessSnapshot[] = [];
381
+ for (const line of output.split(/\r?\n/)) {
382
+ // A candidate whose owner could not be verified — or a top-level query that
383
+ // failed outright — makes the whole enumeration incomplete. The staleness
384
+ // collector must not read the partial result as "nothing running".
385
+ if (line.trim() === "__OCX_ENUM_INCOMPLETE__") throw new Error("windows_enum_incomplete");
386
+ const tab = line.indexOf("\t");
387
+ if (tab <= 0) continue;
388
+ const tab2 = line.indexOf("\t", tab + 1);
389
+ if (tab2 <= tab) continue;
390
+ const pid = Number(line.slice(0, tab));
391
+ const commandLine = line.slice(tab + 1, tab2).trim();
392
+ const owner = line.slice(tab2 + 1).trim();
393
+ if (!Number.isSafeInteger(pid) || pid <= 1 || !commandLine || !owner) continue;
394
+ out.push({ pid, commandLine, owner });
395
+ }
396
+ return out;
397
+ }
398
+
399
+ function windowsSnapshotPowerShellCommand(): string {
400
+ // Newlines keep -Command as a real script (space-joined statements need ';').
401
+ // Double-quoted format string so `t expands to a real tab.
402
+ // Codex candidates only: basename token codex / codex.exe / codex.cmd /
403
+ // official target-triple binaries (optional closing quote after the
404
+ // basename), or code-mode-host — not incidental substrings like a repo
405
+ // path with "opencodex".
406
+ const basenameMatch = powerShellSingleQuotedIgnoreCaseMatch(WINDOWS_CODEX_BASENAME_CANDIDATE_RE.source);
407
+ const codeModeMatch = powerShellSingleQuotedIgnoreCaseMatch(WINDOWS_CODEX_CODE_MODE_HOST_CANDIDATE_RE.source);
408
+ return [
409
+ "$ErrorActionPreference='SilentlyContinue'",
410
+ "$me=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name",
411
+ // -ErrorAction Stop plus the outer try is what makes a TOP-LEVEL query failure
412
+ // observable. Under SilentlyContinue alone, a failing Get-CimInstance emits nothing
413
+ // and the enumeration is indistinguishable from "no Codex process is running" —
414
+ // the parse loop finds no rows, no sentinel is produced, and the staleness collector
415
+ // reports not_running for a machine whose process list it never actually read.
416
+ // The per-process catch below cannot cover this: it only runs once the pipeline has
417
+ // objects to iterate.
418
+ "try {",
419
+ "Get-CimInstance Win32_Process -ErrorAction Stop | Where-Object {",
420
+ " -not [string]::IsNullOrWhiteSpace($_.CommandLine) -and (",
421
+ ` $_.CommandLine -match ${basenameMatch} -or`,
422
+ ` $_.CommandLine -match ${codeModeMatch}`,
423
+ " )",
424
+ "} | ForEach-Object {",
425
+ " try {",
426
+ " $o=Invoke-CimMethod -InputObject $_ -MethodName GetOwner -ErrorAction Stop",
427
+ " if($null -eq $o -or $o.ReturnValue -ne 0 -or [string]::IsNullOrWhiteSpace($o.User)){\"__OCX_ENUM_INCOMPLETE__\"; return}",
428
+ " $owner=if($o.Domain){\"$($o.Domain)\\$($o.User)\"}else{$o.User}",
429
+ " if($owner -ine $me){return}",
430
+ " $cmd=($_.CommandLine -replace \"`t\",\" \")",
431
+ " \"{0}`t{1}`t{2}\" -f $_.ProcessId, $cmd, $owner",
432
+ " } catch { \"__OCX_ENUM_INCOMPLETE__\" }",
433
+ "}",
434
+ "} catch { \"__OCX_ENUM_INCOMPLETE__\" }",
435
+ ].join("\n");
436
+ }
437
+
438
+ export function listWindowsSnapshots(runPowerShell?: (psCommand: string) => string): ProcessSnapshot[] {
439
+ // Top-level exec failure propagates (see listDarwinSnapshots note). The
440
+ // executable resolves from the trusted System32 directory (never PATH), and
441
+ // windowsHide keeps the enumeration console-less on desktop sessions (#1278).
442
+ const psCommand = windowsSnapshotPowerShellCommand();
443
+ const output = runPowerShell
444
+ ? runPowerShell(psCommand)
445
+ : execFileSync(resolveTrustedWindowsPowerShellExe(), [
446
+ "-NoProfile", "-NoLogo", "-NonInteractive",
447
+ "-Command",
448
+ psCommand,
449
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true });
450
+ return parseWindowsSnapshotOutput(output);
451
+ }
452
+
453
+ async function listWindowsSnapshotsAsync(): Promise<ProcessSnapshot[]> {
454
+ const output = await execFileTextAsync(resolveTrustedWindowsPowerShellExe(), [
455
+ "-NoProfile", "-NoLogo", "-NonInteractive",
456
+ "-Command",
457
+ windowsSnapshotPowerShellCommand(),
458
+ ], 8_000);
459
+ return parseWindowsSnapshotOutput(output);
460
+ }
461
+
462
+ function defaultListSnapshots(platform: NodeJS.Platform, getuid: () => number | undefined): ProcessSnapshot[] {
463
+ if (platform === "win32") return listWindowsSnapshots();
464
+ if (platform === "darwin") return listDarwinSnapshots(getuid());
465
+ return listUnixProcSnapshots(getuid());
466
+ }
467
+
468
+ export function listCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): CodexAppServerProcess[] {
469
+ const platform = io.platform ?? process.platform;
470
+ const getuid = io.getuid ?? (() => {
471
+ try {
472
+ return typeof process.getuid === "function" ? process.getuid() : undefined;
473
+ } catch {
474
+ return undefined;
475
+ }
476
+ });
477
+ let snapshots: ProcessSnapshot[];
478
+ if (io.listSnapshots) {
479
+ snapshots = io.listSnapshots();
480
+ } else {
481
+ // Restart/kill contract (#476): enumeration failure means no targets —
482
+ // never signal a process we could not verify.
483
+ try {
484
+ snapshots = defaultListSnapshots(platform, getuid);
485
+ } catch {
486
+ snapshots = [];
487
+ }
488
+ }
489
+ const seen = new Set<number>();
490
+ const matched: CodexAppServerProcess[] = [];
491
+ for (const snapshot of snapshots) {
492
+ if (seen.has(snapshot.pid)) continue;
493
+ if (!isCodexAppServerCommandLine(snapshot.commandLine, snapshot.executable)) continue;
494
+ seen.add(snapshot.pid);
495
+ matched.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
496
+ }
497
+ return matched;
498
+ }
499
+
500
+ export function formatStaleCodexAppServerWarning(
501
+ processes: readonly { pid: number }[],
502
+ ): string {
503
+ const pids = processes.map(process => process.pid).join(", ");
504
+ return (
505
+ `WARNING: ${processes.length} Codex app-server process(es) still running (PID${processes.length === 1 ? "" : "s"}: ${pids}). `
506
+ + "Disk catalog/cache were updated, but Codex may keep showing the old model list until those processes restart. "
507
+ + "Re-run with `ocx sync --restart-codex` (or `ocx sync-cache --restart-codex`) to send SIGTERM only to matching app-server processes. "
508
+ + "Active turns may be interrupted."
509
+ );
510
+ }
511
+
512
+ /** /proc/<pid>/stat starttime (clock ticks since boot) → epoch ms, or null. */
513
+ function readLinuxProcStartMs(pid: number): number | null {
514
+ try {
515
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
516
+ // Field 22 (starttime) follows the comm field, which may contain spaces
517
+ // inside parentheses — split after the final ")".
518
+ const close = stat.lastIndexOf(")");
519
+ if (close < 0) return null;
520
+ const fields = stat.slice(close + 2).split(/\s+/);
521
+ const startTicks = Number(fields[19]); // field 22 = index 19 after comm
522
+ const boot = /^btime\s+(\d+)/m.exec(readFileSync("/proc/stat", "utf8"));
523
+ if (!Number.isFinite(startTicks) || !boot) return null;
524
+ const hertz = 100; // USER_HZ on every supported Linux target
525
+ return (Number(boot[1]) + startTicks / hertz) * 1000;
526
+ } catch {
527
+ return null;
528
+ }
529
+ }
530
+
531
+ /** `ps` lstart → epoch ms, or null (macOS). */
532
+ function readDarwinProcStartMs(pid: number): number | null {
533
+ try {
534
+ const out = execFileSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], {
535
+ encoding: "utf-8",
536
+ stdio: ["ignore", "pipe", "ignore"],
537
+ timeout: 4_000,
538
+ }).trim();
539
+ if (!out) return null;
540
+ const parsed = Date.parse(out);
541
+ return Number.isFinite(parsed) ? parsed : null;
542
+ } catch {
543
+ return null;
544
+ }
545
+ }
546
+
547
+ /** Win32_Process.CreationDate → epoch ms, or null (Windows). */
548
+ function readWindowsProcStartMs(pid: number): number | null {
549
+ try {
550
+ const out = execFileSync(resolveTrustedWindowsPowerShellExe(), [
551
+ "-NoProfile", "-NoLogo", "-NonInteractive",
552
+ "-Command",
553
+ `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate.ToUniversalTime().ToString("o")`,
554
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true }).trim();
555
+ if (!out) return null;
556
+ const parsed = Date.parse(out);
557
+ return Number.isFinite(parsed) ? parsed : null;
558
+ } catch {
559
+ return null;
560
+ }
561
+ }
562
+
563
+ /** Best-effort process start time; null when the platform source is unreadable. */
564
+ export function readProcessStartMs(pid: number, platform: NodeJS.Platform = process.platform): number | null {
565
+ if (platform === "win32") return readWindowsProcStartMs(pid);
566
+ if (platform === "darwin") return readDarwinProcStartMs(pid);
567
+ return readLinuxProcStartMs(pid);
568
+ }
569
+
570
+ function windowsProcessStartPowerShellCommand(pids: readonly number[]): string {
571
+ const filter = pids.map(pid => `ProcessId=${pid}`).join(" OR ");
572
+ return `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId)\t$($_.CreationDate.ToUniversalTime().ToString("o"))" }`;
573
+ }
574
+
575
+ function parseWindowsProcessStartTimes(
576
+ stdout: string,
577
+ pids: readonly number[],
578
+ ): Map<number, number | null> {
579
+ const byPid = new Map<number, number>();
580
+ for (const line of stdout.split(/\r?\n/)) {
581
+ const tab = line.indexOf("\t");
582
+ if (tab <= 0) continue;
583
+ const pid = Number(line.slice(0, tab));
584
+ const parsed = Date.parse(line.slice(tab + 1).trim());
585
+ if (Number.isSafeInteger(pid) && Number.isFinite(parsed)) byPid.set(pid, parsed);
586
+ }
587
+ return new Map(pids.map(pid => [pid, byPid.get(pid) ?? null]));
588
+ }
589
+
590
+ async function readWindowsProcessStartMsBatchAsync(
591
+ pids: readonly number[],
592
+ ): Promise<Map<number, number | null>> {
593
+ try {
594
+ const stdout = await execFileTextAsync(resolveTrustedWindowsPowerShellExe(), [
595
+ "-NoProfile", "-NoLogo", "-NonInteractive",
596
+ "-Command",
597
+ windowsProcessStartPowerShellCommand(pids),
598
+ ], 5_000);
599
+ return parseWindowsProcessStartTimes(stdout, pids);
600
+ } catch {
601
+ return new Map(pids.map(pid => [pid, null]));
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Start times for many pids in ONE platform call where possible, so the
607
+ * staleness check does not serialize per-process ps/PowerShell invocations
608
+ * on the request path (#857). Missing entries come back as null.
609
+ */
610
+ export function readProcessStartMsBatch(
611
+ pids: readonly number[],
612
+ platform: NodeJS.Platform = process.platform,
613
+ ): Map<number, number | null> {
614
+ const out = new Map<number, number | null>();
615
+ if (pids.length === 0) return out;
616
+ if (platform === "darwin") {
617
+ try {
618
+ const stdout = execFileSync("/bin/ps", ["-o", "pid=,lstart=", "-p", pids.join(",")], {
619
+ encoding: "utf-8",
620
+ stdio: ["ignore", "pipe", "ignore"],
621
+ timeout: 3_000,
622
+ });
623
+ const byPid = new Map<number, number>();
624
+ for (const raw of stdout.split(/\r?\n/)) {
625
+ const match = /^\s*(\d+)\s+(.+)$/.exec(raw);
626
+ if (!match) continue;
627
+ const pid = Number(match[1]);
628
+ const parsed = Date.parse(match[2]!.trim());
629
+ if (Number.isSafeInteger(pid) && Number.isFinite(parsed)) byPid.set(pid, parsed);
630
+ }
631
+ for (const pid of pids) out.set(pid, byPid.get(pid) ?? null);
632
+ return out;
633
+ } catch {
634
+ for (const pid of pids) out.set(pid, null);
635
+ return out;
636
+ }
637
+ }
638
+ if (platform === "win32") {
639
+ try {
640
+ const stdout = execFileSync(resolveTrustedWindowsPowerShellExe(), [
641
+ "-NoProfile", "-NoLogo", "-NonInteractive",
642
+ "-Command",
643
+ windowsProcessStartPowerShellCommand(pids),
644
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true });
645
+ return parseWindowsProcessStartTimes(stdout, pids);
646
+ } catch {
647
+ for (const pid of pids) out.set(pid, null);
648
+ return out;
649
+ }
650
+ }
651
+ for (const pid of pids) out.set(pid, readLinuxProcStartMs(pid));
652
+ return out;
653
+ }
654
+
655
+ export type CodexAppServerCatalogState = "fresh" | "stale" | "not_running" | "unknown";
656
+
657
+ export interface CodexAppServerCatalogStatus {
658
+ state: CodexAppServerCatalogState;
659
+ processes: Array<{ pid: number; startedAtMs: number | null }>;
660
+ catalogMtimeMs: number | null;
661
+ }
662
+
663
+ /** Resolve the catalog file Codex app-servers loaded at startup, for staleness checks. */
664
+ function defaultCatalogMtimeMs(): number | null {
665
+ try {
666
+ return statSync(readCodexCatalogPath()).mtimeMs;
667
+ } catch {
668
+ return null;
669
+ }
670
+ }
671
+
672
+ function codexAppServerProcessesFromSnapshots(
673
+ snapshots: readonly ProcessSnapshot[],
674
+ ): CodexAppServerProcess[] {
675
+ const processes: CodexAppServerProcess[] = [];
676
+ const seen = new Set<number>();
677
+ for (const snapshot of snapshots) {
678
+ if (seen.has(snapshot.pid)) continue;
679
+ if (!isCodexAppServerCommandLine(snapshot.commandLine, snapshot.executable)) continue;
680
+ seen.add(snapshot.pid);
681
+ processes.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
682
+ }
683
+ return processes;
684
+ }
685
+
686
+ function catalogStatusFromProcesses(
687
+ processes: readonly CodexAppServerProcess[],
688
+ catalogMtimeMs: number | null,
689
+ starts: ReadonlyMap<number, number | null>,
690
+ ): CodexAppServerCatalogStatus {
691
+ const withStarts = processes.map(proc => ({
692
+ pid: proc.pid,
693
+ startedAtMs: starts.get(proc.pid) ?? null,
694
+ }));
695
+ if (catalogMtimeMs === null || withStarts.some(proc => proc.startedAtMs === null)) {
696
+ return { state: "unknown", processes: withStarts, catalogMtimeMs };
697
+ }
698
+ // `<=` is deliberate: coarse clocks (ps lstart is second-granularity) can
699
+ // report equal values when the catalog actually changed after startup.
700
+ const stale = withStarts.some(proc => proc.startedAtMs! <= catalogMtimeMs);
701
+ return { state: stale ? "stale" : "fresh", processes: withStarts, catalogMtimeMs };
702
+ }
703
+
704
+ // Short TTL: process listing + stat run once per window even under per-turn
705
+ // guidance calls (#857).
706
+ let catalogStateCache: { atMs: number; status: CodexAppServerCatalogStatus } | null = null;
707
+ interface RequestCatalogStateIdentity {
708
+ platform: NodeJS.Platform;
709
+ listSnapshots?: CodexAppServerProcessIo["listSnapshots"];
710
+ listSnapshotsAsync?: CodexAppServerProcessIo["listSnapshotsAsync"];
711
+ readStartMs?: CodexAppServerProcessIo["readStartMs"];
712
+ readStartMsBatchAsync?: CodexAppServerProcessIo["readStartMsBatchAsync"];
713
+ catalogMtimeMs?: CodexAppServerProcessIo["catalogMtimeMs"];
714
+ now?: CodexAppServerProcessIo["now"];
715
+ }
716
+
717
+ interface RequestCatalogStateFlight {
718
+ generation: number;
719
+ identity: RequestCatalogStateIdentity;
720
+ promise: Promise<CodexAppServerCatalogStatus>;
721
+ }
722
+
723
+ let requestCatalogStateGeneration = 0;
724
+ let requestCatalogStateCache: {
725
+ generation: number;
726
+ identity: RequestCatalogStateIdentity;
727
+ atMs: number;
728
+ status: CodexAppServerCatalogStatus;
729
+ } | null = null;
730
+ let requestCatalogStateFlight: RequestCatalogStateFlight | null = null;
731
+ const CATALOG_STATE_TTL_MS = 5_000;
732
+ /**
733
+ * `unknown` is a failure to observe, not an observation, so it gets a much shorter
734
+ * window than a real reading. At the full 5s a single transient enumeration failure
735
+ * suppresses guidance for every call in that window, and the retry that would have
736
+ * succeeded never runs. Keeping a brief window still collapses a burst of per-turn
737
+ * calls into one probe, which is what the cache is for.
738
+ */
739
+ const CATALOG_STATE_UNKNOWN_TTL_MS = 250;
740
+
741
+ export function catalogStateTtlMs(state: CodexAppServerCatalogState): number {
742
+ return state === "unknown" ? CATALOG_STATE_UNKNOWN_TTL_MS : CATALOG_STATE_TTL_MS;
743
+ }
744
+
745
+ function sameRequestCatalogStateIdentity(
746
+ left: RequestCatalogStateIdentity,
747
+ right: RequestCatalogStateIdentity,
748
+ ): boolean {
749
+ return left.platform === right.platform
750
+ && left.listSnapshots === right.listSnapshots
751
+ && left.listSnapshotsAsync === right.listSnapshotsAsync
752
+ && left.readStartMs === right.readStartMs
753
+ && left.readStartMsBatchAsync === right.readStartMsBatchAsync
754
+ && left.catalogMtimeMs === right.catalogMtimeMs
755
+ && left.now === right.now;
756
+ }
757
+
758
+ /**
759
+ * Compare the on-disk catalog mtime against the start time of running Codex
760
+ * app-servers (#857): a server that started before the catalog changed keeps
761
+ * an in-memory copy that disagrees with what ocx advertises.
762
+ *
763
+ * Cost note: a cold call synchronously runs the platform listing plus ONE
764
+ * batched start-time query (hard bounds: ~5s+3s macOS, ~8s+5s Windows,
765
+ * microseconds on Linux); the 5s TTL then serves repeats. Typical cold cost
766
+ * is tens of milliseconds; fully-async background refresh is deliberately
767
+ * out of scope for this slice.
768
+ *
769
+ * - not_running: no app-server process → nothing can disagree.
770
+ * - unknown: catalog unreadable, or any server's start time is unreadable —
771
+ * callers must treat this conservatively (suppress positive model claims).
772
+ * - stale: at least one server predates the catalog mtime.
773
+ */
774
+ export function collectCodexAppServerCatalogState(
775
+ io: CodexAppServerProcessIo = {},
776
+ ): CodexAppServerCatalogStatus {
777
+ const now = (io.now ?? Date.now)();
778
+ const fullyDefault = !io.listSnapshots && !io.readStartMs && !io.catalogMtimeMs
779
+ && !io.platform && !io.getuid && !io.now;
780
+ if (fullyDefault
781
+ && catalogStateCache
782
+ && now - catalogStateCache.atMs < catalogStateTtlMs(catalogStateCache.status.state)) {
783
+ return catalogStateCache.status;
784
+ }
785
+ const compute = (): CodexAppServerCatalogStatus => {
786
+ const platform = io.platform ?? process.platform;
787
+ const getuid = io.getuid ?? (() => {
788
+ try {
789
+ return typeof process.getuid === "function" ? process.getuid() : undefined;
790
+ } catch {
791
+ return undefined;
792
+ }
793
+ });
794
+ let snapshots: ProcessSnapshot[];
795
+ let enumerationFailed = false;
796
+ const enumerate = io.listSnapshots ?? (() => defaultListSnapshots(platform, getuid));
797
+ try {
798
+ snapshots = enumerate();
799
+ } catch {
800
+ // Enumeration failure must never read as "nothing running" — that would let
801
+ // positive model guidance through on guesswork (#857). The injected seam gets
802
+ // the same contract as the default path: whoever enumerates, a failure to read
803
+ // the process list is unknown, not an empty machine.
804
+ snapshots = [];
805
+ enumerationFailed = true;
806
+ }
807
+ const processes = codexAppServerProcessesFromSnapshots(snapshots);
808
+ if (processes.length === 0) {
809
+ return enumerationFailed
810
+ ? { state: "unknown", processes: [], catalogMtimeMs: null }
811
+ : { state: "not_running", processes: [], catalogMtimeMs: null };
812
+ }
813
+ const catalogMtimeMs = (io.catalogMtimeMs ?? defaultCatalogMtimeMs)();
814
+ const starts = io.readStartMs
815
+ ? new Map(processes.map(proc => [proc.pid, io.readStartMs!(proc.pid)] as const))
816
+ : readProcessStartMsBatch(processes.map(proc => proc.pid), platform);
817
+ return catalogStatusFromProcesses(processes, catalogMtimeMs, starts);
818
+ };
819
+ const status = compute();
820
+ if (fullyDefault) {
821
+ catalogStateCache = { atMs: now, status };
822
+ }
823
+ return status;
824
+ }
825
+
826
+ /**
827
+ * Request-path catalog state collector.
828
+ *
829
+ * [Decision Log]
830
+ * - 목적과 의도: keep Windows CIM discovery from blocking Bun's event loop while v2 guidance is built.
831
+ * - 기존 구현 및 제약 조건: CLI/service operations still need the synchronous, fail-closed collector; the request path needs only advisory state.
832
+ * - 검토한 주요 대안: remove stale-catalog guidance, move all process work to workers, or add a Windows-only async boundary.
833
+ * - 선택한 방식: retain the synchronous API and use async PowerShell plus an identity-scoped in-flight refresh, short cache, and invalidation generation only for Windows requests.
834
+ * - 다른 대안 대신 이 방식을 선택한 이유: it fixes unrelated `/healthz` starvation without widening the process-matching or restart contract.
835
+ * - 장점, 단점 및 영향: concurrent turns share one CIM walk, invalidated pre-write results cannot repopulate the cache, and the event loop stays responsive; a cold v2 turn can still await the bounded advisory probe.
836
+ */
837
+ export async function collectCodexAppServerCatalogStateForRequest(
838
+ io: CodexAppServerProcessIo = {},
839
+ ): Promise<CodexAppServerCatalogStatus> {
840
+ const platform = io.platform ?? process.platform;
841
+ if (platform !== "win32") return collectCodexAppServerCatalogState(io);
842
+
843
+ const now = (io.now ?? Date.now)();
844
+ const generation = requestCatalogStateGeneration;
845
+ const identity: RequestCatalogStateIdentity = {
846
+ platform,
847
+ listSnapshots: io.listSnapshots,
848
+ listSnapshotsAsync: io.listSnapshotsAsync,
849
+ readStartMs: io.readStartMs,
850
+ readStartMsBatchAsync: io.readStartMsBatchAsync,
851
+ catalogMtimeMs: io.catalogMtimeMs,
852
+ now: io.now,
853
+ };
854
+ if (requestCatalogStateCache
855
+ && requestCatalogStateCache.generation === generation
856
+ && sameRequestCatalogStateIdentity(requestCatalogStateCache.identity, identity)
857
+ && now - requestCatalogStateCache.atMs < catalogStateTtlMs(requestCatalogStateCache.status.state)) {
858
+ return requestCatalogStateCache.status;
859
+ }
860
+ if (requestCatalogStateFlight
861
+ && requestCatalogStateFlight.generation === generation
862
+ && sameRequestCatalogStateIdentity(requestCatalogStateFlight.identity, identity)) {
863
+ return requestCatalogStateFlight.promise;
864
+ }
865
+
866
+ const refresh = async (): Promise<CodexAppServerCatalogStatus> => {
867
+ let snapshots: ProcessSnapshot[];
868
+ try {
869
+ snapshots = io.listSnapshotsAsync
870
+ ? await io.listSnapshotsAsync()
871
+ : io.listSnapshots
872
+ ? io.listSnapshots()
873
+ : await listWindowsSnapshotsAsync();
874
+ } catch {
875
+ return { state: "unknown", processes: [], catalogMtimeMs: null };
876
+ }
877
+ const processes = codexAppServerProcessesFromSnapshots(snapshots);
878
+ if (processes.length === 0) {
879
+ return { state: "not_running", processes: [], catalogMtimeMs: null };
880
+ }
881
+ let catalogMtimeMs: number | null;
882
+ try {
883
+ catalogMtimeMs = (io.catalogMtimeMs ?? defaultCatalogMtimeMs)();
884
+ } catch {
885
+ catalogMtimeMs = null;
886
+ }
887
+ const pids = processes.map(proc => proc.pid);
888
+ const starts = io.readStartMsBatchAsync
889
+ ? await io.readStartMsBatchAsync(pids)
890
+ : io.readStartMs
891
+ ? new Map(pids.map(pid => [pid, io.readStartMs!(pid)] as const))
892
+ : await readWindowsProcessStartMsBatchAsync(pids);
893
+ return catalogStatusFromProcesses(processes, catalogMtimeMs, starts);
894
+ };
895
+
896
+ const pending = refresh().catch(() => ({
897
+ state: "unknown" as const,
898
+ processes: [],
899
+ catalogMtimeMs: null,
900
+ }));
901
+ let flight: RequestCatalogStateFlight;
902
+ const promise = pending.then(status => {
903
+ // A catalog write can invalidate while slow CIM is still running. The result
904
+ // describes the PRE-write world, so it must neither repopulate the post-write
905
+ // cache nor reach the caller.
906
+ //
907
+ // Suppressing only the cache write is not enough. The awaiting request still
908
+ // received `fresh`, and `fresh` is the one state that authorizes positive
909
+ // guidance (`src/server/responses/collaboration.ts:279-280` returns null for
910
+ // `stale`/`unknown` but describes the catalog for `fresh`). So the request
911
+ // would advertise the newly written disk catalog to an app-server whose
912
+ // in-memory copy the write just made stale — a wrong answer, which is worse
913
+ // than the slow answer this whole change exists to fix.
914
+ //
915
+ // Degrade to `unknown` instead: it is the honest description of what an
916
+ // invalidated observation knows, and the guidance path already treats it as
917
+ // "say nothing positive".
918
+ if (requestCatalogStateGeneration !== generation) {
919
+ return { state: "unknown" as const, processes: [], catalogMtimeMs: null };
920
+ }
921
+ if (requestCatalogStateFlight === flight) {
922
+ requestCatalogStateCache = {
923
+ generation,
924
+ identity,
925
+ atMs: (io.now ?? Date.now)(),
926
+ status,
927
+ };
928
+ }
929
+ return status;
930
+ }).finally(() => {
931
+ if (requestCatalogStateFlight === flight) requestCatalogStateFlight = null;
932
+ });
933
+ flight = { generation, identity, promise };
934
+ requestCatalogStateFlight = flight;
935
+ return flight.promise;
936
+ }
937
+
938
+ /**
939
+ * Drop memoized catalog state after a relevant catalog/cache write and before
940
+ * the post-write state read. Advancing the generation prevents an older
941
+ * in-flight Windows CIM refresh from publishing its pre-write result after the
942
+ * write has completed.
943
+ */
944
+ export function resetCodexAppServerCatalogStateCache(): void {
945
+ catalogStateCache = null;
946
+ requestCatalogStateGeneration += 1;
947
+ requestCatalogStateCache = null;
948
+ requestCatalogStateFlight = null;
949
+ }
950
+
951
+ export interface RestartCodexAppServersResult {
952
+ requested: number[];
953
+ stopped: number[];
954
+ surviving: number[];
955
+ failed: Array<{ pid: number; error: string }>;
956
+ }
957
+
958
+ /**
959
+ * Platform-appropriate termination for a matched app-server.
960
+ *
961
+ * On Windows `process.kill(pid, "SIGTERM")` is not a graceful signal — it is an
962
+ * unconditional terminate of that one process, and it leaves the process tree
963
+ * behind. `taskkill /T /F` is therefore not an escalation there: the kill was
964
+ * hard either way, and `/T` adds the child cleanup that keeps an app-server's
965
+ * children from being orphaned when the window is closed without a quit
966
+ * affordance. That matters most on Windows precisely because there is no Ctrl+Q.
967
+ *
968
+ * The asymmetry with Unix is deliberate and must not be "fixed" into symmetry:
969
+ * on Unix, SIGTERM really is graceful, and following it with SIGKILL would ask a
970
+ * harsher consent than a restart click gives. Survivors are reported instead.
971
+ *
972
+ * The executable is resolved from a trusted system directory rather than PATH,
973
+ * matching how this file already resolves PowerShell — an unqualified
974
+ * `taskkill` is a hijack surface.
975
+ */
976
+ function defaultKillCodexAppServer(
977
+ pid: number,
978
+ signal: NodeJS.Signals,
979
+ io: CodexAppServerProcessIo = {},
980
+ ): void {
981
+ const platform = io.platform ?? process.platform;
982
+ const signalProcess = io.processKill ?? ((target: number, sig: NodeJS.Signals) => {
983
+ process.kill(target, sig);
984
+ });
985
+ if (platform !== "win32") {
986
+ signalProcess(pid, signal);
987
+ return;
988
+ }
989
+ const exec = io.execFile ?? ((file: string, args: readonly string[]) => {
990
+ execFileSync(file, [...args], { stdio: "ignore", timeout: 5_000, windowsHide: true });
991
+ });
992
+ try {
993
+ exec(resolveTrustedWindowsTaskkillExe(), ["/PID", String(pid), "/T", "/F"]);
994
+ } catch {
995
+ // Fall back to the previous behavior rather than reporting a failure the old
996
+ // code would not have reported.
997
+ signalProcess(pid, signal);
998
+ }
999
+ }
1000
+
1001
+
1002
+ /** Send SIGTERM to matched processes and wait briefly; never escalates to SIGKILL. */
1003
+ export function restartCodexAppServers(
1004
+ processes: readonly CodexAppServerProcess[] = listCodexAppServerProcesses(),
1005
+ io: CodexAppServerProcessIo = {},
1006
+ ): RestartCodexAppServersResult {
1007
+ const isAlive = io.isAlive ?? isProcessAlive;
1008
+ const kill = io.kill ?? ((pid, signal) => { defaultKillCodexAppServer(pid, signal, io); });
1009
+ const wait = io.waitExit ?? waitForExit;
1010
+ const now = io.now ?? Date.now;
1011
+ const requested = processes.map(process => process.pid);
1012
+ const stopped: number[] = [];
1013
+ const surviving: number[] = [];
1014
+ const failed: Array<{ pid: number; error: string }> = [];
1015
+
1016
+ // Re-resolve immediately before signaling so a recycled PID is never killed.
1017
+ // Require the same pid+command-line identity as the original match — a new
1018
+ // Codex-shaped process that reused the PID must not receive SIGTERM.
1019
+ const liveByPid = new Map(
1020
+ listCodexAppServerProcesses(io).map(process => [process.pid, process] as const),
1021
+ );
1022
+ const signaled: CodexAppServerProcess[] = [];
1023
+
1024
+ for (const proc of processes) {
1025
+ const live = liveByPid.get(proc.pid);
1026
+ if (!live || codexAppServerProcessIdentity(live) !== codexAppServerProcessIdentity(proc)) {
1027
+ // Original target exited (or identity changed); do not signal a replacement.
1028
+ if (!isAlive(proc.pid)) stopped.push(proc.pid);
1029
+ continue;
1030
+ }
1031
+ try {
1032
+ kill(proc.pid, "SIGTERM");
1033
+ signaled.push(proc);
1034
+ } catch (error) {
1035
+ if (isAlive(proc.pid)) {
1036
+ failed.push({
1037
+ pid: proc.pid,
1038
+ error: error instanceof Error ? error.message : String(error),
1039
+ });
1040
+ surviving.push(proc.pid);
1041
+ } else {
1042
+ stopped.push(proc.pid);
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ // Shared deadline so N survivors wait ~2s total, not N×2s.
1048
+ const deadline = now() + 2_000;
1049
+ for (const proc of signaled) {
1050
+ const remaining = Math.max(0, deadline - now());
1051
+ if (wait(proc.pid, remaining) || !isAlive(proc.pid)) stopped.push(proc.pid);
1052
+ else surviving.push(proc.pid);
1053
+ }
1054
+
1055
+ return { requested, stopped, surviving, failed };
1056
+ }
1057
+
1058
+ export interface AfterCatalogWriteAppServerOptions {
1059
+ restart: boolean;
1060
+ log?: Pick<Console, "log" | "error"> | null;
1061
+ io?: CodexAppServerProcessIo;
1062
+ }
1063
+
1064
+ export interface AfterCatalogWriteAppServerResult {
1065
+ processes: CodexAppServerProcess[];
1066
+ warned: boolean;
1067
+ restart?: RestartCodexAppServersResult;
1068
+ hint: string;
1069
+ }
1070
+
1071
+ /** Warn about stale app-servers after catalog/cache writes, or restart them when requested. */
1072
+ export function afterCatalogWriteHandleAppServers(
1073
+ options: AfterCatalogWriteAppServerOptions,
1074
+ ): AfterCatalogWriteAppServerResult {
1075
+ const processes = listCodexAppServerProcesses(options.io);
1076
+ const hint = STALE_CODEX_APP_SERVER_HINT;
1077
+ if (processes.length === 0) {
1078
+ return { processes, warned: false, hint };
1079
+ }
1080
+ if (!options.restart) {
1081
+ options.log?.error(formatStaleCodexAppServerWarning(processes));
1082
+ return { processes, warned: true, hint };
1083
+ }
1084
+ options.log?.log(
1085
+ `Stopping Codex app-server process(es): ${processes.map(process => process.pid).join(", ")} `
1086
+ + "(active turns may be interrupted).",
1087
+ );
1088
+ const restart = restartCodexAppServers(processes, options.io);
1089
+ if (restart.stopped.length > 0) {
1090
+ options.log?.log(`Stopped Codex app-server PID(s): ${restart.stopped.join(", ")}`);
1091
+ }
1092
+ for (const failure of restart.failed) {
1093
+ options.log?.error(`Failed to stop Codex app-server PID ${failure.pid}: ${failure.error}`);
1094
+ }
1095
+ if (restart.surviving.length > 0) {
1096
+ options.log?.error(
1097
+ `Codex app-server PID(s) still running after SIGTERM: ${restart.surviving.join(", ")}. `
1098
+ + "Stop them manually if the model list stays stale.",
1099
+ );
1100
+ }
1101
+ return { processes, warned: false, restart, hint };
1102
+ }
1103
+
1104
+ /**
1105
+ * Startup-safe counterpart to {@link afterCatalogWriteHandleAppServers} (#1046).
1106
+ *
1107
+ * Service startup rewrites the catalog and the models cache, but an app-server
1108
+ * that booted earlier keeps an in-memory model list — Codex builds a static
1109
+ * manager from the catalog once and never rereads the file — so the picker shows
1110
+ * a roster that no longer exists on disk. Every check a user runs reads the file;
1111
+ * the picker renders memory.
1112
+ *
1113
+ * Two things this deliberately does NOT do, both of which the `--restart-codex`
1114
+ * path does:
1115
+ *
1116
+ * - It never signals anything. Killing an app-server on an unattended boot would
1117
+ * interrupt whatever turn the user has in flight. A human typing
1118
+ * `ocx sync --restart-codex` is consenting to that; a login is not.
1119
+ * - It never warns about a merely-running app-server. It asks the mtime
1120
+ * classifier whether one is actually stale, so a boot with Codex open and a
1121
+ * current catalog stays quiet.
1122
+ *
1123
+ * Failure is swallowed: startup synchronization is best-effort and must not stop
1124
+ * the proxy from coming up.
1125
+ *
1126
+ * The memoized state is dropped first. {@link collectCodexAppServerCatalogState}
1127
+ * caches for 5s when every io field is defaulted, so a `fresh` reading taken
1128
+ * before the write would otherwise be replayed after it and this would stay
1129
+ * silent about the very staleness it exists to report.
1130
+ */
1131
+ export function warnIfStaleCodexAppServersAfterStartupWrite(
1132
+ options: { log?: Pick<Console, "error">; io?: CodexAppServerProcessIo } = {},
1133
+ ): { warned: boolean } {
1134
+ try {
1135
+ resetCodexAppServerCatalogStateCache();
1136
+ const status = collectCodexAppServerCatalogState(options.io ?? {});
1137
+ if (status.state !== "stale") return { warned: false };
1138
+ options.log?.error(formatStaleCodexAppServerWarning(status.processes));
1139
+ return { warned: true };
1140
+ } catch {
1141
+ return { warned: false };
1142
+ }
1143
+ }