@iislee/opencodex 2.11.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 (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,756 @@
1
+ /**
2
+ * Detect / optionally terminate long-lived Codex app-server processes that keep an
3
+ * in-memory model catalog after `ocx sync` rewrites on-disk files (#476).
4
+ *
5
+ * Matching is intentionally narrow: require `app-server` as the Codex subcommand
6
+ * (not merely as a substring in some later argument) or `codex-code-mode-host`.
7
+ * Never match broad `*codex*` patterns that hit unrelated tools such as
8
+ * `hermes-codex-bridge-mcp`.
9
+ */
10
+ import { execFileSync } from "node:child_process";
11
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
12
+ import { isProcessAlive, waitForExit } from "../lib/process-control";
13
+ import { readCodexCatalogPath } from "./catalog/parsing";
14
+
15
+ 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).";
17
+
18
+ /** Attach the shared dashboard hint only after a catalog or models_cache write. */
19
+ export function attachStaleAppServerHint<T extends {
20
+ catalogWritten: boolean;
21
+ cacheSynced: boolean;
22
+ }>(result: T): T & { staleAppServerHint?: string } {
23
+ if (result.catalogWritten || result.cacheSynced) {
24
+ return { ...result, staleAppServerHint: STALE_CODEX_APP_SERVER_HINT };
25
+ }
26
+ return { ...result };
27
+ }
28
+ /**
29
+ * Rust-style target-triple body on official platform-baked Codex binaries
30
+ * (e.g. `x86_64-unknown-linux-musl`, `aarch64-apple-darwin`,
31
+ * `x86_64-pc-windows-msvc`). Requires arch-vendor-os with an optional env
32
+ * segment — not a broad `codex-*` wildcard.
33
+ */
34
+ const CODEX_TARGET_TRIPLE_BODY = "[a-z0-9_]+-[a-z0-9_]+-[a-z0-9_]+(?:-[a-z0-9_]+)?";
35
+
36
+ /**
37
+ * Narrow Win32_Process CommandLine pre-filter (JS + .NET compatible).
38
+ * Allows an optional closing quote after the executable basename so paths like
39
+ * `"C:\Program Files\...\codex.exe" app-server` still reach GetOwner.
40
+ * Also admits official target-triple basenames such as
41
+ * `codex-x86_64-pc-windows-msvc.exe`.
42
+ */
43
+ export const WINDOWS_CODEX_BASENAME_CANDIDATE_RE = new RegExp(
44
+ `(^|[/\\\\\\s'"=])codex(-${CODEX_TARGET_TRIPLE_BODY})?([.]exe|[.]cmd)?['"]?(\\s|$)`,
45
+ "i",
46
+ );
47
+
48
+ export const WINDOWS_CODEX_CODE_MODE_HOST_CANDIDATE_RE = /codex-code-mode-host/i;
49
+
50
+ /** Basename of an official Codex release binary (plain or target-triple). */
51
+ const CODEX_TARGET_TRIPLE_BASENAME_RE = new RegExp(
52
+ `^codex-${CODEX_TARGET_TRIPLE_BODY}(?:\\.exe|\\.cmd)?$`,
53
+ );
54
+
55
+ /** True when a Windows CommandLine is worth paying GetOwner for (current-user scoped later). */
56
+ export function isWindowsCodexCandidateCommandLine(commandLine: string): boolean {
57
+ return WINDOWS_CODEX_BASENAME_CANDIDATE_RE.test(commandLine)
58
+ || WINDOWS_CODEX_CODE_MODE_HOST_CANDIDATE_RE.test(commandLine);
59
+ }
60
+
61
+ /** Embed a regex source in a PowerShell single-quoted -match operand (`''` escapes `'`). */
62
+ function powerShellSingleQuotedIgnoreCaseMatch(patternSource: string): string {
63
+ return `'(?i)${patternSource.replace(/'/g, "''")}'`;
64
+ }
65
+
66
+ export interface CodexAppServerProcess {
67
+ pid: number;
68
+ commandLine: string;
69
+ }
70
+
71
+ export interface ProcessSnapshot {
72
+ pid: number;
73
+ commandLine: string;
74
+ uid?: number;
75
+ owner?: string;
76
+ startedAtMs?: number;
77
+ }
78
+
79
+ export interface CodexAppServerProcessIo {
80
+ platform?: NodeJS.Platform;
81
+ getuid?: () => number | undefined;
82
+ listSnapshots?: () => ProcessSnapshot[];
83
+ isAlive?: (pid: number) => boolean;
84
+ kill?: (pid: number, signal: NodeJS.Signals) => void;
85
+ waitExit?: (pid: number, timeoutMs: number) => boolean;
86
+ now?: () => number;
87
+ readStartMs?: (pid: number) => number | null;
88
+ catalogMtimeMs?: () => number | null;
89
+ }
90
+
91
+ /** Split a process command line into argv-like tokens (handles simple quotes). */
92
+ export function tokenizeCommandLine(commandLine: string): string[] {
93
+ const tokens: string[] = [];
94
+ let current = "";
95
+ let quote: '"' | "'" | null = null;
96
+ for (let i = 0; i < commandLine.length; i++) {
97
+ const ch = commandLine[i]!;
98
+ if (quote) {
99
+ if (ch === quote) quote = null;
100
+ else current += ch;
101
+ continue;
102
+ }
103
+ if (ch === '"' || ch === "'") {
104
+ quote = ch;
105
+ continue;
106
+ }
107
+ if (/\s/.test(ch)) {
108
+ if (current) {
109
+ tokens.push(current);
110
+ current = "";
111
+ }
112
+ continue;
113
+ }
114
+ current += ch;
115
+ }
116
+ if (current) tokens.push(current);
117
+ return tokens;
118
+ }
119
+
120
+ function tokenBasename(token: string): string {
121
+ return token.toLowerCase().replace(/\\/g, "/").split("/").pop() ?? "";
122
+ }
123
+
124
+ function isCodexExecutableToken(token: string): boolean {
125
+ const base = tokenBasename(token);
126
+ return base === "codex" || base === "codex.exe" || base === "codex.cmd"
127
+ || CODEX_TARGET_TRIPLE_BASENAME_RE.test(base);
128
+ }
129
+
130
+ function isCodeModeHostToken(token: string): boolean {
131
+ const base = tokenBasename(token);
132
+ return base === "codex-code-mode-host" || base === "codex-code-mode-host.exe";
133
+ }
134
+
135
+ function isInterpreterToken(token: string): boolean {
136
+ const base = tokenBasename(token);
137
+ return base === "node" || base === "node.exe"
138
+ || base === "bun" || base === "bun.exe"
139
+ || base === "deno" || base === "deno.exe";
140
+ }
141
+
142
+ /**
143
+ * Codex global options that take a following value when written without `=`.
144
+ * Keep this list explicit so unknown flags stay boolean (narrow matching).
145
+ */
146
+ const CODEX_GLOBAL_OPTIONS_WITH_VALUE = new Set([
147
+ "--enable",
148
+ "--disable",
149
+ "--config",
150
+ "-c",
151
+ "--profile",
152
+ "-p",
153
+ "--model",
154
+ "-m",
155
+ "--sandbox",
156
+ "-s",
157
+ "--ask-for-approval",
158
+ "-a",
159
+ "--local-provider",
160
+ "--add-dir",
161
+ "--cd",
162
+ "-C",
163
+ "--color",
164
+ "--image",
165
+ "-i",
166
+ "--output-schema",
167
+ "--output-last-message",
168
+ "-o",
169
+ ]);
170
+
171
+ /** Parse a CLI option token into its flag name and whether a value is inline (`--opt=value`). */
172
+ function splitCliOptionToken(token: string): { name: string; hasInlineValue: boolean } | null {
173
+ if (!token.startsWith("-") || token === "-" || token === "--") return null;
174
+ if (token.startsWith("--")) {
175
+ const eq = token.indexOf("=");
176
+ if (eq >= 0) return { name: token.slice(0, eq).toLowerCase(), hasInlineValue: true };
177
+ return { name: token.toLowerCase(), hasInlineValue: false };
178
+ }
179
+ // Preserve short-option case: `-c` (config) vs `-C` (cd).
180
+ const eq = token.indexOf("=");
181
+ if (eq >= 0) return { name: token.slice(0, eq), hasInlineValue: true };
182
+ return { name: token, hasInlineValue: false };
183
+ }
184
+
185
+ /** Advance past one argv token, consuming a value for known Codex global options. */
186
+ function advancePastCodexGlobalOption(tokens: readonly string[], index: number): number {
187
+ const option = splitCliOptionToken(tokens[index]!);
188
+ if (!option) return index + 1;
189
+ let next = index + 1;
190
+ if (
191
+ !option.hasInlineValue
192
+ && CODEX_GLOBAL_OPTIONS_WITH_VALUE.has(option.name)
193
+ && next < tokens.length
194
+ && !tokens[next]!.startsWith("-")
195
+ ) {
196
+ next += 1; // skip the option value
197
+ }
198
+ return next;
199
+ }
200
+
201
+ /** True when code-mode-host is the process executable or interpreter entrypoint, not a later arg. */
202
+ function isCodeModeHostProcess(tokens: readonly string[]): boolean {
203
+ if (tokens.length === 0) return false;
204
+ if (isCodeModeHostToken(tokens[0]!)) return true;
205
+ return isInterpreterToken(tokens[0]!) && tokens.length > 1 && isCodeModeHostToken(tokens[1]!);
206
+ }
207
+
208
+ /** Stable identity for PID reuse checks: pid + normalized command line. */
209
+ export function codexAppServerProcessIdentity(proc: Pick<CodexAppServerProcess, "pid" | "commandLine">): string {
210
+ return `${proc.pid}\0${proc.commandLine.trim().replace(/\s+/g, " ")}`;
211
+ }
212
+
213
+ /** 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());
216
+ if (tokens.length === 0) return false;
217
+ 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
+ if (!isCodexExecutableToken(tokens[0]!)) return false;
222
+
223
+ let i = 1;
224
+ while (i < tokens.length) {
225
+ const token = tokens[i]!;
226
+ if (token.startsWith("-")) {
227
+ i = advancePastCodexGlobalOption(tokens, i);
228
+ continue;
229
+ }
230
+ // First non-option after globals is the Codex subcommand.
231
+ return token.toLowerCase() === "app-server";
232
+ }
233
+ return false;
234
+ }
235
+
236
+ function parseUnixProcStatusUid(status: string): number | undefined {
237
+ const match = /^Uid:\s+(\d+)/m.exec(status);
238
+ if (!match) return undefined;
239
+ const uid = Number(match[1]);
240
+ return Number.isSafeInteger(uid) ? uid : undefined;
241
+ }
242
+
243
+ function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
244
+ // procfs missing on a Linux-shaped platform is an enumeration failure, not
245
+ // "no processes" — the staleness collector must not read it as not_running.
246
+ if (!existsSync("/proc")) throw new Error("procfs_unavailable");
247
+ const out: ProcessSnapshot[] = [];
248
+ for (const ent of readdirSync("/proc")) {
249
+ if (!/^\d+$/.test(ent)) continue;
250
+ const pid = Number(ent);
251
+ if (!Number.isSafeInteger(pid) || pid <= 1) continue;
252
+ try {
253
+ const status = readFileSync(`/proc/${pid}/status`, "utf8");
254
+ const processUid = parseUnixProcStatusUid(status);
255
+ if (uid !== undefined && processUid !== undefined && processUid !== uid) continue;
256
+ const commandLine = readFileSync(`/proc/${pid}/cmdline`)
257
+ .toString("utf8")
258
+ .replace(/\0/g, " ")
259
+ .trim();
260
+ if (!commandLine) continue;
261
+ out.push({ pid, commandLine, uid: processUid });
262
+ } catch {
263
+ /* process exited mid-scan */
264
+ }
265
+ }
266
+ return out;
267
+ }
268
+
269
+ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
270
+ 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,
278
+ })
279
+ : execFileSync("ps", ["-axo", "pid=,uid=,command="], {
280
+ encoding: "utf-8",
281
+ stdio: ["ignore", "pipe", "ignore"],
282
+ timeout: 5_000,
283
+ });
284
+ for (const raw of output.split(/\r?\n/)) {
285
+ const line = raw.trim();
286
+ if (!line) continue;
287
+ if (uid !== undefined) {
288
+ const match = /^(\d+)\s+(.*)$/.exec(line);
289
+ if (!match) continue;
290
+ const pid = Number(match[1]);
291
+ const commandLine = match[2]?.trim() ?? "";
292
+ if (!Number.isSafeInteger(pid) || pid <= 1 || !commandLine) continue;
293
+ out.push({ pid, commandLine, uid });
294
+ continue;
295
+ }
296
+ const match = /^(\d+)\s+(\d+)\s+(.*)$/.exec(line);
297
+ if (!match) continue;
298
+ const pid = Number(match[1]);
299
+ const processUid = Number(match[2]);
300
+ 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 });
303
+ }
304
+ return out;
305
+ }
306
+
307
+ /**
308
+ * Windows snapshots scoped to the invoking user via Win32_Process GetOwner.
309
+ * PowerShell is the sole path: WMIC lacks reliable owner data and is disabled on
310
+ * many Windows 11 installs; returning unscoped rows would contradict the
311
+ * current-user restart contract.
312
+ *
313
+ * CIM instance methods must use Invoke-CimMethod (direct .GetOwner() calls fail).
314
+ * Candidates are pre-filtered to Codex basename / code-mode-host command lines
315
+ * so we do not pay GetOwner per every process on the machine.
316
+ * Exported for the Windows integration regression that exercises the real
317
+ * PowerShell enumeration.
318
+ */
319
+ export function listWindowsSnapshots(): ProcessSnapshot[] {
320
+ const out: ProcessSnapshot[] = [];
321
+ // Newlines keep -Command as a real script (space-joined statements need ';').
322
+ // Double-quoted format string so `t expands to a real tab.
323
+ // Codex candidates only: basename token codex / codex.exe / codex.cmd /
324
+ // official target-triple binaries (optional closing quote after the
325
+ // basename), or code-mode-host — not incidental substrings like a repo
326
+ // path with "opencodex".
327
+ const basenameMatch = powerShellSingleQuotedIgnoreCaseMatch(WINDOWS_CODEX_BASENAME_CANDIDATE_RE.source);
328
+ const codeModeMatch = powerShellSingleQuotedIgnoreCaseMatch(WINDOWS_CODEX_CODE_MODE_HOST_CANDIDATE_RE.source);
329
+ const psCommand = [
330
+ "$ErrorActionPreference='SilentlyContinue'",
331
+ "$me=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name",
332
+ "Get-CimInstance Win32_Process | Where-Object {",
333
+ " -not [string]::IsNullOrWhiteSpace($_.CommandLine) -and (",
334
+ ` $_.CommandLine -match ${basenameMatch} -or`,
335
+ ` $_.CommandLine -match ${codeModeMatch}`,
336
+ " )",
337
+ "} | ForEach-Object {",
338
+ " try {",
339
+ " $o=Invoke-CimMethod -InputObject $_ -MethodName GetOwner -ErrorAction Stop",
340
+ " if($null -eq $o -or $o.ReturnValue -ne 0 -or [string]::IsNullOrWhiteSpace($o.User)){\"__OCX_ENUM_INCOMPLETE__\"; return}",
341
+ " $owner=if($o.Domain){\"$($o.Domain)\\$($o.User)\"}else{$o.User}",
342
+ " if($owner -ine $me){return}",
343
+ " $cmd=($_.CommandLine -replace \"`t\",\" \")",
344
+ " \"{0}`t{1}`t{2}\" -f $_.ProcessId, $cmd, $owner",
345
+ " } catch { \"__OCX_ENUM_INCOMPLETE__\" }",
346
+ "}",
347
+ ].join("\n");
348
+ // Top-level exec failure propagates (see listDarwinSnapshots note).
349
+ const output = execFileSync("powershell.exe", [
350
+ "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
351
+ "-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;
370
+ }
371
+
372
+ function defaultListSnapshots(platform: NodeJS.Platform, getuid: () => number | undefined): ProcessSnapshot[] {
373
+ if (platform === "win32") return listWindowsSnapshots();
374
+ if (platform === "darwin") return listDarwinSnapshots(getuid());
375
+ return listUnixProcSnapshots(getuid());
376
+ }
377
+
378
+ export function listCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): CodexAppServerProcess[] {
379
+ const platform = io.platform ?? process.platform;
380
+ const getuid = io.getuid ?? (() => {
381
+ try {
382
+ return typeof process.getuid === "function" ? process.getuid() : undefined;
383
+ } catch {
384
+ return undefined;
385
+ }
386
+ });
387
+ let snapshots: ProcessSnapshot[];
388
+ if (io.listSnapshots) {
389
+ snapshots = io.listSnapshots();
390
+ } else {
391
+ // Restart/kill contract (#476): enumeration failure means no targets —
392
+ // never signal a process we could not verify.
393
+ try {
394
+ snapshots = defaultListSnapshots(platform, getuid);
395
+ } catch {
396
+ snapshots = [];
397
+ }
398
+ }
399
+ const seen = new Set<number>();
400
+ const matched: CodexAppServerProcess[] = [];
401
+ for (const snapshot of snapshots) {
402
+ if (seen.has(snapshot.pid)) continue;
403
+ if (!isCodexAppServerCommandLine(snapshot.commandLine)) continue;
404
+ seen.add(snapshot.pid);
405
+ matched.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
406
+ }
407
+ return matched;
408
+ }
409
+
410
+ export function formatStaleCodexAppServerWarning(processes: readonly CodexAppServerProcess[]): string {
411
+ const pids = processes.map(process => process.pid).join(", ");
412
+ return (
413
+ `WARNING: ${processes.length} Codex app-server process(es) still running (PID${processes.length === 1 ? "" : "s"}: ${pids}). `
414
+ + "Disk catalog/cache were updated, but Codex may keep showing the old model list until those processes restart. "
415
+ + "Re-run with `ocx sync --restart-codex` (or `ocx sync-cache --restart-codex`) to send SIGTERM only to matching app-server processes. "
416
+ + "Active turns may be interrupted."
417
+ );
418
+ }
419
+
420
+ /** /proc/<pid>/stat starttime (clock ticks since boot) → epoch ms, or null. */
421
+ function readLinuxProcStartMs(pid: number): number | null {
422
+ try {
423
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
424
+ // Field 22 (starttime) follows the comm field, which may contain spaces
425
+ // inside parentheses — split after the final ")".
426
+ const close = stat.lastIndexOf(")");
427
+ if (close < 0) return null;
428
+ const fields = stat.slice(close + 2).split(/\s+/);
429
+ const startTicks = Number(fields[19]); // field 22 = index 19 after comm
430
+ const boot = /^btime\s+(\d+)/m.exec(readFileSync("/proc/stat", "utf8"));
431
+ if (!Number.isFinite(startTicks) || !boot) return null;
432
+ const hertz = 100; // USER_HZ on every supported Linux target
433
+ return (Number(boot[1]) + startTicks / hertz) * 1000;
434
+ } catch {
435
+ return null;
436
+ }
437
+ }
438
+
439
+ /** `ps` lstart → epoch ms, or null (macOS). */
440
+ function readDarwinProcStartMs(pid: number): number | null {
441
+ try {
442
+ const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
443
+ encoding: "utf-8",
444
+ stdio: ["ignore", "pipe", "ignore"],
445
+ timeout: 4_000,
446
+ }).trim();
447
+ if (!out) return null;
448
+ const parsed = Date.parse(out);
449
+ return Number.isFinite(parsed) ? parsed : null;
450
+ } catch {
451
+ return null;
452
+ }
453
+ }
454
+
455
+ /** Win32_Process.CreationDate → epoch ms, or null (Windows). */
456
+ function readWindowsProcStartMs(pid: number): number | null {
457
+ try {
458
+ const out = execFileSync("powershell.exe", [
459
+ "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
460
+ "-Command",
461
+ `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate.ToUniversalTime().ToString("o")`,
462
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true }).trim();
463
+ if (!out) return null;
464
+ const parsed = Date.parse(out);
465
+ return Number.isFinite(parsed) ? parsed : null;
466
+ } catch {
467
+ return null;
468
+ }
469
+ }
470
+
471
+ /** Best-effort process start time; null when the platform source is unreadable. */
472
+ export function readProcessStartMs(pid: number, platform: NodeJS.Platform = process.platform): number | null {
473
+ if (platform === "win32") return readWindowsProcStartMs(pid);
474
+ if (platform === "darwin") return readDarwinProcStartMs(pid);
475
+ return readLinuxProcStartMs(pid);
476
+ }
477
+
478
+ /**
479
+ * Start times for many pids in ONE platform call where possible, so the
480
+ * staleness check does not serialize per-process ps/PowerShell invocations
481
+ * on the request path (#857). Missing entries come back as null.
482
+ */
483
+ export function readProcessStartMsBatch(
484
+ pids: readonly number[],
485
+ platform: NodeJS.Platform = process.platform,
486
+ ): Map<number, number | null> {
487
+ const out = new Map<number, number | null>();
488
+ if (pids.length === 0) return out;
489
+ if (platform === "darwin") {
490
+ try {
491
+ const stdout = execFileSync("ps", ["-o", "pid=,lstart=", "-p", pids.join(",")], {
492
+ encoding: "utf-8",
493
+ stdio: ["ignore", "pipe", "ignore"],
494
+ timeout: 3_000,
495
+ });
496
+ const byPid = new Map<number, number>();
497
+ for (const raw of stdout.split(/\r?\n/)) {
498
+ const match = /^\s*(\d+)\s+(.+)$/.exec(raw);
499
+ if (!match) continue;
500
+ const pid = Number(match[1]);
501
+ const parsed = Date.parse(match[2]!.trim());
502
+ if (Number.isSafeInteger(pid) && Number.isFinite(parsed)) byPid.set(pid, parsed);
503
+ }
504
+ for (const pid of pids) out.set(pid, byPid.get(pid) ?? null);
505
+ return out;
506
+ } catch {
507
+ for (const pid of pids) out.set(pid, null);
508
+ return out;
509
+ }
510
+ }
511
+ if (platform === "win32") {
512
+ try {
513
+ const filter = pids.map(pid => `ProcessId=${pid}`).join(" OR ");
514
+ const stdout = execFileSync("powershell.exe", [
515
+ "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
516
+ "-Command",
517
+ `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId)\t$($_.CreationDate.ToUniversalTime().ToString("o"))" }`,
518
+ ], { 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;
529
+ } catch {
530
+ for (const pid of pids) out.set(pid, null);
531
+ return out;
532
+ }
533
+ }
534
+ for (const pid of pids) out.set(pid, readLinuxProcStartMs(pid));
535
+ return out;
536
+ }
537
+
538
+ export type CodexAppServerCatalogState = "fresh" | "stale" | "not_running" | "unknown";
539
+
540
+ export interface CodexAppServerCatalogStatus {
541
+ state: CodexAppServerCatalogState;
542
+ processes: Array<{ pid: number; startedAtMs: number | null }>;
543
+ catalogMtimeMs: number | null;
544
+ }
545
+
546
+ /** Resolve the catalog file Codex app-servers loaded at startup, for staleness checks. */
547
+ function defaultCatalogMtimeMs(): number | null {
548
+ try {
549
+ return statSync(readCodexCatalogPath()).mtimeMs;
550
+ } catch {
551
+ return null;
552
+ }
553
+ }
554
+
555
+ // Short TTL: process listing + stat run once per window even under per-turn
556
+ // guidance calls (#857).
557
+ let catalogStateCache: { atMs: number; status: CodexAppServerCatalogStatus } | null = null;
558
+ const CATALOG_STATE_TTL_MS = 5_000;
559
+
560
+ /**
561
+ * Compare the on-disk catalog mtime against the start time of running Codex
562
+ * app-servers (#857): a server that started before the catalog changed keeps
563
+ * an in-memory copy that disagrees with what ocx advertises.
564
+ *
565
+ * Cost note: a cold call synchronously runs the platform listing plus ONE
566
+ * batched start-time query (hard bounds: ~5s+3s macOS, ~8s+5s Windows,
567
+ * microseconds on Linux); the 5s TTL then serves repeats. Typical cold cost
568
+ * is tens of milliseconds; fully-async background refresh is deliberately
569
+ * out of scope for this slice.
570
+ *
571
+ * - not_running: no app-server process → nothing can disagree.
572
+ * - unknown: catalog unreadable, or any server's start time is unreadable —
573
+ * callers must treat this conservatively (suppress positive model claims).
574
+ * - stale: at least one server predates the catalog mtime.
575
+ */
576
+ export function collectCodexAppServerCatalogState(
577
+ io: CodexAppServerProcessIo = {},
578
+ ): CodexAppServerCatalogStatus {
579
+ const now = (io.now ?? Date.now)();
580
+ const fullyDefault = !io.listSnapshots && !io.readStartMs && !io.catalogMtimeMs
581
+ && !io.platform && !io.getuid && !io.now;
582
+ if (fullyDefault
583
+ && catalogStateCache && now - catalogStateCache.atMs < CATALOG_STATE_TTL_MS) {
584
+ return catalogStateCache.status;
585
+ }
586
+ const compute = (): CodexAppServerCatalogStatus => {
587
+ const platform = io.platform ?? process.platform;
588
+ const getuid = io.getuid ?? (() => {
589
+ try {
590
+ return typeof process.getuid === "function" ? process.getuid() : undefined;
591
+ } catch {
592
+ return undefined;
593
+ }
594
+ });
595
+ let snapshots: ProcessSnapshot[];
596
+ 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 });
616
+ }
617
+ if (processes.length === 0) {
618
+ return enumerationFailed
619
+ ? { state: "unknown", processes: [], catalogMtimeMs: null }
620
+ : { state: "not_running", processes: [], catalogMtimeMs: null };
621
+ }
622
+ 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 };
636
+ };
637
+ const status = compute();
638
+ if (fullyDefault) {
639
+ catalogStateCache = { atMs: now, status };
640
+ }
641
+ return status;
642
+ }
643
+
644
+ /** Test hook: drop the memoized catalog state. */
645
+ export function resetCodexAppServerCatalogStateCache(): void {
646
+ catalogStateCache = null;
647
+ }
648
+
649
+ export interface RestartCodexAppServersResult {
650
+ requested: number[];
651
+ stopped: number[];
652
+ surviving: number[];
653
+ failed: Array<{ pid: number; error: string }>;
654
+ }
655
+
656
+ /** Send SIGTERM to matched processes and wait briefly; never escalates to SIGKILL. */
657
+ export function restartCodexAppServers(
658
+ processes: readonly CodexAppServerProcess[] = listCodexAppServerProcesses(),
659
+ io: CodexAppServerProcessIo = {},
660
+ ): RestartCodexAppServersResult {
661
+ const isAlive = io.isAlive ?? isProcessAlive;
662
+ const kill = io.kill ?? ((pid, signal) => { process.kill(pid, signal); });
663
+ const wait = io.waitExit ?? waitForExit;
664
+ const now = io.now ?? Date.now;
665
+ const requested = processes.map(process => process.pid);
666
+ const stopped: number[] = [];
667
+ const surviving: number[] = [];
668
+ const failed: Array<{ pid: number; error: string }> = [];
669
+
670
+ // Re-resolve immediately before signaling so a recycled PID is never killed.
671
+ // Require the same pid+command-line identity as the original match — a new
672
+ // Codex-shaped process that reused the PID must not receive SIGTERM.
673
+ const liveByPid = new Map(
674
+ listCodexAppServerProcesses(io).map(process => [process.pid, process] as const),
675
+ );
676
+ const signaled: CodexAppServerProcess[] = [];
677
+
678
+ for (const proc of processes) {
679
+ const live = liveByPid.get(proc.pid);
680
+ if (!live || codexAppServerProcessIdentity(live) !== codexAppServerProcessIdentity(proc)) {
681
+ // Original target exited (or identity changed); do not signal a replacement.
682
+ if (!isAlive(proc.pid)) stopped.push(proc.pid);
683
+ continue;
684
+ }
685
+ try {
686
+ kill(proc.pid, "SIGTERM");
687
+ signaled.push(proc);
688
+ } catch (error) {
689
+ if (isAlive(proc.pid)) {
690
+ failed.push({
691
+ pid: proc.pid,
692
+ error: error instanceof Error ? error.message : String(error),
693
+ });
694
+ surviving.push(proc.pid);
695
+ } else {
696
+ stopped.push(proc.pid);
697
+ }
698
+ }
699
+ }
700
+
701
+ // Shared deadline so N survivors wait ~2s total, not N×2s.
702
+ const deadline = now() + 2_000;
703
+ for (const proc of signaled) {
704
+ const remaining = Math.max(0, deadline - now());
705
+ if (wait(proc.pid, remaining) || !isAlive(proc.pid)) stopped.push(proc.pid);
706
+ else surviving.push(proc.pid);
707
+ }
708
+
709
+ return { requested, stopped, surviving, failed };
710
+ }
711
+
712
+ export interface AfterCatalogWriteAppServerOptions {
713
+ restart: boolean;
714
+ log?: Pick<Console, "log" | "error"> | null;
715
+ io?: CodexAppServerProcessIo;
716
+ }
717
+
718
+ export interface AfterCatalogWriteAppServerResult {
719
+ processes: CodexAppServerProcess[];
720
+ warned: boolean;
721
+ restart?: RestartCodexAppServersResult;
722
+ hint: string;
723
+ }
724
+
725
+ /** Warn about stale app-servers after catalog/cache writes, or restart them when requested. */
726
+ export function afterCatalogWriteHandleAppServers(
727
+ options: AfterCatalogWriteAppServerOptions,
728
+ ): AfterCatalogWriteAppServerResult {
729
+ const processes = listCodexAppServerProcesses(options.io);
730
+ const hint = STALE_CODEX_APP_SERVER_HINT;
731
+ if (processes.length === 0) {
732
+ return { processes, warned: false, hint };
733
+ }
734
+ if (!options.restart) {
735
+ options.log?.error(formatStaleCodexAppServerWarning(processes));
736
+ return { processes, warned: true, hint };
737
+ }
738
+ options.log?.log(
739
+ `Stopping Codex app-server process(es): ${processes.map(process => process.pid).join(", ")} `
740
+ + "(active turns may be interrupted).",
741
+ );
742
+ const restart = restartCodexAppServers(processes, options.io);
743
+ if (restart.stopped.length > 0) {
744
+ options.log?.log(`Stopped Codex app-server PID(s): ${restart.stopped.join(", ")}`);
745
+ }
746
+ for (const failure of restart.failed) {
747
+ options.log?.error(`Failed to stop Codex app-server PID ${failure.pid}: ${failure.error}`);
748
+ }
749
+ if (restart.surviving.length > 0) {
750
+ options.log?.error(
751
+ `Codex app-server PID(s) still running after SIGTERM: ${restart.surviving.join(", ")}. `
752
+ + "Stop them manually if the model list stays stale.",
753
+ );
754
+ }
755
+ return { processes, warned: false, restart, hint };
756
+ }