@bitkyc08/opencodex 2.56.0 → 2.58.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
- package/src/adapters/command-code.ts +12 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +25 -7
- package/src/adapters/openai-chat.ts +8 -8
- package/src/adapters/openai-responses/passthrough.ts +62 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +8 -2
- package/src/bridge/sse.ts +20 -2
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +32 -4
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +92 -7
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +74 -5
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-store.ts +113 -26
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/login-flow.ts +14 -2
- package/src/codex/auth-api/reset-credit-service.ts +11 -2
- package/src/codex/auth-context.ts +199 -15
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/model-visibility.ts +1 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/retained-sync.ts +9 -1
- package/src/codex/catalog/routed-gather.ts +38 -1
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +231 -32
- package/src/codex/inject.ts +12 -16
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/pool-refresh-backoff.ts +12 -3
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +10 -0
- package/src/codex/routing/selection.ts +79 -2
- package/src/codex/routing/thread-affinity.ts +50 -2
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +29 -49
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/atomic-write.ts +83 -8
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +272 -180
- package/src/images/loop.ts +1 -1
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +147 -21
- package/src/lib/spend-reservation-ledger.ts +18 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +77 -10
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/derive.ts +6 -0
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +35 -7
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +59 -28
- package/src/providers/registry/model-seeds.ts +71 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/live-sideband.ts +37 -1
- package/src/server/index/websocket-handler.ts +54 -3
- package/src/server/index.ts +5 -5
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log.ts +127 -3
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +74 -0
- package/src/server/responses/adapter-continuation.ts +33 -7
- package/src/server/responses/adapter-delivery.ts +5 -11
- package/src/server/responses/adapter-dispatch.ts +84 -13
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +54 -13
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-codex-account.ts +51 -3
- package/src/server/responses/core-combo.ts +129 -23
- package/src/server/responses/core-errors.ts +18 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/core-replay.ts +105 -32
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +30 -6
- package/src/server/responses/passthrough-dispatch.ts +61 -11
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +173 -22
- package/src/server/responses/request-send-budget.ts +97 -2
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +62 -3
- package/src/server/responses/run-turn-execution.ts +59 -31
- package/src/server/responses/sidecar-execution.ts +7 -13
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -1
- package/src/types/provider.ts +13 -0
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
package/src/types/config.ts
CHANGED
|
@@ -749,6 +749,10 @@ export interface OcxConfig {
|
|
|
749
749
|
shutdownTimeoutMs?: number;
|
|
750
750
|
/** Advertise supports_websockets so Codex opens the WS endpoint. Default false; set true to opt in. */
|
|
751
751
|
websockets?: boolean;
|
|
752
|
+
/** Experimental single-lane native OpenAI WebSocket steering; default off. */
|
|
753
|
+
codexNativeSteering?: boolean;
|
|
754
|
+
/** Experimental, default-off saved function-result injection on native multi-agent WebSockets. */
|
|
755
|
+
codexNativeInjection?: boolean;
|
|
752
756
|
/**
|
|
753
757
|
* Opt-in auto-cleanup policy for archived Codex sessions (issue #42 Phase 3).
|
|
754
758
|
* Default OFF (`enabled` false / unset). Never enabled implicitly.
|
|
@@ -1011,6 +1015,7 @@ export type OcxAccountPoolQuotaWindow = "five-hour" | "weekly" | "max-utilizatio
|
|
|
1011
1015
|
|
|
1012
1016
|
export type OcxComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window";
|
|
1013
1017
|
export type OcxComboDefaultEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
|
|
1018
|
+
export type OcxComboDefaultEffortMode = "fallback" | "force";
|
|
1014
1019
|
|
|
1015
1020
|
/**
|
|
1016
1021
|
* How a combo derives the reasoning ladder it publishes to the picker.
|
|
@@ -1046,8 +1051,10 @@ export interface OcxComboConfig {
|
|
|
1046
1051
|
cooldownMs?: number;
|
|
1047
1052
|
/** Maximum wait for an eligible target cooldown to expire before failing closed. Default 0; range 0..600000, per selection attempt. */
|
|
1048
1053
|
waitForCooldownMs?: number;
|
|
1049
|
-
/** Used when the client omits reasoning.effort. null/omitted leaves the target default unchanged. */
|
|
1054
|
+
/** Used as a fallback when the client omits reasoning.effort, or as an override in `force` mode. null/omitted leaves the target default unchanged. */
|
|
1050
1055
|
defaultEffort?: OcxComboDefaultEffort | null;
|
|
1056
|
+
/** `force` makes the combo default override a valid client effort. Omitted / `fallback` preserves client precedence. */
|
|
1057
|
+
defaultEffortMode?: OcxComboDefaultEffortMode;
|
|
1051
1058
|
/**
|
|
1052
1059
|
* Picker-ladder derivation policy. Omitted / `"strict"` keeps the legacy rule where an
|
|
1053
1060
|
* explicitly empty target ladder suppresses the whole combo's effort control.
|
package/src/types/provider.ts
CHANGED
|
@@ -311,6 +311,19 @@ export interface OcxProviderConfig {
|
|
|
311
311
|
* preserved after it, and parallel calls stay together with the reasoning turn that produced them.
|
|
312
312
|
*/
|
|
313
313
|
requiresAdjacentResponsesToolResults?: boolean;
|
|
314
|
+
/**
|
|
315
|
+
* Responses upstream whose parser also rejects a tool call that has no matching output
|
|
316
|
+
* anywhere in the replayed input, not merely one whose result sits out of order. A call left
|
|
317
|
+
* dangling by an interrupted stream is answered with an explicit unknown-status placeholder
|
|
318
|
+
* so the thread can continue.
|
|
319
|
+
*
|
|
320
|
+
* Separate from `requiresAdjacentResponsesToolResults` on purpose: adjacency reorders items a
|
|
321
|
+
* strict parser already accepts in some order, while this synthesizes an item the client never
|
|
322
|
+
* sent. Kimi accepts a dangling call (#4726), so it must not inherit the synthesis.
|
|
323
|
+
* `statelessResponses` implies this, because an upstream that stores nothing cannot resolve
|
|
324
|
+
* the missing half from its own history either.
|
|
325
|
+
*/
|
|
326
|
+
requiresPairedResponsesToolResults?: boolean;
|
|
314
327
|
/**
|
|
315
328
|
* When enabled, a tool result that is present but empty (no usable text or content
|
|
316
329
|
* part) is rewritten to an explicit annotation before it reaches the upstream wire,
|
package/src/types/request.ts
CHANGED
|
@@ -154,9 +154,11 @@ export interface OcxAssistantMessage {
|
|
|
154
154
|
model?: string;
|
|
155
155
|
timestamp: number;
|
|
156
156
|
/**
|
|
157
|
-
* Kiro
|
|
158
|
-
*
|
|
159
|
-
*
|
|
157
|
+
* Kiro's encrypted reasoning blob for THIS assistant turn — the opaque value from the turn's
|
|
158
|
+
* `reasoningContentEvent` (`signature` for the GPT-5.6 family, `redactedContent` for the base64
|
|
159
|
+
* shape), tagged with the wire field it must be replayed on (see kiro/reasoning.ts). Kiro
|
|
160
|
+
* replays it to preserve model reasoning across turns. Provider-specific and unrenderable, so it
|
|
161
|
+
* rides the message rather than a content part: any other adapter simply ignores it.
|
|
160
162
|
*/
|
|
161
163
|
kiroRedactedReasoning?: string;
|
|
162
164
|
}
|
|
@@ -317,8 +319,9 @@ export type AdapterEvent =
|
|
|
317
319
|
// opaque redacted_thinking blocks. Both must be replayed verbatim or tool-use turns 400.
|
|
318
320
|
| { type: "thinking_signature"; signature: string }
|
|
319
321
|
| { type: "redacted_thinking"; data: string }
|
|
320
|
-
// Kiro reasoning round-trip: the encrypted
|
|
321
|
-
// Never rendered — it only rides the reasoning item's envelope
|
|
322
|
+
// Kiro reasoning round-trip: the encrypted reasoning blob for the CURRENT assistant turn, tagged
|
|
323
|
+
// with the wire field it arrived on. Never rendered — it only rides the reasoning item's envelope
|
|
324
|
+
// so the next request can replay it verbatim.
|
|
322
325
|
| { type: "kiro_redacted_reasoning"; data: string }
|
|
323
326
|
| { type: "reasoning_raw_delta"; text: string }
|
|
324
327
|
| { type: "tool_call_start"; id: string; name: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata }
|
package/src/types/tools.ts
CHANGED
|
@@ -67,6 +67,30 @@ const CODE_MODE_HELPER_TOOL_NAMES = [
|
|
|
67
67
|
*/
|
|
68
68
|
export const CODE_MODE_EXEC_TOOL_NAME = "exec";
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Spellings that may never be MANUFACTURED as a bare alias for a namespaced tool.
|
|
72
|
+
*
|
|
73
|
+
* A bare alias is an ordinary compatibility affordance -- providers echo a namespaced tool
|
|
74
|
+
* without its prefix, and restoring the identity needs the bare spelling registered. For these
|
|
75
|
+
* six it is also an authorization decision, because a declared-name set is what
|
|
76
|
+
* `normalizeDeclaredToolName` and `declaresCodeModeExec` read: bare `exec` turns nested-helper
|
|
77
|
+
* normalization on for a catalog that never declared the shell, bare `exec_command` or
|
|
78
|
+
* `shell_command` turns it off for one that did, and the rest are accepted as declared calls the
|
|
79
|
+
* caller only ever authorized under a namespace.
|
|
80
|
+
*
|
|
81
|
+
* This is a property of the SPELLING, not of the namespace that declared it and not of the reason
|
|
82
|
+
* the alias was being added. It lives here, beside the names it protects, because every site that
|
|
83
|
+
* builds a declared-name set has to apply the same list -- the two that kept their own copies each
|
|
84
|
+
* drifted, once to a single namespace and once to a single name.
|
|
85
|
+
*
|
|
86
|
+
* A genuine namespace-free declaration is NOT covered: that is the caller declaring the tool, not
|
|
87
|
+
* a namespace being discarded to synthesize a bare name.
|
|
88
|
+
*/
|
|
89
|
+
export const NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES: ReadonlySet<string> = new Set<string>([
|
|
90
|
+
CODE_MODE_EXEC_TOOL_NAME,
|
|
91
|
+
...CODE_MODE_HELPER_TOOL_NAMES,
|
|
92
|
+
]);
|
|
93
|
+
|
|
70
94
|
/**
|
|
71
95
|
* Normalizes provider-emitted tool names against declared tool catalogs.
|
|
72
96
|
*
|
package/src/types.ts
CHANGED
|
@@ -16,6 +16,7 @@ export {
|
|
|
16
16
|
isAllowedToolChoice,
|
|
17
17
|
toolChoiceToolPredicate,
|
|
18
18
|
declaresCodeModeExec,
|
|
19
|
+
NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES,
|
|
19
20
|
} from "./types/tools";
|
|
20
21
|
|
|
21
22
|
export type { UpstreamHttpVersion, ReasoningSummaryDelivery, CodexAccountMode } from "./types/wire";
|
|
@@ -74,6 +75,7 @@ export type {
|
|
|
74
75
|
OcxAccountPoolQuotaWindow,
|
|
75
76
|
OcxComboStrategy,
|
|
76
77
|
OcxComboDefaultEffort,
|
|
78
|
+
OcxComboDefaultEffortMode,
|
|
77
79
|
OcxComboReasoningEffortMode,
|
|
78
80
|
OcxComboTarget,
|
|
79
81
|
OcxComboConfig,
|
package/src/update/index.ts
CHANGED
|
@@ -481,6 +481,16 @@ export async function runUpdate(): Promise<void> {
|
|
|
481
481
|
" After the update: close the Codex app, run 'ocx doctor', then run 'ocx stop' once to retry.",
|
|
482
482
|
);
|
|
483
483
|
}
|
|
484
|
+
if (decision.reason === "history-deferred") {
|
|
485
|
+
// Not the same warning: nothing was restored here. Saying "history metadata is
|
|
486
|
+
// incomplete" would imply config and catalog came back, and an operator who
|
|
487
|
+
// believed that would not know a teardown is still owed.
|
|
488
|
+
console.warn(
|
|
489
|
+
"⚠️ The shared teardown was refused by the Codex history preflight and restored nothing.\n" +
|
|
490
|
+
" Config, catalog, history and provenance were preserved, and the teardown receipt was kept.\n" +
|
|
491
|
+
" The proxy is down, so the update continues; close the Codex app and run 'ocx stop' once afterwards to finish the restore.",
|
|
492
|
+
);
|
|
493
|
+
}
|
|
484
494
|
}
|
|
485
495
|
|
|
486
496
|
console.log(`Updating${latest ? ` to v${latest}` : ""}…\n$ ${bin} ${cmdArgs.join(" ")}`);
|
|
@@ -13,3 +13,22 @@
|
|
|
13
13
|
* the child's code faithfully enough to propagate the confusion.
|
|
14
14
|
*/
|
|
15
15
|
export const STOP_HISTORY_INCOMPLETE_EXIT_CODE = 79;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The exit code `ocx stop` uses to say "the proxy is down and the shared teardown was
|
|
19
|
+
* refused before it changed anything" (#4718).
|
|
20
|
+
*
|
|
21
|
+
* This is NOT 79. Seventy-nine means the teardown ran: config and catalog came back to
|
|
22
|
+
* their native values and only the Codex history metadata could not be finalized, so the
|
|
23
|
+
* receipt is discharged. Eighty means the Codex history preflight refused FIRST, so
|
|
24
|
+
* config, catalog, history and provenance are all untouched, the client is still routed
|
|
25
|
+
* at the proxy that just stopped, and the receipt stays outstanding for a later stop.
|
|
26
|
+
*
|
|
27
|
+
* Collapsing the two would be a data-loss bug in the quiet direction: a caller reading 79
|
|
28
|
+
* discharges an obligation that was never performed.
|
|
29
|
+
*
|
|
30
|
+
* Eighty sits in the same unoccupied window as 79 — above `sysexits.h` (64-78), below
|
|
31
|
+
* `128 + signal`, and outside 0, 1, 2, 4, 64 and 130, which are the codes this CLI and its
|
|
32
|
+
* dispatcher already emit.
|
|
33
|
+
*/
|
|
34
|
+
export const STOP_HISTORY_DEFERRED_EXIT_CODE = 80;
|
|
@@ -6,5 +6,5 @@ export declare function decidePostStopUpdate(input: {
|
|
|
6
6
|
teardownOutstanding?: boolean;
|
|
7
7
|
}): {
|
|
8
8
|
proceed: boolean;
|
|
9
|
-
reason: "stop-failed" | "runtime-state" | "teardown-outstanding" | "proxy-live" | "proxy-unknown" | "history-only" | "ok";
|
|
9
|
+
reason: "stop-failed" | "runtime-state" | "teardown-outstanding" | "proxy-live" | "proxy-unknown" | "history-only" | "history-deferred" | "ok";
|
|
10
10
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
|
|
1
|
+
import { STOP_HISTORY_DEFERRED_EXIT_CODE, STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* May an update replace package files after `ocx stop` returned?
|
|
@@ -22,13 +22,22 @@ import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
|
|
|
22
22
|
* absence, and replacing files under a live server leaves it running a mix of old and
|
|
23
23
|
* new modules.
|
|
24
24
|
* - `ok` / `history-only` — proceed; the second also prints the manifest warning.
|
|
25
|
+
* - `history-deferred` — proceed; the stop is down but restored nothing, because the
|
|
26
|
+
* Codex history preflight refused first (#4718). The receipts it kept are the ONLY
|
|
27
|
+
* obligations it left, which the child proved before choosing this status, so
|
|
28
|
+
* `teardownOutstanding` seeing them is expected rather than disqualifying. Every other
|
|
29
|
+
* gate still applies: runtime records and a live or unreadable endpoint abort exactly
|
|
30
|
+
* as they do for a clean stop, because package replacement under a live server is the
|
|
31
|
+
* danger this function exists to prevent, and a history refusal says nothing about it.
|
|
25
32
|
*/
|
|
26
33
|
export function decidePostStopUpdate({ status, hasRuntimeState, liveness, teardownOutstanding = false }) {
|
|
27
34
|
const historyOnly = status === STOP_HISTORY_INCOMPLETE_EXIT_CODE;
|
|
28
|
-
|
|
35
|
+
const historyDeferred = status === STOP_HISTORY_DEFERRED_EXIT_CODE;
|
|
36
|
+
if (status !== 0 && !historyOnly && !historyDeferred) return { proceed: false, reason: "stop-failed" };
|
|
29
37
|
if (hasRuntimeState) return { proceed: false, reason: "runtime-state" };
|
|
30
|
-
if (teardownOutstanding) return { proceed: false, reason: "teardown-outstanding" };
|
|
38
|
+
if (teardownOutstanding && !historyDeferred) return { proceed: false, reason: "teardown-outstanding" };
|
|
31
39
|
if (liveness === "live") return { proceed: false, reason: "proxy-live" };
|
|
32
40
|
if (liveness !== "dead") return { proceed: false, reason: "proxy-unknown" };
|
|
41
|
+
if (historyDeferred) return { proceed: true, reason: "history-deferred" };
|
|
33
42
|
return { proceed: true, reason: historyOnly ? "history-only" : "ok" };
|
|
34
43
|
}
|
package/src/usage/log.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated
|
|
|
38
38
|
* The old name `CodexUsageAccountLogLabel` is kept as an alias because it is exported and used
|
|
39
39
|
* across modules; the two predicates below are what callers should choose between.
|
|
40
40
|
*/
|
|
41
|
-
export type UsageAccountLogLabel = "main" | `p${string}` | `o${string}`;
|
|
41
|
+
export type UsageAccountLogLabel = "main" | `p${string}` | `o${string}` | `k${string}`;
|
|
42
42
|
export type CodexUsageAccountLogLabel = UsageAccountLogLabel;
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -205,7 +205,7 @@ export async function describeImageAnthropic(
|
|
|
205
205
|
body: JSON.stringify(body),
|
|
206
206
|
signal: linkedSignal.signal,
|
|
207
207
|
}, recovery)),
|
|
208
|
-
{ abortSignal: linkedSignal.signal, label: "vision-sidecar-anthropic" },
|
|
208
|
+
{ replaySafe: true, abortSignal: linkedSignal.signal, label: "vision-sidecar-anthropic" },
|
|
209
209
|
);
|
|
210
210
|
if (!res.ok) {
|
|
211
211
|
// The body is untrusted and only feeds one auth-failure message, so read a bounded prefix.
|
package/src/vision/describe.ts
CHANGED
|
@@ -87,7 +87,7 @@ export async function describeImage(
|
|
|
87
87
|
input: [{ type: "message", role: "user", content }],
|
|
88
88
|
reasoning: { effort: settings.reasoning },
|
|
89
89
|
// The ChatGPT (codex) backend rejects `max_output_tokens` ("Unsupported parameter"); the shared
|
|
90
|
-
// SSE parser bounds
|
|
90
|
+
// SSE parser bounds wire and decoded payload before DESC_MAX_CHARS applies its display clamp.
|
|
91
91
|
store: false,
|
|
92
92
|
stream: true,
|
|
93
93
|
};
|
|
@@ -108,7 +108,7 @@ export async function describeImage(
|
|
|
108
108
|
// `session_id`, and `x-codex-turn-metadata` to the redirect target.
|
|
109
109
|
redirect: "manual",
|
|
110
110
|
}, recovery)),
|
|
111
|
-
{ abortSignal: linkedSignal.signal, label: "vision-sidecar" },
|
|
111
|
+
{ replaySafe: true, abortSignal: linkedSignal.signal, label: "vision-sidecar" },
|
|
112
112
|
);
|
|
113
113
|
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
114
114
|
try {
|
|
@@ -121,9 +121,9 @@ export async function describeImage(
|
|
|
121
121
|
const parsed = await parseSidecarSSE(res);
|
|
122
122
|
if (linkedSignal.signal.aborted) throw linkedSignal.signal.reason;
|
|
123
123
|
recordOutcome?.(res.status);
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
if (
|
|
124
|
+
// Any parser error invalidates decoded text: it may be a prefix from a bounded or incomplete
|
|
125
|
+
// stream and must never be rendered or cached as a complete image description.
|
|
126
|
+
if (parsed.error) return { text: "", error: parsed.error };
|
|
127
127
|
return { text: parsed.text };
|
|
128
128
|
} finally {
|
|
129
129
|
detachBodyGuard();
|
|
@@ -215,7 +215,7 @@ export async function runAnthropicWebSearch(
|
|
|
215
215
|
body: JSON.stringify(body),
|
|
216
216
|
signal: linkedSignal.signal,
|
|
217
217
|
}, recovery)),
|
|
218
|
-
{ abortSignal: linkedSignal.signal, label: "web-search-sidecar-anthropic" },
|
|
218
|
+
{ replaySafe: true, abortSignal: linkedSignal.signal, label: "web-search-sidecar-anthropic" },
|
|
219
219
|
);
|
|
220
220
|
// Guard before any branch reads the body: the failure branch's `res.text()` ran ahead of
|
|
221
221
|
// the success-path guard, reopening the fetch-resolution-to-reader-attach race
|
|
@@ -46,7 +46,7 @@ export async function runExaWebSearch(
|
|
|
46
46
|
signal: linkedSignal.signal,
|
|
47
47
|
redirect: "manual",
|
|
48
48
|
}, recovery)),
|
|
49
|
-
{ abortSignal: linkedSignal.signal, label: "exa-web-search-sidecar" },
|
|
49
|
+
{ replaySafe: true, abortSignal: linkedSignal.signal, label: "exa-web-search-sidecar" },
|
|
50
50
|
);
|
|
51
51
|
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
52
52
|
try {
|
|
@@ -94,7 +94,7 @@ export async function runWebSearch(
|
|
|
94
94
|
// `session_id`, and `x-codex-turn-metadata` to the redirect target.
|
|
95
95
|
redirect: "manual",
|
|
96
96
|
}, recovery), forwardProvider)),
|
|
97
|
-
{ abortSignal: linkedSignal.signal, label: "web-search-sidecar" },
|
|
97
|
+
{ replaySafe: true, abortSignal: linkedSignal.signal, label: "web-search-sidecar" },
|
|
98
98
|
);
|
|
99
99
|
// Attach the body guard before ANY branch reads it. The success path guarded itself below,
|
|
100
100
|
// but the failure branch's `res.text()` runs first, so a cancel landing between fetch
|
|
@@ -80,7 +80,7 @@ export async function runGeminiWebSearch(
|
|
|
80
80
|
signal: linkedSignal.signal,
|
|
81
81
|
redirect: "manual",
|
|
82
82
|
}, recovery)),
|
|
83
|
-
{ abortSignal: linkedSignal.signal, label: "gemini-web-search-sidecar" },
|
|
83
|
+
{ replaySafe: true, abortSignal: linkedSignal.signal, label: "gemini-web-search-sidecar" },
|
|
84
84
|
);
|
|
85
85
|
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
86
86
|
try {
|
package/src/web-search/loop.ts
CHANGED
|
@@ -512,7 +512,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
512
512
|
signal: headerDeadline.signal,
|
|
513
513
|
}, retryRecovery));
|
|
514
514
|
},
|
|
515
|
-
{ abortSignal: headerDeadline.signal, label: "web-search-loop" },
|
|
515
|
+
{ replaySafe: true, abortSignal: headerDeadline.signal, label: "web-search-loop" },
|
|
516
516
|
);
|
|
517
517
|
}
|
|
518
518
|
} finally {
|
|
@@ -57,7 +57,7 @@ export async function runOllamaWebSearch(
|
|
|
57
57
|
// Bun forwards custom headers across redirects, so a redirect would leak the key.
|
|
58
58
|
redirect: "manual",
|
|
59
59
|
}, recovery)),
|
|
60
|
-
{ abortSignal: linkedSignal.signal, label: "ollama-web-search-bridge" },
|
|
60
|
+
{ replaySafe: true, abortSignal: linkedSignal.signal, label: "ollama-web-search-bridge" },
|
|
61
61
|
);
|
|
62
62
|
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
63
63
|
try {
|
package/src/web-search/parse.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type WebSearchSource = SafeWebSearchSource;
|
|
|
12
12
|
export interface WebSearchResult {
|
|
13
13
|
text: string;
|
|
14
14
|
sources: WebSearchSource[];
|
|
15
|
-
/** Set
|
|
15
|
+
/** Set when the stream failed, including when partial text was decoded before failure. */
|
|
16
16
|
error?: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -31,9 +31,12 @@ interface OutputItem {
|
|
|
31
31
|
content?: OutputTextBlock[];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
// streamed response here, before decoded text and authoritative/delta copies can accumulate.
|
|
34
|
+
// Keep this compatibility export for non-Responses sidecar executors and bounded error bodies.
|
|
36
35
|
export const MAX_SIDECAR_RESPONSE_BYTES = 64 * 1024;
|
|
36
|
+
// Responses SSE can spend far more wire bytes on JSON framing than on useful model output. Keep a
|
|
37
|
+
// larger finite wire ceiling while separately bounding the decoded text copies accumulated below.
|
|
38
|
+
export const MAX_SIDECAR_STREAM_BYTES = MAX_SIDECAR_RESPONSE_BYTES * 16;
|
|
39
|
+
export const MAX_SIDECAR_DECODED_CHARS = 64 * 1024;
|
|
37
40
|
|
|
38
41
|
/** Push a `url_citation` annotation as a source, de-duplicated by URL. */
|
|
39
42
|
function collectAnnotation(ann: AnnotationLike | undefined, sources: WebSearchSource[], seen: Set<string>): void {
|
|
@@ -208,10 +211,11 @@ export function cancelReaderWithoutWaiting(
|
|
|
208
211
|
* `response.output_text.done` text; falls back to accumulated `response.output_text.delta`. Sources are
|
|
209
212
|
* collected from EVERY shape they arrive in — `response.output_text.annotation.added` events (the
|
|
210
213
|
* streaming path, which earlier testing missed → empty citations), `done`-block `annotations[]`, and
|
|
211
|
-
* the final output[].
|
|
214
|
+
* the final output[]. A terminal failure, a safety bound, or EOF before a terminal event surfaces as
|
|
215
|
+
* `error` even when partial answer text was decoded.
|
|
212
216
|
*/
|
|
213
217
|
export async function parseSidecarSSE(response: Response): Promise<WebSearchResult> {
|
|
214
|
-
if (!response.body) return { text: "", sources: [] };
|
|
218
|
+
if (!response.body) return { text: "", sources: [], error: "sidecar stream returned no response body" };
|
|
215
219
|
const reader = response.body.getReader();
|
|
216
220
|
const decoder = new TextDecoder();
|
|
217
221
|
let buffer = "";
|
|
@@ -224,10 +228,37 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
|
|
|
224
228
|
final: WebSearchResult | null;
|
|
225
229
|
streamSources: WebSearchSource[];
|
|
226
230
|
error: string | null;
|
|
227
|
-
|
|
231
|
+
decodedChars: number;
|
|
232
|
+
terminalEvent: boolean;
|
|
233
|
+
limitReached: boolean;
|
|
234
|
+
} = {
|
|
235
|
+
deltaText: "",
|
|
236
|
+
doneText: "",
|
|
237
|
+
final: null,
|
|
238
|
+
streamSources: [],
|
|
239
|
+
error: null,
|
|
240
|
+
decodedChars: 0,
|
|
241
|
+
terminalEvent: false,
|
|
242
|
+
limitReached: false,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const acceptDecodedChars = (count: number): boolean => {
|
|
246
|
+
if (count > MAX_SIDECAR_DECODED_CHARS - acc.decodedChars) {
|
|
247
|
+
acc.error = "sidecar response decoded text limit reached";
|
|
248
|
+
acc.limitReached = true;
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
acc.decodedChars += count;
|
|
252
|
+
return true;
|
|
253
|
+
};
|
|
228
254
|
|
|
229
255
|
const handle = (payload: string): void => {
|
|
230
|
-
if (!payload
|
|
256
|
+
if (!payload) return;
|
|
257
|
+
if (payload === "[DONE]") {
|
|
258
|
+
acc.terminalEvent = true;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (acc.limitReached) return;
|
|
231
262
|
// Neither warning below copies the frame's content. An upstream SSE payload can carry model
|
|
232
263
|
// output or credential material, and a malformed frame is exactly the case where the content
|
|
233
264
|
// is least trustworthy. Length plus a classification separates the two failure modes in a log
|
|
@@ -247,19 +278,27 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
|
|
|
247
278
|
const data = parsed as Record<string, unknown>;
|
|
248
279
|
const type = data.type as string | undefined;
|
|
249
280
|
if (type === "response.output_text.delta" && typeof data.delta === "string") {
|
|
250
|
-
acc.deltaText += data.delta;
|
|
281
|
+
if (acceptDecodedChars(data.delta.length)) acc.deltaText += data.delta;
|
|
251
282
|
} else if (type === "response.output_text.done" && typeof data.text === "string") {
|
|
252
283
|
// The `done` event carries the full, authoritative text for one content part.
|
|
253
|
-
acc.doneText += data.text;
|
|
284
|
+
if (acceptDecodedChars(data.text.length)) acc.doneText += data.text;
|
|
254
285
|
} else if (type === "response.completed" || type === "response.done") {
|
|
286
|
+
acc.terminalEvent = true;
|
|
255
287
|
const resp = data.response as { output?: OutputItem[] } | undefined;
|
|
256
|
-
if (resp?.output)
|
|
288
|
+
if (resp?.output) {
|
|
289
|
+
const final = fromOutputArray(resp.output, seen);
|
|
290
|
+
if (acceptDecodedChars(final.text.length)) acc.final = final;
|
|
291
|
+
}
|
|
257
292
|
} else if (type === "response.failed" || type === "response.incomplete" || type === "error") {
|
|
293
|
+
acc.terminalEvent = true;
|
|
258
294
|
const resp = data.response as { error?: { message?: string } } | undefined;
|
|
259
295
|
const msg = resp?.error?.message
|
|
260
296
|
?? (data.error as { message?: string } | undefined)?.message
|
|
261
297
|
?? (typeof data.message === "string" ? data.message : undefined);
|
|
262
|
-
|
|
298
|
+
acc.error = msg ?? `sidecar stream ended with ${type}`;
|
|
299
|
+
} else if (type?.includes("reasoning") && typeof data.delta === "string") {
|
|
300
|
+
// Reasoning is not returned, but it is still decoded payload retained transiently by JSON.parse.
|
|
301
|
+
acceptDecodedChars(data.delta.length);
|
|
263
302
|
}
|
|
264
303
|
// Citations stream as a dedicated `response.output_text.annotation.added` event (singular
|
|
265
304
|
// `annotation`); capture it regardless of the exact event name so they aren't lost.
|
|
@@ -270,7 +309,7 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
|
|
|
270
309
|
while (true) {
|
|
271
310
|
const { done, value } = await reader.read();
|
|
272
311
|
if (done) break;
|
|
273
|
-
const remaining =
|
|
312
|
+
const remaining = MAX_SIDECAR_STREAM_BYTES - responseBytes;
|
|
274
313
|
const accepted = value.byteLength <= remaining ? value : value.subarray(0, remaining);
|
|
275
314
|
responseBytes += accepted.byteLength;
|
|
276
315
|
buffer += decoder.decode(accepted, { stream: true });
|
|
@@ -280,11 +319,24 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
|
|
|
280
319
|
const data = sseFieldValue(line, "data");
|
|
281
320
|
if (data !== null) handle(data.trim());
|
|
282
321
|
}
|
|
283
|
-
if (
|
|
322
|
+
if (acc.limitReached) {
|
|
323
|
+
cancelReaderWithoutWaiting(reader, "sidecar response decoded text limit reached");
|
|
324
|
+
buffer = "";
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
if (acc.terminalEvent) {
|
|
328
|
+
cancelReaderWithoutWaiting(reader, "sidecar terminal event received");
|
|
329
|
+
buffer = "";
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
if (responseBytes >= MAX_SIDECAR_STREAM_BYTES) {
|
|
284
333
|
// Preserve complete events accepted up to the cap, but discard any unterminated line and
|
|
285
334
|
// TextDecoder carry. Do not let a rejecting/hung cancel turn bounded partial output into
|
|
286
335
|
// an error or keep this parser waiting on upstream teardown.
|
|
287
336
|
cancelReaderWithoutWaiting(reader, "sidecar response byte limit reached");
|
|
337
|
+
acc.error = "sidecar response byte limit reached before terminal event";
|
|
338
|
+
acc.limitReached = true;
|
|
339
|
+
buffer = "";
|
|
288
340
|
break;
|
|
289
341
|
}
|
|
290
342
|
}
|
|
@@ -310,6 +362,7 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
|
|
|
310
362
|
appendSafeWebSearchSource(sources, s);
|
|
311
363
|
}
|
|
312
364
|
const finalText = stripped ? body : (typeof text === "string" ? text : "");
|
|
313
|
-
|
|
365
|
+
const error = acc.error ?? (!acc.terminalEvent ? "sidecar stream ended before terminal event" : null);
|
|
366
|
+
if (error) return { text: finalText, sources, error };
|
|
314
367
|
return { text: finalText, sources };
|
|
315
368
|
}
|