@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/server/lifecycle.ts
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
beginBackgroundShellShutdown,
|
|
21
21
|
terminateAllBackgroundShells,
|
|
22
22
|
} from "../adapters/cursor/native-exec-shell";
|
|
23
|
+
import type { CodexAccountSelectionAdmission } from "../codex/auth-context";
|
|
24
|
+
import { releaseNativeMainStartupLifecycle } from "../codex/native-profile-startup";
|
|
23
25
|
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
25
27
|
// Active turn tracking + graceful shutdown drain
|
|
@@ -29,24 +31,138 @@ export const MAX_ACTIVE_TURNS = 256;
|
|
|
29
31
|
const turnGate = createAdmissionGate("active_turns", MAX_ACTIVE_TURNS);
|
|
30
32
|
export interface ActiveTurnLease extends AdmissionLease {
|
|
31
33
|
bindAbortController(ac: AbortController): void;
|
|
34
|
+
beginCodexAccountSelection(): CodexAccountSelectionAdmission;
|
|
32
35
|
isTransferred(): boolean;
|
|
33
36
|
}
|
|
34
37
|
const activeTurns = new Map<AbortController, ActiveTurnLease>();
|
|
35
38
|
const admittedTurns = new Set<ActiveTurnLease>();
|
|
36
39
|
const knownTurnControllers = new WeakSet<AbortController>();
|
|
37
40
|
let turnReleaseMisses = 0;
|
|
38
|
-
let
|
|
41
|
+
let shutdownDraining = false;
|
|
42
|
+
const temporaryDrainOwners = new Set<symbol>();
|
|
43
|
+
const nativeMainDrainOwners = new Set<symbol>();
|
|
44
|
+
const temporaryDrainWaiters = new Set<() => void>();
|
|
45
|
+
const nativeMainTurns = new Set<ActiveTurnLease>();
|
|
46
|
+
let nativeMainSelections = 0;
|
|
47
|
+
let legacyDrainLease: AdmissionLease | null = null;
|
|
39
48
|
let recyclingForExit = false;
|
|
40
49
|
let _serverRef: ReturnType<typeof Bun.serve> | undefined;
|
|
50
|
+
let serverStopFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
|
|
51
|
+
let serverStartupReleaseFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
|
|
52
|
+
let releaseServerStartupLifecycleImpl: typeof releaseNativeMainStartupLifecycle = releaseNativeMainStartupLifecycle;
|
|
41
53
|
|
|
42
54
|
export function setServerRef(server: ReturnType<typeof Bun.serve> | undefined): void { _serverRef = server; }
|
|
43
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Legacy/test-only drain control. Production scoped operations must use a lease,
|
|
57
|
+
* while process shutdown uses the irreversible shutdown latch.
|
|
58
|
+
*/
|
|
59
|
+
export function setDraining(value: boolean): void {
|
|
60
|
+
if (value) {
|
|
61
|
+
legacyDrainLease ??= acquireTemporaryDrain("legacy");
|
|
62
|
+
} else {
|
|
63
|
+
legacyDrainLease?.release();
|
|
64
|
+
legacyDrainLease = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function temporaryDrainCount(): number {
|
|
69
|
+
return temporaryDrainOwners.size + nativeMainDrainOwners.size;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function notifyTemporaryDrainsSettled(): void {
|
|
73
|
+
if (temporaryDrainCount() !== 0) return;
|
|
74
|
+
for (const resolve of temporaryDrainWaiters) resolve();
|
|
75
|
+
temporaryDrainWaiters.clear();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Acquire the single owner-scoped global data-plane fence. */
|
|
79
|
+
export function acquireTemporaryDrain(owner: string): AdmissionLease | null {
|
|
80
|
+
if (shutdownDraining || temporaryDrainCount() > 0) return null;
|
|
81
|
+
const token = Symbol(owner);
|
|
82
|
+
temporaryDrainOwners.add(token);
|
|
83
|
+
let active = true;
|
|
84
|
+
return {
|
|
85
|
+
release() {
|
|
86
|
+
if (!active) return;
|
|
87
|
+
active = false;
|
|
88
|
+
temporaryDrainOwners.delete(token);
|
|
89
|
+
notifyTemporaryDrainsSettled();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Fence only turns that select the native Codex `__main__` account. */
|
|
95
|
+
export function acquireNativeMainProfileDrain(owner: string): AdmissionLease | null {
|
|
96
|
+
if (shutdownDraining || temporaryDrainCount() > 0) return null;
|
|
97
|
+
const token = Symbol(owner);
|
|
98
|
+
nativeMainDrainOwners.add(token);
|
|
99
|
+
let active = true;
|
|
100
|
+
return {
|
|
101
|
+
release() {
|
|
102
|
+
if (!active) return;
|
|
103
|
+
active = false;
|
|
104
|
+
nativeMainDrainOwners.delete(token);
|
|
105
|
+
notifyTemporaryDrainsSettled();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Permanently fence this process for shutdown; scoped lease release cannot clear it. */
|
|
111
|
+
export function beginShutdownDrain(): boolean {
|
|
112
|
+
if (shutdownDraining) return false;
|
|
113
|
+
shutdownDraining = true;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isShutdownDraining(): boolean { return shutdownDraining; }
|
|
118
|
+
|
|
119
|
+
export function waitForTemporaryDrains(): Promise<void> {
|
|
120
|
+
if (temporaryDrainCount() === 0) return Promise.resolve();
|
|
121
|
+
return new Promise(resolve => temporaryDrainWaiters.add(resolve));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Wait for scoped drains without allowing them to outlive the shutdown deadline. */
|
|
125
|
+
async function waitForTemporaryDrainsUntil(deadlineMs: number): Promise<boolean> {
|
|
126
|
+
if (temporaryDrainCount() === 0) return true;
|
|
127
|
+
const remainingMs = Math.max(0, deadlineMs - Date.now());
|
|
128
|
+
if (remainingMs === 0) return false;
|
|
129
|
+
return new Promise<boolean>((resolve) => {
|
|
130
|
+
let settled = false;
|
|
131
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
132
|
+
const finish = (drained: boolean) => {
|
|
133
|
+
if (settled) return;
|
|
134
|
+
settled = true;
|
|
135
|
+
if (timer) clearTimeout(timer);
|
|
136
|
+
resolve(drained);
|
|
137
|
+
};
|
|
138
|
+
timer = setTimeout(() => finish(false), remainingMs);
|
|
139
|
+
void waitForTemporaryDrains().then(() => finish(true));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Test-only process-lifetime reset. Never call from production recovery paths. */
|
|
144
|
+
export function resetLifecycleDrainStateForTests(): void {
|
|
145
|
+
legacyDrainLease?.release();
|
|
146
|
+
legacyDrainLease = null;
|
|
147
|
+
temporaryDrainOwners.clear();
|
|
148
|
+
nativeMainDrainOwners.clear();
|
|
149
|
+
nativeMainTurns.clear();
|
|
150
|
+
nativeMainSelections = 0;
|
|
151
|
+
for (const resolve of temporaryDrainWaiters) resolve();
|
|
152
|
+
temporaryDrainWaiters.clear();
|
|
153
|
+
shutdownDraining = false;
|
|
154
|
+
serverStopFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
|
|
155
|
+
serverStartupReleaseFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
|
|
156
|
+
releaseServerStartupLifecycleImpl = releaseNativeMainStartupLifecycle;
|
|
157
|
+
}
|
|
44
158
|
export function tryAdmitTurn(): ActiveTurnLease | null {
|
|
159
|
+
if (isDraining()) return null;
|
|
45
160
|
const gateLease = turnGate.tryAcquire();
|
|
46
161
|
if (!gateLease) return null;
|
|
47
162
|
const controllers = new Set<AbortController>();
|
|
48
163
|
let active = true;
|
|
49
164
|
let transferred = false;
|
|
165
|
+
let nativeMainClaimed = false;
|
|
50
166
|
const lease: ActiveTurnLease = {
|
|
51
167
|
bindAbortController(ac) {
|
|
52
168
|
knownTurnControllers.add(ac);
|
|
@@ -58,6 +174,31 @@ export function tryAdmitTurn(): ActiveTurnLease | null {
|
|
|
58
174
|
controllers.add(ac);
|
|
59
175
|
activeTurns.set(ac, lease);
|
|
60
176
|
},
|
|
177
|
+
beginCodexAccountSelection() {
|
|
178
|
+
const mainProfileDraining = nativeMainDrainOwners.size > 0;
|
|
179
|
+
let selectionActive = !mainProfileDraining;
|
|
180
|
+
let released = false;
|
|
181
|
+
if (selectionActive) nativeMainSelections += 1;
|
|
182
|
+
return {
|
|
183
|
+
mainProfileDraining,
|
|
184
|
+
claimMainProfile() {
|
|
185
|
+
if (released || mainProfileDraining || !active) return false;
|
|
186
|
+
if (!nativeMainClaimed) {
|
|
187
|
+
nativeMainClaimed = true;
|
|
188
|
+
nativeMainTurns.add(lease);
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
},
|
|
192
|
+
release() {
|
|
193
|
+
if (released) return;
|
|
194
|
+
released = true;
|
|
195
|
+
if (selectionActive) {
|
|
196
|
+
selectionActive = false;
|
|
197
|
+
nativeMainSelections = Math.max(0, nativeMainSelections - 1);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
},
|
|
61
202
|
isTransferred() { return transferred; },
|
|
62
203
|
release() {
|
|
63
204
|
if (!active) return;
|
|
@@ -67,12 +208,43 @@ export function tryAdmitTurn(): ActiveTurnLease | null {
|
|
|
67
208
|
if (activeTurns.get(controller) === lease) activeTurns.delete(controller);
|
|
68
209
|
}
|
|
69
210
|
controllers.clear();
|
|
211
|
+
nativeMainTurns.delete(lease);
|
|
70
212
|
gateLease.release();
|
|
71
213
|
},
|
|
72
214
|
};
|
|
73
215
|
admittedTurns.add(lease);
|
|
74
216
|
return lease;
|
|
75
217
|
}
|
|
218
|
+
export function codexAccountSelectionForTurn(
|
|
219
|
+
lease?: AdmissionLease,
|
|
220
|
+
): (() => CodexAccountSelectionAdmission | undefined) | undefined {
|
|
221
|
+
if (!lease || !("beginCodexAccountSelection" in lease)) return undefined;
|
|
222
|
+
const activeLease = lease as ActiveTurnLease;
|
|
223
|
+
return () => activeLease.beginCodexAccountSelection();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Promote a physical native-main credential read onto an already admitted turn. */
|
|
227
|
+
export function tryClaimNativeMainProfileForTurn(lease?: AdmissionLease): boolean {
|
|
228
|
+
const beginSelection = codexAccountSelectionForTurn(lease);
|
|
229
|
+
if (!beginSelection) return false;
|
|
230
|
+
const selection = beginSelection();
|
|
231
|
+
if (!selection) return false;
|
|
232
|
+
try {
|
|
233
|
+
return !selection.mainProfileDraining && selection.claimMainProfile();
|
|
234
|
+
} finally {
|
|
235
|
+
selection.release();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Acquire standalone native-main ownership for management/background work. */
|
|
240
|
+
export function tryAcquireNativeMainProfileClaim(): AdmissionLease | null {
|
|
241
|
+
const turn = tryAdmitTurn();
|
|
242
|
+
if (!turn) return null;
|
|
243
|
+
if (tryClaimNativeMainProfileForTurn(turn)) return turn;
|
|
244
|
+
turn.release();
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
76
248
|
export function registerTurn(ac: AbortController, lease?: AdmissionLease): void {
|
|
77
249
|
if (lease && "bindAbortController" in lease) (lease as ActiveTurnLease).bindAbortController(ac);
|
|
78
250
|
}
|
|
@@ -85,8 +257,11 @@ export function unregisterTurn(ac: AbortController): void {
|
|
|
85
257
|
}
|
|
86
258
|
lease.release();
|
|
87
259
|
}
|
|
88
|
-
export function isDraining(): boolean { return
|
|
260
|
+
export function isDraining(): boolean { return shutdownDraining || temporaryDrainOwners.size > 0; }
|
|
89
261
|
export function getActiveTurnCount(): number { return turnGate.metrics().active; }
|
|
262
|
+
export function getNativeMainProfileRequestCount(): number {
|
|
263
|
+
return nativeMainSelections + nativeMainTurns.size;
|
|
264
|
+
}
|
|
90
265
|
export function activeRegistryMetrics(): Record<string, AdmissionMetrics> {
|
|
91
266
|
const turns = turnGate.metrics();
|
|
92
267
|
return {
|
|
@@ -111,6 +286,44 @@ export function getServerListenPort(): number | undefined {
|
|
|
111
286
|
const port = _serverRef?.port;
|
|
112
287
|
return typeof port === "number" && port > 0 ? port : undefined;
|
|
113
288
|
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Stop one concrete listener exactly once. Deadline restart handoff can race the
|
|
292
|
+
* ordinary drain's finally block; both callers must observe the same stop result
|
|
293
|
+
* before any replacement process is allowed to bind the port.
|
|
294
|
+
*/
|
|
295
|
+
export function stopServerListener(
|
|
296
|
+
server: ReturnType<typeof Bun.serve> | undefined = _serverRef,
|
|
297
|
+
): Promise<void> {
|
|
298
|
+
if (!server) return Promise.resolve();
|
|
299
|
+
const existing = serverStopFlights.get(server);
|
|
300
|
+
if (existing) return existing;
|
|
301
|
+
// Bun's Server.stop returns Promise<void>; fire-and-forget races a
|
|
302
|
+
// follow-on listen and can leave the replacement seeing the old proxy.
|
|
303
|
+
const flight = Promise.resolve().then(() => server.stop(true));
|
|
304
|
+
serverStopFlights.set(server, flight);
|
|
305
|
+
return flight;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Native-main startup ownership cleanup, separate from the listen socket flight. */
|
|
309
|
+
export function releaseServerStartupLifecycle(
|
|
310
|
+
server: ReturnType<typeof Bun.serve> | undefined = _serverRef,
|
|
311
|
+
): Promise<void> {
|
|
312
|
+
if (!server) return Promise.resolve();
|
|
313
|
+
const existing = serverStartupReleaseFlights.get(server);
|
|
314
|
+
if (existing) return existing;
|
|
315
|
+
const flight = Promise.resolve().then(() => releaseServerStartupLifecycleImpl(server));
|
|
316
|
+
serverStartupReleaseFlights.set(server, flight);
|
|
317
|
+
return flight;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Test seam for a held/rejected startup lifecycle release. */
|
|
321
|
+
export function setServerStartupLifecycleReleaseForTests(
|
|
322
|
+
release: typeof releaseNativeMainStartupLifecycle | undefined,
|
|
323
|
+
): void {
|
|
324
|
+
releaseServerStartupLifecycleImpl = release ?? releaseNativeMainStartupLifecycle;
|
|
325
|
+
serverStartupReleaseFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
|
|
326
|
+
}
|
|
114
327
|
/**
|
|
115
328
|
* Mark this process as a recycle (dashboard drain-and-restart). Exit cleanup
|
|
116
329
|
* must keep Codex/Grok/system-env injection so the replacement process inherits
|
|
@@ -158,10 +371,16 @@ export async function drainAndShutdown(
|
|
|
158
371
|
timeoutMs: number,
|
|
159
372
|
): Promise<void> {
|
|
160
373
|
const s = server ?? _serverRef;
|
|
161
|
-
|
|
374
|
+
// One absolute budget covers both a pre-existing scoped profile drain and
|
|
375
|
+
// ordinary in-flight turns. A stuck scoped owner must not pin shutdown forever.
|
|
376
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
377
|
+
beginShutdownDrain();
|
|
378
|
+
const temporaryDrainsSettled = await waitForTemporaryDrainsUntil(deadline);
|
|
379
|
+
if (!temporaryDrainsSettled) {
|
|
380
|
+
console.warn("Temporary drain lease did not settle before the shutdown deadline; forcing shutdown");
|
|
381
|
+
}
|
|
162
382
|
beginBackgroundShellShutdown();
|
|
163
383
|
try {
|
|
164
|
-
const deadline = Date.now() + timeoutMs;
|
|
165
384
|
while (admittedTurns.size > 0 && Date.now() < deadline) {
|
|
166
385
|
await Bun.sleep(100);
|
|
167
386
|
}
|
|
@@ -217,11 +436,11 @@ export async function drainAndShutdown(
|
|
|
217
436
|
setStorageCleanupPolicyJobLiveApply(null);
|
|
218
437
|
} finally {
|
|
219
438
|
try {
|
|
220
|
-
|
|
221
|
-
// isolate reclaim / follow-on listen the same way unterminated Workers did.
|
|
222
|
-
if (s) await s.stop(true);
|
|
439
|
+
await stopServerListener(s);
|
|
223
440
|
} finally {
|
|
224
|
-
|
|
441
|
+
await releaseServerStartupLifecycle(s);
|
|
442
|
+
// shutdownDraining is a process-lifetime latch. A stopped server must
|
|
443
|
+
// never resume admission merely because shutdown cleanup returned.
|
|
225
444
|
}
|
|
226
445
|
}
|
|
227
446
|
}
|
package/src/server/live.ts
CHANGED
|
@@ -24,8 +24,10 @@ import { appendFileSync } from "node:fs";
|
|
|
24
24
|
import { formatErrorResponse } from "../bridge";
|
|
25
25
|
import {
|
|
26
26
|
CodexAccountCooldownError,
|
|
27
|
+
codexMainProfileDrainingResponse,
|
|
27
28
|
cooldownErrorResponse,
|
|
28
29
|
CodexAuthContextError,
|
|
30
|
+
CodexMainProfileDrainingError,
|
|
29
31
|
CodexPoolAuthenticationError,
|
|
30
32
|
CodexThreadAffinityExpiredError,
|
|
31
33
|
} from "../codex/auth-context";
|
|
@@ -37,6 +39,8 @@ import { resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../
|
|
|
37
39
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
38
40
|
import type { RequestLogContext } from "./request-log";
|
|
39
41
|
import { codexLogAccountId } from "./responses";
|
|
42
|
+
import type { AdmissionLease } from "../lib/admission";
|
|
43
|
+
import { codexAccountSelectionForTurn } from "./lifecycle";
|
|
40
44
|
|
|
41
45
|
/** Voice call create can wait on SDP negotiation; bound a hung upstream. */
|
|
42
46
|
const LIVE_UPSTREAM_TIMEOUT_MS = 120_000;
|
|
@@ -409,6 +413,7 @@ export async function resolveLiveRelay(
|
|
|
409
413
|
req: Request,
|
|
410
414
|
config: OcxConfig,
|
|
411
415
|
logCtx: RequestLogContext,
|
|
416
|
+
turnAdmissionLease?: AdmissionLease,
|
|
412
417
|
): Promise<LiveRelayTarget | Response> {
|
|
413
418
|
try {
|
|
414
419
|
validateForwardAdmissionCredential(req.headers, config);
|
|
@@ -433,7 +438,9 @@ export async function resolveLiveRelay(
|
|
|
433
438
|
let forwardAuthError: Response | undefined;
|
|
434
439
|
if (candidates.forwardCandidates.length > 0) {
|
|
435
440
|
try {
|
|
436
|
-
forward = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config
|
|
441
|
+
forward = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config, {
|
|
442
|
+
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
443
|
+
});
|
|
437
444
|
if (forward) {
|
|
438
445
|
logCtx.provider = formatCodexProviderForLog(
|
|
439
446
|
forward.providerName,
|
|
@@ -444,6 +451,8 @@ export async function resolveLiveRelay(
|
|
|
444
451
|
} catch (err) {
|
|
445
452
|
if (err instanceof CodexAccountCooldownError) {
|
|
446
453
|
forwardAuthError = cooldownErrorResponse(err);
|
|
454
|
+
} else if (err instanceof CodexMainProfileDrainingError) {
|
|
455
|
+
forwardAuthError = codexMainProfileDrainingResponse();
|
|
447
456
|
} else if (err instanceof CodexThreadAffinityExpiredError) {
|
|
448
457
|
forwardAuthError = formatErrorResponse(
|
|
449
458
|
409,
|
|
@@ -506,13 +515,14 @@ export async function handleLive(
|
|
|
506
515
|
req: Request,
|
|
507
516
|
config: OcxConfig,
|
|
508
517
|
logCtx: RequestLogContext,
|
|
518
|
+
turnAdmissionLease?: AdmissionLease,
|
|
509
519
|
): Promise<Response> {
|
|
510
520
|
const inboundContentType = req.headers.get("content-type") ?? "application/octet-stream";
|
|
511
521
|
const inboundBodyOrError = await readRequestBodyCapped(req, LIVE_REQUEST_MAX_BYTES);
|
|
512
522
|
if (inboundBodyOrError instanceof Response) return inboundBodyOrError;
|
|
513
523
|
const inboundBody = inboundBodyOrError;
|
|
514
524
|
|
|
515
|
-
const relay = await resolveLiveRelay(req, config, logCtx);
|
|
525
|
+
const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease);
|
|
516
526
|
if (relay instanceof Response) return relay;
|
|
517
527
|
|
|
518
528
|
const headers: Record<string, string> = { ...relay.headers };
|
|
@@ -587,8 +597,9 @@ export async function resolveLiveSidebandUpgrade(
|
|
|
587
597
|
config: OcxConfig,
|
|
588
598
|
logCtx: RequestLogContext,
|
|
589
599
|
target: LiveSidebandTarget,
|
|
600
|
+
turnAdmissionLease?: AdmissionLease,
|
|
590
601
|
): Promise<{ headers: Record<string, string>; upstreamWsUrl: string; recordOutcome?: LiveRelayTarget["recordOutcome"] } | Response> {
|
|
591
|
-
const relay = await resolveLiveRelay(req, config, logCtx);
|
|
602
|
+
const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease);
|
|
592
603
|
if (relay instanceof Response) return relay;
|
|
593
604
|
return {
|
|
594
605
|
headers: relay.headers,
|
|
@@ -125,7 +125,7 @@ export function setGrokApplyFlightTestHooks(
|
|
|
125
125
|
import type { ManagementContext } from "./context";
|
|
126
126
|
|
|
127
127
|
export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
128
|
-
const { req, url, config, deps,
|
|
128
|
+
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
129
129
|
|
|
130
130
|
/** Best-effort Desktop 3P config auto-reconcile when providers change. */
|
|
131
131
|
async function autoApplyDesktopBestEffort(): Promise<void> {
|
|
@@ -229,16 +229,11 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
229
229
|
const warnings: string[] = [];
|
|
230
230
|
const requestedFlag = wantsFlag ? body.enabled as boolean : modeFlag;
|
|
231
231
|
if (requestedFlag !== undefined || wantsThreads) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
toggle = (enabled: boolean) => {
|
|
238
|
-
const inv = codexFeaturesInvocation(enabled ? "enable" : "disable");
|
|
239
|
-
execFileSync(inv.file, inv.args,
|
|
240
|
-
{ stdio: ["ignore", "pipe", "pipe"], timeout: 15_000, windowsHide: true, ...inv.options });
|
|
241
|
-
};
|
|
232
|
+
const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
|
|
233
|
+
let toggle = deps.toggleCodexMultiAgentV2;
|
|
234
|
+
if (!toggle) {
|
|
235
|
+
const { runCodexFeaturesCommand } = await import("../../cli/v2");
|
|
236
|
+
toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable");
|
|
242
237
|
}
|
|
243
238
|
const result = transitionMultiAgentV2(targetFlag, toggle, {
|
|
244
239
|
...(wantsThreads ? { threadLimit: body.maxConcurrentThreadsPerSession as number } : {}),
|
|
@@ -282,7 +277,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
282
277
|
if (getAgentsEnabled() === false && isMultiAgentV2Enabled()) {
|
|
283
278
|
warnings.push("agents.enabled = false has no effect while features.multi_agent_v2 is enabled; upstream keeps V2 active.");
|
|
284
279
|
}
|
|
285
|
-
await
|
|
280
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
286
281
|
if (requestedFlag !== undefined) warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the ladder change.");
|
|
287
282
|
const enabled = isMultiAgentV2Enabled();
|
|
288
283
|
return jsonResponse({
|
|
@@ -296,9 +291,64 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
296
291
|
subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
|
|
297
292
|
agentsMaxDepthAppliesWhenV2Disabled: !enabled,
|
|
298
293
|
warnings,
|
|
294
|
+
catalogRefresh,
|
|
299
295
|
});
|
|
300
296
|
}
|
|
301
297
|
|
|
298
|
+
// default_mode_request_user_input feature toggle (Codex Auth page). GET reads the
|
|
299
|
+
// flag from $CODEX_HOME/config.toml; PUT flips it via the official `codex features`
|
|
300
|
+
// CLI so the TOML edit stays upstream-owned and format-preserving.
|
|
301
|
+
if (url.pathname === "/api/codex-auth/features/default-mode-request-user-input" && req.method === "GET") {
|
|
302
|
+
const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY } = await import("../../codex/features");
|
|
303
|
+
return jsonResponse({
|
|
304
|
+
enabled: isDefaultModeRequestUserInputEnabled(),
|
|
305
|
+
key: DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (url.pathname === "/api/codex-auth/features/default-mode-request-user-input" && req.method === "PUT") {
|
|
309
|
+
let parsedBody: unknown;
|
|
310
|
+
try {
|
|
311
|
+
parsedBody = await readManagementJsonBody(req);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
rethrowManagementBodyTooLarge(error);
|
|
314
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
315
|
+
}
|
|
316
|
+
if (!parsedBody || typeof parsedBody !== "object" || Array.isArray(parsedBody)) {
|
|
317
|
+
return jsonResponse({ error: "body must be a JSON object" }, 400);
|
|
318
|
+
}
|
|
319
|
+
const body = parsedBody as { enabled?: unknown };
|
|
320
|
+
if (typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
|
|
321
|
+
const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY } = await import("../../codex/features");
|
|
322
|
+
const before = isDefaultModeRequestUserInputEnabled();
|
|
323
|
+
let toggle = deps.toggleDefaultModeRequestUserInput;
|
|
324
|
+
if (!toggle) {
|
|
325
|
+
const { runCodexFeaturesCommand } = await import("../../cli/v2");
|
|
326
|
+
toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable", DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY);
|
|
327
|
+
}
|
|
328
|
+
let toggleError: string | null = null;
|
|
329
|
+
try {
|
|
330
|
+
toggle(body.enabled);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const err = error as { stderr?: unknown; message?: string };
|
|
333
|
+
const raw = err.stderr;
|
|
334
|
+
const stderrText = typeof raw === "string"
|
|
335
|
+
? raw.trim()
|
|
336
|
+
: raw instanceof Uint8Array ? new TextDecoder().decode(raw).trim() : "";
|
|
337
|
+
toggleError = stderrText || (err.message ?? String(error));
|
|
338
|
+
}
|
|
339
|
+
const enabled = isDefaultModeRequestUserInputEnabled();
|
|
340
|
+
if (toggleError !== null || enabled !== body.enabled) {
|
|
341
|
+
const reason = toggleError
|
|
342
|
+
?? `postcondition failed - the installed Codex build may not know the ${DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY} flag yet`;
|
|
343
|
+
return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${reason}` }, 502);
|
|
344
|
+
}
|
|
345
|
+
const warnings: string[] = [];
|
|
346
|
+
if (enabled !== before) {
|
|
347
|
+
warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the change.");
|
|
348
|
+
}
|
|
349
|
+
return jsonResponse({ ok: true, enabled, changed: enabled !== before, warnings });
|
|
350
|
+
}
|
|
351
|
+
|
|
302
352
|
// Subagent prompt injection model: single native or routed model whose info is
|
|
303
353
|
// dynamically injected into the v1 proactive prompt, plus an optional reasoning
|
|
304
354
|
// effort the prompt tells the agent to pass to spawn_agent. GET returns the current
|
|
@@ -473,10 +523,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
473
523
|
config.subagentModels = chosen;
|
|
474
524
|
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
475
525
|
save(config);
|
|
476
|
-
await
|
|
526
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
477
527
|
await syncClaudeAgentDefsBestEffort();
|
|
478
528
|
await autoApplyDesktopBestEffort();
|
|
479
|
-
return jsonResponse({ ok: true, applied: chosen });
|
|
529
|
+
return jsonResponse({ ok: true, applied: chosen, catalogRefresh });
|
|
480
530
|
}
|
|
481
531
|
|
|
482
532
|
// Priority-ordered subagent model fallback chain for quota-aware spawn routing.
|
|
@@ -66,7 +66,7 @@ import type { ManagementContext } from "./context";
|
|
|
66
66
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
67
67
|
|
|
68
68
|
export async function handleComboRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
69
|
-
const { req, url, config, deps,
|
|
69
|
+
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
70
70
|
|
|
71
71
|
if (url.pathname === "/api/combos" && req.method === "GET") {
|
|
72
72
|
const { comboPublicModelId, getCombo, listComboIds } = await import("../../combos");
|
|
@@ -195,9 +195,9 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
195
195
|
clearComboSelectionState(renameFrom);
|
|
196
196
|
clearComboTargetCooldowns(renameFrom);
|
|
197
197
|
}
|
|
198
|
-
await
|
|
198
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
199
199
|
if (shouldSyncClaudeAgentDefs) await syncClaudeAgentDefsBestEffort();
|
|
200
|
-
return jsonResponse({ success: true, id, model: newPublicModel, combo: normalized });
|
|
200
|
+
return jsonResponse({ success: true, id, model: newPublicModel, combo: normalized, catalogRefresh });
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
if (url.pathname === "/api/combos" && req.method === "DELETE") {
|
|
@@ -213,8 +213,8 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
213
213
|
reconcileLiveStateStores();
|
|
214
214
|
clearComboSelectionState(id);
|
|
215
215
|
clearComboTargetCooldowns(id);
|
|
216
|
-
await
|
|
217
|
-
return jsonResponse({ success: true, id });
|
|
216
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
217
|
+
return jsonResponse({ success: true, id, catalogRefresh });
|
|
218
218
|
}
|
|
219
219
|
return null;
|
|
220
220
|
}
|
|
@@ -74,7 +74,7 @@ import type { ManagementContext } from "./context";
|
|
|
74
74
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
75
75
|
|
|
76
76
|
export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
77
|
-
const { req, url, config, deps,
|
|
77
|
+
const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
|
|
78
78
|
if (url.pathname === "/api/config" && req.method === "GET") {
|
|
79
79
|
return jsonResponse(safeConfigDTO(config));
|
|
80
80
|
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { OcxConfig } from "../../types";
|
|
2
|
+
import type { NativeProfileApiDeps } from "../../codex/native-profile-api";
|
|
2
3
|
import type { StartupInstallAction } from "../startup-action-control";
|
|
4
|
+
import type { ManagementPrincipal } from "../management-auth";
|
|
5
|
+
import type { CatalogModel } from "../../codex/catalog";
|
|
6
|
+
import type { injectGrokConfig } from "../../grok/inject";
|
|
7
|
+
import type { RuntimePortState } from "../../config";
|
|
8
|
+
import type { CatalogDisposition, ConvergeCodex } from "../../codex/convergence-types";
|
|
3
9
|
|
|
4
10
|
export interface ManagementApiDeps {
|
|
5
11
|
toggleCodexMultiAgentV2?: (enabled: boolean) => void;
|
|
6
|
-
|
|
12
|
+
toggleDefaultModeRequestUserInput?: (enabled: boolean) => void;
|
|
13
|
+
createManagementConvergeCodex?: (config: Readonly<OcxConfig>) => ConvergeCodex;
|
|
7
14
|
/**
|
|
8
15
|
* Persistence seam for route-level tests. Production leaves this unset and uses
|
|
9
16
|
* `saveConfigPreservingClaudeCode`; tests that pass an in-memory fixture config
|
|
@@ -11,6 +18,25 @@ export interface ManagementApiDeps {
|
|
|
11
18
|
* OPENCODEX_HOME (incident: devlog 260730.../070).
|
|
12
19
|
*/
|
|
13
20
|
saveConfigPreservingClaudeCode?: (config: OcxConfig) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Catalog seam for the Grok toggle (WP2, devlog 260803_integrations_toggle_all
|
|
23
|
+
* Rev 3 N2). Production leaves this unset and the route dynamic-imports the
|
|
24
|
+
* real one — a static import would close a cycle with management-api.ts.
|
|
25
|
+
* Tests stub it to orphan the fixture file mid-fetch (the r7 recheck test).
|
|
26
|
+
*/
|
|
27
|
+
fetchAllModels?: (config: OcxConfig) => Promise<CatalogModel[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Writer seam for the Grok toggle: lets a test place the file in any state
|
|
30
|
+
* between the pre-write recheck and the write itself (the r8 post-inspection
|
|
31
|
+
* tests). Production leaves this unset and uses the real writer.
|
|
32
|
+
*/
|
|
33
|
+
injectGrokConfig?: typeof injectGrokConfig;
|
|
34
|
+
/**
|
|
35
|
+
* Runtime-state seam: the fence must name the host/port the RUNNING process
|
|
36
|
+
* bound (agent-settings-routes.ts:99-103 pattern), and a test must not depend
|
|
37
|
+
* on the developer's real runtime state file.
|
|
38
|
+
*/
|
|
39
|
+
readRuntimePort?: (pid: number) => RuntimePortState | null;
|
|
14
40
|
clearThreadAccountMap?: () => void;
|
|
15
41
|
clearProviderQuotaCache?: () => void;
|
|
16
42
|
primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise<void> | void;
|
|
@@ -18,6 +44,11 @@ export interface ManagementApiDeps {
|
|
|
18
44
|
action: StartupInstallAction,
|
|
19
45
|
options?: { repair?: boolean },
|
|
20
46
|
) => Promise<{ message: string }>;
|
|
47
|
+
/**
|
|
48
|
+
* Native-main profile persistence seam for server-boundary tests. Production
|
|
49
|
+
* leaves this unset, so the route creates its normal NativeProfileManager.
|
|
50
|
+
*/
|
|
51
|
+
nativeProfileApi?: NativeProfileApiDeps;
|
|
21
52
|
}
|
|
22
53
|
|
|
23
54
|
|
|
@@ -26,6 +57,15 @@ export interface ManagementContext {
|
|
|
26
57
|
url: URL;
|
|
27
58
|
config: OcxConfig;
|
|
28
59
|
deps: ManagementApiDeps;
|
|
29
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Which credential authorized this request, resolved by the auth gate before
|
|
62
|
+
* dispatch. Routes that spend the USER's identity (not just the proxy's) must
|
|
63
|
+
* branch on this instead of on request headers: the admin token is readable by
|
|
64
|
+
* anything running as the user, so a token holder can forge any header a route
|
|
65
|
+
* might otherwise treat as browser evidence. Undefined only in direct-dispatch
|
|
66
|
+
* tests, which are treated as the untrusted `admin-token` case.
|
|
67
|
+
*/
|
|
68
|
+
principal?: ManagementPrincipal;
|
|
69
|
+
convergeCodexCatalog: () => Promise<CatalogDisposition>;
|
|
30
70
|
syncClaudeAgentDefsBestEffort: () => Promise<void>;
|
|
31
71
|
}
|