@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
|
@@ -22,8 +22,34 @@ export interface RenderedDesktopModel extends DesktopProfileModel {
|
|
|
22
22
|
supports1m: boolean;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// Managed-namespace date aliases run 2026-2035. The original 2026-only
|
|
26
|
+
// (365 slots) design failed with "all 365 encoded date slots are occupied"
|
|
27
|
+
// once a catalog exceeded 365 routes (stale assignments are retained by
|
|
28
|
+
// design, so the set only grows). Years before 2026 stay rejected: dated
|
|
29
|
+
// ids like `claude-opus-4-8-20250201` are real model snapshot ids, not
|
|
30
|
+
// managed aliases, and the inbound decoder relies on that distinction.
|
|
31
|
+
// Every emitted suffix stays 8 digits so modelMap date-stripping keeps
|
|
32
|
+
// working.
|
|
33
|
+
const DATE_ALIAS = /^claude-opus-4-8-(202[6-9]\d{4}|203[0-5]\d{4})$/;
|
|
34
|
+
const LEGACY_YEAR = 2026;
|
|
35
|
+
const LEGACY_DAY_COUNT = 365;
|
|
36
|
+
const ALIAS_FIRST_YEAR = 2026;
|
|
37
|
+
const ALIAS_LAST_YEAR = 2035;
|
|
38
|
+
|
|
39
|
+
function isLeapYear(year: number): boolean {
|
|
40
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function daysInAliasYear(year: number): number {
|
|
44
|
+
return isLeapYear(year) ? 366 : 365;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const TOTAL_ALIAS_SLOTS = (() => {
|
|
48
|
+
let total = 0;
|
|
49
|
+
for (let year = ALIAS_FIRST_YEAR; year <= ALIAS_LAST_YEAR; year += 1) total += daysInAliasYear(year);
|
|
50
|
+
return total;
|
|
51
|
+
})();
|
|
52
|
+
export const ALIAS_YEAR_RANGE = { first: ALIAS_FIRST_YEAR, last: ALIAS_LAST_YEAR } as const;
|
|
27
53
|
|
|
28
54
|
export class DesktopProfileError extends Error {
|
|
29
55
|
constructor(message: string, readonly path = "profile") {
|
|
@@ -119,7 +145,7 @@ export function parseDesktopProfile(value: unknown): DesktopProfile {
|
|
|
119
145
|
if (isRealAnthropicRoute(route)) {
|
|
120
146
|
if (raw.alias !== routeModelId(route)) throw new DesktopProfileError("real Anthropic routes must keep their exact model id", `profile.assignments.${route}.alias`);
|
|
121
147
|
} else if (!validDateAlias(raw.alias)) {
|
|
122
|
-
throw new DesktopProfileError("must be a valid claude-opus-4-8-
|
|
148
|
+
throw new DesktopProfileError("must be a valid claude-opus-4-8-YYYYMMDD alias", `profile.assignments.${route}.alias`);
|
|
123
149
|
}
|
|
124
150
|
if (aliases.has(raw.alias)) throw new DesktopProfileError(`duplicate alias "${raw.alias}"`, `profile.assignments.${route}.alias`);
|
|
125
151
|
aliases.add(raw.alias);
|
|
@@ -144,26 +170,57 @@ export function parseDesktopProfile(value: unknown): DesktopProfile {
|
|
|
144
170
|
return { version: 1, assignments, defaults, ...appliedMarkers(value) };
|
|
145
171
|
}
|
|
146
172
|
|
|
147
|
-
function
|
|
148
|
-
const date = new Date(Date.UTC(
|
|
173
|
+
function formatSlotDate(year: number, dayOfYear: number): string {
|
|
174
|
+
const date = new Date(Date.UTC(year, 0, dayOfYear));
|
|
149
175
|
const y = date.getUTCFullYear();
|
|
150
176
|
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
151
177
|
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
152
178
|
return `claude-opus-4-8-${y}${m}${d}`;
|
|
153
179
|
}
|
|
154
180
|
|
|
181
|
+
// Legacy 2026 ring, byte-identical to the original allocator: the same route
|
|
182
|
+
// must keep resolving to the same 2026 alias it always had, and a probe over
|
|
183
|
+
// a nearly-full 2026 set must land on the same free date as before.
|
|
184
|
+
function legacyDayAlias(dayIndex: number): string {
|
|
185
|
+
return formatSlotDate(LEGACY_YEAR, dayIndex + 1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Overflow ring for catalogs past 365 routes (2027-2035). Probed only after
|
|
189
|
+
// every legacy slot is taken, so existing profiles never shift into it.
|
|
190
|
+
const OVERFLOW_FIRST_YEAR = LEGACY_YEAR + 1;
|
|
191
|
+
const OVERFLOW_SLOT_COUNT = TOTAL_ALIAS_SLOTS - LEGACY_DAY_COUNT;
|
|
192
|
+
|
|
193
|
+
function overflowSlotAlias(slotIndex: number): string {
|
|
194
|
+
let remaining = ((slotIndex % OVERFLOW_SLOT_COUNT) + OVERFLOW_SLOT_COUNT) % OVERFLOW_SLOT_COUNT;
|
|
195
|
+
for (let year = OVERFLOW_FIRST_YEAR; year <= ALIAS_LAST_YEAR; year += 1) {
|
|
196
|
+
const days = daysInAliasYear(year);
|
|
197
|
+
if (remaining < days) return formatSlotDate(year, remaining + 1);
|
|
198
|
+
remaining -= days;
|
|
199
|
+
}
|
|
200
|
+
throw new DesktopProfileError("slot index out of range", "profile.assignments");
|
|
201
|
+
}
|
|
202
|
+
|
|
155
203
|
function routeStartDay(route: string): number {
|
|
156
|
-
return createHash("sha256").update(route).digest().readUInt32BE(0) %
|
|
204
|
+
return createHash("sha256").update(route).digest().readUInt32BE(0) % LEGACY_DAY_COUNT;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function routeOverflowStart(route: string): number {
|
|
208
|
+
return createHash("sha256").update(route).digest().readUInt32BE(4) % OVERFLOW_SLOT_COUNT;
|
|
157
209
|
}
|
|
158
210
|
|
|
159
211
|
function allocateAlias(route: string, used: Set<string>): string {
|
|
160
212
|
if (isRealAnthropicRoute(route)) return routeModelId(route);
|
|
161
213
|
const start = routeStartDay(route);
|
|
162
|
-
for (let offset = 0; offset <
|
|
163
|
-
const alias =
|
|
214
|
+
for (let offset = 0; offset < LEGACY_DAY_COUNT; offset += 1) {
|
|
215
|
+
const alias = legacyDayAlias((start + offset) % LEGACY_DAY_COUNT);
|
|
216
|
+
if (!used.has(alias)) return alias;
|
|
217
|
+
}
|
|
218
|
+
const overflowStart = routeOverflowStart(route);
|
|
219
|
+
for (let offset = 0; offset < OVERFLOW_SLOT_COUNT; offset += 1) {
|
|
220
|
+
const alias = overflowSlotAlias((overflowStart + offset) % OVERFLOW_SLOT_COUNT);
|
|
164
221
|
if (!used.has(alias)) return alias;
|
|
165
222
|
}
|
|
166
|
-
throw new DesktopProfileError(
|
|
223
|
+
throw new DesktopProfileError(`all ${TOTAL_ALIAS_SLOTS} encoded date slots are occupied`, `profile.assignments.${route}.alias`);
|
|
167
224
|
}
|
|
168
225
|
|
|
169
226
|
export function reconcileDesktopProfile(
|
package/src/claude/outbound.ts
CHANGED
|
@@ -189,7 +189,7 @@ function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resultConte
|
|
|
189
189
|
return { id, input, resultContent, completed };
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
function messageSnapshot(model: string): Rec {
|
|
192
|
+
function messageSnapshot(model: string, confirmedUsage?: Rec): Rec {
|
|
193
193
|
return {
|
|
194
194
|
id: `msg_${uuid()}`,
|
|
195
195
|
type: "message",
|
|
@@ -198,7 +198,7 @@ function messageSnapshot(model: string): Rec {
|
|
|
198
198
|
model,
|
|
199
199
|
stop_reason: null,
|
|
200
200
|
stop_sequence: null,
|
|
201
|
-
usage: { input_tokens: 0, output_tokens: 0 },
|
|
201
|
+
usage: confirmedUsage ?? { input_tokens: 0, output_tokens: 0 },
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -212,6 +212,8 @@ interface OpenBlock {
|
|
|
212
212
|
argsBuf?: string;
|
|
213
213
|
argsBufBytes?: number;
|
|
214
214
|
webSearchArgsEmitted?: boolean;
|
|
215
|
+
/** True once ordinary function-call arguments were emitted to Anthropic SSE. */
|
|
216
|
+
toolArgsEmitted?: boolean;
|
|
215
217
|
callId?: string;
|
|
216
218
|
/** Last fixed-size reasoning identity (item + summary/content index) seen by this block. */
|
|
217
219
|
reasoningPartKey?: string;
|
|
@@ -244,6 +246,7 @@ export function responsesSseToAnthropicSse(
|
|
|
244
246
|
let open: OpenBlock | null = null;
|
|
245
247
|
let sawToolUse = false;
|
|
246
248
|
let webSearchRequests = 0;
|
|
249
|
+
let earlyAnthropicUsage: Rec | undefined;
|
|
247
250
|
let pingTimer: ReturnType<typeof setInterval> | undefined;
|
|
248
251
|
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
249
252
|
const utf8SliceBytes = (value: string, start: number, end: number): number => {
|
|
@@ -280,7 +283,7 @@ export function responsesSseToAnthropicSse(
|
|
|
280
283
|
const ensureStarted = () => {
|
|
281
284
|
if (started) return;
|
|
282
285
|
started = true;
|
|
283
|
-
emit("message_start", { type: "message_start", message: messageSnapshot(model) });
|
|
286
|
+
emit("message_start", { type: "message_start", message: messageSnapshot(model, earlyAnthropicUsage) });
|
|
284
287
|
emit("ping", { type: "ping" });
|
|
285
288
|
};
|
|
286
289
|
// Keepalive pings protect remote deployments behind LB/NAT idle timeouts even
|
|
@@ -403,8 +406,17 @@ export function responsesSseToAnthropicSse(
|
|
|
403
406
|
const handleFrame = (eventName: string, data: Rec) => {
|
|
404
407
|
switch (eventName) {
|
|
405
408
|
case "response.created":
|
|
406
|
-
|
|
409
|
+
case "response.in_progress": {
|
|
410
|
+
// Lifecycle preludes do not start Anthropic framing, but some upstreams attach
|
|
411
|
+
// confirmed input usage before semantic output. Retain only its bounded Anthropic
|
|
412
|
+
// projection so message_start can report measurements that already arrived.
|
|
413
|
+
const response = isRec(data.response) ? data.response : {};
|
|
414
|
+
const usage = isRec(response.usage) ? response.usage : undefined;
|
|
415
|
+
if (!started && usage && typeof usage.input_tokens === "number") {
|
|
416
|
+
earlyAnthropicUsage = anthropicUsage(usage);
|
|
417
|
+
}
|
|
407
418
|
break;
|
|
419
|
+
}
|
|
408
420
|
case "response.heartbeat":
|
|
409
421
|
if ((controller.desiredSize ?? 0) > 0) emit("ping", { type: "ping" });
|
|
410
422
|
break;
|
|
@@ -488,6 +500,7 @@ export function responsesSseToAnthropicSse(
|
|
|
488
500
|
argsBuf: "",
|
|
489
501
|
argsBufBytes: 0,
|
|
490
502
|
webSearchArgsEmitted: false,
|
|
503
|
+
toolArgsEmitted: false,
|
|
491
504
|
};
|
|
492
505
|
break;
|
|
493
506
|
}
|
|
@@ -518,6 +531,14 @@ export function responsesSseToAnthropicSse(
|
|
|
518
531
|
type: "content_block_delta", index: open.index,
|
|
519
532
|
delta: { type: "input_json_delta", partial_json: data.delta },
|
|
520
533
|
});
|
|
534
|
+
open.toolArgsEmitted = true;
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
case "response.function_call_arguments.done": {
|
|
538
|
+
if (!open || open.kind !== "tool_use" || open.bufferWebSearchArgs || open.toolArgsEmitted) break;
|
|
539
|
+
if (typeof data.arguments !== "string" || data.arguments.length === 0) break;
|
|
540
|
+
emit("content_block_delta", { type: "content_block_delta", index: open.index, delta: { type: "input_json_delta", partial_json: data.arguments } });
|
|
541
|
+
open.toolArgsEmitted = true;
|
|
521
542
|
break;
|
|
522
543
|
}
|
|
523
544
|
case "response.output_item.done": {
|
|
@@ -565,6 +586,13 @@ export function responsesSseToAnthropicSse(
|
|
|
565
586
|
delta: { type: "input_json_delta", partial_json: JSON.stringify(sanitizeWebSearchInput(parsed)) },
|
|
566
587
|
});
|
|
567
588
|
open.webSearchArgsEmitted = true;
|
|
589
|
+
} else if (!open.bufferWebSearchArgs && !open.toolArgsEmitted
|
|
590
|
+
&& typeof item.arguments === "string" && item.arguments.length > 0) {
|
|
591
|
+
emit("content_block_delta", {
|
|
592
|
+
type: "content_block_delta", index: open.index,
|
|
593
|
+
delta: { type: "input_json_delta", partial_json: item.arguments },
|
|
594
|
+
});
|
|
595
|
+
open.toolArgsEmitted = true;
|
|
568
596
|
}
|
|
569
597
|
closeOpenBlock();
|
|
570
598
|
}
|
package/src/cli/account-main.ts
CHANGED
|
@@ -245,7 +245,7 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
|
|
|
245
245
|
}
|
|
246
246
|
if (noWait) {
|
|
247
247
|
printStatus({ flowId: startFlowId, ...pending });
|
|
248
|
-
console.log("follow up: ocx account main reauth status --flow " + startFlowId);
|
|
248
|
+
if (!wantsJson) console.log("follow up: ocx account main reauth status --flow " + startFlowId);
|
|
249
249
|
return 0;
|
|
250
250
|
}
|
|
251
251
|
// Blocking wait bounded by the service flow expiry (15-minute grant + margin).
|
package/src/cli/capabilities.ts
CHANGED
|
@@ -710,10 +710,10 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
710
710
|
},
|
|
711
711
|
{
|
|
712
712
|
command: ["system", "codex-restart"],
|
|
713
|
-
summary: "Restart the Codex app-
|
|
713
|
+
summary: "Restart the Codex desktop app and app-servers.",
|
|
714
714
|
routes: [{ method: "POST", path: "/api/system/codex-restart" }],
|
|
715
715
|
flags: [
|
|
716
|
-
{ name: "--yes", value: "boolean", summary: "Required:
|
|
716
|
+
{ name: "--yes", value: "boolean", summary: "Required: fully quits and relaunches the operator's Codex desktop app and restarts its app-servers." },
|
|
717
717
|
{ name: "--json", value: "boolean", summary: "Emit the restart result as JSON." },
|
|
718
718
|
],
|
|
719
719
|
mutates: true,
|
package/src/cli/combo.ts
CHANGED
|
@@ -15,7 +15,8 @@ const USAGE = `Usage:
|
|
|
15
15
|
ocx combo show <id> [--json]
|
|
16
16
|
ocx combo set <id> --targets <provider/model[:weight],...>
|
|
17
17
|
[--strategy <failover|round-robin|random|least-used|reset-window>] [--sticky <1-100>]
|
|
18
|
-
[--effort <low|medium|high|xhigh|max|ultra|->] [--
|
|
18
|
+
[--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force>]
|
|
19
|
+
(force overrides valid client effort and can increase cost/latency) [--alias <name|->]
|
|
19
20
|
[--native-alias] [--display-name <label|->]
|
|
20
21
|
[--rename-from <id>] [--json]
|
|
21
22
|
ocx combo remove <id> --yes [--json]`;
|
|
@@ -80,6 +81,10 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
80
81
|
if (strategy !== "round-robin") throw new CliUsageError("--sticky applies only to round-robin", USAGE);
|
|
81
82
|
}
|
|
82
83
|
const effort = takeOption(args, "--effort");
|
|
84
|
+
const effortMode = takeOption(args, "--effort-mode");
|
|
85
|
+
if (effortMode !== undefined && effortMode !== "fallback" && effortMode !== "force") {
|
|
86
|
+
throw new CliUsageError("--effort-mode must be fallback or force", USAGE);
|
|
87
|
+
}
|
|
83
88
|
const alias = takeOption(args, "--alias");
|
|
84
89
|
const nativeAlias = takeFlag(args, "--native-alias");
|
|
85
90
|
const displayName = takeOption(args, "--display-name");
|
|
@@ -91,12 +96,16 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
91
96
|
targets: parseTargets(targetsRaw),
|
|
92
97
|
};
|
|
93
98
|
if (effort !== undefined) combo.defaultEffort = effort === "-" ? null : effort;
|
|
99
|
+
if (effortMode !== undefined) combo.defaultEffortMode = effortMode;
|
|
94
100
|
if (alias !== undefined) combo.alias = alias === "-" ? "" : alias;
|
|
95
101
|
if (nativeAlias) combo.nativeAlias = true;
|
|
96
102
|
if (displayName !== undefined) combo.displayName = displayName === "-" ? "" : displayName;
|
|
97
103
|
const current = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps);
|
|
98
104
|
const existing = (current.combos ?? []).find(row => row.id === (renameFrom ?? id));
|
|
99
105
|
if (existing?.imageInput === "disabled") combo.imageInput = "disabled";
|
|
106
|
+
if (effortMode === undefined && existing?.defaultEffortMode === "force") {
|
|
107
|
+
combo.defaultEffortMode = effort === "-" ? "fallback" : "force";
|
|
108
|
+
}
|
|
100
109
|
const result = await runtimeRequest("/api/combos", {
|
|
101
110
|
method: "PUT",
|
|
102
111
|
body: JSON.stringify({ id, combo, ...(renameFrom ? { renameFrom } : {}) }),
|
|
@@ -4,7 +4,7 @@ import { getConfigPath, mutatePersistedConfig, readConfigDiagnostics, sanitizeMo
|
|
|
4
4
|
import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../reasoning-effort";
|
|
5
5
|
import type { OcxConfig } from "../types";
|
|
6
6
|
import { normalizeVisionReasoningForModel } from "../vision/reasoning";
|
|
7
|
-
import type {
|
|
7
|
+
import type { ServiceApiTokenState } from "../lib/service-secrets";
|
|
8
8
|
import { CliUsageError, printData, rejectArgs, runCliAction, takeFlag } from "./runtime-api";
|
|
9
9
|
|
|
10
10
|
const USAGE = `Usage:
|
|
@@ -38,9 +38,8 @@ const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
|
38
38
|
* `client` block, which is the same defect in miniature: the presence of configuration is not
|
|
39
39
|
* evidence that the connection works, and a machine whose data-plane token was revoked, rotated
|
|
40
40
|
* away or deleted would have been labelled `connected: true` while it could not reach the hub at
|
|
41
|
-
* all.
|
|
42
|
-
*
|
|
43
|
-
* pure and testable.
|
|
41
|
+
* all. The read-only projection compares the bounded token file's fingerprint against the
|
|
42
|
+
* connection record, and passes that answer in so this formatter stays pure and testable.
|
|
44
43
|
*
|
|
45
44
|
* Synthetic and NOT persisted, for two reasons. `clientConnectionSchema` is `.strict()`, so a
|
|
46
45
|
* `client.note` field would not validate; and persisted prose drifts from the behaviour it
|
|
@@ -49,7 +48,7 @@ const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
|
49
48
|
*/
|
|
50
49
|
export function remoteHubConfigNote(
|
|
51
50
|
config: OcxConfig,
|
|
52
|
-
readConnection: () =>
|
|
51
|
+
readConnection: () => RemoteHubConnectionObservation,
|
|
53
52
|
): { connected: boolean; origin: string; note: string } | null {
|
|
54
53
|
if (config.runtimeRole !== "client" || !config.client) return null;
|
|
55
54
|
// A thunk, so a standalone or hub install pays nothing: the guard above returns first and the
|
|
@@ -67,6 +66,36 @@ export function remoteHubConfigNote(
|
|
|
67
66
|
return { connected, origin: config.client.serverUrl, note };
|
|
68
67
|
}
|
|
69
68
|
|
|
69
|
+
export type RemoteHubConnectionObservation = {
|
|
70
|
+
state: "disconnected" | "connected" | "invalid" | "mismatched";
|
|
71
|
+
reason?: string;
|
|
72
|
+
token: "owned" | "missing" | "changed" | "unsafe";
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function remoteHubConnectionFromTokenState(
|
|
76
|
+
config: Pick<OcxConfig, "client">,
|
|
77
|
+
tokenState: ServiceApiTokenState,
|
|
78
|
+
): RemoteHubConnectionObservation {
|
|
79
|
+
const token = tokenState.kind === "absent"
|
|
80
|
+
? "missing"
|
|
81
|
+
: tokenState.kind === "unsafe"
|
|
82
|
+
? "unsafe"
|
|
83
|
+
: tokenState.fingerprint === config.client?.tokenFingerprint ? "owned" : "changed";
|
|
84
|
+
return { state: "connected", token };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readRemoteHubConfigNote(config: OcxConfig): Promise<ReturnType<typeof remoteHubConfigNote>> {
|
|
88
|
+
if (config.runtimeRole !== "client" || !config.client) return null;
|
|
89
|
+
// This display command needs only connection ownership, not lifecycle recovery, catalog
|
|
90
|
+
// readiness, or any write-capable connect machinery. Keep the read on the bounded token
|
|
91
|
+
// observer so a cold `config show` never imports the full connect command graph.
|
|
92
|
+
const { readServiceApiTokenState } = await import("../lib/service-secrets");
|
|
93
|
+
return remoteHubConfigNote(
|
|
94
|
+
config,
|
|
95
|
+
() => remoteHubConnectionFromTokenState(config, readServiceApiTokenState()),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
function redact(value: unknown, key = ""): unknown {
|
|
71
100
|
if (SECRET_KEYS.test(key) && typeof value === "string") return value ? "********" : value;
|
|
72
101
|
// `client.priorCatalog` is the base64 catalog snapshot connect took before overwriting the
|
|
@@ -168,19 +197,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
|
|
|
168
197
|
rejectArgs(args, USAGE);
|
|
169
198
|
const diagnostics = readConfigDiagnostics();
|
|
170
199
|
const redacted = redact(diagnostics.config);
|
|
171
|
-
|
|
172
|
-
// in, and `ocx config get/set` has no use for it.
|
|
173
|
-
const { collectClientConnectionStatus } = await import("./connect");
|
|
174
|
-
// The readiness probe is declined explicitly. `collectClientConnectionStatus` observes the
|
|
175
|
-
// local Codex ladder for a connected client, and observing it spawns `codex debug models`
|
|
176
|
-
// under a 45s budget. `ocx config show` reads only `state`, `reason` and `token` from the
|
|
177
|
-
// result, so paying for a subprocess here would buy nothing and would quietly turn a
|
|
178
|
-
// read-only config dump into a runtime probe. Returning no ladder resolves readiness to
|
|
179
|
-
// `unverified`, which is the honest answer for a caller that never asked.
|
|
180
|
-
const note = remoteHubConfigNote(
|
|
181
|
-
diagnostics.config,
|
|
182
|
-
() => collectClientConnectionStatus(undefined, undefined, { supportedEfforts: () => null }),
|
|
183
|
-
);
|
|
200
|
+
const note = await readRemoteHubConfigNote(diagnostics.config);
|
|
184
201
|
// First key, not last: it has to be read before the empty `providers` map that misled a
|
|
185
202
|
// reader into concluding nothing was configured anywhere.
|
|
186
203
|
const config = note && redacted && typeof redacted === "object" && !Array.isArray(redacted)
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "../codex/desired-state";
|
|
25
25
|
import { syncModelsToCodex } from "../codex/sync";
|
|
26
26
|
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
|
|
27
|
-
import { restoreNativeCodexAsync } from "../codex/inject";
|
|
27
|
+
import { restoreNativeCodexAsync, type CodexNativeRestoreResult } from "../codex/inject";
|
|
28
28
|
import { stripGrokConfig } from "../grok/inject";
|
|
29
29
|
import { handleRestartScopeAfterWrite, readRestartScope, type RestartScope } from "./restart-scope";
|
|
30
30
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
@@ -106,6 +106,7 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
106
106
|
restore: async deps => {
|
|
107
107
|
const restoreArgs = deps.args.slice(1);
|
|
108
108
|
const restoreJson = takeFlag(restoreArgs, "--json");
|
|
109
|
+
const removeProviderTable = takeFlag(restoreArgs, "--remove-codex-provider-table");
|
|
109
110
|
if (restoreArgs[0] === "back") {
|
|
110
111
|
// Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
|
|
111
112
|
// lifecycle — the counterpart of `ocx restore`. Start/stop triggers are unchanged;
|
|
@@ -146,6 +147,9 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
146
147
|
const target = collectOrcaCodexHomeDiagnostic();
|
|
147
148
|
return emitBack(true, `Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`, 0);
|
|
148
149
|
}
|
|
150
|
+
if (removeProviderTable && !restoreJson) {
|
|
151
|
+
console.log("⚠️ Removing [model_providers.opencodex] means conversations already tagged opencodex will stop opening.");
|
|
152
|
+
}
|
|
149
153
|
const desired = setIntegrationEnabled("codex", false);
|
|
150
154
|
if (!desired.ok) {
|
|
151
155
|
if (restoreJson) {
|
|
@@ -191,9 +195,9 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
191
195
|
return grokCode;
|
|
192
196
|
}
|
|
193
197
|
}
|
|
194
|
-
let r:
|
|
198
|
+
let r: CodexNativeRestoreResult | Pick<CodexNativeRestoreResult, "success" | "message">;
|
|
195
199
|
try {
|
|
196
|
-
r = await restoreNativeCodexAsync({ revalidateDesiredState: true });
|
|
200
|
+
r = await restoreNativeCodexAsync({ revalidateDesiredState: true, removeProviderTable });
|
|
197
201
|
} catch (err) {
|
|
198
202
|
r = { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
199
203
|
}
|
|
@@ -232,7 +236,16 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
232
236
|
code = 1;
|
|
233
237
|
}
|
|
234
238
|
if (r.success) {
|
|
235
|
-
|
|
239
|
+
const retained = "retainedCodexProviderTable" in r ? r.retainedCodexProviderTable : undefined;
|
|
240
|
+
if (retained) {
|
|
241
|
+
console.log("Codex integration is OFF and plain `codex` now runs natively.");
|
|
242
|
+
console.log("The following lines remain in $CODEX_HOME/config.toml because conversations already tagged opencodex resolve their provider only through this table:");
|
|
243
|
+
console.log(retained.lines.join("\n"));
|
|
244
|
+
console.log(`Follow-up: ${retained.followUp}`);
|
|
245
|
+
console.log("Switch back with: ocx restore back");
|
|
246
|
+
} else {
|
|
247
|
+
console.log("Codex integration is OFF and plain `codex` now runs natively. Switch back with: ocx restore back");
|
|
248
|
+
}
|
|
236
249
|
console.log(`Note: ${OCX_NATIVE_REPLAY_RECOVERY_NOTE}`);
|
|
237
250
|
} else {
|
|
238
251
|
console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
|
package/src/cli/index.ts
CHANGED
|
@@ -13,9 +13,22 @@ try {
|
|
|
13
13
|
/* best-effort */
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
currentExternalCodexModelProvider,
|
|
18
|
+
restoreNativeCodex,
|
|
19
|
+
restoreNativeCodexAsync,
|
|
20
|
+
shouldInjectApiAuthHeader,
|
|
21
|
+
} from "../codex/inject";
|
|
22
|
+
// Straight from the owning modules rather than the facade: these are teardown-reporting
|
|
23
|
+
// helpers, not part of the injection surface, and `inject.ts` sits under a size cap that
|
|
24
|
+
// exists to stop it collecting exactly this kind of passthrough.
|
|
25
|
+
import { readOcxProviderTableBlock } from "../codex/inject/remove";
|
|
26
|
+
import {
|
|
27
|
+
describeRetainedCodexProviderTable,
|
|
28
|
+
type RetainedCodexProviderTable,
|
|
29
|
+
} from "../codex/inject/restore";
|
|
17
30
|
import { stripGrokConfig } from "../grok/inject";
|
|
18
|
-
import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../update/stop-contract.mjs";
|
|
31
|
+
import { STOP_HISTORY_DEFERRED_EXIT_CODE, STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../update/stop-contract.mjs";
|
|
19
32
|
import {
|
|
20
33
|
describeHistoryJobFailure,
|
|
21
34
|
resolveCodexHistoryJobTarget,
|
|
@@ -46,6 +59,7 @@ import {
|
|
|
46
59
|
isPendingTeardownAbandoned,
|
|
47
60
|
listPendingTeardowns,
|
|
48
61
|
pendingTeardownPathFor,
|
|
62
|
+
pendingTeardownsAreExactly,
|
|
49
63
|
quarantinePendingTeardown,
|
|
50
64
|
} from "../config/pending-teardown";
|
|
51
65
|
import { collectStatus, hubStatusLines, remoteHubBannerLine, remoteHubStatusLines, unusedProxyWarningLines } from "./status";
|
|
@@ -118,6 +132,12 @@ function reportShellHookFailure(result: { state: "installed" | "absent" | "faile
|
|
|
118
132
|
console.warn(" Check ~/.zshrc for the '# opencodex claude-env hook' block.");
|
|
119
133
|
}
|
|
120
134
|
|
|
135
|
+
function reportRetainedCodexProviderTable(retained: RetainedCodexProviderTable): void {
|
|
136
|
+
console.log(` ${describeRetainedCodexProviderTable(retained)}`);
|
|
137
|
+
console.log(" Retained config lines:");
|
|
138
|
+
for (const line of retained.lines) console.log(` ${line}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
121
141
|
async function refreshOwnedRaycastCatalog(
|
|
122
142
|
config: ReturnType<typeof loadConfig>,
|
|
123
143
|
port: number,
|
|
@@ -785,20 +805,44 @@ async function handleRestartStartWhenStopped(): Promise<boolean | "skipped"> {
|
|
|
785
805
|
*
|
|
786
806
|
* The distinction exists because `ocx update` must proceed for the first and abort for the
|
|
787
807
|
* second, and it can only see an exit code (#3008).
|
|
808
|
+
*
|
|
809
|
+
* `historyDeferred` is the third kind (#4718). The Codex history preflight refuses BEFORE
|
|
810
|
+
* the config half runs, so nothing was restored at all: config, catalog, history and
|
|
811
|
+
* provenance are untouched and the client is still routed at the proxy that just stopped.
|
|
812
|
+
* Like `historyOnly` the proxy is genuinely down, so an update may replace package files.
|
|
813
|
+
* Unlike `historyOnly` the obligation was not performed, so the receipt must survive.
|
|
788
814
|
*/
|
|
789
|
-
async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boolean; other: boolean }> {
|
|
815
|
+
async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boolean; historyDeferred: boolean; other: boolean }> {
|
|
790
816
|
let historyOnly = false;
|
|
817
|
+
let historyDeferred = false;
|
|
791
818
|
let other = false;
|
|
792
819
|
try {
|
|
793
820
|
const result = await restoreNativeCodexAsync();
|
|
794
|
-
if (result.success)
|
|
821
|
+
if (result.success) {
|
|
822
|
+
console.log(`↩️ ${result.message}`);
|
|
823
|
+
if (result.retainedCodexProviderTable) {
|
|
824
|
+
reportRetainedCodexProviderTable(result.retainedCodexProviderTable);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
795
827
|
else {
|
|
796
828
|
// Codex history is the one restore whose failure leaves the runtime consistent: the
|
|
797
829
|
// manifest is retained and the routed metadata is untouched. Config and catalog are
|
|
798
830
|
// not — a client reads those, so their failure is a real teardown failure.
|
|
799
831
|
const artifacts = result.artifacts;
|
|
800
832
|
const configOrCatalogFailed = artifacts.config.state === "failed" || artifacts.catalog.state === "failed";
|
|
801
|
-
|
|
833
|
+
// A preflight refusal reports every artifact as `skipped` because none of them were
|
|
834
|
+
// attempted. Reading the states alone cannot tell that apart from an ownership
|
|
835
|
+
// refusal, so the structured reason carries it and the states are still required to
|
|
836
|
+
// agree — a refusal that somehow reports a failed artifact is not this case.
|
|
837
|
+
// A degraded restore has no refusal reason and reports config as partial, so it cannot
|
|
838
|
+
// enter this branch: its config obligation was discharged and the stop receipt must be
|
|
839
|
+
// released rather than preserved.
|
|
840
|
+
const preflightRefused = result.historyPreflightRefusal !== undefined
|
|
841
|
+
&& artifacts.config.state === "skipped"
|
|
842
|
+
&& artifacts.catalog.state === "skipped"
|
|
843
|
+
&& artifacts.history.state === "skipped";
|
|
844
|
+
if (preflightRefused) historyDeferred = true;
|
|
845
|
+
else if (!configOrCatalogFailed && artifacts.history.state === "failed") historyOnly = true;
|
|
802
846
|
else other = true;
|
|
803
847
|
console.error(`⚠️ ${result.message}`);
|
|
804
848
|
}
|
|
@@ -816,7 +860,7 @@ async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boole
|
|
|
816
860
|
other = true;
|
|
817
861
|
console.error(`⚠️ Grok config restore failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
818
862
|
}
|
|
819
|
-
return { historyOnly, other };
|
|
863
|
+
return { historyOnly, historyDeferred, other };
|
|
820
864
|
}
|
|
821
865
|
|
|
822
866
|
async function handleStop() {
|
|
@@ -860,6 +904,11 @@ async function handleStop() {
|
|
|
860
904
|
};
|
|
861
905
|
let stopFailed = false;
|
|
862
906
|
let historyOnlyFailure = false;
|
|
907
|
+
/**
|
|
908
|
+
* Obligations this run deliberately kept because the Codex history preflight refused
|
|
909
|
+
* before restoring anything (#4718). Non-null selects the deferred exit code.
|
|
910
|
+
*/
|
|
911
|
+
let historyDeferredNonces: string[] | null = null;
|
|
863
912
|
// Only Task Scheduler respawns after a successful stop (#764), so only it earns the
|
|
864
913
|
// restart-window wait; launchd, systemd and WinSW are down when they say so.
|
|
865
914
|
let schedulerCanRespawn = false;
|
|
@@ -1138,6 +1187,7 @@ async function handleStop() {
|
|
|
1138
1187
|
}
|
|
1139
1188
|
const restore = await restoreSharedClientStateAfterStop();
|
|
1140
1189
|
if (restore.other) stopFailed = true;
|
|
1190
|
+
else if (restore.historyDeferred) historyDeferredNonces = teardownNonce ? [teardownNonce, ...recoveredNonces] : recoveredNonces;
|
|
1141
1191
|
else if (restore.historyOnly) historyOnlyFailure = true;
|
|
1142
1192
|
// The obligation is discharged whether or not history metadata finalized: config and
|
|
1143
1193
|
// catalog are what a client reads, and `restore.other` already fails the stop.
|
|
@@ -1145,7 +1195,16 @@ async function handleStop() {
|
|
|
1145
1195
|
// Each nonce names its own file, so a clear can only ever remove the obligation it
|
|
1146
1196
|
// names — never one a concurrent stop wrote. Both this run's claim and every inherited
|
|
1147
1197
|
// receipt it proved discharged are released together.
|
|
1148
|
-
|
|
1198
|
+
//
|
|
1199
|
+
// A history-preflight refusal is the exception: it restored nothing, so there is
|
|
1200
|
+
// nothing to discharge. Clearing here would drop a real obligation on the floor and
|
|
1201
|
+
// leave the client config pointing at a proxy that is gone, with nothing on disk
|
|
1202
|
+
// saying so — which is the whole failure the receipt exists to prevent (#4718).
|
|
1203
|
+
if (restore.historyDeferred) {
|
|
1204
|
+
console.error(" The shared teardown was refused before it changed anything, so it is still owed.");
|
|
1205
|
+
console.error(" Its receipt is preserved; run 'ocx stop' again once Codex is closed to retry the restore.");
|
|
1206
|
+
}
|
|
1207
|
+
if (!restore.other && !restore.historyDeferred) {
|
|
1149
1208
|
const discharged = teardownNonce ? [teardownNonce, ...recoveredNonces] : recoveredNonces;
|
|
1150
1209
|
for (const nonce of discharged) {
|
|
1151
1210
|
// A receipt that survives its discharge re-triggers recovery forever, so a failed
|
|
@@ -1185,6 +1244,17 @@ async function handleStop() {
|
|
|
1185
1244
|
// still wins: it is the stronger signal.
|
|
1186
1245
|
if (stopFailed) process.exitCode = 1;
|
|
1187
1246
|
else if (historyOnlyFailure) process.exitCode = STOP_HISTORY_INCOMPLETE_EXIT_CODE;
|
|
1247
|
+
// The deferred code says "the only obligations left are the ones I just decided to
|
|
1248
|
+
// keep". It is read across a process boundary by an updater that will replace package
|
|
1249
|
+
// files on the strength of it, so this run has to be able to prove the claim: if any
|
|
1250
|
+
// other obligation is sitting in the home — quarantined, or a concurrent stop's — the
|
|
1251
|
+
// claim is false and the ordinary failure code is the honest answer. That is also the
|
|
1252
|
+
// behaviour before #4718, so the fallback loses nothing that used to work.
|
|
1253
|
+
else if (historyDeferredNonces) {
|
|
1254
|
+
process.exitCode = pendingTeardownsAreExactly(historyDeferredNonces)
|
|
1255
|
+
? STOP_HISTORY_DEFERRED_EXIT_CODE
|
|
1256
|
+
: 1;
|
|
1257
|
+
}
|
|
1188
1258
|
return !stopFailed;
|
|
1189
1259
|
}
|
|
1190
1260
|
|
|
@@ -1314,6 +1384,9 @@ async function handleUninstall() {
|
|
|
1314
1384
|
await runStep("native Codex restored", async () => {
|
|
1315
1385
|
const r = await restoreNativeCodexAsync();
|
|
1316
1386
|
if (!r.success) throw new Error(r.message);
|
|
1387
|
+
if (r.retainedCodexProviderTable) {
|
|
1388
|
+
reportRetainedCodexProviderTable(r.retainedCodexProviderTable);
|
|
1389
|
+
}
|
|
1317
1390
|
});
|
|
1318
1391
|
|
|
1319
1392
|
await runStep("Grok Build config restored", () => {
|
|
@@ -1460,6 +1533,18 @@ async function handleStatus() {
|
|
|
1460
1533
|
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}${local}`);
|
|
1461
1534
|
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}${local}`);
|
|
1462
1535
|
console.log(` ${formatStartupRoutingDetail(status.json.startup)}${local}`);
|
|
1536
|
+
if (status.json.startup.routingKind === "native") {
|
|
1537
|
+
let retainedProviderTable = false;
|
|
1538
|
+
try {
|
|
1539
|
+
retainedProviderTable = readOcxProviderTableBlock() !== null;
|
|
1540
|
+
} catch {
|
|
1541
|
+
// The routing snapshot owns unreadable-config reporting. A later read race must not
|
|
1542
|
+
// turn this diagnostic command into a teardown failure.
|
|
1543
|
+
}
|
|
1544
|
+
if (retainedProviderTable) {
|
|
1545
|
+
console.log(` ⚠️ Codex provider table retained${local}: [model_providers.opencodex] remains while root routing is native. Remove with 'ocx restore --remove-codex-provider-table'; tagged conversations will stop opening.`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1463
1548
|
console.log(` Service: ${status.json.service.summary}${local}`);
|
|
1464
1549
|
console.log(` ${status.json.codexShim.summary}${local}`);
|
|
1465
1550
|
console.log(` Codex runtime: ${status.json.codexRuntime.path}${local}`);
|
package/src/cli/registry.ts
CHANGED
|
@@ -407,7 +407,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
407
407
|
"Ensures the proxy is running, then execs `claude` with ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN,",
|
|
408
408
|
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 and model slots from config.claudeCode.",
|
|
409
409
|
"When Claude routing is explicitly disabled, it launches natively after removing proven OpenCodex-owned proxy state.",
|
|
410
|
-
"Routed models appear in the native /model picker with stable claude-opus-4-8-
|
|
410
|
+
"Routed models appear in the native /model picker with stable claude-opus-4-8-YYYYMMDD slot aliases,",
|
|
411
|
+
"where the year runs 2026-2035 and 2026 slots are allocated first (Claude Code >= 2.1.129).",
|
|
411
412
|
"Older versions: pick models via ANTHROPIC_MODEL or /model <id> directly (any string passes through).",
|
|
412
413
|
"User-exported ANTHROPIC_* variables take precedence for routed launches; native fallback removes only proven OpenCodex-owned proxy values.",
|
|
413
414
|
"",
|