@bitkyc08/opencodex 2.11.1 → 2.12.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 +1 -1
- package/bin/ocx.mjs +4 -0
- package/gui/dist/assets/index-BNVYzdn0.css +1 -0
- package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +94 -5
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google.ts +24 -7
- package/src/adapters/openai-chat.ts +233 -305
- package/src/adapters/openai-responses.ts +62 -0
- package/src/adapters/tool-catalog-nudge.ts +2 -11
- package/src/claude/outbound.ts +42 -14
- package/src/cli/claude.ts +61 -9
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +8 -1
- package/src/cli/config-command.ts +4 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +15 -0
- package/src/cli/index.ts +27 -11
- package/src/cli/lab.ts +367 -0
- package/src/cli/provider.ts +9 -1
- package/src/clients/config-export.ts +100 -3
- package/src/codex/app-server-processes.ts +11 -8
- package/src/codex/auth-api.ts +92 -28
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/catalog/metadata.ts +7 -3
- package/src/codex/catalog/provider-fetch.ts +191 -47
- package/src/codex/catalog.ts +2 -2
- package/src/codex/history-job.ts +83 -18
- package/src/codex/history-migration-guardian.ts +9 -16
- package/src/codex/history-provider.ts +178 -1
- package/src/codex/history-transition.ts +5 -5
- package/src/codex/history-worker.ts +22 -2
- package/src/codex/native-profile-store.ts +1 -1
- package/src/codex/plan.ts +15 -0
- package/src/codex/quota.ts +6 -6
- package/src/codex/routing.ts +3 -3
- package/src/codex/subagent-model-fallback.ts +246 -22
- package/src/codex/user-identity.ts +63 -12
- package/src/config.ts +265 -32
- package/src/images/loop.ts +3 -1
- package/src/images/plan.ts +23 -13
- package/src/integrations/omp-yaml-source.ts +225 -0
- package/src/integrations/ownership.ts +5 -5
- package/src/integrations/state.ts +45 -6
- package/src/integrations/writer.ts +38 -3
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +24 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +160 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +57 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +191 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +228 -0
- package/src/lab/events/validate.ts +780 -0
- package/src/lab/index.ts +36 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +241 -0
- package/src/lab/ledger/store.ts +263 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +311 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +116 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +344 -0
- package/src/lab/observe/from-live.ts +103 -0
- package/src/lab/paths.ts +65 -0
- package/src/lab/projection/rebuild.ts +477 -0
- package/src/lab/projection/schema.ts +122 -0
- package/src/lab/projection/verdicts.ts +438 -0
- package/src/lab/projection/verification.ts +332 -0
- package/src/lab/query/catalog.ts +72 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/index.ts +34 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +81 -0
- package/src/lab/subject/route-subject.ts +73 -0
- package/src/lib/bounded-body.ts +104 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +45 -0
- package/src/lib/pinned-http.ts +36 -7
- package/src/oauth/index.ts +20 -1
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/login-cli.ts +38 -4
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/codex-capacity.ts +7 -4
- package/src/providers/command-code-efforts.ts +5 -2
- package/src/providers/derive.ts +105 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/openai-tiers.ts +59 -3
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/quota.ts +67 -21
- package/src/providers/registry.ts +118 -5
- package/src/router.ts +15 -1
- package/src/routing/analytics.ts +8 -7
- package/src/routing/evaluator.ts +51 -5
- package/src/routing/profile.ts +10 -1
- package/src/routing/trace.ts +42 -6
- package/src/server/auth-cors.ts +15 -0
- package/src/server/background-lifecycle.ts +169 -0
- package/src/server/chat-completions.ts +18 -3
- package/src/server/claude-messages.ts +1 -0
- package/src/server/images.ts +78 -11
- package/src/server/index.ts +72 -18
- package/src/server/lifecycle.ts +6 -0
- package/src/server/live.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +41 -24
- package/src/server/management/body.ts +6 -0
- package/src/server/management/config-routes.ts +51 -8
- package/src/server/management/lab-routes.ts +388 -0
- package/src/server/management/logs-usage-routes.ts +19 -1
- package/src/server/management/model-rows.ts +25 -11
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +5 -0
- package/src/server/management/shared.ts +8 -1
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management/vision-sidecar-options.ts +125 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/request-decompress.ts +106 -5
- package/src/server/responses/collaboration.ts +4 -1
- package/src/server/responses/core.ts +117 -31
- package/src/server/responses-json-events.ts +50 -12
- package/src/server/search.ts +23 -7
- package/src/server/ws-bridge.ts +3 -1
- package/src/service.ts +40 -20
- package/src/types.ts +74 -0
- package/src/update/job.ts +10 -2
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +9 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/describe.ts +2 -2
- package/src/vision/eligibility.ts +229 -0
- package/src/vision/index.ts +20 -2
- package/src/web-search/executor.ts +2 -2
- package/src/web-search/index.ts +4 -2
- package/src/web-search/loop.ts +3 -1
- package/src/web-search/parse.ts +72 -3
- package/gui/dist/assets/index-BA1dgo4q.css +0 -1
- package/gui/dist/assets/index-DDZpgzKk.js +0 -70
package/src/server/index.ts
CHANGED
|
@@ -25,12 +25,11 @@ import { getCodexHome } from "../codex/paths";
|
|
|
25
25
|
import { shouldSyncCodexOnStart } from "../codex/desired-state";
|
|
26
26
|
import { inspectNativeCodexOwnership } from "../integrations/native/ownership-preflight";
|
|
27
27
|
import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
|
|
28
|
-
import { startMemoryWatchdog } from "./memory-watchdog";
|
|
29
28
|
import {
|
|
30
29
|
reconcileLiveStateStores,
|
|
31
30
|
setLiveStateStoreConfig,
|
|
32
31
|
} from "../lib/state-store-registrations";
|
|
33
|
-
import {
|
|
32
|
+
import { startUserCostOverlayReconciler } from "../usage/user-cost-overlay-reconciler";
|
|
34
33
|
import {
|
|
35
34
|
configureAppOwnedMemoryBudget,
|
|
36
35
|
enforceAppOwnedMemoryBudget,
|
|
@@ -41,9 +40,7 @@ import {
|
|
|
41
40
|
registerDefaultAppOwnedMemoryStores,
|
|
42
41
|
registerDefaultAppOwnedObservedBuffers,
|
|
43
42
|
} from "../lib/app-owned-memory-stores";
|
|
44
|
-
import {
|
|
45
|
-
import { setStorageCleanupPolicyJobLiveApply } from "../storage/policy-job";
|
|
46
|
-
import { scheduleStorageCleanupStartupRun, startStorageCleanupScheduler } from "../storage/policy-scheduler";
|
|
43
|
+
import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
|
|
47
44
|
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
48
45
|
import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
|
|
49
46
|
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
@@ -189,11 +186,42 @@ import {
|
|
|
189
186
|
import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
|
|
190
187
|
import { createReadinessGate, type ReadinessGate } from "./readiness";
|
|
191
188
|
|
|
192
|
-
const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
189
|
+
export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
193
190
|
const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
194
191
|
const LIVE_SIDEBAND_PENDING_MAX = 32;
|
|
192
|
+
const LIVE_SIDEBAND_PENDING_BYTES_MAX = 1024 * 1024;
|
|
195
193
|
const LIVE_SIDEBAND_CLOSE_FALLBACK_MS = 1_000;
|
|
196
194
|
|
|
195
|
+
export function exceedsLiveSidebandFrameByteLimit(frameBytes: number): boolean {
|
|
196
|
+
return frameBytes > MAX_WS_FRAME_BYTES;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function exceedsLiveSidebandPendingByteLimit(pendingBytes: number, incomingBytes: number): boolean {
|
|
200
|
+
return incomingBytes > LIVE_SIDEBAND_PENDING_BYTES_MAX - pendingBytes;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function webSocketFrameBytes(frame: string | ArrayBuffer | ArrayBufferView | Blob | Buffer): number {
|
|
204
|
+
if (typeof frame === "string") return Buffer.byteLength(frame);
|
|
205
|
+
if (frame instanceof ArrayBuffer || ArrayBuffer.isView(frame)) return frame.byteLength;
|
|
206
|
+
return frame.size;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export type LiveSidebandPendingEnqueueResult = "queued" | "too-many-frames" | "too-many-bytes";
|
|
210
|
+
|
|
211
|
+
export function enqueueLiveSidebandPendingFrame(
|
|
212
|
+
data: Pick<WsData, "livePending" | "livePendingBytes">,
|
|
213
|
+
frame: string | Buffer,
|
|
214
|
+
frameBytes = webSocketFrameBytes(frame),
|
|
215
|
+
): LiveSidebandPendingEnqueueResult {
|
|
216
|
+
const pending = data.livePending ?? (data.livePending = []);
|
|
217
|
+
if (pending.length >= LIVE_SIDEBAND_PENDING_MAX) return "too-many-frames";
|
|
218
|
+
const pendingBytes = data.livePendingBytes ?? 0;
|
|
219
|
+
if (exceedsLiveSidebandPendingByteLimit(pendingBytes, frameBytes)) return "too-many-bytes";
|
|
220
|
+
pending.push(frame);
|
|
221
|
+
data.livePendingBytes = pendingBytes + frameBytes;
|
|
222
|
+
return "queued";
|
|
223
|
+
}
|
|
224
|
+
|
|
197
225
|
type LiveSidebandWebSocketFactory = (
|
|
198
226
|
url: string,
|
|
199
227
|
headers: Record<string, string>,
|
|
@@ -228,6 +256,7 @@ function finalizeLiveSideband(ws: ServerWebSocket<WsData>, upstream?: WebSocket)
|
|
|
228
256
|
}
|
|
229
257
|
ws.data.liveUpstream = undefined;
|
|
230
258
|
ws.data.livePending = undefined;
|
|
259
|
+
ws.data.livePendingBytes = undefined;
|
|
231
260
|
ws.data.cancel = undefined;
|
|
232
261
|
releaseLiveSidebandAdmission(ws);
|
|
233
262
|
}
|
|
@@ -263,6 +292,7 @@ function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""
|
|
|
263
292
|
if (ws.data.liveClosing) return;
|
|
264
293
|
ws.data.liveClosing = true;
|
|
265
294
|
ws.data.livePending = undefined;
|
|
295
|
+
ws.data.livePendingBytes = undefined;
|
|
266
296
|
ws.data.cancel = undefined;
|
|
267
297
|
const upstream = ws.data.liveUpstream;
|
|
268
298
|
// Bun's `WebSocket` type narrows `readyState` to 0|1|2 even though the DOM
|
|
@@ -318,6 +348,7 @@ function attachLiveSidebandUpstream(
|
|
|
318
348
|
ws.data.liveOpened = true;
|
|
319
349
|
const pending = ws.data.livePending ?? [];
|
|
320
350
|
ws.data.livePending = undefined;
|
|
351
|
+
ws.data.livePendingBytes = undefined;
|
|
321
352
|
for (const frame of pending) {
|
|
322
353
|
try {
|
|
323
354
|
sendUpstreamFrame(upstream, frame);
|
|
@@ -330,6 +361,10 @@ function attachLiveSidebandUpstream(
|
|
|
330
361
|
upstream.addEventListener("message", (event) => {
|
|
331
362
|
if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
|
|
332
363
|
try {
|
|
364
|
+
if (exceedsLiveSidebandFrameByteLimit(webSocketFrameBytes(event.data))) {
|
|
365
|
+
closeLiveSideband(ws, 1009, "message too large");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
333
368
|
logLiveSidebandFrame("u2c", event.data);
|
|
334
369
|
if (typeof event.data === "string") ws.send(event.data);
|
|
335
370
|
else if (event.data instanceof ArrayBuffer) ws.send(event.data);
|
|
@@ -426,6 +461,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
426
461
|
applyProxyEnv(config);
|
|
427
462
|
assertServerAuthConfig(config);
|
|
428
463
|
const managementAuth = deps.managementAuthState ?? initializeManagementAuthState(config);
|
|
464
|
+
let userCostOverlayReconciler: { stop(): void } | null = null;
|
|
429
465
|
// Arm synchronously before listen. A pending journal therefore makes __main__ unusable
|
|
430
466
|
// before any request can resolve its physical credential, while health/management/Pool stay live.
|
|
431
467
|
// Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
|
|
@@ -486,16 +522,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
486
522
|
// usage.jsonl already persists every request; rehydrate the in-memory Logs ring so
|
|
487
523
|
// /api/logs (and the GUI) survive `ocx stop` / `ocx start` process restarts.
|
|
488
524
|
hydrateRequestLogsFromDisk();
|
|
489
|
-
// #314: warn-only RSS observability (unref'd, idempotent — safe under repeated
|
|
490
|
-
// startServer(0) in tests). Snapshot surfaces via GET /api/system/memory.
|
|
491
|
-
startMemoryWatchdog();
|
|
492
525
|
registerDefaultAppOwnedMemoryStores();
|
|
493
526
|
registerDefaultAppOwnedObservedBuffers();
|
|
494
527
|
registerAppOwnedMemorySweepFallback();
|
|
495
528
|
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
|
|
496
529
|
enforceAppOwnedMemoryBudget();
|
|
497
530
|
registerCodexCooldownRecoveryProbeWorker(config);
|
|
498
|
-
startStateStoreSweeper();
|
|
499
531
|
// Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
|
|
500
532
|
// tick for daily/weekly; startup evaluation is fire-and-forget after listen.
|
|
501
533
|
// Heavy work runs in a Worker via the single-flight job controller.
|
|
@@ -503,9 +535,6 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
503
535
|
const applyPolicy = (policy: StorageCleanupPolicy) => {
|
|
504
536
|
config.storageCleanupPolicy = policy;
|
|
505
537
|
};
|
|
506
|
-
setStorageCleanupPolicyLiveSink(applyPolicy);
|
|
507
|
-
setStorageCleanupPolicyJobLiveApply(applyPolicy);
|
|
508
|
-
startStorageCleanupScheduler();
|
|
509
538
|
|
|
510
539
|
const listenPort = port ?? config.port ?? 10100;
|
|
511
540
|
setCorsOrigin(listenPort);
|
|
@@ -632,7 +661,14 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
632
661
|
};
|
|
633
662
|
let server: Server<WsData>;
|
|
634
663
|
let loopbackServer: Server<WsData> | null = null;
|
|
664
|
+
let backgroundLifecycle: ReturnType<typeof acquireServerBackgroundLifecycle> | null = null;
|
|
635
665
|
try {
|
|
666
|
+
backgroundLifecycle = acquireServerBackgroundLifecycle(applyPolicy);
|
|
667
|
+
// External `ocx config set` / direct config.json edits run in other
|
|
668
|
+
// processes; poll the file so Logs/Usage display prices follow them live.
|
|
669
|
+
// Started inside the guarded startup transaction so the catch below can
|
|
670
|
+
// release the owner-scoped lease on any listener failure.
|
|
671
|
+
userCostOverlayReconciler = startUserCostOverlayReconciler({ liveConfig: config });
|
|
636
672
|
const serveOptions = {
|
|
637
673
|
idleTimeout: 255,
|
|
638
674
|
async fetch(req: Request, requestServer: Server<WsData>): Promise<Response> {
|
|
@@ -1264,6 +1300,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1264
1300
|
liveUpstreamUrl: resolved.upstreamWsUrl,
|
|
1265
1301
|
liveUpstreamHeaders: resolved.headers,
|
|
1266
1302
|
livePending: [],
|
|
1303
|
+
livePendingBytes: 0,
|
|
1267
1304
|
liveOpened: false,
|
|
1268
1305
|
liveTurnAdmissionLease: turnAdmissionLease,
|
|
1269
1306
|
} satisfies WsData,
|
|
@@ -1297,6 +1334,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1297
1334
|
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
1298
1335
|
},
|
|
1299
1336
|
websocket: {
|
|
1337
|
+
maxPayloadLength: MAX_WS_FRAME_BYTES,
|
|
1300
1338
|
idleTimeout: WEBSOCKET_IDLE_TIMEOUT_SECONDS,
|
|
1301
1339
|
// Responses WebSocket data plane (phase 120.2). Re-frames the same SSE pipeline onto the
|
|
1302
1340
|
// socket: parse response.create → run handleResponses unchanged → pump its SSE body as WS
|
|
@@ -1321,15 +1359,23 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1321
1359
|
message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
|
|
1322
1360
|
if (ws.data.kind === "live-sideband") {
|
|
1323
1361
|
if (ws.data.liveClosing) return;
|
|
1362
|
+
const rawBytes = webSocketFrameBytes(raw);
|
|
1363
|
+
if (exceedsLiveSidebandFrameByteLimit(rawBytes)) {
|
|
1364
|
+
closeLiveSideband(ws, 1009, "message too large");
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1324
1367
|
logLiveSidebandFrame("c2u", raw);
|
|
1325
1368
|
const upstream = ws.data.liveUpstream;
|
|
1326
1369
|
if (!upstream || upstream.readyState === WebSocket.CONNECTING || !ws.data.liveOpened) {
|
|
1327
|
-
const
|
|
1328
|
-
if (
|
|
1370
|
+
const enqueueResult = enqueueLiveSidebandPendingFrame(ws.data, raw, rawBytes);
|
|
1371
|
+
if (enqueueResult === "too-many-frames") {
|
|
1329
1372
|
closeLiveSideband(ws, 1009, "too many pending frames");
|
|
1330
1373
|
return;
|
|
1331
1374
|
}
|
|
1332
|
-
|
|
1375
|
+
if (enqueueResult === "too-many-bytes") {
|
|
1376
|
+
closeLiveSideband(ws, 1009, "too many pending bytes");
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1333
1379
|
return;
|
|
1334
1380
|
}
|
|
1335
1381
|
if (upstream.readyState !== WebSocket.OPEN) {
|
|
@@ -1527,6 +1573,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1527
1573
|
}
|
|
1528
1574
|
}
|
|
1529
1575
|
} catch (error) {
|
|
1576
|
+
userCostOverlayReconciler?.stop();
|
|
1577
|
+
backgroundLifecycle?.releaseAfterFailedStart();
|
|
1530
1578
|
void nativeMainLifecycle.release();
|
|
1531
1579
|
throw error;
|
|
1532
1580
|
}
|
|
@@ -1545,8 +1593,14 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1545
1593
|
...(loopbackListenerRef
|
|
1546
1594
|
? [() => loopbackListenerRef.stop(closeActiveConnections)]
|
|
1547
1595
|
: []),
|
|
1596
|
+
async () => {
|
|
1597
|
+
userCostOverlayReconciler?.stop();
|
|
1598
|
+
},
|
|
1548
1599
|
],
|
|
1549
|
-
() =>
|
|
1600
|
+
async () => {
|
|
1601
|
+
await backgroundLifecycle.release();
|
|
1602
|
+
await releaseNativeMainStartupLifecycle(server);
|
|
1603
|
+
},
|
|
1550
1604
|
);
|
|
1551
1605
|
},
|
|
1552
1606
|
});
|
|
@@ -1590,7 +1644,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1590
1644
|
}
|
|
1591
1645
|
|
|
1592
1646
|
// Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
|
|
1593
|
-
|
|
1647
|
+
backgroundLifecycle.scheduleStartupRun();
|
|
1594
1648
|
|
|
1595
1649
|
return server;
|
|
1596
1650
|
}
|
package/src/server/lifecycle.ts
CHANGED
|
@@ -447,6 +447,12 @@ export async function drainAndShutdown(
|
|
|
447
447
|
// then drain leftovers; failures must not prevent `server.stop`.
|
|
448
448
|
stopStorageCleanupScheduler();
|
|
449
449
|
stopStateStoreSweeper();
|
|
450
|
+
// The overlay reconciler is owner-scoped: the startServer stop override
|
|
451
|
+
// releases THIS server's lease through runListenerShutdown →
|
|
452
|
+
// userCostOverlayReconciler.stop(), which also recomputes disk-only
|
|
453
|
+
// preservation for any remaining owners. A process-wide stop here would
|
|
454
|
+
// kill reconciliation for every other server in the process, so drain
|
|
455
|
+
// must not call stopUserCostOverlayReconciler().
|
|
450
456
|
cancelQueuedStorageWorkerSpawns();
|
|
451
457
|
const shutdownJoins = await Promise.allSettled([
|
|
452
458
|
abortStorageCleanupPolicyJobAsync(),
|
package/src/server/live.ts
CHANGED
|
@@ -207,8 +207,12 @@ export function parseLiveSidebandTarget(pathname: string, searchParams: URLSearc
|
|
|
207
207
|
*/
|
|
208
208
|
function isLoopbackHost(hostname: string): boolean {
|
|
209
209
|
const lower = hostname.toLowerCase();
|
|
210
|
+
const ipv4 = lower.split(".");
|
|
211
|
+
const ipv4Loopback = ipv4.length === 4
|
|
212
|
+
&& ipv4[0] === "127"
|
|
213
|
+
&& ipv4.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
210
214
|
return lower === "localhost" || lower.endsWith(".localhost")
|
|
211
|
-
||
|
|
215
|
+
|| ipv4Loopback
|
|
212
216
|
|| lower === "::1" || lower === "[::1]";
|
|
213
217
|
}
|
|
214
218
|
|
|
@@ -51,6 +51,11 @@ import {
|
|
|
51
51
|
type DebugFlag,
|
|
52
52
|
} from "../../lib/debug-settings";
|
|
53
53
|
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
54
|
+
import {
|
|
55
|
+
visionCandidateRows,
|
|
56
|
+
visionDescriberIsProvablyBlind,
|
|
57
|
+
visionDescriberRejection,
|
|
58
|
+
} from "./vision-sidecar-options";
|
|
54
59
|
import { drainAndShutdown } from "../lifecycle";
|
|
55
60
|
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
56
61
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
@@ -60,7 +65,7 @@ import { applySystemEnvToggle } from "../system-env";
|
|
|
60
65
|
|
|
61
66
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels, fetchGrokCandidateModels, buildClaudeDesktopState } from "./shared";
|
|
62
67
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
63
|
-
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
68
|
+
import { readManagementJsonBody, readOptionalManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
64
69
|
|
|
65
70
|
const GROK_APPLY_JOIN_MS = 120_000;
|
|
66
71
|
export const GROK_APPLY_TERMINAL_MS = 10 * 60_000;
|
|
@@ -759,32 +764,22 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
759
764
|
}
|
|
760
765
|
if (url.pathname === "/api/claude-desktop/apply" && req.method === "POST") {
|
|
761
766
|
try {
|
|
762
|
-
const { setIntegrationEnabled, claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
|
|
763
|
-
const desired = setIntegrationEnabled("claude-desktop", true);
|
|
764
|
-
if (!desired.ok) return jsonResponse({ error: desired.message }, desired.retryable ? 409 : 500);
|
|
765
|
-
// Disk now says ON; the reused server snapshot must agree, or the native
|
|
766
|
-
// GET reports OFF and a later whole-snapshot save undoes this transition.
|
|
767
|
-
mirrorDesiredEnabledOntoSnapshot(config, "claude-desktop", true);
|
|
768
|
-
// Disk now says ON; the reused server snapshot must agree, or the native
|
|
769
|
-
// GET reports OFF and a later whole-snapshot save undoes this transition.
|
|
770
767
|
// #859: the CLI delegates here so the registry is built in the serving
|
|
771
768
|
// process. Accept an optional mode; default stays static for back-compat.
|
|
772
769
|
let mode: "static" | "hybrid" | "discovery" = "static";
|
|
773
|
-
const rawBody = await req.text();
|
|
774
770
|
let parsed: unknown;
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}
|
|
771
|
+
try {
|
|
772
|
+
parsed = await readOptionalManagementJsonBody(req);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
rethrowManagementBodyTooLarge(error);
|
|
775
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
776
|
+
}
|
|
777
|
+
const requested = (parsed as { mode?: unknown } | null)?.mode;
|
|
778
|
+
if (requested !== undefined) {
|
|
779
|
+
if (requested === "static" || requested === "hybrid" || requested === "discovery") {
|
|
780
|
+
mode = requested;
|
|
781
|
+
} else {
|
|
782
|
+
return jsonResponse({ error: "mode must be static, hybrid, or discovery" }, 400);
|
|
788
783
|
}
|
|
789
784
|
}
|
|
790
785
|
// #859: a delegated CLI apply carries the profile it just saved — the
|
|
@@ -800,6 +795,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
800
795
|
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
801
796
|
}
|
|
802
797
|
}
|
|
798
|
+
const { setIntegrationEnabled, claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
|
|
799
|
+
const desired = setIntegrationEnabled("claude-desktop", true);
|
|
800
|
+
if (!desired.ok) return jsonResponse({ error: desired.message }, desired.retryable ? 409 : 500);
|
|
801
|
+
// Disk now says ON; the reused server snapshot must agree, or the native
|
|
802
|
+
// GET reports OFF and a later whole-snapshot save undoes this transition.
|
|
803
|
+
mirrorDesiredEnabledOntoSnapshot(config, "claude-desktop", true);
|
|
803
804
|
const state = await buildClaudeDesktopState(config, profileOverride);
|
|
804
805
|
// `setIntegrationEnabled` above wrote desired ON to DISK; it does not touch
|
|
805
806
|
// this long-lived server snapshot. Saving the snapshot wholesale would carry
|
|
@@ -866,6 +867,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
866
867
|
}
|
|
867
868
|
return jsonResponse({ ok: true, saved: true, applied: true, path: result.path, fingerprint: result.fingerprint });
|
|
868
869
|
} catch (error) {
|
|
870
|
+
rethrowManagementBodyTooLarge(error);
|
|
869
871
|
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
870
872
|
}
|
|
871
873
|
}
|
|
@@ -1010,6 +1012,19 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1010
1012
|
if (section.model !== undefined && typeof section.model !== "string") {
|
|
1011
1013
|
return jsonResponse({ error: `${field}.model must be a string` }, 400);
|
|
1012
1014
|
}
|
|
1015
|
+
// Vision override only: reject a model we can prove is blind. Unknown ids stay
|
|
1016
|
+
// allowed; webSearchSidecar has no vision requirement and is left alone. Shares
|
|
1017
|
+
// one policy module with /api/sidecar-settings so the two gates cannot drift.
|
|
1018
|
+
if (field === "visionSidecar" && typeof section.model === "string" && section.model !== "") {
|
|
1019
|
+
const requested = section.model;
|
|
1020
|
+
const candidates = await visionCandidateRows(config);
|
|
1021
|
+
const hint = section.backend === "anthropic" || section.backend === "openai"
|
|
1022
|
+
? section.backend
|
|
1023
|
+
: config.claudeCode?.visionSidecar?.backend;
|
|
1024
|
+
if (visionDescriberIsProvablyBlind(config, requested, candidates, hint)) {
|
|
1025
|
+
return jsonResponse(visionDescriberRejection("visionSidecar.model", requested, config, candidates), 400);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1013
1028
|
}
|
|
1014
1029
|
const next = { ...(config.claudeCode ?? {}) };
|
|
1015
1030
|
for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
|
|
@@ -1118,11 +1133,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1118
1133
|
else delete next.tierModels;
|
|
1119
1134
|
}
|
|
1120
1135
|
}
|
|
1136
|
+
let nextFastMode = config.fastMode;
|
|
1121
1137
|
if (body.fastMode !== undefined) {
|
|
1122
1138
|
if (body.fastMode !== true && body.fastMode !== false && body.fastMode !== null) {
|
|
1123
1139
|
return jsonResponse({ error: "fastMode must be true, false, or null" }, 400);
|
|
1124
1140
|
}
|
|
1125
|
-
|
|
1141
|
+
nextFastMode = body.fastMode === null ? undefined : body.fastMode;
|
|
1126
1142
|
}
|
|
1127
1143
|
for (const field of ["model", "smallFastModel"] as const) {
|
|
1128
1144
|
const value = body[field];
|
|
@@ -1149,6 +1165,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1149
1165
|
else delete next.modelMap;
|
|
1150
1166
|
}
|
|
1151
1167
|
}
|
|
1168
|
+
if (body.fastMode !== undefined) config.fastMode = nextFastMode;
|
|
1152
1169
|
config.claudeCode = next;
|
|
1153
1170
|
// Stamp the migration sentinel on EVERY persist of this block. The migration reads
|
|
1154
1171
|
// "a claudeCode block with no authMode" as a pre-upgrade subscriber and pins it to
|
|
@@ -11,6 +11,12 @@ export function readManagementJsonBody<T = unknown>(req: Request): Promise<T> {
|
|
|
11
11
|
return readBoundedJsonRequestBody(req, MANAGEMENT_JSON_BODY_MAX_BYTES) as Promise<T>;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export function readOptionalManagementJsonBody<T = unknown>(req: Request): Promise<T> {
|
|
15
|
+
return readBoundedJsonRequestBody(req, MANAGEMENT_JSON_BODY_MAX_BYTES, undefined, {
|
|
16
|
+
emptyBodyFallback: {},
|
|
17
|
+
}) as Promise<T>;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
export function managementBodyTooLargeResponse(
|
|
15
21
|
error: unknown,
|
|
16
22
|
req: Request,
|
|
@@ -54,6 +54,13 @@ import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
|
54
54
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
55
55
|
import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../../reasoning-effort";
|
|
56
56
|
import { normalizeVisionReasoningForModel } from "../../vision/reasoning";
|
|
57
|
+
import { findAnthropicVisionProvider, resolveEffectiveVisionModel, resolveVisionBackend } from "../../vision";
|
|
58
|
+
import {
|
|
59
|
+
visionCandidateRows,
|
|
60
|
+
visionDescriberIsProvablyBlind,
|
|
61
|
+
visionDescriberRejection,
|
|
62
|
+
visionModelOptionsFor,
|
|
63
|
+
} from "./vision-sidecar-options";
|
|
57
64
|
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
|
|
58
65
|
import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
|
|
59
66
|
import {
|
|
@@ -80,6 +87,27 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
|
|
|
80
87
|
import type { ManagementContext } from "./context";
|
|
81
88
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
82
89
|
|
|
90
|
+
async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
|
|
91
|
+
model: string;
|
|
92
|
+
reasoning: string;
|
|
93
|
+
models: Awaited<ReturnType<typeof visionModelOptionsFor>>;
|
|
94
|
+
}> {
|
|
95
|
+
const vs = config.visionSidecar ?? {};
|
|
96
|
+
// Match the runtime's one selected Anthropic executor for both backend fallback
|
|
97
|
+
// and catalog reachability; resolving it once prevents the two projections drifting.
|
|
98
|
+
const anthropicSidecar = findAnthropicVisionProvider(config);
|
|
99
|
+
const backend = resolveVisionBackend(vs.backend, anthropicSidecar);
|
|
100
|
+
const model = resolveEffectiveVisionModel(config, backend);
|
|
101
|
+
const reasoning = normalizeVisionReasoningForModel(model, vs.reasoning) ?? "low";
|
|
102
|
+
const models = await visionModelOptionsFor(config, anthropicSidecar);
|
|
103
|
+
// Display-only grandfather: a persisted id stays selectable, but the write gate
|
|
104
|
+
// remains stricter and rejects a model that is positively proven blind.
|
|
105
|
+
if (!models.some(option => option.value === model)) {
|
|
106
|
+
models.unshift({ value: model, label: model, backend });
|
|
107
|
+
}
|
|
108
|
+
return { model, reasoning, models };
|
|
109
|
+
}
|
|
110
|
+
|
|
83
111
|
export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
84
112
|
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
85
113
|
if (url.pathname === "/api/config" && req.method === "GET") {
|
|
@@ -380,16 +408,16 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
380
408
|
if (url.pathname === "/api/sidecar-settings" && req.method === "GET") {
|
|
381
409
|
const ws = config.webSearchSidecar ?? {};
|
|
382
410
|
const vs = config.visionSidecar ?? {};
|
|
383
|
-
const
|
|
384
|
-
const visionReasoning = normalizeVisionReasoningForModel(visionModel, vs.reasoning) ?? "low";
|
|
411
|
+
const vision = await sidecarVisionResponseSettings(config);
|
|
385
412
|
return jsonResponse({
|
|
386
413
|
webSearch: { model: ws.model ?? "gpt-5.6-luna", backend: ws.backend },
|
|
387
414
|
vision: {
|
|
388
|
-
model:
|
|
415
|
+
model: vision.model,
|
|
389
416
|
backend: vs.backend,
|
|
390
|
-
reasoning:
|
|
417
|
+
reasoning: vision.reasoning,
|
|
391
418
|
maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
|
|
392
419
|
},
|
|
420
|
+
visionModels: vision.models,
|
|
393
421
|
});
|
|
394
422
|
}
|
|
395
423
|
|
|
@@ -422,6 +450,21 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
422
450
|
if (body.vision?.reasoning !== undefined && !isVisionReasoningEffort(body.vision.reasoning)) {
|
|
423
451
|
return jsonResponse({ error: `vision.reasoning must be ${VISION_REASONING_EFFORTS.join(", ")}` }, 400);
|
|
424
452
|
}
|
|
453
|
+
// Reject ONLY a model we can prove is blind. An id nothing knows about stays
|
|
454
|
+
// allowed: the operator may be ahead of our catalog, and the runtime never
|
|
455
|
+
// required catalog membership (`tests/vision-reasoning-contract.test.ts`
|
|
456
|
+
// pins `custom-vision` → 200). The catalog is read ONCE and reused for the
|
|
457
|
+
// rejection body, so a 400 cannot cost two provider fetches.
|
|
458
|
+
if (body.vision && typeof body.vision.model === "string" && body.vision.model !== "") {
|
|
459
|
+
const requested = body.vision.model;
|
|
460
|
+
const candidates = await visionCandidateRows(config);
|
|
461
|
+
const hint = body.vision.backend === "anthropic" || body.vision.backend === "openai"
|
|
462
|
+
? body.vision.backend
|
|
463
|
+
: config.visionSidecar?.backend;
|
|
464
|
+
if (visionDescriberIsProvablyBlind(config, requested, candidates, hint)) {
|
|
465
|
+
return jsonResponse(visionDescriberRejection("vision.model", requested, config, candidates), 400);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
425
468
|
|
|
426
469
|
let normalizedVisionReasoning: ReturnType<typeof normalizeVisionReasoningForModel>;
|
|
427
470
|
let visionReasoningTouched = false;
|
|
@@ -469,17 +512,17 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
469
512
|
saveConfigPreservingClaudeCode(config);
|
|
470
513
|
const ws = config.webSearchSidecar ?? {};
|
|
471
514
|
const vs = config.visionSidecar ?? {};
|
|
472
|
-
const
|
|
473
|
-
const visionReasoning = normalizeVisionReasoningForModel(visionModel, vs.reasoning) ?? "low";
|
|
515
|
+
const vision = await sidecarVisionResponseSettings(config);
|
|
474
516
|
return jsonResponse({
|
|
475
517
|
ok: true,
|
|
476
518
|
webSearch: { model: ws.model ?? "gpt-5.6-luna", backend: ws.backend },
|
|
477
519
|
vision: {
|
|
478
|
-
model:
|
|
520
|
+
model: vision.model,
|
|
479
521
|
backend: vs.backend,
|
|
480
|
-
reasoning:
|
|
522
|
+
reasoning: vision.reasoning,
|
|
481
523
|
maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
|
|
482
524
|
},
|
|
525
|
+
visionModels: vision.models,
|
|
483
526
|
});
|
|
484
527
|
}
|
|
485
528
|
|