@yansigit/opencodex 2.36.1-dev.20260829.47 → 2.36.1-dev.20260829.50
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-DAuYQsGJ.js → ApiKeys-BLPOvK4z.js} +1 -1
- package/gui/dist/assets/{Claude-bN_80RT8.js → Claude-BbHv3EU7.js} +1 -1
- package/gui/dist/assets/{CodexSet-D3dWX_ay.js → CodexSet-hgu5hES-.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-BGyzkPs2.js → FileIntegrationPage-RCmDvvsx.js} +1 -1
- package/gui/dist/assets/{Grok-CuaKd1TT.js → Grok-DH-ggRqA.js} +1 -1
- package/gui/dist/assets/{Integrations-C7NGQoxR.js → Integrations-D6m5E2-T.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-DjVorYO5.js → IntegrationsOverview-C1rbnXV7.js} +1 -1
- package/gui/dist/assets/{Logs-B21a-9xe.js → Logs-Bqb0vYRk.js} +1 -1
- package/gui/dist/assets/{Models-CN7Q3kuK.js → Models-gMnzHf0c.js} +1 -1
- package/gui/dist/assets/{NumberStepper-BWg90tEs.js → NumberStepper-awaoZNlj.js} +1 -1
- package/gui/dist/assets/{Providers-CP_i-7LS.js → Providers-Bo6wuQMa.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-3M0NqCiD.js → RestoreDialog-DX7SGWIk.js} +1 -1
- package/gui/dist/assets/{Startup-BVPFoP2s.js → Startup-B1htiLU9.js} +1 -1
- package/gui/dist/assets/{Storage-BdsG9ypz.js → Storage-B3MfosmS.js} +1 -1
- package/gui/dist/assets/{Subagents-CguR_bGQ.js → Subagents-L6Q2V9PZ.js} +1 -1
- package/gui/dist/assets/{Usage-C0kKi9_H.js → Usage-xbZnkDoj.js} +1 -1
- package/gui/dist/assets/{codex-stale-banner-D4gvSWnO.js → codex-stale-banner-W4FhikN7.js} +1 -1
- package/gui/dist/assets/{data-surface-BtO3lwam.js → data-surface-B8PX8rjm.js} +1 -1
- package/gui/dist/assets/{data-surface-BCycbObp.js → data-surface-BbBMp0Wj.js} +1 -1
- package/gui/dist/assets/{index-CU1jE0st.js → index-JEC_6CUW.js} +3 -3
- package/gui/dist/assets/{model-display-D2elXkuE.js → model-display-BvLWy9D5.js} +1 -1
- package/gui/dist/assets/{provider-payload-DfkXWE7v.js → provider-payload-BRfSsonS.js} +1 -1
- package/gui/dist/assets/{section-tabs-BP7gEPK6.js → section-tabs-B8sLMiz6.js} +1 -1
- package/gui/dist/assets/shared-DI18-uF7.js +69 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/base.ts +26 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +34 -41
- package/src/adapters/cursor/envelope-echo.ts +128 -0
- package/src/adapters/cursor/request-builder.ts +19 -12
- package/src/adapters/cursor/tool-definitions.ts +2 -1
- package/src/adapters/cursor/tool-result-normalize.ts +23 -31
- package/src/adapters/cursor.ts +21 -2
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +71 -2
- package/src/adapters/google.ts +19 -4
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro.ts +128 -11
- package/src/adapters/openai-chat.ts +16 -2
- package/src/adapters/openai-responses.ts +15 -2
- package/src/adapters/tool-catalog-nudge.ts +2 -1
- package/src/adapters/xai-web-search.ts +10 -14
- package/src/claude/outbound.ts +14 -3
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +93 -16
- package/src/cli/account-extended.ts +262 -36
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +40 -10
- package/src/cli/agent.ts +8 -1
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +535 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/config-command.ts +7 -3
- package/src/cli/dispatch.ts +195 -27
- package/src/cli/doctor.ts +100 -1
- package/src/cli/help.ts +11 -2
- package/src/cli/index.ts +19 -3
- package/src/cli/init.ts +2 -2
- package/src/cli/inspect.ts +230 -0
- package/src/cli/observe.ts +11 -3
- package/src/cli/provider.ts +58 -74
- package/src/cli/registry.ts +34 -2
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +16 -0
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/usage-report.ts +52 -2
- package/src/cli/version-skew.ts +46 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/catalog/provider-fetch.ts +4 -0
- package/src/codex/convergence.ts +110 -15
- package/src/codex/transition-state.ts +12 -3
- package/src/compatibility/openai-responses.ts +9 -1
- package/src/config.ts +209 -11
- package/src/generated/compatibility-version.json +115 -75
- package/src/integrations/ownership-policy.ts +24 -5
- package/src/integrations/ownership.ts +36 -2
- package/src/integrations/state.ts +40 -7
- package/src/integrations/writer.ts +21 -3
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/errors.ts +25 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/oauth/index.ts +143 -82
- package/src/oauth/login-cli.ts +53 -11
- package/src/oauth/store.ts +16 -6
- 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/label.ts +34 -1
- package/src/providers/model-rename-startup.ts +25 -7
- package/src/providers/openai-tier-startup.ts +38 -14
- package/src/responses/turn-termination.ts +107 -0
- package/src/server/index.ts +10 -0
- package/src/server/management/context.ts +10 -3
- package/src/server/management/logs-usage-routes.ts +24 -43
- 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/management/route-registry.ts +317 -0
- package/src/server/proxy-liveness.ts +27 -4
- package/src/server/request-log.ts +37 -17
- package/src/server/responses/core.ts +80 -4
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/service.ts +34 -0
- package/src/storage/policy-input.ts +166 -0
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +115 -221
- package/src/usage/log.ts +32 -6
- package/src/usage/summary.ts +22 -34
- package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { OcxAssistantMessage, OcxMessage, OcxParsedRequest } from "../types";
|
|
3
|
+
|
|
4
|
+
const DELIVERED_FINAL_ANSWER_TTL_MS = 60 * 60 * 1_000;
|
|
5
|
+
const DELIVERED_FINAL_ANSWER_MAX_ENTRIES = 1_024;
|
|
6
|
+
|
|
7
|
+
interface DeliveredFinalAnswerRecord {
|
|
8
|
+
fingerprint: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const scopesByRequest = new WeakMap<OcxParsedRequest, string>();
|
|
13
|
+
const deliveredFinalAnswers = new Map<string, DeliveredFinalAnswerRecord>();
|
|
14
|
+
|
|
15
|
+
function pruneDeliveredFinalAnswers(at = Date.now()): void {
|
|
16
|
+
for (const [scope, record] of deliveredFinalAnswers) {
|
|
17
|
+
if (at - record.createdAt > DELIVERED_FINAL_ANSWER_TTL_MS) deliveredFinalAnswers.delete(scope);
|
|
18
|
+
}
|
|
19
|
+
while (deliveredFinalAnswers.size > DELIVERED_FINAL_ANSWER_MAX_ENTRIES) {
|
|
20
|
+
const oldest = deliveredFinalAnswers.keys().next().value;
|
|
21
|
+
if (oldest === undefined) break;
|
|
22
|
+
deliveredFinalAnswers.delete(oldest);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function textFingerprint(text: string): string {
|
|
27
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assistantText(message: OcxAssistantMessage): string | undefined {
|
|
31
|
+
if (message.content.some(part => part.type === "toolCall")) return undefined;
|
|
32
|
+
const text = message.content
|
|
33
|
+
.filter((part): part is Extract<typeof part, { type: "text" }> => part.type === "text")
|
|
34
|
+
.map(part => part.text)
|
|
35
|
+
.join("");
|
|
36
|
+
return text.trim().length > 0 ? text : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function deliveredFinalAnswerText(response: unknown): string | undefined {
|
|
40
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) return undefined;
|
|
41
|
+
const output = (response as { output?: unknown }).output;
|
|
42
|
+
if (!Array.isArray(output)) return undefined;
|
|
43
|
+
for (let i = output.length - 1; i >= 0; i--) {
|
|
44
|
+
const item = output[i];
|
|
45
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
46
|
+
const message = item as { type?: unknown; role?: unknown; phase?: unknown; content?: unknown };
|
|
47
|
+
if (message.type !== "message" || message.role !== "assistant" || message.phase !== "final_answer") continue;
|
|
48
|
+
if (!Array.isArray(message.content)) return undefined;
|
|
49
|
+
const text = message.content
|
|
50
|
+
.filter(part => !!part && typeof part === "object" && !Array.isArray(part)
|
|
51
|
+
&& (part as { type?: unknown }).type === "output_text"
|
|
52
|
+
&& typeof (part as { text?: unknown }).text === "string")
|
|
53
|
+
.map(part => (part as { text: string }).text)
|
|
54
|
+
.join("");
|
|
55
|
+
return text.trim().length > 0 ? text : undefined;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Bind the normalized per-conversation digest without adding proxy-private fields to the wire body. */
|
|
61
|
+
export function bindTurnTerminationScope(parsed: OcxParsedRequest, scope: string | undefined): void {
|
|
62
|
+
// Only the normalized log-conversation digest may key this process-wide map. Refusing any raw
|
|
63
|
+
// fallback prevents a future caller from retaining a client header or account identifier here.
|
|
64
|
+
if (!scope || !/^[0-9a-f]{32}$/.test(scope)) return;
|
|
65
|
+
scopesByRequest.set(parsed, scope);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Remember only a final-answer message the proxy actually emitted for this exact conversation. */
|
|
69
|
+
export function rememberDeliveredFinalAnswer(parsed: OcxParsedRequest, response: unknown): void {
|
|
70
|
+
const scope = scopesByRequest.get(parsed);
|
|
71
|
+
if (!scope) return;
|
|
72
|
+
const text = deliveredFinalAnswerText(response);
|
|
73
|
+
if (!text) return;
|
|
74
|
+
const at = Date.now();
|
|
75
|
+
pruneDeliveredFinalAnswers(at);
|
|
76
|
+
// Refresh insertion order so cap eviction removes the least recently delivered conversation.
|
|
77
|
+
deliveredFinalAnswers.delete(scope);
|
|
78
|
+
deliveredFinalAnswers.set(scope, { fingerprint: textFingerprint(text), createdAt: at });
|
|
79
|
+
pruneDeliveredFinalAnswers(at);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Match only when the delivered assistant answer is still the trailing content-bearing message.
|
|
84
|
+
* A later user/tool-result message is new work and must reach the provider, even though every
|
|
85
|
+
* legitimate next turn necessarily contains such a message somewhere in its history.
|
|
86
|
+
*/
|
|
87
|
+
export function hasRecordedTrailingDeliveredFinalAnswer(
|
|
88
|
+
parsed: OcxParsedRequest,
|
|
89
|
+
messages: readonly OcxMessage[],
|
|
90
|
+
): boolean {
|
|
91
|
+
const scope = scopesByRequest.get(parsed);
|
|
92
|
+
if (!scope) return false;
|
|
93
|
+
pruneDeliveredFinalAnswers();
|
|
94
|
+
const record = deliveredFinalAnswers.get(scope);
|
|
95
|
+
if (!record) return false;
|
|
96
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
97
|
+
const message = messages[i];
|
|
98
|
+
if (message.role !== "assistant") return false;
|
|
99
|
+
const text = assistantText(message as OcxAssistantMessage);
|
|
100
|
+
if (text === undefined) {
|
|
101
|
+
if ((message as OcxAssistantMessage).content.some(part => part.type === "toolCall")) return false;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
return textFingerprint(text) === record.fingerprint;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
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();
|
|
@@ -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
|
}
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
submitManualLoginCode,
|
|
21
21
|
upsertOAuthProvider,
|
|
22
22
|
} from "../../oauth";
|
|
23
|
-
import {
|
|
23
|
+
import { removeCredential } from "../../oauth/store";
|
|
24
24
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
25
25
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
26
26
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
@@ -31,21 +31,13 @@ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
|
31
31
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
32
32
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
33
33
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
34
|
-
import { resolveCodexHomeDir } from "../../codex/home";
|
|
35
|
-
import { scanStorage } from "../../storage/scanner";
|
|
36
34
|
import { executeArchivedCleanup, listTrashEntries, pickWireCleanupTestHooks, previewArchivedCleanup, type CleanupMode, type RestoreErrorCode } from "../../storage/cleanup";
|
|
37
35
|
import { runArchivedCleanupJob } from "../../storage/cleanup-job";
|
|
38
36
|
import { getRestoreTrashTestStreamResponse, runRestoreTrashEntryJob } from "../../storage/restore-job";
|
|
39
37
|
import {
|
|
40
38
|
normalizeStorageCleanupPolicy,
|
|
41
39
|
parseStorageCleanupPolicyInput,
|
|
42
|
-
|
|
43
|
-
} from "../../storage/policy";
|
|
44
|
-
import {
|
|
45
|
-
getStorageCleanupPolicyJobState,
|
|
46
|
-
getStorageCleanupPolicyTestStreamResponse,
|
|
47
|
-
requestStorageCleanupPolicyRun,
|
|
48
|
-
} from "../../storage/policy-job";
|
|
40
|
+
} from "../../storage/policy-input";
|
|
49
41
|
import {
|
|
50
42
|
currentUsageLogRevision,
|
|
51
43
|
readUsageSnapshotForManagement,
|
|
@@ -77,7 +69,7 @@ import { applySystemEnvToggle } from "../system-env";
|
|
|
77
69
|
|
|
78
70
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
79
71
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
80
|
-
import type
|
|
72
|
+
import { MissingManagementPersistenceError, mutateManagementConfig, type ManagementContext } from "./context";
|
|
81
73
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
82
74
|
import {
|
|
83
75
|
discardUsageSummaryCacheEntry,
|
|
@@ -131,6 +123,7 @@ function snapshotWindow(entries: PersistedUsageEntry[]): { start: number | null;
|
|
|
131
123
|
|
|
132
124
|
export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
133
125
|
const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
|
|
126
|
+
const storagePolicyJobState = () => deps.storageCleanupPolicyJob?.getState() ?? { status: "idle" as const };
|
|
134
127
|
|
|
135
128
|
if (url.pathname === "/api/logs" && req.method === "GET") {
|
|
136
129
|
const all = getRequestLogEntries();
|
|
@@ -203,16 +196,10 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
203
196
|
const filter = {
|
|
204
197
|
provider: url.searchParams.get("provider"),
|
|
205
198
|
model: url.searchParams.get("model"),
|
|
206
|
-
account: url.searchParams.get("account"),
|
|
207
199
|
};
|
|
208
|
-
const fallbackAccounts: Record<string, string> = {};
|
|
209
|
-
for (const p of listOAuthProviders()) {
|
|
210
|
-
const set = getAccountSet(p);
|
|
211
|
-
if (set && set.accounts.length === 1 && set.activeAccountId) fallbackAccounts[p] = set.activeAccountId;
|
|
212
|
-
}
|
|
213
200
|
const project = <T extends UsageSummary>(summary: T, entries?: PersistedUsageEntry[]) =>
|
|
214
|
-
projectUsageSummary(summary, filter, entries
|
|
215
|
-
const filterRequested = Boolean(filter.provider ?? filter.model
|
|
201
|
+
projectUsageSummary(summary, filter, entries);
|
|
202
|
+
const filterRequested = Boolean(filter.provider ?? filter.model);
|
|
216
203
|
const now = Date.now();
|
|
217
204
|
try {
|
|
218
205
|
const cacheKey = `${range}:${surface}`;
|
|
@@ -247,7 +234,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
247
234
|
const revisionReadAt = Date.now();
|
|
248
235
|
const window = snapshotWindow(snapshot.entries);
|
|
249
236
|
const summary = {
|
|
250
|
-
...summarizeUsage(snapshot.entries, range, now, surface
|
|
237
|
+
...summarizeUsage(snapshot.entries, range, now, surface),
|
|
251
238
|
historyTruncated: snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
|
|
252
239
|
truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
|
|
253
240
|
entriesTruncated: snapshot.entriesTruncated,
|
|
@@ -275,7 +262,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
275
262
|
for (const nextRange of ranges) {
|
|
276
263
|
for (const nextSurface of surfaces) {
|
|
277
264
|
const nextSummary = nextRange === range && nextSurface === surface ? summary : {
|
|
278
|
-
...summarizeUsage(snapshot.entries, nextRange, now, nextSurface
|
|
265
|
+
...summarizeUsage(snapshot.entries, nextRange, now, nextSurface),
|
|
279
266
|
historyTruncated: summary.historyTruncated,
|
|
280
267
|
truncatedPrefixBytes: summary.truncatedPrefixBytes,
|
|
281
268
|
entriesTruncated: summary.entriesTruncated,
|
|
@@ -348,20 +335,6 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
348
335
|
}
|
|
349
336
|
}
|
|
350
337
|
|
|
351
|
-
if (url.pathname === "/api/storage" && req.method === "GET") {
|
|
352
|
-
try {
|
|
353
|
-
return jsonResponse(scanStorage());
|
|
354
|
-
} catch {
|
|
355
|
-
return jsonResponse({
|
|
356
|
-
codexHome: resolveCodexHomeDir(),
|
|
357
|
-
generatedAt: Date.now(),
|
|
358
|
-
total: { bytes: 0, fileCount: 0 },
|
|
359
|
-
buckets: [],
|
|
360
|
-
error: "scan_failed",
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
338
|
if (url.pathname === "/api/storage/cleanup/preview" && req.method === "POST") {
|
|
366
339
|
let body: { percent?: unknown };
|
|
367
340
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
|
|
@@ -555,7 +528,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
555
528
|
}
|
|
556
529
|
|
|
557
530
|
if (url.pathname === "/api/storage/cleanup-policy/test-stream" && req.method === "GET") {
|
|
558
|
-
const stream =
|
|
531
|
+
const stream = deps.storageCleanupPolicyJob?.getTestStream();
|
|
559
532
|
if (stream) return stream;
|
|
560
533
|
// Production: hook is off. Return an explicit JSON 404 — do not fall through to the GUI.
|
|
561
534
|
return jsonResponse({ error: "not_found" }, 404);
|
|
@@ -565,7 +538,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
565
538
|
const policy = normalizeStorageCleanupPolicy(config.storageCleanupPolicy);
|
|
566
539
|
return jsonResponse({
|
|
567
540
|
...policy,
|
|
568
|
-
job:
|
|
541
|
+
job: storagePolicyJobState(),
|
|
569
542
|
});
|
|
570
543
|
}
|
|
571
544
|
|
|
@@ -575,17 +548,25 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
575
548
|
const previous = normalizeStorageCleanupPolicy(config.storageCleanupPolicy);
|
|
576
549
|
const parsed = parseStorageCleanupPolicyInput(raw, previous);
|
|
577
550
|
if (!parsed.ok) return jsonResponse({ error: parsed.error }, 400);
|
|
578
|
-
// Never enable implicitly: if client omitted enabled, keep previous (default false).
|
|
579
551
|
const body = raw as Record<string, unknown>;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
552
|
+
const persisted = mutateManagementConfig(deps, disk => {
|
|
553
|
+
const latest = normalizeStorageCleanupPolicy(disk.storageCleanupPolicy);
|
|
554
|
+
const rebased = parseStorageCleanupPolicyInput(raw, latest);
|
|
555
|
+
if (!rebased.ok) throw new Error(rebased.error);
|
|
556
|
+
// Never enable implicitly: if client omitted enabled, keep the latest persisted value.
|
|
557
|
+
if (body.enabled === undefined) rebased.policy.enabled = latest.enabled;
|
|
558
|
+
disk.storageCleanupPolicy = rebased.policy;
|
|
559
|
+
return { changed: true, value: structuredClone(rebased.policy) };
|
|
560
|
+
});
|
|
561
|
+
if (persisted.status === "unavailable") return jsonResponse({ error: "management persistence unavailable" }, 500);
|
|
562
|
+
config.storageCleanupPolicy = persisted.value;
|
|
563
|
+
return jsonResponse({ ok: true, policy: persisted.value, job: storagePolicyJobState() });
|
|
584
564
|
}
|
|
585
565
|
|
|
586
566
|
if (url.pathname === "/api/storage/cleanup-policy/run" && req.method === "POST") {
|
|
587
567
|
try {
|
|
588
|
-
|
|
568
|
+
if (!deps.storageCleanupPolicyJob) throw new MissingManagementPersistenceError();
|
|
569
|
+
const accepted = deps.storageCleanupPolicyJob.requestRun({ reason: "manual", force: true });
|
|
589
570
|
if (!accepted.accepted) {
|
|
590
571
|
return jsonResponse({
|
|
591
572
|
ok: false,
|