@bitkyc08/opencodex 2.81.0 → 2.82.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 +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +43 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +303 -171
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +6 -2
- package/src/oauth/store.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +1 -0
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +31 -2
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +34 -13
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +19 -2
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +25 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +5 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -3,6 +3,9 @@ import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-sta
|
|
|
3
3
|
import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../../claude/desktop-applied-marker";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { prepareCodexHome } from "../../codex/prepared-home";
|
|
8
|
+
import { codexHomeIsAbsent } from "../../codex/codex-home-owner";
|
|
6
9
|
import type { CatalogModel } from "../../codex/catalog";
|
|
7
10
|
import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
8
11
|
import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError } from "../../config/provider-validation";
|
|
@@ -400,6 +403,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
400
403
|
getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
|
|
401
404
|
getMultiAgentModeHintText, probeCodexSupportsModeHint, setAgentsEnabled, setAgentsMaxDepth,
|
|
402
405
|
setSubagentDeveloperInstructions, setMultiAgentModeHintText, MODE_HINT_UNSUPPORTED_ERROR,
|
|
406
|
+
activeCodexConfigPath,
|
|
403
407
|
} = await import("../../codex/features");
|
|
404
408
|
// Probe the capability before any combined-request mutation. The scalar writer
|
|
405
409
|
// repeats this check, but doing it here prevents an earlier flag/mode/agents
|
|
@@ -419,17 +423,37 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
419
423
|
const requestedFlag = wantsFlag
|
|
420
424
|
? body.enabled as boolean
|
|
421
425
|
: modeFlag ?? (wantsKeepNative && hybridPinActive ? false : undefined);
|
|
426
|
+
/*
|
|
427
|
+
* One held section for every config.toml write this PUT makes — the
|
|
428
|
+
* feature transition plus each scalar edit. Individually-locked writers
|
|
429
|
+
* would let a foreign write land between them and leave a half-applied
|
|
430
|
+
* combination on disk; the lock is released before the post-write readers
|
|
431
|
+
* and catalog converge below.
|
|
432
|
+
*/
|
|
433
|
+
const needsCodexConfigWrites = requestedFlag !== undefined || wantsThreads
|
|
434
|
+
|| wantsAgentsEnabled || wantsMaxDepth || wantsSubagentInstructions || wantsModeHintText;
|
|
435
|
+
if (needsCodexConfigWrites && codexHomeIsAbsent(dirname(activeCodexConfigPath()))) {
|
|
436
|
+
return jsonResponse({ error: `config.toml not readable at ${activeCodexConfigPath()}` }, 502);
|
|
437
|
+
}
|
|
438
|
+
const { acquireConfigWriteLock, releaseConfigWriteLock, configWriteLockFailureMessage } = await import("../../codex/config-write-lock");
|
|
439
|
+
const configWriteLock = needsCodexConfigWrites ? await acquireConfigWriteLock(activeCodexConfigPath()) : null;
|
|
440
|
+
if (configWriteLock !== null && !configWriteLock.ok) {
|
|
441
|
+
return jsonResponse({ error: configWriteLockFailureMessage(configWriteLock), retryable: configWriteLock.error === "locked" }, 502);
|
|
442
|
+
}
|
|
443
|
+
const heldWriteLock = configWriteLock !== null && configWriteLock.ok ? configWriteLock.handle : undefined;
|
|
444
|
+
try {
|
|
422
445
|
if (requestedFlag !== undefined || wantsThreads) {
|
|
423
446
|
const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
|
|
424
447
|
let toggle = deps.toggleCodexMultiAgentV2;
|
|
425
448
|
if (!toggle) {
|
|
426
449
|
const { runCodexFeaturesCommand } = await import("../../cli/v2");
|
|
427
|
-
toggle = (enabled
|
|
450
|
+
toggle = (enabled, env, validate) => runCodexFeaturesCommand(enabled ? "enable" : "disable", "multi_agent_v2", env, validate);
|
|
428
451
|
}
|
|
429
452
|
const result = transitionMultiAgentV2(targetFlag, toggle, {
|
|
430
453
|
...(wantsThreads ? { threadLimit: body.maxConcurrentThreadsPerSession as number } : {}),
|
|
454
|
+
heldConfigWriteLock: heldWriteLock,
|
|
431
455
|
});
|
|
432
|
-
if (!result.ok) return jsonResponse({ error: `multi_agent_v2 transition failed: ${result.error}
|
|
456
|
+
if (!result.ok) return jsonResponse({ error: `multi_agent_v2 transition failed: ${result.error}`, ...(result.retryable === false ? { retryable: false } : {}) }, 502);
|
|
433
457
|
if (result.changed && result.threadLimit !== null) warnings.push(`Thread limit ${result.threadLimit} preserved for ${targetFlag ? "v2" : "v1"}.`);
|
|
434
458
|
}
|
|
435
459
|
if (wantsMode) {
|
|
@@ -465,10 +489,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
465
489
|
// asserts this route file contains no direct write primitive, and matches on the
|
|
466
490
|
// symbol name even inside a comment.
|
|
467
491
|
const scalarWrites: Array<{ field: string; run: () => { ok: true; changed: boolean } | { ok: false; error: string } }> = [];
|
|
468
|
-
if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => setAgentsEnabled(body.agentsEnabled as boolean | null) });
|
|
469
|
-
if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => setAgentsMaxDepth(body.agentsMaxDepth as number | null) });
|
|
470
|
-
if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => setSubagentDeveloperInstructions(body.subagentDeveloperInstructions as string | null) });
|
|
471
|
-
if (wantsModeHintText) scalarWrites.push({ field: "multiAgentModeHintText", run: () => setMultiAgentModeHintText(body.multiAgentModeHintText as string | null) });
|
|
492
|
+
if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => setAgentsEnabled(body.agentsEnabled as boolean | null, undefined, heldWriteLock) });
|
|
493
|
+
if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => setAgentsMaxDepth(body.agentsMaxDepth as number | null, undefined, heldWriteLock) });
|
|
494
|
+
if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => setSubagentDeveloperInstructions(body.subagentDeveloperInstructions as string | null, undefined, heldWriteLock) });
|
|
495
|
+
if (wantsModeHintText) scalarWrites.push({ field: "multiAgentModeHintText", run: () => setMultiAgentModeHintText(body.multiAgentModeHintText as string | null, undefined, heldWriteLock) });
|
|
472
496
|
const landed: string[] = [];
|
|
473
497
|
for (const write of scalarWrites) {
|
|
474
498
|
try {
|
|
@@ -482,6 +506,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
482
506
|
return jsonResponse({ error: `writing ${write.field} failed: ${message}${landed.length > 0 ? ` (already applied: ${landed.join(", ")})` : ""}` }, 502);
|
|
483
507
|
}
|
|
484
508
|
}
|
|
509
|
+
} finally {
|
|
510
|
+
if (heldWriteLock) releaseConfigWriteLock(heldWriteLock);
|
|
511
|
+
}
|
|
485
512
|
// Derived from fresh post-write readers (readConfigText is uncached): upstream
|
|
486
513
|
// lets an enabled multi_agent_v2 feature override [agents].enabled = false, so
|
|
487
514
|
// warn rather than reject — silently accepting would imply multi-agent is off.
|
|
@@ -533,35 +560,59 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
533
560
|
}
|
|
534
561
|
const body = parsedBody as { enabled?: unknown };
|
|
535
562
|
if (typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
|
|
536
|
-
const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY } = await import("../../codex/features");
|
|
563
|
+
const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, activeCodexConfigPath } = await import("../../codex/features");
|
|
537
564
|
const before = isDefaultModeRequestUserInputEnabled();
|
|
538
565
|
let toggle = deps.toggleDefaultModeRequestUserInput;
|
|
539
566
|
if (!toggle) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const raw = err.stderr;
|
|
549
|
-
const stderrText = typeof raw === "string"
|
|
550
|
-
? raw.trim()
|
|
551
|
-
: raw instanceof Uint8Array ? new TextDecoder().decode(raw).trim() : "";
|
|
552
|
-
toggleError = stderrText || (err.message ?? String(error));
|
|
553
|
-
}
|
|
554
|
-
const enabled = isDefaultModeRequestUserInputEnabled();
|
|
555
|
-
if (toggleError !== null || enabled !== body.enabled) {
|
|
556
|
-
const reason = toggleError
|
|
557
|
-
?? `postcondition failed - the installed Codex build may not know the ${DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY} flag yet`;
|
|
558
|
-
return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${reason}` }, 502);
|
|
567
|
+
try {
|
|
568
|
+
const { codexFeaturesInvocation, runCodexFeaturesCommand } = await import("../../cli/v2");
|
|
569
|
+
const action = body.enabled ? "enable" : "disable";
|
|
570
|
+
const invocation = codexFeaturesInvocation(action, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, process.platform, { requireAvailable: true });
|
|
571
|
+
toggle = (_enabled, env, validate) => runCodexFeaturesCommand(action, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, env, validate, invocation);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${error instanceof Error ? error.message : String(error)}` }, 502);
|
|
574
|
+
}
|
|
559
575
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
576
|
+
// The `codex features` subprocess rewrites config.toml itself — run it
|
|
577
|
+
// under the shared write lock so it cannot interleave with an opencodex
|
|
578
|
+
// scalar edit or injection mid-write on either side.
|
|
579
|
+
const { acquireConfigWriteLock, releaseConfigWriteLock, configWriteLockFailureMessage, runConfigWriteChild, ConfigWriteDestinationChanged } = await import("../../codex/config-write-lock");
|
|
580
|
+
const configPath = activeCodexConfigPath();
|
|
581
|
+
// Preserve native home creation only after executable resolution succeeds.
|
|
582
|
+
prepareCodexHome(dirname(configPath));
|
|
583
|
+
{
|
|
584
|
+
const configLock = await acquireConfigWriteLock(configPath);
|
|
585
|
+
if (!configLock.ok) {
|
|
586
|
+
return jsonResponse({ error: configWriteLockFailureMessage(configLock), retryable: configLock.error === "locked" }, 502);
|
|
587
|
+
}
|
|
588
|
+
let toggleError: string | null = null;
|
|
589
|
+
let destinationChanged = false;
|
|
590
|
+
try {
|
|
591
|
+
runConfigWriteChild(configPath, configLock.handle, (env, validate) => toggle!(body.enabled as boolean, env, validate));
|
|
592
|
+
} catch (error) {
|
|
593
|
+
destinationChanged = error instanceof ConfigWriteDestinationChanged;
|
|
594
|
+
const err = error as { stderr?: unknown; message?: string };
|
|
595
|
+
const raw = err.stderr;
|
|
596
|
+
const stderrText = typeof raw === "string"
|
|
597
|
+
? raw.trim()
|
|
598
|
+
: raw instanceof Uint8Array ? new TextDecoder().decode(raw).trim() : "";
|
|
599
|
+
toggleError = stderrText || (err.message ?? String(error));
|
|
600
|
+
} finally {
|
|
601
|
+
releaseConfigWriteLock(configLock.handle);
|
|
602
|
+
}
|
|
603
|
+
if (destinationChanged) return jsonResponse({ error: toggleError, retryable: false }, 502);
|
|
604
|
+
const enabled = isDefaultModeRequestUserInputEnabled();
|
|
605
|
+
if (toggleError !== null || enabled !== body.enabled) {
|
|
606
|
+
const reason = toggleError
|
|
607
|
+
?? `postcondition failed - the installed Codex build may not know the ${DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY} flag yet`;
|
|
608
|
+
return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${reason}` }, 502);
|
|
609
|
+
}
|
|
610
|
+
const warnings: string[] = [];
|
|
611
|
+
if (enabled !== before) {
|
|
612
|
+
warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the change.");
|
|
613
|
+
}
|
|
614
|
+
return jsonResponse({ ok: true, enabled, changed: enabled !== before, warnings });
|
|
563
615
|
}
|
|
564
|
-
return jsonResponse({ ok: true, enabled, changed: enabled !== before, warnings });
|
|
565
616
|
}
|
|
566
617
|
|
|
567
618
|
// Subagent prompt injection model: single native or routed model whose info is
|
|
@@ -1421,6 +1472,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1421
1472
|
alwaysEnableEffort: config.claudeCode?.alwaysEnableEffort === true,
|
|
1422
1473
|
autoContext: config.claudeCode?.autoContext !== false,
|
|
1423
1474
|
autoCompactWindow: config.claudeCode?.autoCompactWindow ?? null,
|
|
1475
|
+
contextAccounting: config.claudeCode?.contextAccounting === "200k" ? "200k" : "1m",
|
|
1424
1476
|
blockedSkills: config.claudeCode?.blockedSkills ?? null,
|
|
1425
1477
|
injectAgents: config.claudeCode?.injectAgents !== false,
|
|
1426
1478
|
sidecarPools: {
|
|
@@ -1457,7 +1509,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1457
1509
|
return prototype === Object.prototype || prototype === null;
|
|
1458
1510
|
};
|
|
1459
1511
|
if (!isPlainObject(parsedBody)) return jsonResponse({ error: "body must be an object" }, 400);
|
|
1460
|
-
const body = parsedBody as { enabled?: unknown; cliFirstParty?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
|
|
1512
|
+
const body = parsedBody as { enabled?: unknown; cliFirstParty?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; contextAccounting?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
|
|
1461
1513
|
if (body.cliFirstParty !== undefined && typeof body.cliFirstParty !== "boolean")
|
|
1462
1514
|
return jsonResponse({ error: "cliFirstParty must be a boolean" }, 400);
|
|
1463
1515
|
if (body.cliFirstParty !== undefined && Object.keys(body).length !== 1)
|
|
@@ -1727,6 +1779,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1727
1779
|
if (body.autoContext) delete next.autoContext;
|
|
1728
1780
|
else next.autoContext = false;
|
|
1729
1781
|
}
|
|
1782
|
+
if (body.contextAccounting !== undefined) {
|
|
1783
|
+
// Sparse like autoContext (devlog/_plan/261009_claude_1m_default/030): "1m" is the default
|
|
1784
|
+
// and drops the key; "200k" is the only stored value.
|
|
1785
|
+
if (body.contextAccounting !== "1m" && body.contextAccounting !== "200k") {
|
|
1786
|
+
return jsonResponse({ error: "contextAccounting must be \"1m\" or \"200k\"" }, 400);
|
|
1787
|
+
}
|
|
1788
|
+
if (body.contextAccounting === "200k") next.contextAccounting = "200k";
|
|
1789
|
+
else delete next.contextAccounting;
|
|
1790
|
+
}
|
|
1730
1791
|
if (body.injectAgents !== undefined) {
|
|
1731
1792
|
// Default-on boolean (devlog 260712 070): true = drop the key, false = store.
|
|
1732
1793
|
if (typeof body.injectAgents !== "boolean") return jsonResponse({ error: "injectAgents must be a boolean" }, 400);
|
|
@@ -1841,7 +1902,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1841
1902
|
// levers feed the same injection, so a changed or cleared slot must not linger in
|
|
1842
1903
|
// launchd until the next restart.
|
|
1843
1904
|
const systemEnvInputs = ["systemEnv", "authMode", "model", "smallFastModel", "tierModels",
|
|
1844
|
-
"maxContextTokens", "alwaysEnableEffort", "autoContext", "autoCompactWindow"] as const;
|
|
1905
|
+
"maxContextTokens", "alwaysEnableEffort", "autoContext", "autoCompactWindow", "contextAccounting"] as const;
|
|
1845
1906
|
if (systemEnvInputs.some(field => body[field] !== undefined)) {
|
|
1846
1907
|
try {
|
|
1847
1908
|
await applySystemEnvToggle(config, config.port);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-role model for omo (Codex / LazyCodex): `$CODEX_HOME/agents/<role>.toml` plus LazyCodex's
|
|
3
|
-
* `codex.agents.<role
|
|
3
|
+
* `"[codex]".agents.<role>` model and reasoning mirror in omo.jsonc. Both halves exist only when LazyCodex is
|
|
4
4
|
* detected; Pi-based and OpenCode-based omo are never read here.
|
|
5
5
|
*
|
|
6
6
|
* Loaded on demand from `src/server/management-api.ts`, like the quota-reset handler, so a
|
|
@@ -40,6 +40,7 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
|
|
|
40
40
|
omoJsonc: { state: omoState.state },
|
|
41
41
|
roles: roles.listCodexAgentRoleModels(codexHome).map(entry => ({
|
|
42
42
|
...entry,
|
|
43
|
+
effort: roles.readCodexAgentRoleEffort(entry.role, codexHome),
|
|
43
44
|
omoJsoncModel: omoModels[entry.role] ?? null,
|
|
44
45
|
})),
|
|
45
46
|
}, 200, req, config);
|
|
@@ -114,7 +115,8 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
|
|
|
114
115
|
// The role TOML is what Codex obeys, so its write stands even when the omo mirror cannot follow.
|
|
115
116
|
let omoStatus: ReturnType<typeof omo.writeOmoRoleModel> | "write_failed";
|
|
116
117
|
try {
|
|
117
|
-
|
|
118
|
+
const reasoning = effort === undefined ? undefined : omo.omoReasoningFor(effort);
|
|
119
|
+
omoStatus = omo.writeOmoRoleModel(role, model, omo.omoJsoncPath(), reasoning);
|
|
118
120
|
} catch {
|
|
119
121
|
omoStatus = "write_failed";
|
|
120
122
|
}
|
|
@@ -58,6 +58,10 @@ async function modelCandidates(slugs: readonly string[], models: readonly Catalo
|
|
|
58
58
|
]);
|
|
59
59
|
const nativeSlugs = catalog.listCatalogNativeSlugs();
|
|
60
60
|
const routed = new Map(models.map(model => [catalog.catalogModelSlug(model), model]));
|
|
61
|
+
// A routed model that declares no levels still gets the ladder Codex shows for it, the same one
|
|
62
|
+
// the Models page lists, so its proposal carries an effort instead of none. An explicit empty
|
|
63
|
+
// ladder means the model takes no effort control, and the written catalog lists none for it.
|
|
64
|
+
const written = catalog.catalogModelLadders(slugs);
|
|
61
65
|
const unitPrice = (provider: string, modelId: string): number | null => {
|
|
62
66
|
const cost = resolveMatchedPrice(provider, modelId)?.cost4;
|
|
63
67
|
return cost ? cost.input + cost.output : null;
|
|
@@ -65,11 +69,18 @@ async function modelCandidates(slugs: readonly string[], models: readonly Catalo
|
|
|
65
69
|
return slugs.map(slug => {
|
|
66
70
|
const row = routed.get(slug);
|
|
67
71
|
if (row) {
|
|
72
|
+
const ladder = written.get(slug);
|
|
73
|
+
const efforts = Array.isArray(row.reasoningEfforts) ? row.reasoningEfforts : ladder?.efforts ?? [];
|
|
74
|
+
// A row without its own default keeps the one Codex shows, when that rung is on the ladder.
|
|
75
|
+
const shownDefault = ladder?.defaultEffort !== undefined && efforts.includes(ladder.defaultEffort)
|
|
76
|
+
? ladder.defaultEffort
|
|
77
|
+
: undefined;
|
|
78
|
+
const defaultEffort = row.defaultReasoningEffort ?? shownDefault;
|
|
68
79
|
return {
|
|
69
80
|
model: slug,
|
|
70
81
|
unitPrice: unitPrice(row.provider, row.id),
|
|
71
|
-
efforts
|
|
72
|
-
...(
|
|
82
|
+
efforts,
|
|
83
|
+
...(defaultEffort ? { defaultEffort } : {}),
|
|
73
84
|
};
|
|
74
85
|
}
|
|
75
86
|
if (nativeSlugs.includes(slug)) {
|
|
@@ -9,6 +9,7 @@ import { isAnthropicInstanceId } from "../../providers/anthropic-instance-id";
|
|
|
9
9
|
import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
|
|
10
10
|
import type { IntegrationClientId } from "../../integrations/registry";
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { isDeepStrictEqual } from "node:util";
|
|
12
13
|
import { readFileSync } from "node:fs";
|
|
13
14
|
import type { CatalogModel } from "../../codex/catalog";
|
|
14
15
|
import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
@@ -182,6 +183,17 @@ interface ClientIntegrationSyncOutcome {
|
|
|
182
183
|
readonly profileId?: number;
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
/** Only the Desktop write's own applied markers may change during a background projection. */
|
|
187
|
+
function clientProjectionConfig(config: OcxConfig): OcxConfig {
|
|
188
|
+
const snapshot = structuredClone(config);
|
|
189
|
+
const profile = snapshot.claudeCode?.desktopProfile;
|
|
190
|
+
if (profile) {
|
|
191
|
+
delete profile.appliedFingerprint;
|
|
192
|
+
delete profile.appliedAt;
|
|
193
|
+
}
|
|
194
|
+
return snapshot;
|
|
195
|
+
}
|
|
196
|
+
|
|
185
197
|
/**
|
|
186
198
|
* Re-inject native clients that are switched ON and every file integration whose
|
|
187
199
|
* OpenCodex ownership record is the operator's durable opt-in.
|
|
@@ -205,17 +217,41 @@ export async function syncEnabledClientIntegrations(
|
|
|
205
217
|
config: OcxConfig,
|
|
206
218
|
deps: Pick<ManagementContext["deps"],
|
|
207
219
|
"fetchAllModels" | "refreshOwnedCatalogIntegrations" | "writeDesktop3pConfig"> = {},
|
|
220
|
+
options: { unattended?: { isCurrent: () => boolean } } = {},
|
|
208
221
|
): Promise<ClientIntegrationSyncOutcome[]> {
|
|
209
222
|
// A sibling instance passes its OWN port here; the Grok fence and the Desktop gateway profile
|
|
210
223
|
// stay on the live owner's (`src/codex/sibling-start.ts`).
|
|
211
224
|
if (port === undefined || siblingOfLivePort() !== null) return [];
|
|
212
|
-
const
|
|
225
|
+
const projectionConfig = options.unattended ? clientProjectionConfig(config) : undefined;
|
|
226
|
+
const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled, localClientSyncAllowed } = await import("../../codex/desired-state");
|
|
213
227
|
const out: ClientIntegrationSyncOutcome[] = [];
|
|
228
|
+
const unattended = options.unattended;
|
|
229
|
+
const admit = () => {
|
|
230
|
+
if (!unattended!.isCurrent()) return false;
|
|
231
|
+
const fresh = loadConfig();
|
|
232
|
+
return localClientSyncAllowed(fresh) && isDeepStrictEqual(clientProjectionConfig(fresh), projectionConfig);
|
|
233
|
+
};
|
|
234
|
+
const stale = () => unattended !== undefined && !admit();
|
|
235
|
+
if (stale()) return out;
|
|
236
|
+
const grok = unattended ? await import("../../grok/inject") : undefined;
|
|
214
237
|
|
|
215
|
-
if (grokIntegrationEnabled(config)) {
|
|
238
|
+
if (grokIntegrationEnabled(config) && (!grok || grok.grokManagedBlockPresent())) {
|
|
216
239
|
try {
|
|
217
240
|
const { syncGrokConfig } = await import("../../grok/sync");
|
|
218
|
-
|
|
241
|
+
if (stale()) return out;
|
|
242
|
+
const grokDeps = grok ? {
|
|
243
|
+
fetchAllModels: deps.fetchAllModels ?? (await import("../management-api")).fetchAllModels,
|
|
244
|
+
injectGrokConfig: ((...args: Parameters<typeof grok.injectGrokConfig>) =>
|
|
245
|
+
admit() && grokIntegrationEnabled(loadConfig()) && grok.grokManagedBlockPresent()
|
|
246
|
+
? grok.injectGrokConfig(...args)
|
|
247
|
+
: { ok: true, changed: false, message: "Grok refresh skipped" }),
|
|
248
|
+
} : undefined;
|
|
249
|
+
const r = unattended
|
|
250
|
+
? await syncGrokConfig(port, config, {
|
|
251
|
+
...config.hostname ? { hostname: config.hostname } : {},
|
|
252
|
+
refreshOnly: { admit },
|
|
253
|
+
}, grokDeps)
|
|
254
|
+
: await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {}, grokDeps);
|
|
219
255
|
out.push(r.ok
|
|
220
256
|
? { client: "grok", ok: true, changed: r.changed === true }
|
|
221
257
|
: { client: "grok", ok: false, reason: r.message });
|
|
@@ -224,9 +260,11 @@ export async function syncEnabledClientIntegrations(
|
|
|
224
260
|
}
|
|
225
261
|
}
|
|
226
262
|
|
|
263
|
+
if (stale()) return out;
|
|
227
264
|
const { observeClaudeDesktopMode, resolveClaudeDesktopMode } = await import("../../claude/desktop-first-party");
|
|
228
265
|
// A first-party Desktop must never get a gateway profile written and selected by a sync.
|
|
229
|
-
if (claudeDesktopIntegrationEnabled(config) && resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party"
|
|
266
|
+
if (claudeDesktopIntegrationEnabled(config) && resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party"
|
|
267
|
+
&& (!unattended || !!config.claudeCode?.desktopProfile?.appliedFingerprint)) {
|
|
230
268
|
try {
|
|
231
269
|
const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
|
|
232
270
|
const { desktopVisibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog");
|
|
@@ -240,12 +278,15 @@ export async function syncEnabledClientIntegrations(
|
|
|
240
278
|
const latest = loadConfig();
|
|
241
279
|
// Discovery awaited: the mode may have changed meanwhile. Re-resolve on the fresh read,
|
|
242
280
|
// immediately before the writer, so a first-party switch during fetchAllModels still wins.
|
|
243
|
-
if (
|
|
281
|
+
if (stale()) return;
|
|
282
|
+
if (claudeDesktopIntegrationEnabled(latest) && resolveClaudeDesktopMode(latest, observeClaudeDesktopMode(latest)) !== "first-party"
|
|
283
|
+
&& (!unattended || !!latest.claudeCode?.desktopProfile?.appliedFingerprint)) {
|
|
244
284
|
const routed = filterCatalogVisibleModels(models, latest)
|
|
245
285
|
.map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow }));
|
|
246
286
|
const writtenProfile = latest.claudeCode?.desktopProfile;
|
|
247
287
|
const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
|
|
248
|
-
|
|
288
|
+
// Keep the attended writer's argument count unchanged, including its lock test seam.
|
|
289
|
+
const writeArgs: Parameters<typeof writeDesktop3pConfig> = [
|
|
249
290
|
port,
|
|
250
291
|
[...desktopVisibleNativeSlugs(latest)],
|
|
251
292
|
routed,
|
|
@@ -253,7 +294,13 @@ export async function syncEnabledClientIntegrations(
|
|
|
253
294
|
"static",
|
|
254
295
|
writtenProfile,
|
|
255
296
|
nativeContextLimits(latest),
|
|
256
|
-
|
|
297
|
+
];
|
|
298
|
+
if (unattended) {
|
|
299
|
+
writeArgs[7] = undefined;
|
|
300
|
+
writeArgs[8] = { appliedFingerprint: writtenProfile!.appliedFingerprint!, admit };
|
|
301
|
+
}
|
|
302
|
+
const r = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(...writeArgs);
|
|
303
|
+
if (unattended && r.reason === "desktop_refresh_only_skipped") return;
|
|
257
304
|
if (!r.written || !r.fingerprint) {
|
|
258
305
|
out.push({ client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
|
|
259
306
|
} else {
|
|
@@ -271,6 +318,7 @@ export async function syncEnabledClientIntegrations(
|
|
|
271
318
|
}
|
|
272
319
|
}
|
|
273
320
|
|
|
321
|
+
if (stale()) return out;
|
|
274
322
|
const { refreshOwnedCatalogIntegrations } = await import("../../integrations/catalog-refresh");
|
|
275
323
|
const refreshOwned = deps.refreshOwnedCatalogIntegrations ?? refreshOwnedCatalogIntegrations;
|
|
276
324
|
out.push(...await refreshOwned({
|
|
@@ -280,7 +328,10 @@ export async function syncEnabledClientIntegrations(
|
|
|
280
328
|
},
|
|
281
329
|
config,
|
|
282
330
|
port,
|
|
283
|
-
},
|
|
331
|
+
}, unattended
|
|
332
|
+
? ["mcode", "pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo"]
|
|
333
|
+
: ["mcode", "pi", "aside", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"],
|
|
334
|
+
...unattended ? [{ refreshOnly: true, admit }] as const : []));
|
|
284
335
|
|
|
285
336
|
return out;
|
|
286
337
|
}
|
|
@@ -64,8 +64,8 @@ export interface ManagementApiDeps {
|
|
|
64
64
|
refreshOwnedCatalogIntegrations?: typeof refreshOwnedCatalogIntegrations;
|
|
65
65
|
/** Platform seam for capability projections; does not alter host-level startup behavior. */
|
|
66
66
|
platform?: NodeJS.Platform;
|
|
67
|
-
toggleCodexMultiAgentV2?: (enabled: boolean) => void;
|
|
68
|
-
toggleDefaultModeRequestUserInput?: (enabled: boolean) => void;
|
|
67
|
+
toggleCodexMultiAgentV2?: (enabled: boolean, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void;
|
|
68
|
+
toggleDefaultModeRequestUserInput?: (enabled: boolean, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void;
|
|
69
69
|
createManagementConvergeCodex?: (config: Readonly<OcxConfig>) => ConvergeCodex;
|
|
70
70
|
/** Test-only destination for best-effort Claude agent-definition sync. */
|
|
71
71
|
claudeAgentConfigDir?: string;
|
|
@@ -364,7 +364,7 @@ export function stripRegistryOnlyStaticHeaders(name: string, provider: OcxProvid
|
|
|
364
364
|
/** Shared Desktop profile DTO builder for the management API and CLI. */
|
|
365
365
|
export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxClaudeDesktopProfile) {
|
|
366
366
|
const { filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextWindow, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
|
|
367
|
-
const {
|
|
367
|
+
const { routeSupportsOneMillion } = await import("../../claude/long-context");
|
|
368
368
|
const { reconcileDesktopProfile, renderDesktopProfile } = await import("../../claude/desktop-profile");
|
|
369
369
|
const routed = filterCatalogVisibleModels(await fetchAllModels(config), config);
|
|
370
370
|
const profileModels: DesktopProfileModel[] = [
|
|
@@ -413,8 +413,8 @@ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxCla
|
|
|
413
413
|
...(modelByRoute.get(route)?.contextWindow ? { contextWindow: modelByRoute.get(route)!.contextWindow } : {}),
|
|
414
414
|
effortSupported: effortByRoute.get(route) ?? false,
|
|
415
415
|
// Read-only view of the 1M capability the written Desktop config already emits,
|
|
416
|
-
// derived from the SAME
|
|
417
|
-
supports1m: (modelByRoute.get(route)?.contextWindow
|
|
416
|
+
// derived from the SAME predicate so the dashboard chip can never disagree.
|
|
417
|
+
supports1m: routeSupportsOneMillion(route, modelByRoute.get(route)?.contextWindow),
|
|
418
418
|
assignment: profile.assignments[route]!,
|
|
419
419
|
}));
|
|
420
420
|
return {
|
|
@@ -139,19 +139,20 @@ export async function resolveNativeOAuthBindingForInstance(
|
|
|
139
139
|
const routeDecision = currentRoute(instance, config, model);
|
|
140
140
|
const manualSelectionGeneration = routing.captureAnthropicManualSelectionGeneration();
|
|
141
141
|
const selection = captureOAuthAccountSelection(instance);
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
assertOwner();
|
|
145
|
-
if (!routeIsCurrent(instance, config, model, routeDecision)) continue;
|
|
146
|
-
const proposed = routing.resolveAnthropicAccountForSession(sessionKey, config, Date.now(), routeDecision, model);
|
|
147
|
-
// A lost selection commit discards a recovery proposal: a newer manual/policy choice wins.
|
|
142
|
+
// A recovery candidate may replace a terminal active credential, including pool-off.
|
|
143
|
+
// A newer committed selection or route discards the proposal and uses normal admission.
|
|
148
144
|
const recoverySelectionMatches = !!options.expectedRecoverySelection
|
|
149
145
|
&& selection?.accountId === options.expectedRecoverySelection.accountId
|
|
150
146
|
&& selection?.revision === options.expectedRecoverySelection.revision;
|
|
151
147
|
const recoveryRouteMatches = options.expectedRecoveryRouteDecision === undefined
|
|
152
148
|
|| JSON.stringify(options.expectedRecoveryRouteDecision) === JSON.stringify(routeDecision);
|
|
153
|
-
const
|
|
154
|
-
? options.candidateAccountId :
|
|
149
|
+
const recoveryAccountId = attempt === 0 && options.candidateAccountId && recoverySelectionMatches && recoveryRouteMatches
|
|
150
|
+
? options.candidateAccountId : undefined;
|
|
151
|
+
const accountId = recoveryAccountId
|
|
152
|
+
?? await routing.resolveAnthropicDispatchAccountId(config, sessionKey, routeDecision, model);
|
|
153
|
+
assertOwner();
|
|
154
|
+
if (!routeIsCurrent(instance, config, model, routeDecision)) continue;
|
|
155
|
+
const proposed = routing.resolveAnthropicAccountForSession(sessionKey, config, Date.now(), routeDecision, model);
|
|
155
156
|
if (!selection) continue;
|
|
156
157
|
if (!routeCandidates(routing.getEligibleAnthropicAccounts(Date.now(), model), routeDecision).includes(accountId)) {
|
|
157
158
|
throw new NativeOAuthSelectionChangedError();
|