@bitkyc08/opencodex 2.7.42 → 2.7.43
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/README.md +8 -1
- package/bin/ocx.mjs +13 -14
- package/gui/dist/assets/index-Czw-jpTU.css +1 -0
- package/gui/dist/assets/index-cmds12BG.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +49 -6
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/inbound.ts +26 -9
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/claude.ts +3 -3
- package/src/cli/doctor.ts +23 -18
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +88 -19
- package/src/cli/interactive-confirm.ts +129 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +55 -16
- package/src/cli/status.ts +47 -13
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/parsing.ts +16 -1
- package/src/codex/catalog/provider-fetch.ts +48 -12
- package/src/codex/catalog/sync.ts +18 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +15 -0
- package/src/codex/routing.ts +695 -106
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +566 -25
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +122 -4
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +164 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +283 -72
- package/src/oauth/key-providers.ts +17 -6
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +322 -10
- package/src/oauth/kiro.ts +423 -43
- package/src/oauth/login-cli.ts +32 -5
- package/src/oauth/store.ts +54 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +11 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +14 -4
- package/src/server/auth-cors.ts +110 -22
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/management/agent-settings-routes.ts +28 -4
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +5 -2
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +35 -7
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +2 -2
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +583 -152
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +49 -2
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +366 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +242 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/tray/windows.ts +2 -0
- package/src/types.ts +96 -1
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +103 -12
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/log.ts +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-flight controller for storage cleanup policy runs.
|
|
3
|
+
*
|
|
4
|
+
* Manual, startup, and scheduled evaluations share one in-flight slot. Heavy
|
|
5
|
+
* work (archive scan, FS cleanup, SQLite reconcile) runs in a Bun Worker so the
|
|
6
|
+
* proxy event loop stays responsive.
|
|
7
|
+
*/
|
|
8
|
+
import type { CleanupMode, CleanupResult } from "./cleanup";
|
|
9
|
+
import { resolveCodexHomeDir } from "../codex/home";
|
|
10
|
+
import {
|
|
11
|
+
endStorageMutation,
|
|
12
|
+
tryBeginStorageMutation,
|
|
13
|
+
} from "./storage-mutation-coordinator";
|
|
14
|
+
import {
|
|
15
|
+
isPolicyDue,
|
|
16
|
+
readStorageCleanupPolicyFromConfig,
|
|
17
|
+
type PolicyRunReason,
|
|
18
|
+
type PolicyRunResult,
|
|
19
|
+
type PolicySkipReason,
|
|
20
|
+
} from "./policy";
|
|
21
|
+
|
|
22
|
+
export type PolicyJobStatus = "idle" | "running";
|
|
23
|
+
|
|
24
|
+
export interface PolicyJobOutcome {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
skipped?: PolicySkipReason;
|
|
27
|
+
deferred?: "codex_busy";
|
|
28
|
+
error?: CleanupResult["error"] | "evaluation_failed" | "worker_failed" | "storage_mutation_busy";
|
|
29
|
+
mode?: CleanupMode;
|
|
30
|
+
freedBytes?: number;
|
|
31
|
+
removed?: number;
|
|
32
|
+
trashDir?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PolicyJobState {
|
|
36
|
+
status: PolicyJobStatus;
|
|
37
|
+
reason?: PolicyRunReason;
|
|
38
|
+
startedAt?: number;
|
|
39
|
+
finishedAt?: number;
|
|
40
|
+
lastError?: string;
|
|
41
|
+
lastOutcome?: PolicyJobOutcome;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RequestPolicyRunOptions {
|
|
45
|
+
reason: PolicyRunReason;
|
|
46
|
+
force?: boolean;
|
|
47
|
+
codexHome?: string;
|
|
48
|
+
busyTimeoutMs?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PolicyJobTestHooks {
|
|
52
|
+
/**
|
|
53
|
+
* Block the worker (or in-process run) this many ms after the start-of-job
|
|
54
|
+
* policy load, so concurrent PUTs can race completion metadata writes.
|
|
55
|
+
*/
|
|
56
|
+
blockMs?: number;
|
|
57
|
+
/**
|
|
58
|
+
* When true, run on the main thread via `queueMicrotask` + optional sleep.
|
|
59
|
+
* Used only for unit tests that cannot spawn workers; responsiveness tests
|
|
60
|
+
* must leave this unset so work stays in a Worker.
|
|
61
|
+
*/
|
|
62
|
+
runInProcess?: boolean;
|
|
63
|
+
/** Expose GET /api/storage/cleanup-policy/test-stream for responsiveness tests. */
|
|
64
|
+
enableTestStream?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const WORKER_TIMEOUT_MS = 10 * 60 * 1000;
|
|
68
|
+
|
|
69
|
+
let state: PolicyJobState = { status: "idle" };
|
|
70
|
+
let inflight: Promise<void> | null = null;
|
|
71
|
+
let activeWorker: Worker | null = null;
|
|
72
|
+
let testHooks: PolicyJobTestHooks | null = null;
|
|
73
|
+
/** Optional mirror so completed worker runs refresh the live server config. */
|
|
74
|
+
let livePolicyApply: ((policy: PolicyRunResult["policy"]) => void) | undefined;
|
|
75
|
+
/** Settles the active `runInWorker` promise (clears watchdog) before hard terminate. */
|
|
76
|
+
let cancelActiveRun: (() => void) | null = null;
|
|
77
|
+
/** Bumped on abort/reset so a late worker completion cannot clobber newer job state. */
|
|
78
|
+
let runGeneration = 0;
|
|
79
|
+
/** CODEX_HOME whose mutation slot this job holds (parent thread only). */
|
|
80
|
+
let heldMutationHome: string | undefined;
|
|
81
|
+
|
|
82
|
+
function releaseHeldMutationSlot(): void {
|
|
83
|
+
if (heldMutationHome === undefined) return;
|
|
84
|
+
endStorageMutation(heldMutationHome);
|
|
85
|
+
heldMutationHome = undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function setStorageCleanupPolicyJobLiveApply(
|
|
89
|
+
apply: ((policy: PolicyRunResult["policy"]) => void) | null,
|
|
90
|
+
): void {
|
|
91
|
+
livePolicyApply = apply ?? undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getStorageCleanupPolicyJobState(): PolicyJobState {
|
|
95
|
+
return { ...state, ...(state.lastOutcome ? { lastOutcome: { ...state.lastOutcome } } : {}) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Test-only SSE/text stream served from the proxy while a worker is blocked. */
|
|
99
|
+
export function getStorageCleanupPolicyTestStreamResponse(): Response | null {
|
|
100
|
+
if (!testHooks?.enableTestStream) return null;
|
|
101
|
+
const encoder = new TextEncoder();
|
|
102
|
+
return new Response(
|
|
103
|
+
new ReadableStream({
|
|
104
|
+
async start(controller) {
|
|
105
|
+
for (let i = 0; i < 8; i++) {
|
|
106
|
+
controller.enqueue(encoder.encode(`chunk-${i}\n`));
|
|
107
|
+
await Bun.sleep(50);
|
|
108
|
+
}
|
|
109
|
+
controller.close();
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
{ headers: { "content-type": "text/plain; charset=utf-8" } },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function setStorageCleanupPolicyJobTestHooks(hooks: PolicyJobTestHooks | null): void {
|
|
117
|
+
testHooks = hooks;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function disownActiveRun(): void {
|
|
121
|
+
runGeneration += 1;
|
|
122
|
+
const cancel = cancelActiveRun;
|
|
123
|
+
cancelActiveRun = null;
|
|
124
|
+
cancel?.();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resetStorageCleanupPolicyJobForTests(): void {
|
|
128
|
+
disownActiveRun();
|
|
129
|
+
if (activeWorker) {
|
|
130
|
+
try { activeWorker.terminate(); } catch { /* */ }
|
|
131
|
+
activeWorker = null;
|
|
132
|
+
}
|
|
133
|
+
inflight = null;
|
|
134
|
+
testHooks = null;
|
|
135
|
+
releaseHeldMutationSlot();
|
|
136
|
+
state = { status: "idle" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Terminate an in-flight worker during process shutdown. */
|
|
140
|
+
export function abortStorageCleanupPolicyJob(): void {
|
|
141
|
+
disownActiveRun();
|
|
142
|
+
if (activeWorker) {
|
|
143
|
+
try { activeWorker.terminate(); } catch { /* */ }
|
|
144
|
+
activeWorker = null;
|
|
145
|
+
}
|
|
146
|
+
releaseHeldMutationSlot();
|
|
147
|
+
if (state.status === "running") {
|
|
148
|
+
state = {
|
|
149
|
+
...state,
|
|
150
|
+
status: "idle",
|
|
151
|
+
finishedAt: Date.now(),
|
|
152
|
+
lastError: "aborted",
|
|
153
|
+
lastOutcome: {
|
|
154
|
+
ok: false,
|
|
155
|
+
error: "evaluation_failed",
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
inflight = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function outcomeFromResult(result: PolicyRunResult): PolicyJobOutcome {
|
|
163
|
+
return {
|
|
164
|
+
ok: result.ok,
|
|
165
|
+
...(result.skipped ? { skipped: result.skipped } : {}),
|
|
166
|
+
...(result.deferred ? { deferred: result.deferred } : {}),
|
|
167
|
+
...(result.error ? { error: result.error } : {}),
|
|
168
|
+
...(result.mode ? { mode: result.mode } : {}),
|
|
169
|
+
...(result.freedBytes !== undefined ? { freedBytes: result.freedBytes } : {}),
|
|
170
|
+
...(result.removed !== undefined ? { removed: result.removed } : {}),
|
|
171
|
+
...(result.trashDir ? { trashDir: result.trashDir } : {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function applyFinished(result: PolicyRunResult): void {
|
|
176
|
+
// Prefer the latest persisted policy over `result.policy`. The worker (or
|
|
177
|
+
// in-process run) already merged run metadata into disk; a concurrent PUT
|
|
178
|
+
// may also have landed after that write. Re-reading avoids applying a stale
|
|
179
|
+
// start-of-job snapshot when the run skipped without saving.
|
|
180
|
+
try {
|
|
181
|
+
livePolicyApply?.(readStorageCleanupPolicyFromConfig());
|
|
182
|
+
} catch {
|
|
183
|
+
livePolicyApply?.(result.policy);
|
|
184
|
+
}
|
|
185
|
+
state = {
|
|
186
|
+
status: "idle",
|
|
187
|
+
reason: state.reason,
|
|
188
|
+
startedAt: state.startedAt,
|
|
189
|
+
finishedAt: Date.now(),
|
|
190
|
+
lastOutcome: outcomeFromResult(result),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function applyFailed(message: string): void {
|
|
195
|
+
state = {
|
|
196
|
+
status: "idle",
|
|
197
|
+
reason: state.reason,
|
|
198
|
+
startedAt: state.startedAt,
|
|
199
|
+
finishedAt: Date.now(),
|
|
200
|
+
lastError: message,
|
|
201
|
+
lastOutcome: { ok: false, error: "worker_failed" },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function applyMutationBusy(): void {
|
|
206
|
+
state = {
|
|
207
|
+
status: "idle",
|
|
208
|
+
reason: state.reason,
|
|
209
|
+
startedAt: state.startedAt,
|
|
210
|
+
finishedAt: Date.now(),
|
|
211
|
+
lastError: "storage_mutation_busy",
|
|
212
|
+
lastOutcome: { ok: false, error: "storage_mutation_busy" },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Promise<PolicyRunResult> {
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
const requestId = crypto.randomUUID();
|
|
219
|
+
let settled = false;
|
|
220
|
+
const worker = new Worker(new URL("./policy-worker.ts", import.meta.url).href);
|
|
221
|
+
activeWorker = worker;
|
|
222
|
+
|
|
223
|
+
const timer = setTimeout(() => {
|
|
224
|
+
if (settled) return;
|
|
225
|
+
settled = true;
|
|
226
|
+
cancelActiveRun = null;
|
|
227
|
+
try { worker.terminate(); } catch { /* */ }
|
|
228
|
+
if (activeWorker === worker) activeWorker = null;
|
|
229
|
+
reject(new Error("storage_cleanup_worker_timeout"));
|
|
230
|
+
}, WORKER_TIMEOUT_MS);
|
|
231
|
+
|
|
232
|
+
const finish = (fn: () => void) => {
|
|
233
|
+
if (settled) return;
|
|
234
|
+
settled = true;
|
|
235
|
+
cancelActiveRun = null;
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
if (activeWorker === worker) activeWorker = null;
|
|
238
|
+
try { worker.terminate(); } catch { /* */ }
|
|
239
|
+
fn();
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
cancelActiveRun = () => {
|
|
243
|
+
finish(() => reject(new Error("aborted")));
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
worker.onmessage = (event: MessageEvent<unknown>) => {
|
|
247
|
+
const data = event.data;
|
|
248
|
+
if (!data || typeof data !== "object") return;
|
|
249
|
+
const msg = data as Record<string, unknown>;
|
|
250
|
+
if (msg.requestId !== requestId) return;
|
|
251
|
+
if (msg.type === "done" && msg.result && typeof msg.result === "object") {
|
|
252
|
+
finish(() => resolve(msg.result as PolicyRunResult));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (msg.type === "error") {
|
|
256
|
+
const message = typeof msg.message === "string" ? msg.message : "worker_failed";
|
|
257
|
+
finish(() => reject(new Error(message)));
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
worker.onerror = (err: ErrorEvent) => {
|
|
262
|
+
finish(() => reject(err.error instanceof Error ? err.error : new Error(err.message || "worker_failed")));
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
worker.postMessage({
|
|
266
|
+
type: "run",
|
|
267
|
+
requestId,
|
|
268
|
+
reason: opts.reason,
|
|
269
|
+
force: opts.force === true,
|
|
270
|
+
...(opts.codexHome ? { codexHome: opts.codexHome } : {}),
|
|
271
|
+
...(opts.busyTimeoutMs !== undefined ? { busyTimeoutMs: opts.busyTimeoutMs } : {}),
|
|
272
|
+
...(opts.blockMs !== undefined ? { blockMs: opts.blockMs } : {}),
|
|
273
|
+
env: {
|
|
274
|
+
...(process.env.CODEX_HOME ? { CODEX_HOME: process.env.CODEX_HOME } : {}),
|
|
275
|
+
...(process.env.OPENCODEX_HOME ? { OPENCODEX_HOME: process.env.OPENCODEX_HOME } : {}),
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function executeJob(opts: RequestPolicyRunOptions): Promise<void> {
|
|
282
|
+
const generation = ++runGeneration;
|
|
283
|
+
const codexHome = opts.codexHome ?? resolveCodexHomeDir();
|
|
284
|
+
const gate = tryBeginStorageMutation("policy", codexHome);
|
|
285
|
+
if (!gate.acquired) {
|
|
286
|
+
if (generation === runGeneration) {
|
|
287
|
+
applyMutationBusy();
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
heldMutationHome = codexHome;
|
|
292
|
+
try {
|
|
293
|
+
const blockMs = testHooks?.blockMs;
|
|
294
|
+
let result: PolicyRunResult;
|
|
295
|
+
|
|
296
|
+
if (testHooks?.runInProcess) {
|
|
297
|
+
// Dynamic import keeps the sync engine out of the hot path for the default Worker mode.
|
|
298
|
+
const { runStorageCleanupPolicy } = await import("./policy");
|
|
299
|
+
result = runStorageCleanupPolicy({
|
|
300
|
+
reason: opts.reason,
|
|
301
|
+
force: opts.force === true,
|
|
302
|
+
codexHome,
|
|
303
|
+
...(opts.busyTimeoutMs !== undefined ? { busyTimeoutMs: opts.busyTimeoutMs } : {}),
|
|
304
|
+
...(typeof blockMs === "number" && blockMs > 0 ? { holdAfterLoadMs: blockMs } : {}),
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
result = await runInWorker({
|
|
308
|
+
...opts,
|
|
309
|
+
codexHome,
|
|
310
|
+
...(typeof blockMs === "number" && blockMs > 0 ? { blockMs } : {}),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (generation !== runGeneration) return;
|
|
315
|
+
applyFinished(result);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (generation !== runGeneration) return;
|
|
318
|
+
applyFailed(err instanceof Error ? err.message : "worker_failed");
|
|
319
|
+
} finally {
|
|
320
|
+
releaseHeldMutationSlot();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Start a cleanup evaluation if idle. Returns immediately; poll GET for outcome.
|
|
326
|
+
*/
|
|
327
|
+
export function requestStorageCleanupPolicyRun(
|
|
328
|
+
opts: RequestPolicyRunOptions,
|
|
329
|
+
): { accepted: true; state: PolicyJobState } | { accepted: false; error: "already_running"; state: PolicyJobState } {
|
|
330
|
+
if (inflight || state.status === "running") {
|
|
331
|
+
return { accepted: false, error: "already_running", state: getStorageCleanupPolicyJobState() };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
state = {
|
|
335
|
+
status: "running",
|
|
336
|
+
reason: opts.reason,
|
|
337
|
+
startedAt: Date.now(),
|
|
338
|
+
lastError: undefined,
|
|
339
|
+
...(state.lastOutcome ? { lastOutcome: state.lastOutcome } : {}),
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const job = executeJob(opts);
|
|
343
|
+
inflight = job;
|
|
344
|
+
void job.finally(() => {
|
|
345
|
+
if (inflight === job) inflight = null;
|
|
346
|
+
});
|
|
347
|
+
return { accepted: true, state: getStorageCleanupPolicyJobState() };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Fire-and-forget entry for startup / schedule ticks.
|
|
352
|
+
* Skips the single-flight slot when disabled or not due so manual runs stay free.
|
|
353
|
+
*/
|
|
354
|
+
export function maybeRequestStorageCleanupPolicyRun(
|
|
355
|
+
reason: PolicyRunReason,
|
|
356
|
+
opts?: Omit<RequestPolicyRunOptions, "reason">,
|
|
357
|
+
): void {
|
|
358
|
+
try {
|
|
359
|
+
const policy = readStorageCleanupPolicyFromConfig();
|
|
360
|
+
if (!policy.enabled) return;
|
|
361
|
+
if (!isPolicyDue(policy, Date.now(), reason)) return;
|
|
362
|
+
requestStorageCleanupPolicyRun({ ...opts, reason });
|
|
363
|
+
} catch {
|
|
364
|
+
// Keep scheduler/startup non-throwing.
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long-running schedule tick for daily/weekly storage cleanup policies.
|
|
3
|
+
* Unref'd interval — never keeps the process alive alone (safe for tests).
|
|
4
|
+
* Startup kickoff is tracked here so shutdown can cancel it.
|
|
5
|
+
*/
|
|
6
|
+
import { maybeRequestStorageCleanupPolicyRun } from "./policy-job";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_INTERVAL_MS = 60 * 60 * 1000; // 1h
|
|
9
|
+
|
|
10
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
11
|
+
let startupTimer: ReturnType<typeof setTimeout> | null = null;
|
|
12
|
+
|
|
13
|
+
export function startStorageCleanupScheduler(intervalMs = DEFAULT_INTERVAL_MS): void {
|
|
14
|
+
if (timer) return;
|
|
15
|
+
timer = setInterval(() => {
|
|
16
|
+
maybeRequestStorageCleanupPolicyRun("schedule");
|
|
17
|
+
}, intervalMs);
|
|
18
|
+
timer.unref?.();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Fire-and-forget startup evaluation; cancellable via stopStorageCleanupScheduler. */
|
|
22
|
+
export function scheduleStorageCleanupStartupRun(): void {
|
|
23
|
+
if (startupTimer) return;
|
|
24
|
+
startupTimer = setTimeout(() => {
|
|
25
|
+
startupTimer = null;
|
|
26
|
+
maybeRequestStorageCleanupPolicyRun("startup");
|
|
27
|
+
}, 0);
|
|
28
|
+
startupTimer.unref?.();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function stopStorageCleanupScheduler(): void {
|
|
32
|
+
if (timer) {
|
|
33
|
+
clearInterval(timer);
|
|
34
|
+
timer = null;
|
|
35
|
+
}
|
|
36
|
+
if (startupTimer) {
|
|
37
|
+
clearTimeout(startupTimer);
|
|
38
|
+
startupTimer = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-thread entry for storage cleanup policy runs.
|
|
3
|
+
* Keeps archive scan / FS cleanup / SQLite reconcile off the proxy event loop.
|
|
4
|
+
*/
|
|
5
|
+
import { runStorageCleanupPolicy, type PolicyRunReason } from "./policy";
|
|
6
|
+
|
|
7
|
+
interface RunMessage {
|
|
8
|
+
type: "run";
|
|
9
|
+
requestId: string;
|
|
10
|
+
reason: PolicyRunReason;
|
|
11
|
+
force?: boolean;
|
|
12
|
+
codexHome?: string;
|
|
13
|
+
busyTimeoutMs?: number;
|
|
14
|
+
/** Test-only: block after loading the start-of-job policy (see holdAfterLoadMs). */
|
|
15
|
+
blockMs?: number;
|
|
16
|
+
/** Env snapshot — Workers may not see parent mutations on all platforms. */
|
|
17
|
+
env?: { CODEX_HOME?: string; OPENCODEX_HOME?: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isRunMessage(data: unknown): data is RunMessage {
|
|
21
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
22
|
+
const o = data as Record<string, unknown>;
|
|
23
|
+
return o.type === "run"
|
|
24
|
+
&& typeof o.requestId === "string"
|
|
25
|
+
&& (o.reason === "startup" || o.reason === "schedule" || o.reason === "manual");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare const self: Worker;
|
|
29
|
+
|
|
30
|
+
self.onmessage = (event: MessageEvent<unknown>) => {
|
|
31
|
+
if (!isRunMessage(event.data)) return;
|
|
32
|
+
const { requestId, reason, force, codexHome, busyTimeoutMs, blockMs, env } = event.data;
|
|
33
|
+
try {
|
|
34
|
+
if (env?.CODEX_HOME) process.env.CODEX_HOME = env.CODEX_HOME;
|
|
35
|
+
if (env?.OPENCODEX_HOME) process.env.OPENCODEX_HOME = env.OPENCODEX_HOME;
|
|
36
|
+
const result = runStorageCleanupPolicy({
|
|
37
|
+
reason,
|
|
38
|
+
force: force === true,
|
|
39
|
+
...(codexHome ? { codexHome } : {}),
|
|
40
|
+
...(busyTimeoutMs !== undefined ? { busyTimeoutMs } : {}),
|
|
41
|
+
...(typeof blockMs === "number" && Number.isFinite(blockMs) && blockMs > 0
|
|
42
|
+
? { holdAfterLoadMs: Math.floor(blockMs) }
|
|
43
|
+
: {}),
|
|
44
|
+
});
|
|
45
|
+
self.postMessage({ type: "done", requestId, result });
|
|
46
|
+
} catch (err) {
|
|
47
|
+
self.postMessage({
|
|
48
|
+
type: "error",
|
|
49
|
+
requestId,
|
|
50
|
+
message: err instanceof Error ? err.message : "worker_failed",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|