@bitkyc08/opencodex 2.67.0 → 2.68.0-preview.20260927
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 +3 -0
- package/gui/dist/assets/App-B5-cAEd2.js +51 -0
- package/gui/dist/assets/App-DCBismRi.css +1 -0
- package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
- package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
- package/gui/dist/assets/index-BcXblvem.js +86 -0
- package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
- package/gui/dist/index.html +21 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-guard.ts +13 -1
- package/src/adapters/anthropic-image-normalize.ts +3 -2
- package/src/adapters/anthropic-output-schema.ts +36 -0
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/codebuddy/live-models.ts +180 -0
- package/src/adapters/coding-agent/protocol.ts +91 -12
- package/src/adapters/coding-agent/turn.ts +32 -27
- package/src/adapters/devin/cloud-direct/index.ts +1 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
- package/src/adapters/devin.ts +9 -6
- package/src/adapters/google-http.ts +4 -4
- package/src/adapters/google.ts +93 -3
- package/src/adapters/kiro/adapter.ts +6 -2
- package/src/adapters/kiro/stream.ts +12 -2
- package/src/adapters/kiro/usage.ts +3 -1
- package/src/adapters/kiro-errors.ts +11 -1
- package/src/adapters/kiro-events.ts +27 -1
- package/src/adapters/kiro-refusal.ts +30 -0
- package/src/adapters/kiro-retry.ts +71 -31
- package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
- package/src/adapters/openai-chat/messages.ts +31 -4
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
- package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
- package/src/adapters/openai-chat/tool-schema.ts +6 -2
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -3
- package/src/adapters/opencode-go-additional-tools.ts +19 -10
- package/src/adapters/physical-send.ts +10 -3
- package/src/adapters/registry.ts +2 -1
- package/src/adapters/responses-tool-schema.ts +31 -3
- package/src/adapters/run-turn-queue.ts +63 -23
- package/src/adapters/unique-tool-call-ids.ts +63 -0
- package/src/adapters/xai-web-search.ts +32 -2
- package/src/bridge/sse.ts +14 -2
- package/src/chat/inbound.ts +20 -6
- package/src/claude/agents-inject.ts +4 -0
- package/src/claude/desktop-3p.ts +53 -13
- package/src/claude/desktop-profile.ts +41 -4
- package/src/claude/inbound-model-options.ts +14 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +17 -1
- package/src/claude/intercept/local-ca.ts +7 -1
- package/src/claude/message-threads.ts +28 -0
- package/src/cli/account-api.ts +13 -0
- package/src/cli/account-auth.ts +49 -2
- package/src/cli/account-extended.ts +4 -2
- package/src/cli/account.ts +4 -2
- package/src/cli/capabilities.ts +17 -0
- package/src/cli/claude.ts +3 -1
- package/src/cli/dispatch.ts +62 -5
- package/src/cli/index.ts +84 -79
- package/src/cli/minimax.ts +4 -2
- package/src/cli/opencode.ts +6 -1
- package/src/cli/restart-handoff.ts +110 -0
- package/src/cli/status.ts +51 -0
- package/src/client/connect.ts +34 -15
- package/src/client/link-ingress.ts +102 -0
- package/src/client/link-join.ts +37 -16
- package/src/client/link-relay.ts +228 -45
- package/src/client/link-state.ts +54 -0
- package/src/client/link-status.ts +39 -0
- package/src/client/link-teardown.ts +2 -2
- package/src/client/link-tunnel.ts +615 -86
- package/src/client/machine-api.ts +10 -4
- package/src/client/machine-listener.ts +57 -10
- package/src/client/runtime.ts +210 -35
- package/src/clients/config-export/constants.ts +1 -12
- package/src/clients/config-export/contracts.ts +2 -0
- package/src/clients/config-export/model-metadata.ts +9 -3
- package/src/clients/config-export/omp.ts +1 -1
- package/src/clients/config-export/zcode-store.ts +2 -2
- package/src/clients/config-export.ts +36 -3
- package/src/codex/app-server-processes.ts +32 -0
- package/src/codex/app-server-restart-service.ts +20 -3
- package/src/codex/catalog/access-programs.ts +50 -0
- package/src/codex/catalog/build-entries.ts +13 -0
- package/src/codex/catalog/derive-entry.ts +1 -0
- package/src/codex/catalog/model-hints.ts +11 -0
- package/src/codex/catalog/parsing.ts +6 -2
- package/src/codex/catalog/provider-models.ts +81 -0
- package/src/codex/catalog/retained-sync.ts +3 -0
- package/src/codex/codex-write-lock.ts +2 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desired-state.ts +14 -4
- package/src/codex/home.ts +21 -3
- package/src/codex/inject/restore.ts +17 -0
- package/src/codex/inject/routing-classify.ts +3 -2
- package/src/codex/inject-coordination.ts +4 -1
- package/src/codex/inject.ts +8 -1
- package/src/codex/journal.ts +6 -2
- package/src/codex/management-convergence.ts +9 -0
- package/src/codex/model-entitlements.ts +28 -5
- package/src/codex/routing/idle-window.ts +58 -0
- package/src/codex/routing-drift.ts +134 -0
- package/src/codex/routing-healer.ts +419 -0
- package/src/codex/routing.ts +10 -0
- package/src/codex/runtime.ts +188 -57
- package/src/codex/sibling-handoff.ts +71 -0
- package/src/codex/sibling-start.ts +139 -0
- package/src/codex/sync.ts +5 -3
- package/src/combos/failover.ts +32 -5
- package/src/combos/request.ts +11 -3
- package/src/combos/reset-window.ts +10 -5
- package/src/combos/resolve.ts +36 -6
- package/src/config/diagnostics.ts +2 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/paths.ts +7 -1
- package/src/config/process-state.ts +10 -1
- package/src/config/schema/compaction-recovery.ts +15 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +9 -3
- package/src/generated/compatibility-version.json +360 -204
- package/src/images/loop.ts +47 -32
- package/src/integrations/catalog-refresh.ts +4 -0
- package/src/integrations/omp-yaml-source.ts +1 -0
- package/src/lib/claude-request-projection.ts +101 -0
- package/src/lib/local-account-switch-capability.ts +48 -0
- package/src/lib/local-upstream.ts +146 -0
- package/src/lib/package-tree-integrity.ts +25 -3
- package/src/lib/package-tree-retarget.ts +130 -0
- package/src/lib/process-control.ts +4 -1
- package/src/lib/system-restart-contract.ts +106 -0
- package/src/lib/upstream-retry.ts +57 -1
- package/src/link/ports.ts +9 -0
- package/src/link/ssh-argv.ts +16 -0
- package/src/link/ssh-runner.ts +70 -2
- package/src/link/tunnel-state.ts +58 -9
- package/src/oauth/account-quota-rank.ts +29 -18
- package/src/oauth/generic-account-failover.ts +179 -22
- package/src/oauth/index.ts +17 -4
- package/src/oauth/kiro-account-load.ts +107 -0
- package/src/oauth/kiro-device-login.ts +312 -0
- package/src/oauth/kiro-terminal-failover.ts +28 -0
- package/src/oauth/login-flow-state.ts +7 -1
- package/src/oauth/pool-settings-capability.ts +23 -14
- package/src/oauth/store.ts +56 -3
- package/src/oauth/types.ts +4 -0
- package/src/plugins/loader.ts +350 -0
- package/src/plugins/upstream-hooks.ts +119 -0
- package/src/protocols/encoders/adapter-events.ts +9 -2
- package/src/providers/account-quota-disk.ts +42 -4
- package/src/providers/codebuddy-models.ts +6 -2
- package/src/providers/command-code-efforts.ts +33 -10
- package/src/providers/kiro-account-state-disk.ts +44 -0
- package/src/providers/kiro-model-catalog.ts +162 -0
- package/src/providers/kiro-models.ts +5 -4
- package/src/providers/kiro-quota-metrics.ts +35 -0
- package/src/providers/kiro-usage.ts +118 -14
- package/src/providers/quota/account-cache.ts +43 -7
- package/src/providers/quota/antigravity.ts +8 -3
- package/src/providers/quota/kiro-account-probe.ts +12 -0
- package/src/providers/quota/vendor-probes-key.ts +31 -26
- package/src/providers/quota/vendor-probes-oauth.ts +16 -7
- package/src/providers/quota-types.ts +14 -0
- package/src/providers/quota.ts +21 -21
- package/src/providers/registry/entries-extended.ts +5 -3
- package/src/providers/request-pacing.ts +410 -12
- package/src/remote-control/workspace-codex-runtime.ts +4 -3
- package/src/responses/citation-markers.ts +132 -65
- package/src/responses/hosted-tool-policy.ts +14 -3
- package/src/responses/parser-content.ts +3 -2
- package/src/responses/parser.ts +4 -1
- package/src/responses/schema.ts +4 -0
- package/src/responses/visualization-directives.ts +182 -0
- package/src/server/chat-native-sse.ts +24 -12
- package/src/server/chat-native.ts +137 -68
- package/src/server/claude-messages.ts +147 -12
- package/src/server/index/link-listener.ts +7 -0
- package/src/server/index/optional-listeners.ts +4 -1
- package/src/server/index/package-tree-guard.ts +35 -16
- package/src/server/index/serve-options.ts +4 -2
- package/src/server/index/startup-warnings.ts +6 -0
- package/src/server/index.ts +7 -7
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/inference/context.ts +30 -2
- package/src/server/local-account-switch-auth.ts +79 -0
- package/src/server/management/agent-settings-routes.ts +11 -3
- package/src/server/management/config-routes.ts +28 -11
- package/src/server/management/context.ts +2 -0
- package/src/server/management/link-routes.ts +123 -39
- package/src/server/management/logs-usage-routes.ts +9 -42
- package/src/server/management/model-rows.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +67 -12
- package/src/server/management/route-registry.ts +5 -5
- package/src/server/management/sibling-guard.ts +60 -0
- package/src/server/management/storage-log-guard-routes.ts +72 -6
- package/src/server/management/system-restart.ts +92 -135
- package/src/server/management/system-routes.ts +1 -1
- package/src/server/management-api.ts +31 -3
- package/src/server/management-auth.ts +3 -0
- package/src/server/port-reclaim.ts +111 -1
- package/src/server/proxy-liveness.ts +16 -0
- package/src/server/relay.ts +16 -15
- package/src/server/request-log-filter.ts +28 -27
- package/src/server/request-log.ts +7 -3
- package/src/server/request-metrics.ts +28 -0
- package/src/server/responses/adapter-continuation.ts +151 -21
- package/src/server/responses/adapter-delivery.ts +21 -7
- package/src/server/responses/adapter-dispatch.ts +239 -51
- package/src/server/responses/codex-ws-pool.ts +5 -4
- package/src/server/responses/codex-ws-request.ts +4 -1
- package/src/server/responses/compact.ts +8 -4
- package/src/server/responses/compaction-recovery-policy.ts +116 -0
- package/src/server/responses/compaction-recovery.ts +330 -0
- package/src/server/responses/core-combo-failure.ts +1 -0
- package/src/server/responses/core-combo.ts +5 -1
- package/src/server/responses/core-lifetime.ts +22 -0
- package/src/server/responses/core-options.ts +16 -1
- package/src/server/responses/core.ts +14 -4
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +82 -31
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +12 -2
- package/src/server/responses/passthrough-execution.ts +2 -2
- package/src/server/responses/request-send-budget.ts +21 -5
- package/src/server/responses/request-transport.ts +116 -18
- package/src/server/responses/reset-replay.ts +32 -0
- package/src/server/responses/run-turn-execution.ts +136 -19
- package/src/server/responses/sidecar-execution.ts +41 -8
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses/ws-upstream.ts +44 -4
- package/src/server/restart-replacement.ts +382 -0
- package/src/server/startup-health-cache.ts +23 -4
- package/src/server/stop-teardown.ts +35 -1
- package/src/server/system-env.ts +3 -0
- package/src/service/windows-taskxml.ts +11 -7
- package/src/service/windows-wrapper-exit.ts +8 -0
- package/src/stall-timeout.ts +35 -10
- package/src/storage/policy-job.ts +4 -0
- package/src/storage/scanner.ts +177 -29
- package/src/storage/storage-mutation-coordinator.ts +17 -0
- package/src/tray/windows-tray.ps1 +50 -23
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +5 -1
- package/src/types/request.ts +13 -2
- package/src/update/index.ts +3 -1
- package/src/update/job.ts +6 -2
- package/src/update/mise-launcher-target.ts +100 -0
- package/src/update/worker-launch.ts +46 -0
- package/src/usage/log.ts +2 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-BJsT8Icc.css +0 -1
- package/gui/dist/assets/App-D3pNGiN4.js +0 -51
- package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
- package/gui/dist/assets/index-CouvvtMV.js +0 -86
- package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
|
@@ -1095,6 +1095,38 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
1095
1095
|
return servableStale ?? flight.promise;
|
|
1096
1096
|
}
|
|
1097
1097
|
|
|
1098
|
+
/**
|
|
1099
|
+
* Dashboard read of the request-path catalog state, bounded by `deadlineMs`.
|
|
1100
|
+
*
|
|
1101
|
+
* The request collector keeps the event loop free, but a cold Windows probe still
|
|
1102
|
+
* takes 4-7s (CIM walk plus GetOwner per candidate), and the Models and Subagents
|
|
1103
|
+
* pages cannot render their rosters until these routes answer. The reading only
|
|
1104
|
+
* drives an advisory banner, so a slow probe answers `unknown` (which renders no
|
|
1105
|
+
* banner) and keeps running; its result is cached for the next poll.
|
|
1106
|
+
*
|
|
1107
|
+
* The deadline bounds the Windows path, where the request collector is asynchronous. On
|
|
1108
|
+
* other platforms the request collector keeps its existing synchronous read (a /proc walk on
|
|
1109
|
+
* Linux, `ps` on macOS, typically tens of milliseconds), which completes before the deadline
|
|
1110
|
+
* timer can fire; making those reads asynchronous is outside this change.
|
|
1111
|
+
*/
|
|
1112
|
+
export async function collectCodexAppServerCatalogStateWithin(
|
|
1113
|
+
deadlineMs: number,
|
|
1114
|
+
io: CodexAppServerProcessIo = {},
|
|
1115
|
+
): Promise<CodexAppServerCatalogStatus> {
|
|
1116
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1117
|
+
const deadline = new Promise<CodexAppServerCatalogStatus>(resolve => {
|
|
1118
|
+
timer = setTimeout(() => resolve({ state: "unknown", processes: [], catalogMtimeMs: null }), deadlineMs);
|
|
1119
|
+
});
|
|
1120
|
+
try {
|
|
1121
|
+
return await Promise.race([collectCodexAppServerCatalogStateForRequest(io), deadline]);
|
|
1122
|
+
} finally {
|
|
1123
|
+
clearTimeout(timer);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/** Deadline for dashboard catalog-state reads; a cached reading answers well within it. */
|
|
1128
|
+
export const DASHBOARD_CATALOG_STATE_DEADLINE_MS = 250;
|
|
1129
|
+
|
|
1098
1130
|
/**
|
|
1099
1131
|
* Drop memoized catalog state after a relevant catalog/cache write and before
|
|
1100
1132
|
* the post-write state read. Advancing the generation prevents an older
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import {
|
|
22
22
|
collectCodexAppServerCatalogState,
|
|
23
|
+
collectCodexAppServerCatalogStateWithin,
|
|
24
|
+
DASHBOARD_CATALOG_STATE_DEADLINE_MS,
|
|
23
25
|
listCodexAppServerProcesses,
|
|
24
26
|
readProcessStartMsBatch,
|
|
25
27
|
resetCodexAppServerCatalogStateCache,
|
|
@@ -59,6 +61,8 @@ export interface CodexRestartServiceIo {
|
|
|
59
61
|
*/
|
|
60
62
|
listenPort?: () => number | undefined;
|
|
61
63
|
collectState?: typeof collectCodexAppServerCatalogState;
|
|
64
|
+
/** Deadline for the default dashboard state read (ms). */
|
|
65
|
+
stateDeadlineMs?: number;
|
|
62
66
|
listProcesses?: typeof listCodexAppServerProcesses;
|
|
63
67
|
restart?: typeof restartCodexAppServers;
|
|
64
68
|
resetStateCache?: () => void;
|
|
@@ -88,10 +92,23 @@ class CodexAppServerIdentityChanged extends Error {
|
|
|
88
92
|
*/
|
|
89
93
|
let inFlight: Promise<CodexRestartResponse> | null = null;
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Read-only state for the dashboard route. The default classifier is the bounded
|
|
97
|
+
* request-path read: on Windows the synchronous one runs PowerShell CIM enumeration
|
|
98
|
+
* inline, which measured 4-7s per call and stalled every listener, proxy traffic
|
|
99
|
+
* included, each time the Models page opened. A probe slower than the deadline answers
|
|
100
|
+
* `unknown` and keeps refreshing the cache. The restart path below keeps the
|
|
101
|
+
* synchronous reading because it acts on it.
|
|
102
|
+
*/
|
|
103
|
+
export async function readCodexAppServerState(
|
|
92
104
|
io: CodexRestartServiceIo = {},
|
|
93
|
-
): CodexAppServerStateResponse {
|
|
94
|
-
const status =
|
|
105
|
+
): Promise<CodexAppServerStateResponse> {
|
|
106
|
+
const status = io.collectState
|
|
107
|
+
? io.collectState(io.processIo ?? {})
|
|
108
|
+
: await collectCodexAppServerCatalogStateWithin(
|
|
109
|
+
io.stateDeadlineMs ?? DASHBOARD_CATALOG_STATE_DEADLINE_MS,
|
|
110
|
+
io.processIo ?? {},
|
|
111
|
+
);
|
|
95
112
|
return { state: status.state, runningCount: status.processes.length };
|
|
96
113
|
}
|
|
97
114
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isMainCodexAccountTarget } from "../account-namespaces";
|
|
2
|
+
import { COMBO_NAMESPACE } from "../../combos";
|
|
3
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
4
|
+
import type { CodexModelEntitlementSnapshot } from "../model-entitlements";
|
|
5
|
+
import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
6
|
+
import { isNativeAliasCatalogEntry, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./metadata";
|
|
7
|
+
import type { RawEntry } from "./parsing";
|
|
8
|
+
|
|
9
|
+
/** Project authenticated account metadata onto native rows only. */
|
|
10
|
+
export function applyNativeAccessPrograms(
|
|
11
|
+
entries: RawEntry[],
|
|
12
|
+
snapshot: CodexModelEntitlementSnapshot,
|
|
13
|
+
accountTargets: ReadonlyMap<string, string>,
|
|
14
|
+
): void {
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
// A combo may deliberately claim a bare native slug. It is still a routed combo row,
|
|
17
|
+
// so the matching native Codex roster must not project its access programs onto it.
|
|
18
|
+
if (isNativeAliasCatalogEntry(entry)) continue;
|
|
19
|
+
if (entry.owned_by === COMBO_NAMESPACE) {
|
|
20
|
+
delete entry.available_access_programs;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const accountBoundSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
24
|
+
const bareSlug = typeof entry.slug === "string" && !entry.slug.includes("/")
|
|
25
|
+
&& SUPPORTED_NATIVE_OPENAI_SLUGS.has(entry.slug) ? entry.slug : undefined;
|
|
26
|
+
const slug = accountBoundSlug ?? bareSlug;
|
|
27
|
+
if (!slug) continue;
|
|
28
|
+
const selector = accountBoundSlug && typeof entry.slug === "string"
|
|
29
|
+
? entry.slug.slice(0, entry.slug.indexOf("/")) : undefined;
|
|
30
|
+
const target = selector === undefined ? MAIN_CODEX_ACCOUNT_ID : accountTargets.get(selector);
|
|
31
|
+
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
32
|
+
if (!accountId) {
|
|
33
|
+
delete entry.available_access_programs;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// An old on-disk catalog can contain metadata from a previous credential. Until this
|
|
37
|
+
// account has a confirmed roster, that value is no longer evidence of a grant.
|
|
38
|
+
if (!snapshot.confirmedAccountIds.has(accountId)
|
|
39
|
+
|| !snapshot.modelsByAccount.get(accountId)?.has(slug)) {
|
|
40
|
+
delete entry.available_access_programs;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const accountPrograms = snapshot.accessProgramsByAccount?.get(accountId);
|
|
44
|
+
if (accountPrograms?.has(slug)) {
|
|
45
|
+
entry.available_access_programs = accountPrograms.get(slug) ?? null;
|
|
46
|
+
} else {
|
|
47
|
+
delete entry.available_access_programs;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -865,9 +865,22 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
865
865
|
const normalized = reserveProjection ? m : normalizeServiceTiers(m);
|
|
866
866
|
if (!reserveProjection && !isNativeAliasCatalogEntry(normalized)) applyNativeOpenAiContextOverride(normalized, openaiContextCap);
|
|
867
867
|
const exactCombo = isExactComboCatalogEntry(m, exactComboSlugs);
|
|
868
|
+
// The builder copied this metadata from the pinned first-party row only for an exact
|
|
869
|
+
// ChatGPT/Codex forward custom alias. Freshness plus source equality prevents an old or
|
|
870
|
+
// unrelated routed row from claiming the native account's access programs at this second
|
|
871
|
+
// normalization boundary.
|
|
872
|
+
const nativeSourceSlug = typeof m.slug === "string" && m.slug.startsWith("openai/")
|
|
873
|
+
? m.slug.slice("openai/".length) : undefined;
|
|
874
|
+
const nativeSource = nativeSourceSlug ? upstreamNativeEntry(nativeSourceSlug) : undefined;
|
|
875
|
+
const preserveNativeAccessPrograms = freshCustomEntries.has(m)
|
|
876
|
+
&& m.owned_by !== COMBO_NAMESPACE
|
|
877
|
+
&& nativeSource != null
|
|
878
|
+
&& Object.hasOwn(nativeSource, "available_access_programs")
|
|
879
|
+
&& JSON.stringify(m.available_access_programs) === JSON.stringify(nativeSource.available_access_programs);
|
|
868
880
|
const e = reserveProjection ? normalized : ensureStrictCatalogFields(normalized, {
|
|
869
881
|
preserveExactInputModalities: exactCombo,
|
|
870
882
|
isRouted: finalRoutedEntrySet.has(m),
|
|
883
|
+
preserveNativeAccessPrograms,
|
|
871
884
|
});
|
|
872
885
|
// Mock-max universality (260709): preserved routed entries from disk may predate
|
|
873
886
|
// the max rung — ensure it here so subagent max spawns validate on every
|
|
@@ -195,6 +195,7 @@ export function deriveEntry(
|
|
|
195
195
|
return ensureStrictCatalogFields(normalizeServiceTiers(e), {
|
|
196
196
|
preserveExactInputModalities: preserveExact,
|
|
197
197
|
isRouted,
|
|
198
|
+
preserveNativeAccessPrograms: codexForwardNativeCapabilityAlias !== null,
|
|
198
199
|
});
|
|
199
200
|
}
|
|
200
201
|
// Fallback when no template is available (best-effort; strict parser may need more).
|
|
@@ -426,6 +426,17 @@ export function suppressedSyntheticMaxCatalogSlugs(
|
|
|
426
426
|
export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai"]);
|
|
427
427
|
|
|
428
428
|
export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
429
|
+
// CodeBuddy's vendor defaults are real callable selectors — both the bundled manifests
|
|
430
|
+
// (`product.json` / `product.internal.json`) and `--model` accept them — but the key-scoped
|
|
431
|
+
// configuration roster omits them. Without this entry, a successful live roster
|
|
432
|
+
// would drop the configured default ("default" for CN, "default-model" for Global) from
|
|
433
|
+
// the catalog even though the client can still call it (maintainer review, #5147).
|
|
434
|
+
codebuddy: new Set([
|
|
435
|
+
"default-model",
|
|
436
|
+
]),
|
|
437
|
+
"codebuddy-cn": new Set([
|
|
438
|
+
"default",
|
|
439
|
+
]),
|
|
429
440
|
kimi: new Set([
|
|
430
441
|
"k3[1m]",
|
|
431
442
|
"kimi-k2.7-code",
|
|
@@ -582,9 +582,10 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ
|
|
|
582
582
|
}
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
+
/** Normalize a row for Codex's catalog parser, stripping native eligibility from routed rows unless explicitly preserved. */
|
|
585
586
|
export function ensureStrictCatalogFields(
|
|
586
587
|
entry: RawEntry,
|
|
587
|
-
options: { preserveExactInputModalities?: boolean; isRouted?: boolean } = {},
|
|
588
|
+
options: { preserveExactInputModalities?: boolean; isRouted?: boolean; preserveNativeAccessPrograms?: boolean } = {},
|
|
588
589
|
): RawEntry {
|
|
589
590
|
if (entry.shell_type === "default" || entry.shell_type === "local" || entry.shell_type === "shell_command") {
|
|
590
591
|
entry.shell_type = "unified_exec";
|
|
@@ -634,7 +635,7 @@ export function ensureStrictCatalogFields(
|
|
|
634
635
|
if (typeof entry.effective_context_window_percent !== "number") entry.effective_context_window_percent = 95;
|
|
635
636
|
if (typeof entry.comp_hash !== "string") entry.comp_hash = "opencodex";
|
|
636
637
|
// Routed rows must not carry NATIVE eligibility metadata. `deriveEntry` deep-clones a
|
|
637
|
-
// native template and deletes a fixed denylist, so these
|
|
638
|
+
// native template and deletes a fixed denylist, so these eligibility fields survive onto rows backed
|
|
638
639
|
// by unrelated provider credentials — advertising ChatGPT plan eligibility for a model
|
|
639
640
|
// that never touches a ChatGPT account (#2813).
|
|
640
641
|
//
|
|
@@ -644,6 +645,9 @@ export function ensureStrictCatalogFields(
|
|
|
644
645
|
// leave already-contaminated rows contaminated forever.
|
|
645
646
|
if (options.isRouted === true) {
|
|
646
647
|
entry.supported_in_api = true;
|
|
648
|
+
// Exact Codex-forward aliases still use a ChatGPT credential and may retain their native
|
|
649
|
+
// source metadata. Other routed rows cannot claim that account's access programs.
|
|
650
|
+
if (!options.preserveNativeAccessPrograms) delete entry.available_access_programs;
|
|
647
651
|
delete entry.available_in_plans;
|
|
648
652
|
delete entry.minimal_client_version;
|
|
649
653
|
delete entry.availability_nux;
|
|
@@ -6,6 +6,8 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from
|
|
|
6
6
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
7
7
|
import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config";
|
|
8
8
|
import { resolveProviderApiKey } from "../../providers/key-store";
|
|
9
|
+
import { getAccountSet } from "../../oauth/store";
|
|
10
|
+
import { readKiroAccountModels, kiroObservedContextWindow } from "../../providers/kiro-model-catalog";
|
|
9
11
|
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
|
|
10
12
|
import {
|
|
11
13
|
clearModelCache,
|
|
@@ -55,6 +57,9 @@ import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
|
55
57
|
import { cursorLiveRosterScope, recordLiveCursorClaudeModels, recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog";
|
|
56
58
|
import { fetchQoderModels } from "../../adapters/qoder/live-models";
|
|
57
59
|
import { resolveQoderProfile } from "../../adapters/qoder/profiles";
|
|
60
|
+
import { CODEBUDDY_PROFILES, type CodeBuddyProfile } from "../../adapters/codebuddy/profiles";
|
|
61
|
+
import { fetchCodeBuddyModels } from "../../adapters/codebuddy/live-models";
|
|
62
|
+
import { resolveProfileByBaseUrl } from "../../adapters/coding-agent/profile";
|
|
58
63
|
import { fetchDevinUsableModels } from "../../adapters/devin/live-models";
|
|
59
64
|
import { resolveDevinApiBaseUrl } from "../../oauth/devin/api-base";
|
|
60
65
|
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
@@ -107,6 +112,11 @@ import { QUIET_AUTHORITATIVE_CATALOG_PROVIDERS, applyConfigHintsToCachedModels,
|
|
|
107
112
|
import { mergeConfiguredModelsIntoLiveCatalog, shouldExposeProviderModel, warnDroppedConfiguredIdsOnce } from "./model-visibility";
|
|
108
113
|
import { captureModelsRequest, captureProviderGather, materializeCapturedHeaders } from "./gather-capture";
|
|
109
114
|
|
|
115
|
+
|
|
116
|
+
/** Observed Kiro ids advertised in the public catalog: plain ids that need no router decoding. */
|
|
117
|
+
const KIRO_PUBLISHABLE_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
118
|
+
/** Upper bound on observed Kiro ids added to the catalog across the whole roster. */
|
|
119
|
+
const KIRO_OBSERVED_CATALOG_MAX = 64;
|
|
110
120
|
export interface ProviderModelsResult {
|
|
111
121
|
readonly models: CatalogModel[];
|
|
112
122
|
readonly outcome: CatalogGatherProviderModelOutcome;
|
|
@@ -214,6 +224,30 @@ export async function fetchProviderModelsWithAuth(
|
|
|
214
224
|
// discovery failure left by an older live configuration even when the account is logged out.
|
|
215
225
|
if (prov.liveModels === false) {
|
|
216
226
|
clearProviderDiscoveryStatus(name);
|
|
227
|
+
if (name === "kiro") {
|
|
228
|
+
const ids = [...configuredIds];
|
|
229
|
+
// Observed ids are advertised only when they round-trip through catalog encoding (no "/"
|
|
230
|
+
// or other separators the router would have to decode), and the roster adds at most
|
|
231
|
+
// KIRO_OBSERVED_CATALOG_MAX of them. Every observed id still informs routing preference.
|
|
232
|
+
let observedAdded = 0;
|
|
233
|
+
for (const account of getAccountSet("kiro")?.accounts ?? []) {
|
|
234
|
+
if (account.needsReauth === true) continue;
|
|
235
|
+
for (const row of readKiroAccountModels(account) ?? []) {
|
|
236
|
+
if (observedAdded >= KIRO_OBSERVED_CATALOG_MAX) break;
|
|
237
|
+
if (!KIRO_PUBLISHABLE_MODEL_ID.test(row.modelId) || ids.includes(row.modelId)) continue;
|
|
238
|
+
ids.push(row.modelId);
|
|
239
|
+
observedAdded += 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return observed(ids.map(id => {
|
|
243
|
+
const hints = catalogHintsFromProviderConfig(name, prov, id, contextCap,
|
|
244
|
+
metadataModelIdCaseFold, captured.effectiveAlias);
|
|
245
|
+
const observedWindow = kiroObservedContextWindow(id);
|
|
246
|
+
return { id, provider: name, ...hints,
|
|
247
|
+
...(observedWindow !== undefined
|
|
248
|
+
? { contextWindow: applyProviderContextCap(observedWindow, contextCap) } : {}) };
|
|
249
|
+
}), "authoritative");
|
|
250
|
+
}
|
|
217
251
|
return observed(configured, "authoritative");
|
|
218
252
|
}
|
|
219
253
|
const auth: ModelsAuthResolution = captured.observedAuth ?? (resolveAuth.kind === "refreshing"
|
|
@@ -245,6 +279,53 @@ export async function fetchProviderModelsWithAuth(
|
|
|
245
279
|
? [...models, vertexDefaultSeed]
|
|
246
280
|
: models
|
|
247
281
|
);
|
|
282
|
+
if (prov.adapter === "codebuddy") {
|
|
283
|
+
if (!apiKey) return observed(configured, "degraded");
|
|
284
|
+
const resolvedProfile = resolveProfileByBaseUrl(CODEBUDDY_PROFILES, prov.baseUrl);
|
|
285
|
+
if (!resolvedProfile) return observed(configured, "degraded");
|
|
286
|
+
const profile = resolvedProfile as CodeBuddyProfile;
|
|
287
|
+
// Cache reads/writes are provider/key-fingerprint-scoped: an irreversible fingerprint of
|
|
288
|
+
// the configured key means a key switch never reuses the roster cached for the previous
|
|
289
|
+
// key. The roster comes from the product configuration endpoint authenticated with that
|
|
290
|
+
// same key, so the fingerprint scope and the roster's authority are the same identity: the
|
|
291
|
+
// roster is the key's own account answer, never the CLI login's.
|
|
292
|
+
const authorityIdentity = createHash("sha256").update(apiKey).digest("hex");
|
|
293
|
+
const fresh = getFreshCached(name, ttlMs, Date.now(), authorityIdentity);
|
|
294
|
+
if (fresh) {
|
|
295
|
+
return observed(withConfiguredRetention(
|
|
296
|
+
applyConfigHintsToCachedModels(name, prov, fresh, contextCap, metadataModelIdCaseFold, captured.effectiveAlias),
|
|
297
|
+
), "authoritative");
|
|
298
|
+
}
|
|
299
|
+
const scopedStale = getStaleCached(name, authorityIdentity);
|
|
300
|
+
if (isModelsFetchCoolingDown(name, undefined, undefined, authorityIdentity) && scopedStale) {
|
|
301
|
+
return observed(withConfiguredRetention(
|
|
302
|
+
applyConfigHintsToCachedModels(name, prov, scopedStale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias),
|
|
303
|
+
), "degraded");
|
|
304
|
+
}
|
|
305
|
+
const live = await fetchCodeBuddyModels(profile, apiKey);
|
|
306
|
+
if (live.ok) {
|
|
307
|
+
const discovered = live.models.map(id => ({
|
|
308
|
+
id,
|
|
309
|
+
provider: name,
|
|
310
|
+
...catalogHintsFromProviderConfig(name, prov, id, contextCap, metadataModelIdCaseFold, captured.effectiveAlias),
|
|
311
|
+
}));
|
|
312
|
+
const forCache = withConfiguredRetention(discovered, { retainComboTargets: false });
|
|
313
|
+
if (!setCached(name, forCache, Date.now(), cacheGeneration, authorityIdentity)) {
|
|
314
|
+
return observed(withConfiguredRetention(configured), "degraded");
|
|
315
|
+
}
|
|
316
|
+
markProviderDiscoveryOk(name, live.models.length);
|
|
317
|
+
return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
|
|
318
|
+
}
|
|
319
|
+
if (isCurrentCacheGeneration()) {
|
|
320
|
+
markModelsFetchFailure(name, undefined, authorityIdentity);
|
|
321
|
+
markProviderDiscoveryFailed(name, { reason: "provider" });
|
|
322
|
+
console.warn(`[opencodex] CodeBuddy model discovery failed [${live.error}]${live.status === undefined ? "" : ` status=${live.status}`}; using stale/static catalog degradation.`);
|
|
323
|
+
}
|
|
324
|
+
const stale = getStaleCached(name, authorityIdentity);
|
|
325
|
+
return observed(withConfiguredRetention(
|
|
326
|
+
stale ? applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias) : configured,
|
|
327
|
+
), "degraded");
|
|
328
|
+
}
|
|
248
329
|
if (prov.adapter === "qoder") {
|
|
249
330
|
if (!apiKey) return observed(configured, "degraded");
|
|
250
331
|
const profile = resolveQoderProfile(prov.baseUrl);
|
|
@@ -12,6 +12,7 @@ import { providerCodexAccountMode } from "../../providers/registry";
|
|
|
12
12
|
import { COMBO_NAMESPACE } from "../../combos";
|
|
13
13
|
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../account-namespaces";
|
|
14
14
|
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
15
|
+
import { applyNativeAccessPrograms } from "./access-programs";
|
|
15
16
|
import {
|
|
16
17
|
availableAccountGatedNativeModels,
|
|
17
18
|
codexModelEntitlementStateForAccount,
|
|
@@ -266,6 +267,7 @@ function catalogModelsForMergeWithNativeRecovery(
|
|
|
266
267
|
]);
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
/** Merge retained rows, project confirmed account metadata, and publish only changed catalog bytes. */
|
|
269
271
|
function writeRetainedCatalogSync({
|
|
270
272
|
config,
|
|
271
273
|
goModels,
|
|
@@ -526,6 +528,7 @@ function writeRetainedCatalogSync({
|
|
|
526
528
|
warningPolicy: "emit",
|
|
527
529
|
},
|
|
528
530
|
});
|
|
531
|
+
applyNativeAccessPrograms(catalog.models, modelEntitlements, accountTargets);
|
|
529
532
|
clampCatalogModelsToCodexSupport(catalog.models);
|
|
530
533
|
finalizeAutoReviewModelOverride(catalog.models, catalogModelsForMerge, config);
|
|
531
534
|
// Last mutation before serialization; see `enforceCatalogSlugUniqueness` for why the ordering
|
|
@@ -144,9 +144,9 @@ export interface CodexWriteCommitContext {
|
|
|
144
144
|
*
|
|
145
145
|
* `hub-gated` is not the user's switch: a hub declines to rewrite its own local clients, and
|
|
146
146
|
* reporting that as "integration is OFF" sent operators hunting for a toggle they never set
|
|
147
|
-
* (#4236).
|
|
147
|
+
* (#4236). `sibling` is a second instance beside a live proxy that owns the routing.
|
|
148
148
|
*/
|
|
149
|
-
export type CodexWriteLockSkipReason = "desired_disabled" | "desired_enabled" | "hub-gated";
|
|
149
|
+
export type CodexWriteLockSkipReason = "desired_disabled" | "desired_enabled" | "hub-gated" | "sibling";
|
|
150
150
|
|
|
151
151
|
/** A synchronous under-lock policy re-read proved the requested apply stale. */
|
|
152
152
|
export class CodexWriteLockSkipped extends Error {
|
package/src/codex/convergence.ts
CHANGED
|
@@ -74,6 +74,7 @@ import { suppressedSyntheticMaxCatalogSlugs } from "./catalog/model-hints";
|
|
|
74
74
|
import { codexRuntimeStatePath, peekCodexRuntimeProcessCache } from "./runtime";
|
|
75
75
|
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "./account-namespaces";
|
|
76
76
|
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
77
|
+
import { applyNativeAccessPrograms } from "./catalog/access-programs";
|
|
77
78
|
import {
|
|
78
79
|
availableAccountGatedNativeModels,
|
|
79
80
|
codexModelEntitlementStateForAccount,
|
|
@@ -384,6 +385,7 @@ function prepareCatalog(
|
|
|
384
385
|
warningPolicy: "suppress",
|
|
385
386
|
},
|
|
386
387
|
});
|
|
388
|
+
applyNativeAccessPrograms(mergedModels, modelEntitlements, accountTargets);
|
|
387
389
|
clampCatalogModelsToObservedCodexSupport(
|
|
388
390
|
mergedModels,
|
|
389
391
|
source.runtimeSupport.kind === "available"
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { deleteConfigTopLevelKey, loadConfig, mutatePersistedConfig } from "../config";
|
|
24
24
|
import type { OcxClientIntegrationsConfig, OcxConfig } from "../types";
|
|
25
25
|
import { runStartupReadinessSync, type ReadinessGate, type SyncOutcomeLike } from "../server/readiness";
|
|
26
|
+
import { siblingOfLivePort, siblingSkipMessage } from "./sibling-start";
|
|
26
27
|
|
|
27
28
|
/** Clients whose durable intent this module owns. */
|
|
28
29
|
export type DurableIntentClientId = keyof OcxClientIntegrationsConfig;
|
|
@@ -77,6 +78,8 @@ type LocalClientSyncConfig = Pick<
|
|
|
77
78
|
>;
|
|
78
79
|
|
|
79
80
|
export function localClientSyncAllowed(config: LocalClientSyncConfig): boolean {
|
|
81
|
+
// A sibling instance never writes the live owner's client routing (`sibling-start.ts`).
|
|
82
|
+
if (siblingOfLivePort() !== null) return false;
|
|
80
83
|
return config.runtimeRole !== "hub"
|
|
81
84
|
|| config.unauthenticatedLoopbackListener?.enabled === true;
|
|
82
85
|
}
|
|
@@ -93,8 +96,11 @@ export const HUB_GATED_SKIP_MESSAGE =
|
|
|
93
96
|
"This machine is a hub; it does not rewrite its own Codex/Grok/Claude configs unless "
|
|
94
97
|
+ "unauthenticatedLoopbackListener is enabled.";
|
|
95
98
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Why a local-client write was skipped. The gate outranks the toggle: it is the surprising one.
|
|
101
|
+
* `sibling` outranks both: this process is a second instance beside a live owner.
|
|
102
|
+
*/
|
|
103
|
+
export type LocalClientSkipReason = "desired_disabled" | "hub-gated" | "sibling";
|
|
98
104
|
|
|
99
105
|
/**
|
|
100
106
|
* "hub-gated" is claimed only when the toggle is ON and the gate is what stopped the write.
|
|
@@ -106,6 +112,7 @@ export function localClientSkipReason(
|
|
|
106
112
|
config: LocalClientSyncConfig,
|
|
107
113
|
client: DurableIntentClientId = "codex",
|
|
108
114
|
): LocalClientSkipReason {
|
|
115
|
+
if (siblingOfLivePort() !== null) return "sibling";
|
|
109
116
|
return integrationEnabled(config, client) && !localClientSyncAllowed(config)
|
|
110
117
|
? "hub-gated"
|
|
111
118
|
: "desired_disabled";
|
|
@@ -123,7 +130,9 @@ export function localClientSkipMessage(
|
|
|
123
130
|
hubSuffix?: string,
|
|
124
131
|
client: DurableIntentClientId = "codex",
|
|
125
132
|
): string {
|
|
126
|
-
|
|
133
|
+
const reason = localClientSkipReason(config, client);
|
|
134
|
+
if (reason === "sibling") return siblingSkipMessage();
|
|
135
|
+
if (reason !== "hub-gated") return integrationOffMessage;
|
|
127
136
|
return hubSuffix ? `${HUB_GATED_SKIP_MESSAGE} ${hubSuffix}` : HUB_GATED_SKIP_MESSAGE;
|
|
128
137
|
}
|
|
129
138
|
|
|
@@ -134,7 +143,8 @@ export function shouldSyncCodexOnStart(config: LocalClientSyncConfig): boolean {
|
|
|
134
143
|
// /readyz failed because it tried to run the full local client sync).
|
|
135
144
|
// A hub can be a local client only through its explicitly enabled loopback
|
|
136
145
|
// listener. The public hub bind remains outside this gate and still requires
|
|
137
|
-
// admission; an explicit client OFF continues to win.
|
|
146
|
+
// admission; an explicit client OFF continues to win. A sibling instance is closed by the
|
|
147
|
+
// same gate: the live owner keeps the routing it maintains.
|
|
138
148
|
return localClientSyncAllowed(config) && codexIntegrationEnabled(config);
|
|
139
149
|
}
|
|
140
150
|
|
package/src/codex/home.ts
CHANGED
|
@@ -132,17 +132,35 @@ export function findWslWindowsCodexHome(deps: CodexHomeDeps = {}): string | null
|
|
|
132
132
|
return candidates.length === 1 ? candidates[0]! : null;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
function canonicalExistingCodexHome(path: string, deps: CodexHomeDeps): string {
|
|
136
|
+
const stat = deps.statSync ?? statSync;
|
|
137
|
+
try {
|
|
138
|
+
if (!stat(path).isDirectory()) return path;
|
|
139
|
+
// Use the portable resolver here rather than realpathSync.native. The
|
|
140
|
+
// Windows Bun standalone runtime can reject a valid junction through the
|
|
141
|
+
// native resolver, while the effective home still needs to be physical
|
|
142
|
+
// before callers open auth.json or native-profile state.
|
|
143
|
+
return (deps.realpathSync ?? realpathSync)(path);
|
|
144
|
+
} catch {
|
|
145
|
+
// Preserve the existing lexical-path behavior for missing or unreadable
|
|
146
|
+
// homes; callers that require the directory still fail at their boundary.
|
|
147
|
+
return path;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
135
151
|
export function defaultCodexHome(deps: CodexHomeDeps = {}): string {
|
|
136
152
|
const home = (deps.homedir ?? homedir)();
|
|
137
153
|
const defaultHome = join(home, ".codex");
|
|
154
|
+
const canonicalDefaultHome = canonicalExistingCodexHome(defaultHome, deps);
|
|
138
155
|
// A local ~/.codex that Codex is already using is the user's Codex home even before
|
|
139
156
|
// config.toml exists (a fresh install: login writes auth.json, first use writes
|
|
140
157
|
// sessions/ and history.jsonl). A local directory with none of that state is not
|
|
141
158
|
// evidence of a local Codex: before #5441 such a home let WSL discovery pick the
|
|
142
159
|
// Windows home, and existing WSL users who run against that Windows home must not
|
|
143
|
-
// be moved to an empty local one on upgrade.
|
|
144
|
-
|
|
145
|
-
|
|
160
|
+
// be moved to an empty local one on upgrade. Return the canonical path so a
|
|
161
|
+
// Windows junction cannot leak into later credential and profile writes.
|
|
162
|
+
if (localCodexHomeIsDirectory(defaultHome, deps) && localCodexHomeInUse(defaultHome, deps)) return canonicalDefaultHome;
|
|
163
|
+
return findWslWindowsCodexHome(deps) ?? canonicalDefaultHome;
|
|
146
164
|
}
|
|
147
165
|
|
|
148
166
|
function localCodexHomeInUse(home: string, deps: CodexHomeDeps): boolean {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadConfig } from "../../config";
|
|
2
2
|
import { shouldSyncCodexOnStart } from "../desired-state";
|
|
3
|
+
import { siblingOfLivePort, siblingSkipMessage } from "../sibling-start";
|
|
3
4
|
import { withCatalogWriteSerialization } from "../catalog-write-serialization";
|
|
4
5
|
import { restoreCodexCatalogWithPermit } from "../catalog/sync";
|
|
5
6
|
import { withCodexWriteLock, CodexWriteLockSkipped } from "../codex-write-lock";
|
|
@@ -253,6 +254,18 @@ function desiredEnabledRestoreSkip(): CodexNativeRestoreResult {
|
|
|
253
254
|
return skippedRestoreEnvelope(true, message);
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
/**
|
|
258
|
+
* A sibling instance restores nothing: the journal, config and catalog are the live owner's.
|
|
259
|
+
*
|
|
260
|
+
* It has to be checked before anything else, not left to the desired-state re-reads below. Those
|
|
261
|
+
* skip a restore when `shouldSyncCodexOnStart` says ON, and the sibling mark makes that gate
|
|
262
|
+
* answer OFF, so without this the gate would read as "the user turned Codex off" and the restore
|
|
263
|
+
* would replay the owner's journal.
|
|
264
|
+
*/
|
|
265
|
+
function siblingRestoreSkip(): CodexNativeRestoreResult | null {
|
|
266
|
+
return siblingOfLivePort() === null ? null : skippedRestoreEnvelope(true, siblingSkipMessage());
|
|
267
|
+
}
|
|
268
|
+
|
|
256
269
|
/**
|
|
257
270
|
* A schema-complete all-skipped envelope for outcomes decided before any
|
|
258
271
|
* restore machinery runs. Every `restore --json` path must stay shape-stable
|
|
@@ -471,6 +484,8 @@ function restoreCodexCatalogArtifact(
|
|
|
471
484
|
export async function restoreNativeCodexAsync(
|
|
472
485
|
options: { revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
|
|
473
486
|
): Promise<CodexNativeRestoreResult> {
|
|
487
|
+
const sibling = siblingRestoreSkip();
|
|
488
|
+
if (sibling) return sibling;
|
|
474
489
|
try {
|
|
475
490
|
return await restoreNativeCodexAsyncImpl(options);
|
|
476
491
|
} catch (error) {
|
|
@@ -669,6 +684,8 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
669
684
|
export function restoreNativeCodex(
|
|
670
685
|
options: { skipHistory?: boolean; revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
|
|
671
686
|
): CodexNativeRestoreResult {
|
|
687
|
+
const sibling = siblingRestoreSkip();
|
|
688
|
+
if (sibling) return sibling;
|
|
672
689
|
const activeProvider = currentExternalCodexModelProvider();
|
|
673
690
|
if (activeProvider) {
|
|
674
691
|
removeJournal();
|
|
@@ -11,7 +11,7 @@ import { CODEX_CONFIG_PATH } from "../paths";
|
|
|
11
11
|
export type CodexRoutingKind =
|
|
12
12
|
"native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown";
|
|
13
13
|
|
|
14
|
-
type RoutingEndpointKind = "local" | "remote" | "unknown";
|
|
14
|
+
export type RoutingEndpointKind = "local" | "remote" | "unknown";
|
|
15
15
|
|
|
16
16
|
function ipv4Octets(hostname: string): number[] | null {
|
|
17
17
|
const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
@@ -26,7 +26,8 @@ function ipv4Octets(hostname: string): number[] | null {
|
|
|
26
26
|
return [high >>> 8, high & 0xff, low >>> 8, low & 0xff];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/** The one loopback rule for routing URLs; the drift detector (`routing-drift.ts`) shares it. */
|
|
30
|
+
export function classifyRoutingEndpoint(value: string): RoutingEndpointKind {
|
|
30
31
|
try {
|
|
31
32
|
const url = new URL(value);
|
|
32
33
|
if (url.protocol !== "http:" && url.protocol !== "https:") return "unknown";
|
|
@@ -10,6 +10,7 @@ import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
|
10
10
|
import { atomicWriteFile } from "../config";
|
|
11
11
|
import type { CodexWriteLockResult, CodexWriteLockSkipReason } from "./codex-write-lock";
|
|
12
12
|
import { HUB_GATED_SKIP_MESSAGE } from "./desired-state";
|
|
13
|
+
import { siblingSkipMessage } from "./sibling-start";
|
|
13
14
|
import { inspectCodexCoordinatorPath } from "./coordinator-doctor";
|
|
14
15
|
import { JOURNAL_PATH } from "./journal";
|
|
15
16
|
import { updateIntegrationRecord } from "./integration-record";
|
|
@@ -452,10 +453,12 @@ export function codexInjectLockOutcome(
|
|
|
452
453
|
success: true,
|
|
453
454
|
status: "skipped",
|
|
454
455
|
skippedReason: result.reason,
|
|
455
|
-
//
|
|
456
|
+
// Four distinct facts, four sentences. The hub gate in particular must not borrow the
|
|
456
457
|
// toggle's wording — that is the phantom "integration is OFF" report from #4236.
|
|
457
458
|
message: result.reason === "hub-gated"
|
|
458
459
|
? `${HUB_GATED_SKIP_MESSAGE} No Codex config, catalog, cache, or history was changed.`
|
|
460
|
+
: result.reason === "sibling"
|
|
461
|
+
? `${siblingSkipMessage()} No Codex config, catalog, cache, or history was changed.`
|
|
459
462
|
: result.reason === "desired_disabled"
|
|
460
463
|
? "Codex integration is OFF; no Codex config, catalog, cache, or history was changed."
|
|
461
464
|
: "Codex integration was re-enabled; native restore was skipped.",
|
package/src/codex/inject.ts
CHANGED
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
localClientSkipMessage,
|
|
13
13
|
localClientSkipReason,
|
|
14
14
|
shouldSyncCodexOnStart,
|
|
15
|
+
type LocalClientSkipReason,
|
|
15
16
|
} from "./desired-state";
|
|
17
|
+
import { siblingOfLivePort, siblingSkipMessage } from "./sibling-start";
|
|
16
18
|
import { resolveCodexHistoryTransition } from "./history-transition";
|
|
17
19
|
import {
|
|
18
20
|
buildInjectWitness,
|
|
@@ -147,7 +149,7 @@ export interface CodexInjectResult {
|
|
|
147
149
|
/** Busy write lock, emitted by `codexInjectLockOutcome` and undeclared here until #4809. */
|
|
148
150
|
retryable?: boolean;
|
|
149
151
|
/** `hub-gated` is the hub-role gate (#4236), distinct from the user's own OFF switch. */
|
|
150
|
-
skippedReason?:
|
|
152
|
+
skippedReason?: LocalClientSkipReason | "desired_enabled";
|
|
151
153
|
nativeSubagentDefaultsWarning?: string;
|
|
152
154
|
}
|
|
153
155
|
|
|
@@ -184,6 +186,11 @@ export async function injectCodexConfig(
|
|
|
184
186
|
config?: OcxConfig,
|
|
185
187
|
options: InjectCodexOptions = {},
|
|
186
188
|
): Promise<CodexInjectResult> {
|
|
189
|
+
// First, before the external-provider branch below removes the SHARED journal: a sibling owns
|
|
190
|
+
// none of this home's routing, not even the courtesy cleanup.
|
|
191
|
+
if (siblingOfLivePort() !== null) {
|
|
192
|
+
return { success: true, status: "skipped", skippedReason: "sibling", message: siblingSkipMessage() };
|
|
193
|
+
}
|
|
187
194
|
try { return await injectCodexConfigImpl(port, config, options); }
|
|
188
195
|
catch (error) {
|
|
189
196
|
if (error instanceof CodexHistoryPreflightRefusal) return { success: false, historyPreflightFailureReason: error.message, message: `Codex config injection refused: ${error.message}. Existing configuration and history were preserved.` };
|
package/src/codex/journal.ts
CHANGED
|
@@ -255,8 +255,12 @@ function readJournal(cleanInvalid = true): Journal | null {
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Who owns the journal. `readOnly` never deletes an unreadable journal: a background reader (the
|
|
260
|
+
* routing healer, `routing-healer.ts`) must not destroy recovery evidence it only looked at.
|
|
261
|
+
*/
|
|
262
|
+
export function journalOwner(options: { readOnly?: boolean } = {}): JournalOwner | null {
|
|
263
|
+
const journal = readJournal(options.readOnly !== true);
|
|
260
264
|
if (!journal) return null;
|
|
261
265
|
if (journal.owner?.kind === "client" && typeof journal.owner.apiKeyId === "string" && journal.owner.apiKeyId) {
|
|
262
266
|
return { kind: "client", apiKeyId: journal.owner.apiKeyId };
|
|
@@ -2,6 +2,7 @@ import type { OcxConfig } from "../types";
|
|
|
2
2
|
import { resolvePendingInitialModelSelection } from "../providers/initial-model-selection-runtime";
|
|
3
3
|
import { captureCatalogAdmissionSnapshot } from "./catalog-admission";
|
|
4
4
|
import { convergeCodexCatalog } from "./convergence";
|
|
5
|
+
import { siblingOfLivePort } from "./sibling-start";
|
|
5
6
|
import type {
|
|
6
7
|
CatalogDisposition,
|
|
7
8
|
CatalogFailureCause,
|
|
@@ -155,6 +156,14 @@ export function createManagementConvergeCodex(
|
|
|
155
156
|
}
|
|
156
157
|
// Registration choices are committed independently, before sealing catalog authority.
|
|
157
158
|
await resolvePendingInitialModelSelection(retainedConfig as OcxConfig);
|
|
159
|
+
// The catalog and models cache live in the shared `CODEX_HOME`; a sibling instance leaves
|
|
160
|
+
// them to the live owner. This funnel serves every management caller and the auto-refresh tick.
|
|
161
|
+
if (siblingOfLivePort() !== null) {
|
|
162
|
+
return projectCatalogOnlyOutcome({
|
|
163
|
+
changed: false,
|
|
164
|
+
catalogRefresh: { status: "skipped", reason: "refused", retryable: false },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
158
167
|
const snapshot = captureCatalogAdmissionSnapshot(retainedConfig);
|
|
159
168
|
const result = await convergeCodexCatalog(snapshot, request, {
|
|
160
169
|
onCommitBegin: () => { commitBegan = true; },
|