@bitkyc08/opencodex 2.83.0 → 2.86.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/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -3
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +56 -15
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +24 -0
- package/src/bridge.ts +1 -1
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/context-windows.ts +54 -1
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +14 -2
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +60 -11
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +21 -4
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +15 -2
- package/src/cli/help.ts +2 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +26 -8
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +20 -7
- package/src/cli/restart-scope.ts +3 -1
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +59 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +7 -1
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +6 -3
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-models.ts +16 -2
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +15 -6
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +18 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +56 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +8 -2
- package/src/config/paths.ts +13 -3
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +19 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +713 -285
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +14 -7
- package/src/integrations/registry.ts +52 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/store.ts +3 -0
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +24 -7
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +28 -17
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +55 -6
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +21 -20
- package/src/providers/registry/entries-extended.ts +19 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +37 -20
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +46 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +15 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/lifecycle.ts +2 -1
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +50 -14
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +16 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +9 -1
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +73 -43
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +261 -81
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +313 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +24 -1
- package/src/server/responses/core-combo.ts +28 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +48 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +20 -11
- package/src/server/responses/passthrough-dispatch.ts +29 -7
- package/src/server/responses/request-prepare.ts +74 -9
- package/src/server/responses/request-transport.ts +22 -8
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +89 -5
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +38 -4
- package/src/server/startup-health-cache.ts +5 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +161 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +466 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +71 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +68 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +19 -1
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- package/gui/dist/assets/index-BHRZdJuv.js +0 -94
- package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
|
@@ -11,7 +11,9 @@ import type { CodexWsSession } from "./codex-ws-session";
|
|
|
11
11
|
import { UPGRADE_DEADLINE_MS, CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES,
|
|
12
12
|
MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, markCodexWsPreludeProjection, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage,
|
|
13
13
|
codexWsCreateFrameExceedsLimit, codexWsFailureDetail, codexWsPreResponseFailure, markCodexWsStage, codexWsOcxVersion,
|
|
14
|
-
markCodexWsSocketDeath, type CodexWsFailureStage, type CodexWsStageRecord
|
|
14
|
+
markCodexWsSocketDeath, codexWsWrapperStatus, isCodexWsTerminalEvent, type CodexWsFailureStage, type CodexWsStageRecord,
|
|
15
|
+
type CodexWsTerminalEvent, codexWsPreOutputRefusalStatus, isEmptyCodexWsLifecycleEvent, codexWsFrameResponseIds,
|
|
16
|
+
markCodexWsPreOutputRefusal, type CodexWsPreOutputRefusalHold } from "./codex-ws-wire";
|
|
15
17
|
|
|
16
18
|
interface ExchangeOptions {
|
|
17
19
|
nativeControl?: NativeResponseControl;
|
|
@@ -26,6 +28,13 @@ interface ExchangeOptions {
|
|
|
26
28
|
onPhysicalDispatch?: () => void;
|
|
27
29
|
/** Bun version string the caller gated on; stamped onto the stage record. */
|
|
28
30
|
bunVersion?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Opt-in (#6969): how long empty lifecycle frames may be held so an authoritative upstream
|
|
33
|
+
* refusal that follows them, before any output, can settle as a replayable gateway status instead
|
|
34
|
+
* of a committed 200. Absent or zero keeps the exchange exactly as it was. Ignored on native
|
|
35
|
+
* control exchanges and on transports without the metadata channel.
|
|
36
|
+
*/
|
|
37
|
+
preOutputRefusalHold?: CodexWsPreOutputRefusalHold;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
const HTTP_HEADER_TOKEN = /^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
|
|
@@ -75,6 +84,8 @@ function rejectionHeaders(source: Record<string, unknown>, prelude: Headers): He
|
|
|
75
84
|
* responses_websocket.rs accepts status/status_code and scalar header values;
|
|
76
85
|
* unlike its native client, this relay converts only precommit 4xx. Returning a
|
|
77
86
|
* post-send 5xx or fetch rejection could cause the outer retry wrapper to resend.
|
|
87
|
+
* The one 5xx exception is the opt-in pre-output refusal below (#6969): a structured upstream
|
|
88
|
+
* refusal with nothing relayed but empty lifecycle frames, which the caller may replay.
|
|
78
89
|
*/
|
|
79
90
|
function wrappedRejectionResponse(payload: Record<string, unknown>, prelude: Headers): Response | null {
|
|
80
91
|
if (payload.type !== "error" || payload.stream_id !== undefined) return null;
|
|
@@ -92,9 +103,20 @@ function wrappedRejectionResponse(payload: Record<string, unknown>, prelude: Hea
|
|
|
92
103
|
}), { status, headers });
|
|
93
104
|
}
|
|
94
105
|
|
|
106
|
+
/** The client-facing error of a pre-output refusal projection, from the refusal's own error object. */
|
|
107
|
+
function refusalErrorBody(source: unknown): Record<string, string> {
|
|
108
|
+
const error = record(source) ? source : {};
|
|
109
|
+
return {
|
|
110
|
+
type: typeof error.type === "string" ? error.type : "server_error",
|
|
111
|
+
...(typeof error.code === "string" ? { code: error.code } : {}),
|
|
112
|
+
message: typeof error.message === "string" ? error.message : "Upstream declined the request before any output",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
95
116
|
/** The sole SSE exchange state machine for both one-shot and retained sockets. */
|
|
96
117
|
export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
97
|
-
const { session, url, init, prepared, sseFallback, onQuota, beforeDispatch, bunVersion, nativeControl, beforeContinuation
|
|
118
|
+
const { session, url, init, prepared, sseFallback, onQuota, beforeDispatch, bunVersion, nativeControl, beforeContinuation,
|
|
119
|
+
preOutputRefusalHold } = options;
|
|
98
120
|
const { frameText, headers } = prepared;
|
|
99
121
|
const signal = init.signal ?? undefined;
|
|
100
122
|
return new Promise<Response>((resolve, reject) => {
|
|
@@ -120,6 +142,9 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
120
142
|
// Numeric close code for the durable stage record; the reason string stays
|
|
121
143
|
// out of it on purpose (#4191 content-free contract).
|
|
122
144
|
let closeCode: number | null = null;
|
|
145
|
+
// #6740: which terminal frame the upstream itself sent, before any synthetic tail.
|
|
146
|
+
let terminalEvent: CodexWsTerminalEvent | null = null;
|
|
147
|
+
let terminalErrorStatus: number | null = null;
|
|
123
148
|
let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
124
149
|
const encoder = new TextEncoder();
|
|
125
150
|
const metadata = url === CODEX_RESPONSES_HTTP_URL ? new CodexWsMetadata(onQuota) : null;
|
|
@@ -135,6 +160,18 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
135
160
|
// The resolved 200, retained so a later body failure can replace its
|
|
136
161
|
// success-shaped stage record with the failure-shaped one (#4191).
|
|
137
162
|
let committedResponse: Response | null = null;
|
|
163
|
+
// #6969: the pre-output refusal hold. While it is open, empty lifecycle frames are queued on the
|
|
164
|
+
// not-yet-exposed stream without resolving it. Every settle other than an eligible refusal
|
|
165
|
+
// commits first (deferredCommit), so the outcome is exactly what the exchange produced before.
|
|
166
|
+
const holdEnabled = (preOutputRefusalHold?.maxMs ?? 0) > 0 && metadata !== null && !nativeControl;
|
|
167
|
+
// Drops for good on any identity the exchange cannot tie to one response: a stream id, two
|
|
168
|
+
// different ids in one frame, or an id that differs from the first one seen. Independent of
|
|
169
|
+
// socket reuse, so a cold one-shot socket is held to the same rule as a pooled one.
|
|
170
|
+
let refusalEligible = holdEnabled;
|
|
171
|
+
let refusalResponseId: string | null = null;
|
|
172
|
+
let heldHeaders: Headers | null = null;
|
|
173
|
+
let deferredCommit = false;
|
|
174
|
+
let holdTimer: ReturnType<typeof setTimeout> | undefined;
|
|
138
175
|
const stream = new ReadableStream<Uint8Array>({
|
|
139
176
|
start(c) { controller = c; },
|
|
140
177
|
cancel() {
|
|
@@ -149,6 +186,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
149
186
|
clearTimeout(upgradeTimer);
|
|
150
187
|
clearTimeout(silenceTimer);
|
|
151
188
|
clearTimeout(pingTimer);
|
|
189
|
+
clearTimeout(holdTimer);
|
|
152
190
|
signal?.removeEventListener("abort", onAbort);
|
|
153
191
|
metadata?.finish();
|
|
154
192
|
correlation?.finish();
|
|
@@ -200,14 +238,20 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
200
238
|
reused: session.reused,
|
|
201
239
|
ocxVersion: codexWsOcxVersion(),
|
|
202
240
|
bunVersion: bunVersion ?? "unknown",
|
|
241
|
+
terminalEvent,
|
|
242
|
+
terminalErrorStatus,
|
|
203
243
|
});
|
|
204
244
|
|
|
205
245
|
const commitResponse = () => {
|
|
206
246
|
if (responseCommitted) return;
|
|
207
247
|
responseCommitted = true;
|
|
248
|
+
deferredCommit = false;
|
|
249
|
+
clearTimeout(holdTimer);
|
|
208
250
|
clearTimeout(silenceTimer);
|
|
209
251
|
clearTimeout(pingTimer);
|
|
210
|
-
|
|
252
|
+
// A held preamble froze the prelude headers at the first Responses event, which is the moment
|
|
253
|
+
// they were frozen before the hold existed.
|
|
254
|
+
const responseHeaders = heldHeaders ? new Headers(heldHeaders) : metadata?.snapshot() ?? new Headers();
|
|
211
255
|
responseHeaders.set("content-type", "text/event-stream; charset=utf-8");
|
|
212
256
|
const response = new Response(stream, { status: 200, headers: responseHeaders });
|
|
213
257
|
metadata?.commit();
|
|
@@ -218,9 +262,73 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
218
262
|
resolve(response);
|
|
219
263
|
};
|
|
220
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Open the hold at the first empty lifecycle frame: freeze the prelude and bound the wait by
|
|
267
|
+
* both the hold's own maximum and its absolute end. False when no time is left, so the frame
|
|
268
|
+
* commits exactly as it would without the hold.
|
|
269
|
+
*/
|
|
270
|
+
const beginHold = (): boolean => {
|
|
271
|
+
if (heldHeaders) return true;
|
|
272
|
+
if (!metadata || !preOutputRefusalHold) return false;
|
|
273
|
+
const waitMs = Math.min(preOutputRefusalHold.maxMs, preOutputRefusalHold.until - Date.now());
|
|
274
|
+
if (waitMs <= 0) return false;
|
|
275
|
+
heldHeaders = metadata.snapshot();
|
|
276
|
+
metadata.commit();
|
|
277
|
+
holdTimer = setTimeout(() => {
|
|
278
|
+
if (!terminal && !responseCommitted) commitResponse();
|
|
279
|
+
}, waitMs);
|
|
280
|
+
return true;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const noteRefusalIdentity = (payload: Record<string, unknown>) => {
|
|
284
|
+
const ids = codexWsFrameResponseIds(payload);
|
|
285
|
+
if (payload.stream_id !== undefined || ids === null || ids.length > 1) {
|
|
286
|
+
refusalEligible = false;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const id = ids[0];
|
|
290
|
+
if (id === undefined) return;
|
|
291
|
+
if (refusalResponseId === null) refusalResponseId = id;
|
|
292
|
+
else if (id !== refusalResponseId) refusalEligible = false;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Settle an authoritative pre-output refusal (#6969) as the gateway status the upstream stated,
|
|
297
|
+
* without the non-replayable marker: nothing but empty lifecycle frames was relayed, and the
|
|
298
|
+
* upstream ended that response itself. The caller's transient ladder and pool move may send the
|
|
299
|
+
* turn again; if neither does, the registered restore hands the client the original stream,
|
|
300
|
+
* which is already complete on `stream`.
|
|
301
|
+
*/
|
|
302
|
+
const resolveRefusal = (status: number, payload: Record<string, unknown>, headers: Headers, prelude: Headers) => {
|
|
303
|
+
responseCommitted = true;
|
|
304
|
+
deferredCommit = false;
|
|
305
|
+
clearTimeout(holdTimer);
|
|
306
|
+
const stage = stageRecord(null);
|
|
307
|
+
const source = payload.type === "error"
|
|
308
|
+
? payload.error
|
|
309
|
+
: record(payload.response) ? payload.response.error : undefined;
|
|
310
|
+
const projection = new Response(JSON.stringify({ error: refusalErrorBody(source) }), { status, headers });
|
|
311
|
+
markCodexWsPreludeProjection(projection);
|
|
312
|
+
markCodexWsStage(projection, stage);
|
|
313
|
+
const observed = Boolean(metadata && onQuota);
|
|
314
|
+
markCodexWsPreOutputRefusal(projection, () => {
|
|
315
|
+
const restoredHeaders = new Headers(prelude);
|
|
316
|
+
restoredHeaders.set("content-type", "text/event-stream; charset=utf-8");
|
|
317
|
+
const restored = new Response(stream, { status: 200, headers: restoredHeaders });
|
|
318
|
+
markCodexWsResponse(restored, observed);
|
|
319
|
+
// The same record object, so the attempt that adopted it keeps reading one stage.
|
|
320
|
+
markCodexWsStage(restored, stage);
|
|
321
|
+
return restored;
|
|
322
|
+
});
|
|
323
|
+
resolve(projection);
|
|
324
|
+
};
|
|
325
|
+
|
|
221
326
|
const failStream = (error: unknown, status: 502 | 504 = 502, { socketDied = false } = {}) => {
|
|
222
327
|
if (terminal) return;
|
|
223
328
|
terminal = true;
|
|
329
|
+
// A held preamble or refusal was a commit deferred, not avoided: settle as the committed
|
|
330
|
+
// body failure it would have been, never as a new pre-response gateway status.
|
|
331
|
+
if (deferredCommit) commitResponse();
|
|
224
332
|
if (sent && !responseCommitted && metadata) {
|
|
225
333
|
// Nothing has been promised to the client yet, so the honest answer is a gateway
|
|
226
334
|
// status, not a 200 whose body then fails. The frame may already be executing
|
|
@@ -305,7 +413,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
305
413
|
reject(reason);
|
|
306
414
|
return;
|
|
307
415
|
}
|
|
308
|
-
if (!responseCommitted && metadata) {
|
|
416
|
+
if (!responseCommitted && metadata && !deferredCommit) {
|
|
309
417
|
// Sent, unacknowledged. The proxy's own connect deadline is an origin-silence
|
|
310
418
|
// verdict and settles like one; a caller abort is the caller's decision, so the
|
|
311
419
|
// exchange rejects with that reason and disposing the socket cancels the turn.
|
|
@@ -497,16 +605,23 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
497
605
|
}
|
|
498
606
|
if (!controlFrame && !type.startsWith("response.") && type !== "error") return;
|
|
499
607
|
let steeringEnded = false;
|
|
608
|
+
let refusalStatus: number | undefined;
|
|
609
|
+
let refusalHeaders: Headers | undefined;
|
|
610
|
+
let refusalPrelude: Headers | undefined;
|
|
500
611
|
if (!controlFrame) {
|
|
501
612
|
firstResponseAt ??= Date.now();
|
|
502
613
|
try {
|
|
503
614
|
if (nativeControl) steeringEnded = nativeControl.observe(normalized.payload);
|
|
504
615
|
else correlation?.accept(normalized.payload);
|
|
505
616
|
} catch (error) { failStream(error); return; }
|
|
617
|
+
const holdOpen = refusalEligible && sent && !responseCommitted;
|
|
618
|
+
if (holdOpen) noteRefusalIdentity(normalized.payload);
|
|
506
619
|
// Correlation must run first: a reused socket's foreign-stream error settles as a
|
|
507
620
|
// non-replayable 502 above, never as the refused-create 4xx projection below, which
|
|
508
621
|
// is the one status family that could authorize an account replay.
|
|
509
|
-
|
|
622
|
+
// Only before anything was held: after an empty preamble a 4xx frame keeps today's
|
|
623
|
+
// in-body delivery, so a held 429 can never become a quota rotation it was not before.
|
|
624
|
+
if (metadata && sent && !responseCommitted && !deferredCommit && type === "error") {
|
|
510
625
|
let rejection: Response | null;
|
|
511
626
|
try { rejection = wrappedRejectionResponse(normalized.payload, metadata.snapshot()); }
|
|
512
627
|
catch (error) { failStream(error); return; }
|
|
@@ -520,7 +635,30 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
520
635
|
return;
|
|
521
636
|
}
|
|
522
637
|
}
|
|
523
|
-
|
|
638
|
+
let holdFrame = false;
|
|
639
|
+
if (holdOpen && refusalEligible) {
|
|
640
|
+
refusalStatus = codexWsPreOutputRefusalStatus(normalized.payload);
|
|
641
|
+
if (refusalStatus !== undefined) {
|
|
642
|
+
// Built before anything settles: a refusal whose metadata cannot be projected within the
|
|
643
|
+
// header bounds is delivered the way it was before, in the committed body.
|
|
644
|
+
try {
|
|
645
|
+
refusalPrelude = heldHeaders ?? metadata?.snapshot() ?? new Headers();
|
|
646
|
+
const declared = normalized.payload.headers;
|
|
647
|
+
refusalHeaders = rejectionHeaders(record(declared) ? declared : {}, refusalPrelude);
|
|
648
|
+
} catch {
|
|
649
|
+
refusalStatus = undefined;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
holdFrame = refusalStatus === undefined && isEmptyCodexWsLifecycleEvent(normalized.payload) && beginHold();
|
|
653
|
+
}
|
|
654
|
+
if (refusalStatus === undefined && !holdFrame) commitResponse();
|
|
655
|
+
else deferredCommit = true;
|
|
656
|
+
// Record the terminal the upstream sent as soon as it is accepted, before the relay-size and
|
|
657
|
+
// queue checks below: a body failure that follows is still an exchange that received it.
|
|
658
|
+
if (!nativeControl && isCodexWsTerminalEvent(type)) {
|
|
659
|
+
terminalEvent = type;
|
|
660
|
+
terminalErrorStatus = type === "error" ? (codexWsWrapperStatus(normalized.payload) ?? null) : null;
|
|
661
|
+
}
|
|
524
662
|
}
|
|
525
663
|
const prefix = encoder.encode(`event: ${type}\ndata: `);
|
|
526
664
|
const suffix = encoder.encode("\n\n");
|
|
@@ -558,6 +696,9 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
558
696
|
markCodexWsStage(committedResponse, stageRecord(null));
|
|
559
697
|
}
|
|
560
698
|
session.release(completedId);
|
|
699
|
+
if (refusalStatus !== undefined && refusalHeaders && refusalPrelude && !responseCommitted) {
|
|
700
|
+
resolveRefusal(refusalStatus, normalized.payload, refusalHeaders, refusalPrelude);
|
|
701
|
+
}
|
|
561
702
|
}
|
|
562
703
|
};
|
|
563
704
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
|
|
2
2
|
import {
|
|
3
|
+
isTransientUpstreamStatus,
|
|
3
4
|
markResponseNonReplayable,
|
|
4
5
|
UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE,
|
|
5
6
|
UPSTREAM_NO_RESPONSE_CODE,
|
|
6
7
|
} from "../../lib/upstream-retry";
|
|
8
|
+
import { isRateLimitOrQuotaFailureMessage, recognizedUpstreamError } from "../../lib/errors";
|
|
7
9
|
import type { RequestFailureCause, RequestFailureStage } from "../../lib/request-failure-model";
|
|
8
10
|
import { packageVersion } from "../../lib/package-version";
|
|
9
11
|
// If the 101 never arrives (network black hole), give SSE a chance well before
|
|
@@ -107,8 +109,209 @@ export type CodexWsStageRecord = Omit<CodexWsFailureStage, "requestBytes"> & {
|
|
|
107
109
|
ocxVersion: string;
|
|
108
110
|
/** Bun runtime version the exchange gated on. */
|
|
109
111
|
bunVersion: string;
|
|
112
|
+
/**
|
|
113
|
+
* Normalized type of the Responses terminal frame that ended a plain exchange, as the upstream
|
|
114
|
+
* sent it and before any tail this proxy synthesizes (#6740). It is what separates an upstream
|
|
115
|
+
* `error` frame from a genuine `response.failed` when both reach a row as the same synthetic 502.
|
|
116
|
+
* Null when no terminal frame arrived, and on native-control exchanges, which can end on an
|
|
117
|
+
* acknowledgement instead. Records written before it existed read as null.
|
|
118
|
+
*/
|
|
119
|
+
terminalEvent: CodexWsTerminalEvent | null;
|
|
120
|
+
/** Wrapper status of a terminal `error` frame ({@link codexWsWrapperStatus}); null otherwise. */
|
|
121
|
+
terminalErrorStatus: number | null;
|
|
110
122
|
};
|
|
111
123
|
|
|
124
|
+
/** The frames a plain exchange ends on, closed so a stage record can never carry free text. */
|
|
125
|
+
export const CODEX_WS_TERMINAL_EVENTS = [
|
|
126
|
+
"response.completed",
|
|
127
|
+
"response.failed",
|
|
128
|
+
"response.incomplete",
|
|
129
|
+
"error",
|
|
130
|
+
] as const;
|
|
131
|
+
export type CodexWsTerminalEvent = typeof CODEX_WS_TERMINAL_EVENTS[number];
|
|
132
|
+
|
|
133
|
+
export function isCodexWsTerminalEvent(value: unknown): value is CodexWsTerminalEvent {
|
|
134
|
+
return typeof value === "string" && (CODEX_WS_TERMINAL_EVENTS as readonly string[]).includes(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The HTTP status a Codex WebSocket `error` frame states on its wrapper, or undefined.
|
|
139
|
+
*
|
|
140
|
+
* The backend wraps a failure as `{type:"error", status|status_code, error}` and the native client
|
|
141
|
+
* turns that status into an HTTP error of the same status
|
|
142
|
+
* (codex-rs/codex-api/src/endpoint/responses_websocket.rs map_wrapped_websocket_error_event). Only
|
|
143
|
+
* that contract is read: a top-level integer in 100..599 under exactly one of the two aliases, the
|
|
144
|
+
* same rule the pre-commit rejection projection applies.
|
|
145
|
+
*/
|
|
146
|
+
export function codexWsWrapperStatus(payload: Record<string, unknown>): number | undefined {
|
|
147
|
+
if (payload.type !== "error") return undefined;
|
|
148
|
+
const hasStatus = Object.hasOwn(payload, "status");
|
|
149
|
+
if (hasStatus === Object.hasOwn(payload, "status_code")) return undefined;
|
|
150
|
+
const status = hasStatus ? payload.status : payload.status_code;
|
|
151
|
+
return typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599
|
|
152
|
+
? status
|
|
153
|
+
: undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function plainRecord(value: unknown): value is Record<string, unknown> {
|
|
157
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** An absent or empty `output` array; anything else may already be visible to the client. */
|
|
161
|
+
function emptyOutput(response: Record<string, unknown>): boolean {
|
|
162
|
+
const output = response.output;
|
|
163
|
+
return output === undefined || (Array.isArray(output) && output.length === 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Upper bound on how long an exchange may hold empty lifecycle frames while it waits to see
|
|
168
|
+
* whether the upstream refuses the turn before any output (#6969).
|
|
169
|
+
*
|
|
170
|
+
* Field rows put 97.5% of authoritative refusals within 5 s of the first Responses event and
|
|
171
|
+
* 98.2% within 10 s, with nothing gained after that.
|
|
172
|
+
*/
|
|
173
|
+
export const CODEX_WS_PRE_OUTPUT_REFUSAL_HOLD_MAX_MS = 10_000;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The hold one send may use: at most `maxMs` from the first empty lifecycle frame, and never past
|
|
177
|
+
* `until`, an absolute time.
|
|
178
|
+
*/
|
|
179
|
+
export interface CodexWsPreOutputRefusalHold {
|
|
180
|
+
maxMs: number;
|
|
181
|
+
until: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The hold for a send starting now. The caller's header deadline (`connectTimeoutMs`) starts no
|
|
186
|
+
* earlier than this, so ending every hold by half of it means a held turn always commits well before
|
|
187
|
+
* that deadline could abort it, however late the first lifecycle frame arrives.
|
|
188
|
+
*/
|
|
189
|
+
export function codexWsPreOutputRefusalHold(connectMs: number, now = Date.now()): CodexWsPreOutputRefusalHold {
|
|
190
|
+
const half = Math.max(0, Math.floor(connectMs / 2));
|
|
191
|
+
return { maxMs: Math.min(CODEX_WS_PRE_OUTPUT_REFUSAL_HOLD_MAX_MS, half), until: now + half };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A lifecycle frame that commits nothing the client could see or act on (#6969): created,
|
|
196
|
+
* in-progress or queued, carrying a response object whose output is absent or empty. A snapshot
|
|
197
|
+
* with any output item, an unknown event, or a frame addressed to another stream is not one.
|
|
198
|
+
*/
|
|
199
|
+
export function isEmptyCodexWsLifecycleEvent(payload: Record<string, unknown>): boolean {
|
|
200
|
+
if (payload.type !== "response.created" && payload.type !== "response.in_progress"
|
|
201
|
+
&& payload.type !== "response.queued") return false;
|
|
202
|
+
if (payload.stream_id !== undefined) return false;
|
|
203
|
+
return plainRecord(payload.response) && emptyOutput(payload.response);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const QUOTA_REFUSAL_IDENTITIES: ReadonlySet<string> = new Set(["usage_limit_reached", "usage_not_included"]);
|
|
207
|
+
|
|
208
|
+
/** Quota and rate-limit identities keep their own routing: every `rate_limit*` and `*quota*` name. */
|
|
209
|
+
function quotaRefusalIdentity(value: string): boolean {
|
|
210
|
+
const name = value.toLowerCase();
|
|
211
|
+
return QUOTA_REFUSAL_IDENTITIES.has(name) || name.startsWith("rate_limit") || name.includes("quota");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The status of an authoritative upstream refusal that ended a turn before any output (#6969),
|
|
216
|
+
* or undefined.
|
|
217
|
+
*
|
|
218
|
+
* Only structured evidence counts: a bare `error` frame's single wrapper status (the alias the
|
|
219
|
+
* native client turns into an HTTP error of that status, see {@link codexWsWrapperStatus}), or,
|
|
220
|
+
* with no wrapper alias at all, the recognized overload class; and a `response.failed` with an
|
|
221
|
+
* empty output whose error is the recognized overload class. The status must be a transient
|
|
222
|
+
* gateway status. Message wording never authorizes anything here; it can only veto, so a 5xx whose
|
|
223
|
+
* error names quota or a rate limit keeps the routing it has today.
|
|
224
|
+
*/
|
|
225
|
+
export function codexWsPreOutputRefusalStatus(payload: Record<string, unknown>): number | undefined {
|
|
226
|
+
if (payload.stream_id !== undefined) return undefined;
|
|
227
|
+
let error: unknown;
|
|
228
|
+
let status: number | undefined;
|
|
229
|
+
if (payload.type === "error") {
|
|
230
|
+
error = payload.error;
|
|
231
|
+
if (error !== undefined && !plainRecord(error)) return undefined;
|
|
232
|
+
const statedAlias = Object.hasOwn(payload, "status") || Object.hasOwn(payload, "status_code");
|
|
233
|
+
status = codexWsWrapperStatus(payload);
|
|
234
|
+
// A malformed or doubled alias is not evidence of anything.
|
|
235
|
+
if (statedAlias && status === undefined) return undefined;
|
|
236
|
+
if (status === undefined && plainRecord(error)) status = recognizedUpstreamError(error)?.httpStatus;
|
|
237
|
+
} else if (payload.type === "response.failed") {
|
|
238
|
+
const response = payload.response;
|
|
239
|
+
if (!plainRecord(response) || !emptyOutput(response)) return undefined;
|
|
240
|
+
error = response.error;
|
|
241
|
+
if (!plainRecord(error)) return undefined;
|
|
242
|
+
status = recognizedUpstreamError(error)?.httpStatus;
|
|
243
|
+
} else {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
if (status === undefined || !isTransientUpstreamStatus(status)) return undefined;
|
|
247
|
+
if (plainRecord(error)) {
|
|
248
|
+
const { code, type, message } = error;
|
|
249
|
+
if ([code, type, message].some(value => value != null && typeof value !== "string")) return undefined;
|
|
250
|
+
if (typeof code === "string" && quotaRefusalIdentity(code)) return undefined;
|
|
251
|
+
if (typeof type === "string" && quotaRefusalIdentity(type)) return undefined;
|
|
252
|
+
if (typeof message === "string" && isRateLimitOrQuotaFailureMessage(message)) return undefined;
|
|
253
|
+
}
|
|
254
|
+
return status;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The response ids a frame names, for the exchange's replay-eligibility latch (#6969): the nested
|
|
259
|
+
* `response.id` and the top-level `response_id`. Null when a present id is not a string, which the
|
|
260
|
+
* latch treats as a conflict.
|
|
261
|
+
*/
|
|
262
|
+
export function codexWsFrameResponseIds(payload: Record<string, unknown>): string[] | null {
|
|
263
|
+
const ids: string[] = [];
|
|
264
|
+
const nested = plainRecord(payload.response) ? payload.response.id : undefined;
|
|
265
|
+
for (const value of [nested, payload.response_id]) {
|
|
266
|
+
if (value === undefined) continue;
|
|
267
|
+
if (typeof value !== "string") return null;
|
|
268
|
+
if (!ids.includes(value)) ids.push(value);
|
|
269
|
+
}
|
|
270
|
+
return ids;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
interface PreOutputRefusalRecord {
|
|
274
|
+
restore: (() => Response) | null;
|
|
275
|
+
outcomeRecorded: boolean;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const preOutputRefusals = new WeakMap<Response, PreOutputRefusalRecord>();
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Register a pre-output refusal projection (#6969): a replayable gateway status standing in for a
|
|
282
|
+
* WebSocket exchange whose upstream refused the turn before any output. `restore` rebuilds the
|
|
283
|
+
* 200 event stream the client would have received without the projection.
|
|
284
|
+
*/
|
|
285
|
+
export function markCodexWsPreOutputRefusal(projection: Response, restore: () => Response): void {
|
|
286
|
+
preOutputRefusals.set(projection, { restore, outcomeRecorded: false });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function isCodexWsPreOutputRefusal(response: Response): boolean {
|
|
290
|
+
return preOutputRefusals.has(response);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Record that this projection's account outcome was written, so no later owner writes it again. */
|
|
294
|
+
export function noteCodexWsPreOutputRefusalOutcome(response: Response): void {
|
|
295
|
+
const record = preOutputRefusals.get(response);
|
|
296
|
+
if (record) record.outcomeRecorded = true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function codexWsPreOutputRefusalOutcomeRecorded(response: Response): boolean {
|
|
300
|
+
return preOutputRefusals.get(response)?.outcomeRecorded === true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The response the client should receive in place of a projection no recovery replaced: the
|
|
305
|
+
* original event stream, byte for byte. Single use; any other response is returned unchanged.
|
|
306
|
+
*/
|
|
307
|
+
export function restoreCodexWsPreOutputRefusal(response: Response): Response {
|
|
308
|
+
const record = preOutputRefusals.get(response);
|
|
309
|
+
const restore = record?.restore;
|
|
310
|
+
if (!record || !restore) return response;
|
|
311
|
+
record.restore = null;
|
|
312
|
+
return restore();
|
|
313
|
+
}
|
|
314
|
+
|
|
112
315
|
const codexWsStageByResponse = new WeakMap<Response, CodexWsStageRecord>();
|
|
113
316
|
|
|
114
317
|
export function markCodexWsStage(response: Response, record: CodexWsStageRecord): void {
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
import { parseRequest } from "../../responses/parser";
|
|
10
10
|
import { externalTaskInputContent } from "../../responses/task-input";
|
|
11
11
|
import { MULTI_AGENT_MODE_HINT_RECOMMENDATION } from "../../codex/multi-agent-mode-policy";
|
|
12
|
+
import { analyzeCodeModeExec } from "./code-mode-collab-surface";
|
|
13
|
+
import { deferredV1Evidence } from "./deferred-collab-surface";
|
|
12
14
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
13
15
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
14
16
|
import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
|
|
@@ -316,26 +318,53 @@ export function isV1CollabSurface(parsed: OcxParsedRequest): boolean {
|
|
|
316
318
|
|
|
317
319
|
|
|
318
320
|
export function collabSurface(parsed: OcxParsedRequest): "v1" | "v2" | null {
|
|
321
|
+
const direct = directCollabSurface(parsed.context.tools);
|
|
322
|
+
// #6844: code-mode-only turns nest the collaboration tools inside `exec`, and tool-search turns
|
|
323
|
+
// defer them entirely. A rendered V1 listing or the deferred-namespace fragment can each infer V1;
|
|
324
|
+
// neither ever yields v2, overrides a different direct surface, or survives a rendered foreign
|
|
325
|
+
// collaboration claim.
|
|
326
|
+
const codeMode = analyzeCodeModeExec(parsed.context.tools);
|
|
327
|
+
if (codeMode.renderedV1 && codeMode.foreignCollabClaim) return null;
|
|
328
|
+
const inferredV1 = codeMode.renderedV1 || deferredV1Evidence(parsed, codeMode);
|
|
329
|
+
if (!inferredV1) return direct.surface;
|
|
330
|
+
if (codeMode.foreignCollabClaim) return null;
|
|
331
|
+
return !direct.sawNativeCollabTool || direct.surface === "v1" ? "v1" : null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function directCollabSurface(tools: OcxParsedRequest["context"]["tools"]): {
|
|
335
|
+
surface: "v1" | "v2" | null;
|
|
336
|
+
sawNativeCollabTool: boolean;
|
|
337
|
+
} {
|
|
319
338
|
let namespacedSpawn = false;
|
|
320
339
|
let flatSpawn = false;
|
|
321
340
|
let v1Only = false;
|
|
322
341
|
let v2Only = false;
|
|
323
|
-
|
|
342
|
+
// Collaboration-named direct tools outside an MCP namespace; an MCP server's own send_message or
|
|
343
|
+
// spawn_agent says nothing about the native surface, matching the nested check.
|
|
344
|
+
let sawNativeCollabTool = false;
|
|
345
|
+
for (const t of tools ?? []) {
|
|
346
|
+
const native = !t.namespace?.startsWith("mcp__");
|
|
324
347
|
if (t.name === "spawn_agent") {
|
|
325
348
|
if (t.namespace) namespacedSpawn = true;
|
|
326
349
|
else flatSpawn = true;
|
|
350
|
+
if (native) sawNativeCollabTool = true;
|
|
327
351
|
} else if (t.name === "send_input" || t.name === "resume_agent" || t.name === "close_agent") {
|
|
328
352
|
v1Only = true;
|
|
353
|
+
if (native) sawNativeCollabTool = true;
|
|
329
354
|
} else if (t.name === "send_message" || t.name === "followup_task" || t.name === "interrupt_agent" || t.name === "list_agents") {
|
|
330
355
|
v2Only = true;
|
|
356
|
+
if (native) sawNativeCollabTool = true;
|
|
331
357
|
}
|
|
332
358
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
359
|
+
const surface = (): "v1" | "v2" | null => {
|
|
360
|
+
if (!namespacedSpawn && !flatSpawn) return null; // no spawn_agent -> no collab surface
|
|
361
|
+
if (namespacedSpawn && flatSpawn) return null; // contradictory spawn shapes
|
|
362
|
+
if (v1Only && v2Only) return null; // contradictory companions
|
|
363
|
+
if (v1Only) return "v1";
|
|
364
|
+
if (v2Only) return "v2";
|
|
365
|
+
return namespacedSpawn ? "v1" : "v2"; // companionless fallbacks (legacy defaults)
|
|
366
|
+
};
|
|
367
|
+
return { surface: surface(), sawNativeCollabTool };
|
|
339
368
|
}
|
|
340
369
|
|
|
341
370
|
|