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