@yansigit/opencodex 2.36.1-dev.20260829.42 → 2.36.1-dev.20260829.48
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/{ApiKeys-C_cz6Rww.js → ApiKeys-LOOFiZfv.js} +1 -1
- package/gui/dist/assets/{Claude-ofGguLfF.js → Claude-BwKzpXe3.js} +1 -1
- package/gui/dist/assets/{CodexSet-fhGiVwxe.js → CodexSet-B5qi9KeE.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-B-dI69Kn.js → FileIntegrationPage-B0sUef6W.js} +1 -1
- package/gui/dist/assets/{Grok-CgkBu_Sm.js → Grok-bsb4n-f4.js} +1 -1
- package/gui/dist/assets/{Integrations-CTWMNzkD.js → Integrations-CSMwFDvM.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-BKZ0dZnj.js → IntegrationsOverview-CTORdKJA.js} +1 -1
- package/gui/dist/assets/{Logs-6r90C0x4.js → Logs-BWYbfELf.js} +1 -1
- package/gui/dist/assets/{Models-CpcS1zZr.js → Models-CexEtdT1.js} +1 -1
- package/gui/dist/assets/{NumberStepper-C70ttiJY.js → NumberStepper-BapSFQnW.js} +1 -1
- package/gui/dist/assets/{Providers-BROTzuSF.js → Providers-BwUFeAgA.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-DR5vgFXB.js → RestoreDialog-CD8piq90.js} +1 -1
- package/gui/dist/assets/{Startup-BSoZDYbS.js → Startup-BgX731C2.js} +1 -1
- package/gui/dist/assets/{Storage-CUvBwbeB.js → Storage-Bgq7HphP.js} +1 -1
- package/gui/dist/assets/{Subagents-CYu3_DBt.js → Subagents-ChxItbeF.js} +1 -1
- package/gui/dist/assets/{Usage-BV36sd7V.js → Usage-BkylzP50.js} +1 -1
- package/gui/dist/assets/{data-surface-DhcMe0J_.js → data-surface-pCXChiBf.js} +1 -1
- package/gui/dist/assets/{index-DnuNKc0D.js → index-CNopOid3.js} +2 -2
- package/gui/dist/assets/{model-display-BPrQ7F3P.js → model-display-CiUpg8T9.js} +1 -1
- package/gui/dist/assets/{provider-payload-CMb37zWF.js → provider-payload-b2jlS-On.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/cli/agent.ts +19 -2
- package/src/cli/config-command.ts +7 -3
- package/src/cli/init.ts +2 -2
- package/src/cli/provider.ts +58 -74
- package/src/cli/registry.ts +1 -1
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence.ts +110 -15
- package/src/codex/inject.ts +2 -0
- package/src/codex/subagent-defaults.ts +1 -8
- package/src/codex/subagent-model-authority.ts +189 -0
- package/src/config.ts +209 -11
- package/src/generated/compatibility-version.json +35 -27
- package/src/oauth/index.ts +143 -82
- package/src/oauth/login-cli.ts +53 -11
- package/src/providers/alibaba-region-startup.ts +31 -11
- package/src/providers/api-keys.ts +58 -34
- package/src/providers/key-failover.ts +59 -46
- package/src/providers/model-rename-startup.ts +25 -7
- package/src/providers/openai-tier-startup.ts +38 -14
- package/src/server/index.ts +10 -0
- package/src/server/management/agent-settings-routes.ts +12 -0
- package/src/server/management/context.ts +10 -3
- package/src/server/management/logs-usage-routes.ts +19 -16
- package/src/server/management/model-routes.ts +284 -165
- package/src/server/management/oauth-account-routes.ts +93 -15
- package/src/server/management/provider-routes.ts +200 -87
- package/src/server/responses/collaboration.ts +1 -1
- package/src/storage/policy-input.ts +166 -0
- package/src/storage/policy.ts +59 -211
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Modelled after src/codex/routing.ts cooldown logic but scoped to plain API-key pools.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { mutatePersistedConfig } from "../config";
|
|
12
12
|
import { isAzureIdentityProvider } from "../config/provider-validation";
|
|
13
|
+
import { routedProviderConfig } from "../router";
|
|
13
14
|
import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy } from "../types";
|
|
14
15
|
import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
|
|
15
16
|
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
@@ -159,50 +160,66 @@ export function rotateKeyOn429(
|
|
|
159
160
|
if (isAzureIdentityProvider(provider)) return null;
|
|
160
161
|
if (provider.authMode === "oauth" || provider.authMode === "forward") return null;
|
|
161
162
|
|
|
162
|
-
const pool = provider.apiKeyPool;
|
|
163
|
-
if (!pool || pool.length < 2) return null;
|
|
164
|
-
|
|
165
|
-
// Cool the key that ACTUALLY failed. Under concurrent 429s another request may already have
|
|
166
|
-
// rotated provider.apiKey — cooling the live key would punish an innocent replacement and can
|
|
167
|
-
// exhaust a 2-key pool from a single bad key. CAS semantics: callers pass the key they used.
|
|
168
163
|
const failedKey = attemptedKey ?? provider.apiKey;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
164
|
+
type Rotation =
|
|
165
|
+
| { provider: OcxProviderConfig; failedId?: string; candidateId?: string }
|
|
166
|
+
| { exhaustedCount: number };
|
|
167
|
+
const outcome = mutatePersistedConfig<Rotation | null>(fresh => {
|
|
168
|
+
const freshProvider = fresh.providers[providerName];
|
|
169
|
+
if (!freshProvider || isAzureIdentityProvider(freshProvider)
|
|
170
|
+
|| freshProvider.authMode === "oauth" || freshProvider.authMode === "forward") {
|
|
171
|
+
return { changed: false, value: null };
|
|
172
|
+
}
|
|
173
|
+
const pool = freshProvider.apiKeyPool;
|
|
174
|
+
if (!pool || pool.length < 2) return { changed: false, value: null };
|
|
177
175
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
176
|
+
// Cool the key that actually failed. The callback is rerun after rebasing,
|
|
177
|
+
// so both the id and the active-key comparison come from the commit preimage.
|
|
178
|
+
const failedEntry = pool.find(entry => entry.key === failedKey);
|
|
179
|
+
if (failedEntry) {
|
|
180
|
+
const cooldownMs = parseRetryAfterMs(retryAfterHeader, now) ?? DEFAULT_COOLDOWN_MS;
|
|
181
|
+
keyCooldowns.set(cooldownKey(providerName, failedEntry.id), { cooldownUntil: now + cooldownMs });
|
|
182
|
+
sweepExpiredOnWrite(now);
|
|
184
183
|
}
|
|
185
|
-
}
|
|
186
184
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
console.warn(
|
|
196
|
-
// Log ids only — labels are user-supplied free text and could carry secret material.
|
|
197
|
-
`[key-failover] ${providerName}: 429 on key ${currentEntry?.id ?? "?"}; rotating to key ${candidate.id}`,
|
|
198
|
-
);
|
|
199
|
-
return { ...provider };
|
|
185
|
+
if (freshProvider.apiKey !== failedKey) {
|
|
186
|
+
const activeEntry = pool.find(entry => entry.key === freshProvider.apiKey);
|
|
187
|
+
if (activeEntry && !isKeyInCooldown(providerName, activeEntry.id, now)) {
|
|
188
|
+
return {
|
|
189
|
+
changed: false,
|
|
190
|
+
value: { provider: structuredClone(freshProvider), failedId: failedEntry?.id, candidateId: undefined },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
200
193
|
}
|
|
194
|
+
|
|
195
|
+
const currentIndex = failedEntry ? pool.indexOf(failedEntry) : -1;
|
|
196
|
+
const candidateCount = failedEntry ? pool.length - 1 : pool.length;
|
|
197
|
+
for (let offset = 1; offset <= candidateCount; offset += 1) {
|
|
198
|
+
const candidate = pool[(currentIndex + offset) % pool.length]!;
|
|
199
|
+
if (isKeyInCooldown(providerName, candidate.id, now)) continue;
|
|
200
|
+
freshProvider.apiKey = candidate.key;
|
|
201
|
+
return {
|
|
202
|
+
changed: true,
|
|
203
|
+
value: { provider: structuredClone(freshProvider), failedId: failedEntry?.id, candidateId: candidate.id },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return { changed: false, value: { exhaustedCount: pool.length } };
|
|
207
|
+
});
|
|
208
|
+
if (outcome.status === "unavailable" || outcome.value === null) return null;
|
|
209
|
+
if ("exhaustedCount" in outcome.value) {
|
|
210
|
+
console.warn(`[key-failover] ${providerName}: all ${outcome.value.exhaustedCount} keys in cooldown; returning 429 to client`);
|
|
211
|
+
return null;
|
|
201
212
|
}
|
|
202
213
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
214
|
+
const committed = structuredClone(outcome.value.provider);
|
|
215
|
+
config.providers[providerName] = committed;
|
|
216
|
+
if (outcome.value.candidateId) {
|
|
217
|
+
console.warn(
|
|
218
|
+
// Log ids only — labels are user-supplied free text and could carry secret material.
|
|
219
|
+
`[key-failover] ${providerName}: 429 on key ${outcome.value.failedId ?? "?"}; rotating to key ${outcome.value.candidateId}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return structuredClone(committed);
|
|
206
223
|
}
|
|
207
224
|
|
|
208
225
|
export function sweepExpiredApiKeyCooldowns(now = Date.now()): number {
|
|
@@ -225,13 +242,9 @@ interface RotateProviderTransportOptions {
|
|
|
225
242
|
/**
|
|
226
243
|
* Rotate a failed key and re-apply provider-specific transport metadata to the replacement.
|
|
227
244
|
*
|
|
228
|
-
*
|
|
229
|
-
* route
|
|
230
|
-
*
|
|
231
|
-
* provider from that snapshot would silently drop every field the registry merged in at
|
|
232
|
-
* routing time (scalar flags like `promptCacheKey`/`parallelToolCalls`, merged model
|
|
233
|
-
* metadata such as `noTemperatureModels`, a pinned baseUrl). Mirrors the OAuth-401 replay
|
|
234
|
-
* path in src/server/responses/core.ts, which spreads `route.provider` for the same reason.
|
|
245
|
+
* Put the authoritative committed row over request-only fields and registry backfills, then
|
|
246
|
+
* route it again so concurrent provider edits take effect without losing either kind of runtime
|
|
247
|
+
* metadata.
|
|
235
248
|
*/
|
|
236
249
|
export function rotateProviderTransportOn429(
|
|
237
250
|
config: OcxConfig,
|
|
@@ -249,7 +262,7 @@ export function rotateProviderTransportOn429(
|
|
|
249
262
|
return rotated
|
|
250
263
|
? resolveProviderTransport(
|
|
251
264
|
providerName,
|
|
252
|
-
{ ...routedProvider,
|
|
265
|
+
routedProviderConfig(providerName, { ...routedProvider, ...rotated }),
|
|
253
266
|
options.promptCacheKey,
|
|
254
267
|
)
|
|
255
268
|
: null;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mutatePersistedConfig } from "../config";
|
|
2
2
|
import { projectModelRenames } from "./model-rename-migration";
|
|
3
3
|
import type { OcxConfig } from "../types";
|
|
4
4
|
|
|
5
5
|
export interface ModelRenameStartupDeps {
|
|
6
6
|
project: typeof projectModelRenames;
|
|
7
|
-
save
|
|
7
|
+
save?: (config: OcxConfig) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function adoptConfig(target: OcxConfig, source: OcxConfig): void {
|
|
11
|
+
if (target === source) return;
|
|
12
|
+
for (const key of Object.keys(target)) delete (target as unknown as Record<string, unknown>)[key];
|
|
13
|
+
Object.assign(target, structuredClone(source));
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
/**
|
|
@@ -18,11 +24,23 @@ export interface ModelRenameStartupDeps {
|
|
|
18
24
|
*/
|
|
19
25
|
export function runModelRenameStartupMigration(
|
|
20
26
|
config: OcxConfig,
|
|
21
|
-
deps: ModelRenameStartupDeps = { project: projectModelRenames
|
|
27
|
+
deps: ModelRenameStartupDeps = { project: projectModelRenames },
|
|
22
28
|
): OcxConfig {
|
|
23
|
-
const projection = deps.project(config);
|
|
24
|
-
for (const warning of projection.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
29
|
+
const projection = deps.project(structuredClone(config));
|
|
25
30
|
if (!projection.changed) return projection.config;
|
|
26
|
-
deps.save
|
|
27
|
-
|
|
31
|
+
if (deps.save) {
|
|
32
|
+
deps.save(projection.config);
|
|
33
|
+
adoptConfig(config, projection.config);
|
|
34
|
+
for (const warning of projection.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
const outcome = mutatePersistedConfig(fresh => {
|
|
38
|
+
const next = deps.project(fresh);
|
|
39
|
+
if (next.changed) adoptConfig(fresh, next.config);
|
|
40
|
+
return { changed: next.changed, value: next };
|
|
41
|
+
});
|
|
42
|
+
if (outcome.status === "unavailable") return config;
|
|
43
|
+
adoptConfig(config, outcome.value.config);
|
|
44
|
+
for (const warning of outcome.value.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
45
|
+
return config;
|
|
28
46
|
}
|
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
backupConfigBeforeOpenAiTierMigration,
|
|
3
3
|
OpenAiTierBackupCollisionError,
|
|
4
4
|
OpenAiTierRollbackPreserveError,
|
|
5
|
+
mutatePersistedConfig,
|
|
5
6
|
preserveOpenAiTierRollbackSnapshot,
|
|
6
|
-
saveConfig,
|
|
7
7
|
} from "../config";
|
|
8
8
|
import type { OcxConfig } from "../types";
|
|
9
9
|
import { projectOpenAiTierMigration } from "./openai-tiers";
|
|
@@ -11,7 +11,7 @@ import { projectOpenAiTierMigration } from "./openai-tiers";
|
|
|
11
11
|
export interface OpenAiTierStartupDeps {
|
|
12
12
|
project: typeof projectOpenAiTierMigration;
|
|
13
13
|
backup: () => void;
|
|
14
|
-
save
|
|
14
|
+
save?: (config: OcxConfig) => void;
|
|
15
15
|
preserveRollback?: (error: OpenAiTierBackupCollisionError) => void;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -34,23 +34,47 @@ function defaultPreserveRollback(error: OpenAiTierBackupCollisionError): void {
|
|
|
34
34
|
const DEFAULT_DEPS: OpenAiTierStartupDeps = {
|
|
35
35
|
project: projectOpenAiTierMigration,
|
|
36
36
|
backup: backupConfigBeforeOpenAiTierMigration,
|
|
37
|
-
save: saveConfig,
|
|
38
37
|
};
|
|
39
38
|
|
|
40
39
|
export function runOpenAiTierStartupMigration(
|
|
41
40
|
config: OcxConfig,
|
|
42
41
|
deps: OpenAiTierStartupDeps = DEFAULT_DEPS,
|
|
43
42
|
): OcxConfig {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
const backup = (): void => {
|
|
44
|
+
try {
|
|
45
|
+
deps.backup();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!(error instanceof OpenAiTierBackupCollisionError)) throw error;
|
|
48
|
+
(deps.preserveRollback ?? defaultPreserveRollback)(error);
|
|
49
|
+
deps.backup();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
if (deps.save) {
|
|
53
|
+
const projection = deps.project(config);
|
|
54
|
+
if (!projection.changed) return projection.config;
|
|
55
|
+
backup();
|
|
56
|
+
deps.save(projection.config);
|
|
57
|
+
for (const warning of projection.warnings) console.warn(`[openai-provider-migration] ${warning}`);
|
|
58
|
+
return projection.config;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let previousSnapshot: string | undefined;
|
|
62
|
+
const outcome = mutatePersistedConfig(fresh => {
|
|
63
|
+
const snapshot = JSON.stringify(fresh);
|
|
64
|
+
const next = deps.project(fresh);
|
|
65
|
+
// mutatePersistedConfig confirms a candidate by invoking us again with the same
|
|
66
|
+
// snapshot after rebasing. Back up only that confirmed preimage.
|
|
67
|
+
if (next.changed && snapshot === previousSnapshot) backup();
|
|
68
|
+
previousSnapshot = snapshot;
|
|
69
|
+
if (next.changed) {
|
|
70
|
+
for (const key of Object.keys(fresh)) delete (fresh as unknown as Record<string, unknown>)[key];
|
|
71
|
+
Object.assign(fresh, next.config);
|
|
72
|
+
}
|
|
73
|
+
return { changed: next.changed, value: next };
|
|
74
|
+
});
|
|
75
|
+
if (outcome.status === "unavailable") return config;
|
|
76
|
+
if (outcome.status === "committed") {
|
|
77
|
+
for (const warning of outcome.value.warnings) console.warn(`[openai-provider-migration] ${warning}`);
|
|
52
78
|
}
|
|
53
|
-
|
|
54
|
-
for (const warning of projection.warnings) console.warn(`[openai-provider-migration] ${warning}`);
|
|
55
|
-
return projection.config;
|
|
79
|
+
return outcome.value.config;
|
|
56
80
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -38,6 +38,11 @@ import {
|
|
|
38
38
|
setLiveStateStoreConfig,
|
|
39
39
|
} from "../lib/state-store-registrations";
|
|
40
40
|
import { startUserCostOverlayReconciler } from "../usage/user-cost-overlay-reconciler";
|
|
41
|
+
import {
|
|
42
|
+
getStorageCleanupPolicyJobState,
|
|
43
|
+
getStorageCleanupPolicyTestStreamResponse,
|
|
44
|
+
requestStorageCleanupPolicyRun,
|
|
45
|
+
} from "../storage/policy-job";
|
|
41
46
|
import {
|
|
42
47
|
configureAppOwnedMemoryBudget,
|
|
43
48
|
enforceAppOwnedMemoryBudget,
|
|
@@ -537,6 +542,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
537
542
|
const managementApi: ManagementApiDeps = {
|
|
538
543
|
saveConfigPreservingClaudeCode,
|
|
539
544
|
mutatePersistedConfig,
|
|
545
|
+
storageCleanupPolicyJob: {
|
|
546
|
+
getState: getStorageCleanupPolicyJobState,
|
|
547
|
+
getTestStream: getStorageCleanupPolicyTestStreamResponse,
|
|
548
|
+
requestRun: requestStorageCleanupPolicyRun,
|
|
549
|
+
},
|
|
540
550
|
...deps.managementApi,
|
|
541
551
|
};
|
|
542
552
|
const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
|
|
@@ -792,6 +792,18 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
792
792
|
// dynamically injected into the v1 proactive prompt, plus an optional reasoning
|
|
793
793
|
// effort the prompt tells the agent to pass to spawn_agent. GET returns the current
|
|
794
794
|
// picks + available models/efforts; PUT sets or clears them.
|
|
795
|
+
if (url.pathname === "/api/subagent-model-authority" && req.method === "POST") {
|
|
796
|
+
let body: unknown;
|
|
797
|
+
try { body = await readManagementJsonBody(req); } catch (error) {
|
|
798
|
+
rethrowManagementBodyTooLarge(error);
|
|
799
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
800
|
+
}
|
|
801
|
+
const { parseSubagentModelAuthorityInput, resolveOpenCodexSubagentModelAuthority } = await import("../../codex/subagent-model-authority");
|
|
802
|
+
const input = parseSubagentModelAuthorityInput(body);
|
|
803
|
+
if (!input) return jsonResponse({ error: "invalid subagent model authority input" }, 400);
|
|
804
|
+
return jsonResponse(await resolveOpenCodexSubagentModelAuthority(input, config));
|
|
805
|
+
}
|
|
806
|
+
|
|
795
807
|
if (url.pathname === "/api/injection-model" && req.method === "GET") {
|
|
796
808
|
const models = await fetchAllModels(config);
|
|
797
809
|
const disabled = new Set(config.disabledModels ?? []);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxConfig } from "../../types";
|
|
2
|
+
import type { PersistedConfigMutation, PersistedConfigMutationOutcome } from "../../config";
|
|
2
3
|
import type { NativeProfileApiDeps } from "../../codex/native-profile-api";
|
|
3
4
|
import type { CodexLogGuardProtectionDeps } from "../../codex/log-guard/protection";
|
|
4
5
|
import type { CodexLogGuardMaintenanceDeps } from "../../codex/log-guard/maintenance";
|
|
@@ -40,6 +41,12 @@ export interface ManagementApiDeps {
|
|
|
40
41
|
saveConfigPreservingClaudeCode?: (config: OcxConfig) => void;
|
|
41
42
|
/** Config-mutation seam for routes that commit through `mutatePersistedConfig`. */
|
|
42
43
|
mutatePersistedConfig?: typeof import("../../config").mutatePersistedConfig;
|
|
44
|
+
/** Storage-policy job seam keeps management routes off persistence-bearing worker modules. */
|
|
45
|
+
storageCleanupPolicyJob?: {
|
|
46
|
+
getState: typeof import("../../storage/policy-job").getStorageCleanupPolicyJobState;
|
|
47
|
+
getTestStream: typeof import("../../storage/policy-job").getStorageCleanupPolicyTestStreamResponse;
|
|
48
|
+
requestRun: typeof import("../../storage/policy-job").requestStorageCleanupPolicyRun;
|
|
49
|
+
};
|
|
43
50
|
/** Test-only fetch injection for Replit gateway install probes. */
|
|
44
51
|
probeFetch?: typeof globalThis.fetch;
|
|
45
52
|
/**
|
|
@@ -142,11 +149,11 @@ export function saveManagementConfig(deps: ManagementApiDeps, config: OcxConfig)
|
|
|
142
149
|
/** The only locked, field-scoped on-disk mutation boundary available to management routes. */
|
|
143
150
|
export function mutateManagementConfig<T>(
|
|
144
151
|
deps: ManagementApiDeps,
|
|
145
|
-
mutate:
|
|
146
|
-
):
|
|
152
|
+
mutate: (config: OcxConfig) => PersistedConfigMutation<T>,
|
|
153
|
+
): PersistedConfigMutationOutcome<T> {
|
|
147
154
|
if (!deps.mutatePersistedConfig) throw new MissingManagementPersistenceError();
|
|
148
155
|
try {
|
|
149
|
-
return deps.mutatePersistedConfig(mutate)
|
|
156
|
+
return deps.mutatePersistedConfig(mutate);
|
|
150
157
|
} catch (error) {
|
|
151
158
|
throw new ManagementPersistenceError(error);
|
|
152
159
|
}
|
|
@@ -39,13 +39,7 @@ import { getRestoreTrashTestStreamResponse, runRestoreTrashEntryJob } from "../.
|
|
|
39
39
|
import {
|
|
40
40
|
normalizeStorageCleanupPolicy,
|
|
41
41
|
parseStorageCleanupPolicyInput,
|
|
42
|
-
|
|
43
|
-
} from "../../storage/policy";
|
|
44
|
-
import {
|
|
45
|
-
getStorageCleanupPolicyJobState,
|
|
46
|
-
getStorageCleanupPolicyTestStreamResponse,
|
|
47
|
-
requestStorageCleanupPolicyRun,
|
|
48
|
-
} from "../../storage/policy-job";
|
|
42
|
+
} from "../../storage/policy-input";
|
|
49
43
|
import {
|
|
50
44
|
currentUsageLogRevision,
|
|
51
45
|
readUsageSnapshotForManagement,
|
|
@@ -77,7 +71,7 @@ import { applySystemEnvToggle } from "../system-env";
|
|
|
77
71
|
|
|
78
72
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
79
73
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
80
|
-
import type
|
|
74
|
+
import { MissingManagementPersistenceError, mutateManagementConfig, type ManagementContext } from "./context";
|
|
81
75
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
82
76
|
import {
|
|
83
77
|
discardUsageSummaryCacheEntry,
|
|
@@ -131,6 +125,7 @@ function snapshotWindow(entries: PersistedUsageEntry[]): { start: number | null;
|
|
|
131
125
|
|
|
132
126
|
export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
133
127
|
const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
|
|
128
|
+
const storagePolicyJobState = () => deps.storageCleanupPolicyJob?.getState() ?? { status: "idle" as const };
|
|
134
129
|
|
|
135
130
|
if (url.pathname === "/api/logs" && req.method === "GET") {
|
|
136
131
|
const all = getRequestLogEntries();
|
|
@@ -555,7 +550,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
555
550
|
}
|
|
556
551
|
|
|
557
552
|
if (url.pathname === "/api/storage/cleanup-policy/test-stream" && req.method === "GET") {
|
|
558
|
-
const stream =
|
|
553
|
+
const stream = deps.storageCleanupPolicyJob?.getTestStream();
|
|
559
554
|
if (stream) return stream;
|
|
560
555
|
// Production: hook is off. Return an explicit JSON 404 — do not fall through to the GUI.
|
|
561
556
|
return jsonResponse({ error: "not_found" }, 404);
|
|
@@ -565,7 +560,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
565
560
|
const policy = normalizeStorageCleanupPolicy(config.storageCleanupPolicy);
|
|
566
561
|
return jsonResponse({
|
|
567
562
|
...policy,
|
|
568
|
-
job:
|
|
563
|
+
job: storagePolicyJobState(),
|
|
569
564
|
});
|
|
570
565
|
}
|
|
571
566
|
|
|
@@ -575,17 +570,25 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
575
570
|
const previous = normalizeStorageCleanupPolicy(config.storageCleanupPolicy);
|
|
576
571
|
const parsed = parseStorageCleanupPolicyInput(raw, previous);
|
|
577
572
|
if (!parsed.ok) return jsonResponse({ error: parsed.error }, 400);
|
|
578
|
-
// Never enable implicitly: if client omitted enabled, keep previous (default false).
|
|
579
573
|
const body = raw as Record<string, unknown>;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
574
|
+
const persisted = mutateManagementConfig(deps, disk => {
|
|
575
|
+
const latest = normalizeStorageCleanupPolicy(disk.storageCleanupPolicy);
|
|
576
|
+
const rebased = parseStorageCleanupPolicyInput(raw, latest);
|
|
577
|
+
if (!rebased.ok) throw new Error(rebased.error);
|
|
578
|
+
// Never enable implicitly: if client omitted enabled, keep the latest persisted value.
|
|
579
|
+
if (body.enabled === undefined) rebased.policy.enabled = latest.enabled;
|
|
580
|
+
disk.storageCleanupPolicy = rebased.policy;
|
|
581
|
+
return { changed: true, value: structuredClone(rebased.policy) };
|
|
582
|
+
});
|
|
583
|
+
if (persisted.status === "unavailable") return jsonResponse({ error: "management persistence unavailable" }, 500);
|
|
584
|
+
config.storageCleanupPolicy = persisted.value;
|
|
585
|
+
return jsonResponse({ ok: true, policy: persisted.value, job: storagePolicyJobState() });
|
|
584
586
|
}
|
|
585
587
|
|
|
586
588
|
if (url.pathname === "/api/storage/cleanup-policy/run" && req.method === "POST") {
|
|
587
589
|
try {
|
|
588
|
-
|
|
590
|
+
if (!deps.storageCleanupPolicyJob) throw new MissingManagementPersistenceError();
|
|
591
|
+
const accepted = deps.storageCleanupPolicyJob.requestRun({ reason: "manual", force: true });
|
|
589
592
|
if (!accepted.accepted) {
|
|
590
593
|
return jsonResponse({
|
|
591
594
|
ok: false,
|