@bitkyc08/opencodex 2.59.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/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/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 +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- 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 +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- 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/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- 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 +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- 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/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- 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 +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- 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 +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- 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/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 +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- 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 +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- 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/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/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- 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/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- 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/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.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";
|
|
@@ -342,6 +344,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
342
344
|
reason: "not_requested",
|
|
343
345
|
retryable: false,
|
|
344
346
|
}),
|
|
347
|
+
compactionRouting: config.compactionRouting ?? null,
|
|
345
348
|
startupHealth: await readStartupHealth(config),
|
|
346
349
|
codexRuntime: {
|
|
347
350
|
path: displayCodexRuntimePath(resolved.runtime.command),
|
|
@@ -433,6 +436,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
433
436
|
codexMainAccountHardLock?: unknown;
|
|
434
437
|
codexDesktopAuthless?: unknown;
|
|
435
438
|
codexClientCompaction?: unknown;
|
|
439
|
+
compactionRouting?: unknown;
|
|
436
440
|
};
|
|
437
441
|
if (body.codexAutoStart === undefined
|
|
438
442
|
&& body.streamMode === undefined
|
|
@@ -444,8 +448,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
444
448
|
&& body.fastRows === undefined
|
|
445
449
|
&& body.codexMainAccountHardLock === undefined
|
|
446
450
|
&& body.codexDesktopAuthless === undefined
|
|
447
|
-
&& body.codexClientCompaction === undefined
|
|
448
|
-
|
|
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);
|
|
449
454
|
}
|
|
450
455
|
if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
|
|
451
456
|
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
@@ -475,6 +480,12 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
475
480
|
if (body.codexClientCompaction !== undefined && typeof body.codexClientCompaction !== "boolean") {
|
|
476
481
|
return jsonResponse({ error: "codexClientCompaction boolean is required" }, 400);
|
|
477
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
|
+
}
|
|
478
489
|
let quotaAutoRefreshChange: { id: string; window: "fiveHour" | "weekly"; enabled: boolean } | undefined;
|
|
479
490
|
if (body.codexQuotaAutoRefresh !== undefined) {
|
|
480
491
|
if (!isPlainRecord(body.codexQuotaAutoRefresh)) {
|
|
@@ -504,6 +515,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
504
515
|
)) {
|
|
505
516
|
return jsonResponse({ error: `appOwnedMemoryBudgetMb must be an integer from ${MIN_APP_OWNED_MEMORY_BUDGET_MB} to ${MAX_APP_OWNED_MEMORY_BUDGET_MB}` }, 400);
|
|
506
517
|
}
|
|
518
|
+
const restoreCompactionRouting = captureConfigTopLevelRollback(config, ["compactionRouting"]);
|
|
507
519
|
const previousSettings = {
|
|
508
520
|
codexAutoStart: config.codexAutoStart,
|
|
509
521
|
hasCodexAutoStart: Object.hasOwn(config, "codexAutoStart"),
|
|
@@ -570,6 +582,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
570
582
|
else if (body.codexDesktopAuthless === false) deleteConfigTopLevelKey(config, "codexDesktopAuthless");
|
|
571
583
|
if (body.codexClientCompaction === true) config.codexClientCompaction = true;
|
|
572
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;
|
|
573
587
|
if (quotaAutoRefreshChange) {
|
|
574
588
|
const { id, window, enabled } = quotaAutoRefreshChange;
|
|
575
589
|
const setting = { ...(config.codexQuotaAutoRefresh?.[id] ?? {}) };
|
|
@@ -618,6 +632,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
618
632
|
if (previousSettings.hasCodexClientCompaction) {
|
|
619
633
|
config.codexClientCompaction = previousSettings.codexClientCompaction;
|
|
620
634
|
} else deleteConfigTopLevelKey(config, "codexClientCompaction");
|
|
635
|
+
restoreCompactionRouting();
|
|
621
636
|
throw error;
|
|
622
637
|
}
|
|
623
638
|
if (typeof body.appOwnedMemoryBudgetMb === "number") {
|
|
@@ -669,6 +684,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
669
684
|
codexDesktopAuthless: authlessIsEnabled,
|
|
670
685
|
codexClientCompaction: clientCompactionIsEnabled,
|
|
671
686
|
codexDesktopSwitches,
|
|
687
|
+
compactionRouting: config.compactionRouting ?? null,
|
|
672
688
|
codexMainAccountHardLock: config.codexMainAccountHardLock === true,
|
|
673
689
|
mainAccountHardLock: getMainAccountHardLockStatus(config),
|
|
674
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. */
|