grepmax 0.26.64 → 0.26.66

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.
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.26.64",
13
+ "version": "0.26.66",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -485,7 +485,17 @@ When heavy work is paused, the daemon keeps metadata, bounded symbol/file lookup
485
485
 
486
486
  To serve bounded reads under an existing startup quarantine, run `gmax watch --daemon --read-only -b`. This preserves the safety markers. `status --json` reports `daemon.service.mode` and its reason; `doctor` reports paused availability and skips native corpus diagnostics. Pausing is sticky for that daemon process. Full service requires a deliberate restart after host recovery and quarantine review. Known critical OS or kernel pressure still stops even the read-only service.
487
487
 
488
- Full-table compaction and new FTS/ANN index builds are disabled in the containment release, including forced maintenance and doctor repair. Existing indexes remain readable; missing lexical indexes fall back to available retrieval. Retained copies are not recovered by this release. A separate exclusive prune-only path requires validation before deployment. Do not repeatedly force repair, use future cutoffs or manually remove index fragments.
488
+ Full-table compaction and new FTS/ANN index builds are disabled in the containment release, including forced maintenance and doctor repair. Existing indexes remain readable; missing lexical indexes fall back to available retrieval. Do not repeatedly force repair, use future cutoffs or manually remove index fragments.
489
+
490
+ The package includes a separate prune-only helper with pinned Lance 12 dependencies, plus an exclusively leased Node wrapper. Runtime preparation uses `uv >=0.12.18`, an existing Python interpreter and locked wheels; it does not download Python, build from source, load an embedding model or run during installation. The helper currently supports macOS and Linux. No timer, CLI repair or startup path calls it automatically. Explicit recovery has separate production admission; packaging does not authorize live cleanup.
491
+
492
+ `gmax recover --table /path/to/lancedb/chunks.lance --json` reads the durable recovery receipt without starting a daemon or preparing Python. `--check` samples admission without pruning. `--prune --version <current-version> --cutoff <absolute-ISO-timestamp>` explicitly requests one offline prune. A prior uncertain attempt requires `--acknowledge-uncertain <attempt-id>` after inspection. Recovery never creates/clears quarantine, stops other owners, retries automatically or enables rewriting.
493
+
494
+ Production recovery currently requires macOS, a persistent `autostart-disabled` file in the selected store home, and closed store owners. Recovery alone uses known healthy memory/kernel pressure, measured physical headroom and the shared aggregate resource budget; normal development retains its configured guard policy. The helper reserves 512 MiB of admission capacity; this is not an OS-enforced footprint cap. Recovery samples have an eight-second total ceiling, including the established five-second kernel probe allowance, with fresh sampling at deletion admission and five-second heartbeat scheduling. Unknown, stale, warning or critical measurements refuse/interrupt recovery. Pruning needs at least 1 MiB of measured metadata disk headroom, not full-table rewrite space. Initial wheel setup requires 512 MiB; an existing environment is checked against the lock offline without that download-space requirement.
495
+
496
+ The child waits for launch and deletion admission tokens, with verified helper PID/start identity recorded as the primary exclusive owner. Existing readers retain exclusion if the parent dies while the helper is alive. Current/tagged/post-cutoff metadata is verified after pruning; recovery refuses more than 256 protected versions before deletion to bound verification. Interrupted attempts require verification and explicit acknowledgement before retry. Linux/APFS fixtures exercise native retention and interruption; supported production host admission is tested separately and still runs freshly for each operation.
497
+
498
+ Explicit helper calls persist a private `.gmax-prune-state.json` receipt outside the table before deletion. Cancellation, invalid results, lost ownership and abandoned running receipts remain uncertain; a verified explicit retry records the prior uncertain attempt. Successful receipts include before/after file lengths, allocated bytes and filesystem free space. These measurements do not imply zero retained copies or a macOS physical-footprint cap. Current, tagged and post-cutoff versions are protected; unverified files are retained. Full-table rewriting remains disabled regardless of receipt state.
489
499
 
490
500
  `~/.gmax/logs/daemon.log` records containment skips and retained historical compaction outcomes, including duration, logical size, disk size before/after, free space, and bytes reclaimed. `gmax status --json` exposes the last outcome under `daemon.compaction`; its `at` timestamp identifies when it was recorded. Five-minute resource snapshots track footprint, RSS, heap, external/ArrayBuffer memory, Lance cache, workers and pending work for memory investigations. Reading status uses retained snapshots and does not scan the index directory. Logs rotate to `daemon.log.prev`.
491
501
 
package/dist/bin.js CHANGED
@@ -77,7 +77,7 @@ function selectHome() {
77
77
  const argv = process.argv.slice(2);
78
78
  // These enumerate/route stores per request rather than pinning the process
79
79
  // to its launch cwd's store. In particular an MCP session can serve both.
80
- if (argv[0] === "mcp" || argv[0] === "status")
80
+ if (argv[0] === "mcp" || argv[0] === "status" || argv[0] === "recover")
81
81
  return;
82
82
  const match = (0, stores_1.matchStore)(commandTarget(argv, process.cwd()));
83
83
  if (match.kind === "primary")
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.recover = void 0;
46
+ exports.parseRecoveryCutoff = parseRecoveryCutoff;
47
+ const fs = __importStar(require("node:fs"));
48
+ const path = __importStar(require("node:path"));
49
+ const commander_1 = require("commander");
50
+ const prune_state_1 = require("../lib/store/prune-state");
51
+ const recovery_1 = require("../lib/store/recovery");
52
+ function parseRecoveryCutoff(value) {
53
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/.exec(value);
54
+ if (!match)
55
+ throw new Error("--cutoff requires an absolute ISO timestamp with timezone");
56
+ const calendar = new Date(`${value.slice(0, 19)}Z`);
57
+ const parts = [
58
+ calendar.getUTCFullYear(),
59
+ calendar.getUTCMonth() + 1,
60
+ calendar.getUTCDate(),
61
+ calendar.getUTCHours(),
62
+ calendar.getUTCMinutes(),
63
+ calendar.getUTCSeconds(),
64
+ ];
65
+ const result = new Date(value);
66
+ if (parts.some((part, i) => part !== Number(match[i + 1])) ||
67
+ !Number.isFinite(result.getTime()))
68
+ throw new Error("--cutoff is not a valid calendar timestamp");
69
+ return result;
70
+ }
71
+ exports.recover = new commander_1.Command("recover")
72
+ .description("Inspect or explicitly prune an offline Lance store without rewriting it")
73
+ .requiredOption("--table <path>", "Existing .lance table directory")
74
+ .option("--check", "Check recovery admission without preparing or pruning")
75
+ .option("--prune", "Explicitly perform exclusively leased prune-only recovery")
76
+ .option("--version <number>", "Expected current table version", Number)
77
+ .option("--cutoff <timestamp>", "Real absolute ISO retention cutoff")
78
+ .option("--acknowledge-uncertain <attempt>", "Acknowledge an inspected uncertain attempt before explicit retry")
79
+ .option("--json", "Structured outcome and durable receipt")
80
+ .addHelpText("after", "\nStatus is metadata-only. Pruning requires a persistent autostart-disabled file in the store home, closed store owners, known healthy macOS resources, --version and --cutoff. Recovery does not stop services or clear containment. --check can refuse under unknown pressure without pausing normal service.")
81
+ .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
82
+ const controller = new AbortController();
83
+ const abort = () => controller.abort();
84
+ process.once("SIGINT", abort);
85
+ process.once("SIGTERM", abort);
86
+ try {
87
+ const report = yield (0, recovery_1.recoverStore)({
88
+ table: opts.table,
89
+ check: opts.check,
90
+ prune: opts.prune,
91
+ version: opts.version,
92
+ cutoff: opts.cutoff ? parseRecoveryCutoff(opts.cutoff) : undefined,
93
+ acknowledgeUncertain: opts.acknowledgeUncertain,
94
+ signal: controller.signal,
95
+ });
96
+ console.log(JSON.stringify(report, null, opts.json ? undefined : 2));
97
+ }
98
+ catch (error) {
99
+ let state = "unverified";
100
+ try {
101
+ state = (0, prune_state_1.readPruneState)(path.dirname(fs.realpathSync(opts.table)));
102
+ }
103
+ catch (_a) { }
104
+ console.error(JSON.stringify({
105
+ outcome: state === "unverified" || (state === null || state === void 0 ? void 0 : state.outcome) === "uncertain"
106
+ ? "uncertain"
107
+ : "refused",
108
+ error: error instanceof Error ? error.message : "Recovery failed",
109
+ state,
110
+ }));
111
+ process.exitCode = 1;
112
+ }
113
+ finally {
114
+ process.removeListener("SIGINT", abort);
115
+ process.removeListener("SIGTERM", abort);
116
+ }
117
+ }));
package/dist/index.js CHANGED
@@ -62,6 +62,7 @@ const peek_1 = require("./commands/peek");
62
62
  const plugin_1 = require("./commands/plugin");
63
63
  const project_1 = require("./commands/project");
64
64
  const recent_1 = require("./commands/recent");
65
+ const recover_1 = require("./commands/recover");
65
66
  const related_1 = require("./commands/related");
66
67
  const remove_1 = require("./commands/remove");
67
68
  const repair_1 = require("./commands/repair");
@@ -146,6 +147,7 @@ commander_1.program.addCommand(setup_1.setup);
146
147
  commander_1.program.addCommand(config_1.config);
147
148
  commander_1.program.addCommand(doctor_1.doctor);
148
149
  commander_1.program.addCommand(repair_1.repair);
150
+ commander_1.program.addCommand(recover_1.recover);
149
151
  // Plugins
150
152
  commander_1.program.addCommand(plugin_1.plugin);
151
153
  // Legacy plugin installers (hidden — use `gmax plugin` instead)
@@ -161,4 +163,8 @@ opencode_1.installOpencode._hidden = true;
161
163
  commander_1.program.addCommand(opencode_1.installOpencode);
162
164
  opencode_1.uninstallOpencode._hidden = true;
163
165
  commander_1.program.addCommand(opencode_1.uninstallOpencode);
166
+ // Recovery owns --version as a required table-version value. Keep the root
167
+ // package-version flag from consuming it; other commands retain global options.
168
+ if (process.argv[2] === "recover")
169
+ commander_1.program.enablePositionalOptions();
164
170
  commander_1.program.parse();
@@ -0,0 +1,425 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.runCleanupProcess = runCleanupProcess;
46
+ exports.prepareCleanupRuntime = prepareCleanupRuntime;
47
+ exports.pruneVersions = pruneVersions;
48
+ const node_child_process_1 = require("node:child_process");
49
+ const node_crypto_1 = require("node:crypto");
50
+ const fs = __importStar(require("node:fs"));
51
+ const path = __importStar(require("node:path"));
52
+ const config_1 = require("../../config");
53
+ const prune_state_1 = require("./prune-state");
54
+ const recovery_admission_1 = require("./recovery-admission");
55
+ const store_lease_1 = require("./store-lease");
56
+ const project = path.resolve(__dirname, "../../../lance-maintenance");
57
+ const MAX_OUTPUT_BYTES = 128 * 1024;
58
+ const MAX_HELPER_MS = 120000;
59
+ /** No shell, inherited service credentials, Python path hooks or source builds.
60
+ * Thread settings apply before Arrow/Lance imports in the child. */
61
+ function helperEnvironment() {
62
+ const env = {};
63
+ for (const name of [
64
+ "PATH",
65
+ "HOME",
66
+ "TMPDIR",
67
+ "TEMP",
68
+ "SystemRoot",
69
+ "LOCALAPPDATA",
70
+ "SSL_CERT_FILE",
71
+ "SSL_CERT_DIR",
72
+ ]) {
73
+ if (process.env[name] !== undefined)
74
+ env[name] = process.env[name];
75
+ }
76
+ return Object.assign(Object.assign({}, env), { RAYON_NUM_THREADS: "1", TOKIO_WORKER_THREADS: "1", OMP_NUM_THREADS: "1", OPENBLAS_NUM_THREADS: "1", MKL_NUM_THREADS: "1", NUMEXPR_NUM_THREADS: "1", UV_CONCURRENT_DOWNLOADS: "1", UV_CONCURRENT_INSTALLS: "1", UV_CONCURRENT_BUILDS: "1", UV_PYTHON_DOWNLOADS: "never" });
77
+ }
78
+ /** Settle only after close, including abort/timeout. Releasing an exclusive lease
79
+ * on AbortError before the child exits would let writers race native deletion. */
80
+ function runCleanupProcess(executable, args, options = {}) {
81
+ var _a;
82
+ if ((_a = options.signal) === null || _a === void 0 ? void 0 : _a.aborted)
83
+ return Promise.reject(new Error("Cleanup cancelled before launch"));
84
+ return new Promise((resolve, reject) => {
85
+ var _a, _b, _c;
86
+ var _d;
87
+ const child = (0, node_child_process_1.spawn)(executable, args, {
88
+ env: Object.assign(Object.assign({}, helperEnvironment()), options.env),
89
+ stdio: [options.admission ? "pipe" : "ignore", "pipe", "pipe"],
90
+ windowsHide: true,
91
+ });
92
+ let output = "";
93
+ let diagnostics = "";
94
+ let bytes = 0;
95
+ let failure;
96
+ let approved = false;
97
+ let launched = false;
98
+ let readyBuffer = "";
99
+ let monitor;
100
+ let forceKill;
101
+ const stop = (reason) => {
102
+ failure !== null && failure !== void 0 ? failure : (failure = new Error(`${reason}; cleanup completion is uncertain`));
103
+ child.kill("SIGTERM");
104
+ forceKill !== null && forceKill !== void 0 ? forceKill : (forceKill = setTimeout(() => child.kill("SIGKILL"), 1000));
105
+ };
106
+ const abort = () => stop("Cleanup cancelled");
107
+ const timeout = setTimeout(() => stop("Cleanup timed out"), Math.min((_d = options.timeoutMs) !== null && _d !== void 0 ? _d : MAX_HELPER_MS, MAX_HELPER_MS));
108
+ (_a = options.signal) === null || _a === void 0 ? void 0 : _a.addEventListener("abort", abort, { once: true });
109
+ // stderr is deliberately not returned: child diagnostics may contain paths,
110
+ // tokens or registry URLs. Retain a concrete bounded exit/signal/error code.
111
+ for (const stream of [child.stdout, child.stderr]) {
112
+ stream.on("data", (chunk) => {
113
+ bytes += chunk.length;
114
+ if (bytes > MAX_OUTPUT_BYTES)
115
+ stop("Cleanup output exceeded limit");
116
+ else if (stream === child.stdout) {
117
+ if (options.admission && !approved) {
118
+ readyBuffer += chunk.toString("utf8");
119
+ let end = readyBuffer.indexOf("\n");
120
+ while (end >= 0 && !approved && !failure) {
121
+ try {
122
+ const phase = JSON.parse(readyBuffer.slice(0, end)).admission;
123
+ readyBuffer = readyBuffer.slice(end + 1);
124
+ if (phase === "launch" && !launched) {
125
+ if (!child.stdin)
126
+ throw Error();
127
+ child.stdin.write(`${options.admission.nonce}\n`);
128
+ launched = true;
129
+ }
130
+ else if (phase === "ready" && launched) {
131
+ options.admission.approve();
132
+ child.stdin.end(`${options.admission.nonce}\n`);
133
+ approved = true;
134
+ output += readyBuffer;
135
+ readyBuffer = "";
136
+ }
137
+ else
138
+ throw Error();
139
+ }
140
+ catch (_a) {
141
+ stop("Prune deletion admission refused");
142
+ }
143
+ end = readyBuffer.indexOf("\n");
144
+ }
145
+ }
146
+ else
147
+ output += chunk.toString("utf8");
148
+ }
149
+ else
150
+ diagnostics += chunk.toString("utf8");
151
+ });
152
+ }
153
+ child.on("error", (error) => {
154
+ var _a;
155
+ failure !== null && failure !== void 0 ? failure : (failure = new Error(`Cleanup process could not start (${(_a = error.code) !== null && _a !== void 0 ? _a : "unknown"})`));
156
+ });
157
+ child.once("close", (code, signal) => {
158
+ var _a, _b;
159
+ var _c;
160
+ clearTimeout(timeout);
161
+ if (forceKill)
162
+ clearTimeout(forceKill);
163
+ if (monitor)
164
+ clearInterval(monitor);
165
+ try {
166
+ (_a = options.admission) === null || _a === void 0 ? void 0 : _a.close();
167
+ }
168
+ catch (_d) {
169
+ failure !== null && failure !== void 0 ? failure : (failure = new Error("Prune ownership/resource close failed; completion is uncertain"));
170
+ }
171
+ if (options.admission && !approved)
172
+ failure !== null && failure !== void 0 ? failure : (failure = new Error("Prune deletion was not admitted; completion is uncertain"));
173
+ (_b = options.signal) === null || _b === void 0 ? void 0 : _b.removeEventListener("abort", abort);
174
+ if (failure)
175
+ reject(failure);
176
+ else if (code !== 0) {
177
+ // Whitelist categories, never repeat registry credentials, file paths,
178
+ // full traceback text or arbitrary diagnostics from a subprocess.
179
+ const detail = /lockfile.*(?:needs|updated|out.of.date)|locked.*(?:changed|update)/i.test(diagnostics)
180
+ ? "locked project/runtime mismatch"
181
+ : /no space left|ENOSPC/i.test(diagnostics)
182
+ ? "disk full"
183
+ : /no (?:python|interpreter)|python.*(?:not found|unavailable)/i.test(diagnostics)
184
+ ? "compatible Python unavailable"
185
+ : /failed to (?:download|fetch)|connection refused|network|cache missing/i.test(diagnostics)
186
+ ? "download, network or cache unavailable"
187
+ : /exclusive (?:lease|ownership)|shared store owners/i.test(diagnostics)
188
+ ? "exclusive ownership verification failed"
189
+ : /protected state|table changed|state.*changed/i.test(diagnostics)
190
+ ? "current/protected state verification failed"
191
+ : "unclassified child failure";
192
+ reject(new Error(`Cleanup process exited (${(_c = signal !== null && signal !== void 0 ? signal : code) !== null && _c !== void 0 ? _c : "unknown"}): ${detail}; cleanup completion is uncertain`));
193
+ }
194
+ else
195
+ resolve(output);
196
+ });
197
+ (_b = child.stdin) === null || _b === void 0 ? void 0 : _b.on("error", () => stop("Prune admission pipe failed"));
198
+ if (options.admission) {
199
+ try {
200
+ if (!child.pid)
201
+ throw Error();
202
+ options.admission.start(child.pid);
203
+ monitor = setInterval(() => {
204
+ try {
205
+ options.admission.check();
206
+ }
207
+ catch (_a) {
208
+ stop("Prune host/resource admission changed");
209
+ }
210
+ }, 5000);
211
+ }
212
+ catch (_e) {
213
+ stop("Prune helper launch admission refused");
214
+ }
215
+ }
216
+ if ((_c = options.signal) === null || _c === void 0 ? void 0 : _c.aborted)
217
+ abort();
218
+ });
219
+ }
220
+ let preparation;
221
+ /** Explicit recovery only. Containment has no automatic caller of this setup. */
222
+ function prepareCleanupRuntime(signal) {
223
+ return __awaiter(this, void 0, void 0, function* () {
224
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted)
225
+ throw new Error("Cleanup setup cancelled");
226
+ if (process.platform !== "darwin" && process.platform !== "linux")
227
+ throw new Error("Prune-only runtime currently supports macOS and Linux");
228
+ if (!preparation) {
229
+ preparation = (() => __awaiter(this, void 0, void 0, function* () {
230
+ const stdout = yield runCleanupProcess("uv", ["--version"], {
231
+ signal,
232
+ timeoutMs: 10000,
233
+ });
234
+ const match = /^uv (\d+)\.(\d+)\.(\d+)/.exec(stdout);
235
+ if (!match ||
236
+ (Number(match[1]) === 0 &&
237
+ (Number(match[2]) < 12 ||
238
+ (Number(match[2]) === 12 && Number(match[3]) < 18)))) {
239
+ throw new Error("Prune-only runtime requires uv >=0.12.18");
240
+ }
241
+ const hash = (0, node_crypto_1.createHash)("sha256")
242
+ .update(fs.readFileSync(path.join(project, "pyproject.toml")))
243
+ .update(fs.readFileSync(path.join(project, "uv.lock")))
244
+ .digest("hex")
245
+ .slice(0, 16);
246
+ const root = path.join(config_1.PATHS.sharedRoot, "runtimes");
247
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
248
+ if (fs.lstatSync(root).isSymbolicLink())
249
+ throw new Error("Unverified cleanup runtime directory");
250
+ fs.chmodSync(root, 0o700);
251
+ const environment = path.join(root, `lance-cleanup-${hash}`);
252
+ const python = path.join(environment, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
253
+ if (fs.existsSync(environment)) {
254
+ if (!fs.lstatSync(environment).isDirectory() ||
255
+ fs.lstatSync(environment).isSymbolicLink())
256
+ throw new Error("Unverified cleanup environment");
257
+ yield runCleanupProcess("uv", [
258
+ "sync",
259
+ "--check",
260
+ "--locked",
261
+ "--offline",
262
+ "--project",
263
+ project,
264
+ "--no-dev",
265
+ "--no-python-downloads",
266
+ "--no-build",
267
+ ], {
268
+ signal,
269
+ timeoutMs: 10000,
270
+ env: { UV_PROJECT_ENVIRONMENT: environment },
271
+ });
272
+ return { python, script: path.join(project, "prune.py") };
273
+ }
274
+ const available = fs.statfsSync(root);
275
+ const freeBytes = available.bavail * available.bsize;
276
+ if (!Number.isFinite(freeBytes) || freeBytes < 512 * Math.pow(1024, 2)) {
277
+ throw new Error("Cleanup runtime setup refused: less than 512MiB free");
278
+ }
279
+ yield runCleanupProcess("uv", [
280
+ "sync",
281
+ "--locked",
282
+ "--project",
283
+ project,
284
+ "--no-dev",
285
+ "--no-python-downloads",
286
+ "--no-build",
287
+ ], {
288
+ signal,
289
+ env: { UV_PROJECT_ENVIRONMENT: environment },
290
+ });
291
+ fs.chmodSync(environment, 0o700);
292
+ return {
293
+ python,
294
+ script: path.join(project, "prune.py"),
295
+ };
296
+ }))().catch((error) => {
297
+ preparation = undefined;
298
+ throw error;
299
+ });
300
+ }
301
+ return preparation;
302
+ });
303
+ }
304
+ /** Explicit recovery only. Caller must have drained local work/readers before
305
+ * acquiring the exclusive lease. The lease cannot be released during this call. */
306
+ function pruneVersions(runtime, store, version, cutoff, options) {
307
+ return __awaiter(this, void 0, void 0, function* () {
308
+ var _a;
309
+ if (!((options === null || options === void 0 ? void 0 : options.lease) instanceof store_lease_1.StoreLease))
310
+ throw new Error("Pruning requires a verified StoreLease token");
311
+ if (!Number.isSafeInteger(version) ||
312
+ version < 1 ||
313
+ !Number.isSafeInteger(cutoff.getTime()) ||
314
+ cutoff.getTime() > Date.now()) {
315
+ throw new Error("Invalid prune version or future cutoff");
316
+ }
317
+ const table = fs.realpathSync(store);
318
+ if (!table.endsWith(".lance") || !fs.statSync(table).isDirectory())
319
+ throw new Error("Pruning requires a local Lance table");
320
+ const storeDir = path.dirname(table);
321
+ if ((_a = options.signal) === null || _a === void 0 ? void 0 : _a.aborted)
322
+ throw new Error("Cleanup cancelled before launch");
323
+ let attempt;
324
+ let admission;
325
+ try {
326
+ const result = yield options.lease.withExclusiveUse(storeDir, () => __awaiter(this, void 0, void 0, function* () {
327
+ var _a;
328
+ const previous = (0, prune_state_1.readPruneState)(storeDir);
329
+ if ((previous === null || previous === void 0 ? void 0 : previous.outcome) === "uncertain" &&
330
+ options.acknowledgeUncertain !== previous.attemptId)
331
+ throw new Error(`Previous prune completion is uncertain; explicit acknowledgement required for attempt ${previous.attemptId}`);
332
+ admission = (_a = options.admission) !== null && _a !== void 0 ? _a : (0, recovery_admission_1.admitPrune)(storeDir);
333
+ attempt = {
334
+ schemaVersion: 1,
335
+ storeIdentity: fs.realpathSync(storeDir),
336
+ attemptId: (0, node_crypto_1.randomUUID)(),
337
+ version,
338
+ cutoffMs: cutoff.getTime(),
339
+ startedAt: Date.now(),
340
+ outcome: "running",
341
+ previousUncertainAttemptId: (previous === null || previous === void 0 ? void 0 : previous.outcome) === "uncertain"
342
+ ? previous.attemptId
343
+ : previous === null || previous === void 0 ? void 0 : previous.previousUncertainAttemptId,
344
+ };
345
+ (0, prune_state_1.writePruneState)(attempt);
346
+ let unpin;
347
+ const stdout = yield runCleanupProcess(runtime.python, [
348
+ "-I",
349
+ runtime.script,
350
+ "--store",
351
+ table,
352
+ "--version",
353
+ String(version),
354
+ "--cutoff-ms",
355
+ String(cutoff.getTime()),
356
+ "--lease-owner",
357
+ (0, store_lease_1.storeLeasePaths)(storeDir).intentOwnerFile,
358
+ "--lease-nonce",
359
+ options.lease.owner.nonce,
360
+ "--require-admission",
361
+ ], {
362
+ signal: options.signal,
363
+ admission: {
364
+ nonce: options.lease.owner.nonce,
365
+ start: (pid) => {
366
+ unpin = options.lease.pinExclusiveHelper(pid);
367
+ admission.start(pid);
368
+ },
369
+ approve: () => admission.approve(),
370
+ check: () => admission.check(),
371
+ close: () => {
372
+ try {
373
+ unpin === null || unpin === void 0 ? void 0 : unpin();
374
+ }
375
+ finally {
376
+ admission.close();
377
+ admission = undefined;
378
+ }
379
+ },
380
+ },
381
+ });
382
+ const result = JSON.parse(stdout);
383
+ if (result.engine !== "12.0.0" ||
384
+ result.version !== version ||
385
+ result.rewritten !== false ||
386
+ ![
387
+ "fragments",
388
+ "bytesRemoved",
389
+ "versionsRemoved",
390
+ "fileBytesBefore",
391
+ "fileBytesAfter",
392
+ "allocatedBytesBefore",
393
+ "allocatedBytesAfter",
394
+ "freeBytesBefore",
395
+ "freeBytesAfter",
396
+ ].every((key) => {
397
+ const value = result[key];
398
+ return (typeof value === "number" &&
399
+ Number.isSafeInteger(value) &&
400
+ value >= 0);
401
+ }))
402
+ throw new Error("Invalid prune-only cleanup result; cleanup completion is uncertain");
403
+ return result;
404
+ }));
405
+ // withExclusiveUse verifies ownership again before this synchronous write.
406
+ // The caller still holds the lease; it cannot release it during the helper.
407
+ (0, prune_state_1.writePruneState)(Object.assign(Object.assign({}, attempt), { outcome: "completed", finishedAt: Date.now(), result }));
408
+ return result;
409
+ }
410
+ catch (error) {
411
+ if (attempt) {
412
+ try {
413
+ (0, prune_state_1.writePruneState)(Object.assign(Object.assign({}, attempt), { outcome: "uncertain", finishedAt: Date.now() }));
414
+ }
415
+ catch (_b) {
416
+ throw new Error("Prune completion is uncertain; durable outcome could not be recorded");
417
+ }
418
+ }
419
+ throw error;
420
+ }
421
+ finally {
422
+ admission === null || admission === void 0 ? void 0 : admission.close();
423
+ }
424
+ });
425
+ }