@bitkyc08/opencodex 2.82.0 → 2.84.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/gui/dist/assets/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +47 -15
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +5 -0
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +26 -1
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +57 -4
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- 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/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +44 -12
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- 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 +5 -4
- package/src/cli/resolve.ts +1 -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/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +47 -17
- 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 +4 -5
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +8 -3
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +17 -3
- package/src/codex/catalog/provider-models.ts +25 -0
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/main-account.ts +6 -7
- 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.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- 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 +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +373 -217
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/inject.ts +245 -221
- package/src/grok/reset-coupon-ledger.ts +293 -57
- package/src/grok/reset-coupons.ts +15 -6
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/shadow-call.ts +44 -5
- 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/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +25 -16
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +3 -1
- package/src/plugins/loader.ts +11 -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-fast.ts +1 -1
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/muse-subscription-usage.ts +1 -1
- 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 +20 -20
- package/src/providers/registry/entries-extended.ts +4 -1
- package/src/providers/registry/model-seeds.ts +20 -3
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/inbound-body-admission.ts +24 -7
- package/src/server/index/link-listener.ts +30 -17
- 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 +21 -2
- package/src/server/index.ts +1 -1
- package/src/server/lifecycle.ts +33 -5
- package/src/server/management/agent-settings-routes.ts +28 -10
- 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/grok-coupon-routes.ts +333 -57
- 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 +92 -390
- package/src/server/management/model-rows.ts +3 -1
- 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 +7 -5
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +4 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +243 -0
- package/src/server/request-log.ts +15 -118
- package/src/server/responses/adapter-continuation.ts +9 -1
- package/src/server/responses/adapter-dispatch.ts +293 -81
- package/src/server/responses/code-mode-collab-surface.ts +205 -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 +129 -50
- 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 +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core.ts +6 -6
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +130 -64
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +131 -13
- package/src/server/responses/run-turn-execution.ts +13 -5
- package/src/server/responses/sidecar-execution.ts +16 -2
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -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 +147 -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 +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/usage/expected-prices.ts +5 -1
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
- package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
- package/gui/dist/assets/index-D3FBCNiP.js +0 -94
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { sanitizeLogMetadataString } from "../../lib/redact";
|
|
2
|
+
import { isOpenAiOperatedResponsesDestination } from "../../providers/openai-tiers";
|
|
3
|
+
import { normalizedProviderEndpoint, PROVIDER_REGISTRY } from "../../providers/registry";
|
|
4
|
+
import type { OcxProviderConfig } from "../../types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* #6675: a custom Responses provider drops replayed plaintext reasoning unless it sets
|
|
8
|
+
* `preserveResponsesReasoningContent`. That default is deliberate (the ChatGPT backend rejects
|
|
9
|
+
* echoed raw reasoning, and forwarding it costs tokens), but it is silent, and the similarly named
|
|
10
|
+
* Chat-wire `preserveReasoningContentModels` does not opt in. Say so once per destination.
|
|
11
|
+
*
|
|
12
|
+
* Only a hand-configured `openai-responses` row with key auth qualifies. Registry presets (matched
|
|
13
|
+
* by endpoint on either OpenAI wire, since a preset may be routed to Responses per model), OAuth and
|
|
14
|
+
* forward rows, Azure, and OpenAI-operated destinations are skipped: their replay contract is
|
|
15
|
+
* curated, so blanking there is a decision rather than a missing setting. The notice carries only
|
|
16
|
+
* the destination host and option names, never request content.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_NOTICED_DESTINATIONS = 64;
|
|
19
|
+
const noticedDestinations = new Set<string>();
|
|
20
|
+
|
|
21
|
+
function destinationHost(baseUrl: unknown): string | undefined {
|
|
22
|
+
if (typeof baseUrl !== "string") return undefined;
|
|
23
|
+
try {
|
|
24
|
+
return sanitizeLogMetadataString(new URL(baseUrl).host, 128);
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Azure OpenAI and Azure AI Foundry serve OpenAI-operated models under per-resource hosts. */
|
|
31
|
+
const AZURE_OPENAI_HOST_SUFFIXES = [".openai.azure.com", ".services.ai.azure.com", ".cognitiveservices.azure.com"];
|
|
32
|
+
|
|
33
|
+
function hostnameOf(url: string): string | undefined {
|
|
34
|
+
try {
|
|
35
|
+
return new URL(url).hostname.toLowerCase();
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A templated preset (`https://{resource}.openai.azure.com/...`) matches by host pattern. */
|
|
42
|
+
function templatedHostMatches(template: string, hostname: string): boolean {
|
|
43
|
+
const host = /^[a-z][a-z0-9+.-]*:\/\/([^/?#]+)/i.exec(template)?.[1];
|
|
44
|
+
if (!host || !/\{[^}]*\}/.test(host)) return false;
|
|
45
|
+
const pattern = host.toLowerCase().split(/\{[^}]*\}/).map(part => part.replace(/[.*+?^$()|[\]\\]/g, "\\$&")).join("[^.]+");
|
|
46
|
+
return new RegExp(`^${pattern}$`).test(hostname);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isRegistryDestination(baseUrl: string): boolean {
|
|
50
|
+
const endpoint = normalizedProviderEndpoint(baseUrl);
|
|
51
|
+
const hostname = hostnameOf(baseUrl);
|
|
52
|
+
if (hostname && AZURE_OPENAI_HOST_SUFFIXES.some(suffix => hostname.endsWith(suffix))) return true;
|
|
53
|
+
return PROVIDER_REGISTRY.some(entry =>
|
|
54
|
+
normalizedProviderEndpoint(entry.baseUrl) === endpoint
|
|
55
|
+
|| (hostname !== undefined && templatedHostMatches(entry.baseUrl, hostname))
|
|
56
|
+
|| (entry.destinationAliases ?? []).some(alias => normalizedProviderEndpoint(alias.baseUrl) === endpoint));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isCustomResponsesProvider(provider: OcxProviderConfig): boolean {
|
|
60
|
+
if (provider.adapter !== "openai-responses") return false;
|
|
61
|
+
if (provider.authMode !== undefined && provider.authMode !== "key") return false;
|
|
62
|
+
if (typeof provider.baseUrl !== "string" || !provider.baseUrl) return false;
|
|
63
|
+
return !isOpenAiOperatedResponsesDestination(provider) && !isRegistryDestination(provider.baseUrl);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function plaintextReasoningDroppedNotice(host: string): string {
|
|
67
|
+
return `[opencodex] Replayed plaintext reasoning was dropped for the custom Responses provider at ${host}. `
|
|
68
|
+
+ "If that endpoint accepts reasoning_text replay, set \"preserveResponsesReasoningContent\": true on the provider; "
|
|
69
|
+
+ "\"preserveReasoningContentModels\" applies only to the Chat wire.";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function notePlaintextReasoningDropped(
|
|
73
|
+
provider: OcxProviderConfig,
|
|
74
|
+
warn: (message: string) => void = message => console.warn(message),
|
|
75
|
+
): void {
|
|
76
|
+
if (!isCustomResponsesProvider(provider)) return;
|
|
77
|
+
const host = destinationHost(provider.baseUrl);
|
|
78
|
+
if (!host || noticedDestinations.has(host) || noticedDestinations.size >= MAX_NOTICED_DESTINATIONS) return;
|
|
79
|
+
noticedDestinations.add(host);
|
|
80
|
+
warn(plaintextReasoningDroppedNotice(host));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resetPlaintextReasoningNoticesForTests(): void {
|
|
84
|
+
noticedDestinations.clear();
|
|
85
|
+
}
|
|
@@ -78,6 +78,11 @@ export function sanitizeReasoningInputContent(
|
|
|
78
78
|
* placeholder, rather than emitted in the shape the upstream 400s on.
|
|
79
79
|
*/
|
|
80
80
|
requirePlaintextReasoning?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Called once per sanitized body when a reasoning item that carried nonempty plaintext
|
|
83
|
+
* `reasoning_text` was blanked. Observability only; it never changes the body (#6675).
|
|
84
|
+
*/
|
|
85
|
+
onPlaintextReasoningBlanked?: () => void;
|
|
81
86
|
},
|
|
82
87
|
): unknown {
|
|
83
88
|
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
@@ -85,6 +90,7 @@ export function sanitizeReasoningInputContent(
|
|
|
85
90
|
if (!Array.isArray(raw.input)) return body;
|
|
86
91
|
|
|
87
92
|
let changed = false;
|
|
93
|
+
let blankedPlaintext = false;
|
|
88
94
|
const input = raw.input.map(item => {
|
|
89
95
|
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
90
96
|
const rec = item as Record<string, unknown>;
|
|
@@ -148,11 +154,15 @@ export function sanitizeReasoningInputContent(
|
|
|
148
154
|
// guide merges reasoning items into the adjacent assistant message), so providers flagged
|
|
149
155
|
// `preserveResponsesReasoningContent` keep it — deleting valid replay content there breaks
|
|
150
156
|
// continuations after tool calls (issue #875 family).
|
|
151
|
-
if (blankContent)
|
|
157
|
+
if (blankContent) {
|
|
158
|
+
if (hasPlaintextReasoningContent(rec.content)) blankedPlaintext = true;
|
|
159
|
+
next.content = [];
|
|
160
|
+
}
|
|
152
161
|
if (missingPlaintextReasoning) next.content = plaintextReasoningBackfill(next);
|
|
153
162
|
return next;
|
|
154
163
|
});
|
|
155
164
|
|
|
165
|
+
if (blankedPlaintext) opts?.onPlaintextReasoningBlanked?.();
|
|
156
166
|
return changed ? { ...raw, input } : body;
|
|
157
167
|
}
|
|
158
168
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../../responses/compaction";
|
|
2
|
+
import { COMPACT_PROMPT, appendCheckpointContinuationNote, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../../responses/compaction";
|
|
3
3
|
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
4
4
|
import { modelInList } from "../../types";
|
|
5
5
|
import { isPlainObject } from "./internal";
|
|
@@ -260,10 +260,12 @@ export function scrubOcxCompactionItems(
|
|
|
260
260
|
body: unknown,
|
|
261
261
|
destinationDecodesNativeBlob: boolean,
|
|
262
262
|
threadServingIdentityChanged: boolean,
|
|
263
|
+
addContinuationNote = false,
|
|
263
264
|
): unknown {
|
|
264
265
|
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
265
266
|
|
|
266
267
|
let changed = false;
|
|
268
|
+
let decodedCheckpoint = false;
|
|
267
269
|
const input = body.input.map(item => {
|
|
268
270
|
if (!isPlainObject(item) || !isCompactionItemType(item.type)) return item;
|
|
269
271
|
const encrypted = typeof item.encrypted_content === "string" ? item.encrypted_content : undefined;
|
|
@@ -274,6 +276,7 @@ export function scrubOcxCompactionItems(
|
|
|
274
276
|
&& !threadServingIdentityChanged
|
|
275
277
|
) return item;
|
|
276
278
|
changed = true;
|
|
279
|
+
if (decodeCompactionSummary(encrypted) !== null) decodedCheckpoint = true;
|
|
277
280
|
return {
|
|
278
281
|
type: "message",
|
|
279
282
|
role: "user",
|
|
@@ -281,5 +284,10 @@ export function scrubOcxCompactionItems(
|
|
|
281
284
|
};
|
|
282
285
|
});
|
|
283
286
|
|
|
284
|
-
|
|
287
|
+
if (!changed) return body;
|
|
288
|
+
// #6764: a decoded checkpoint is plain user text from here on, so name its role in the
|
|
289
|
+
// instructions. Compaction requests skip this; the summarizer must not be told to continue.
|
|
290
|
+
return addContinuationNote && decodedCheckpoint
|
|
291
|
+
? { ...body, input, instructions: appendCheckpointContinuationNote(body.instructions) }
|
|
292
|
+
: { ...body, input };
|
|
285
293
|
}
|
|
@@ -5,7 +5,7 @@ import { mapReasoningEffort } from "../../reasoning-effort";
|
|
|
5
5
|
import { buildSystemPrompt, MAX_TOOL_BLOCK_STARTS } from "../coding-agent/protocol";
|
|
6
6
|
import { buildCodingAgentToolCatalog, CodingAgentToolCatalogError } from "../coding-agent/tool-catalog";
|
|
7
7
|
import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps, type CodingAgentToolBridgeInput } from "../coding-agent/turn";
|
|
8
|
-
import { QODER_PROFILES, type QoderProfile } from "./profiles";
|
|
8
|
+
import { QODER_PROFILES, resolveQoderProfile, type QoderProfile } from "./profiles";
|
|
9
9
|
import { QoderScaffoldFilter, QODER_SCAFFOLD_ERROR_CODE, qoderScaffoldErrorMessage } from "./scaffold-guard";
|
|
10
10
|
|
|
11
11
|
export type QoderAdapterDeps = CodingAgentDeps;
|
|
@@ -114,19 +114,21 @@ export function createQoderAdapter(provider: OcxProviderConfig, deps: QoderAdapt
|
|
|
114
114
|
yield { type: "error", message: "Qoder adapter uses runTurn; the fetch/parseStream path is disabled." };
|
|
115
115
|
},
|
|
116
116
|
async runTurn(parsed, incoming, emit): Promise<void> {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
117
|
+
if (resolveQoderProfile(provider.baseUrl)?.region === "cn") {
|
|
118
|
+
const hasImage = parsed.context.messages.some(message =>
|
|
119
|
+
Array.isArray(message.content) && message.content.some(part => part.type === "image"),
|
|
120
|
+
);
|
|
121
|
+
if (hasImage) {
|
|
122
|
+
emit({
|
|
123
|
+
type: "error",
|
|
124
|
+
message: "Qoder image input is not enabled because the CLI provider route has no verified multimodal contract.",
|
|
125
|
+
status: 400,
|
|
126
|
+
errorType: "invalid_request_error",
|
|
127
|
+
code: "unsupported_input_modality",
|
|
128
|
+
retryable: false,
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
130
132
|
}
|
|
131
133
|
let catalog: ReturnType<typeof buildCodingAgentToolCatalog>;
|
|
132
134
|
try {
|
package/src/bridge/errors.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
cyberPolicyErrorType,
|
|
13
13
|
CYBER_POLICY_ERROR_CODE,
|
|
14
14
|
isCyberPolicyCode,
|
|
15
|
+
SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE,
|
|
15
16
|
type OcxErrorPayload,
|
|
16
17
|
} from "../lib/errors";
|
|
17
18
|
|
|
@@ -22,6 +23,10 @@ export function formatErrorResponse(
|
|
|
22
23
|
options?: { code?: string | null; retryAfter?: string | null },
|
|
23
24
|
): Response {
|
|
24
25
|
const error = classifyError(status, type, message);
|
|
26
|
+
if (options?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) {
|
|
27
|
+
error.type = "server_error";
|
|
28
|
+
error.code = SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
|
|
29
|
+
}
|
|
25
30
|
if (isCyberPolicyCode(options?.code)) {
|
|
26
31
|
error.code = CYBER_POLICY_ERROR_CODE;
|
|
27
32
|
error.type = cyberPolicyErrorType(type);
|
|
@@ -36,7 +41,8 @@ export function formatErrorResponse(
|
|
|
36
41
|
// that would hand the client a 5xx it is configured to retry four times.
|
|
37
42
|
const finalStatus = error.code === CYBER_POLICY_ERROR_CODE
|
|
38
43
|
? 400
|
|
39
|
-
:
|
|
44
|
+
: error.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? 503
|
|
45
|
+
: isReplayRefusalCode(error.code) ? REPLAY_REFUSED_STATUS : status;
|
|
40
46
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
41
47
|
const retryAfter = options?.retryAfter?.trim();
|
|
42
48
|
if (error.code !== CYBER_POLICY_ERROR_CODE
|
|
@@ -62,3 +68,22 @@ export function formatErrorResponse(
|
|
|
62
68
|
if (refusal) retainReplayRefusal(response);
|
|
63
69
|
return response;
|
|
64
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A spent usage window, in the shape the Codex client treats as final (#6991): HTTP 429 with
|
|
74
|
+
* `error.type: "usage_limit_reached"` and, when the reset is known, `resets_at` in epoch
|
|
75
|
+
* seconds. Codex stops (and shows the reset time when present) instead of backing off through its
|
|
76
|
+
* retry budget, which is what it does with `rate_limit_exceeded`. No `plan_type` is sent: Codex
|
|
77
|
+
* would read it as a ChatGPT plan and print that plan's upgrade advice. `formatErrorResponse`
|
|
78
|
+
* cannot build this, because every 429 it classifies becomes `rate_limit_exceeded` and its
|
|
79
|
+
* payload has no `resets_at`.
|
|
80
|
+
*/
|
|
81
|
+
export function formatUsageLimitReachedResponse(message: string, resetsAtMs: number | null, retryAfterSeconds: number | null): Response {
|
|
82
|
+
const error = {
|
|
83
|
+
message, type: "usage_limit_reached", code: "usage_limit_reached",
|
|
84
|
+
...(resetsAtMs !== null ? { resets_at: Math.ceil(resetsAtMs / 1000) } : {}),
|
|
85
|
+
};
|
|
86
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
87
|
+
if (retryAfterSeconds !== null) headers.set("Retry-After", String(retryAfterSeconds));
|
|
88
|
+
return new Response(JSON.stringify({ error }), { status: 429, headers });
|
|
89
|
+
}
|
package/src/bridge.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { adapterFailureFromMessage } from "./lib/errors";
|
|
2
2
|
|
|
3
|
-
export { formatErrorResponse } from "./bridge/errors";
|
|
3
|
+
export { formatErrorResponse, formatUsageLimitReachedResponse } from "./bridge/errors";
|
|
4
4
|
export { setOwnedBudgetAbandonedMsForTests } from "./bridge/internal";
|
|
5
5
|
export { buildResponseJSON } from "./bridge/response-json";
|
|
6
6
|
export { bridgeToResponsesSSE } from "./bridge/sse";
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* adapter-reported CatalogModel.contextWindow / the Anthropic registry for passthrough ids);
|
|
11
11
|
* nothing is guessed.
|
|
12
12
|
*/
|
|
13
|
-
import { aliasForNative, aliasForRoute, currentClaudeAliasSpelling, legacyAliasForNative, legacyAliasForRoute } from "./alias";
|
|
13
|
+
import { aliasForNative, aliasForRoute, currentClaudeAliasSpelling, legacyAliasForNative, legacyAliasForRoute, resolveAlias } from "./alias";
|
|
14
14
|
import { desktop3pAlias } from "./desktop-3p";
|
|
15
15
|
import { nativeOpenAiContextWindow, type CatalogModel, type NativeContextLimitsInput } from "../codex/catalog";
|
|
16
16
|
import { ANTHROPIC_MODEL_CONTEXT_WINDOWS } from "../providers/registry/model-seeds";
|
|
@@ -20,13 +20,13 @@ import { AUTO_COMPACT_WINDOW_DEFAULT, AUTO_CONTEXT_FLOOR, ONE_MILLION } from "./
|
|
|
20
20
|
export { AUTO_COMPACT_WINDOW_DEFAULT, AUTO_CONTEXT_FLOOR } from "./long-context";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* The native id each Claude Code tier alias resolves to (2.1.
|
|
24
|
-
* `claude-opus-5-5`, `sonnet` sends `claude-sonnet-5`, `fable` sends `claude-fable-5-1`).
|
|
23
|
+
* The native id each Claude Code tier alias resolves to (2.1.296: `--model opus` sends
|
|
24
|
+
* `claude-opus-5-5`, `sonnet` sends `claude-sonnet-5-5`, `fable` sends `claude-fable-5-1`).
|
|
25
25
|
* Behind a gateway Claude Code accounts an unmarked id at 200k, so an unset tier slot is filled
|
|
26
26
|
* with this id when the map marks it (#5755). A marked alias cannot stand in: Claude Code sends
|
|
27
27
|
* `sonnet[1m]` upstream unresolved. Haiku has no entry; its 200k window never marks.
|
|
28
28
|
*/
|
|
29
|
-
const CLAUDE_CODE_NATIVE_TIERS = { opus: "claude-opus-5-5", sonnet: "claude-sonnet-5", fable: "claude-fable-5-1" } as const;
|
|
29
|
+
const CLAUDE_CODE_NATIVE_TIERS = { opus: "claude-opus-5-5", sonnet: "claude-sonnet-5-5", fable: "claude-fable-5-1" } as const;
|
|
30
30
|
|
|
31
31
|
/** Binary-verified accepted range for CLAUDE_CODE_AUTO_COMPACT_WINDOW (2.1.207: pSo=1e5, yDs=1e6). */
|
|
32
32
|
export const AUTO_COMPACT_WINDOW_MIN = 100_000;
|
|
@@ -277,6 +277,59 @@ export function effectiveModelEnv(
|
|
|
277
277
|
return out;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Tier slots Claude Code pairs with a `<slot>_NAME` label (#6926). Its /model tier row is
|
|
282
|
+
* `label: <slot>_NAME ?? <first-party display name> ?? <slot value>` (2.1.296), and an
|
|
283
|
+
* `ocx-claude-*` alias is not a first-party id, so without the label the row prints the alias.
|
|
284
|
+
* The alias itself stays the submitted id: saved selections and inbound resolution depend on it.
|
|
285
|
+
*/
|
|
286
|
+
export const CLAUDE_TIER_LABEL_SLOTS = [
|
|
287
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
288
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
289
|
+
"ANTHROPIC_DEFAULT_FABLE_MODEL",
|
|
290
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
291
|
+
] as const;
|
|
292
|
+
|
|
293
|
+
export type ClaudeTierLabelSlot = (typeof CLAUDE_TIER_LABEL_SLOTS)[number];
|
|
294
|
+
|
|
295
|
+
export function isClaudeTierLabelSlot(name: string): name is ClaudeTierLabelSlot {
|
|
296
|
+
return (CLAUDE_TIER_LABEL_SLOTS as readonly string[]).includes(name);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The picker label for a tier slot holding an OpenCodex alias, spelled like the discovery
|
|
301
|
+
* row's `display_name` (model-info.ts): `<slug> (native)`, `<model> (<provider>)` and a
|
|
302
|
+
* ` · 1M` variant. The native-namespaced `claude-*` selector is the Anthropic Fable picker
|
|
303
|
+
* alias, which discovery labels `(anthropic)`. A trailing `--fast` stays in the model name:
|
|
304
|
+
* without the catalog this cannot tell a synthetic Fast sibling from a real model whose id ends
|
|
305
|
+
* in `--fast`, and naming the exact model beats a prettier label that could name the wrong one.
|
|
306
|
+
* Anything that is not an alias (a first-party id, `provider/model`, a bare slug) gets no
|
|
307
|
+
* label: Claude Code either names it itself or the id is already what the user typed.
|
|
308
|
+
*/
|
|
309
|
+
export function claudeTierDisplayName(selector: string): string | undefined {
|
|
310
|
+
const trimmed = selector.trim();
|
|
311
|
+
const oneMillion = hasOneMillionMarker(trimmed);
|
|
312
|
+
const route = resolveAlias(stripOneMillionMarker(trimmed));
|
|
313
|
+
if (route === null) return undefined;
|
|
314
|
+
const slash = route.indexOf("/");
|
|
315
|
+
let provider = slash > 0 ? route.slice(0, slash) : "native";
|
|
316
|
+
const model = slash > 0 ? route.slice(slash + 1) : route;
|
|
317
|
+
if (!model) return undefined;
|
|
318
|
+
if (provider === "native" && model.startsWith("claude-")) provider = "anthropic";
|
|
319
|
+
return `${model} (${provider})${oneMillion ? " · 1M" : ""}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** `{ "<slot>_NAME": label }` for each tier slot in `env` that holds an OpenCodex alias. */
|
|
323
|
+
export function claudeTierDisplayNameEnv(env: Readonly<Record<string, string | undefined>>): Record<string, string> {
|
|
324
|
+
const out: Record<string, string> = {};
|
|
325
|
+
for (const slot of CLAUDE_TIER_LABEL_SLOTS) {
|
|
326
|
+
const value = env[slot];
|
|
327
|
+
const label = value ? claudeTierDisplayName(value) : undefined;
|
|
328
|
+
if (label !== undefined) out[`${slot}_NAME`] = label;
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
|
|
280
333
|
/** Shared 3s bounded acquisition (audit R4#3) for context-window sources. */
|
|
281
334
|
export async function boundedContextWindows(
|
|
282
335
|
acquire: () => Promise<Record<string, number>>,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Claude Desktop picker mode: the one server-side controller for every picker mutation.
|
|
3
3
|
*
|
|
4
4
|
* While a server runs, enable, disable and Desktop mode transitions run here, serialized by one
|
|
5
|
-
* lock (devlog/
|
|
5
|
+
* lock (devlog/_fin/260924_claude_desktop_picker_mode/030_wp4_picker_activation.md, D11).
|
|
6
6
|
*/
|
|
7
7
|
import type { OcxConfig } from "../types";
|
|
8
8
|
import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
|
|
@@ -113,12 +113,19 @@ export function isLoopbackTarget(host: string): boolean {
|
|
|
113
113
|
return /^(0x[0-9a-f]+|\d+)(\.(0x[0-9a-f]+|\d+))*$/.test(host);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
function respond(
|
|
116
|
+
function respond(
|
|
117
|
+
socket: Socket, status: number, reason: string, headers: Readonly<Record<string, string>> = {}, body = "",
|
|
118
|
+
): void {
|
|
117
119
|
if (socket.destroyed) return;
|
|
118
120
|
const extra = Object.entries(headers).map(([name, value]) => `${name}: ${value}\r\n`).join("");
|
|
119
|
-
|
|
121
|
+
const type = body ? "Content-Type: text/plain; charset=utf-8\r\n" : "";
|
|
122
|
+
socket.end(`HTTP/1.1 ${status} ${reason}\r\n${extra}${type}Connection: close\r\nContent-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
// Plain proxied HTTP (often loopback MCP traffic missing a NO_PROXY bypass) lands here; say why.
|
|
126
|
+
const CONNECT_ONLY_BODY = "OpenCodex Claude intercept proxy accepts only CONNECT tunnels. "
|
|
127
|
+
+ "Exclude this host via NO_PROXY (loopback and unspecified hosts: 127.0.0.1,localhost,::1,0.0.0.0).\n";
|
|
128
|
+
|
|
122
129
|
function splice(client: Socket, upstream: Socket, pending: Uint8Array): void {
|
|
123
130
|
const teardown = () => {
|
|
124
131
|
client.destroy();
|
|
@@ -173,7 +180,7 @@ function handleConnection(socket: Socket, options: ResolvedConnectProxyOptions):
|
|
|
173
180
|
// Bytes after the head belong to the tunnel (a client may pipeline its TLS ClientHello).
|
|
174
181
|
let pending = head.subarray(end + 4);
|
|
175
182
|
if (!target) {
|
|
176
|
-
respond(socket, 405, "Method Not Allowed");
|
|
183
|
+
respond(socket, 405, "Method Not Allowed", { Allow: "CONNECT" }, CONNECT_ONLY_BODY);
|
|
177
184
|
return;
|
|
178
185
|
}
|
|
179
186
|
if (options.authToken !== undefined) {
|
|
@@ -22,6 +22,7 @@ export function canonicalPickerConfigDir(configDir: string): string {
|
|
|
22
22
|
/** Public records still cannot follow symlinks or accept oversized/malformed recovery data. */
|
|
23
23
|
function readState(path: string, configId: string, initialization: boolean): Initialization | AuthorityMetadata | null {
|
|
24
24
|
// BigInt stats: a Windows file ID exceeds 2^53, so Number ino loses low bits and two files can compare equal.
|
|
25
|
+
// Refuse zero identity or metadata changes between the path snapshot and opened descriptor.
|
|
25
26
|
let expected: BigIntStats;
|
|
26
27
|
try { expected = lstatSync(path, { bigint: true }); }
|
|
27
28
|
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw new Error("picker_ca_metadata_unsafe"); }
|
|
@@ -31,7 +32,8 @@ function readState(path: string, configId: string, initialization: boolean): Ini
|
|
|
31
32
|
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw new Error("picker_ca_metadata_unsafe"); }
|
|
32
33
|
try {
|
|
33
34
|
const stat = fstatSync(fd, { bigint: true });
|
|
34
|
-
if (stat.dev !== expected.dev || stat.ino !== expected.ino
|
|
35
|
+
if (stat.dev !== expected.dev || stat.ino !== expected.ino || stat.ino === 0n
|
|
36
|
+
|| stat.ctimeNs !== expected.ctimeNs) throw new Error("picker_ca_metadata_unsafe");
|
|
35
37
|
if (!stat.isFile() || stat.nlink !== 1n || stat.size > BigInt(MAX_PUBLIC_STATE_BYTES)
|
|
36
38
|
|| (process.platform !== "win32" && stat.uid !== BigInt(process.getuid!()))) throw new Error();
|
|
37
39
|
const bytes = Buffer.alloc(MAX_PUBLIC_STATE_BYTES + 1);
|
|
@@ -34,6 +34,11 @@ export interface PickerListenerOptions {
|
|
|
34
34
|
maxEncodedBytes?: number;
|
|
35
35
|
/** Test seam: concurrent upstream requests; production uses PICKER_MAX_ACTIVE_UPSTREAMS. */
|
|
36
36
|
maxActiveUpstreams?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Test seam: arms the drop of a connection whose backend ended, after PICKER_ABANDONED_INPUT_LINGER_MS;
|
|
39
|
+
* returns a cancel function. Production uses a timer.
|
|
40
|
+
*/
|
|
41
|
+
scheduleAbandonedInputDrop?: (drop: () => void, ms: number) => () => void;
|
|
37
42
|
log?: (line: string) => void;
|
|
38
43
|
}
|
|
39
44
|
export interface PickerListenerHandle { port: number; close(): Promise<void> }
|
|
@@ -51,6 +56,12 @@ export const PICKER_MAX_ACTIVE_UPSTREAMS = 256;
|
|
|
51
56
|
export const PICKER_MAX_CONCURRENT_STREAMS = 100;
|
|
52
57
|
/** A connection that has not sent a complete ClientHello by then is dropped. */
|
|
53
58
|
const CLIENT_HELLO_TIMEOUT_MS = 10_000;
|
|
59
|
+
/**
|
|
60
|
+
* Once the backend has ended a connection and its reply has been flushed, input the client still
|
|
61
|
+
* sends is discarded for at most this long before the connection is dropped. A cooperative client
|
|
62
|
+
* closes as soon as it reads a Connection: close reply; this only bounds one that keeps uploading.
|
|
63
|
+
*/
|
|
64
|
+
export const PICKER_ABANDONED_INPUT_LINGER_MS = 30_000;
|
|
54
65
|
|
|
55
66
|
const HOP_HEADERS = new Set([
|
|
56
67
|
"connection", "keep-alive", "proxy-connection", "proxy-authenticate", "proxy-authorization",
|
|
@@ -152,6 +163,10 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
|
|
|
152
163
|
const upstream = options.upstream ?? { host: "claude.ai", port: 443, servername: "claude.ai" };
|
|
153
164
|
const cap = options.maxEncodedBytes ?? BOOTSTRAP_MAX_ENCODED_BYTES;
|
|
154
165
|
const maxActiveUpstreams = options.maxActiveUpstreams ?? PICKER_MAX_ACTIVE_UPSTREAMS;
|
|
166
|
+
const scheduleAbandonedInputDrop = options.scheduleAbandonedInputDrop ?? ((drop: () => void, ms: number) => {
|
|
167
|
+
const timer = setTimeout(drop, ms);
|
|
168
|
+
return () => clearTimeout(timer);
|
|
169
|
+
});
|
|
155
170
|
let activeUpstreams = 0;
|
|
156
171
|
const upgrades = new Set<Duplex>();
|
|
157
172
|
const spliced = new Set<Socket>();
|
|
@@ -205,8 +220,18 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
|
|
|
205
220
|
} else req.destroy();
|
|
206
221
|
}
|
|
207
222
|
};
|
|
223
|
+
// An HTTP/1.1 reply sent while the upload is unfinished advertises Connection: close. Bun 1.4.3
|
|
224
|
+
// ends such a response with socket.destroySoon() before our finish listeners run, which resets
|
|
225
|
+
// the splice before the reply reaches the client; keep it a graceful FIN that discards input.
|
|
226
|
+
// Only that closing socket is changed, and destroy() stays available for errors and shutdown.
|
|
227
|
+
const closesUnfinishedUpload = () => !h2 && uploading && !req.complete;
|
|
228
|
+
const keepClosingReplyIntact = () => {
|
|
229
|
+
const socket = req.socket;
|
|
230
|
+
socket.destroySoon = () => { req.resume(); socket.end(); return socket; };
|
|
231
|
+
};
|
|
208
232
|
// Refusals answer with an empty response and a fixed log line that carries no request data.
|
|
209
233
|
const refuse = (status: 400 | 503) => {
|
|
234
|
+
if (closesUnfinishedUpload()) keepClosingReplyIntact();
|
|
210
235
|
// Do not leave a refused, still-uploading stream behind after delivering its empty reply.
|
|
211
236
|
if (uploading && !req.complete) {
|
|
212
237
|
req.once("error", () => res.destroy());
|
|
@@ -236,6 +261,7 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
|
|
|
236
261
|
if (clientGone) return;
|
|
237
262
|
if (res.headersSent) res.destroy();
|
|
238
263
|
else {
|
|
264
|
+
if (closesUnfinishedUpload()) keepClosingReplyIntact();
|
|
239
265
|
res.writeHead(502, { "Content-Length": "0", ...(!h2 && uploading && !req.complete ? { Connection: "close" } : {}) });
|
|
240
266
|
res.end(); log(502);
|
|
241
267
|
}
|
|
@@ -260,7 +286,10 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
|
|
|
260
286
|
// HTTP/2 has no reason phrase; its compat writeHead takes the same flat raw header array.
|
|
261
287
|
if (h2) (relayRes as Http2ServerResponse).writeHead(status, raw as unknown as Record<string, string>);
|
|
262
288
|
else {
|
|
263
|
-
if (uploading && !req.complete)
|
|
289
|
+
if (uploading && !req.complete) {
|
|
290
|
+
keepClosingReplyIntact();
|
|
291
|
+
raw.push("Connection", "close");
|
|
292
|
+
}
|
|
264
293
|
res.writeHead(status, upRes.statusMessage, raw);
|
|
265
294
|
}
|
|
266
295
|
log(status);
|
|
@@ -438,6 +467,8 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
|
|
|
438
467
|
const offersH2 = clientHelloOffersH2(head);
|
|
439
468
|
if (offersH2 === null) return;
|
|
440
469
|
client.off("data", onData);
|
|
470
|
+
// Keep reading an aborted upload after the backend reply FIN.
|
|
471
|
+
client.allowHalfOpen = true;
|
|
441
472
|
// Paused, a FIN or later bytes wait in the readable buffer and follow `head` once piped.
|
|
442
473
|
client.pause();
|
|
443
474
|
clearTimeout(deadline);
|
|
@@ -450,6 +481,23 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
|
|
|
450
481
|
// its peer (reset, dial failure) tears the peer down instead of leaving it open.
|
|
451
482
|
inner.once("close", () => { spliced.delete(inner); if (!client.writableEnded) client.destroy(); });
|
|
452
483
|
client.once("close", () => { if (!inner.writableEnded) inner.destroy(); });
|
|
484
|
+
// The backend reply ended; discard ciphertext still coming from the aborted upload.
|
|
485
|
+
// Unpipe before resuming, so backpressure cannot hide the peer FIN. The discard is bounded
|
|
486
|
+
// from the moment the reply has been flushed to the socket (finish), never before, so the
|
|
487
|
+
// drop cannot cut a reply still being written; finish is not peer receipt, so a peer that
|
|
488
|
+
// also stops reading for the whole budget can lose unread bytes. A peer that keeps sending
|
|
489
|
+
// past the budget is dropped instead of holding the connection until shutdown (#6984).
|
|
490
|
+
inner.once("end", () => {
|
|
491
|
+
client.unpipe(inner);
|
|
492
|
+
client.resume();
|
|
493
|
+
const linger = () => {
|
|
494
|
+
if (client.destroyed) return;
|
|
495
|
+
const cancel = scheduleAbandonedInputDrop(() => client.destroy(), PICKER_ABANDONED_INPUT_LINGER_MS);
|
|
496
|
+
client.once("close", cancel);
|
|
497
|
+
};
|
|
498
|
+
if (client.writableFinished) linger();
|
|
499
|
+
else client.once("finish", linger);
|
|
500
|
+
});
|
|
453
501
|
inner.once("connect", () => {
|
|
454
502
|
inner.setNoDelay(true);
|
|
455
503
|
client.setNoDelay(true);
|