@bitkyc08/opencodex 2.43.0 → 2.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -17
- package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
- package/gui/dist/assets/index-ltx3L-WS.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +23 -2
- package/src/adapters/cursor/native-exec.ts +63 -11
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/cursor/tool-schemas.ts +59 -0
- package/src/adapters/kiro.ts +54 -8
- package/src/adapters/openai-chat.ts +23 -6
- package/src/adapters/openai-responses.ts +3 -1
- package/src/adapters/opencode-go.ts +35 -0
- package/src/claude/agents-inject.ts +12 -5
- package/src/claude/desktop-3p-library.ts +89 -0
- package/src/claude/desktop-3p.ts +166 -95
- package/src/claude/desktop-discovery-inputs.ts +44 -0
- package/src/claude/desktop-profile.ts +1 -1
- package/src/claude/desktop-remote-store-artifact.ts +185 -0
- package/src/claude/desktop-remote-store-io.ts +91 -0
- package/src/claude/desktop-remote-store-state.ts +127 -0
- package/src/claude/desktop-remote-store.ts +269 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +1 -1
- package/src/claude/model-info.ts +16 -3
- package/src/cli/account-api.ts +7 -1
- package/src/cli/aside-profiles.ts +17 -0
- package/src/cli/capabilities.ts +39 -0
- package/src/cli/claude-agent-startup-sync.ts +3 -1
- package/src/cli/claude-desktop.ts +127 -18
- package/src/cli/connect.ts +27 -14
- package/src/cli/dispatch.ts +30 -16
- package/src/cli/export-command.ts +3 -1
- package/src/cli/index.ts +28 -16
- package/src/cli/integrations.ts +46 -12
- package/src/cli/opencode.ts +11 -4
- package/src/cli/runtime-api.ts +7 -2
- package/src/cli/uninstall-client-state.ts +78 -0
- package/src/client/connect.ts +476 -223
- package/src/client/hub-client.ts +81 -0
- package/src/client/hub-relay.ts +38 -6
- package/src/client/lifecycle-lock.ts +129 -0
- package/src/client/state.ts +71 -42
- package/src/clients/aside-profiles.ts +224 -0
- package/src/codex/account-lifecycle.ts +5 -9
- package/src/codex/auth-api.ts +50 -4
- package/src/codex/catalog/metadata.ts +0 -2
- package/src/codex/catalog/provider-fetch.ts +7 -5
- package/src/codex/catalog/sync.ts +88 -24
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +45 -14
- package/src/codex/journal.ts +6 -6
- package/src/codex/quota-refresh-outcome.ts +27 -0
- package/src/codex/shim.ts +7 -1
- package/src/combos/resolve.ts +10 -7
- package/src/config.ts +18 -5
- package/src/generated/compatibility-version.json +209 -105
- package/src/images/loop.ts +5 -2
- package/src/integrations/aside-profile-context.ts +270 -0
- package/src/integrations/aside-profile-journal.ts +229 -0
- package/src/integrations/aside-profiles.ts +176 -0
- package/src/integrations/catalog-refresh.ts +40 -0
- package/src/integrations/config-io.ts +16 -1
- package/src/integrations/owned-refresh.ts +14 -2
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +3 -1
- package/src/lab/fabric/producer-isolate.ts +84 -46
- package/src/lib/account-selection-events.ts +32 -0
- package/src/lib/errors.ts +9 -1
- package/src/lib/provider-outbound.ts +2 -45
- package/src/lib/proxy-env.ts +67 -0
- package/src/oauth/account-quota-rank.ts +9 -1
- package/src/oauth/anthropic-routing.ts +91 -23
- package/src/oauth/generic-account-failover.ts +15 -53
- package/src/oauth/index.ts +67 -68
- package/src/oauth/pool-settings-capability.ts +2 -1
- package/src/oauth/store.ts +91 -7
- package/src/oauth/types.ts +8 -0
- package/src/providers/api-key-selection.ts +110 -0
- package/src/providers/api-keys.ts +38 -31
- package/src/providers/context-cap.ts +27 -3
- package/src/providers/key-failover.ts +27 -17
- package/src/providers/provider-id-rewrite.ts +10 -8
- package/src/providers/registry.ts +1 -0
- package/src/responses/custom-tool-compat.ts +10 -0
- package/src/responses/function-call-compat.ts +173 -0
- package/src/responses/namespace-tool-compat.ts +27 -4
- package/src/responses/parser.ts +8 -0
- package/src/responses/task-input.ts +36 -0
- package/src/responses/tool-name-aliases.ts +79 -0
- package/src/router.ts +2 -0
- package/src/routing/capability.ts +10 -3
- package/src/routing/compatibility/assemble.ts +16 -1
- package/src/routing/evaluator.ts +5 -1
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-native.ts +44 -9
- package/src/server/claude-messages.ts +71 -36
- package/src/server/grok-responses-snapshot-repair.ts +333 -0
- package/src/server/index.ts +26 -10
- package/src/server/management/account-selection-stream.ts +70 -0
- package/src/server/management/aside-profile-routes.ts +231 -0
- package/src/server/management/config-routes.ts +36 -41
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +61 -3
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/model-routes.ts +20 -9
- package/src/server/management/model-rows.ts +10 -2
- package/src/server/management/oauth-account-routes.ts +7 -0
- package/src/server/management/provider-routes.ts +10 -8
- package/src/server/management/route-registry.ts +20 -2
- package/src/server/management-auth.ts +16 -0
- package/src/server/relay-eager.ts +17 -5
- package/src/server/relay.ts +43 -4
- package/src/server/request-log.ts +29 -1
- package/src/server/responses/agent-task-recovery-cache.ts +11 -0
- package/src/server/responses/agent-task-recovery.ts +24 -4
- package/src/server/responses/codex-ws-pool.ts +4 -4
- package/src/server/responses/codex-ws-session.ts +2 -2
- package/src/server/responses/collaboration.ts +2 -1
- package/src/server/responses/combo-stream-preflight.ts +26 -7
- package/src/server/responses/core.ts +668 -151
- package/src/server/responses/fetch-helpers.ts +6 -1
- package/src/server/responses/ws-upstream.ts +8 -3
- package/src/server/responses-custom-tool-repair.ts +29 -9
- package/src/server/responses-function-tool-repair.ts +183 -0
- package/src/server/responses-snapshot-codec.ts +14 -0
- package/src/server/responses-snapshot-repair.ts +1 -13
- package/src/server/responses-undeclared-tool-guard.ts +1 -68
- package/src/server/sse-payload-rewrite.ts +3 -1
- package/src/service.ts +6 -2
- package/src/types/config.ts +23 -16
- package/src/types/provider.ts +14 -3
- package/src/types/request.ts +2 -0
- package/src/usage/log.ts +9 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
- package/gui/dist/assets/index-Djowl68T.js +0 -112
|
@@ -88,15 +88,15 @@ import {
|
|
|
88
88
|
export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
|
|
89
89
|
|
|
90
90
|
// Base for config.modelPickerOrder display priorities (#1649). modelPickerOrder is a DISPLAY-ONLY
|
|
91
|
-
// reordering of the Codex model picker: it rewrites a row's Codex-visible `priority` but
|
|
92
|
-
//
|
|
93
|
-
// priority
|
|
91
|
+
// reordering of the Codex model picker: it rewrites a row's Codex-visible `priority` but not
|
|
92
|
+
// OpenCodex's natural-priority guidance window. Native Codex advertisements still follow the
|
|
93
|
+
// visible priority and can differ from that guidance window.
|
|
94
94
|
export const PICKER_ORDER_PRIORITY_BASE = 1_000;
|
|
95
95
|
|
|
96
|
-
// OpenCodex-private catalog field: the
|
|
96
|
+
// OpenCodex-private catalog field: the guidance candidate priority a row would have WITHOUT
|
|
97
97
|
// modelPickerOrder. Codex ignores unknown catalog fields (same as opencodex_catalog_kind), so this
|
|
98
|
-
// is invisible to Codex; effectiveSubagentRoster reads it
|
|
99
|
-
//
|
|
98
|
+
// is invisible to Codex; effectiveSubagentRoster reads it to keep OpenCodex guidance candidates
|
|
99
|
+
// independent of display order. It does not freeze native advertisements. Absent on unmoved rows.
|
|
100
100
|
export const SPAWN_PRIORITY_FIELD = "opencodex_spawn_priority";
|
|
101
101
|
|
|
102
102
|
export type SpawnAgentSurface = "v1" | "v2";
|
|
@@ -154,7 +154,9 @@ export interface SubagentRosterExclusion {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
export interface EffectiveSubagentRoster {
|
|
157
|
+
/** OpenCodex's natural-priority guidance projection, not captured native tool text. */
|
|
157
158
|
candidates: EffectiveSubagentModel[];
|
|
159
|
+
/** Configured models within that projection; exact-name eligibility is a separate check. */
|
|
158
160
|
advertised: EffectiveSubagentModel[];
|
|
159
161
|
excluded: SubagentRosterExclusion[];
|
|
160
162
|
}
|
|
@@ -191,8 +193,8 @@ export function effectiveSubagentRoster(
|
|
|
191
193
|
.filter(({ entry }) => entry.visibility === "list")
|
|
192
194
|
.filter(({ entry }) => surface !== "v2" || isEligibleV2SubagentEntry(entry))
|
|
193
195
|
.sort((left, right) => {
|
|
194
|
-
//
|
|
195
|
-
// modelPickerOrder
|
|
196
|
+
// OpenCodex guidance candidates rank by natural priority (SPAWN_PRIORITY_FIELD when present),
|
|
197
|
+
// so modelPickerOrder does not change this projection. Native tool advertisements differ. Rows the
|
|
196
198
|
// override did not move fall back to their Codex-visible `priority`.
|
|
197
199
|
const spawnPriorityOf = (entry: RawEntry): number => {
|
|
198
200
|
const spawn = entry[SPAWN_PRIORITY_FIELD];
|
|
@@ -315,6 +317,8 @@ export function deriveEntry(
|
|
|
315
317
|
contextCap?: NativeContextLimitsInput,
|
|
316
318
|
): RawEntry {
|
|
317
319
|
const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
|
|
320
|
+
// Go exposes model-specific upstream enums; synthetic tiers mislead subagent overrides.
|
|
321
|
+
const preserveExactReasoning = preserveExact || model?.provider === "opencode-go";
|
|
318
322
|
const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true
|
|
319
323
|
? upstreamNativeEntry(model.id)
|
|
320
324
|
: null;
|
|
@@ -328,6 +332,8 @@ export function deriveEntry(
|
|
|
328
332
|
}
|
|
329
333
|
if (template || codexForwardNativeCapabilityAlias) {
|
|
330
334
|
const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
|
|
335
|
+
// A cached template may carry display-order history; each new row owns its natural rank.
|
|
336
|
+
delete e[SPAWN_PRIORITY_FIELD];
|
|
331
337
|
e.slug = slug;
|
|
332
338
|
e.display_name = routedDisplayName(slug, model);
|
|
333
339
|
e.description = desc;
|
|
@@ -359,7 +365,7 @@ export function deriveEntry(
|
|
|
359
365
|
e,
|
|
360
366
|
model?.reasoningEfforts,
|
|
361
367
|
model?.defaultReasoningEffort,
|
|
362
|
-
|
|
368
|
+
preserveExactReasoning || codexForwardNativeCapabilityAlias !== null,
|
|
363
369
|
);
|
|
364
370
|
// This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
|
|
365
371
|
// native tool/search/responses-lite contract while preserving the routed slug and wire id.
|
|
@@ -409,7 +415,7 @@ export function deriveEntry(
|
|
|
409
415
|
};
|
|
410
416
|
if (isRouted) {
|
|
411
417
|
applyRoutedCodexToolMode(entry, model?.codexToolMode);
|
|
412
|
-
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort,
|
|
418
|
+
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoning);
|
|
413
419
|
}
|
|
414
420
|
else {
|
|
415
421
|
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
|
|
@@ -516,27 +522,25 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
516
522
|
// catalog stays put across rebuilds. Featured rows keep their existing 0..N-1 band; when
|
|
517
523
|
// modelPickerOrder is unset the helper is a no-op and every priority below is byte-identical to
|
|
518
524
|
// before. The spawn_agent candidate window is derived separately from SPAWN_PRIORITY_FIELD, so
|
|
519
|
-
// this display reorder
|
|
520
|
-
const pickerOrder =
|
|
521
|
-
? modelPickerOrder.filter((id): id is string => typeof id === "string" && id.length > 0)
|
|
522
|
-
: [];
|
|
525
|
+
// this display reorder does not change OpenCodex's guidance candidate calculation.
|
|
526
|
+
const pickerOrder = normalizeModelPickerOrder(modelPickerOrder);
|
|
523
527
|
const pickerOrderRank = new Map(pickerOrder.map((slug, i) => [slug, i] as const));
|
|
524
528
|
const pickerOrderActive = pickerOrder.length > 0;
|
|
525
529
|
// The display band reuses the existing high priority tier (>= PICKER_ORDER_PRIORITY_BASE, the
|
|
526
530
|
// same 1_000+ neighborhood account rows occupy), keeping listed rows visually after the featured
|
|
527
|
-
// band.
|
|
531
|
+
// band. OpenCodex guidance membership does not depend on this — see SPAWN_PRIORITY_FIELD.
|
|
528
532
|
/**
|
|
529
533
|
* Priority for a non-featured routed row that is explicitly LISTED in modelPickerOrder. Listed
|
|
530
534
|
* slugs sort in declared order within the high picker-order display tier
|
|
531
535
|
* (>= PICKER_ORDER_PRIORITY_BASE). This sets the Codex-visible `priority` only; the caller records
|
|
532
|
-
* the row's natural priority in SPAWN_PRIORITY_FIELD
|
|
533
|
-
*
|
|
536
|
+
* the row's natural priority in SPAWN_PRIORITY_FIELD for OpenCodex's unchanged guidance window.
|
|
537
|
+
* Returns undefined when the feature is off or the row is not listed, so those rows
|
|
534
538
|
* keep their original assignment (default 5 / account 1_000+) untouched.
|
|
535
539
|
*
|
|
536
540
|
* Scope: only the generic routed `<provider>/<model>` rows call this (see the goModels loop
|
|
537
541
|
* below). Native passthrough rows and account-qualified native rows keep their own priority
|
|
538
|
-
* logic and are intentionally not reordered
|
|
539
|
-
*
|
|
542
|
+
* logic and are intentionally not reordered in this legacy builder pass. The final merge can
|
|
543
|
+
* apply complete ordering when the configured list includes a bare id.
|
|
540
544
|
*/
|
|
541
545
|
const pickerOrderPriority = (slug: string, altSlug?: string): number | undefined => {
|
|
542
546
|
if (!pickerOrderActive) return undefined;
|
|
@@ -658,9 +662,9 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
658
662
|
// Keep the generated account rows together in Codex's priority-sorted flat picker.
|
|
659
663
|
e.priority = 1_000 + (typeof e.priority === "number" ? e.priority : 5);
|
|
660
664
|
}
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
//
|
|
665
|
+
// The legacy routed-only builder pass keeps featured ranks and records natural priority
|
|
666
|
+
// before changing non-featured display priority. The final complete-order pass may move
|
|
667
|
+
// featured display rows too; OpenCodex guidance continues to use their natural ranks.
|
|
664
668
|
if (rankHit === undefined) {
|
|
665
669
|
const pickerPriority = pickerOrderPriority(slug, `${m.provider}/${m.id}`);
|
|
666
670
|
if (pickerPriority !== undefined) {
|
|
@@ -779,12 +783,39 @@ export const CANONICAL_NATIVE_CATALOG_CONTENT_POLICY: Readonly<
|
|
|
779
783
|
unsupportedNativeEntries: "drop",
|
|
780
784
|
});
|
|
781
785
|
|
|
786
|
+
function normalizeModelPickerOrder(order: unknown): string[] {
|
|
787
|
+
return Array.isArray(order)
|
|
788
|
+
? order.filter((id): id is string => typeof id === "string" && id.trim().length > 0)
|
|
789
|
+
: [];
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** Preserve exact-id precedence while accepting the existing raw/encoded slug spellings. */
|
|
793
|
+
function modelPickerRank(order: readonly string[]): (slug: string) => number | undefined {
|
|
794
|
+
const exact = new Map(order.map((slug, index) => [slug, index]));
|
|
795
|
+
const equivalent = new Map(order.map((slug, index) => [slugEquivalenceKey(slug), index]));
|
|
796
|
+
return slug => exact.get(slug) ?? equivalent.get(slugEquivalenceKey(slug));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** Complete display ordering retains natural ranks for OpenCodex's separate guidance projection. */
|
|
800
|
+
export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly string[]): void {
|
|
801
|
+
const pickerOrder = normalizeModelPickerOrder(order);
|
|
802
|
+
if (!pickerOrder.some(slug => !slug.includes("/"))) return;
|
|
803
|
+
const rankOf = modelPickerRank(pickerOrder);
|
|
804
|
+
for (const entry of entries) {
|
|
805
|
+
const natural = entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9;
|
|
806
|
+
entry[SPAWN_PRIORITY_FIELD] = natural;
|
|
807
|
+
entry.priority = rankOf(String(entry.slug)) ?? pickerOrder.length + Number(natural);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
782
811
|
export interface ObservedCatalogMergeInput {
|
|
783
812
|
readonly catalogModels: readonly RawEntry[];
|
|
784
813
|
readonly baselineCatalogModels: readonly RawEntry[];
|
|
785
814
|
readonly routedEntries: readonly RawEntry[];
|
|
786
815
|
readonly baseline: ReadonlyMap<string, number>;
|
|
787
816
|
readonly featured: readonly string[];
|
|
817
|
+
readonly modelPickerOrder?: readonly string[];
|
|
818
|
+
readonly accountSelectors?: readonly string[];
|
|
788
819
|
readonly wsEnabled: boolean;
|
|
789
820
|
readonly template: RawEntry | null;
|
|
790
821
|
readonly disabledModels: ReadonlySet<string>;
|
|
@@ -817,6 +848,8 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
817
848
|
routedEntries,
|
|
818
849
|
baseline,
|
|
819
850
|
featured,
|
|
851
|
+
modelPickerOrder = [],
|
|
852
|
+
accountSelectors = [],
|
|
820
853
|
wsEnabled,
|
|
821
854
|
template,
|
|
822
855
|
disabledModels,
|
|
@@ -975,7 +1008,9 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
975
1008
|
finished.priority = nativePriority(slug, upstream.priority);
|
|
976
1009
|
return finished;
|
|
977
1010
|
}
|
|
978
|
-
const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m.priority) });
|
|
1011
|
+
const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m[SPAWN_PRIORITY_FIELD] ?? m.priority) });
|
|
1012
|
+
// Recompute spawn rank from current featured models, not a prior picker override.
|
|
1013
|
+
delete preserved[SPAWN_PRIORITY_FIELD];
|
|
979
1014
|
// Older natives kept from disk still need the mock top tiers (max + ultra always
|
|
980
1015
|
// for subagent max spawns; wire-clamped to the model's real top rung).
|
|
981
1016
|
if (!isGpt56NativeSlug(slug) && slug !== NATIVE_RESERVE_MODEL) ensureUltraReasoningLevel(preserved);
|
|
@@ -1060,6 +1095,32 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
1060
1095
|
// remain outside provider ownership and survive unless a fresh row replaces their exact slug.
|
|
1061
1096
|
return !isOcxAuthoredRoutedEntry(entry);
|
|
1062
1097
|
});
|
|
1098
|
+
// Retained rows bypass the builder. Recompute managed spawn ranks from current config
|
|
1099
|
+
// before either display-order mode; a saved display override is not current roster authority.
|
|
1100
|
+
const pickerOrder = normalizeModelPickerOrder(modelPickerOrder);
|
|
1101
|
+
const fullPickerOrder = pickerOrder.some(slug => !slug.includes("/"));
|
|
1102
|
+
const rankOf = modelPickerRank(pickerOrder);
|
|
1103
|
+
const featuredRankOf = modelPickerRank(featured);
|
|
1104
|
+
const priorityStride = Math.max(accountSelectors.length, 1);
|
|
1105
|
+
for (const entry of preservedRoutedEntries) {
|
|
1106
|
+
const natural = entry[SPAWN_PRIORITY_FIELD];
|
|
1107
|
+
if (typeof natural === "number") {
|
|
1108
|
+
entry.priority = natural;
|
|
1109
|
+
delete entry[SPAWN_PRIORITY_FIELD];
|
|
1110
|
+
}
|
|
1111
|
+
const slug = String(entry.slug);
|
|
1112
|
+
if (!isOcxAuthoredRoutedEntry(entry) || isNativeAliasCatalogEntry(entry)) continue;
|
|
1113
|
+
const featuredRank = featuredRankOf(slug);
|
|
1114
|
+
entry.priority = featuredRank !== undefined
|
|
1115
|
+
? featuredRank * priorityStride
|
|
1116
|
+
: (accountSelectors.length > 0 ? 1_000 : 0) + 5;
|
|
1117
|
+
if (featuredRank !== undefined || fullPickerOrder) continue;
|
|
1118
|
+
const pickerIndex = rankOf(slug);
|
|
1119
|
+
if (pickerIndex !== undefined) {
|
|
1120
|
+
entry[SPAWN_PRIORITY_FIELD] = entry.priority;
|
|
1121
|
+
entry.priority = PICKER_ORDER_PRIORITY_BASE + pickerIndex * priorityStride;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1063
1124
|
let finalRoutedEntries = [...admittedRoutedEntries, ...preservedRoutedEntries];
|
|
1064
1125
|
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
1065
1126
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
@@ -1134,7 +1195,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
1134
1195
|
// Mock-max universality (260709): preserved routed entries from disk may predate
|
|
1135
1196
|
// the max rung — ensure it here so subagent max spawns validate on every
|
|
1136
1197
|
// reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact.
|
|
1137
|
-
if (!exactCombo && !reserveProjection) {
|
|
1198
|
+
if (!exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
|
|
1138
1199
|
const levels = Array.isArray(e.supported_reasoning_levels)
|
|
1139
1200
|
? e.supported_reasoning_levels as Array<{ effort?: string }>
|
|
1140
1201
|
: [];
|
|
@@ -1161,6 +1222,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
1161
1222
|
multiAgentV2Enabled,
|
|
1162
1223
|
{ keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection },
|
|
1163
1224
|
);
|
|
1225
|
+
applyFullModelPickerOrder(versionedEntries, modelPickerOrder);
|
|
1164
1226
|
for (const entry of versionedEntries) {
|
|
1165
1227
|
const kind = entry.opencodex_catalog_kind;
|
|
1166
1228
|
if (trustedAccountBoundNativeCatalogSlug(entry) === undefined
|
|
@@ -1762,6 +1824,8 @@ function writeRetainedCatalogSync({
|
|
|
1762
1824
|
}).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined)
|
|
1763
1825
|
: [];
|
|
1764
1826
|
catalog.models = mergeCatalogEntriesFromObservedState({
|
|
1827
|
+
modelPickerOrder,
|
|
1828
|
+
accountSelectors,
|
|
1765
1829
|
catalogModels: catalogModelsForMerge,
|
|
1766
1830
|
baselineCatalogModels: baselineCatalog?.models ?? [],
|
|
1767
1831
|
routedEntries: goEntries,
|
package/src/codex/convergence.ts
CHANGED
package/src/codex/inject.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
readConfigAdmissionSnapshot,
|
|
7
7
|
subagentDefaultSyncEffective,
|
|
8
8
|
websocketsEnabled,
|
|
9
|
+
withConfigMutationLockSync,
|
|
9
10
|
} from "../config";
|
|
10
11
|
import { CodexWriteLockSkipped, withCodexWriteLock } from "./codex-write-lock";
|
|
11
12
|
import { shouldSyncCodexOnStart } from "./desired-state";
|
|
@@ -148,6 +149,18 @@ export interface InjectCodexOptions {
|
|
|
148
149
|
/** Explicit remote routing target. Absence preserves byte-compatible standalone output. */
|
|
149
150
|
routingTarget?: CodexRoutingTarget;
|
|
150
151
|
journalOwner?: { kind: "process" } | { kind: "client"; apiKeyId: string };
|
|
152
|
+
/** Synchronous read-only client ownership guard, evaluated at the artifact commit boundary. */
|
|
153
|
+
beforeClientWrite?: () => void;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function runClientWriteGuard(guard: InjectCodexOptions["beforeClientWrite"]): void {
|
|
157
|
+
const result: unknown = guard?.();
|
|
158
|
+
if (result !== null && (typeof result === "object" || typeof result === "function")
|
|
159
|
+
&& typeof (result as { then?: unknown }).then === "function") {
|
|
160
|
+
// Reject async guards without leaving their eventual rejection unhandled.
|
|
161
|
+
void Promise.resolve(result).catch(() => {});
|
|
162
|
+
throw new Error("Connected client write guard must be synchronous");
|
|
163
|
+
}
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
export interface CodexRoutingTarget {
|
|
@@ -901,7 +914,14 @@ export async function injectCodexConfig(
|
|
|
901
914
|
if (activeProvider) {
|
|
902
915
|
// A launcher may have journaled before the provider manager took ownership. Never let shutdown
|
|
903
916
|
// replay that stale snapshot over externally managed config.
|
|
904
|
-
if (!options.validateOnly)
|
|
917
|
+
if (!options.validateOnly) {
|
|
918
|
+
if (options.beforeClientWrite) {
|
|
919
|
+
withConfigMutationLockSync(() => {
|
|
920
|
+
runClientWriteGuard(options.beforeClientWrite);
|
|
921
|
+
removeJournal();
|
|
922
|
+
});
|
|
923
|
+
} else removeJournal();
|
|
924
|
+
}
|
|
905
925
|
const nativeSubagentDefaultsWarning = configuredManagedSubagentDefaults(
|
|
906
926
|
config,
|
|
907
927
|
)
|
|
@@ -974,8 +994,8 @@ export async function injectCodexConfig(
|
|
|
974
994
|
// not-ours (which would make them unrestorable).
|
|
975
995
|
content = stripJournaledOpenaiBaseUrl(
|
|
976
996
|
content,
|
|
977
|
-
journaledInjectedOpenaiBaseUrl(),
|
|
978
|
-
journaledInjectedRealtimeWsBaseUrl(),
|
|
997
|
+
journaledInjectedOpenaiBaseUrl({ readOnly: !!options.beforeClientWrite }),
|
|
998
|
+
journaledInjectedRealtimeWsBaseUrl({ readOnly: !!options.beforeClientWrite }),
|
|
979
999
|
);
|
|
980
1000
|
if (hasOcxProviderTable(content)) {
|
|
981
1001
|
content = removeOcxSection(content);
|
|
@@ -1179,17 +1199,24 @@ export async function injectCodexConfig(
|
|
|
1179
1199
|
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
1180
1200
|
|
|
1181
1201
|
if (eligibility.kind === "legacy-uncoordinated") {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1202
|
+
const applyLegacy = (): CodexInjectResult | undefined => {
|
|
1203
|
+
if (!shouldSyncCodexOnStart(loadConfig())) {
|
|
1204
|
+
return {
|
|
1205
|
+
success: true,
|
|
1206
|
+
status: "skipped",
|
|
1207
|
+
skippedReason: "desired_disabled",
|
|
1208
|
+
message: "Codex integration is OFF; no Codex config, catalog, cache, or history was changed.",
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
runClientWriteGuard(options.beforeClientWrite);
|
|
1212
|
+
applyNativeArtifacts();
|
|
1213
|
+
};
|
|
1214
|
+
// Only connected guarded writes add C here. A concurrent disconnect claim
|
|
1215
|
+
// either follows this commit or is observed by the guard before any write.
|
|
1216
|
+
const skipped = options.beforeClientWrite
|
|
1217
|
+
? withConfigMutationLockSync(applyLegacy)
|
|
1218
|
+
: applyLegacy();
|
|
1219
|
+
if (skipped) return skipped;
|
|
1193
1220
|
} else {
|
|
1194
1221
|
const coordinated = await withCodexWriteLock(
|
|
1195
1222
|
{
|
|
@@ -1210,6 +1237,10 @@ export async function injectCodexConfig(
|
|
|
1210
1237
|
if (!shouldSyncCodexOnStart(loadConfig())) {
|
|
1211
1238
|
throw new CodexWriteLockSkipped("desired_disabled");
|
|
1212
1239
|
}
|
|
1240
|
+
// N and C are held here. Reject stale client work before publishing a
|
|
1241
|
+
// transition or capturing preimages; rejection must not compensate over
|
|
1242
|
+
// a disconnect's restored files.
|
|
1243
|
+
runClientWriteGuard(options.beforeClientWrite);
|
|
1213
1244
|
/*
|
|
1214
1245
|
* Publish BEFORE touching the filesystem. `assertPublished` runs after this
|
|
1215
1246
|
* callback returns and throws unless a transition was recorded, so writing
|
package/src/codex/journal.ts
CHANGED
|
@@ -161,13 +161,13 @@ export function markJournalInjectedState(
|
|
|
161
161
|
* survives such a rewrite, so restore can still prove the URL is ours -- and, just as
|
|
162
162
|
* importantly, prove that a DIFFERENT URL is not.
|
|
163
163
|
*/
|
|
164
|
-
export function journaledInjectedOpenaiBaseUrl(): string | null {
|
|
165
|
-
return readJournal()?.injectedOpenaiBaseUrl ?? null;
|
|
164
|
+
export function journaledInjectedOpenaiBaseUrl(options: { readOnly?: boolean } = {}): string | null {
|
|
165
|
+
return readJournal(options.readOnly !== true)?.injectedOpenaiBaseUrl ?? null;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/** The root `experimental_realtime_ws_base_url` the last injection wrote, or null. */
|
|
169
|
-
export function journaledInjectedRealtimeWsBaseUrl(): string | null {
|
|
170
|
-
return readJournal()?.injectedRealtimeWsBaseUrl ?? null;
|
|
169
|
+
export function journaledInjectedRealtimeWsBaseUrl(options: { readOnly?: boolean } = {}): string | null {
|
|
170
|
+
return readJournal(options.readOnly !== true)?.injectedRealtimeWsBaseUrl ?? null;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/** The catalog path the last injection wrote to, or null when none was recorded. */
|
|
@@ -179,14 +179,14 @@ export function removeJournal(): void {
|
|
|
179
179
|
try { unlinkSync(JOURNAL_PATH); } catch { /* ignore */ }
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
function readJournal(): Journal | null {
|
|
182
|
+
function readJournal(cleanInvalid = true): Journal | null {
|
|
183
183
|
if (!existsSync(JOURNAL_PATH)) return null;
|
|
184
184
|
try {
|
|
185
185
|
const journal = JSON.parse(readFileSync(JOURNAL_PATH, "utf-8")) as Journal;
|
|
186
186
|
if (journal.version !== 1) throw new Error("unknown version");
|
|
187
187
|
return journal;
|
|
188
188
|
} catch {
|
|
189
|
-
removeJournal();
|
|
189
|
+
if (cleanInvalid) removeJournal();
|
|
190
190
|
return null;
|
|
191
191
|
}
|
|
192
192
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Diagnostic only: never use this outcome as quota, entitlement, or admission evidence. */
|
|
2
|
+
export type CodexQuotaRefreshOutcome =
|
|
3
|
+
| { status: "http_error"; httpStatus: number }
|
|
4
|
+
| { status: "ok" | "not_reported" | "timeout" | "network_error" | "invalid_response" | "internal_error" };
|
|
5
|
+
|
|
6
|
+
/** The management response is untrusted at the CLI boundary; copy only the fixed vocabulary. */
|
|
7
|
+
export function projectCodexQuotaRefreshOutcome(value: unknown): CodexQuotaRefreshOutcome | undefined {
|
|
8
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
9
|
+
const row = value as Record<string, unknown>;
|
|
10
|
+
if (row.status === "http_error") {
|
|
11
|
+
return typeof row.httpStatus === "number" && Number.isInteger(row.httpStatus)
|
|
12
|
+
&& row.httpStatus >= 100 && row.httpStatus <= 599
|
|
13
|
+
? { status: "http_error", httpStatus: row.httpStatus }
|
|
14
|
+
: undefined;
|
|
15
|
+
}
|
|
16
|
+
switch (row.status) {
|
|
17
|
+
case "ok":
|
|
18
|
+
case "not_reported":
|
|
19
|
+
case "timeout":
|
|
20
|
+
case "network_error":
|
|
21
|
+
case "invalid_response":
|
|
22
|
+
case "internal_error":
|
|
23
|
+
return { status: row.status };
|
|
24
|
+
default:
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/codex/shim.ts
CHANGED
|
@@ -961,14 +961,20 @@ function unixProcessGroupAlive(groupId: number): boolean {
|
|
|
961
961
|
}
|
|
962
962
|
|
|
963
963
|
function terminateUnixProcessGroup(groupId: number): void {
|
|
964
|
+
let permissionError: unknown;
|
|
964
965
|
try {
|
|
965
966
|
process.kill(-groupId, "SIGKILL");
|
|
966
967
|
} catch (error) {
|
|
967
|
-
|
|
968
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
969
|
+
if (code === "EPERM") permissionError = error;
|
|
970
|
+
else if (code !== "ESRCH") throw error;
|
|
968
971
|
}
|
|
972
|
+
// A concurrently exiting group can briefly reject a second signal. Only
|
|
973
|
+
// observed disappearance clears that uncertainty; never send another signal.
|
|
969
974
|
const deadline = Date.now() + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS;
|
|
970
975
|
while (Date.now() < deadline && unixProcessGroupAlive(groupId)) Bun.sleepSync(10);
|
|
971
976
|
if (unixProcessGroupAlive(groupId)) {
|
|
977
|
+
if (permissionError) throw permissionError;
|
|
972
978
|
throw new Error(`Codex shim install probe process group ${groupId} did not terminate`);
|
|
973
979
|
}
|
|
974
980
|
}
|
package/src/combos/resolve.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OcxComboTarget, OcxConfig } from "../types";
|
|
2
2
|
import { getCachedProviderQuota } from "../providers/quota-routing-cache";
|
|
3
3
|
import type { ProviderQuota } from "../providers/quota-types";
|
|
4
|
+
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
4
5
|
import { sleepWithAbort } from "../lib/upstream-retry";
|
|
5
6
|
import {
|
|
6
7
|
coolComboTarget,
|
|
@@ -60,9 +61,13 @@ export class NoAvailableComboTargetsError extends Error {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget): boolean {
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget, now: number): boolean {
|
|
65
|
+
if (!Object.hasOwn(config.providers, target.provider)) return false;
|
|
66
|
+
const provider = config.providers[target.provider];
|
|
67
|
+
if (!provider || provider.disabled === true) return false;
|
|
68
|
+
// Native account selection owns model-scoped quota; a provider summary cannot veto it.
|
|
69
|
+
return isCanonicalOpenAiForwardProvider(provider)
|
|
70
|
+
|| !cachedProviderQuotaIsExhausted(getCachedProviderQuota(target.provider, now), now);
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
function quotaWindowExhausted(percent: number | undefined, resetAt: number | undefined, now: number): boolean {
|
|
@@ -159,8 +164,7 @@ export function pickComboTarget(
|
|
|
159
164
|
const excluded = new Set(options.exclude ?? []);
|
|
160
165
|
const now = options.now ?? Date.now();
|
|
161
166
|
const eligible = (target: Required<OcxComboTarget>): boolean =>
|
|
162
|
-
targetProviderIsUsable(config, target)
|
|
163
|
-
&& !cachedProviderQuotaIsExhausted(getCachedProviderQuota(target.provider, now), now)
|
|
167
|
+
targetProviderIsUsable(config, target, now)
|
|
164
168
|
&& !isComboTargetInCooldown(comboId, target, now)
|
|
165
169
|
&& !excluded.has(targetKey(target))
|
|
166
170
|
&& (options.eligible?.(target) ?? true);
|
|
@@ -338,8 +342,7 @@ export async function pickComboTargetWithWait(
|
|
|
338
342
|
const combo = getCombo(config, comboId);
|
|
339
343
|
if (!combo) throw new UnknownComboError(comboId);
|
|
340
344
|
const waitingTargets = combo.targets.filter(target =>
|
|
341
|
-
targetProviderIsUsable(config, target)
|
|
342
|
-
&& !cachedProviderQuotaIsExhausted(getCachedProviderQuota(target.provider, now), now)
|
|
345
|
+
targetProviderIsUsable(config, target, now)
|
|
343
346
|
&& !excluded.has(targetKey(target))
|
|
344
347
|
&& isComboTargetInCooldown(comboId, target, now)
|
|
345
348
|
&& (customEligible?.(target) ?? true),
|
package/src/config.ts
CHANGED
|
@@ -932,6 +932,15 @@ const clientIntegrationsSchema = z.object({
|
|
|
932
932
|
"claude-desktop": z.boolean().optional().catch(undefined),
|
|
933
933
|
}).passthrough();
|
|
934
934
|
|
|
935
|
+
const asideProfileSyncSchema = z.object({
|
|
936
|
+
allProfiles: z.boolean().optional(),
|
|
937
|
+
profiles: z.record(
|
|
938
|
+
z.string().regex(/^(0|[1-9][0-9]*)$/).refine(value => Number.isSafeInteger(Number(value))),
|
|
939
|
+
z.boolean(),
|
|
940
|
+
).optional(),
|
|
941
|
+
legacyProfileId: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).nullable().optional(),
|
|
942
|
+
}).passthrough();
|
|
943
|
+
|
|
935
944
|
const agentTaskRecoverySchema = z.object({
|
|
936
945
|
enabled: z.boolean().optional(),
|
|
937
946
|
model: z.string().trim().min(1).optional(),
|
|
@@ -1134,7 +1143,10 @@ const configSchema = z.object({
|
|
|
1134
1143
|
subagentModelsVersion: z.number().int().positive().optional().catch(undefined),
|
|
1135
1144
|
subagentModels: z.array(z.string().min(1)).optional().catch(undefined),
|
|
1136
1145
|
clientIntegrations: clientIntegrationsSchema.optional().catch(undefined),
|
|
1146
|
+
// A malformed profile policy must not fall back to legacy all-profile activation.
|
|
1147
|
+
asideProfileSync: asideProfileSyncSchema.optional().catch({ allProfiles: false }),
|
|
1137
1148
|
providerContextCaps: z.record(z.string(), z.number().int().positive()).optional(),
|
|
1149
|
+
providerContextCapValues: z.record(z.string(), z.number().int().positive()).optional(),
|
|
1138
1150
|
contextCapValue: z.number().int().positive().optional(),
|
|
1139
1151
|
multiAgentGuidanceEnabled: z.boolean().optional(),
|
|
1140
1152
|
// Invalid optional recovery config must not discard unrelated provider/account state.
|
|
@@ -3738,11 +3750,12 @@ function warnProxyConfigDiscardOnce(kind: "proxy" | "noProxy" | "noProxyElements
|
|
|
3738
3750
|
}
|
|
3739
3751
|
|
|
3740
3752
|
/**
|
|
3741
|
-
* Mirror `config.proxy` into HTTP(S)_PROXY env vars
|
|
3742
|
-
*
|
|
3743
|
-
*
|
|
3744
|
-
* CLI's own health checks and
|
|
3745
|
-
*
|
|
3753
|
+
* Mirror `config.proxy` into HTTP(S)_PROXY env vars. Bun fetch consumes them natively; transports
|
|
3754
|
+
* such as the ChatGPT upstream WebSocket select the same environment explicitly. User-set HTTP(S)_PROXY
|
|
3755
|
+
* variables win; config fills missing scheme proxies, which take precedence over ALL_PROXY for WS.
|
|
3756
|
+
* localhost/127.0.0.1 are appended to NO_PROXY so the CLI's own health checks and
|
|
3757
|
+
* running-proxy API calls stay direct. Call once per process entry that makes outbound provider
|
|
3758
|
+
* requests (server start, catalog sync).
|
|
3746
3759
|
*/
|
|
3747
3760
|
export function applyProxyEnv(config: OcxConfig): void {
|
|
3748
3761
|
applyProxyEnvWith(config);
|