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