@bitkyc08/opencodex 2.65.0 → 2.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/gui/dist/assets/App-DyHlFeNB.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-CI8cVKWu.js} +1 -1
- package/gui/dist/assets/index-BiwnVAUm.js +86 -0
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-_WyYzbMt.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
package/src/combos/resolve.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./failover";
|
|
11
11
|
import { quotaResetRemainingMs } from "./reset-window";
|
|
12
12
|
import { getCombo, resolveComboId, targetKey } from "./types";
|
|
13
|
-
import type { NormalizedComboConfig } from "./types";
|
|
13
|
+
import type { NormalizedComboConfig, NormalizedComboTarget } from "./types";
|
|
14
14
|
import {
|
|
15
15
|
captureConfigGeneration,
|
|
16
16
|
type GenerationContext,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
|
|
19
19
|
export interface ComboPick {
|
|
20
20
|
comboId: string;
|
|
21
|
-
target:
|
|
21
|
+
target: NormalizedComboTarget;
|
|
22
22
|
targetIndex: number;
|
|
23
23
|
attempted: string[];
|
|
24
24
|
writerGeneration: number;
|
|
@@ -138,9 +138,9 @@ export function quotaInactiveReason(
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function smoothWeightedIndex(
|
|
141
|
-
targets:
|
|
141
|
+
targets: NormalizedComboTarget[],
|
|
142
142
|
state: SelectionState,
|
|
143
|
-
eligible: (target:
|
|
143
|
+
eligible: (target: NormalizedComboTarget) => boolean,
|
|
144
144
|
): number {
|
|
145
145
|
let best = -1;
|
|
146
146
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
@@ -175,8 +175,8 @@ function smoothWeightedIndex(
|
|
|
175
175
|
*/
|
|
176
176
|
function resetWindowIndex(
|
|
177
177
|
config: OcxConfig,
|
|
178
|
-
targets:
|
|
179
|
-
eligible: (target:
|
|
178
|
+
targets: NormalizedComboTarget[],
|
|
179
|
+
eligible: (target: NormalizedComboTarget) => boolean,
|
|
180
180
|
now = Date.now(),
|
|
181
181
|
): number {
|
|
182
182
|
let selected = -1;
|
|
@@ -202,7 +202,7 @@ export function pickComboTarget(
|
|
|
202
202
|
comboId: string,
|
|
203
203
|
options: {
|
|
204
204
|
exclude?: Iterable<string>;
|
|
205
|
-
eligible?: (target:
|
|
205
|
+
eligible?: (target: NormalizedComboTarget) => boolean;
|
|
206
206
|
now?: number;
|
|
207
207
|
} = {},
|
|
208
208
|
): ComboPick | null {
|
|
@@ -211,7 +211,7 @@ export function pickComboTarget(
|
|
|
211
211
|
if (!combo) throw new UnknownComboError(comboId);
|
|
212
212
|
const excluded = new Set(options.exclude ?? []);
|
|
213
213
|
const now = options.now ?? Date.now();
|
|
214
|
-
const eligible = (target:
|
|
214
|
+
const eligible = (target: NormalizedComboTarget): boolean =>
|
|
215
215
|
targetProviderIsUsable(config, target, now)
|
|
216
216
|
&& !isComboTargetInCooldown(comboId, target, now)
|
|
217
217
|
&& !excluded.has(targetKey(target))
|
|
@@ -291,7 +291,7 @@ export function pickComboTarget(
|
|
|
291
291
|
export function noteComboSuccess(
|
|
292
292
|
comboId: string,
|
|
293
293
|
combo: NormalizedComboConfig,
|
|
294
|
-
target:
|
|
294
|
+
target: NormalizedComboTarget,
|
|
295
295
|
writerGeneration = captureConfigGeneration(),
|
|
296
296
|
): void {
|
|
297
297
|
const key = targetKey(target);
|
|
@@ -336,8 +336,9 @@ export function advanceComboAfterFailure(
|
|
|
336
336
|
resetAt?: unknown | unknown[];
|
|
337
337
|
now?: number;
|
|
338
338
|
cooldownMs?: number;
|
|
339
|
-
eligible?: (target:
|
|
339
|
+
eligible?: (target: NormalizedComboTarget) => boolean;
|
|
340
340
|
cooldownScope?: ComboFailureCooldownScope;
|
|
341
|
+
onCooldownRecorded?: (target: Pick<OcxComboTarget, "provider" | "model">) => void;
|
|
341
342
|
status?: number;
|
|
342
343
|
code?: string | null;
|
|
343
344
|
message?: string;
|
|
@@ -352,11 +353,12 @@ export function advanceComboAfterFailure(
|
|
|
352
353
|
? combo.targets.filter(target => target.provider === pick.target.provider)
|
|
353
354
|
: [pick.target];
|
|
354
355
|
for (const target of cooldownTargets) {
|
|
355
|
-
coolComboTarget(pick.comboId, target, {
|
|
356
|
+
const recorded = coolComboTarget(pick.comboId, target, {
|
|
356
357
|
...options,
|
|
357
358
|
cooldownMs: options.cooldownMs ?? combo?.cooldownMs,
|
|
358
359
|
writerGeneration: pick.writerGeneration,
|
|
359
360
|
});
|
|
361
|
+
if (recorded) options.onCooldownRecorded?.(target);
|
|
360
362
|
}
|
|
361
363
|
}
|
|
362
364
|
// #5691: under `cooldownWaitPolicy: "before-last-resort"` this final synchronous pick
|
|
@@ -380,7 +382,7 @@ export async function pickComboTargetWithWait(
|
|
|
380
382
|
comboId: string,
|
|
381
383
|
options: {
|
|
382
384
|
exclude?: Iterable<string>;
|
|
383
|
-
eligible?: (target:
|
|
385
|
+
eligible?: (target: NormalizedComboTarget) => boolean;
|
|
384
386
|
waitForCooldownMs: number;
|
|
385
387
|
abortSignal?: AbortSignal;
|
|
386
388
|
now?: number;
|
|
@@ -390,10 +392,10 @@ export async function pickComboTargetWithWait(
|
|
|
390
392
|
const now = options.now ?? Date.now();
|
|
391
393
|
const excluded = new Set(options.exclude ?? []);
|
|
392
394
|
const customEligible = options.eligible;
|
|
393
|
-
const eligibleAt = (target:
|
|
395
|
+
const eligibleAt = (target: NormalizedComboTarget, at: number): boolean =>
|
|
394
396
|
!isComboTargetInCooldown(comboId, target, at)
|
|
395
397
|
&& (customEligible?.(target) ?? true);
|
|
396
|
-
const eligible = (target:
|
|
398
|
+
const eligible = (target: NormalizedComboTarget): boolean => eligibleAt(target, now);
|
|
397
399
|
// Milliseconds already slept inside this call. `waitForCooldownMs` is documented as a cap
|
|
398
400
|
// per *selection attempt*, so a deferral wait and the ordinary wait below must share it —
|
|
399
401
|
// otherwise a 3s deferral followed by a 9s ordinary wait spends 12s against a 10s budget.
|
|
@@ -411,7 +413,7 @@ export async function pickComboTargetWithWait(
|
|
|
411
413
|
const defersLastResort = policyCombo?.cooldownWaitPolicy === "before-last-resort"
|
|
412
414
|
&& policyCombo.targets.some(target => !target.lastResort);
|
|
413
415
|
if (defersLastResort && !options.abortSignal?.aborted) {
|
|
414
|
-
const normalOnly = (target:
|
|
416
|
+
const normalOnly = (target: NormalizedComboTarget): boolean =>
|
|
415
417
|
!target.lastResort && eligible(target);
|
|
416
418
|
const normalPick = pickComboTarget(config, comboId, {
|
|
417
419
|
exclude: excluded,
|
package/src/combos/types.ts
CHANGED
|
@@ -20,6 +20,15 @@ export interface ComboValidationIssue {
|
|
|
20
20
|
message: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface NormalizedComboTarget {
|
|
24
|
+
provider: string;
|
|
25
|
+
model: string;
|
|
26
|
+
weight: number;
|
|
27
|
+
/** Emergency-only target, deferred under `cooldownWaitPolicy` (#5691). */
|
|
28
|
+
lastResort: boolean;
|
|
29
|
+
reasoningEfforts?: OcxComboDefaultEffort[];
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
export interface NormalizedComboConfig {
|
|
24
33
|
strategy: OcxComboStrategy;
|
|
25
34
|
stickyLimit: number;
|
|
@@ -40,7 +49,7 @@ export interface NormalizedComboConfig {
|
|
|
40
49
|
nativeAlias: boolean;
|
|
41
50
|
/** Display-only label for the catalog row, or null when unset. */
|
|
42
51
|
displayName: string | null;
|
|
43
|
-
targets:
|
|
52
|
+
targets: NormalizedComboTarget[];
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
/**
|
|
@@ -142,8 +151,9 @@ export function comboConfigIssues(
|
|
|
142
151
|
&& body.strategy !== "round-robin"
|
|
143
152
|
&& body.strategy !== "random"
|
|
144
153
|
&& body.strategy !== "least-used"
|
|
145
|
-
&& body.strategy !== "reset-window"
|
|
146
|
-
|
|
154
|
+
&& body.strategy !== "reset-window"
|
|
155
|
+
&& body.strategy !== "jev") {
|
|
156
|
+
issues.push({ path: ["strategy"], message: 'strategy must be "failover", "round-robin", "random", "least-used", "reset-window", or "jev"' });
|
|
147
157
|
}
|
|
148
158
|
if (body.stickyLimit !== undefined
|
|
149
159
|
&& (typeof body.stickyLimit !== "number" || !Number.isInteger(body.stickyLimit)
|
|
@@ -269,6 +279,11 @@ export function comboConfigIssues(
|
|
|
269
279
|
path: ["targets", i, "provider"],
|
|
270
280
|
message: `targets[${i}].provider "${provider}" is not configured`,
|
|
271
281
|
});
|
|
282
|
+
} else if (providers[provider]?.adapter === "jev-decision") {
|
|
283
|
+
issues.push({
|
|
284
|
+
path: ["targets", i, "provider"],
|
|
285
|
+
message: `targets[${i}].provider "${provider}" is a decision service and cannot be a model target`,
|
|
286
|
+
});
|
|
272
287
|
} else {
|
|
273
288
|
configuredProviderCount += 1;
|
|
274
289
|
if (providers[provider]?.disabled !== true) enabledProviderCount += 1;
|
|
@@ -286,6 +301,32 @@ export function comboConfigIssues(
|
|
|
286
301
|
message: `targets[${i}].weight must be an integer from 1 to 10000`,
|
|
287
302
|
});
|
|
288
303
|
}
|
|
304
|
+
if (target.reasoningEfforts !== undefined) {
|
|
305
|
+
if (!Array.isArray(target.reasoningEfforts) || target.reasoningEfforts.length === 0) {
|
|
306
|
+
issues.push({
|
|
307
|
+
path: ["targets", i, "reasoningEfforts"],
|
|
308
|
+
message: `targets[${i}].reasoningEfforts must be a non-empty array`,
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
const seenEfforts = new Set<OcxComboDefaultEffort>();
|
|
312
|
+
for (let effortIndex = 0; effortIndex < target.reasoningEfforts.length; effortIndex++) {
|
|
313
|
+
const effort = target.reasoningEfforts[effortIndex];
|
|
314
|
+
if (typeof effort !== "string" || !isCodexReasoningEffort(effort)) {
|
|
315
|
+
issues.push({
|
|
316
|
+
path: ["targets", i, "reasoningEfforts", effortIndex],
|
|
317
|
+
message: `targets[${i}].reasoningEfforts[${effortIndex}] must be one of: low, medium, high, xhigh, max, ultra`,
|
|
318
|
+
});
|
|
319
|
+
} else if (seenEfforts.has(effort as OcxComboDefaultEffort)) {
|
|
320
|
+
issues.push({
|
|
321
|
+
path: ["targets", i, "reasoningEfforts", effortIndex],
|
|
322
|
+
message: `targets[${i}].reasoningEfforts must not contain duplicates`,
|
|
323
|
+
});
|
|
324
|
+
} else {
|
|
325
|
+
seenEfforts.add(effort as OcxComboDefaultEffort);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
289
330
|
if (target.lastResort !== undefined && typeof target.lastResort !== "boolean") {
|
|
290
331
|
issues.push({
|
|
291
332
|
path: ["targets", i, "lastResort"],
|
|
@@ -345,6 +386,9 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
|
|
|
345
386
|
provider: target.provider.trim(),
|
|
346
387
|
model: target.model.trim(),
|
|
347
388
|
weight: target.weight ?? 1,
|
|
389
|
+
...(target.reasoningEfforts !== undefined
|
|
390
|
+
? { reasoningEfforts: [...target.reasoningEfforts] }
|
|
391
|
+
: {}),
|
|
348
392
|
lastResort: target.lastResort === true,
|
|
349
393
|
})),
|
|
350
394
|
};
|
|
@@ -73,6 +73,8 @@ export interface AtomicWriteHooks {
|
|
|
73
73
|
afterTempWrite?: (tempPath: string, targetPath: string) => void;
|
|
74
74
|
beforeRename?: (tempPath: string, targetPath: string) => void;
|
|
75
75
|
validateBeforeRename?: (targetPath: string) => void;
|
|
76
|
+
/** Publication receipt; runs before any post-rename cleanup can fail. */
|
|
77
|
+
afterRename?: (targetPath: string) => void;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
export class AtomicWriteResidualTempError extends Error {
|
|
@@ -314,6 +316,7 @@ function atomicWriteFileToTarget(
|
|
|
314
316
|
hooks.beforeRename?.(tmp, target);
|
|
315
317
|
hooks.validateBeforeRename?.(target);
|
|
316
318
|
effective.rename(tmp, target);
|
|
319
|
+
hooks.afterRename?.(target);
|
|
317
320
|
// The rename is only as durable as the directory entry recording it. Fsyncing the temp's
|
|
318
321
|
// CONTENT and then losing the entry in a power cut leaves the old file in place, or the
|
|
319
322
|
// directory in an indeterminate state, while the caller was told the replacement landed.
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
projectConfigRebaseProvenance,
|
|
15
15
|
} from "./rebase-provenance";
|
|
16
16
|
import { withConfigMutationLockSync, bumpGenerationForCooperatingConfigWrite } from "./mutation-lock";
|
|
17
|
-
import { persistConfigUnlocked, readRawConfigJson } from "./persist-unlocked";
|
|
17
|
+
import { ConfigWritePublishedError, persistConfigUnlocked, readRawConfigJson } from "./persist-unlocked";
|
|
18
18
|
import { configDiagnosticsFromRaw, readConfigDiagnostics } from "./diagnostics";
|
|
19
19
|
import { normalizePersistedClaudeCode } from "./load-degrade";
|
|
20
20
|
|
|
@@ -425,7 +425,13 @@ function readPersistedServerBinding(
|
|
|
425
425
|
export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
426
426
|
const pinError = configReasoningPinsConfigError(config);
|
|
427
427
|
if (pinError) throw new Error(pinError);
|
|
428
|
-
|
|
428
|
+
let published = false;
|
|
429
|
+
const persist = (candidate: OcxConfig): void => {
|
|
430
|
+
const changed = persistConfigUnlocked(candidate);
|
|
431
|
+
published = true;
|
|
432
|
+
if (changed) bumpGenerationForCooperatingConfigWrite();
|
|
433
|
+
};
|
|
434
|
+
const save = () => withConfigMutationLockSync(() => {
|
|
429
435
|
const childDeletions = prepareConfigObjectChildDeletionRebase(config);
|
|
430
436
|
const bindingBaseline = persistedLiveServerBinding.get(config);
|
|
431
437
|
// One authoritative pre-write read feeds both the live-config reconciliation and
|
|
@@ -510,10 +516,10 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
510
516
|
const persistedConfig: OcxConfig = { ...projectedConfig, port: persistedBinding.port };
|
|
511
517
|
if (persistedBinding.hostname === undefined) delete persistedConfig.hostname;
|
|
512
518
|
else persistedConfig.hostname = persistedBinding.hostname;
|
|
513
|
-
|
|
519
|
+
persist(persistedConfig);
|
|
514
520
|
persistedLiveServerBinding.set(config, persistedBinding);
|
|
515
521
|
} else {
|
|
516
|
-
|
|
522
|
+
persist(projectedConfig);
|
|
517
523
|
}
|
|
518
524
|
adoptCustomModelCatalogMigration(config, projectedConfig);
|
|
519
525
|
if (claudeCodeBaseline.has(config)) {
|
|
@@ -527,4 +533,14 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
527
533
|
clearPendingConfigObjectChildDeletions(config);
|
|
528
534
|
clearPendingConfigTopLevelDeletions(config);
|
|
529
535
|
});
|
|
536
|
+
try {
|
|
537
|
+
save();
|
|
538
|
+
} catch (error) {
|
|
539
|
+
// Generation/baseline updates and the lock's COMMIT run after publication.
|
|
540
|
+
// They may fail, but the file replacement cannot be undone by a live rollback.
|
|
541
|
+
if (published && !(error instanceof ConfigWritePublishedError)) {
|
|
542
|
+
throw new ConfigWritePublishedError(error);
|
|
543
|
+
}
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
530
546
|
}
|
|
@@ -8,6 +8,14 @@ import { getConfigPath } from "./paths";
|
|
|
8
8
|
import { configRebaseDeletionKeys, projectConfigRebaseProvenance } from "./rebase-provenance";
|
|
9
9
|
import { clientConnectionSchema } from "./schema/leaf-validators";
|
|
10
10
|
|
|
11
|
+
/** The requested bytes are current; callers must not roll back only live state. */
|
|
12
|
+
export class ConfigWritePublishedError extends Error {
|
|
13
|
+
constructor(cause: unknown) {
|
|
14
|
+
super("Config was published, but post-publication bookkeeping failed", { cause });
|
|
15
|
+
this.name = "ConfigWritePublishedError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
/** The literal file, with no schema merge or default injection. */
|
|
12
20
|
export function readRawConfigJson(): Record<string, unknown> | undefined {
|
|
13
21
|
try {
|
|
@@ -81,13 +89,18 @@ export function persistConfigUnlocked(config: OcxConfig): boolean {
|
|
|
81
89
|
// including byte-identical saves: a cooperating CLI process may have written
|
|
82
90
|
// the same bytes (e.g. before a proxy notification), and Logs/Usage must
|
|
83
91
|
// adopt the overlay without waiting for a changed save or restart.
|
|
84
|
-
|
|
92
|
+
let published = unchanged;
|
|
93
|
+
try {
|
|
94
|
+
if (!unchanged) {
|
|
95
|
+
atomicWriteFile(configPath, bytes, undefined, { afterRename: () => { published = true; } });
|
|
96
|
+
published = true;
|
|
97
|
+
}
|
|
98
|
+
// Publication, not successful cache refresh, is the rollback boundary. A
|
|
99
|
+
// byte-identical save already has the requested state on disk as well.
|
|
85
100
|
refreshConfigDerivedRegistries(persisted);
|
|
86
|
-
return
|
|
101
|
+
return !unchanged;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (published) throw new ConfigWritePublishedError(error);
|
|
104
|
+
throw error;
|
|
87
105
|
}
|
|
88
|
-
atomicWriteFile(configPath, bytes);
|
|
89
|
-
// For changed saves, refresh only AFTER the write succeeded so a failed
|
|
90
|
-
// write cannot leave estimates reflecting configuration never persisted.
|
|
91
|
-
refreshConfigDerivedRegistries(persisted);
|
|
92
|
-
return true;
|
|
93
106
|
}
|
|
@@ -79,6 +79,22 @@ export const configSchema = z.object({
|
|
|
79
79
|
privacy: z.object({ maskEmails: z.boolean().optional() }).strict().optional().catch(undefined),
|
|
80
80
|
// Malformed hand edits disable this opt-in exporter. Live writes reject them in diagnostics.ts.
|
|
81
81
|
metricsExport: z.object({ enabled: z.boolean().optional() }).strict().optional().catch(undefined),
|
|
82
|
+
// Kept raw on purpose: `.catch(undefined)` would turn a mistyped `enabled` into "inherit",
|
|
83
|
+
// which can reopen a surface the operator meant to close. src/protocols/settings.ts parses it
|
|
84
|
+
// and fails closed instead.
|
|
85
|
+
apiSurfaces: z.unknown().optional(),
|
|
86
|
+
// Every protocol default is the conservative one (legacy policy, rollout off), so a malformed
|
|
87
|
+
// block dropping to undefined cannot widen behavior.
|
|
88
|
+
protocols: z.object({
|
|
89
|
+
unrepresentable: z.enum(["legacy", "reject"]).optional(),
|
|
90
|
+
rollout: z.object({
|
|
91
|
+
nativeChatCombos: z.boolean().optional(),
|
|
92
|
+
managedMessagesNative: z.boolean().optional(),
|
|
93
|
+
managedMessagesNativeOAuth: z.boolean().optional(),
|
|
94
|
+
directEncoders: z.boolean().optional(),
|
|
95
|
+
shadowPlan: z.boolean().optional(),
|
|
96
|
+
}).strict().optional(),
|
|
97
|
+
}).strict().optional().catch(undefined),
|
|
82
98
|
// A malformed present client block must remain diagnosable from raw config and
|
|
83
99
|
// fail closed through src/client/state.ts; unrelated provider state still loads.
|
|
84
100
|
client: clientConnectionSchema.optional().catch(undefined),
|
|
@@ -779,6 +779,7 @@ export const agentTaskRecoverySchema = z.object({
|
|
|
779
779
|
model: z.string().trim().min(1).optional(),
|
|
780
780
|
timeoutMs: z.number().int().min(1_000).max(120_000).optional(),
|
|
781
781
|
cacheEntries: z.number().int().min(1).max(512).optional(),
|
|
782
|
+
retries: z.number().int().min(0).max(2).optional(),
|
|
782
783
|
}).strict();
|
|
783
784
|
|
|
784
785
|
export const runtimeRoleSchema = z.enum(["standalone", "hub", "client"]);
|
|
@@ -852,6 +853,13 @@ export const remoteGuiConfigSchema = z.object({
|
|
|
852
853
|
|
|
853
854
|
const connectedClientIdSchema = z.enum(["codex", "claude"]);
|
|
854
855
|
const clientTimestampSchema = z.string().datetime({ offset: true });
|
|
856
|
+
const clientTransportSchema = z.enum(["hub", "link"]);
|
|
857
|
+
const linkTransportSchema = z.object({
|
|
858
|
+
// Same range as isLinkPort in src/link/ports.ts, restated here because the config schema sits on
|
|
859
|
+
// every install's core path and must not import link code (tests/lab/core-link-boundary.test.ts).
|
|
860
|
+
tunnelPort: z.number().int().min(1024).max(65535),
|
|
861
|
+
linkId: z.string().regex(/^lnk_[0-9a-f]{16}$/),
|
|
862
|
+
}).strict();
|
|
855
863
|
const clientOriginSchema = z.string().transform((value, ctx) => {
|
|
856
864
|
const origin = canonicalHttpOrigin(value);
|
|
857
865
|
if (!origin) {
|
|
@@ -864,6 +872,8 @@ export const clientConnectionSchema = z.object({
|
|
|
864
872
|
serverUrl: clientOriginSchema,
|
|
865
873
|
managementUrl: clientOriginSchema,
|
|
866
874
|
managementTransport: z.enum(["direct", "relay"]),
|
|
875
|
+
transport: clientTransportSchema.optional(),
|
|
876
|
+
link: linkTransportSchema.optional(),
|
|
867
877
|
selectedClients: z.array(connectedClientIdSchema).min(1).max(2).superRefine((clients, ctx) => {
|
|
868
878
|
if (new Set(clients).size !== clients.length) {
|
|
869
879
|
ctx.addIssue({ code: "custom", message: "must contain unique client ids" });
|
|
@@ -890,7 +900,34 @@ export const clientConnectionSchema = z.object({
|
|
|
890
900
|
ctx.addIssue({ code: "custom", path: ["oldKeyBackupPath"], message: `must equal ${expected}` });
|
|
891
901
|
}
|
|
892
902
|
}).optional(),
|
|
893
|
-
}).strict()
|
|
903
|
+
}).strict().superRefine((connection, ctx) => {
|
|
904
|
+
const transport = connection.transport ?? "hub";
|
|
905
|
+
if (transport === "hub" && connection.link !== undefined) {
|
|
906
|
+
ctx.addIssue({ code: "custom", path: ["link"], message: "link is allowed only when transport is link" });
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (transport !== "link") return;
|
|
910
|
+
if (!connection.link) {
|
|
911
|
+
ctx.addIssue({ code: "custom", path: ["link"], message: "link is required when transport is link" });
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (connection.managementTransport !== "direct") {
|
|
915
|
+
ctx.addIssue({ code: "custom", path: ["managementTransport"], message: "link transport requires direct management transport" });
|
|
916
|
+
}
|
|
917
|
+
if (connection.serverUrl !== connection.managementUrl) {
|
|
918
|
+
ctx.addIssue({ code: "custom", path: ["managementUrl"], message: "link transport requires serverUrl and managementUrl to match" });
|
|
919
|
+
}
|
|
920
|
+
let origin: URL;
|
|
921
|
+
try {
|
|
922
|
+
origin = new URL(connection.serverUrl);
|
|
923
|
+
} catch {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
if (origin.protocol !== "http:" || origin.hostname !== "127.0.0.1"
|
|
927
|
+
|| origin.port !== String(connection.link.tunnelPort)) {
|
|
928
|
+
ctx.addIssue({ code: "custom", path: ["serverUrl"], message: "link transport requires http://127.0.0.1:<tunnelPort>" });
|
|
929
|
+
}
|
|
930
|
+
});
|
|
894
931
|
|
|
895
932
|
/**
|
|
896
933
|
* Codex pool selection policy section.
|