@iislee/opencodex 2.11.0 → 2.35.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 (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -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 +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -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 +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
@@ -7,13 +7,18 @@
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 =
16
- "If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex).";
20
+ "If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex). "
21
+ + "On Windows the desktop app itself may also need a full restart (ocx sync --restart-desktop-app).";
17
22
 
18
23
  /** Attach the shared dashboard hint only after a catalog or models_cache write. */
19
24
  export function attachStaleAppServerHint<T extends {
@@ -71,6 +76,7 @@ export interface CodexAppServerProcess {
71
76
  export interface ProcessSnapshot {
72
77
  pid: number;
73
78
  commandLine: string;
79
+ executable?: string;
74
80
  uid?: number;
75
81
  owner?: string;
76
82
  startedAtMs?: number;
@@ -82,12 +88,41 @@ export interface CodexAppServerProcessIo {
82
88
  listSnapshots?: () => ProcessSnapshot[];
83
89
  isAlive?: (pid: number) => boolean;
84
90
  kill?: (pid: number, signal: NodeJS.Signals) => void;
91
+ /** Windows termination seam: drives the taskkill branch without a real exec. */
92
+ execFile?: (file: string, args: readonly string[]) => void;
93
+ /**
94
+ * Signal seam for the Unix branch and the taskkill fallback. Without it,
95
+ * injecting `kill` bypasses defaultKillCodexAppServer entirely, so the
96
+ * fallback could never be observed.
97
+ */
98
+ processKill?: (pid: number, signal: NodeJS.Signals) => void;
85
99
  waitExit?: (pid: number, timeoutMs: number) => boolean;
86
100
  now?: () => number;
87
101
  readStartMs?: (pid: number) => number | null;
102
+ /** Async process-list seam used by the request-path Windows collector. */
103
+ listSnapshotsAsync?: () => Promise<ProcessSnapshot[]>;
104
+ /** Async batch start-time seam used by the request-path Windows collector. */
105
+ readStartMsBatchAsync?: (pids: readonly number[]) => Promise<Map<number, number | null>>;
88
106
  catalogMtimeMs?: () => number | null;
89
107
  }
90
108
 
109
+ function execFileTextAsync(
110
+ file: string,
111
+ args: readonly string[],
112
+ timeoutMs: number,
113
+ ): Promise<string> {
114
+ return new Promise((resolve, reject) => {
115
+ execFile(file, [...args], {
116
+ encoding: "utf-8",
117
+ timeout: timeoutMs,
118
+ windowsHide: true,
119
+ }, (error: ExecFileException | null, stdout: string) => {
120
+ if (error) reject(error);
121
+ else resolve(stdout);
122
+ });
123
+ });
124
+ }
125
+
91
126
  /** Split a process command line into argv-like tokens (handles simple quotes). */
92
127
  export function tokenizeCommandLine(commandLine: string): string[] {
93
128
  const tokens: string[] = [];
@@ -211,13 +246,37 @@ export function codexAppServerProcessIdentity(proc: Pick<CodexAppServerProcess,
211
246
  }
212
247
 
213
248
  /** 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());
249
+ export function isCodexAppServerCommandLine(commandLine: string, executable?: string): boolean {
250
+ const trimmed = commandLine.trim();
251
+ let tokens = tokenizeCommandLine(trimmed);
252
+ if (executable) {
253
+ if (isCodeModeHostToken(executable)) return true;
254
+ if (isCodexExecutableToken(executable)) {
255
+ let remainder = trimmed;
256
+ if (remainder.startsWith(executable)) remainder = remainder.slice(executable.length).trimStart();
257
+ else if (remainder.startsWith(`"${executable}"`)) remainder = remainder.slice(executable.length + 2).trimStart();
258
+ tokens = [executable, ...tokenizeCommandLine(remainder)];
259
+ }
260
+ }
216
261
  if (tokens.length === 0) return false;
217
262
  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`).
263
+ // An npm-installed Codex runs as a PAIR: `node /usr/local/bin/codex app-server` and the
264
+ // vendored native binary that wrapper spawns. Only the child used to match, so
265
+ // `--restart-codex` signalled the child while its supervisor kept holding the socket -
266
+ // which is what "PID(s) still running after SIGTERM" was reporting on Linux.
267
+ //
268
+ // The codex-shaped token must be IMMEDIATELY next. Skipping interpreter flags to reach
269
+ // it looks tempting and is wrong: interpreter options take values, so a generic skip
270
+ // reads the value of `node --require codex app-server worker.js` as the entrypoint.
271
+ // Supporting flags needs a real Node/Bun/Deno entrypoint parser, not a loop over
272
+ // hyphens; the observed wrappers put the path first, so this stays narrow on purpose.
273
+ //
274
+ // Dropping only the interpreter and re-running the ordinary scan is what preserves the
275
+ // subcommand discipline below: `node <codex> exec 'hi'` stays unmatched exactly like
276
+ // `codex exec 'hi'` does.
277
+ if (isInterpreterToken(tokens[0]!) && tokens.length > 1 && isCodexExecutableToken(tokens[1]!)) {
278
+ tokens = tokens.slice(1);
279
+ }
221
280
  if (!isCodexExecutableToken(tokens[0]!)) return false;
222
281
 
223
282
  let i = 1;
@@ -227,7 +286,6 @@ export function isCodexAppServerCommandLine(commandLine: string): boolean {
227
286
  i = advancePastCodexGlobalOption(tokens, i);
228
287
  continue;
229
288
  }
230
- // First non-option after globals is the Codex subcommand.
231
289
  return token.toLowerCase() === "app-server";
232
290
  }
233
291
  return false;
@@ -253,12 +311,13 @@ function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
253
311
  const status = readFileSync(`/proc/${pid}/status`, "utf8");
254
312
  const processUid = parseUnixProcStatusUid(status);
255
313
  if (uid !== undefined && processUid !== undefined && processUid !== uid) continue;
256
- const commandLine = readFileSync(`/proc/${pid}/cmdline`)
314
+ const argv = readFileSync(`/proc/${pid}/cmdline`)
257
315
  .toString("utf8")
258
- .replace(/\0/g, " ")
259
- .trim();
316
+ .split("\0")
317
+ .filter(Boolean);
318
+ const commandLine = argv.join(" ").trim();
260
319
  if (!commandLine) continue;
261
- out.push({ pid, commandLine, uid: processUid });
320
+ out.push({ pid, commandLine, executable: argv[0], uid: processUid });
262
321
  } catch {
263
322
  /* process exited mid-scan */
264
323
  }
@@ -268,20 +327,29 @@ function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
268
327
 
269
328
  function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
270
329
  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,
330
+ const commandOutput = uid !== undefined
331
+ ? execFileSync("/bin/ps", ["-u", String(uid), "-o", "pid=,command="], {
332
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
278
333
  })
279
- : execFileSync("ps", ["-axo", "pid=,uid=,command="], {
280
- encoding: "utf-8",
281
- stdio: ["ignore", "pipe", "ignore"],
282
- timeout: 5_000,
334
+ : execFileSync("/bin/ps", ["-axo", "pid=,uid=,command="], {
335
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
336
+ });
337
+ const executableOutput = uid !== undefined
338
+ ? execFileSync("/bin/ps", ["-u", String(uid), "-o", "pid=,comm="], {
339
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
340
+ })
341
+ : execFileSync("/bin/ps", ["-axo", "pid=,comm="], {
342
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
283
343
  });
284
- for (const raw of output.split(/\r?\n/)) {
344
+ const executableByPid = new Map<number, string>();
345
+ for (const raw of executableOutput.split(/\r?\n/)) {
346
+ const match = /^\s*(\d+)\s+(.+)$/.exec(raw);
347
+ if (!match) continue;
348
+ const pid = Number(match[1]);
349
+ const executable = match[2]?.trim() ?? "";
350
+ if (Number.isSafeInteger(pid) && pid > 0 && executable) executableByPid.set(pid, executable);
351
+ }
352
+ for (const raw of commandOutput.split(/\r?\n/)) {
285
353
  const line = raw.trim();
286
354
  if (!line) continue;
287
355
  if (uid !== undefined) {
@@ -289,8 +357,8 @@ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
289
357
  if (!match) continue;
290
358
  const pid = Number(match[1]);
291
359
  const commandLine = match[2]?.trim() ?? "";
292
- if (!Number.isSafeInteger(pid) || pid <= 1 || !commandLine) continue;
293
- out.push({ pid, commandLine, uid });
360
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !commandLine) continue;
361
+ out.push({ pid, commandLine, executable: executableByPid.get(pid), uid });
294
362
  continue;
295
363
  }
296
364
  const match = /^(\d+)\s+(\d+)\s+(.*)$/.exec(line);
@@ -298,8 +366,11 @@ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
298
366
  const pid = Number(match[1]);
299
367
  const processUid = Number(match[2]);
300
368
  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 });
369
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !commandLine) continue;
370
+ out.push({
371
+ pid, commandLine, executable: executableByPid.get(pid),
372
+ uid: Number.isSafeInteger(processUid) ? processUid : undefined,
373
+ });
303
374
  }
304
375
  return out;
305
376
  }
@@ -316,8 +387,34 @@ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
316
387
  * Exported for the Windows integration regression that exercises the real
317
388
  * PowerShell enumeration.
318
389
  */
319
- export function listWindowsSnapshots(): ProcessSnapshot[] {
390
+ /**
391
+ * Turn one PowerShell enumeration's stdout into snapshots.
392
+ *
393
+ * Split out from the spawn so the failure contract is testable off-Windows: the
394
+ * sentinel path is the difference between "no Codex process is running" and "we could
395
+ * not read the process list", and only one of those is safe to act on.
396
+ */
397
+ export function parseWindowsSnapshotOutput(output: string): ProcessSnapshot[] {
320
398
  const out: ProcessSnapshot[] = [];
399
+ for (const line of output.split(/\r?\n/)) {
400
+ // A candidate whose owner could not be verified — or a top-level query that
401
+ // failed outright — makes the whole enumeration incomplete. The staleness
402
+ // collector must not read the partial result as "nothing running".
403
+ if (line.trim() === "__OCX_ENUM_INCOMPLETE__") throw new Error("windows_enum_incomplete");
404
+ const tab = line.indexOf("\t");
405
+ if (tab <= 0) continue;
406
+ const tab2 = line.indexOf("\t", tab + 1);
407
+ if (tab2 <= tab) continue;
408
+ const pid = Number(line.slice(0, tab));
409
+ const commandLine = line.slice(tab + 1, tab2).trim();
410
+ const owner = line.slice(tab2 + 1).trim();
411
+ if (!Number.isSafeInteger(pid) || pid <= 1 || !commandLine || !owner) continue;
412
+ out.push({ pid, commandLine, owner });
413
+ }
414
+ return out;
415
+ }
416
+
417
+ function windowsSnapshotPowerShellCommand(): string {
321
418
  // Newlines keep -Command as a real script (space-joined statements need ';').
322
419
  // Double-quoted format string so `t expands to a real tab.
323
420
  // Codex candidates only: basename token codex / codex.exe / codex.cmd /
@@ -326,10 +423,18 @@ export function listWindowsSnapshots(): ProcessSnapshot[] {
326
423
  // path with "opencodex".
327
424
  const basenameMatch = powerShellSingleQuotedIgnoreCaseMatch(WINDOWS_CODEX_BASENAME_CANDIDATE_RE.source);
328
425
  const codeModeMatch = powerShellSingleQuotedIgnoreCaseMatch(WINDOWS_CODEX_CODE_MODE_HOST_CANDIDATE_RE.source);
329
- const psCommand = [
426
+ return [
330
427
  "$ErrorActionPreference='SilentlyContinue'",
331
428
  "$me=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name",
332
- "Get-CimInstance Win32_Process | Where-Object {",
429
+ // -ErrorAction Stop plus the outer try is what makes a TOP-LEVEL query failure
430
+ // observable. Under SilentlyContinue alone, a failing Get-CimInstance emits nothing
431
+ // and the enumeration is indistinguishable from "no Codex process is running" —
432
+ // the parse loop finds no rows, no sentinel is produced, and the staleness collector
433
+ // reports not_running for a machine whose process list it never actually read.
434
+ // The per-process catch below cannot cover this: it only runs once the pipeline has
435
+ // objects to iterate.
436
+ "try {",
437
+ "Get-CimInstance Win32_Process -ErrorAction Stop | Where-Object {",
333
438
  " -not [string]::IsNullOrWhiteSpace($_.CommandLine) -and (",
334
439
  ` $_.CommandLine -match ${basenameMatch} -or`,
335
440
  ` $_.CommandLine -match ${codeModeMatch}`,
@@ -344,29 +449,32 @@ export function listWindowsSnapshots(): ProcessSnapshot[] {
344
449
  " \"{0}`t{1}`t{2}\" -f $_.ProcessId, $cmd, $owner",
345
450
  " } catch { \"__OCX_ENUM_INCOMPLETE__\" }",
346
451
  "}",
452
+ "} catch { \"__OCX_ENUM_INCOMPLETE__\" }",
347
453
  ].join("\n");
348
- // Top-level exec failure propagates (see listDarwinSnapshots note).
349
- const output = execFileSync("powershell.exe", [
350
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
454
+ }
455
+
456
+ export function listWindowsSnapshots(runPowerShell?: (psCommand: string) => string): ProcessSnapshot[] {
457
+ // Top-level exec failure propagates (see listDarwinSnapshots note). The
458
+ // executable resolves from the trusted System32 directory (never PATH), and
459
+ // windowsHide keeps the enumeration console-less on desktop sessions (#1278).
460
+ const psCommand = windowsSnapshotPowerShellCommand();
461
+ const output = runPowerShell
462
+ ? runPowerShell(psCommand)
463
+ : execFileSync(resolveTrustedWindowsPowerShellExe(), [
464
+ "-NoProfile", "-NoLogo", "-NonInteractive",
465
+ "-Command",
466
+ psCommand,
467
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true });
468
+ return parseWindowsSnapshotOutput(output);
469
+ }
470
+
471
+ async function listWindowsSnapshotsAsync(): Promise<ProcessSnapshot[]> {
472
+ const output = await execFileTextAsync(resolveTrustedWindowsPowerShellExe(), [
473
+ "-NoProfile", "-NoLogo", "-NonInteractive",
351
474
  "-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;
475
+ windowsSnapshotPowerShellCommand(),
476
+ ], 8_000);
477
+ return parseWindowsSnapshotOutput(output);
370
478
  }
371
479
 
372
480
  function defaultListSnapshots(platform: NodeJS.Platform, getuid: () => number | undefined): ProcessSnapshot[] {
@@ -400,19 +508,22 @@ export function listCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): C
400
508
  const matched: CodexAppServerProcess[] = [];
401
509
  for (const snapshot of snapshots) {
402
510
  if (seen.has(snapshot.pid)) continue;
403
- if (!isCodexAppServerCommandLine(snapshot.commandLine)) continue;
511
+ if (!isCodexAppServerCommandLine(snapshot.commandLine, snapshot.executable)) continue;
404
512
  seen.add(snapshot.pid);
405
513
  matched.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
406
514
  }
407
515
  return matched;
408
516
  }
409
517
 
410
- export function formatStaleCodexAppServerWarning(processes: readonly CodexAppServerProcess[]): string {
518
+ export function formatStaleCodexAppServerWarning(
519
+ processes: readonly { pid: number }[],
520
+ ): string {
411
521
  const pids = processes.map(process => process.pid).join(", ");
412
522
  return (
413
523
  `WARNING: ${processes.length} Codex app-server process(es) still running (PID${processes.length === 1 ? "" : "s"}: ${pids}). `
414
524
  + "Disk catalog/cache were updated, but Codex may keep showing the old model list until those processes restart. "
415
525
  + "Re-run with `ocx sync --restart-codex` (or `ocx sync-cache --restart-codex`) to send SIGTERM only to matching app-server processes. "
526
+ + "On Windows the desktop app itself may also need a full restart (`ocx sync --restart-desktop-app`). "
416
527
  + "Active turns may be interrupted."
417
528
  );
418
529
  }
@@ -439,7 +550,7 @@ function readLinuxProcStartMs(pid: number): number | null {
439
550
  /** `ps` lstart → epoch ms, or null (macOS). */
440
551
  function readDarwinProcStartMs(pid: number): number | null {
441
552
  try {
442
- const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
553
+ const out = execFileSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], {
443
554
  encoding: "utf-8",
444
555
  stdio: ["ignore", "pipe", "ignore"],
445
556
  timeout: 4_000,
@@ -455,8 +566,8 @@ function readDarwinProcStartMs(pid: number): number | null {
455
566
  /** Win32_Process.CreationDate → epoch ms, or null (Windows). */
456
567
  function readWindowsProcStartMs(pid: number): number | null {
457
568
  try {
458
- const out = execFileSync("powershell.exe", [
459
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
569
+ const out = execFileSync(resolveTrustedWindowsPowerShellExe(), [
570
+ "-NoProfile", "-NoLogo", "-NonInteractive",
460
571
  "-Command",
461
572
  `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate.ToUniversalTime().ToString("o")`,
462
573
  ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true }).trim();
@@ -475,6 +586,41 @@ export function readProcessStartMs(pid: number, platform: NodeJS.Platform = proc
475
586
  return readLinuxProcStartMs(pid);
476
587
  }
477
588
 
589
+ function windowsProcessStartPowerShellCommand(pids: readonly number[]): string {
590
+ const filter = pids.map(pid => `ProcessId=${pid}`).join(" OR ");
591
+ return `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId)\t$($_.CreationDate.ToUniversalTime().ToString("o"))" }`;
592
+ }
593
+
594
+ function parseWindowsProcessStartTimes(
595
+ stdout: string,
596
+ pids: readonly number[],
597
+ ): Map<number, number | null> {
598
+ const byPid = new Map<number, number>();
599
+ for (const line of stdout.split(/\r?\n/)) {
600
+ const tab = line.indexOf("\t");
601
+ if (tab <= 0) continue;
602
+ const pid = Number(line.slice(0, tab));
603
+ const parsed = Date.parse(line.slice(tab + 1).trim());
604
+ if (Number.isSafeInteger(pid) && Number.isFinite(parsed)) byPid.set(pid, parsed);
605
+ }
606
+ return new Map(pids.map(pid => [pid, byPid.get(pid) ?? null]));
607
+ }
608
+
609
+ async function readWindowsProcessStartMsBatchAsync(
610
+ pids: readonly number[],
611
+ ): Promise<Map<number, number | null>> {
612
+ try {
613
+ const stdout = await execFileTextAsync(resolveTrustedWindowsPowerShellExe(), [
614
+ "-NoProfile", "-NoLogo", "-NonInteractive",
615
+ "-Command",
616
+ windowsProcessStartPowerShellCommand(pids),
617
+ ], 5_000);
618
+ return parseWindowsProcessStartTimes(stdout, pids);
619
+ } catch {
620
+ return new Map(pids.map(pid => [pid, null]));
621
+ }
622
+ }
623
+
478
624
  /**
479
625
  * Start times for many pids in ONE platform call where possible, so the
480
626
  * staleness check does not serialize per-process ps/PowerShell invocations
@@ -488,7 +634,7 @@ export function readProcessStartMsBatch(
488
634
  if (pids.length === 0) return out;
489
635
  if (platform === "darwin") {
490
636
  try {
491
- const stdout = execFileSync("ps", ["-o", "pid=,lstart=", "-p", pids.join(",")], {
637
+ const stdout = execFileSync("/bin/ps", ["-o", "pid=,lstart=", "-p", pids.join(",")], {
492
638
  encoding: "utf-8",
493
639
  stdio: ["ignore", "pipe", "ignore"],
494
640
  timeout: 3_000,
@@ -510,22 +656,12 @@ export function readProcessStartMsBatch(
510
656
  }
511
657
  if (platform === "win32") {
512
658
  try {
513
- const filter = pids.map(pid => `ProcessId=${pid}`).join(" OR ");
514
- const stdout = execFileSync("powershell.exe", [
515
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
659
+ const stdout = execFileSync(resolveTrustedWindowsPowerShellExe(), [
660
+ "-NoProfile", "-NoLogo", "-NonInteractive",
516
661
  "-Command",
517
- `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId)\t$($_.CreationDate.ToUniversalTime().ToString("o"))" }`,
662
+ windowsProcessStartPowerShellCommand(pids),
518
663
  ], { 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;
664
+ return parseWindowsProcessStartTimes(stdout, pids);
529
665
  } catch {
530
666
  for (const pid of pids) out.set(pid, null);
531
667
  return out;
@@ -552,10 +688,113 @@ function defaultCatalogMtimeMs(): number | null {
552
688
  }
553
689
  }
554
690
 
691
+ function codexAppServerProcessesFromSnapshots(
692
+ snapshots: readonly ProcessSnapshot[],
693
+ ): CodexAppServerProcess[] {
694
+ const processes: CodexAppServerProcess[] = [];
695
+ const seen = new Set<number>();
696
+ for (const snapshot of snapshots) {
697
+ if (seen.has(snapshot.pid)) continue;
698
+ if (!isCodexAppServerCommandLine(snapshot.commandLine, snapshot.executable)) continue;
699
+ seen.add(snapshot.pid);
700
+ processes.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
701
+ }
702
+ return processes;
703
+ }
704
+
705
+ function catalogStatusFromProcesses(
706
+ processes: readonly CodexAppServerProcess[],
707
+ catalogMtimeMs: number | null,
708
+ starts: ReadonlyMap<number, number | null>,
709
+ ): CodexAppServerCatalogStatus {
710
+ const withStarts = processes.map(proc => ({
711
+ pid: proc.pid,
712
+ startedAtMs: starts.get(proc.pid) ?? null,
713
+ }));
714
+ if (catalogMtimeMs === null || withStarts.some(proc => proc.startedAtMs === null)) {
715
+ return { state: "unknown", processes: withStarts, catalogMtimeMs };
716
+ }
717
+ // `<=` is deliberate: coarse clocks (ps lstart is second-granularity) can
718
+ // report equal values when the catalog actually changed after startup.
719
+ const stale = withStarts.some(proc => proc.startedAtMs! <= catalogMtimeMs);
720
+ return { state: stale ? "stale" : "fresh", processes: withStarts, catalogMtimeMs };
721
+ }
722
+
555
723
  // Short TTL: process listing + stat run once per window even under per-turn
556
724
  // guidance calls (#857).
557
725
  let catalogStateCache: { atMs: number; status: CodexAppServerCatalogStatus } | null = null;
726
+ interface RequestCatalogStateIdentity {
727
+ platform: NodeJS.Platform;
728
+ listSnapshots?: CodexAppServerProcessIo["listSnapshots"];
729
+ listSnapshotsAsync?: CodexAppServerProcessIo["listSnapshotsAsync"];
730
+ readStartMs?: CodexAppServerProcessIo["readStartMs"];
731
+ readStartMsBatchAsync?: CodexAppServerProcessIo["readStartMsBatchAsync"];
732
+ catalogMtimeMs?: CodexAppServerProcessIo["catalogMtimeMs"];
733
+ now?: CodexAppServerProcessIo["now"];
734
+ }
735
+
736
+ interface RequestCatalogStateFlight {
737
+ generation: number;
738
+ identity: RequestCatalogStateIdentity;
739
+ promise: Promise<CodexAppServerCatalogStatus>;
740
+ }
741
+
742
+ let requestCatalogStateGeneration = 0;
743
+ let requestCatalogStateCache: {
744
+ generation: number;
745
+ identity: RequestCatalogStateIdentity;
746
+ atMs: number;
747
+ status: CodexAppServerCatalogStatus;
748
+ } | null = null;
749
+ let requestCatalogStateFlight: RequestCatalogStateFlight | null = null;
558
750
  const CATALOG_STATE_TTL_MS = 5_000;
751
+ /**
752
+ * `unknown` is a failure to observe, not an observation, so it gets a much shorter
753
+ * window than a real reading. At the full 5s a single transient enumeration failure
754
+ * suppresses guidance for every call in that window, and the retry that would have
755
+ * succeeded never runs. Keeping a brief window still collapses a burst of per-turn
756
+ * calls into one probe, which is what the cache is for.
757
+ */
758
+ const CATALOG_STATE_UNKNOWN_TTL_MS = 250;
759
+
760
+ /**
761
+ * How long a real observation may still be SERVED after it expires, while a refresh
762
+ * runs behind it (#2499).
763
+ *
764
+ * The probe is advisory and, on Windows, slow: `Invoke-CimMethod GetOwner` costs
765
+ * ~0.4s per candidate process, so a cold probe routinely outlives the 5s TTL and
766
+ * every turn that misses the cache pays for it on the request path. Serving the
767
+ * previous reading immediately keeps that cost off the turn without pretending it is
768
+ * fresh -- the refresh it triggers is what makes the next reading current.
769
+ *
770
+ * Measured from expiry rather than from when the reading was taken, so a `fresh`
771
+ * entry stays servable for its own TTL plus this bound. Anchoring it to expiry keeps
772
+ * the stale window independent of the TTL -- a cap on total age would quietly turn
773
+ * this path off if the TTL were ever raised past it.
774
+ *
775
+ * Bounded rather than unlimited: if the refresh keeps failing, an observation this
776
+ * old stops being evidence about the machine and it is better to wait for a real one.
777
+ * `unknown` is never served this way -- it is a failure to observe, not an
778
+ * observation, and it already has its own short window for exactly that reason.
779
+ */
780
+ export const CATALOG_STATE_MAX_STALE_MS = 60_000;
781
+
782
+ export function catalogStateTtlMs(state: CodexAppServerCatalogState): number {
783
+ return state === "unknown" ? CATALOG_STATE_UNKNOWN_TTL_MS : CATALOG_STATE_TTL_MS;
784
+ }
785
+
786
+ function sameRequestCatalogStateIdentity(
787
+ left: RequestCatalogStateIdentity,
788
+ right: RequestCatalogStateIdentity,
789
+ ): boolean {
790
+ return left.platform === right.platform
791
+ && left.listSnapshots === right.listSnapshots
792
+ && left.listSnapshotsAsync === right.listSnapshotsAsync
793
+ && left.readStartMs === right.readStartMs
794
+ && left.readStartMsBatchAsync === right.readStartMsBatchAsync
795
+ && left.catalogMtimeMs === right.catalogMtimeMs
796
+ && left.now === right.now;
797
+ }
559
798
 
560
799
  /**
561
800
  * Compare the on-disk catalog mtime against the start time of running Codex
@@ -580,7 +819,8 @@ export function collectCodexAppServerCatalogState(
580
819
  const fullyDefault = !io.listSnapshots && !io.readStartMs && !io.catalogMtimeMs
581
820
  && !io.platform && !io.getuid && !io.now;
582
821
  if (fullyDefault
583
- && catalogStateCache && now - catalogStateCache.atMs < CATALOG_STATE_TTL_MS) {
822
+ && catalogStateCache
823
+ && now - catalogStateCache.atMs < catalogStateTtlMs(catalogStateCache.status.state)) {
584
824
  return catalogStateCache.status;
585
825
  }
586
826
  const compute = (): CodexAppServerCatalogStatus => {
@@ -594,45 +834,28 @@ export function collectCodexAppServerCatalogState(
594
834
  });
595
835
  let snapshots: ProcessSnapshot[];
596
836
  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 });
837
+ const enumerate = io.listSnapshots ?? (() => defaultListSnapshots(platform, getuid));
838
+ try {
839
+ snapshots = enumerate();
840
+ } catch {
841
+ // Enumeration failure must never read as "nothing running" — that would let
842
+ // positive model guidance through on guesswork (#857). The injected seam gets
843
+ // the same contract as the default path: whoever enumerates, a failure to read
844
+ // the process list is unknown, not an empty machine.
845
+ snapshots = [];
846
+ enumerationFailed = true;
616
847
  }
848
+ const processes = codexAppServerProcessesFromSnapshots(snapshots);
617
849
  if (processes.length === 0) {
618
850
  return enumerationFailed
619
851
  ? { state: "unknown", processes: [], catalogMtimeMs: null }
620
852
  : { state: "not_running", processes: [], catalogMtimeMs: null };
621
853
  }
622
854
  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 };
855
+ const starts = io.readStartMs
856
+ ? new Map(processes.map(proc => [proc.pid, io.readStartMs!(proc.pid)] as const))
857
+ : readProcessStartMsBatch(processes.map(proc => proc.pid), platform);
858
+ return catalogStatusFromProcesses(processes, catalogMtimeMs, starts);
636
859
  };
637
860
  const status = compute();
638
861
  if (fullyDefault) {
@@ -641,9 +864,164 @@ export function collectCodexAppServerCatalogState(
641
864
  return status;
642
865
  }
643
866
 
644
- /** Test hook: drop the memoized catalog state. */
867
+ /**
868
+ * Request-path catalog state collector.
869
+ *
870
+ * [Decision Log]
871
+ * - 목적과 의도: keep Windows CIM discovery from blocking Bun's event loop while v2 guidance is built.
872
+ * - 기존 구현 및 제약 조건: CLI/service operations still need the synchronous, fail-closed collector; the request path needs only advisory state.
873
+ * - 검토한 주요 대안: remove stale-catalog guidance, move all process work to workers, or add a Windows-only async boundary.
874
+ * - 선택한 방식: retain the synchronous API and use async PowerShell plus an identity-scoped in-flight refresh, short cache, and invalidation generation only for Windows requests.
875
+ * - 다른 대안 대신 이 방식을 선택한 이유: it fixes unrelated `/healthz` starvation without widening the process-matching or restart contract.
876
+ * - 장점, 단점 및 영향: 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.
877
+ *
878
+ * [Decision Log · #2499]
879
+ * - 목적과 의도: a cold probe outlives its own 5s TTL on Windows (~435ms per candidate process for `Invoke-CimMethod GetOwner`), so the cache expires before it can serve and the miss lands on a turn.
880
+ * - 기존 구현 및 제약 조건: the reading is advisory, and only `fresh` authorizes positive guidance (`src/server/responses/collaboration.ts`); `unknown` is a failure to observe rather than an observation.
881
+ * - 검토한 주요 대안: drop the per-process GetOwner fan-out (issue suggestion 1), or widen the TTL past the probe duration (suggestion 3).
882
+ * - 선택한 방식: serve an expired reading immediately when its generation still matches, bounded by `CATALOG_STATE_MAX_STALE_MS`, never for `unknown`, and refresh behind it; a failed refresh no longer evicts the reading it was refreshing.
883
+ * - 다른 대안 대신 이 방식을 선택한 이유: the fan-out change alters what "could not verify the owner" means for the current-user scoping contract and needs its own ground-truth comparison; a wider TTL still pays the probe on every human-paced turn.
884
+ * - 장점, 단점 및 영향: after the first probe the request path never waits; a server that stopped between readings can be described as `fresh` for up to the stale bound; a catalog write still invalidates immediately through the generation, and the cold path is unchanged.
885
+ */
886
+ export async function collectCodexAppServerCatalogStateForRequest(
887
+ io: CodexAppServerProcessIo = {},
888
+ ): Promise<CodexAppServerCatalogStatus> {
889
+ const platform = io.platform ?? process.platform;
890
+ if (platform !== "win32") return collectCodexAppServerCatalogState(io);
891
+
892
+ const now = (io.now ?? Date.now)();
893
+ const generation = requestCatalogStateGeneration;
894
+ const identity: RequestCatalogStateIdentity = {
895
+ platform,
896
+ listSnapshots: io.listSnapshots,
897
+ listSnapshotsAsync: io.listSnapshotsAsync,
898
+ readStartMs: io.readStartMs,
899
+ readStartMsBatchAsync: io.readStartMsBatchAsync,
900
+ catalogMtimeMs: io.catalogMtimeMs,
901
+ now: io.now,
902
+ };
903
+ const cached = requestCatalogStateCache
904
+ && requestCatalogStateCache.generation === generation
905
+ && sameRequestCatalogStateIdentity(requestCatalogStateCache.identity, identity)
906
+ ? requestCatalogStateCache
907
+ : null;
908
+ if (cached && now - cached.atMs < catalogStateTtlMs(cached.status.state)) {
909
+ return cached.status;
910
+ }
911
+ // An expired real reading is still worth handing back while the refresh runs. It
912
+ // cannot have been invalidated by an ocx catalog write: every such write calls
913
+ // `resetCodexAppServerCatalogStateCache`, which advances the generation and drops
914
+ // this entry, so a generation match means no write has landed since it was taken.
915
+ // What it can miss is an app-server that started or stopped meanwhile -- and a
916
+ // server started after the reading is newer than the catalog, which is the `fresh`
917
+ // this entry already says.
918
+ const servableStale = cached
919
+ && cached.status.state !== "unknown"
920
+ && now - cached.atMs < catalogStateTtlMs(cached.status.state) + CATALOG_STATE_MAX_STALE_MS
921
+ ? cached.status
922
+ : null;
923
+ if (requestCatalogStateFlight
924
+ && requestCatalogStateFlight.generation === generation
925
+ && sameRequestCatalogStateIdentity(requestCatalogStateFlight.identity, identity)) {
926
+ return servableStale ?? requestCatalogStateFlight.promise;
927
+ }
928
+
929
+ const refresh = async (): Promise<CodexAppServerCatalogStatus> => {
930
+ let snapshots: ProcessSnapshot[];
931
+ try {
932
+ snapshots = io.listSnapshotsAsync
933
+ ? await io.listSnapshotsAsync()
934
+ : io.listSnapshots
935
+ ? io.listSnapshots()
936
+ : await listWindowsSnapshotsAsync();
937
+ } catch {
938
+ return { state: "unknown", processes: [], catalogMtimeMs: null };
939
+ }
940
+ const processes = codexAppServerProcessesFromSnapshots(snapshots);
941
+ if (processes.length === 0) {
942
+ return { state: "not_running", processes: [], catalogMtimeMs: null };
943
+ }
944
+ let catalogMtimeMs: number | null;
945
+ try {
946
+ catalogMtimeMs = (io.catalogMtimeMs ?? defaultCatalogMtimeMs)();
947
+ } catch {
948
+ catalogMtimeMs = null;
949
+ }
950
+ const pids = processes.map(proc => proc.pid);
951
+ const starts = io.readStartMsBatchAsync
952
+ ? await io.readStartMsBatchAsync(pids)
953
+ : io.readStartMs
954
+ ? new Map(pids.map(pid => [pid, io.readStartMs!(pid)] as const))
955
+ : await readWindowsProcessStartMsBatchAsync(pids);
956
+ return catalogStatusFromProcesses(processes, catalogMtimeMs, starts);
957
+ };
958
+
959
+ const pending = refresh().catch(() => ({
960
+ state: "unknown" as const,
961
+ processes: [],
962
+ catalogMtimeMs: null,
963
+ }));
964
+ let flight: RequestCatalogStateFlight;
965
+ const promise = pending.then(status => {
966
+ // A catalog write can invalidate while slow CIM is still running. The result
967
+ // describes the PRE-write world, so it must neither repopulate the post-write
968
+ // cache nor reach the caller.
969
+ //
970
+ // Suppressing only the cache write is not enough. The awaiting request still
971
+ // received `fresh`, and `fresh` is the one state that authorizes positive
972
+ // guidance (`src/server/responses/collaboration.ts:279-280` returns null for
973
+ // `stale`/`unknown` but describes the catalog for `fresh`). So the request
974
+ // would advertise the newly written disk catalog to an app-server whose
975
+ // in-memory copy the write just made stale — a wrong answer, which is worse
976
+ // than the slow answer this whole change exists to fix.
977
+ //
978
+ // Degrade to `unknown` instead: it is the honest description of what an
979
+ // invalidated observation knows, and the guidance path already treats it as
980
+ // "say nothing positive".
981
+ if (requestCatalogStateGeneration !== generation) {
982
+ return { state: "unknown" as const, processes: [], catalogMtimeMs: null };
983
+ }
984
+ // A refresh that failed must not evict a real reading. Before this function
985
+ // served stale entries, caching `unknown` cost at most the 250ms that state
986
+ // is allowed to live. Now that an expired observation is what callers are
987
+ // handed, overwriting one with `unknown` would take the answer AWAY from
988
+ // them on a transient failure -- `unknown` is not servable, so the next
989
+ // caller waits for a probe instead of getting the reading it would have had.
990
+ // Keep the observation and let its own age retire it.
991
+ const wouldEvictAnObservation = status.state === "unknown"
992
+ && requestCatalogStateCache?.generation === generation
993
+ && requestCatalogStateCache.status.state !== "unknown"
994
+ && sameRequestCatalogStateIdentity(requestCatalogStateCache.identity, identity);
995
+ if (requestCatalogStateFlight === flight && !wouldEvictAnObservation) {
996
+ requestCatalogStateCache = {
997
+ generation,
998
+ identity,
999
+ atMs: (io.now ?? Date.now)(),
1000
+ status,
1001
+ };
1002
+ }
1003
+ return status;
1004
+ }).finally(() => {
1005
+ if (requestCatalogStateFlight === flight) requestCatalogStateFlight = null;
1006
+ });
1007
+ flight = { generation, identity, promise };
1008
+ requestCatalogStateFlight = flight;
1009
+ // `promise` already absorbs its own failures, so leaving it unawaited here cannot
1010
+ // surface as an unhandled rejection; the next caller picks up whatever it stored.
1011
+ return servableStale ?? flight.promise;
1012
+ }
1013
+
1014
+ /**
1015
+ * Drop memoized catalog state after a relevant catalog/cache write and before
1016
+ * the post-write state read. Advancing the generation prevents an older
1017
+ * in-flight Windows CIM refresh from publishing its pre-write result after the
1018
+ * write has completed.
1019
+ */
645
1020
  export function resetCodexAppServerCatalogStateCache(): void {
646
1021
  catalogStateCache = null;
1022
+ requestCatalogStateGeneration += 1;
1023
+ requestCatalogStateCache = null;
1024
+ requestCatalogStateFlight = null;
647
1025
  }
648
1026
 
649
1027
  export interface RestartCodexAppServersResult {
@@ -653,13 +1031,57 @@ export interface RestartCodexAppServersResult {
653
1031
  failed: Array<{ pid: number; error: string }>;
654
1032
  }
655
1033
 
1034
+ /**
1035
+ * Platform-appropriate termination for a matched app-server.
1036
+ *
1037
+ * On Windows `process.kill(pid, "SIGTERM")` is not a graceful signal — it is an
1038
+ * unconditional terminate of that one process, and it leaves the process tree
1039
+ * behind. `taskkill /T /F` is therefore not an escalation there: the kill was
1040
+ * hard either way, and `/T` adds the child cleanup that keeps an app-server's
1041
+ * children from being orphaned when the window is closed without a quit
1042
+ * affordance. That matters most on Windows precisely because there is no Ctrl+Q.
1043
+ *
1044
+ * The asymmetry with Unix is deliberate and must not be "fixed" into symmetry:
1045
+ * on Unix, SIGTERM really is graceful, and following it with SIGKILL would ask a
1046
+ * harsher consent than a restart click gives. Survivors are reported instead.
1047
+ *
1048
+ * The executable is resolved from a trusted system directory rather than PATH,
1049
+ * matching how this file already resolves PowerShell — an unqualified
1050
+ * `taskkill` is a hijack surface.
1051
+ */
1052
+ function defaultKillCodexAppServer(
1053
+ pid: number,
1054
+ signal: NodeJS.Signals,
1055
+ io: CodexAppServerProcessIo = {},
1056
+ ): void {
1057
+ const platform = io.platform ?? process.platform;
1058
+ const signalProcess = io.processKill ?? ((target: number, sig: NodeJS.Signals) => {
1059
+ process.kill(target, sig);
1060
+ });
1061
+ if (platform !== "win32") {
1062
+ signalProcess(pid, signal);
1063
+ return;
1064
+ }
1065
+ const exec = io.execFile ?? ((file: string, args: readonly string[]) => {
1066
+ execFileSync(file, [...args], { stdio: "ignore", timeout: 5_000, windowsHide: true });
1067
+ });
1068
+ try {
1069
+ exec(resolveTrustedWindowsTaskkillExe(), ["/PID", String(pid), "/T", "/F"]);
1070
+ } catch {
1071
+ // Fall back to the previous behavior rather than reporting a failure the old
1072
+ // code would not have reported.
1073
+ signalProcess(pid, signal);
1074
+ }
1075
+ }
1076
+
1077
+
656
1078
  /** Send SIGTERM to matched processes and wait briefly; never escalates to SIGKILL. */
657
1079
  export function restartCodexAppServers(
658
1080
  processes: readonly CodexAppServerProcess[] = listCodexAppServerProcesses(),
659
1081
  io: CodexAppServerProcessIo = {},
660
1082
  ): RestartCodexAppServersResult {
661
1083
  const isAlive = io.isAlive ?? isProcessAlive;
662
- const kill = io.kill ?? ((pid, signal) => { process.kill(pid, signal); });
1084
+ const kill = io.kill ?? ((pid, signal) => { defaultKillCodexAppServer(pid, signal, io); });
663
1085
  const wait = io.waitExit ?? waitForExit;
664
1086
  const now = io.now ?? Date.now;
665
1087
  const requested = processes.map(process => process.pid);
@@ -754,3 +1176,44 @@ export function afterCatalogWriteHandleAppServers(
754
1176
  }
755
1177
  return { processes, warned: false, restart, hint };
756
1178
  }
1179
+
1180
+ /**
1181
+ * Startup-safe counterpart to {@link afterCatalogWriteHandleAppServers} (#1046).
1182
+ *
1183
+ * Service startup rewrites the catalog and the models cache, but an app-server
1184
+ * that booted earlier keeps an in-memory model list — Codex builds a static
1185
+ * manager from the catalog once and never rereads the file — so the picker shows
1186
+ * a roster that no longer exists on disk. Every check a user runs reads the file;
1187
+ * the picker renders memory.
1188
+ *
1189
+ * Two things this deliberately does NOT do, both of which the `--restart-codex`
1190
+ * path does:
1191
+ *
1192
+ * - It never signals anything. Killing an app-server on an unattended boot would
1193
+ * interrupt whatever turn the user has in flight. A human typing
1194
+ * `ocx sync --restart-codex` is consenting to that; a login is not.
1195
+ * - It never warns about a merely-running app-server. It asks the mtime
1196
+ * classifier whether one is actually stale, so a boot with Codex open and a
1197
+ * current catalog stays quiet.
1198
+ *
1199
+ * Failure is swallowed: startup synchronization is best-effort and must not stop
1200
+ * the proxy from coming up.
1201
+ *
1202
+ * The memoized state is dropped first. {@link collectCodexAppServerCatalogState}
1203
+ * caches for 5s when every io field is defaulted, so a `fresh` reading taken
1204
+ * before the write would otherwise be replayed after it and this would stay
1205
+ * silent about the very staleness it exists to report.
1206
+ */
1207
+ export function warnIfStaleCodexAppServersAfterStartupWrite(
1208
+ options: { log?: Pick<Console, "error">; io?: CodexAppServerProcessIo } = {},
1209
+ ): { warned: boolean } {
1210
+ try {
1211
+ resetCodexAppServerCatalogStateCache();
1212
+ const status = collectCodexAppServerCatalogState(options.io ?? {});
1213
+ if (status.state !== "stale") return { warned: false };
1214
+ options.log?.error(formatStaleCodexAppServerWarning(status.processes));
1215
+ return { warned: true };
1216
+ } catch {
1217
+ return { warned: false };
1218
+ }
1219
+ }