@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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { StorageCleanupPolicy } from "../types";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_ARCHIVED_BYTES_OVER = 5 * 1024 ** 3;
|
|
4
|
+
export const DEFAULT_REMOVE_OLDEST_PERCENT = 25;
|
|
5
|
+
|
|
6
|
+
export type PolicySchedule = StorageCleanupPolicy["schedule"];
|
|
7
|
+
|
|
8
|
+
/** Canonical defaults — enabled is always false. */
|
|
9
|
+
export function defaultStorageCleanupPolicy(): StorageCleanupPolicy {
|
|
10
|
+
return {
|
|
11
|
+
enabled: false,
|
|
12
|
+
trigger: { archivedBytesOver: DEFAULT_ARCHIVED_BYTES_OVER },
|
|
13
|
+
target: { removeOldestPercent: DEFAULT_REMOVE_OLDEST_PERCENT },
|
|
14
|
+
schedule: "manual",
|
|
15
|
+
mode: "quarantine",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isFiniteNonNegInt(n: unknown): n is number {
|
|
20
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 && Math.floor(n) === n;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isFinitePositiveInt(n: unknown): n is number {
|
|
24
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 && Math.floor(n) === n;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** True when target has exactly one of reduceToBytes / removeOldestPercent. */
|
|
28
|
+
export function isValidPolicyTarget(target: StorageCleanupPolicy["target"]): boolean {
|
|
29
|
+
if (!target || typeof target !== "object") return false;
|
|
30
|
+
const reduce = (target as { reduceToBytes?: unknown }).reduceToBytes;
|
|
31
|
+
const percent = (target as { removeOldestPercent?: unknown }).removeOldestPercent;
|
|
32
|
+
const hasReduce = reduce !== undefined;
|
|
33
|
+
const hasPercent = percent !== undefined;
|
|
34
|
+
if (hasReduce === hasPercent) return false;
|
|
35
|
+
if (hasReduce) return isFiniteNonNegInt(reduce);
|
|
36
|
+
return typeof percent === "number" && Number.isFinite(percent) && percent > 0 && percent <= 100;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Normalize persisted or partial policy input without ever enabling implicitly. */
|
|
40
|
+
export function normalizeStorageCleanupPolicy(raw: unknown): StorageCleanupPolicy {
|
|
41
|
+
const base = defaultStorageCleanupPolicy();
|
|
42
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return base;
|
|
43
|
+
const o = raw as Record<string, unknown>;
|
|
44
|
+
let enabled = o.enabled === true;
|
|
45
|
+
|
|
46
|
+
let archivedBytesOver = base.trigger.archivedBytesOver;
|
|
47
|
+
if (o.trigger && typeof o.trigger === "object" && !Array.isArray(o.trigger)) {
|
|
48
|
+
const value = (o.trigger as { archivedBytesOver?: unknown }).archivedBytesOver;
|
|
49
|
+
if (isFiniteNonNegInt(value)) archivedBytesOver = value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let target: StorageCleanupPolicy["target"] = base.target;
|
|
53
|
+
if (Object.hasOwn(o, "target")) {
|
|
54
|
+
if (o.target && typeof o.target === "object" && !Array.isArray(o.target)) {
|
|
55
|
+
const candidate = o.target as StorageCleanupPolicy["target"];
|
|
56
|
+
if (isValidPolicyTarget(candidate)) {
|
|
57
|
+
const reduce = (candidate as { reduceToBytes?: number }).reduceToBytes;
|
|
58
|
+
const percent = (candidate as { removeOldestPercent?: number }).removeOldestPercent;
|
|
59
|
+
target = reduce !== undefined
|
|
60
|
+
? { reduceToBytes: reduce }
|
|
61
|
+
: { removeOldestPercent: Math.min(100, Math.max(1, Math.floor(percent!))) };
|
|
62
|
+
} else {
|
|
63
|
+
enabled = false;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
enabled = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const schedule: PolicySchedule =
|
|
71
|
+
o.schedule === "startup" || o.schedule === "daily" || o.schedule === "weekly" || o.schedule === "manual"
|
|
72
|
+
? o.schedule
|
|
73
|
+
: base.schedule;
|
|
74
|
+
const mode = o.mode === "permanent" ? "permanent" : "quarantine";
|
|
75
|
+
|
|
76
|
+
let lastRun: StorageCleanupPolicy["lastRun"];
|
|
77
|
+
if (o.lastRun && typeof o.lastRun === "object" && !Array.isArray(o.lastRun)) {
|
|
78
|
+
const row = o.lastRun as Record<string, unknown>;
|
|
79
|
+
if (isFinitePositiveInt(row.at) && isFiniteNonNegInt(row.freedBytes) && isFiniteNonNegInt(row.removed)) {
|
|
80
|
+
lastRun = { at: row.at, freedBytes: row.freedBytes, removed: row.removed };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const nextRun = o.nextRun === undefined || o.nextRun === null
|
|
85
|
+
? undefined
|
|
86
|
+
: isFinitePositiveInt(o.nextRun) ? o.nextRun : undefined;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
enabled,
|
|
90
|
+
trigger: { archivedBytesOver },
|
|
91
|
+
target,
|
|
92
|
+
schedule,
|
|
93
|
+
mode,
|
|
94
|
+
...(lastRun ? { lastRun } : {}),
|
|
95
|
+
...(nextRun !== undefined ? { nextRun } : {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Validate a management PUT body and merge it over the latest policy. */
|
|
100
|
+
export function parseStorageCleanupPolicyInput(
|
|
101
|
+
raw: unknown,
|
|
102
|
+
previous?: StorageCleanupPolicy,
|
|
103
|
+
): { ok: true; policy: StorageCleanupPolicy } | { ok: false; error: string } {
|
|
104
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
105
|
+
return { ok: false, error: "body must be a JSON object" };
|
|
106
|
+
}
|
|
107
|
+
const o = raw as Record<string, unknown>;
|
|
108
|
+
const prev = previous ?? defaultStorageCleanupPolicy();
|
|
109
|
+
|
|
110
|
+
if (o.enabled !== undefined && typeof o.enabled !== "boolean") {
|
|
111
|
+
return { ok: false, error: "enabled must be a boolean" };
|
|
112
|
+
}
|
|
113
|
+
if (o.mode !== undefined && o.mode !== "quarantine" && o.mode !== "permanent") {
|
|
114
|
+
return { ok: false, error: "mode must be quarantine or permanent" };
|
|
115
|
+
}
|
|
116
|
+
if (o.schedule !== undefined
|
|
117
|
+
&& o.schedule !== "startup"
|
|
118
|
+
&& o.schedule !== "daily"
|
|
119
|
+
&& o.schedule !== "weekly"
|
|
120
|
+
&& o.schedule !== "manual") {
|
|
121
|
+
return { ok: false, error: "schedule must be startup, daily, weekly, or manual" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const merged: Record<string, unknown> = {
|
|
125
|
+
...prev,
|
|
126
|
+
...o,
|
|
127
|
+
trigger: o.trigger !== undefined ? o.trigger : prev.trigger,
|
|
128
|
+
target: o.target !== undefined ? o.target : prev.target,
|
|
129
|
+
lastRun: o.lastRun !== undefined ? o.lastRun : prev.lastRun,
|
|
130
|
+
nextRun: o.nextRun !== undefined ? o.nextRun : prev.nextRun,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (o.trigger !== undefined) {
|
|
134
|
+
if (!o.trigger || typeof o.trigger !== "object" || Array.isArray(o.trigger)) {
|
|
135
|
+
return { ok: false, error: "trigger must be an object" };
|
|
136
|
+
}
|
|
137
|
+
const bytes = (o.trigger as { archivedBytesOver?: unknown }).archivedBytesOver;
|
|
138
|
+
if (!isFiniteNonNegInt(bytes)) {
|
|
139
|
+
return { ok: false, error: "trigger.archivedBytesOver must be a non-negative integer" };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (o.target !== undefined && !isValidPolicyTarget(o.target as StorageCleanupPolicy["target"])) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: "target must set exactly one of reduceToBytes (non-negative int) or removeOldestPercent (1-100)",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const policy = normalizeStorageCleanupPolicy(merged);
|
|
150
|
+
if ((policy.schedule === "daily" || policy.schedule === "weekly")
|
|
151
|
+
&& o.nextRun === undefined
|
|
152
|
+
&& (policy.nextRun === undefined || (o.schedule !== undefined && o.schedule !== prev.schedule))) {
|
|
153
|
+
policy.nextRun = computeNextRun(policy.schedule, Date.now());
|
|
154
|
+
}
|
|
155
|
+
if ((policy.schedule === "manual" || policy.schedule === "startup") && o.nextRun === undefined) {
|
|
156
|
+
delete policy.nextRun;
|
|
157
|
+
}
|
|
158
|
+
return { ok: true, policy };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Wall-clock next run for daily/weekly. Startup/manual → undefined. */
|
|
162
|
+
export function computeNextRun(schedule: PolicySchedule, now: number): number | undefined {
|
|
163
|
+
if (schedule === "daily") return now + 24 * 60 * 60 * 1000;
|
|
164
|
+
if (schedule === "weekly") return now + 7 * 24 * 60 * 60 * 1000;
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
package/src/storage/policy.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Privacy: logs never include host paths, digests of file contents, or secrets.
|
|
9
9
|
*/
|
|
10
10
|
import { resolveCodexHomeDir } from "../codex/home";
|
|
11
|
-
import { loadConfig,
|
|
11
|
+
import { loadConfig, mutatePersistedConfig } from "../config";
|
|
12
12
|
import type { StorageCleanupPolicy } from "../types";
|
|
13
13
|
import {
|
|
14
14
|
computePreviewDigest,
|
|
@@ -22,9 +22,21 @@ import {
|
|
|
22
22
|
type CleanupResult,
|
|
23
23
|
type ExecuteCleanupOptions,
|
|
24
24
|
} from "./cleanup";
|
|
25
|
+
import {
|
|
26
|
+
computeNextRun,
|
|
27
|
+
normalizeStorageCleanupPolicy,
|
|
28
|
+
} from "./policy-input";
|
|
29
|
+
export {
|
|
30
|
+
computeNextRun,
|
|
31
|
+
defaultStorageCleanupPolicy,
|
|
32
|
+
isValidPolicyTarget,
|
|
33
|
+
normalizeStorageCleanupPolicy,
|
|
34
|
+
parseStorageCleanupPolicyInput,
|
|
35
|
+
DEFAULT_ARCHIVED_BYTES_OVER,
|
|
36
|
+
DEFAULT_REMOVE_OLDEST_PERCENT,
|
|
37
|
+
type PolicySchedule,
|
|
38
|
+
} from "./policy-input";
|
|
25
39
|
|
|
26
|
-
export const DEFAULT_ARCHIVED_BYTES_OVER = 5 * 1024 ** 3; // 5 GiB
|
|
27
|
-
export const DEFAULT_REMOVE_OLDEST_PERCENT = 25;
|
|
28
40
|
export const BUSY_DEFER_MS = 15 * 60 * 1000;
|
|
29
41
|
|
|
30
42
|
/** Optional sink so background policy writes stay synced with the live server config. */
|
|
@@ -36,7 +48,10 @@ export function setStorageCleanupPolicyLiveSink(
|
|
|
36
48
|
livePolicySink = sink ?? undefined;
|
|
37
49
|
}
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
function adoptStorageCleanupPolicy(policy: StorageCleanupPolicy): void {
|
|
52
|
+
livePolicySink?.(policy);
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
export type PolicyRunReason = "startup" | "schedule" | "manual";
|
|
41
56
|
|
|
42
57
|
export type PolicySkipReason =
|
|
@@ -76,206 +91,20 @@ export interface PolicyRunDeps {
|
|
|
76
91
|
onPolicyLoaded?: () => void;
|
|
77
92
|
}
|
|
78
93
|
|
|
79
|
-
/** Canonical defaults — enabled is always false. */
|
|
80
|
-
export function defaultStorageCleanupPolicy(): StorageCleanupPolicy {
|
|
81
|
-
return {
|
|
82
|
-
enabled: false,
|
|
83
|
-
trigger: { archivedBytesOver: DEFAULT_ARCHIVED_BYTES_OVER },
|
|
84
|
-
target: { removeOldestPercent: DEFAULT_REMOVE_OLDEST_PERCENT },
|
|
85
|
-
schedule: "manual",
|
|
86
|
-
mode: "quarantine",
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function isFiniteNonNegInt(n: unknown): n is number {
|
|
91
|
-
return typeof n === "number" && Number.isFinite(n) && n >= 0 && Math.floor(n) === n;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function isFinitePositiveInt(n: unknown): n is number {
|
|
95
|
-
return typeof n === "number" && Number.isFinite(n) && n > 0 && Math.floor(n) === n;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** True when target has exactly one of reduceToBytes / removeOldestPercent. */
|
|
99
|
-
export function isValidPolicyTarget(target: StorageCleanupPolicy["target"]): boolean {
|
|
100
|
-
if (!target || typeof target !== "object") return false;
|
|
101
|
-
const reduce = (target as { reduceToBytes?: unknown }).reduceToBytes;
|
|
102
|
-
const percent = (target as { removeOldestPercent?: unknown }).removeOldestPercent;
|
|
103
|
-
const hasReduce = reduce !== undefined;
|
|
104
|
-
const hasPercent = percent !== undefined;
|
|
105
|
-
if (hasReduce === hasPercent) return false; // none or both
|
|
106
|
-
if (hasReduce) return isFiniteNonNegInt(reduce);
|
|
107
|
-
return typeof percent === "number" && Number.isFinite(percent) && percent > 0 && percent <= 100;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Normalize a partial/unknown policy into a complete StorageCleanupPolicy.
|
|
112
|
-
* Never flips enabled to true unless the input explicitly sets enabled: true.
|
|
113
|
-
*/
|
|
114
|
-
export function normalizeStorageCleanupPolicy(raw: unknown): StorageCleanupPolicy {
|
|
115
|
-
const base = defaultStorageCleanupPolicy();
|
|
116
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return base;
|
|
117
|
-
const o = raw as Record<string, unknown>;
|
|
118
|
-
|
|
119
|
-
// only explicit true enables — and never when a present target is malformed
|
|
120
|
-
let enabled = o.enabled === true;
|
|
121
|
-
|
|
122
|
-
let archivedBytesOver = base.trigger.archivedBytesOver;
|
|
123
|
-
if (o.trigger && typeof o.trigger === "object" && !Array.isArray(o.trigger)) {
|
|
124
|
-
const t = (o.trigger as { archivedBytesOver?: unknown }).archivedBytesOver;
|
|
125
|
-
if (isFiniteNonNegInt(t)) archivedBytesOver = t;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
let target: StorageCleanupPolicy["target"] = base.target;
|
|
129
|
-
if (Object.prototype.hasOwnProperty.call(o, "target")) {
|
|
130
|
-
if (o.target && typeof o.target === "object" && !Array.isArray(o.target)) {
|
|
131
|
-
const candidate = o.target as StorageCleanupPolicy["target"];
|
|
132
|
-
if (isValidPolicyTarget(candidate)) {
|
|
133
|
-
const reduce = (candidate as { reduceToBytes?: number }).reduceToBytes;
|
|
134
|
-
const percent = (candidate as { removeOldestPercent?: number }).removeOldestPercent;
|
|
135
|
-
target = reduce !== undefined
|
|
136
|
-
? { reduceToBytes: reduce }
|
|
137
|
-
: { removeOldestPercent: Math.min(100, Math.max(1, Math.floor(percent!))) };
|
|
138
|
-
} else {
|
|
139
|
-
// Fail closed: malformed persisted target must not become delete-oldest 25%.
|
|
140
|
-
enabled = false;
|
|
141
|
-
}
|
|
142
|
-
} else {
|
|
143
|
-
enabled = false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const schedule: PolicySchedule =
|
|
148
|
-
o.schedule === "startup" || o.schedule === "daily" || o.schedule === "weekly" || o.schedule === "manual"
|
|
149
|
-
? o.schedule
|
|
150
|
-
: base.schedule;
|
|
151
|
-
|
|
152
|
-
const mode: CleanupMode = o.mode === "permanent" ? "permanent" : "quarantine";
|
|
153
|
-
|
|
154
|
-
let lastRun: StorageCleanupPolicy["lastRun"];
|
|
155
|
-
if (o.lastRun && typeof o.lastRun === "object" && !Array.isArray(o.lastRun)) {
|
|
156
|
-
const lr = o.lastRun as Record<string, unknown>;
|
|
157
|
-
if (
|
|
158
|
-
isFinitePositiveInt(lr.at)
|
|
159
|
-
&& isFiniteNonNegInt(lr.freedBytes)
|
|
160
|
-
&& isFiniteNonNegInt(lr.removed)
|
|
161
|
-
) {
|
|
162
|
-
lastRun = { at: lr.at, freedBytes: lr.freedBytes, removed: lr.removed };
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
let nextRun: number | undefined;
|
|
167
|
-
if (o.nextRun === undefined || o.nextRun === null) {
|
|
168
|
-
nextRun = undefined;
|
|
169
|
-
} else if (isFinitePositiveInt(o.nextRun)) {
|
|
170
|
-
nextRun = o.nextRun;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
enabled,
|
|
175
|
-
trigger: { archivedBytesOver },
|
|
176
|
-
target,
|
|
177
|
-
schedule,
|
|
178
|
-
mode,
|
|
179
|
-
...(lastRun ? { lastRun } : {}),
|
|
180
|
-
...(nextRun !== undefined ? { nextRun } : {}),
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Validate a PUT body. Returns a normalized policy or an error string.
|
|
186
|
-
* Does not invent enabled=true.
|
|
187
|
-
*/
|
|
188
|
-
export function parseStorageCleanupPolicyInput(
|
|
189
|
-
raw: unknown,
|
|
190
|
-
previous?: StorageCleanupPolicy,
|
|
191
|
-
): { ok: true; policy: StorageCleanupPolicy } | { ok: false; error: string } {
|
|
192
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
193
|
-
return { ok: false, error: "body must be a JSON object" };
|
|
194
|
-
}
|
|
195
|
-
const o = raw as Record<string, unknown>;
|
|
196
|
-
const prev = previous ?? defaultStorageCleanupPolicy();
|
|
197
|
-
|
|
198
|
-
if (o.enabled !== undefined && typeof o.enabled !== "boolean") {
|
|
199
|
-
return { ok: false, error: "enabled must be a boolean" };
|
|
200
|
-
}
|
|
201
|
-
if (o.mode !== undefined && o.mode !== "quarantine" && o.mode !== "permanent") {
|
|
202
|
-
return { ok: false, error: "mode must be quarantine or permanent" };
|
|
203
|
-
}
|
|
204
|
-
if (
|
|
205
|
-
o.schedule !== undefined
|
|
206
|
-
&& o.schedule !== "startup"
|
|
207
|
-
&& o.schedule !== "daily"
|
|
208
|
-
&& o.schedule !== "weekly"
|
|
209
|
-
&& o.schedule !== "manual"
|
|
210
|
-
) {
|
|
211
|
-
return { ok: false, error: "schedule must be startup, daily, weekly, or manual" };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const merged: Record<string, unknown> = {
|
|
215
|
-
...prev,
|
|
216
|
-
...o,
|
|
217
|
-
trigger: o.trigger !== undefined ? o.trigger : prev.trigger,
|
|
218
|
-
target: o.target !== undefined ? o.target : prev.target,
|
|
219
|
-
// Preserve run metadata unless the client explicitly sends replacements.
|
|
220
|
-
lastRun: o.lastRun !== undefined ? o.lastRun : prev.lastRun,
|
|
221
|
-
nextRun: o.nextRun !== undefined ? o.nextRun : prev.nextRun,
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
if (o.trigger !== undefined) {
|
|
225
|
-
if (!o.trigger || typeof o.trigger !== "object" || Array.isArray(o.trigger)) {
|
|
226
|
-
return { ok: false, error: "trigger must be an object" };
|
|
227
|
-
}
|
|
228
|
-
const bytes = (o.trigger as { archivedBytesOver?: unknown }).archivedBytesOver;
|
|
229
|
-
if (!isFiniteNonNegInt(bytes)) {
|
|
230
|
-
return { ok: false, error: "trigger.archivedBytesOver must be a non-negative integer" };
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (o.target !== undefined) {
|
|
235
|
-
if (!isValidPolicyTarget(o.target as StorageCleanupPolicy["target"])) {
|
|
236
|
-
return {
|
|
237
|
-
ok: false,
|
|
238
|
-
error: "target must set exactly one of reduceToBytes (non-negative int) or removeOldestPercent (1-100)",
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Clients must not clear lastRun/nextRun via null unless intentional — accept omit only.
|
|
244
|
-
const policy = normalizeStorageCleanupPolicy(merged);
|
|
245
|
-
// Recompute nextRun only when schedule newly becomes timed, or no valid nextRun exists.
|
|
246
|
-
// Do not reset nextRun on every PUT that merely re-sends an unchanged schedule.
|
|
247
|
-
if (
|
|
248
|
-
(policy.schedule === "daily" || policy.schedule === "weekly")
|
|
249
|
-
&& o.nextRun === undefined
|
|
250
|
-
&& (policy.nextRun === undefined || (o.schedule !== undefined && o.schedule !== prev.schedule))
|
|
251
|
-
) {
|
|
252
|
-
policy.nextRun = computeNextRun(policy.schedule, Date.now());
|
|
253
|
-
}
|
|
254
|
-
if (policy.schedule === "manual" || policy.schedule === "startup") {
|
|
255
|
-
if (o.nextRun === undefined) delete policy.nextRun;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return { ok: true, policy };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
94
|
export function readStorageCleanupPolicyFromConfig(): StorageCleanupPolicy {
|
|
262
95
|
return normalizeStorageCleanupPolicy(loadConfig().storageCleanupPolicy);
|
|
263
96
|
}
|
|
264
97
|
|
|
265
98
|
export function writeStorageCleanupPolicyToConfig(policy: StorageCleanupPolicy): StorageCleanupPolicy {
|
|
266
99
|
const normalized = normalizeStorageCleanupPolicy(policy);
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
export function computeNextRun(schedule: PolicySchedule, now: number): number | undefined {
|
|
276
|
-
if (schedule === "daily") return now + 24 * 60 * 60 * 1000;
|
|
277
|
-
if (schedule === "weekly") return now + 7 * 24 * 60 * 60 * 1000;
|
|
278
|
-
return undefined;
|
|
100
|
+
const outcome = mutatePersistedConfig(fresh => {
|
|
101
|
+
const changed = JSON.stringify(fresh.storageCleanupPolicy) !== JSON.stringify(normalized);
|
|
102
|
+
fresh.storageCleanupPolicy = normalized;
|
|
103
|
+
return { changed, value: structuredClone(normalized) };
|
|
104
|
+
});
|
|
105
|
+
if (outcome.status === "unavailable") throw new Error(`storage cleanup policy persistence unavailable: ${outcome.reason}`);
|
|
106
|
+
adoptStorageCleanupPolicy(outcome.value);
|
|
107
|
+
return outcome.value;
|
|
279
108
|
}
|
|
280
109
|
|
|
281
110
|
export function isPolicyDue(
|
|
@@ -401,6 +230,17 @@ export type PolicyRunMetadataPatch = {
|
|
|
401
230
|
lastRun?: StorageCleanupPolicy["lastRun"];
|
|
402
231
|
};
|
|
403
232
|
|
|
233
|
+
function applyPolicyRunMetadata(
|
|
234
|
+
latest: StorageCleanupPolicy,
|
|
235
|
+
patch: PolicyRunMetadataPatch,
|
|
236
|
+
): StorageCleanupPolicy {
|
|
237
|
+
let next = patch.nextRun === "defer_busy"
|
|
238
|
+
? deferBusy(latest, patch.now)
|
|
239
|
+
: advanceNextRun(latest, patch.now);
|
|
240
|
+
if (patch.lastRun) next = { ...next, lastRun: patch.lastRun };
|
|
241
|
+
return next;
|
|
242
|
+
}
|
|
243
|
+
|
|
404
244
|
/**
|
|
405
245
|
* Reload the latest persisted policy and write only run-owned metadata
|
|
406
246
|
* (`lastRun` / `nextRun`). Preserves concurrent edits to enabled, trigger,
|
|
@@ -412,17 +252,23 @@ export function commitPolicyRunMetadata(
|
|
|
412
252
|
patch: PolicyRunMetadataPatch,
|
|
413
253
|
): StorageCleanupPolicy {
|
|
414
254
|
const latest = normalizeStorageCleanupPolicy(load());
|
|
415
|
-
|
|
416
|
-
patch.nextRun === "defer_busy"
|
|
417
|
-
? deferBusy(latest, patch.now)
|
|
418
|
-
: advanceNextRun(latest, patch.now);
|
|
419
|
-
if (patch.lastRun) {
|
|
420
|
-
next = { ...next, lastRun: patch.lastRun };
|
|
421
|
-
}
|
|
255
|
+
const next = applyPolicyRunMetadata(latest, patch);
|
|
422
256
|
save(next);
|
|
423
257
|
return next;
|
|
424
258
|
}
|
|
425
259
|
|
|
260
|
+
function commitPersistedPolicyRunMetadata(patch: PolicyRunMetadataPatch): StorageCleanupPolicy {
|
|
261
|
+
const outcome = mutatePersistedConfig(fresh => {
|
|
262
|
+
const before = JSON.stringify(fresh.storageCleanupPolicy);
|
|
263
|
+
const next = applyPolicyRunMetadata(normalizeStorageCleanupPolicy(fresh.storageCleanupPolicy), patch);
|
|
264
|
+
fresh.storageCleanupPolicy = next;
|
|
265
|
+
return { changed: JSON.stringify(next) !== before, value: structuredClone(next) };
|
|
266
|
+
});
|
|
267
|
+
if (outcome.status === "unavailable") throw new Error(`storage cleanup policy persistence unavailable: ${outcome.reason}`);
|
|
268
|
+
adoptStorageCleanupPolicy(outcome.value);
|
|
269
|
+
return outcome.value;
|
|
270
|
+
}
|
|
271
|
+
|
|
426
272
|
function logPolicyEvent(message: string): void {
|
|
427
273
|
console.log(`[storage-policy] ${message}`);
|
|
428
274
|
}
|
|
@@ -434,8 +280,10 @@ function logPolicyEvent(message: string): void {
|
|
|
434
280
|
export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
|
|
435
281
|
const now = deps.now ?? Date.now();
|
|
436
282
|
const load = deps.loadPolicy ?? readStorageCleanupPolicyFromConfig;
|
|
437
|
-
const save = deps.savePolicy ?? writeStorageCleanupPolicyToConfig;
|
|
438
283
|
const execute = deps.execute ?? executeArchivedCleanup;
|
|
284
|
+
const commit = deps.savePolicy
|
|
285
|
+
? (patch: PolicyRunMetadataPatch) => commitPolicyRunMetadata(load, deps.savePolicy!, patch)
|
|
286
|
+
: commitPersistedPolicyRunMetadata;
|
|
439
287
|
|
|
440
288
|
const policy = normalizeStorageCleanupPolicy(load());
|
|
441
289
|
deps.onPolicyLoaded?.();
|
|
@@ -454,13 +302,13 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
|
|
|
454
302
|
|
|
455
303
|
const selection = selectPolicyPreview(policy, deps.codexHome);
|
|
456
304
|
if (selection.archivedBytes <= policy.trigger.archivedBytesOver) {
|
|
457
|
-
const saved =
|
|
305
|
+
const saved = commit({ now, nextRun: "advance" });
|
|
458
306
|
logPolicyEvent("skip under_threshold");
|
|
459
307
|
return { ok: true, skipped: "under_threshold", policy: saved };
|
|
460
308
|
}
|
|
461
309
|
|
|
462
310
|
if (selection.count === 0) {
|
|
463
|
-
const saved =
|
|
311
|
+
const saved = commit({ now, nextRun: "advance" });
|
|
464
312
|
logPolicyEvent("skip nothing_selected");
|
|
465
313
|
return { ok: true, skipped: "nothing_selected", policy: saved };
|
|
466
314
|
}
|
|
@@ -476,14 +324,14 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
|
|
|
476
324
|
});
|
|
477
325
|
|
|
478
326
|
if (!result.ok && result.error === "codex_busy") {
|
|
479
|
-
const saved =
|
|
327
|
+
const saved = commit({ now, nextRun: "defer_busy" });
|
|
480
328
|
logPolicyEvent("defer codex_busy");
|
|
481
329
|
return { ok: false, deferred: "codex_busy", error: "codex_busy", policy: saved };
|
|
482
330
|
}
|
|
483
331
|
|
|
484
332
|
if (!result.ok) {
|
|
485
333
|
// Non-busy failure: still advance schedule so we do not tight-loop.
|
|
486
|
-
const saved =
|
|
334
|
+
const saved = commit({ now, nextRun: "advance" });
|
|
487
335
|
logPolicyEvent(`fail ${result.error ?? "cleanup_failed"}`);
|
|
488
336
|
return {
|
|
489
337
|
ok: false,
|
|
@@ -494,7 +342,7 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
|
|
|
494
342
|
};
|
|
495
343
|
}
|
|
496
344
|
|
|
497
|
-
const saved =
|
|
345
|
+
const saved = commit({
|
|
498
346
|
now,
|
|
499
347
|
nextRun: "advance",
|
|
500
348
|
lastRun: {
|