@iislee/opencodex 2.11.0 → 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 (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
@@ -7,9 +7,13 @@
7
7
  * Never match broad `*codex*` patterns that hit unrelated tools such as
8
8
  * `hermes-codex-bridge-mcp`.
9
9
  */
10
- import { execFileSync } from "node:child_process";
10
+ import { execFile, execFileSync, type ExecFileException } from "node:child_process";
11
11
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
12
12
  import { isProcessAlive, waitForExit } from "../lib/process-control";
13
+ import {
14
+ resolveTrustedWindowsPowerShellExe,
15
+ resolveTrustedWindowsTaskkillExe,
16
+ } from "../lib/windows-elevation";
13
17
  import { readCodexCatalogPath } from "./catalog/parsing";
14
18
 
15
19
  export const STALE_CODEX_APP_SERVER_HINT =
@@ -71,6 +75,7 @@ export interface CodexAppServerProcess {
71
75
  export interface ProcessSnapshot {
72
76
  pid: number;
73
77
  commandLine: string;
78
+ executable?: string;
74
79
  uid?: number;
75
80
  owner?: string;
76
81
  startedAtMs?: number;
@@ -82,12 +87,41 @@ export interface CodexAppServerProcessIo {
82
87
  listSnapshots?: () => ProcessSnapshot[];
83
88
  isAlive?: (pid: number) => boolean;
84
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;
85
98
  waitExit?: (pid: number, timeoutMs: number) => boolean;
86
99
  now?: () => number;
87
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>>;
88
105
  catalogMtimeMs?: () => number | null;
89
106
  }
90
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
+
91
125
  /** Split a process command line into argv-like tokens (handles simple quotes). */
92
126
  export function tokenizeCommandLine(commandLine: string): string[] {
93
127
  const tokens: string[] = [];
@@ -211,13 +245,20 @@ export function codexAppServerProcessIdentity(proc: Pick<CodexAppServerProcess,
211
245
  }
212
246
 
213
247
  /** True when the command line is a Codex app-server (or code-mode host) worth restarting. */
214
- export function isCodexAppServerCommandLine(commandLine: string): boolean {
215
- const tokens = tokenizeCommandLine(commandLine.trim());
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
+ }
216
260
  if (tokens.length === 0) return false;
217
261
  if (isCodeModeHostProcess(tokens)) return true;
218
-
219
- // Require Codex as argv0 so later-argument occurrences stay unmatched
220
- // (e.g. `node worker.js codex app-server`).
221
262
  if (!isCodexExecutableToken(tokens[0]!)) return false;
222
263
 
223
264
  let i = 1;
@@ -227,7 +268,6 @@ export function isCodexAppServerCommandLine(commandLine: string): boolean {
227
268
  i = advancePastCodexGlobalOption(tokens, i);
228
269
  continue;
229
270
  }
230
- // First non-option after globals is the Codex subcommand.
231
271
  return token.toLowerCase() === "app-server";
232
272
  }
233
273
  return false;
@@ -253,12 +293,13 @@ function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
253
293
  const status = readFileSync(`/proc/${pid}/status`, "utf8");
254
294
  const processUid = parseUnixProcStatusUid(status);
255
295
  if (uid !== undefined && processUid !== undefined && processUid !== uid) continue;
256
- const commandLine = readFileSync(`/proc/${pid}/cmdline`)
296
+ const argv = readFileSync(`/proc/${pid}/cmdline`)
257
297
  .toString("utf8")
258
- .replace(/\0/g, " ")
259
- .trim();
298
+ .split("\0")
299
+ .filter(Boolean);
300
+ const commandLine = argv.join(" ").trim();
260
301
  if (!commandLine) continue;
261
- out.push({ pid, commandLine, uid: processUid });
302
+ out.push({ pid, commandLine, executable: argv[0], uid: processUid });
262
303
  } catch {
263
304
  /* process exited mid-scan */
264
305
  }
@@ -268,20 +309,29 @@ function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
268
309
 
269
310
  function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
270
311
  const out: ProcessSnapshot[] = [];
271
- // Top-level exec failure propagates: callers decide their own safe default
272
- // (restart flow treat as none; staleness check → unknown, never "fresh").
273
- const output = uid !== undefined
274
- ? execFileSync("ps", ["-u", String(uid), "-o", "pid=,command="], {
275
- encoding: "utf-8",
276
- stdio: ["ignore", "pipe", "ignore"],
277
- timeout: 5_000,
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,
278
315
  })
279
- : execFileSync("ps", ["-axo", "pid=,uid=,command="], {
280
- encoding: "utf-8",
281
- stdio: ["ignore", "pipe", "ignore"],
282
- timeout: 5_000,
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,
283
325
  });
284
- for (const raw of output.split(/\r?\n/)) {
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/)) {
285
335
  const line = raw.trim();
286
336
  if (!line) continue;
287
337
  if (uid !== undefined) {
@@ -289,8 +339,8 @@ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
289
339
  if (!match) continue;
290
340
  const pid = Number(match[1]);
291
341
  const commandLine = match[2]?.trim() ?? "";
292
- if (!Number.isSafeInteger(pid) || pid <= 1 || !commandLine) continue;
293
- out.push({ pid, commandLine, uid });
342
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !commandLine) continue;
343
+ out.push({ pid, commandLine, executable: executableByPid.get(pid), uid });
294
344
  continue;
295
345
  }
296
346
  const match = /^(\d+)\s+(\d+)\s+(.*)$/.exec(line);
@@ -298,8 +348,11 @@ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
298
348
  const pid = Number(match[1]);
299
349
  const processUid = Number(match[2]);
300
350
  const commandLine = match[3]?.trim() ?? "";
301
- if (!Number.isSafeInteger(pid) || pid <= 1 || !commandLine) continue;
302
- out.push({ pid, commandLine, uid: Number.isSafeInteger(processUid) ? processUid : undefined });
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
+ });
303
356
  }
304
357
  return out;
305
358
  }
@@ -316,8 +369,34 @@ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
316
369
  * Exported for the Windows integration regression that exercises the real
317
370
  * PowerShell enumeration.
318
371
  */
319
- export function listWindowsSnapshots(): ProcessSnapshot[] {
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[] {
320
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 {
321
400
  // Newlines keep -Command as a real script (space-joined statements need ';').
322
401
  // Double-quoted format string so `t expands to a real tab.
323
402
  // Codex candidates only: basename token codex / codex.exe / codex.cmd /
@@ -326,10 +405,18 @@ export function listWindowsSnapshots(): ProcessSnapshot[] {
326
405
  // path with "opencodex".
327
406
  const basenameMatch = powerShellSingleQuotedIgnoreCaseMatch(WINDOWS_CODEX_BASENAME_CANDIDATE_RE.source);
328
407
  const codeModeMatch = powerShellSingleQuotedIgnoreCaseMatch(WINDOWS_CODEX_CODE_MODE_HOST_CANDIDATE_RE.source);
329
- const psCommand = [
408
+ return [
330
409
  "$ErrorActionPreference='SilentlyContinue'",
331
410
  "$me=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name",
332
- "Get-CimInstance Win32_Process | Where-Object {",
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 {",
333
420
  " -not [string]::IsNullOrWhiteSpace($_.CommandLine) -and (",
334
421
  ` $_.CommandLine -match ${basenameMatch} -or`,
335
422
  ` $_.CommandLine -match ${codeModeMatch}`,
@@ -344,29 +431,32 @@ export function listWindowsSnapshots(): ProcessSnapshot[] {
344
431
  " \"{0}`t{1}`t{2}\" -f $_.ProcessId, $cmd, $owner",
345
432
  " } catch { \"__OCX_ENUM_INCOMPLETE__\" }",
346
433
  "}",
434
+ "} catch { \"__OCX_ENUM_INCOMPLETE__\" }",
347
435
  ].join("\n");
348
- // Top-level exec failure propagates (see listDarwinSnapshots note).
349
- const output = execFileSync("powershell.exe", [
350
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
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",
351
456
  "-Command",
352
- psCommand,
353
- ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true });
354
- for (const line of output.split(/\r?\n/)) {
355
- // A candidate whose owner could not be verified makes the whole
356
- // enumeration incomplete — the staleness collector must not read the
357
- // partial result as "nothing running".
358
- if (line.trim() === "__OCX_ENUM_INCOMPLETE__") throw new Error("windows_enum_incomplete");
359
- const tab = line.indexOf("\t");
360
- if (tab <= 0) continue;
361
- const tab2 = line.indexOf("\t", tab + 1);
362
- if (tab2 <= tab) continue;
363
- const pid = Number(line.slice(0, tab));
364
- const commandLine = line.slice(tab + 1, tab2).trim();
365
- const owner = line.slice(tab2 + 1).trim();
366
- if (!Number.isSafeInteger(pid) || pid <= 1 || !commandLine || !owner) continue;
367
- out.push({ pid, commandLine, owner });
368
- }
369
- return out;
457
+ windowsSnapshotPowerShellCommand(),
458
+ ], 8_000);
459
+ return parseWindowsSnapshotOutput(output);
370
460
  }
371
461
 
372
462
  function defaultListSnapshots(platform: NodeJS.Platform, getuid: () => number | undefined): ProcessSnapshot[] {
@@ -400,14 +490,16 @@ export function listCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): C
400
490
  const matched: CodexAppServerProcess[] = [];
401
491
  for (const snapshot of snapshots) {
402
492
  if (seen.has(snapshot.pid)) continue;
403
- if (!isCodexAppServerCommandLine(snapshot.commandLine)) continue;
493
+ if (!isCodexAppServerCommandLine(snapshot.commandLine, snapshot.executable)) continue;
404
494
  seen.add(snapshot.pid);
405
495
  matched.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
406
496
  }
407
497
  return matched;
408
498
  }
409
499
 
410
- export function formatStaleCodexAppServerWarning(processes: readonly CodexAppServerProcess[]): string {
500
+ export function formatStaleCodexAppServerWarning(
501
+ processes: readonly { pid: number }[],
502
+ ): string {
411
503
  const pids = processes.map(process => process.pid).join(", ");
412
504
  return (
413
505
  `WARNING: ${processes.length} Codex app-server process(es) still running (PID${processes.length === 1 ? "" : "s"}: ${pids}). `
@@ -439,7 +531,7 @@ function readLinuxProcStartMs(pid: number): number | null {
439
531
  /** `ps` lstart → epoch ms, or null (macOS). */
440
532
  function readDarwinProcStartMs(pid: number): number | null {
441
533
  try {
442
- const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
534
+ const out = execFileSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], {
443
535
  encoding: "utf-8",
444
536
  stdio: ["ignore", "pipe", "ignore"],
445
537
  timeout: 4_000,
@@ -455,8 +547,8 @@ function readDarwinProcStartMs(pid: number): number | null {
455
547
  /** Win32_Process.CreationDate → epoch ms, or null (Windows). */
456
548
  function readWindowsProcStartMs(pid: number): number | null {
457
549
  try {
458
- const out = execFileSync("powershell.exe", [
459
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
550
+ const out = execFileSync(resolveTrustedWindowsPowerShellExe(), [
551
+ "-NoProfile", "-NoLogo", "-NonInteractive",
460
552
  "-Command",
461
553
  `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate.ToUniversalTime().ToString("o")`,
462
554
  ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true }).trim();
@@ -475,6 +567,41 @@ export function readProcessStartMs(pid: number, platform: NodeJS.Platform = proc
475
567
  return readLinuxProcStartMs(pid);
476
568
  }
477
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
+
478
605
  /**
479
606
  * Start times for many pids in ONE platform call where possible, so the
480
607
  * staleness check does not serialize per-process ps/PowerShell invocations
@@ -488,7 +615,7 @@ export function readProcessStartMsBatch(
488
615
  if (pids.length === 0) return out;
489
616
  if (platform === "darwin") {
490
617
  try {
491
- const stdout = execFileSync("ps", ["-o", "pid=,lstart=", "-p", pids.join(",")], {
618
+ const stdout = execFileSync("/bin/ps", ["-o", "pid=,lstart=", "-p", pids.join(",")], {
492
619
  encoding: "utf-8",
493
620
  stdio: ["ignore", "pipe", "ignore"],
494
621
  timeout: 3_000,
@@ -510,22 +637,12 @@ export function readProcessStartMsBatch(
510
637
  }
511
638
  if (platform === "win32") {
512
639
  try {
513
- const filter = pids.map(pid => `ProcessId=${pid}`).join(" OR ");
514
- const stdout = execFileSync("powershell.exe", [
515
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
640
+ const stdout = execFileSync(resolveTrustedWindowsPowerShellExe(), [
641
+ "-NoProfile", "-NoLogo", "-NonInteractive",
516
642
  "-Command",
517
- `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId)\t$($_.CreationDate.ToUniversalTime().ToString("o"))" }`,
643
+ windowsProcessStartPowerShellCommand(pids),
518
644
  ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true });
519
- const byPid = new Map<number, number>();
520
- for (const line of stdout.split(/\r?\n/)) {
521
- const tab = line.indexOf("\t");
522
- if (tab <= 0) continue;
523
- const pid = Number(line.slice(0, tab));
524
- const parsed = Date.parse(line.slice(tab + 1).trim());
525
- if (Number.isSafeInteger(pid) && Number.isFinite(parsed)) byPid.set(pid, parsed);
526
- }
527
- for (const pid of pids) out.set(pid, byPid.get(pid) ?? null);
528
- return out;
645
+ return parseWindowsProcessStartTimes(stdout, pids);
529
646
  } catch {
530
647
  for (const pid of pids) out.set(pid, null);
531
648
  return out;
@@ -552,10 +669,91 @@ function defaultCatalogMtimeMs(): number | null {
552
669
  }
553
670
  }
554
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
+
555
704
  // Short TTL: process listing + stat run once per window even under per-turn
556
705
  // guidance calls (#857).
557
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;
558
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
+ }
559
757
 
560
758
  /**
561
759
  * Compare the on-disk catalog mtime against the start time of running Codex
@@ -580,7 +778,8 @@ export function collectCodexAppServerCatalogState(
580
778
  const fullyDefault = !io.listSnapshots && !io.readStartMs && !io.catalogMtimeMs
581
779
  && !io.platform && !io.getuid && !io.now;
582
780
  if (fullyDefault
583
- && catalogStateCache && now - catalogStateCache.atMs < CATALOG_STATE_TTL_MS) {
781
+ && catalogStateCache
782
+ && now - catalogStateCache.atMs < catalogStateTtlMs(catalogStateCache.status.state)) {
584
783
  return catalogStateCache.status;
585
784
  }
586
785
  const compute = (): CodexAppServerCatalogStatus => {
@@ -594,45 +793,28 @@ export function collectCodexAppServerCatalogState(
594
793
  });
595
794
  let snapshots: ProcessSnapshot[];
596
795
  let enumerationFailed = false;
597
- if (io.listSnapshots) {
598
- snapshots = io.listSnapshots();
599
- } else {
600
- try {
601
- snapshots = defaultListSnapshots(platform, getuid);
602
- } catch {
603
- // Enumeration failure must never read as "nothing running" that
604
- // would let positive model guidance through on guesswork (#857).
605
- snapshots = [];
606
- enumerationFailed = true;
607
- }
608
- }
609
- const processes: CodexAppServerProcess[] = [];
610
- const seen = new Set<number>();
611
- for (const snapshot of snapshots) {
612
- if (seen.has(snapshot.pid)) continue;
613
- if (!isCodexAppServerCommandLine(snapshot.commandLine)) continue;
614
- seen.add(snapshot.pid);
615
- processes.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
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;
616
806
  }
807
+ const processes = codexAppServerProcessesFromSnapshots(snapshots);
617
808
  if (processes.length === 0) {
618
809
  return enumerationFailed
619
810
  ? { state: "unknown", processes: [], catalogMtimeMs: null }
620
811
  : { state: "not_running", processes: [], catalogMtimeMs: null };
621
812
  }
622
813
  const catalogMtimeMs = (io.catalogMtimeMs ?? defaultCatalogMtimeMs)();
623
- const withStarts = io.readStartMs
624
- ? processes.map(proc => ({ pid: proc.pid, startedAtMs: io.readStartMs!(proc.pid) }))
625
- : (() => {
626
- const batch = readProcessStartMsBatch(processes.map(proc => proc.pid), platform);
627
- return processes.map(proc => ({ pid: proc.pid, startedAtMs: batch.get(proc.pid) ?? null }));
628
- })();
629
- if (catalogMtimeMs === null || withStarts.some(proc => proc.startedAtMs === null)) {
630
- return { state: "unknown", processes: withStarts, catalogMtimeMs };
631
- }
632
- // `<=` is deliberate: coarse clocks (ps lstart is second-granularity) can
633
- // report equal values when the catalog actually changed after startup.
634
- const stale = withStarts.some(proc => proc.startedAtMs! <= catalogMtimeMs);
635
- return { state: stale ? "stale" : "fresh", processes: withStarts, catalogMtimeMs };
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);
636
818
  };
637
819
  const status = compute();
638
820
  if (fullyDefault) {
@@ -641,9 +823,129 @@ export function collectCodexAppServerCatalogState(
641
823
  return status;
642
824
  }
643
825
 
644
- /** Test hook: drop the memoized catalog state. */
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
+ */
645
944
  export function resetCodexAppServerCatalogStateCache(): void {
646
945
  catalogStateCache = null;
946
+ requestCatalogStateGeneration += 1;
947
+ requestCatalogStateCache = null;
948
+ requestCatalogStateFlight = null;
647
949
  }
648
950
 
649
951
  export interface RestartCodexAppServersResult {
@@ -653,13 +955,57 @@ export interface RestartCodexAppServersResult {
653
955
  failed: Array<{ pid: number; error: string }>;
654
956
  }
655
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
+
656
1002
  /** Send SIGTERM to matched processes and wait briefly; never escalates to SIGKILL. */
657
1003
  export function restartCodexAppServers(
658
1004
  processes: readonly CodexAppServerProcess[] = listCodexAppServerProcesses(),
659
1005
  io: CodexAppServerProcessIo = {},
660
1006
  ): RestartCodexAppServersResult {
661
1007
  const isAlive = io.isAlive ?? isProcessAlive;
662
- const kill = io.kill ?? ((pid, signal) => { process.kill(pid, signal); });
1008
+ const kill = io.kill ?? ((pid, signal) => { defaultKillCodexAppServer(pid, signal, io); });
663
1009
  const wait = io.waitExit ?? waitForExit;
664
1010
  const now = io.now ?? Date.now;
665
1011
  const requested = processes.map(process => process.pid);
@@ -754,3 +1100,44 @@ export function afterCatalogWriteHandleAppServers(
754
1100
  }
755
1101
  return { processes, warned: false, restart, hint };
756
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
+ }