@bitkyc08/opencodex 2.58.0 → 2.60.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 +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -3,14 +3,19 @@ import { ClientPathError } from "../../clients/config-export";
|
|
|
3
3
|
import { IntegrationMutationBusyError } from "../../integrations/mutation-flight";
|
|
4
4
|
import { IntegrationWriterLockBusyError } from "../../integrations/writer-lock";
|
|
5
5
|
import {
|
|
6
|
-
getAsideProfileState, listAsideProfileStates, mutateAsideProfiles, refreshAsideProfiles,
|
|
7
|
-
type AsideProfilesInput,
|
|
6
|
+
getAsideProfileState, listAsideProfileStates, mutateAsideProfiles, previewAsideProfile, refreshAsideProfiles,
|
|
7
|
+
type AsideProfilesInput, type AsideProfileWriteOutcome,
|
|
8
8
|
} from "../../integrations/aside-profiles";
|
|
9
9
|
import {
|
|
10
10
|
listAsideOperations, findAsideOperation, restoreAsideProfile, deleteAsideOperation,
|
|
11
11
|
asideOperationMatchesCurrent,
|
|
12
12
|
} from "../../integrations/aside-profile-journal";
|
|
13
|
+
import type { AsideOperation } from "../../integrations/aside-profile-journal";
|
|
13
14
|
import type { WriteRefused } from "../../integrations/writer";
|
|
15
|
+
import type { IntegrationWriteInput } from "../../integrations/writer";
|
|
16
|
+
import type { IntegrationMutationPlan, IntegrationPlanOperation } from "../../integrations/mutation-plan";
|
|
17
|
+
import { previewIntegration } from "../../integrations/mutation-plan";
|
|
18
|
+
import { exportSnapshotIdentity, previewExportSnapshot } from "./model-rows";
|
|
14
19
|
import type { ManagementContext } from "./context";
|
|
15
20
|
import { readManagementJsonBody, readOptionalManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
16
21
|
import { jsonResponse } from "../auth-cors";
|
|
@@ -22,6 +27,142 @@ export interface AsideProfileRouteOptions {
|
|
|
22
27
|
|
|
23
28
|
class ProfileQueryError extends Error { readonly status = 400; readonly code = "invalid_aside_profile"; }
|
|
24
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The confirmed plan carried with this request, or null when there is none.
|
|
32
|
+
*
|
|
33
|
+
* Both fields or neither, and the operation must be the one being requested. Dropping a supplied
|
|
34
|
+
* binding would be the worst available behaviour: the caller believes their confirmation is being
|
|
35
|
+
* checked while the mutation proceeds unchecked.
|
|
36
|
+
*/
|
|
37
|
+
function asideBinding(
|
|
38
|
+
body: { operation?: unknown; planFingerprint?: unknown },
|
|
39
|
+
expected: IntegrationPlanOperation,
|
|
40
|
+
): { operation: IntegrationPlanOperation; fingerprint: string } | null {
|
|
41
|
+
const { operation, planFingerprint } = body;
|
|
42
|
+
if (operation === undefined && planFingerprint === undefined) return null;
|
|
43
|
+
if (operation === undefined || typeof planFingerprint !== "string" || planFingerprint.length === 0) {
|
|
44
|
+
throw new ProfileQueryError("operation and planFingerprint must be sent together");
|
|
45
|
+
}
|
|
46
|
+
if (operation !== expected) throw new ProfileQueryError("operation does not match the requested change");
|
|
47
|
+
return { operation: expected, fingerprint: planFingerprint };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The guard a bound profile change runs before it writes anything.
|
|
52
|
+
*
|
|
53
|
+
* It re-plans the same profile scope and compares. The hooks that call it sit ahead of the
|
|
54
|
+
* preference write and the journal import, which is the only placement that helps here: those two
|
|
55
|
+
* happen before any writer lock is taken, so a check under that lock would fire after the thing it
|
|
56
|
+
* was meant to prevent.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* The check a bound Aside change runs, against the input that change is about to write from.
|
|
60
|
+
*
|
|
61
|
+
* It plans the prepared input it is given rather than building one of its own. Rebuilding meant
|
|
62
|
+
* reading the live configuration a second time, so a configuration edited to something else and
|
|
63
|
+
* back again while this action was in flight produced a check that agreed with a plan the write
|
|
64
|
+
* never described.
|
|
65
|
+
*
|
|
66
|
+
* Exported so the check itself can be exercised with a real prepared input and a roster that
|
|
67
|
+
* actually moved. The window it closes needs an ordinary load to complete while a mutation is
|
|
68
|
+
* preparing, which no test seam reaches from outside; a copy of the guard would prove nothing
|
|
69
|
+
* about the one the route installs. It takes only the configuration from the request context.
|
|
70
|
+
*/
|
|
71
|
+
export function asideGuardFor(
|
|
72
|
+
ctx: Pick<ManagementContext, "config">,
|
|
73
|
+
capturedIdentity: string,
|
|
74
|
+
profileIdValue: number,
|
|
75
|
+
binding: { operation: IntegrationPlanOperation; fingerprint: string },
|
|
76
|
+
request: { opId?: string; confirmDrift?: boolean; resolved?: AsideOperation },
|
|
77
|
+
capture: { plan: IntegrationMutationPlan | null },
|
|
78
|
+
): (prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null> {
|
|
79
|
+
return async prepared => {
|
|
80
|
+
const plan = planFor(prepared, profileIdValue, binding, request);
|
|
81
|
+
/*
|
|
82
|
+
* The roster this confirmation was planned against has to still be the retained one. Only its
|
|
83
|
+
* rows were carried into the mutation, so an ordinary load completing while this action
|
|
84
|
+
* prepared could have replaced or retired the snapshot, and fingerprinting the carried rows
|
|
85
|
+
* would then accept a confirmation for a roster the operator no longer has. The non-Aside
|
|
86
|
+
* guard verifies the same identity; this one verifies it before the preference write, which is
|
|
87
|
+
* the last moment that still precedes every effect this action has.
|
|
88
|
+
*/
|
|
89
|
+
const rosterMoved = exportSnapshotIdentity(ctx.config) !== capturedIdentity;
|
|
90
|
+
if (!rosterMoved && plan.canApply && plan.fingerprint === binding.fingerprint) return null;
|
|
91
|
+
capture.plan = plan;
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
reason: "conflict",
|
|
95
|
+
state: plan.state,
|
|
96
|
+
clientId: "aside",
|
|
97
|
+
message: rosterMoved
|
|
98
|
+
? "the model roster changed while confirming"
|
|
99
|
+
: "that confirmation no longer describes this profile",
|
|
100
|
+
profileId: profileIdValue,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The same comparison, run again immediately before the document is written.
|
|
107
|
+
*
|
|
108
|
+
* Aside takes no writer lock, and its preference write and journal import sit between the first
|
|
109
|
+
* check and the write that check authorizes. A target edited in that window is read fresh by the
|
|
110
|
+
* writer, which then compares it against itself and finds nothing to object to, so a confirmation
|
|
111
|
+
* about the earlier file would still overwrite the later one.
|
|
112
|
+
*
|
|
113
|
+
* The roster is deliberately not re-examined here. This action has just written the operator's
|
|
114
|
+
* preference into the configuration, which retires the retained roster by design, so asking that
|
|
115
|
+
* question at this point would refuse every confirmation on principle. What is asked is the one
|
|
116
|
+
* thing this moment can answer: does the plan the operator confirmed still describe this file.
|
|
117
|
+
*/
|
|
118
|
+
export function asideLateGuardFor(
|
|
119
|
+
profileIdValue: number,
|
|
120
|
+
binding: { operation: IntegrationPlanOperation; fingerprint: string },
|
|
121
|
+
request: { opId?: string; confirmDrift?: boolean; resolved?: AsideOperation },
|
|
122
|
+
capture: { plan: IntegrationMutationPlan | null },
|
|
123
|
+
): (prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null> {
|
|
124
|
+
return async prepared => {
|
|
125
|
+
const plan = planFor(prepared, profileIdValue, binding, request);
|
|
126
|
+
if (plan.canApply && plan.fingerprint === binding.fingerprint) return null;
|
|
127
|
+
capture.plan = plan;
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
reason: "conflict",
|
|
131
|
+
state: plan.state,
|
|
132
|
+
clientId: "aside",
|
|
133
|
+
message: "that confirmation no longer describes this profile",
|
|
134
|
+
profileId: profileIdValue,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function planFor(
|
|
140
|
+
prepared: IntegrationWriteInput,
|
|
141
|
+
profileIdValue: number,
|
|
142
|
+
binding: { operation: IntegrationPlanOperation; fingerprint: string },
|
|
143
|
+
request: { opId?: string; confirmDrift?: boolean; resolved?: AsideOperation },
|
|
144
|
+
): IntegrationMutationPlan {
|
|
145
|
+
return previewIntegration(prepared, {
|
|
146
|
+
profileId: profileIdValue,
|
|
147
|
+
operation: binding.operation,
|
|
148
|
+
...(request.opId === undefined ? {} : { opId: request.opId }),
|
|
149
|
+
...(request.confirmDrift === undefined ? {} : { confirmDrift: request.confirmDrift }),
|
|
150
|
+
// The row the route selected, so the guard and the mutation mean the same operation even
|
|
151
|
+
// when more than one valid copy exists.
|
|
152
|
+
...(request.resolved === undefined
|
|
153
|
+
? {}
|
|
154
|
+
: { resolved: { entry: request.resolved.entry, store: request.resolved.store } }),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stalePlanResponse(ctx: ManagementContext, plan: IntegrationMutationPlan): Response {
|
|
159
|
+
return jsonResponse({
|
|
160
|
+
error: "integration preview is stale",
|
|
161
|
+
code: "integration_preview_stale",
|
|
162
|
+
plan,
|
|
163
|
+
}, 409, ctx.req, ctx.config);
|
|
164
|
+
}
|
|
165
|
+
|
|
25
166
|
const ASIDE_INTEGRATION_PATH = "/api/client-integrations/aside";
|
|
26
167
|
const ASIDE_PROFILES_PATH = "/api/client-integrations/aside/profiles";
|
|
27
168
|
|
|
@@ -75,7 +216,7 @@ function nestedProfileContext(ctx: ManagementContext): { ctx: ManagementContext;
|
|
|
75
216
|
url.searchParams.set("client", "aside");
|
|
76
217
|
return { ctx: { ...ctx, url }, action: "journal" };
|
|
77
218
|
}
|
|
78
|
-
if (parts.length > 2 || !parts[0] || (parts[1] !== undefined && !["journal", "restore"].includes(parts[1]))) {
|
|
219
|
+
if (parts.length > 2 || !parts[0] || (parts[1] !== undefined && !["journal", "restore", "preview"].includes(parts[1]))) {
|
|
79
220
|
throw new ProfileQueryError("Invalid Aside profile path");
|
|
80
221
|
}
|
|
81
222
|
const prior = url.searchParams.get("profile");
|
|
@@ -107,12 +248,62 @@ export async function handleAsideProfileRoutes(
|
|
|
107
248
|
}
|
|
108
249
|
return null;
|
|
109
250
|
}
|
|
251
|
+
if (normalized.action === "preview") {
|
|
252
|
+
if (req.method !== "POST") return null;
|
|
253
|
+
// A plan describes one profile, so an unscoped preview has nothing to describe.
|
|
254
|
+
if (id === undefined) throw new ProfileQueryError("a plan applies to one profile");
|
|
255
|
+
const body = await readProfileBody(req);
|
|
256
|
+
if (!isObject(body)) throw new ProfileQueryError("preview body must be an object");
|
|
257
|
+
const operation = body.operation;
|
|
258
|
+
if (operation !== "apply" && operation !== "overwrite" && operation !== "disable" && operation !== "restore") {
|
|
259
|
+
throw new ProfileQueryError("operation must be apply, overwrite, disable or restore");
|
|
260
|
+
}
|
|
261
|
+
if (operation === "restore" && (typeof body.opId !== "string" || !body.opId.trim())) {
|
|
262
|
+
throw new ProfileQueryError("opId must be a non-empty string");
|
|
263
|
+
}
|
|
264
|
+
// The preview resolves the row the same way the mutation will, once, here.
|
|
265
|
+
const selected = operation === "restore"
|
|
266
|
+
? findAsideOperation(options.input(), String(body.opId).trim(), id)
|
|
267
|
+
: null;
|
|
268
|
+
if (operation === "restore" && selected === null) {
|
|
269
|
+
return jsonResponse({
|
|
270
|
+
error: "integration operation not found",
|
|
271
|
+
code: "integration_operation_not_found",
|
|
272
|
+
}, 404, req, ctx.config);
|
|
273
|
+
}
|
|
274
|
+
const roster = previewExportSnapshot(ctx.config);
|
|
275
|
+
if (roster === null) {
|
|
276
|
+
return jsonResponse({
|
|
277
|
+
error: "no model roster is cached yet, so this change cannot be planned",
|
|
278
|
+
code: "integration_preview_unavailable",
|
|
279
|
+
}, 409, req, ctx.config);
|
|
280
|
+
}
|
|
281
|
+
const plan = await previewAsideProfile({ ...options.input(), models: roster.models }, {
|
|
282
|
+
profileId: id,
|
|
283
|
+
operation,
|
|
284
|
+
...(operation === "restore"
|
|
285
|
+
? {
|
|
286
|
+
opId: String(body.opId).trim(),
|
|
287
|
+
confirmDrift: body.confirmDrift === true,
|
|
288
|
+
...(selected === null ? {} : { resolved: { entry: selected.entry, store: selected.store } }),
|
|
289
|
+
}
|
|
290
|
+
: {}),
|
|
291
|
+
});
|
|
292
|
+
return jsonResponse(plan, 200, req, ctx.config);
|
|
293
|
+
}
|
|
110
294
|
if (normalized.action === "restore") {
|
|
111
295
|
if (req.method !== "POST") return null;
|
|
112
296
|
const body = await readProfileBody(req);
|
|
113
297
|
if (!isObject(body) || typeof body.opId !== "string" || !body.opId.trim()
|
|
114
298
|
|| (body.confirmDrift !== undefined && typeof body.confirmDrift !== "boolean")) throw new ProfileQueryError("Invalid Aside restore request");
|
|
115
|
-
|
|
299
|
+
// Rebuilding the body here previously dropped any binding, so a bound nested restore
|
|
300
|
+
// reached the writer with its confirmation unexamined. Forward the fields as sent.
|
|
301
|
+
return asideRestoreResponse(ctx, {
|
|
302
|
+
opId: body.opId.trim(),
|
|
303
|
+
confirmDrift: body.confirmDrift === true,
|
|
304
|
+
...(body.operation === undefined ? {} : { operation: body.operation }),
|
|
305
|
+
...(body.planFingerprint === undefined ? {} : { planFingerprint: body.planFingerprint }),
|
|
306
|
+
}, options);
|
|
116
307
|
}
|
|
117
308
|
if (url.pathname === "/api/client-integrations/aside/sync") {
|
|
118
309
|
if (req.method !== "POST") return null;
|
|
@@ -134,7 +325,38 @@ export async function handleAsideProfileRoutes(
|
|
|
134
325
|
if (!isObject(body) || typeof body.enabled !== "boolean") throw new ProfileQueryError("enabled must be a boolean");
|
|
135
326
|
if (body.overwriteConflict !== undefined && typeof body.overwriteConflict !== "boolean") throw new ProfileQueryError("overwriteConflict must be a boolean");
|
|
136
327
|
if (body.overwriteConflict === true && !body.enabled) throw new ProfileQueryError("overwriteConflict applies only to enabling an integration");
|
|
137
|
-
|
|
328
|
+
// An exact profile is required before a binding could ever mean anything: one fingerprint
|
|
329
|
+
// cannot honestly describe several independently changing files.
|
|
330
|
+
const expected: IntegrationPlanOperation = body.enabled
|
|
331
|
+
? (body.overwriteConflict === true ? "overwrite" : "apply")
|
|
332
|
+
: "disable";
|
|
333
|
+
if ((body.operation !== undefined || body.planFingerprint !== undefined) && id === undefined) {
|
|
334
|
+
throw new ProfileQueryError("a confirmed plan applies to one profile");
|
|
335
|
+
}
|
|
336
|
+
const binding = asideBinding(body, expected);
|
|
337
|
+
const capture: { plan: IntegrationMutationPlan | null } = { plan: null };
|
|
338
|
+
let mutationInput = options.input();
|
|
339
|
+
let revalidate: ((prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null>) | undefined;
|
|
340
|
+
let revalidateBeforeWrite: ((prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null>) | undefined;
|
|
341
|
+
if (binding !== null && id !== undefined) {
|
|
342
|
+
const roster = previewExportSnapshot(ctx.config);
|
|
343
|
+
if (roster === null) {
|
|
344
|
+
return jsonResponse({
|
|
345
|
+
error: "no model roster is cached yet, so this change cannot be planned",
|
|
346
|
+
code: "integration_preview_unavailable",
|
|
347
|
+
}, 409, req, ctx.config);
|
|
348
|
+
}
|
|
349
|
+
// One roster for the guard and the mutation, so they cannot disagree by construction.
|
|
350
|
+
mutationInput = { ...mutationInput, models: roster.models };
|
|
351
|
+
revalidate = asideGuardFor(ctx, roster.identity, id, binding, {}, capture);
|
|
352
|
+
revalidateBeforeWrite = asideLateGuardFor(id, binding, {}, capture);
|
|
353
|
+
}
|
|
354
|
+
const batch = await mutateAsideProfiles(
|
|
355
|
+
mutationInput,
|
|
356
|
+
{ enabled: body.enabled, profileId: id, overwriteConflict: body.overwriteConflict === true },
|
|
357
|
+
revalidate ? { revalidate, ...(revalidateBeforeWrite ? { revalidateBeforeWrite } : {}) } : undefined,
|
|
358
|
+
);
|
|
359
|
+
if (capture.plan) return stalePlanResponse(ctx, capture.plan);
|
|
138
360
|
if (id !== undefined) {
|
|
139
361
|
const result = batch.results[0];
|
|
140
362
|
if (!result) throw new Error("Aside profile mutation returned no result");
|
|
@@ -180,7 +402,9 @@ export async function asideJournalResponse(
|
|
|
180
402
|
}
|
|
181
403
|
|
|
182
404
|
export async function asideRestoreResponse(
|
|
183
|
-
ctx: ManagementContext,
|
|
405
|
+
ctx: ManagementContext,
|
|
406
|
+
body: { opId: string; confirmDrift?: boolean; operation?: unknown; planFingerprint?: unknown },
|
|
407
|
+
options: AsideProfileRouteOptions,
|
|
184
408
|
): Promise<Response | null> {
|
|
185
409
|
try {
|
|
186
410
|
validateClientSelector(ctx);
|
|
@@ -196,7 +420,42 @@ export async function asideRestoreResponse(
|
|
|
196
420
|
if (id === undefined) return null;
|
|
197
421
|
return jsonResponse({ error: "integration operation not found", code: "integration_operation_not_found", opId: body.opId }, 404, ctx.req, ctx.config);
|
|
198
422
|
}
|
|
199
|
-
|
|
423
|
+
if ((body.operation !== undefined || body.planFingerprint !== undefined) && id === undefined) {
|
|
424
|
+
throw new ProfileQueryError("a confirmed plan applies to one profile");
|
|
425
|
+
}
|
|
426
|
+
const restoreBinding = asideBinding(body, "restore");
|
|
427
|
+
const capture: { plan: IntegrationMutationPlan | null } = { plan: null };
|
|
428
|
+
let restoreInput = input;
|
|
429
|
+
let revalidate: ((prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null>) | undefined;
|
|
430
|
+
let revalidateBeforeWrite: ((prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null>) | undefined;
|
|
431
|
+
if (restoreBinding !== null) {
|
|
432
|
+
const roster = previewExportSnapshot(ctx.config);
|
|
433
|
+
if (roster === null) {
|
|
434
|
+
return jsonResponse({
|
|
435
|
+
error: "no model roster is cached yet, so this change cannot be planned",
|
|
436
|
+
code: "integration_preview_unavailable",
|
|
437
|
+
}, 409, ctx.req, ctx.config);
|
|
438
|
+
}
|
|
439
|
+
restoreInput = { ...restoreInput, models: roster.models };
|
|
440
|
+
revalidate = asideGuardFor(ctx, roster.identity, operation.profileId, restoreBinding, {
|
|
441
|
+
opId: body.opId,
|
|
442
|
+
confirmDrift: body.confirmDrift === true,
|
|
443
|
+
resolved: operation,
|
|
444
|
+
}, capture);
|
|
445
|
+
revalidateBeforeWrite = asideLateGuardFor(operation.profileId, restoreBinding, {
|
|
446
|
+
opId: body.opId,
|
|
447
|
+
confirmDrift: body.confirmDrift === true,
|
|
448
|
+
resolved: operation,
|
|
449
|
+
}, capture);
|
|
450
|
+
}
|
|
451
|
+
const result = await restoreAsideProfile(
|
|
452
|
+
restoreInput,
|
|
453
|
+
// The same row the preview and guard used. Letting the mutation resolve its own copy is how
|
|
454
|
+
// a confirmation ends up bound to an operation other than the one that runs.
|
|
455
|
+
{ ...body, profileId: operation.profileId, selectedOperation: operation },
|
|
456
|
+
revalidate ? { revalidate, ...(revalidateBeforeWrite ? { revalidateBeforeWrite } : {}) } : undefined,
|
|
457
|
+
);
|
|
458
|
+
if (capture.plan) return stalePlanResponse(ctx, capture.plan);
|
|
200
459
|
return result.ok ? jsonResponse(result, 200, ctx.req, ctx.config) : options.failure(result);
|
|
201
460
|
} catch (error) {
|
|
202
461
|
if (error instanceof ClientPathError && !ctx.url.searchParams.has("profile")
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { compactionRoutingSchema } from "../../config/schema/leaf-validators";
|
|
2
|
+
import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
|
|
1
3
|
import type { IntegrationClientId } from "../../integrations/registry";
|
|
2
4
|
import { randomUUID } from "node:crypto";
|
|
3
5
|
import { readFileSync } from "node:fs";
|
|
@@ -110,7 +112,7 @@ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from ".
|
|
|
110
112
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
111
113
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
112
114
|
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
113
|
-
import {
|
|
115
|
+
import { withProviderCatalogCapabilityDTO } from "./provider-capability-config";
|
|
114
116
|
import { applySystemEnvToggle } from "../system-env";
|
|
115
117
|
import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache";
|
|
116
118
|
import { runWindowsTrayAction } from "../windows-tray-control";
|
|
@@ -186,7 +188,8 @@ interface ClientIntegrationSyncOutcome {
|
|
|
186
188
|
export async function syncEnabledClientIntegrations(
|
|
187
189
|
port: number | undefined,
|
|
188
190
|
config: OcxConfig,
|
|
189
|
-
deps: Pick<ManagementContext["deps"],
|
|
191
|
+
deps: Pick<ManagementContext["deps"],
|
|
192
|
+
"fetchAllModels" | "refreshOwnedCatalogIntegrations" | "writeDesktop3pConfig"> = {},
|
|
190
193
|
): Promise<ClientIntegrationSyncOutcome[]> {
|
|
191
194
|
if (port === undefined) return [];
|
|
192
195
|
const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled } = await import("../../codex/desired-state");
|
|
@@ -235,7 +238,8 @@ export async function syncEnabledClientIntegrations(
|
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
const { refreshOwnedCatalogIntegrations } = await import("../../integrations/catalog-refresh");
|
|
238
|
-
|
|
241
|
+
const refreshOwned = deps.refreshOwnedCatalogIntegrations ?? refreshOwnedCatalogIntegrations;
|
|
242
|
+
out.push(...await refreshOwned({
|
|
239
243
|
models: async () => {
|
|
240
244
|
const { loadExportModels } = await import("./model-rows");
|
|
241
245
|
return loadExportModels(config);
|
|
@@ -266,7 +270,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
266
270
|
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
267
271
|
const readStartupHealth = deps.getCachedStartupHealth ?? getCachedStartupHealth;
|
|
268
272
|
if (url.pathname === "/api/config" && req.method === "GET") {
|
|
269
|
-
return jsonResponse(
|
|
273
|
+
return jsonResponse(withProviderCatalogCapabilityDTO(safeConfigDTO(config), config));
|
|
270
274
|
}
|
|
271
275
|
|
|
272
276
|
if (url.pathname === "/api/config" && req.method === "PUT") {
|
|
@@ -324,6 +328,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
324
328
|
// Absent means off, same convention: the GUI renders a plain switch without
|
|
325
329
|
// needing to know that `undefined` and `false` mean the same thing here.
|
|
326
330
|
ultraFastTier: config.ultraFastTier === true,
|
|
331
|
+
// Absent means on by default: the GUI renders a switch enabled unless explicit false.
|
|
332
|
+
fastRows: config.fastRows !== false,
|
|
327
333
|
codexMainAccountHardLock: config.codexMainAccountHardLock === true,
|
|
328
334
|
mainAccountHardLock: getMainAccountHardLockStatus(config),
|
|
329
335
|
// Absent means the historical auto-open, so the GUI can render the toggle
|
|
@@ -338,6 +344,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
338
344
|
reason: "not_requested",
|
|
339
345
|
retryable: false,
|
|
340
346
|
}),
|
|
347
|
+
compactionRouting: config.compactionRouting ?? null,
|
|
341
348
|
startupHealth: await readStartupHealth(config),
|
|
342
349
|
codexRuntime: {
|
|
343
350
|
path: displayCodexRuntimePath(resolved.runtime.command),
|
|
@@ -425,9 +432,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
425
432
|
codexQuotaAutoRefresh?: unknown;
|
|
426
433
|
oauthOpenBrowser?: unknown;
|
|
427
434
|
ultraFastTier?: unknown;
|
|
435
|
+
fastRows?: unknown;
|
|
428
436
|
codexMainAccountHardLock?: unknown;
|
|
429
437
|
codexDesktopAuthless?: unknown;
|
|
430
438
|
codexClientCompaction?: unknown;
|
|
439
|
+
compactionRouting?: unknown;
|
|
431
440
|
};
|
|
432
441
|
if (body.codexAutoStart === undefined
|
|
433
442
|
&& body.streamMode === undefined
|
|
@@ -436,10 +445,12 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
436
445
|
&& body.codexQuotaAutoRefresh === undefined
|
|
437
446
|
&& body.oauthOpenBrowser === undefined
|
|
438
447
|
&& body.ultraFastTier === undefined
|
|
448
|
+
&& body.fastRows === undefined
|
|
439
449
|
&& body.codexMainAccountHardLock === undefined
|
|
440
450
|
&& body.codexDesktopAuthless === undefined
|
|
441
|
-
&& body.codexClientCompaction === undefined
|
|
442
|
-
|
|
451
|
+
&& body.codexClientCompaction === undefined
|
|
452
|
+
&& body.compactionRouting === undefined) {
|
|
453
|
+
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, ultraFastTier, fastRows, codexMainAccountHardLock, codexDesktopAuthless, codexClientCompaction, or compactionRouting" }, 400);
|
|
443
454
|
}
|
|
444
455
|
if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
|
|
445
456
|
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
@@ -457,6 +468,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
457
468
|
if (body.ultraFastTier !== undefined && typeof body.ultraFastTier !== "boolean") {
|
|
458
469
|
return jsonResponse({ error: "ultraFastTier boolean is required" }, 400);
|
|
459
470
|
}
|
|
471
|
+
if (body.fastRows !== undefined && typeof body.fastRows !== "boolean") {
|
|
472
|
+
return jsonResponse({ error: "fastRows boolean is required" }, 400);
|
|
473
|
+
}
|
|
460
474
|
if (body.codexMainAccountHardLock !== undefined && typeof body.codexMainAccountHardLock !== "boolean") {
|
|
461
475
|
return jsonResponse({ error: "codexMainAccountHardLock boolean is required" }, 400);
|
|
462
476
|
}
|
|
@@ -466,6 +480,12 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
466
480
|
if (body.codexClientCompaction !== undefined && typeof body.codexClientCompaction !== "boolean") {
|
|
467
481
|
return jsonResponse({ error: "codexClientCompaction boolean is required" }, 400);
|
|
468
482
|
}
|
|
483
|
+
const compactionRouting = body.compactionRouting == null
|
|
484
|
+
? body.compactionRouting
|
|
485
|
+
: compactionRoutingSchema.safeParse(body.compactionRouting);
|
|
486
|
+
if (compactionRouting != null && !compactionRouting.success) {
|
|
487
|
+
return jsonResponse({ error: "compactionRouting requires a model, an optional valid reasoningEffort, and optional non-repeating triggers drawn from \"manual\" and \"auto\"" }, 400);
|
|
488
|
+
}
|
|
469
489
|
let quotaAutoRefreshChange: { id: string; window: "fiveHour" | "weekly"; enabled: boolean } | undefined;
|
|
470
490
|
if (body.codexQuotaAutoRefresh !== undefined) {
|
|
471
491
|
if (!isPlainRecord(body.codexQuotaAutoRefresh)) {
|
|
@@ -495,6 +515,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
495
515
|
)) {
|
|
496
516
|
return jsonResponse({ error: `appOwnedMemoryBudgetMb must be an integer from ${MIN_APP_OWNED_MEMORY_BUDGET_MB} to ${MAX_APP_OWNED_MEMORY_BUDGET_MB}` }, 400);
|
|
497
517
|
}
|
|
518
|
+
const restoreCompactionRouting = captureConfigTopLevelRollback(config, ["compactionRouting"]);
|
|
498
519
|
const previousSettings = {
|
|
499
520
|
codexAutoStart: config.codexAutoStart,
|
|
500
521
|
hasCodexAutoStart: Object.hasOwn(config, "codexAutoStart"),
|
|
@@ -512,6 +533,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
512
533
|
hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
|
|
513
534
|
ultraFastTier: config.ultraFastTier,
|
|
514
535
|
hasUltraFastTier: Object.hasOwn(config, "ultraFastTier"),
|
|
536
|
+
fastRows: config.fastRows,
|
|
537
|
+
hasFastRows: Object.hasOwn(config, "fastRows"),
|
|
515
538
|
codexMainAccountHardLock: config.codexMainAccountHardLock,
|
|
516
539
|
hasCodexMainAccountHardLock: Object.hasOwn(config, "codexMainAccountHardLock"),
|
|
517
540
|
codexDesktopAuthless: config.codexDesktopAuthless,
|
|
@@ -523,6 +546,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
523
546
|
let pickerIsEnabled = pickerWasEnabled;
|
|
524
547
|
const authlessWasEnabled = config.codexDesktopAuthless === true;
|
|
525
548
|
const clientCompactionWasEnabled = config.codexClientCompaction === true;
|
|
549
|
+
const fastRowsWasEnabled = config.fastRows !== false;
|
|
526
550
|
try {
|
|
527
551
|
if (typeof body.codexAutoStart === "boolean") {
|
|
528
552
|
config.codexAutoStart = body.codexAutoStart;
|
|
@@ -550,12 +574,16 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
550
574
|
// default, and a written `false` would survive as a decision nobody made.
|
|
551
575
|
if (body.ultraFastTier === true) config.ultraFastTier = true;
|
|
552
576
|
else if (body.ultraFastTier === false) deleteConfigTopLevelKey(config, "ultraFastTier");
|
|
577
|
+
if (body.fastRows === false) config.fastRows = false;
|
|
578
|
+
else if (body.fastRows === true) deleteConfigTopLevelKey(config, "fastRows");
|
|
553
579
|
if (body.codexMainAccountHardLock === true) config.codexMainAccountHardLock = true;
|
|
554
580
|
else if (body.codexMainAccountHardLock === false) deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
|
|
555
581
|
if (body.codexDesktopAuthless === true) config.codexDesktopAuthless = true;
|
|
556
582
|
else if (body.codexDesktopAuthless === false) deleteConfigTopLevelKey(config, "codexDesktopAuthless");
|
|
557
583
|
if (body.codexClientCompaction === true) config.codexClientCompaction = true;
|
|
558
584
|
else if (body.codexClientCompaction === false) deleteConfigTopLevelKey(config, "codexClientCompaction");
|
|
585
|
+
if (compactionRouting === null) deleteConfigTopLevelKey(config, "compactionRouting");
|
|
586
|
+
else if (compactionRouting?.success) config.compactionRouting = compactionRouting.data;
|
|
559
587
|
if (quotaAutoRefreshChange) {
|
|
560
588
|
const { id, window, enabled } = quotaAutoRefreshChange;
|
|
561
589
|
const setting = { ...(config.codexQuotaAutoRefresh?.[id] ?? {}) };
|
|
@@ -592,6 +620,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
592
620
|
if (previousSettings.hasUltraFastTier) {
|
|
593
621
|
config.ultraFastTier = previousSettings.ultraFastTier;
|
|
594
622
|
} else deleteConfigTopLevelKey(config, "ultraFastTier");
|
|
623
|
+
if (previousSettings.hasFastRows) {
|
|
624
|
+
config.fastRows = previousSettings.fastRows;
|
|
625
|
+
} else deleteConfigTopLevelKey(config, "fastRows");
|
|
595
626
|
if (previousSettings.hasCodexMainAccountHardLock) {
|
|
596
627
|
config.codexMainAccountHardLock = previousSettings.codexMainAccountHardLock;
|
|
597
628
|
} else deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
|
|
@@ -601,6 +632,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
601
632
|
if (previousSettings.hasCodexClientCompaction) {
|
|
602
633
|
config.codexClientCompaction = previousSettings.codexClientCompaction;
|
|
603
634
|
} else deleteConfigTopLevelKey(config, "codexClientCompaction");
|
|
635
|
+
restoreCompactionRouting();
|
|
604
636
|
throw error;
|
|
605
637
|
}
|
|
606
638
|
if (typeof body.appOwnedMemoryBudgetMb === "number") {
|
|
@@ -609,6 +641,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
609
641
|
}
|
|
610
642
|
const authlessIsEnabled = config.codexDesktopAuthless === true;
|
|
611
643
|
const clientCompactionIsEnabled = config.codexClientCompaction === true;
|
|
644
|
+
const fastRowsIsEnabled = config.fastRows !== false;
|
|
645
|
+
const fastRowsChanged = fastRowsWasEnabled !== fastRowsIsEnabled;
|
|
612
646
|
const desktopSwitchesChanged = authlessWasEnabled !== authlessIsEnabled
|
|
613
647
|
|| clientCompactionWasEnabled !== clientCompactionIsEnabled;
|
|
614
648
|
// Catalog convergence is not config injection, and the comment that used to sit here said
|
|
@@ -616,9 +650,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
616
650
|
// injector, which is why flipping either switch left `config.toml` in its old shape until
|
|
617
651
|
// a separate `ocx sync` (#4809). Both halves are needed when a Desktop switch changes; a
|
|
618
652
|
// picker-only update still refreshes just the catalog.
|
|
619
|
-
const catalogRefresh = pickerWasEnabled !== pickerIsEnabled || desktopSwitchesChanged
|
|
653
|
+
const catalogRefresh = pickerWasEnabled !== pickerIsEnabled || desktopSwitchesChanged || fastRowsChanged
|
|
620
654
|
? await convergeCodexCatalog()
|
|
621
655
|
: undefined;
|
|
656
|
+
if (fastRowsChanged) {
|
|
657
|
+
const { readRuntimePort } = await import("../../config/process-state");
|
|
658
|
+
const runtime = (deps.readRuntimePort ?? readRuntimePort)(process.pid);
|
|
659
|
+
await syncEnabledClientIntegrations(runtime?.port, config, deps);
|
|
660
|
+
}
|
|
622
661
|
// Injection second, matching `syncModelsToCodex`: the injected `model_catalog_json` should
|
|
623
662
|
// point at a catalog that has already settled. And it runs here rather than inside the save
|
|
624
663
|
// because coordinated Codex writes acquire the Codex write lock N before the config mutation
|
|
@@ -641,9 +680,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
641
680
|
codexQuotaAutoRefresh: quotaAutoRefreshSettings(config),
|
|
642
681
|
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
643
682
|
catalogRefreshPending,
|
|
683
|
+
fastRows: config.fastRows !== false,
|
|
644
684
|
codexDesktopAuthless: authlessIsEnabled,
|
|
645
685
|
codexClientCompaction: clientCompactionIsEnabled,
|
|
646
686
|
codexDesktopSwitches,
|
|
687
|
+
compactionRouting: config.compactionRouting ?? null,
|
|
647
688
|
codexMainAccountHardLock: config.codexMainAccountHardLock === true,
|
|
648
689
|
mainAccountHardLock: getMainAccountHardLockStatus(config),
|
|
649
690
|
startupHealth: await readStartupHealth(config),
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
performCodexRestart,
|
|
20
20
|
readCodexAppServerState,
|
|
21
21
|
} from "../../codex/app-server-restart-service";
|
|
22
|
+
import type { RequestMetricsSnapshotter } from "../request-metrics";
|
|
22
23
|
|
|
23
24
|
import type { RemoteWorkspaceHub } from "../../remote-control/workspace-hub";
|
|
24
25
|
import type { RemoteWorkspaceSessionService } from "../../remote-control/workspace-sessions";
|
|
@@ -31,6 +32,8 @@ export type RemoteWorkspaceSessionsApi = Pick<RemoteWorkspaceSessionService,
|
|
|
31
32
|
"availability" | "list" | "create" | "prompt" | "submitPrompt" | "stop" | "shutdown">;
|
|
32
33
|
|
|
33
34
|
export interface ManagementApiDeps {
|
|
35
|
+
/** Read-only process-local aggregate metrics; absent keeps the scrape route unavailable. */
|
|
36
|
+
requestMetrics?: RequestMetricsSnapshotter;
|
|
34
37
|
remoteWorkspaceHub?: RemoteWorkspaceHubApi;
|
|
35
38
|
remoteWorkspaceSessions?: RemoteWorkspaceSessionsApi;
|
|
36
39
|
/** The listener retains and awaits teardown only after this optional subsystem activates. */
|