@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.
Files changed (51) hide show
  1. package/gui/dist/assets/{ApiKeys-C_cz6Rww.js → ApiKeys-LOOFiZfv.js} +1 -1
  2. package/gui/dist/assets/{Claude-ofGguLfF.js → Claude-BwKzpXe3.js} +1 -1
  3. package/gui/dist/assets/{CodexSet-fhGiVwxe.js → CodexSet-B5qi9KeE.js} +1 -1
  4. package/gui/dist/assets/{FileIntegrationPage-B-dI69Kn.js → FileIntegrationPage-B0sUef6W.js} +1 -1
  5. package/gui/dist/assets/{Grok-CgkBu_Sm.js → Grok-bsb4n-f4.js} +1 -1
  6. package/gui/dist/assets/{Integrations-CTWMNzkD.js → Integrations-CSMwFDvM.js} +2 -2
  7. package/gui/dist/assets/{IntegrationsOverview-BKZ0dZnj.js → IntegrationsOverview-CTORdKJA.js} +1 -1
  8. package/gui/dist/assets/{Logs-6r90C0x4.js → Logs-BWYbfELf.js} +1 -1
  9. package/gui/dist/assets/{Models-CpcS1zZr.js → Models-CexEtdT1.js} +1 -1
  10. package/gui/dist/assets/{NumberStepper-C70ttiJY.js → NumberStepper-BapSFQnW.js} +1 -1
  11. package/gui/dist/assets/{Providers-BROTzuSF.js → Providers-BwUFeAgA.js} +1 -1
  12. package/gui/dist/assets/{RestoreDialog-DR5vgFXB.js → RestoreDialog-CD8piq90.js} +1 -1
  13. package/gui/dist/assets/{Startup-BSoZDYbS.js → Startup-BgX731C2.js} +1 -1
  14. package/gui/dist/assets/{Storage-CUvBwbeB.js → Storage-Bgq7HphP.js} +1 -1
  15. package/gui/dist/assets/{Subagents-CYu3_DBt.js → Subagents-ChxItbeF.js} +1 -1
  16. package/gui/dist/assets/{Usage-BV36sd7V.js → Usage-BkylzP50.js} +1 -1
  17. package/gui/dist/assets/{data-surface-DhcMe0J_.js → data-surface-pCXChiBf.js} +1 -1
  18. package/gui/dist/assets/{index-DnuNKc0D.js → index-CNopOid3.js} +2 -2
  19. package/gui/dist/assets/{model-display-BPrQ7F3P.js → model-display-CiUpg8T9.js} +1 -1
  20. package/gui/dist/assets/{provider-payload-CMb37zWF.js → provider-payload-b2jlS-On.js} +1 -1
  21. package/gui/dist/index.html +1 -1
  22. package/package.json +1 -1
  23. package/src/cli/agent.ts +19 -2
  24. package/src/cli/config-command.ts +7 -3
  25. package/src/cli/init.ts +2 -2
  26. package/src/cli/provider.ts +58 -74
  27. package/src/cli/registry.ts +1 -1
  28. package/src/codex/catalog.ts +1 -1
  29. package/src/codex/convergence.ts +110 -15
  30. package/src/codex/inject.ts +2 -0
  31. package/src/codex/subagent-defaults.ts +1 -8
  32. package/src/codex/subagent-model-authority.ts +189 -0
  33. package/src/config.ts +209 -11
  34. package/src/generated/compatibility-version.json +35 -27
  35. package/src/oauth/index.ts +143 -82
  36. package/src/oauth/login-cli.ts +53 -11
  37. package/src/providers/alibaba-region-startup.ts +31 -11
  38. package/src/providers/api-keys.ts +58 -34
  39. package/src/providers/key-failover.ts +59 -46
  40. package/src/providers/model-rename-startup.ts +25 -7
  41. package/src/providers/openai-tier-startup.ts +38 -14
  42. package/src/server/index.ts +10 -0
  43. package/src/server/management/agent-settings-routes.ts +12 -0
  44. package/src/server/management/context.ts +10 -3
  45. package/src/server/management/logs-usage-routes.ts +19 -16
  46. package/src/server/management/model-routes.ts +284 -165
  47. package/src/server/management/oauth-account-routes.ts +93 -15
  48. package/src/server/management/provider-routes.ts +200 -87
  49. package/src/server/responses/collaboration.ts +1 -1
  50. package/src/storage/policy-input.ts +166 -0
  51. 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
+ }
@@ -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, saveConfigPreservingClaudeCode } from "../config";
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
- export type PolicySchedule = StorageCleanupPolicy["schedule"];
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 config = loadConfig();
268
- config.storageCleanupPolicy = normalized;
269
- saveConfigPreservingClaudeCode(config);
270
- livePolicySink?.(normalized);
271
- return normalized;
272
- }
273
-
274
- /** Wall-clock next run for daily/weekly. Startup/manual → undefined. */
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
- let next =
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 = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
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 = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
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 = commitPolicyRunMetadata(load, save, { now, nextRun: "defer_busy" });
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 = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
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 = commitPolicyRunMetadata(load, save, {
345
+ const saved = commit({
498
346
  now,
499
347
  nextRun: "advance",
500
348
  lastRun: {