@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913
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/index-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-D7ynYo2K.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-CWXut3rG.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import type { CleanupErrorCode } from "./types";
|
|
5
|
+
|
|
6
|
+
const STATE_DB_FILE = /^state_(\d+)\.sqlite$/;
|
|
7
|
+
|
|
8
|
+
const LOGS_DB_FILE = /^logs_(\d+)\.sqlite$/;
|
|
9
|
+
|
|
10
|
+
const GOALS_DB_FILE = /^goals_(\d+)\.sqlite$/;
|
|
11
|
+
|
|
12
|
+
const MEMORIES_DB_FILE = /^memories_(\d+)\.sqlite$/;
|
|
13
|
+
|
|
14
|
+
/** Chunk size for `IN (...)` binds; spawn-edge checks bind each id twice. */
|
|
15
|
+
export const SQLITE_ID_CHUNK = 200;
|
|
16
|
+
|
|
17
|
+
export function chunkIds(ids: string[], chunkSize: number): string[][] {
|
|
18
|
+
const chunks: string[][] = [];
|
|
19
|
+
for (let i = 0; i < ids.length; i += chunkSize) chunks.push(ids.slice(i, i + chunkSize));
|
|
20
|
+
return chunks;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Newest `prefix_N.sqlite` under CODEX_HOME, or null when absent. */
|
|
24
|
+
function newestVersionedDb(codexHome: string, pattern: RegExp): string | null {
|
|
25
|
+
let best: string | null = null;
|
|
26
|
+
let bestVersion = -1;
|
|
27
|
+
let names: string[] = [];
|
|
28
|
+
try {
|
|
29
|
+
names = readdirSync(codexHome);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
for (const name of names) {
|
|
34
|
+
const match = name.match(pattern);
|
|
35
|
+
if (!match) continue;
|
|
36
|
+
const version = Number(match[1]);
|
|
37
|
+
if (version > bestVersion) {
|
|
38
|
+
bestVersion = version;
|
|
39
|
+
best = name;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return best ? join(codexHome, best) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function newestStateDb(codexHome: string): string | null {
|
|
46
|
+
return newestVersionedDb(codexHome, STATE_DB_FILE);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RuntimeDbPaths {
|
|
50
|
+
state: string | null;
|
|
51
|
+
logs: string | null;
|
|
52
|
+
goals: string | null;
|
|
53
|
+
memories: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function discoverRuntimeDbPaths(codexHome: string): RuntimeDbPaths {
|
|
57
|
+
return {
|
|
58
|
+
state: newestVersionedDb(codexHome, STATE_DB_FILE),
|
|
59
|
+
logs: newestVersionedDb(codexHome, LOGS_DB_FILE),
|
|
60
|
+
goals: newestVersionedDb(codexHome, GOALS_DB_FILE),
|
|
61
|
+
memories: newestVersionedDb(codexHome, MEMORIES_DB_FILE),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function openDbWritable(dbPath: string, busyTimeoutMs = 100): Database {
|
|
66
|
+
const db = new Database(dbPath);
|
|
67
|
+
try {
|
|
68
|
+
// bun:sqlite exposes a binding-level timeout; set both so Windows lock waits
|
|
69
|
+
// honor the caller's budget (pragma alone has been flaky under CI contention).
|
|
70
|
+
(db as Database & { timeout?: number }).timeout = busyTimeoutMs;
|
|
71
|
+
} catch {
|
|
72
|
+
/* older bindings */
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
|
|
76
|
+
} catch {
|
|
77
|
+
/* older sqlite */
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
81
|
+
} catch {
|
|
82
|
+
/* ignore */
|
|
83
|
+
}
|
|
84
|
+
return db;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isBusyError(error: unknown): boolean {
|
|
88
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
89
|
+
const code = (error as { code?: string })?.code ?? "";
|
|
90
|
+
return (
|
|
91
|
+
code === "SQLITE_BUSY" ||
|
|
92
|
+
code === "SQLITE_LOCKED" ||
|
|
93
|
+
/SQLITE_BUSY|SQLITE_LOCKED|database is locked|database table is locked/i.test(msg)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function mapDbError(error: unknown): CleanupErrorCode {
|
|
98
|
+
if (isBusyError(error)) return "codex_busy";
|
|
99
|
+
return "db_reconcile_failed";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Probe a single DB with BEGIN IMMEDIATE; missing path is a no-op success. */
|
|
103
|
+
function probeDbWritable(
|
|
104
|
+
path: string | null,
|
|
105
|
+
busyTimeoutMs: number,
|
|
106
|
+
): { ok: true } | { ok: false; error: CleanupErrorCode } {
|
|
107
|
+
if (!path || !existsSync(path)) return { ok: true };
|
|
108
|
+
let db: Database | undefined;
|
|
109
|
+
try {
|
|
110
|
+
db = openDbWritable(path, busyTimeoutMs);
|
|
111
|
+
db.exec("BEGIN IMMEDIATE");
|
|
112
|
+
db.exec("ROLLBACK");
|
|
113
|
+
return { ok: true };
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (isBusyError(error)) return { ok: false, error: "codex_busy" };
|
|
116
|
+
return { ok: false, error: "db_reconcile_failed" };
|
|
117
|
+
} finally {
|
|
118
|
+
try { db?.close(); } catch { /* */ }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* True when every present Codex runtime DB can be written (BEGIN IMMEDIATE).
|
|
124
|
+
* Busy / corrupt stores abort cleanup before any filesystem mutation.
|
|
125
|
+
*/
|
|
126
|
+
export function probeStateDbWritable(
|
|
127
|
+
codexHome: string,
|
|
128
|
+
busyTimeoutMs = 100,
|
|
129
|
+
): { ok: true; path: string } | { ok: false; error: CleanupErrorCode } {
|
|
130
|
+
const paths = discoverRuntimeDbPaths(codexHome);
|
|
131
|
+
for (const path of [paths.state, paths.logs, paths.goals, paths.memories]) {
|
|
132
|
+
const probed = probeDbWritable(path, busyTimeoutMs);
|
|
133
|
+
if (!probed.ok) return probed;
|
|
134
|
+
}
|
|
135
|
+
return { ok: true, path: paths.state ?? "" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function tableExists(db: Database, name: string): boolean {
|
|
139
|
+
const row = db.query<{ name: string }, [string]>(
|
|
140
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
|
141
|
+
).get(name);
|
|
142
|
+
return Boolean(row);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function columnExists(db: Database, table: string, column: string): boolean {
|
|
146
|
+
if (!tableExists(db, table)) return false;
|
|
147
|
+
// `table` is only ever a hardcoded identifier already verified via sqlite_master.
|
|
148
|
+
const rows = db.query<{ name: string }, []>(
|
|
149
|
+
`PRAGMA table_info("${table.replaceAll('"', '""')}")`,
|
|
150
|
+
).all();
|
|
151
|
+
return rows.some(r => r.name === column);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ReconcileErr {
|
|
155
|
+
ok: false;
|
|
156
|
+
error: CleanupErrorCode;
|
|
157
|
+
/** True when satellite rows were mutated and could not all be restored. */
|
|
158
|
+
satelliteRestoreFailed?: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type SqlRow = Record<string, string | number | bigint | null | Uint8Array>;
|
|
162
|
+
|
|
163
|
+
function quoteIdent(name: string): string {
|
|
164
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function selectRows(db: Database, sql: string, params: Array<string | number>): SqlRow[] {
|
|
168
|
+
return db.query<SqlRow, Array<string | number>>(sql).all(...params) as SqlRow[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function tableColumnNames(db: Database, table: string): Set<string> {
|
|
172
|
+
if (!tableExists(db, table)) return new Set();
|
|
173
|
+
const rows = db.query<{ name: string }, []>(
|
|
174
|
+
`PRAGMA table_info("${table.replaceAll('"', '""')}")`,
|
|
175
|
+
).all();
|
|
176
|
+
return new Set(rows.map(r => r.name));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Insert rows with ON CONFLICT DO NOTHING; returns only rows that were newly inserted. */
|
|
180
|
+
export function insertRowsConflictIgnore(db: Database, table: string, rows: SqlRow[]): SqlRow[] {
|
|
181
|
+
const inserted: SqlRow[] = [];
|
|
182
|
+
if (rows.length === 0) return inserted;
|
|
183
|
+
const allowed = tableColumnNames(db, table);
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
const cols = Object.keys(row).filter(c => allowed.has(c));
|
|
186
|
+
if (cols.length === 0) continue;
|
|
187
|
+
const result = db.run(
|
|
188
|
+
`INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${cols.map(() => "?").join(", ")}) ON CONFLICT DO NOTHING`,
|
|
189
|
+
cols.map(c => row[c] as string | number | bigint | null | Uint8Array),
|
|
190
|
+
);
|
|
191
|
+
if (result.changes > 0) inserted.push(row);
|
|
192
|
+
}
|
|
193
|
+
return inserted;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function updateRowFromSnapshot(
|
|
197
|
+
db: Database,
|
|
198
|
+
table: string,
|
|
199
|
+
row: SqlRow,
|
|
200
|
+
pkCols: string[],
|
|
201
|
+
): void {
|
|
202
|
+
const cols = Object.keys(row).filter(c => !pkCols.includes(c));
|
|
203
|
+
if (cols.length === 0) return;
|
|
204
|
+
const sets = cols.map(c => `${quoteIdent(c)} = ?`).join(", ");
|
|
205
|
+
const where = pkCols.map(c => `${quoteIdent(c)} = ?`).join(" AND ");
|
|
206
|
+
db.run(
|
|
207
|
+
`UPDATE ${quoteIdent(table)} SET ${sets} WHERE ${where}`,
|
|
208
|
+
[
|
|
209
|
+
...cols.map(c => row[c] as string | number | bigint | null | Uint8Array),
|
|
210
|
+
...pkCols.map(c => row[c] as string | number | bigint | null | Uint8Array),
|
|
211
|
+
],
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizeSqlValue(
|
|
216
|
+
v: string | number | bigint | null | Uint8Array | undefined,
|
|
217
|
+
): string {
|
|
218
|
+
if (v === null || v === undefined) return "";
|
|
219
|
+
if (typeof v === "bigint") return v.toString();
|
|
220
|
+
if (v instanceof Uint8Array) return Buffer.from(v).toString("base64");
|
|
221
|
+
return String(v);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function sqlRowEqual(a: SqlRow, b: SqlRow): boolean {
|
|
225
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
226
|
+
for (const key of keys) {
|
|
227
|
+
if (normalizeSqlValue(a[key]) !== normalizeSqlValue(b[key])) return false;
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function withWritableDb(
|
|
233
|
+
path: string,
|
|
234
|
+
busyTimeoutMs: number,
|
|
235
|
+
body: (db: Database) => void,
|
|
236
|
+
): { ok: true } | ReconcileErr {
|
|
237
|
+
let db: Database | undefined;
|
|
238
|
+
try {
|
|
239
|
+
db = openDbWritable(path, busyTimeoutMs);
|
|
240
|
+
db.exec("BEGIN IMMEDIATE");
|
|
241
|
+
try {
|
|
242
|
+
body(db);
|
|
243
|
+
db.exec("COMMIT");
|
|
244
|
+
return { ok: true };
|
|
245
|
+
} catch (error) {
|
|
246
|
+
try { db.exec("ROLLBACK"); } catch { /* */ }
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return { ok: false, error: mapDbError(error) };
|
|
251
|
+
} finally {
|
|
252
|
+
try { db?.close(); } catch { /* */ }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function isSqlRowArray(value: unknown): value is SqlRow[] {
|
|
257
|
+
return Array.isArray(value) && value.every(row => row && typeof row === "object" && !Array.isArray(row));
|
|
258
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveCodexHomeDir } from "../../codex/home";
|
|
4
|
+
import { discoverRuntimeDbPaths, probeStateDbWritable } from "./db";
|
|
5
|
+
import { createExclusiveStageDir, normalizeArchivedRolloutPath, writePrivateFile } from "./paths";
|
|
6
|
+
import { collectRestorePendingAcceptedDestRels } from "./pending";
|
|
7
|
+
import { candidateOverlapsPendingRestore, clampPercent, computeExactPreviewDigest, computePreviewDigest, listArchivedCandidates, previewArchivedCleanup, previewExactArchivedCleanup, resolveExactArchivedCandidates, selectOldestPercent } from "./preview";
|
|
8
|
+
import { loadThreadsForCleanup, reconcileDeletedThreads } from "./reconcile";
|
|
9
|
+
import type { ThreadSnapshot } from "./reconcile";
|
|
10
|
+
import { purgeStaged, removeEmptyTrashRoot, removeStageIfEmpty, rollbackStaged, stageCandidates, trashRelPath } from "./staging";
|
|
11
|
+
import type { ArchivedCandidate, CleanupErrorCode, CleanupManifestEntry, CleanupMode, CleanupPreview, CleanupResult } from "./types";
|
|
12
|
+
|
|
13
|
+
export interface ExecuteCleanupOptions {
|
|
14
|
+
percent: number;
|
|
15
|
+
mode: CleanupMode;
|
|
16
|
+
/** Required digest from preview; rejects when the candidate set drifted. */
|
|
17
|
+
digest: string;
|
|
18
|
+
/**
|
|
19
|
+
* Optional exact candidate set (logical relPaths). When set, selection bypasses
|
|
20
|
+
* percent rounding and the digest must match `computeExactPreviewDigest`.
|
|
21
|
+
*/
|
|
22
|
+
candidateRelPaths?: string[];
|
|
23
|
+
codexHome?: string;
|
|
24
|
+
/** Test-only: shrink busy_timeout so lock tests fail fast. */
|
|
25
|
+
busyTimeoutMs?: number;
|
|
26
|
+
now?: number;
|
|
27
|
+
/** Test-only failure injection for atomicity regressions. */
|
|
28
|
+
_test?: {
|
|
29
|
+
failManifestWrite?: boolean;
|
|
30
|
+
/** Observe the complete temp and prior destination before publication. Never serialized. */
|
|
31
|
+
beforeManifestReplace?: (
|
|
32
|
+
temporaryPath: string,
|
|
33
|
+
targetPath: string,
|
|
34
|
+
phase: "staging" | "pre-commit" | "purge-incomplete",
|
|
35
|
+
) => void;
|
|
36
|
+
failPurgeBasenames?: string[];
|
|
37
|
+
failRollbackBasenames?: string[];
|
|
38
|
+
blockStageDestBasenames?: string[];
|
|
39
|
+
failAfterLogsMutation?: boolean;
|
|
40
|
+
failAfterMemoriesMutation?: boolean;
|
|
41
|
+
failAfterGoalsMutation?: boolean;
|
|
42
|
+
failBeforeStateCommit?: boolean;
|
|
43
|
+
failSatelliteRestore?: boolean;
|
|
44
|
+
failSatelliteBackupWrite?: boolean;
|
|
45
|
+
failSatelliteBackupReplace?: boolean;
|
|
46
|
+
afterSatelliteMutations?: () => void;
|
|
47
|
+
beforeReconcileLock?: () => void;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Serializable cleanup test hooks allowed on the management API wire. */
|
|
52
|
+
export type CleanupWireTestHooks = Omit<
|
|
53
|
+
NonNullable<ExecuteCleanupOptions["_test"]>,
|
|
54
|
+
"afterSatelliteMutations" | "beforeReconcileLock" | "beforeManifestReplace"
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
function isStringArray(v: unknown): v is string[] {
|
|
58
|
+
return Array.isArray(v) && v.every(e => typeof e === "string");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Pick only allowlisted serializable hooks; drops all function hooks and unknown keys. */
|
|
62
|
+
export function pickWireCleanupTestHooks(raw: unknown): CleanupWireTestHooks | undefined {
|
|
63
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
64
|
+
const o = raw as Record<string, unknown>;
|
|
65
|
+
const out: CleanupWireTestHooks = {};
|
|
66
|
+
if (typeof o.failManifestWrite === "boolean") out.failManifestWrite = o.failManifestWrite;
|
|
67
|
+
if (isStringArray(o.failPurgeBasenames)) out.failPurgeBasenames = o.failPurgeBasenames;
|
|
68
|
+
if (isStringArray(o.failRollbackBasenames)) out.failRollbackBasenames = o.failRollbackBasenames;
|
|
69
|
+
if (isStringArray(o.blockStageDestBasenames)) out.blockStageDestBasenames = o.blockStageDestBasenames;
|
|
70
|
+
if (typeof o.failAfterLogsMutation === "boolean") out.failAfterLogsMutation = o.failAfterLogsMutation;
|
|
71
|
+
if (typeof o.failAfterMemoriesMutation === "boolean") out.failAfterMemoriesMutation = o.failAfterMemoriesMutation;
|
|
72
|
+
if (typeof o.failAfterGoalsMutation === "boolean") out.failAfterGoalsMutation = o.failAfterGoalsMutation;
|
|
73
|
+
if (typeof o.failBeforeStateCommit === "boolean") out.failBeforeStateCommit = o.failBeforeStateCommit;
|
|
74
|
+
if (typeof o.failSatelliteRestore === "boolean") out.failSatelliteRestore = o.failSatelliteRestore;
|
|
75
|
+
if (typeof o.failSatelliteBackupWrite === "boolean") out.failSatelliteBackupWrite = o.failSatelliteBackupWrite;
|
|
76
|
+
if (typeof o.failSatelliteBackupReplace === "boolean") out.failSatelliteBackupReplace = o.failSatelliteBackupReplace;
|
|
77
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fail(
|
|
81
|
+
mode: CleanupMode,
|
|
82
|
+
percent: number,
|
|
83
|
+
error: CleanupErrorCode,
|
|
84
|
+
extra?: { trashDir?: string },
|
|
85
|
+
): CleanupResult {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
mode,
|
|
89
|
+
percent,
|
|
90
|
+
count: 0,
|
|
91
|
+
bytes: 0,
|
|
92
|
+
removedPaths: [],
|
|
93
|
+
error,
|
|
94
|
+
...(extra?.trashDir ? { trashDir: extra.trashDir } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Execute archived cleanup bound to a preview digest.
|
|
100
|
+
* Stages every physical file, writes the recovery manifest, then commits DB deletes.
|
|
101
|
+
* Rollback never deletes a stage directory that still holds unrestored files.
|
|
102
|
+
*/
|
|
103
|
+
export function executeArchivedCleanup(options: ExecuteCleanupOptions): CleanupResult {
|
|
104
|
+
const codexHome = options.codexHome ?? resolveCodexHomeDir();
|
|
105
|
+
const mode = options.mode;
|
|
106
|
+
const percent = clampPercent(options.percent);
|
|
107
|
+
const busyTimeoutMs = options.busyTimeoutMs ?? 100;
|
|
108
|
+
const failRollback = new Set(options._test?.failRollbackBasenames ?? []);
|
|
109
|
+
const failPurge = new Set(options._test?.failPurgeBasenames ?? []);
|
|
110
|
+
const blockStageDest = new Set(options._test?.blockStageDestBasenames ?? []);
|
|
111
|
+
|
|
112
|
+
if (mode !== "quarantine" && mode !== "permanent") {
|
|
113
|
+
return fail(mode, percent, "invalid_mode");
|
|
114
|
+
}
|
|
115
|
+
if (typeof options.digest !== "string" || !/^[a-f0-9]{64}$/i.test(options.digest)) {
|
|
116
|
+
return fail(mode, percent, "invalid_digest");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let preview: CleanupPreview;
|
|
120
|
+
let unfilteredSelected: ArchivedCandidate[];
|
|
121
|
+
if (options.candidateRelPaths !== undefined) {
|
|
122
|
+
const selected = resolveExactArchivedCandidates(options.candidateRelPaths, codexHome);
|
|
123
|
+
if (selected === null) {
|
|
124
|
+
return fail(mode, percent, "stale_preview");
|
|
125
|
+
}
|
|
126
|
+
unfilteredSelected = selected;
|
|
127
|
+
preview = previewExactArchivedCleanup(selected, codexHome);
|
|
128
|
+
} else {
|
|
129
|
+
const all = listArchivedCandidates(codexHome);
|
|
130
|
+
unfilteredSelected = selectOldestPercent(all, percent);
|
|
131
|
+
preview = previewArchivedCleanup(percent, codexHome);
|
|
132
|
+
}
|
|
133
|
+
if (preview.digest.toLowerCase() !== options.digest.toLowerCase()) {
|
|
134
|
+
const pendingDestRels = collectRestorePendingAcceptedDestRels(codexHome);
|
|
135
|
+
const blocked = unfilteredSelected.filter(c => candidateOverlapsPendingRestore(c, pendingDestRels));
|
|
136
|
+
const unfilteredDigest = options.candidateRelPaths !== undefined
|
|
137
|
+
? computeExactPreviewDigest(unfilteredSelected)
|
|
138
|
+
: computePreviewDigest(unfilteredSelected, percent);
|
|
139
|
+
if (
|
|
140
|
+
unfilteredDigest.toLowerCase() === options.digest.toLowerCase()
|
|
141
|
+
&& blocked.length > 0
|
|
142
|
+
) {
|
|
143
|
+
return fail(mode, percent, "restore_pending_overlap");
|
|
144
|
+
}
|
|
145
|
+
return fail(mode, percent, "stale_preview");
|
|
146
|
+
}
|
|
147
|
+
const pendingDestRels = collectRestorePendingAcceptedDestRels(codexHome);
|
|
148
|
+
if (preview.candidates.some(c => candidateOverlapsPendingRestore(c, pendingDestRels))) {
|
|
149
|
+
return fail(mode, percent, "restore_pending_overlap");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (preview.candidates.length === 0) {
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
mode,
|
|
156
|
+
percent,
|
|
157
|
+
count: 0,
|
|
158
|
+
bytes: 0,
|
|
159
|
+
removedPaths: [],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const paths = discoverRuntimeDbPaths(codexHome);
|
|
164
|
+
const probe = probeStateDbWritable(codexHome, busyTimeoutMs);
|
|
165
|
+
if (!probe.ok) {
|
|
166
|
+
return fail(mode, percent, probe.error);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Preflight referenced-history / matching while DB is free, before any rename.
|
|
170
|
+
const loaded = loadThreadsForCleanup(paths.state ?? "", preview.candidates, codexHome, busyTimeoutMs);
|
|
171
|
+
if (!loaded.ok) {
|
|
172
|
+
return fail(mode, percent, loaded.error);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const epoch = options.now ?? Date.now();
|
|
176
|
+
let stageDir: string;
|
|
177
|
+
try {
|
|
178
|
+
stageDir = createExclusiveStageDir(codexHome, epoch);
|
|
179
|
+
} catch {
|
|
180
|
+
return fail(mode, percent, "fs_failed");
|
|
181
|
+
}
|
|
182
|
+
const trashDir = trashRelPath(codexHome, stageDir);
|
|
183
|
+
|
|
184
|
+
const threadByRelPath = new Map<string, ThreadSnapshot>();
|
|
185
|
+
for (const thread of loaded.threads) {
|
|
186
|
+
const normalized = normalizeArchivedRolloutPath(thread.rollout_path, codexHome);
|
|
187
|
+
if (normalized) threadByRelPath.set(normalized, thread);
|
|
188
|
+
}
|
|
189
|
+
const skippedReferencedPaths = loaded.skipped
|
|
190
|
+
.map(thread => normalizeArchivedRolloutPath(thread.rollout_path, codexHome))
|
|
191
|
+
.filter((path): path is string => path !== null);
|
|
192
|
+
const matchedPaths = new Set([
|
|
193
|
+
...threadByRelPath.keys(),
|
|
194
|
+
...skippedReferencedPaths,
|
|
195
|
+
]);
|
|
196
|
+
const candidates = preview.candidates.filter(candidate => {
|
|
197
|
+
return !matchedPaths.has(candidate.relPath) || threadByRelPath.has(candidate.relPath);
|
|
198
|
+
});
|
|
199
|
+
if (candidates.length === 0) {
|
|
200
|
+
removeStageIfEmpty(stageDir, []);
|
|
201
|
+
removeEmptyTrashRoot(codexHome);
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
mode,
|
|
205
|
+
percent,
|
|
206
|
+
count: 0,
|
|
207
|
+
bytes: 0,
|
|
208
|
+
removedPaths: [],
|
|
209
|
+
...(skippedReferencedPaths.length ? { skippedReferencedPaths } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const manifestEntries: CleanupManifestEntry[] = candidates.map(candidate => {
|
|
213
|
+
const thread = threadByRelPath.get(candidate.relPath);
|
|
214
|
+
return {
|
|
215
|
+
relPath: candidate.relPath,
|
|
216
|
+
bytes: candidate.bytes,
|
|
217
|
+
mtimeMs: candidate.mtimeMs,
|
|
218
|
+
physicalRelPaths: candidate.physicalRelPaths,
|
|
219
|
+
...(thread
|
|
220
|
+
? { threadId: thread.id, rolloutPath: thread.rollout_path, archived: thread.archived }
|
|
221
|
+
: {}),
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const writeManifest = (extra: Record<string, unknown> = {}) => {
|
|
226
|
+
writePrivateFile(
|
|
227
|
+
join(stageDir, "manifest.json"),
|
|
228
|
+
JSON.stringify({
|
|
229
|
+
quarantinedAt: epoch,
|
|
230
|
+
mode,
|
|
231
|
+
percent,
|
|
232
|
+
digest: preview.digest,
|
|
233
|
+
entries: manifestEntries,
|
|
234
|
+
...extra,
|
|
235
|
+
}, null, 2),
|
|
236
|
+
(temporaryPath, targetPath) => options._test?.beforeManifestReplace?.(
|
|
237
|
+
temporaryPath, targetPath, extra.staging ? "staging" : "pre-commit",
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Journal staged paths before the first rename so a crash mid-stage is recoverable.
|
|
243
|
+
try {
|
|
244
|
+
if (options._test?.failManifestWrite) {
|
|
245
|
+
throw new Error("test_fail_manifest_write");
|
|
246
|
+
}
|
|
247
|
+
writeManifest({ staging: true });
|
|
248
|
+
} catch {
|
|
249
|
+
removeStageIfEmpty(stageDir, []);
|
|
250
|
+
return fail(mode, percent, "fs_failed");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const stageResult = stageCandidates(codexHome, candidates, stageDir, {
|
|
254
|
+
blockDestBasenames: blockStageDest.size > 0 ? blockStageDest : undefined,
|
|
255
|
+
});
|
|
256
|
+
if (!stageResult.ok) {
|
|
257
|
+
const rolled = rollbackStaged(stageResult.staged, { failBasenames: failRollback });
|
|
258
|
+
removeStageIfEmpty(stageDir, rolled.remaining);
|
|
259
|
+
return fail(mode, percent, "fs_failed", rolled.restored ? undefined : { trashDir });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Final manifest before DB deletion so a mid-flight crash still has recovery metadata.
|
|
263
|
+
try {
|
|
264
|
+
writeManifest();
|
|
265
|
+
} catch {
|
|
266
|
+
const rolled = rollbackStaged(stageResult.staged, { failBasenames: failRollback });
|
|
267
|
+
removeStageIfEmpty(stageDir, rolled.remaining);
|
|
268
|
+
return fail(mode, percent, "fs_failed", rolled.restored ? undefined : { trashDir });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const deleted = reconcileDeletedThreads(
|
|
272
|
+
paths,
|
|
273
|
+
candidates,
|
|
274
|
+
codexHome,
|
|
275
|
+
busyTimeoutMs,
|
|
276
|
+
stageDir,
|
|
277
|
+
options._test,
|
|
278
|
+
);
|
|
279
|
+
if (!deleted.ok) {
|
|
280
|
+
const rolled = rollbackStaged(stageResult.staged, { failBasenames: failRollback });
|
|
281
|
+
// Keep the stage (and recovery manifest) when files or satellite DB rows remain unrestored.
|
|
282
|
+
const keepTrash = Boolean(deleted.satelliteRestoreFailed) || !rolled.restored;
|
|
283
|
+
if (!keepTrash) {
|
|
284
|
+
removeStageIfEmpty(stageDir, rolled.remaining);
|
|
285
|
+
removeEmptyTrashRoot(codexHome);
|
|
286
|
+
}
|
|
287
|
+
return fail(mode, percent, deleted.error, keepTrash ? { trashDir } : undefined);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const removedPaths = candidates.map(c => c.relPath);
|
|
291
|
+
const bytes = candidates.reduce((sum, c) => sum + c.bytes, 0);
|
|
292
|
+
|
|
293
|
+
if (mode === "quarantine") {
|
|
294
|
+
return {
|
|
295
|
+
ok: true,
|
|
296
|
+
mode,
|
|
297
|
+
percent,
|
|
298
|
+
count: removedPaths.length,
|
|
299
|
+
bytes,
|
|
300
|
+
trashDir,
|
|
301
|
+
removedPaths,
|
|
302
|
+
...(skippedReferencedPaths.length ? { skippedReferencedPaths } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Permanent: purge staged files only after a successful DB commit.
|
|
307
|
+
const purge = purgeStaged(stageResult.staged, { failBasenames: failPurge });
|
|
308
|
+
if (purge.remaining.length > 0) {
|
|
309
|
+
// Overwrite the pre-commit manifest so recovery reflects what actually survived.
|
|
310
|
+
const survivingRelPaths = new Set(purge.remaining.map(item => item.relPath));
|
|
311
|
+
try {
|
|
312
|
+
writePrivateFile(
|
|
313
|
+
join(stageDir, "manifest.json"),
|
|
314
|
+
JSON.stringify({
|
|
315
|
+
quarantinedAt: epoch,
|
|
316
|
+
mode: "permanent",
|
|
317
|
+
percent,
|
|
318
|
+
digest: preview.digest,
|
|
319
|
+
purgeIncomplete: true,
|
|
320
|
+
purgedRelPaths: purge.purged.map(item => item.relPath),
|
|
321
|
+
entries: manifestEntries
|
|
322
|
+
.map(entry => ({
|
|
323
|
+
...entry,
|
|
324
|
+
physicalRelPaths: entry.physicalRelPaths.filter(rel => survivingRelPaths.has(rel)),
|
|
325
|
+
}))
|
|
326
|
+
.filter(entry => entry.physicalRelPaths.length > 0),
|
|
327
|
+
}, null, 2),
|
|
328
|
+
(temporaryPath, targetPath) => options._test?.beforeManifestReplace?.(
|
|
329
|
+
temporaryPath, targetPath, "purge-incomplete",
|
|
330
|
+
),
|
|
331
|
+
);
|
|
332
|
+
} catch { /* best-effort: the pre-commit manifest is still on disk */ }
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
mode,
|
|
336
|
+
percent,
|
|
337
|
+
count: 0,
|
|
338
|
+
bytes: 0,
|
|
339
|
+
trashDir,
|
|
340
|
+
removedPaths: [],
|
|
341
|
+
error: "fs_failed",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try { rmSync(stageDir, { recursive: true, force: true }); } catch { /* empty dir */ }
|
|
346
|
+
// Drop an empty `.trash` root so permanent cleanup leaves no quarantine tree behind.
|
|
347
|
+
removeEmptyTrashRoot(codexHome);
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
ok: true,
|
|
351
|
+
mode,
|
|
352
|
+
percent,
|
|
353
|
+
count: removedPaths.length,
|
|
354
|
+
bytes,
|
|
355
|
+
removedPaths,
|
|
356
|
+
...(skippedReferencedPaths.length ? { skippedReferencedPaths } : {}),
|
|
357
|
+
};
|
|
358
|
+
}
|