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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +11 -1
- package/dist/bin.js +1 -1
- package/dist/commands/recover.js +117 -0
- package/dist/index.js +6 -0
- package/dist/lib/store/lance-cleanup.js +425 -0
- package/dist/lib/store/prune-state.js +113 -0
- package/dist/lib/store/recovery-admission.js +92 -0
- package/dist/lib/store/recovery.js +163 -0
- package/dist/lib/store/store-lease.js +119 -4
- package/dist/lib/utils/host-resource.js +3 -3
- package/dist/lib/utils/resource-budget.js +6 -6
- package/lance-maintenance/prune.py +200 -0
- package/lance-maintenance/pyproject.toml +9 -0
- package/lance-maintenance/uv.lock +531 -0
- package/package.json +5 -2
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.pruneStatePath = pruneStatePath;
|
|
37
|
+
exports.readPruneState = readPruneState;
|
|
38
|
+
exports.writePruneState = writePruneState;
|
|
39
|
+
const node_crypto_1 = require("node:crypto");
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
function pruneStatePath(storeDir) {
|
|
43
|
+
return path.join(storeDir, ".gmax-prune-state.json");
|
|
44
|
+
}
|
|
45
|
+
/** A running receipt survives parent death. Readers must treat it as uncertain,
|
|
46
|
+
* never as successful cleanup or permission to launch automatic maintenance. */
|
|
47
|
+
function readPruneState(storeDir) {
|
|
48
|
+
var _a, _b, _c;
|
|
49
|
+
const identity = fs.realpathSync(storeDir);
|
|
50
|
+
const target = pruneStatePath(identity);
|
|
51
|
+
let stat;
|
|
52
|
+
try {
|
|
53
|
+
stat = fs.lstatSync(target);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code === "ENOENT")
|
|
57
|
+
return null;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (!stat.isFile() || stat.size > 16 * 1024)
|
|
61
|
+
throw new Error("Prune state is unverified or oversized");
|
|
62
|
+
const state = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
63
|
+
if (state.schemaVersion !== 1 ||
|
|
64
|
+
state.storeIdentity !== identity ||
|
|
65
|
+
typeof state.attemptId !== "string" ||
|
|
66
|
+
!state.attemptId ||
|
|
67
|
+
!Number.isSafeInteger(state.version) ||
|
|
68
|
+
state.version < 1 ||
|
|
69
|
+
!Number.isSafeInteger(state.cutoffMs) ||
|
|
70
|
+
!Number.isSafeInteger(state.startedAt) ||
|
|
71
|
+
!["running", "uncertain", "completed"].includes(state.outcome) ||
|
|
72
|
+
(state.outcome === "completed" &&
|
|
73
|
+
(!Number.isSafeInteger(state.finishedAt) ||
|
|
74
|
+
((_a = state.result) === null || _a === void 0 ? void 0 : _a.version) !== state.version ||
|
|
75
|
+
((_b = state.result) === null || _b === void 0 ? void 0 : _b.engine) !== "12.0.0" ||
|
|
76
|
+
((_c = state.result) === null || _c === void 0 ? void 0 : _c.rewritten) !== false)))
|
|
77
|
+
throw new Error("Prune state is invalid; cleanup remains pending");
|
|
78
|
+
return state.outcome === "running"
|
|
79
|
+
? Object.assign(Object.assign({}, state), { outcome: "uncertain" }) : state;
|
|
80
|
+
}
|
|
81
|
+
/** Persist and sync before native deletion. Failure refuses the operation;
|
|
82
|
+
* failed final writes leave a running/uncertain receipt for the next reader. */
|
|
83
|
+
function writePruneState(state) {
|
|
84
|
+
const target = pruneStatePath(state.storeIdentity);
|
|
85
|
+
if (fs.existsSync(target)) {
|
|
86
|
+
const stat = fs.lstatSync(target);
|
|
87
|
+
if (!stat.isFile() || stat.size > 16 * 1024)
|
|
88
|
+
throw new Error("Prune state is unverified or oversized");
|
|
89
|
+
}
|
|
90
|
+
const temporary = path.join(state.storeIdentity, `.gmax-prune-${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
91
|
+
try {
|
|
92
|
+
const descriptor = fs.openSync(temporary, "wx", 0o600);
|
|
93
|
+
try {
|
|
94
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(state)}\n`);
|
|
95
|
+
fs.fsyncSync(descriptor);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
fs.closeSync(descriptor);
|
|
99
|
+
}
|
|
100
|
+
fs.renameSync(temporary, target);
|
|
101
|
+
const directory = fs.openSync(state.storeIdentity, "r");
|
|
102
|
+
try {
|
|
103
|
+
fs.fsyncSync(directory);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
fs.closeSync(directory);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
if (fs.existsSync(temporary))
|
|
111
|
+
fs.unlinkSync(temporary);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PRUNE_METADATA_HEADROOM_BYTES = void 0;
|
|
37
|
+
exports.createRecoveryBudget = createRecoveryBudget;
|
|
38
|
+
exports.assertRecoveryAdmission = assertRecoveryAdmission;
|
|
39
|
+
exports.admitPrune = admitPrune;
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const resource_budget_1 = require("../utils/resource-budget");
|
|
43
|
+
const safety_latch_1 = require("../utils/safety-latch");
|
|
44
|
+
const maintenance_policy_1 = require("./maintenance-policy");
|
|
45
|
+
exports.PRUNE_METADATA_HEADROOM_BYTES = 1024 * 1024;
|
|
46
|
+
/** Explicit recovery has aggregate admission without changing normal service
|
|
47
|
+
* policy. Offline autostart containment is allowed; a host safety stop is not. */
|
|
48
|
+
function createRecoveryBudget() {
|
|
49
|
+
return new resource_budget_1.ResourceBudget({
|
|
50
|
+
policy: () => "strict",
|
|
51
|
+
quarantine: safety_latch_1.safetyStopReason,
|
|
52
|
+
requireClientRegistration: false,
|
|
53
|
+
samplerOverrides: { sampleTimeoutMs: 8000, kernelProbeTimeoutMs: 5000 },
|
|
54
|
+
sampleMaxAgeMs: 10000,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function assertRecoveryAdmission(storeDir, budget) {
|
|
58
|
+
const marker = path.join(path.dirname(fs.realpathSync(storeDir)), "autostart-disabled");
|
|
59
|
+
const stat = fs.lstatSync(marker, { throwIfNoEntry: false });
|
|
60
|
+
if (!(stat === null || stat === void 0 ? void 0 : stat.isFile()) || stat.isSymbolicLink())
|
|
61
|
+
throw new Error("Recovery requires a persistent autostart-disabled file in the store home; close store owners separately");
|
|
62
|
+
const disk = (0, maintenance_policy_1.availableStoreDiskBytes)(storeDir);
|
|
63
|
+
if (!Number.isFinite(disk) || disk < exports.PRUNE_METADATA_HEADROOM_BYTES)
|
|
64
|
+
throw new Error("Prune metadata disk headroom is low or unknown");
|
|
65
|
+
const snapshot = budget.check();
|
|
66
|
+
if ((snapshot === null || snapshot === void 0 ? void 0 : snapshot.platform) !== "darwin")
|
|
67
|
+
throw new Error("Production recovery admission currently requires macOS host measurements");
|
|
68
|
+
if (snapshot.physicalFreeMb === null ||
|
|
69
|
+
!Number.isFinite(snapshot.physicalFreeMb) ||
|
|
70
|
+
snapshot.physicalFreeMb < resource_budget_1.HELPER_RESOURCE_RESERVE_MB)
|
|
71
|
+
throw new Error("Insufficient measured physical headroom for prune helper");
|
|
72
|
+
return snapshot;
|
|
73
|
+
}
|
|
74
|
+
function admitPrune(storeDir, budget = createRecoveryBudget()) {
|
|
75
|
+
assertRecoveryAdmission(storeDir, budget);
|
|
76
|
+
const reserve = budget.reserve(resource_budget_1.HELPER_RESOURCE_RESERVE_MB, "prune-helper");
|
|
77
|
+
let snapshot;
|
|
78
|
+
try {
|
|
79
|
+
snapshot = assertRecoveryAdmission(storeDir, budget);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
reserve.release();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
snapshot,
|
|
87
|
+
start: (pid) => reserve.attach(pid),
|
|
88
|
+
approve: () => assertRecoveryAdmission(storeDir, budget),
|
|
89
|
+
check: () => assertRecoveryAdmission(storeDir, budget),
|
|
90
|
+
close: () => reserve.release(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
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.recoverStore = recoverStore;
|
|
46
|
+
const fs = __importStar(require("node:fs"));
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
48
|
+
const resource_budget_1 = require("../utils/resource-budget");
|
|
49
|
+
const lance_cleanup_1 = require("./lance-cleanup");
|
|
50
|
+
const prune_state_1 = require("./prune-state");
|
|
51
|
+
const recovery_admission_1 = require("./recovery-admission");
|
|
52
|
+
const store_lease_1 = require("./store-lease");
|
|
53
|
+
function recoverStore(request) {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
var _a, _b, _c, _d;
|
|
56
|
+
(_a = request.signal) === null || _a === void 0 ? void 0 : _a.throwIfAborted();
|
|
57
|
+
const input = path.resolve(request.table);
|
|
58
|
+
const stat = fs.lstatSync(input);
|
|
59
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || !input.endsWith(".lance"))
|
|
60
|
+
throw new Error("Recovery requires an existing local Lance table directory, without a table symlink");
|
|
61
|
+
const table = fs.realpathSync(input);
|
|
62
|
+
const store = path.dirname(table);
|
|
63
|
+
const previous = (0, prune_state_1.readPruneState)(store);
|
|
64
|
+
if (!request.prune && !request.check)
|
|
65
|
+
return {
|
|
66
|
+
outcome: "status",
|
|
67
|
+
table,
|
|
68
|
+
state: previous,
|
|
69
|
+
fullTableRewriting: "disabled",
|
|
70
|
+
};
|
|
71
|
+
if (request.prune &&
|
|
72
|
+
(!Number.isSafeInteger(request.version) ||
|
|
73
|
+
request.version < 1 ||
|
|
74
|
+
!request.cutoff ||
|
|
75
|
+
!Number.isSafeInteger(request.cutoff.getTime()) ||
|
|
76
|
+
request.cutoff.getTime() > Date.now()))
|
|
77
|
+
throw new Error("Explicit prune requires a positive --version and a real, nonfuture --cutoff");
|
|
78
|
+
if (request.prune &&
|
|
79
|
+
(previous === null || previous === void 0 ? void 0 : previous.outcome) === "uncertain" &&
|
|
80
|
+
request.acknowledgeUncertain !== previous.attemptId)
|
|
81
|
+
throw new Error(`Previous prune completion is uncertain; inspect retained state, then use --acknowledge-uncertain ${previous.attemptId} for a verified explicit retry`);
|
|
82
|
+
const budget = (0, recovery_admission_1.createRecoveryBudget)();
|
|
83
|
+
(0, recovery_admission_1.assertRecoveryAdmission)(store, budget);
|
|
84
|
+
if (!request.prune) {
|
|
85
|
+
const admission = (0, recovery_admission_1.admitPrune)(store, budget);
|
|
86
|
+
try {
|
|
87
|
+
return {
|
|
88
|
+
outcome: "preflight-passed",
|
|
89
|
+
table,
|
|
90
|
+
state: previous,
|
|
91
|
+
host: admission.snapshot,
|
|
92
|
+
helperReserveMb: resource_budget_1.HELPER_RESOURCE_RESERVE_MB,
|
|
93
|
+
fullTableRewriting: "disabled",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
admission.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Setup is admitted and monitored before acquiring store exclusion. No owner
|
|
101
|
+
// is killed and no marker is created or cleared by recovery itself.
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const abort = () => controller.abort();
|
|
104
|
+
(_b = request.signal) === null || _b === void 0 ? void 0 : _b.addEventListener("abort", abort, { once: true });
|
|
105
|
+
if ((_c = request.signal) === null || _c === void 0 ? void 0 : _c.aborted)
|
|
106
|
+
controller.abort();
|
|
107
|
+
let setup;
|
|
108
|
+
let monitor;
|
|
109
|
+
let lease;
|
|
110
|
+
try {
|
|
111
|
+
setup = budget.reserve(resource_budget_1.HELPER_RESOURCE_RESERVE_MB, "prune-runtime-setup");
|
|
112
|
+
monitor = setInterval(() => {
|
|
113
|
+
try {
|
|
114
|
+
(0, recovery_admission_1.assertRecoveryAdmission)(store, budget);
|
|
115
|
+
}
|
|
116
|
+
catch (_a) {
|
|
117
|
+
controller.abort();
|
|
118
|
+
}
|
|
119
|
+
}, 5000);
|
|
120
|
+
const runtime = yield (0, lance_cleanup_1.prepareCleanupRuntime)(controller.signal);
|
|
121
|
+
clearInterval(monitor);
|
|
122
|
+
setup.release();
|
|
123
|
+
(0, recovery_admission_1.assertRecoveryAdmission)(store, budget);
|
|
124
|
+
lease = yield store_lease_1.StoreLease.acquireExclusive({
|
|
125
|
+
storeDir: store,
|
|
126
|
+
timeoutMs: 1000,
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
role: "explicit-prune-recovery",
|
|
129
|
+
});
|
|
130
|
+
// Another completed/interrupted attempt may have happened during setup.
|
|
131
|
+
const current = (0, prune_state_1.readPruneState)(store);
|
|
132
|
+
if ((current === null || current === void 0 ? void 0 : current.outcome) === "uncertain" &&
|
|
133
|
+
request.acknowledgeUncertain !== current.attemptId)
|
|
134
|
+
throw new Error(`Prune completion is uncertain; inspect and acknowledge attempt ${current.attemptId} before retry`);
|
|
135
|
+
(0, recovery_admission_1.assertRecoveryAdmission)(store, budget);
|
|
136
|
+
const result = yield (0, lance_cleanup_1.pruneVersions)(runtime, table, request.version, request.cutoff, {
|
|
137
|
+
lease,
|
|
138
|
+
signal: controller.signal,
|
|
139
|
+
acknowledgeUncertain: request.acknowledgeUncertain,
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
outcome: "verified",
|
|
143
|
+
table,
|
|
144
|
+
state: (0, prune_state_1.readPruneState)(store),
|
|
145
|
+
result,
|
|
146
|
+
allocatedBytesRecovered: result.allocatedBytesBefore - result.allocatedBytesAfter,
|
|
147
|
+
filesystemFreeBytesChange: result.freeBytesAfter - result.freeBytesBefore,
|
|
148
|
+
fullTableRewriting: "disabled",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error instanceof store_lease_1.StoreLeaseTimeoutError)
|
|
153
|
+
throw new Error(`Recovery refused: store owners are live or unknown (${error.blockers.map((owner) => `${owner.role} pid=${owner.pid}`).join(", ") || "unverified owner"}); close them separately`);
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
clearInterval(monitor);
|
|
158
|
+
(_d = request.signal) === null || _d === void 0 ? void 0 : _d.removeEventListener("abort", abort);
|
|
159
|
+
setup === null || setup === void 0 ? void 0 : setup.release();
|
|
160
|
+
yield lease === null || lease === void 0 ? void 0 : lease.release();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -108,6 +108,14 @@ const defaultProbeDeps = {
|
|
|
108
108
|
}).trim(),
|
|
109
109
|
};
|
|
110
110
|
function defaultProbeOwner(owner, deps = defaultProbeDeps) {
|
|
111
|
+
const parent = probeProcessOwner(owner, deps);
|
|
112
|
+
if ((parent === "dead" || parent === "reused") && owner.activeHelper) {
|
|
113
|
+
const child = probeProcessOwner(Object.assign(Object.assign({}, owner), { pid: owner.activeHelper.pid, processStart: owner.activeHelper.processStart }), deps);
|
|
114
|
+
return child === "same" || child === "unknown" ? child : parent;
|
|
115
|
+
}
|
|
116
|
+
return parent;
|
|
117
|
+
}
|
|
118
|
+
function probeProcessOwner(owner, deps) {
|
|
111
119
|
try {
|
|
112
120
|
deps.signal(owner.pid);
|
|
113
121
|
}
|
|
@@ -124,6 +132,8 @@ function defaultProbeOwner(owner, deps = defaultProbeDeps) {
|
|
|
124
132
|
if (code !== "EPERM")
|
|
125
133
|
return "unknown";
|
|
126
134
|
}
|
|
135
|
+
if (owner.processStart === "unverified")
|
|
136
|
+
return "unknown";
|
|
127
137
|
if (owner.processStart.startsWith(`pid:${owner.pid}:started:`))
|
|
128
138
|
return "same";
|
|
129
139
|
try {
|
|
@@ -138,6 +148,7 @@ function defaultProbeOwner(owner, deps = defaultProbeDeps) {
|
|
|
138
148
|
}
|
|
139
149
|
}
|
|
140
150
|
function readOwner(filePath) {
|
|
151
|
+
var _a;
|
|
141
152
|
try {
|
|
142
153
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
143
154
|
if (!parsed || typeof parsed !== "object")
|
|
@@ -147,18 +158,40 @@ function readOwner(filePath) {
|
|
|
147
158
|
typeof owner.processStart === "string" &&
|
|
148
159
|
typeof owner.nonce === "string" &&
|
|
149
160
|
typeof owner.role === "string" &&
|
|
150
|
-
typeof owner.acquiredAt === "number"
|
|
161
|
+
typeof owner.acquiredAt === "number" &&
|
|
162
|
+
(owner.activeHelper === undefined ||
|
|
163
|
+
(Number.isSafeInteger((_a = owner.activeHelper) === null || _a === void 0 ? void 0 : _a.pid) &&
|
|
164
|
+
owner.activeHelper.pid > 0 &&
|
|
165
|
+
typeof owner.activeHelper.processStart === "string" &&
|
|
166
|
+
!!owner.activeHelper.processStart))
|
|
151
167
|
? owner
|
|
152
168
|
: null;
|
|
153
169
|
}
|
|
154
|
-
catch (
|
|
170
|
+
catch (_b) {
|
|
155
171
|
return null;
|
|
156
172
|
}
|
|
157
173
|
}
|
|
158
|
-
function writeOwnerAtomic(filePath, owner) {
|
|
174
|
+
function writeOwnerAtomic(filePath, owner, durable = false) {
|
|
159
175
|
const temp = `${filePath}.${owner.nonce}.tmp`;
|
|
160
|
-
fs.
|
|
176
|
+
const fd = fs.openSync(temp, "wx", 0o600);
|
|
177
|
+
try {
|
|
178
|
+
fs.writeFileSync(fd, `${JSON.stringify(owner)}\n`);
|
|
179
|
+
if (durable)
|
|
180
|
+
fs.fsyncSync(fd);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
fs.closeSync(fd);
|
|
184
|
+
}
|
|
161
185
|
fs.renameSync(temp, filePath);
|
|
186
|
+
if (durable) {
|
|
187
|
+
const dir = fs.openSync(path.dirname(filePath), "r");
|
|
188
|
+
try {
|
|
189
|
+
fs.fsyncSync(dir);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
fs.closeSync(dir);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
162
195
|
}
|
|
163
196
|
function delay(ms, signal) {
|
|
164
197
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -189,6 +222,7 @@ class StoreLease {
|
|
|
189
222
|
this.storeDir = storeDir;
|
|
190
223
|
this.released = false;
|
|
191
224
|
this.claimedBy = null;
|
|
225
|
+
this.exclusiveUseRunning = false;
|
|
192
226
|
}
|
|
193
227
|
static acquireShared(options) {
|
|
194
228
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -269,6 +303,9 @@ class StoreLease {
|
|
|
269
303
|
}
|
|
270
304
|
release() {
|
|
271
305
|
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
306
|
+
if (this.exclusiveUseRunning) {
|
|
307
|
+
throw new Error("Exclusive store operation must finish before releasing its lease");
|
|
308
|
+
}
|
|
272
309
|
if (this.released)
|
|
273
310
|
return;
|
|
274
311
|
if (this.mode === "shared") {
|
|
@@ -306,7 +343,82 @@ class StoreLease {
|
|
|
306
343
|
if (this.claimedBy === holder)
|
|
307
344
|
this.claimedBy = null;
|
|
308
345
|
}
|
|
346
|
+
/** Hold verified cross-process exclusion through a destructive helper and its
|
|
347
|
+
* verification. A local write gate is not a substitute for this ownership. */
|
|
348
|
+
withExclusiveUse(storeDir, operation) {
|
|
349
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
350
|
+
if (this.exclusiveUseRunning) {
|
|
351
|
+
throw new Error("An exclusive store operation is already running");
|
|
352
|
+
}
|
|
353
|
+
const verify = () => {
|
|
354
|
+
if (this.mode !== "exclusive" || this.released) {
|
|
355
|
+
throw new Error("Pruning requires an unreleased exclusive store lease");
|
|
356
|
+
}
|
|
357
|
+
if (fs.realpathSync(storeDir) !== fs.realpathSync(this.storeDir)) {
|
|
358
|
+
throw new Error("Exclusive store lease belongs to a different store");
|
|
359
|
+
}
|
|
360
|
+
const ownerStat = fs.lstatSync(this.paths.intentOwnerFile);
|
|
361
|
+
if (!ownerStat.isFile() || ownerStat.size > 16 * 1024) {
|
|
362
|
+
throw new Error("Exclusive store lease owner is unverified or oversized");
|
|
363
|
+
}
|
|
364
|
+
const current = readOwner(this.paths.intentOwnerFile);
|
|
365
|
+
if ((current === null || current === void 0 ? void 0 : current.nonce) !== this.owner.nonce ||
|
|
366
|
+
current.pid !== this.owner.pid ||
|
|
367
|
+
current.processStart !== this.owner.processStart) {
|
|
368
|
+
throw new Error("Exclusive store lease ownership could not be verified");
|
|
369
|
+
}
|
|
370
|
+
// acquireExclusive has already removed verified stale readers. Refuse any
|
|
371
|
+
// remaining marker rather than assuming ignored/unknown readers are safe.
|
|
372
|
+
if (fs
|
|
373
|
+
.readdirSync(this.paths.readersDir)
|
|
374
|
+
.some((name) => name.endsWith(".json"))) {
|
|
375
|
+
throw new Error("Pruning requires all shared store owners to close");
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
verify();
|
|
379
|
+
this.exclusiveUseRunning = true;
|
|
380
|
+
try {
|
|
381
|
+
const result = yield operation();
|
|
382
|
+
verify();
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
this.exclusiveUseRunning = false;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
309
390
|
/** Atomically replace this shared lease with an exclusive lease. */
|
|
391
|
+
pinExclusiveHelper(pid) {
|
|
392
|
+
if (!this.exclusiveUseRunning ||
|
|
393
|
+
this.owner.activeHelper ||
|
|
394
|
+
!Number.isSafeInteger(pid) ||
|
|
395
|
+
pid < 1)
|
|
396
|
+
throw new Error("Helper requires an active exclusive store operation");
|
|
397
|
+
const current = readOwner(this.paths.intentOwnerFile);
|
|
398
|
+
if ((current === null || current === void 0 ? void 0 : current.nonce) !== this.owner.nonce || current.activeHelper)
|
|
399
|
+
throw new Error("Exclusive store ownership changed before helper launch");
|
|
400
|
+
// Publish uncertainty first. A failed identity probe must retain exclusion
|
|
401
|
+
// until this child is confirmed dead, even if the parent is killed.
|
|
402
|
+
this.owner.activeHelper = { pid, processStart: "unverified" };
|
|
403
|
+
// The helper is the primary on-disk owner, so older readers also retain
|
|
404
|
+
// exclusion after parent death. It is blocked on stdin until this identity
|
|
405
|
+
// is durably verified; uncertainty here cannot admit native deletion.
|
|
406
|
+
writeOwnerAtomic(this.paths.intentOwnerFile, Object.assign(Object.assign({}, this.owner), { pid, processStart: "unverified" }), true);
|
|
407
|
+
const start = defaultProbeDeps.processStartOf(pid);
|
|
408
|
+
if (!start)
|
|
409
|
+
throw new Error("Prune helper identity unavailable");
|
|
410
|
+
this.owner.activeHelper.processStart = start;
|
|
411
|
+
writeOwnerAtomic(this.paths.intentOwnerFile, Object.assign(Object.assign({}, this.owner), { pid, processStart: start }), true);
|
|
412
|
+
return () => {
|
|
413
|
+
var _a;
|
|
414
|
+
const latest = readOwner(this.paths.intentOwnerFile);
|
|
415
|
+
if ((latest === null || latest === void 0 ? void 0 : latest.nonce) !== this.owner.nonce ||
|
|
416
|
+
((_a = latest.activeHelper) === null || _a === void 0 ? void 0 : _a.pid) !== pid)
|
|
417
|
+
throw new Error("Exclusive store ownership changed during helper close");
|
|
418
|
+
delete this.owner.activeHelper;
|
|
419
|
+
writeOwnerAtomic(this.paths.intentOwnerFile, this.owner, true);
|
|
420
|
+
};
|
|
421
|
+
}
|
|
310
422
|
upgrade() {
|
|
311
423
|
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
312
424
|
if (this.mode !== "shared") {
|
|
@@ -328,6 +440,9 @@ class StoreLease {
|
|
|
328
440
|
/** Replace this exclusive lease with a shared lease without an ownership gap. */
|
|
329
441
|
downgrade() {
|
|
330
442
|
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
443
|
+
if (this.exclusiveUseRunning) {
|
|
444
|
+
throw new Error("Exclusive store operation must finish before downgrading its lease");
|
|
445
|
+
}
|
|
331
446
|
if (this.mode !== "exclusive") {
|
|
332
447
|
throw new Error("Only an exclusive store lease can be downgraded");
|
|
333
448
|
}
|
|
@@ -132,7 +132,7 @@ function parseBatchFootprints(output) {
|
|
|
132
132
|
function sampleHostResources(extraPids = [], overrides = {}, extraGroups = []) {
|
|
133
133
|
var _a;
|
|
134
134
|
var _b, _c;
|
|
135
|
-
const deps = Object.assign({ platform: process.platform, pid: process.pid, uid: (_b = (_a = process.getuid) === null || _a === void 0 ? void 0 : _a.call(process)) !== null && _b !== void 0 ? _b : -1, now: Date.now, monotonic: () => performance.now(), freeBytes: os.freemem, run: (command, args, timeoutMs) => (0, node_child_process_1.execFileSync)(command, args, {
|
|
135
|
+
const deps = Object.assign({ platform: process.platform, pid: process.pid, uid: (_b = (_a = process.getuid) === null || _a === void 0 ? void 0 : _a.call(process)) !== null && _b !== void 0 ? _b : -1, now: Date.now, monotonic: () => performance.now(), freeBytes: os.freemem, sampleTimeoutMs: exports.HOST_SAMPLE_TIMEOUT_MS, kernelProbeTimeoutMs: 500, run: (command, args, timeoutMs) => (0, node_child_process_1.execFileSync)(command, args, {
|
|
136
136
|
encoding: "utf8",
|
|
137
137
|
timeout: timeoutMs,
|
|
138
138
|
maxBuffer: 512 * 1024,
|
|
@@ -153,7 +153,7 @@ function sampleHostResources(extraPids = [], overrides = {}, extraGroups = []) {
|
|
|
153
153
|
};
|
|
154
154
|
if (deps.platform !== "darwin")
|
|
155
155
|
return snapshot;
|
|
156
|
-
const deadline = deps.monotonic() +
|
|
156
|
+
const deadline = deps.monotonic() + deps.sampleTimeoutMs;
|
|
157
157
|
const run = (command, args, maximumMs = 500) => {
|
|
158
158
|
const remaining = Math.floor(deadline - deps.monotonic());
|
|
159
159
|
if (remaining <= 0)
|
|
@@ -167,7 +167,7 @@ function sampleHostResources(extraPids = [], overrides = {}, extraGroups = []) {
|
|
|
167
167
|
}
|
|
168
168
|
catch (_d) { }
|
|
169
169
|
try {
|
|
170
|
-
const zone = (0, kernel_zone_1.parseZprintOutput)(run("zprint", ["-L", "data.kalloc.1024"]));
|
|
170
|
+
const zone = (0, kernel_zone_1.parseZprintOutput)(run("zprint", ["-L", "data.kalloc.1024"], deps.kernelProbeTimeoutMs));
|
|
171
171
|
if (zone) {
|
|
172
172
|
const bytes = zone.elements * zone.elementSize;
|
|
173
173
|
if (Number.isSafeInteger(bytes)) {
|
|
@@ -75,7 +75,7 @@ function resolveResourceBudgetMb(raw = process.env.GMAX_RESOURCE_BUDGET_MB) {
|
|
|
75
75
|
throw new ResourceAdmissionError("resource budget must be 256..6144 MiB");
|
|
76
76
|
return n;
|
|
77
77
|
}
|
|
78
|
-
function assertResourceSnapshot(snapshot, chargedMb, budgetMb, now = Date.now()) {
|
|
78
|
+
function assertResourceSnapshot(snapshot, chargedMb, budgetMb, now = Date.now(), sampleMaxAgeMs = 5000) {
|
|
79
79
|
if (snapshot.memoryPressure === "critical" ||
|
|
80
80
|
snapshot.kernelPressure === "critical")
|
|
81
81
|
throw new ResourceAdmissionError("critical host pressure; heavy work refused", true);
|
|
@@ -84,7 +84,7 @@ function assertResourceSnapshot(snapshot, chargedMb, budgetMb, now = Date.now())
|
|
|
84
84
|
if (snapshot.platform !== "darwin")
|
|
85
85
|
return;
|
|
86
86
|
if (now < snapshot.at ||
|
|
87
|
-
now - snapshot.at >
|
|
87
|
+
now - snapshot.at > sampleMaxAgeMs ||
|
|
88
88
|
snapshot.completedAt < snapshot.at ||
|
|
89
89
|
snapshot.completedAt > now)
|
|
90
90
|
throw new ResourceAdmissionError("resource sample stale or clock changed; heavy work paused");
|
|
@@ -107,8 +107,8 @@ class ResourceBudget {
|
|
|
107
107
|
constructor(overrides = {}) {
|
|
108
108
|
this.deps = Object.assign({ root: path.join(config_1.PATHS.sharedRoot, "resource-budget"), platform: process.platform, pid: process.pid, now: Date.now, sample: (extraPids = []) => {
|
|
109
109
|
const records = this.readRecords();
|
|
110
|
-
return (0, host_resource_1.sampleHostResources)([...records.map((r) => r.pid), ...extraPids],
|
|
111
|
-
}, signal: (pid) => process.kill(pid, 0), policy: host_guard_policy_1.hostGuardPolicy, criticalSample: host_guard_policy_1.sampleCriticalPressure, quarantine: autostart_1.daemonStartDeniedReason, latch: safety_latch_1.latchSafetyStop, processStart: () => (0, node_child_process_1.execFileSync)("ps", ["-p", String(process.pid), "-o", "lstart="], {
|
|
110
|
+
return (0, host_resource_1.sampleHostResources)([...records.map((r) => r.pid), ...extraPids], this.deps.samplerOverrides, records.flatMap((r) => (r.groupPid ? [r.groupPid] : [])));
|
|
111
|
+
}, signal: (pid) => process.kill(pid, 0), policy: host_guard_policy_1.hostGuardPolicy, requireClientRegistration: true, samplerOverrides: {}, sampleMaxAgeMs: 5000, criticalSample: host_guard_policy_1.sampleCriticalPressure, quarantine: autostart_1.daemonStartDeniedReason, latch: safety_latch_1.latchSafetyStop, processStart: () => (0, node_child_process_1.execFileSync)("ps", ["-p", String(process.pid), "-o", "lstart="], {
|
|
112
112
|
encoding: "utf8",
|
|
113
113
|
timeout: 500,
|
|
114
114
|
maxBuffer: 1024,
|
|
@@ -250,10 +250,10 @@ class ResourceBudget {
|
|
|
250
250
|
}
|
|
251
251
|
checked(snapshot, records) {
|
|
252
252
|
try {
|
|
253
|
-
assertResourceSnapshot(snapshot, this.charge(snapshot, records), resolveResourceBudgetMb(), this.deps.now());
|
|
253
|
+
assertResourceSnapshot(snapshot, this.charge(snapshot, records), resolveResourceBudgetMb(), this.deps.now(), this.deps.sampleMaxAgeMs);
|
|
254
254
|
const legacy = snapshot.processes.filter((p) => p.role === "mcp" &&
|
|
255
255
|
!records.some((r) => r.pid === p.pid && r.kind === "client"));
|
|
256
|
-
if (legacy.length)
|
|
256
|
+
if (legacy.length && this.deps.requireClientRegistration)
|
|
257
257
|
throw new ResourceAdmissionError(`${legacy.length} MCP client(s) need to reconnect after upgrading; heavy work paused`);
|
|
258
258
|
}
|
|
259
259
|
catch (error) {
|