grepmax 0.26.14 → 0.26.16
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/dist/lib/daemon/daemon.js +48 -0
- package/dist/lib/index/syncer.js +6 -2
- package/dist/lib/store/vector-db.js +100 -1
- package/dist/lib/utils/file-utils.js +101 -8
- package/dist/lib/utils/kernel-zone.js +117 -0
- package/dist/lib/workers/orchestrator.js +1 -1
- package/package.json +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/hooks/start.js +16 -0
|
@@ -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.
|
|
13
|
+
"version": "0.26.16",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Robert Owens",
|
|
16
16
|
"email": "robowens@me.com"
|
|
@@ -61,6 +61,7 @@ const store_lease_1 = require("../store/store-lease");
|
|
|
61
61
|
const vector_db_1 = require("../store/vector-db");
|
|
62
62
|
const daemon_client_1 = require("../utils/daemon-client");
|
|
63
63
|
const daemon_launcher_1 = require("../utils/daemon-launcher");
|
|
64
|
+
const kernel_zone_1 = require("../utils/kernel-zone");
|
|
64
65
|
const keyed_mutex_1 = require("../utils/keyed-mutex");
|
|
65
66
|
const log_rotate_1 = require("../utils/log-rotate");
|
|
66
67
|
const logger_1 = require("../utils/logger");
|
|
@@ -127,6 +128,8 @@ class Daemon {
|
|
|
127
128
|
this.heartbeatInterval = null;
|
|
128
129
|
this.idleInterval = null;
|
|
129
130
|
this.heartbeatTick = 0;
|
|
131
|
+
/** Rate-limits the kernel-zone warn log — see checkKernelZonePressure. */
|
|
132
|
+
this.lastZoneWarnMs = 0;
|
|
130
133
|
this.shuttingDown = false;
|
|
131
134
|
this.recycling = false;
|
|
132
135
|
// False until LanceDB + MetaCache are open. The socket starts listening early
|
|
@@ -406,6 +409,7 @@ class Daemon {
|
|
|
406
409
|
void this.mlxServerManager.checkMlxHealth();
|
|
407
410
|
this.processManager.sweepOrphanWorkers();
|
|
408
411
|
this.maybeRecycle();
|
|
412
|
+
this.checkKernelZonePressure();
|
|
409
413
|
}
|
|
410
414
|
}, HEARTBEAT_INTERVAL_MS);
|
|
411
415
|
// 10. Idle timeout (skip when disabled via env)
|
|
@@ -1698,6 +1702,50 @@ class Daemon {
|
|
|
1698
1702
|
this.recycling = true;
|
|
1699
1703
|
void this.shutdown({ relaunch: true }).finally(() => process.exit(0));
|
|
1700
1704
|
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Stand down before our own write volume exhausts a leaking kernel zone.
|
|
1707
|
+
*
|
|
1708
|
+
* On macOS 26.5.2 sustained filesystem writes leak `data.kalloc.1024`
|
|
1709
|
+
* irreversibly, and exhausting it panics the host with no warning of any kind —
|
|
1710
|
+
* the daemon does not get to observe the failure, it just dies with the machine.
|
|
1711
|
+
* This host lost three sessions that way before the compactor was identified as
|
|
1712
|
+
* the largest write source on it.
|
|
1713
|
+
*
|
|
1714
|
+
* So the daemon watches the zone it is pressurizing. A warn-level reading is
|
|
1715
|
+
* logged and left alone; a critical reading exits without relaunch, because
|
|
1716
|
+
* relaunching would resume the writes that caused it. Nothing here can reclaim
|
|
1717
|
+
* the leaked memory — only a reboot does that — so the only useful move is to
|
|
1718
|
+
* stop adding to it while the user still has a working machine.
|
|
1719
|
+
*
|
|
1720
|
+
* Deliberately does nothing when the sample is unavailable or unparseable
|
|
1721
|
+
* (non-macOS, no `zprint`, changed output format): an unknown reading must never
|
|
1722
|
+
* shut the daemon down.
|
|
1723
|
+
*/
|
|
1724
|
+
checkKernelZonePressure() {
|
|
1725
|
+
if (this.shuttingDown || this.recycling)
|
|
1726
|
+
return;
|
|
1727
|
+
const usage = (0, kernel_zone_1.readKernelZoneUsage)();
|
|
1728
|
+
if (!usage || usage.pressure === "ok")
|
|
1729
|
+
return;
|
|
1730
|
+
if (usage.pressure === "warn") {
|
|
1731
|
+
// Rate-limited to once an hour: at the 5-minute heartbeat this would
|
|
1732
|
+
// otherwise print 12 identical lines an hour for as long as the condition
|
|
1733
|
+
// holds, which on a leaking host is until reboot.
|
|
1734
|
+
const now = Date.now();
|
|
1735
|
+
if (now - this.lastZoneWarnMs < 60 * 60 * 1000)
|
|
1736
|
+
return;
|
|
1737
|
+
this.lastZoneWarnMs = now;
|
|
1738
|
+
console.log(`[daemon] WARNING: ${(0, kernel_zone_1.formatZoneUsage)(usage)} — macOS kernel zone is leaking ` +
|
|
1739
|
+
`under write pressure. The daemon will stop itself at ` +
|
|
1740
|
+
`${(kernel_zone_1.ZONE_THRESHOLDS.criticalBytes / Math.pow(1024, 3)).toFixed(0)}GiB. ` +
|
|
1741
|
+
`Only a reboot reclaims this memory.`);
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
console.log(`[daemon] CRITICAL: ${(0, kernel_zone_1.formatZoneUsage)(usage)} — stopping to avoid a kernel panic. ` +
|
|
1745
|
+
`Reboot to reclaim the zone; the index will catch up on next start.`);
|
|
1746
|
+
this.recycling = true;
|
|
1747
|
+
void this.shutdown().finally(() => process.exit(0));
|
|
1748
|
+
}
|
|
1701
1749
|
shutdown(opts = {}) {
|
|
1702
1750
|
if (!this.shutdownPromise) {
|
|
1703
1751
|
this.shutdownPromise = this.performShutdown(opts);
|
package/dist/lib/index/syncer.js
CHANGED
|
@@ -324,9 +324,13 @@ function initialSync(options) {
|
|
|
324
324
|
yield currentFlush;
|
|
325
325
|
(0, logger_1.debug)("index", `flush done: ${Date.now() - flushStart}ms`);
|
|
326
326
|
flushCount++;
|
|
327
|
-
// Periodically compact during sync to prevent fragment accumulation
|
|
327
|
+
// Periodically compact during sync to prevent fragment accumulation.
|
|
328
|
+
// Uses the shared threshold deliberately: a hardcoded 30 here meant
|
|
329
|
+
// the full-index path kept compacting at the old rate long after the
|
|
330
|
+
// threshold was raised to 400, re-triggering the whole-table rewrite
|
|
331
|
+
// that raising it exists to avoid.
|
|
328
332
|
if (flushCount % 10 === 0) {
|
|
329
|
-
yield vectorDb.compactIfNeeded(
|
|
333
|
+
yield vectorDb.compactIfNeeded(config_1.FRAGMENT_COMPACT_THRESHOLD);
|
|
330
334
|
}
|
|
331
335
|
}
|
|
332
336
|
catch (err) {
|
|
@@ -157,6 +157,36 @@ function isMissingTableError(err) {
|
|
|
157
157
|
const TABLE_NAME = "chunks";
|
|
158
158
|
const MAINTENANCE_INTERVAL_MS = 5 * 60 * 1000;
|
|
159
159
|
const CLEAN_MAINTENANCE_INTERVAL_MS = 60 * 60 * 1000;
|
|
160
|
+
/**
|
|
161
|
+
* Floor between opportunistic full-table compactions.
|
|
162
|
+
*
|
|
163
|
+
* A compaction rewrites the whole table, so its cost scales with store size, not
|
|
164
|
+
* with how much changed. Neither opportunistic caller was rate-limited: the
|
|
165
|
+
* 5-minute maintenance tick re-optimizes whenever the write epoch moved, and
|
|
166
|
+
* `compactIfNeeded` fires whenever small fragments cross a threshold. On a store
|
|
167
|
+
* under continuous reindex pressure both are true essentially always, so the
|
|
168
|
+
* table gets rewritten every few minutes forever.
|
|
169
|
+
*
|
|
170
|
+
* That is not hypothetical. On 2026-08-16..17 this store (16 GB) took 43 full
|
|
171
|
+
* compactions in two days — an accelerating 6 → 30 → 63 → 54 per day — which a
|
|
172
|
+
* microstackshot measured as 549.76 GB of file-backed writes in 12.3 hours. On
|
|
173
|
+
* macOS 26.5.2 that write volume exhausted the `data.kalloc.1024` kernel zone and
|
|
174
|
+
* panicked the host. See docs/2026-08-04-macos-kernel-zone-panic-incident.md.
|
|
175
|
+
*
|
|
176
|
+
* The floor caps compaction at twice an hour regardless of how much churn arrives.
|
|
177
|
+
* Fragments accumulating between passes cost read performance, which is recoverable;
|
|
178
|
+
* unbounded rewrite volume was not.
|
|
179
|
+
*/
|
|
180
|
+
const COMPACTION_MIN_INTERVAL_MS = 30 * 60 * 1000;
|
|
181
|
+
/**
|
|
182
|
+
* Ceiling for the unproductive-compaction backoff.
|
|
183
|
+
*
|
|
184
|
+
* A compaction that removes no fragments and frees no bytes rewrote the table for
|
|
185
|
+
* nothing, and the next one a few minutes later will almost certainly do the same.
|
|
186
|
+
* The interval doubles on each such pass and resets the moment one does real work,
|
|
187
|
+
* so a store that cannot be improved stops paying for the attempt.
|
|
188
|
+
*/
|
|
189
|
+
const COMPACTION_MAX_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
160
190
|
class VectorDB {
|
|
161
191
|
constructor(lancedbDir, vectorDim, suppliedLease) {
|
|
162
192
|
this.lancedbDir = lancedbDir;
|
|
@@ -174,6 +204,9 @@ class VectorDB {
|
|
|
174
204
|
/** Only an index owner may create/rebuild shared indexes — see markIndexOwner. */
|
|
175
205
|
this.indexOwner = false;
|
|
176
206
|
this.lastOptimizeDidWork = false;
|
|
207
|
+
/** Compaction rate limiter — see COMPACTION_MIN_INTERVAL_MS. */
|
|
208
|
+
this.lastCompactionMs = 0;
|
|
209
|
+
this.compactionIntervalMs = COMPACTION_MIN_INTERVAL_MS;
|
|
177
210
|
this.maintenanceRunner = (fn) => fn();
|
|
178
211
|
/** Latched when a from-scratch FTS rebuild failed to stop an optimize
|
|
179
212
|
* panic. Stops the (expensive) rebuild from re-running every maintenance
|
|
@@ -1152,13 +1185,30 @@ class VectorDB {
|
|
|
1152
1185
|
}
|
|
1153
1186
|
yield this.createFTSIndex();
|
|
1154
1187
|
yield this.createVectorIndex();
|
|
1155
|
-
|
|
1188
|
+
// Index creation above is idempotent and self-guarding, so it stays on the
|
|
1189
|
+
// 5-minute cadence. Compaction is the expensive half — it rewrites the whole
|
|
1190
|
+
// table — and gets rate-limited. Without this the tick compacts every 5
|
|
1191
|
+
// minutes for as long as writes keep arriving, which is how this store
|
|
1192
|
+
// reached 43 full rewrites in two days and panicked the host.
|
|
1193
|
+
const throttleMs = options.force
|
|
1194
|
+
? 0
|
|
1195
|
+
: this.compactionThrottleRemainingMs();
|
|
1196
|
+
if (throttleMs > 0) {
|
|
1197
|
+
(0, logger_1.debug)("vectordb", `Compaction throttled — ${Math.ceil(throttleMs / 60000)}min until next full pass ` +
|
|
1198
|
+
`(interval ${Math.round(this.compactionIntervalMs / 60000)}min)`);
|
|
1199
|
+
}
|
|
1200
|
+
else if (pressure === "low") {
|
|
1156
1201
|
(0, logger_1.log)("vectordb", `Low disk — single-pass optimize (no bloat retry)`);
|
|
1157
1202
|
yield this.optimize(1, 0, true);
|
|
1203
|
+
this.noteCompaction(this.lastOptimizeDidWork);
|
|
1158
1204
|
}
|
|
1159
1205
|
else {
|
|
1160
1206
|
// Normal maintenance: full optimize + bloat check
|
|
1161
1207
|
yield this.optimize(5, 0, true);
|
|
1208
|
+
// Track across both passes: `lastOptimizeDidWork` is reset per optimize()
|
|
1209
|
+
// call, so a productive first pass followed by a barren bloat retry would
|
|
1210
|
+
// otherwise read as unproductive and trigger a spurious backoff.
|
|
1211
|
+
let didWork = this.lastOptimizeDidWork;
|
|
1162
1212
|
if (this.lastOptimizeDidWork) {
|
|
1163
1213
|
const table = yield this.openExistingTableUnsafe();
|
|
1164
1214
|
if (table) {
|
|
@@ -1180,9 +1230,11 @@ class VectorDB {
|
|
|
1180
1230
|
(0, logger_1.log)("vectordb", `Bloat detected after optimize: ${(diskSize / 1024 / 1024).toFixed(0)}MB disk vs ${(logicalSize / 1024 / 1024).toFixed(0)}MB logical (${bloatRatio.toFixed(1)}x) — retrying`);
|
|
1181
1231
|
yield new Promise((r) => setTimeout(r, 2000));
|
|
1182
1232
|
yield this.optimize(5, 0, true);
|
|
1233
|
+
didWork = didWork || this.lastOptimizeDidWork;
|
|
1183
1234
|
}
|
|
1184
1235
|
}
|
|
1185
1236
|
}
|
|
1237
|
+
this.noteCompaction(didWork);
|
|
1186
1238
|
}
|
|
1187
1239
|
const maintainedTable = yield this.openExistingTableUnsafe();
|
|
1188
1240
|
this.maintainedTableVersion =
|
|
@@ -1195,6 +1247,47 @@ class VectorDB {
|
|
|
1195
1247
|
}
|
|
1196
1248
|
});
|
|
1197
1249
|
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Milliseconds until an opportunistic full-table compaction is allowed again,
|
|
1252
|
+
* or 0 if one may run now. Forced callers (`gmax index`, `doctor --fix`) are
|
|
1253
|
+
* expected to bypass this rather than consult it.
|
|
1254
|
+
*/
|
|
1255
|
+
compactionThrottleRemainingMs(now = Date.now()) {
|
|
1256
|
+
if (this.lastCompactionMs === 0)
|
|
1257
|
+
return 0;
|
|
1258
|
+
const elapsed = now - this.lastCompactionMs;
|
|
1259
|
+
// A clock that jumped backwards would otherwise wedge compaction shut.
|
|
1260
|
+
if (elapsed < 0)
|
|
1261
|
+
return 0;
|
|
1262
|
+
return Math.max(0, this.compactionIntervalMs - elapsed);
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Record a completed compaction and adjust the interval. Productive passes reset
|
|
1266
|
+
* to the floor; unproductive ones double up to the ceiling so a store that cannot
|
|
1267
|
+
* be improved stops being rewritten every few minutes.
|
|
1268
|
+
*/
|
|
1269
|
+
noteCompaction(didWork) {
|
|
1270
|
+
this.lastCompactionMs = Date.now();
|
|
1271
|
+
if (didWork) {
|
|
1272
|
+
if (this.compactionIntervalMs !== COMPACTION_MIN_INTERVAL_MS) {
|
|
1273
|
+
(0, logger_1.debug)("vectordb", "Productive compaction — interval reset to floor");
|
|
1274
|
+
}
|
|
1275
|
+
this.compactionIntervalMs = COMPACTION_MIN_INTERVAL_MS;
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const next = Math.min(this.compactionIntervalMs * 2, COMPACTION_MAX_INTERVAL_MS);
|
|
1279
|
+
if (next !== this.compactionIntervalMs) {
|
|
1280
|
+
(0, logger_1.log)("vectordb", `Compaction reclaimed nothing — backing off to ${Math.round(next / 60000)}min`);
|
|
1281
|
+
}
|
|
1282
|
+
this.compactionIntervalMs = next;
|
|
1283
|
+
}
|
|
1284
|
+
/** Test/diagnostic view of the rate limiter. */
|
|
1285
|
+
getCompactionThrottleState() {
|
|
1286
|
+
return {
|
|
1287
|
+
intervalMs: this.compactionIntervalMs,
|
|
1288
|
+
remainingMs: this.compactionThrottleRemainingMs(),
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1198
1291
|
compactIfNeeded() {
|
|
1199
1292
|
return __awaiter(this, arguments, void 0, function* (threshold = config_1.FRAGMENT_COMPACT_THRESHOLD) {
|
|
1200
1293
|
if (this.maintenanceRunning)
|
|
@@ -1206,8 +1299,14 @@ class VectorDB {
|
|
|
1206
1299
|
const table = yield this.ensureTable();
|
|
1207
1300
|
const stats = yield table.stats();
|
|
1208
1301
|
if (stats.fragmentStats.numSmallFragments > threshold) {
|
|
1302
|
+
const throttleMs = this.compactionThrottleRemainingMs();
|
|
1303
|
+
if (throttleMs > 0) {
|
|
1304
|
+
(0, logger_1.debug)("vectordb", `Fragment threshold exceeded (${stats.fragmentStats.numSmallFragments} > ${threshold}) — throttled, ${Math.ceil(throttleMs / 60000)}min remaining`);
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1209
1307
|
(0, logger_1.log)("vectordb", `Fragment threshold exceeded (${stats.fragmentStats.numSmallFragments} > ${threshold}) — compacting`);
|
|
1210
1308
|
yield this.optimize(2, 0, true);
|
|
1309
|
+
this.noteCompaction(this.lastOptimizeDidWork);
|
|
1211
1310
|
return true;
|
|
1212
1311
|
}
|
|
1213
1312
|
}
|
|
@@ -187,22 +187,115 @@ function isIndexableFile(filePath, size) {
|
|
|
187
187
|
function isIndexablePath(filePath) {
|
|
188
188
|
return isIndexableFile(filePath);
|
|
189
189
|
}
|
|
190
|
-
//
|
|
191
|
-
// Tools (protoc, graphql-codegen, Apollo, Relay, Go `stringer`,
|
|
192
|
-
// stamp one of these
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
190
|
+
// Markers that only ever appear in a codegen banner. Any one of these, on its
|
|
191
|
+
// own, is enough. Tools (protoc, graphql-codegen, Apollo, Relay, Go `stringer`,
|
|
192
|
+
// OpenAPI) stamp one of these; doc generators fence their output with the
|
|
193
|
+
// `generated-body` / `AUTO-GENERATED-CONTENT` style directives.
|
|
194
|
+
const STRONG_GENERATED_MARKER = /@generated\b|@autogenerated\b|\bCode generated by |\bauto-?generated by |\bThis (?:file|code) (?:is|was) (?:\w+[ -])?generated|\bmachine[ -]generated\b|\bauto-?generated[-_ ]?content\b|\bgenerated[-_](?:body|content|section|block)\b|\bBEGIN GENERATED\b/i;
|
|
195
|
+
// "DO NOT EDIT" is not proof of codegen on its own — humans write it on prose
|
|
196
|
+
// they want left alone (archived vendor snapshots, verbatim captures). Real
|
|
197
|
+
// codegen banners pair it with a generation word, so require that pairing.
|
|
198
|
+
const DO_NOT_EDIT_MARKER = /\bDO NOT EDIT\b/i;
|
|
199
|
+
const GENERATION_CONTEXT = /generat|codegen|autogen/i;
|
|
196
200
|
const GENERATED_SNIFF_BYTES = 2048;
|
|
201
|
+
const DOC_LIKE_EXTENSIONS = /\.(?:md|mdx|markdown|txt|rst|adoc|asciidoc)$/i;
|
|
202
|
+
/**
|
|
203
|
+
* Extract the leading banner: the run of shebang, frontmatter, blank, and
|
|
204
|
+
* comment lines at the very top, stopping at the first line of actual code.
|
|
205
|
+
*
|
|
206
|
+
* Scanning raw head bytes instead was the bug this replaces. Two ways it
|
|
207
|
+
* misfired, both silently erasing hand-written files from the index:
|
|
208
|
+
*
|
|
209
|
+
* - A *generator* was classified as its own output, because the banner it
|
|
210
|
+
* emits appears as a string literal in its body
|
|
211
|
+
* (`lines.push('# Auto-generated by ...')`).
|
|
212
|
+
* - A comment quoting another file's rules ("its README says do not edit")
|
|
213
|
+
* counted as a banner.
|
|
214
|
+
*
|
|
215
|
+
* A real codegen banner precedes all code, so anchoring here removes both
|
|
216
|
+
* without weakening detection.
|
|
217
|
+
*
|
|
218
|
+
* In doc-like files `#` starts a heading, not a comment, so only frontmatter
|
|
219
|
+
* and HTML comments count — otherwise the whole document body would be read as
|
|
220
|
+
* banner.
|
|
221
|
+
*/
|
|
222
|
+
function leadingBanner(head, filePath) {
|
|
223
|
+
var _a, _b;
|
|
224
|
+
const docLike = filePath ? DOC_LIKE_EXTENSIONS.test(filePath) : false;
|
|
225
|
+
const lines = head.split("\n");
|
|
226
|
+
const banner = [];
|
|
227
|
+
let i = 0;
|
|
228
|
+
if (!docLike && ((_a = lines[0]) === null || _a === void 0 ? void 0 : _a.startsWith("#!"))) {
|
|
229
|
+
banner.push(lines[0]);
|
|
230
|
+
i = 1;
|
|
231
|
+
}
|
|
232
|
+
// YAML frontmatter is metadata, not code — a generated-doc marker often sits
|
|
233
|
+
// directly after it.
|
|
234
|
+
if (((_b = lines[i]) === null || _b === void 0 ? void 0 : _b.trim()) === "---") {
|
|
235
|
+
banner.push(lines[i]);
|
|
236
|
+
i++;
|
|
237
|
+
while (i < lines.length && lines[i].trim() !== "---")
|
|
238
|
+
banner.push(lines[i++]);
|
|
239
|
+
if (i < lines.length)
|
|
240
|
+
banner.push(lines[i++]);
|
|
241
|
+
}
|
|
242
|
+
let blockEnd;
|
|
243
|
+
for (; i < lines.length; i++) {
|
|
244
|
+
const line = lines[i];
|
|
245
|
+
const trimmed = line.trim();
|
|
246
|
+
if (blockEnd) {
|
|
247
|
+
banner.push(line);
|
|
248
|
+
if (trimmed.includes(blockEnd))
|
|
249
|
+
blockEnd = undefined;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (trimmed === "") {
|
|
253
|
+
banner.push(line);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (trimmed.startsWith("<!--")) {
|
|
257
|
+
banner.push(line);
|
|
258
|
+
if (!trimmed.includes("-->"))
|
|
259
|
+
blockEnd = "-->";
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (!docLike) {
|
|
263
|
+
if (trimmed.startsWith("/*")) {
|
|
264
|
+
banner.push(line);
|
|
265
|
+
if (!trimmed.includes("*/"))
|
|
266
|
+
blockEnd = "*/";
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (trimmed.startsWith("//") ||
|
|
270
|
+
trimmed.startsWith("#") ||
|
|
271
|
+
trimmed.startsWith("--") ||
|
|
272
|
+
trimmed.startsWith(";") ||
|
|
273
|
+
trimmed.startsWith("*")) {
|
|
274
|
+
banner.push(line);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
break; // first real line of content — the banner is over
|
|
279
|
+
}
|
|
280
|
+
return banner.join("\n");
|
|
281
|
+
}
|
|
197
282
|
/**
|
|
198
283
|
* Detect machine-generated source by its banner. Generated files flood both the
|
|
199
284
|
* vector index and the symbol graph (codegen types rank as god nodes), so they
|
|
200
285
|
* are skipped at index time. Path globs in `ignore-patterns.ts` catch the
|
|
201
286
|
* obvious filenames; this catches the rest by content.
|
|
287
|
+
*
|
|
288
|
+
* `filePath` is optional but worth passing: without it, doc-like files are
|
|
289
|
+
* scanned with code comment rules, so a markdown `# Heading` reads as a comment.
|
|
202
290
|
*/
|
|
203
|
-
function isGeneratedContent(buffer) {
|
|
291
|
+
function isGeneratedContent(buffer, filePath) {
|
|
204
292
|
const head = buffer.subarray(0, GENERATED_SNIFF_BYTES).toString("utf-8");
|
|
205
|
-
|
|
293
|
+
const banner = leadingBanner(head, filePath);
|
|
294
|
+
if (!banner.trim())
|
|
295
|
+
return false;
|
|
296
|
+
if (STRONG_GENERATED_MARKER.test(banner))
|
|
297
|
+
return true;
|
|
298
|
+
return DO_NOT_EDIT_MARKER.test(banner) && GENERATION_CONTEXT.test(banner);
|
|
206
299
|
}
|
|
207
300
|
function formatDenseSnippet(text, maxLength = 1500) {
|
|
208
301
|
const clean = text !== null && text !== void 0 ? text : "";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZONE_THRESHOLDS = void 0;
|
|
4
|
+
exports.classifyZonePressure = classifyZonePressure;
|
|
5
|
+
exports.parseZprintOutput = parseZprintOutput;
|
|
6
|
+
exports.readKernelZoneUsage = readKernelZoneUsage;
|
|
7
|
+
exports.formatZoneUsage = formatZoneUsage;
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
/**
|
|
10
|
+
* macOS kernel-zone pressure guard.
|
|
11
|
+
*
|
|
12
|
+
* On macOS 26.5.2 (build 25F84) the `data.kalloc.1024` kernel zone leaks under
|
|
13
|
+
* sustained filesystem write pressure and is never reclaimed. Exhausting it panics
|
|
14
|
+
* the host outright — no jetsam, no swap warning, no chance to intervene, because
|
|
15
|
+
* the failing resource is wired kernel memory rather than anything the VM system
|
|
16
|
+
* manages.
|
|
17
|
+
*
|
|
18
|
+
* This host panicked three times that way. The third time the zone sat near 1.4 GB
|
|
19
|
+
* for twelve days, then climbed to 17.9 GB in eight hours while gmax's compactor
|
|
20
|
+
* dirtied 549.76 GB of file-backed memory. See
|
|
21
|
+
* docs/2026-08-04-macos-kernel-zone-panic-incident.md.
|
|
22
|
+
*
|
|
23
|
+
* gmax cannot fix the kernel defect, but it can decline to be the thing that
|
|
24
|
+
* detonates it. Sampling the zone is cheap (~0.2 s), so the daemon watches its own
|
|
25
|
+
* blast radius and stands down while there are still hours of headroom.
|
|
26
|
+
*/
|
|
27
|
+
/** The zone that leaks. Named explicitly so `zprint` returns a single line. */
|
|
28
|
+
const ZONE_NAME = "data.kalloc.1024";
|
|
29
|
+
/**
|
|
30
|
+
* Warn threshold.
|
|
31
|
+
*
|
|
32
|
+
* Twelve days of ordinary use held this zone between 0.67 and 1.37 GiB, so 4 GiB is
|
|
33
|
+
* roughly 3x anything a healthy host has been observed to reach. During the
|
|
34
|
+
* August 17 burst the zone crossed this about 90 minutes in — early enough that
|
|
35
|
+
* stopping then would have avoided the panic with hours to spare.
|
|
36
|
+
*/
|
|
37
|
+
const WARN_BYTES = 4 * Math.pow(1024, 3);
|
|
38
|
+
/**
|
|
39
|
+
* Critical threshold — the daemon stands down here.
|
|
40
|
+
*
|
|
41
|
+
* The observed zone map cap on this hardware is ~17.6 GiB. Eight GiB is under half
|
|
42
|
+
* of it, and at the burst rate actually measured (~2.0 GiB/hour) it still leaves
|
|
43
|
+
* roughly five hours before exhaustion. The margin is deliberately generous: the
|
|
44
|
+
* cost of standing down early is a stale index, and the cost of standing down late
|
|
45
|
+
* is an unplanned reboot.
|
|
46
|
+
*/
|
|
47
|
+
const CRITICAL_BYTES = 8 * Math.pow(1024, 3);
|
|
48
|
+
function classifyZonePressure(bytes) {
|
|
49
|
+
if (bytes >= CRITICAL_BYTES)
|
|
50
|
+
return "critical";
|
|
51
|
+
if (bytes >= WARN_BYTES)
|
|
52
|
+
return "warn";
|
|
53
|
+
return "ok";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse one `zprint <zone>` report.
|
|
57
|
+
*
|
|
58
|
+
* Exported for tests; the column layout is a stable but undocumented text format,
|
|
59
|
+
* so this validates rather than trusting it:
|
|
60
|
+
*
|
|
61
|
+
* ```
|
|
62
|
+
* elem cur max cur max cur alloc alloc
|
|
63
|
+
* zone name size size size #elts #elts inuse size count
|
|
64
|
+
* data.kalloc.1024 1024 0K 0K 0 0 3416 0K 0
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* The `cur size` column reads `0K` on this build, which is why the byte figure is
|
|
68
|
+
* derived from `cur inuse` x `elem size` rather than read directly.
|
|
69
|
+
*/
|
|
70
|
+
function parseZprintOutput(output, zoneName = ZONE_NAME) {
|
|
71
|
+
for (const line of output.split("\n")) {
|
|
72
|
+
const fields = line.trim().split(/\s+/);
|
|
73
|
+
if (fields[0] !== zoneName)
|
|
74
|
+
continue;
|
|
75
|
+
const elementSize = Number(fields[1]);
|
|
76
|
+
const elements = Number(fields[6]);
|
|
77
|
+
if (!Number.isInteger(elementSize) || elementSize <= 0)
|
|
78
|
+
return null;
|
|
79
|
+
if (!Number.isInteger(elements) || elements < 0)
|
|
80
|
+
return null;
|
|
81
|
+
return { elements, elementSize };
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Sample the zone. Returns null off macOS, or whenever the sample cannot be
|
|
87
|
+
* trusted — an unparseable report must read as "unknown", never as "healthy" and
|
|
88
|
+
* never as a reason to stop working.
|
|
89
|
+
*/
|
|
90
|
+
function readKernelZoneUsage(zoneName = ZONE_NAME) {
|
|
91
|
+
if (process.platform !== "darwin")
|
|
92
|
+
return null;
|
|
93
|
+
try {
|
|
94
|
+
const output = (0, node_child_process_1.execFileSync)("zprint", [zoneName], {
|
|
95
|
+
encoding: "utf-8",
|
|
96
|
+
timeout: 5000,
|
|
97
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
98
|
+
});
|
|
99
|
+
const parsed = parseZprintOutput(output, zoneName);
|
|
100
|
+
if (!parsed)
|
|
101
|
+
return null;
|
|
102
|
+
const bytes = parsed.elements * parsed.elementSize;
|
|
103
|
+
return Object.assign(Object.assign({}, parsed), { bytes, pressure: classifyZonePressure(bytes) });
|
|
104
|
+
}
|
|
105
|
+
catch (_a) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function formatZoneUsage(usage) {
|
|
110
|
+
const gib = (usage.bytes / Math.pow(1024, 3)).toFixed(2);
|
|
111
|
+
return `${ZONE_NAME} at ${gib}GiB (${usage.elements.toLocaleString()} elements)`;
|
|
112
|
+
}
|
|
113
|
+
exports.ZONE_THRESHOLDS = {
|
|
114
|
+
warnBytes: WARN_BYTES,
|
|
115
|
+
criticalBytes: CRITICAL_BYTES,
|
|
116
|
+
zoneName: ZONE_NAME,
|
|
117
|
+
};
|
|
@@ -256,7 +256,7 @@ class WorkerOrchestrator {
|
|
|
256
256
|
(0, logger_1.debug)("orch", `skip ${input.path} (empty or binary)`);
|
|
257
257
|
return { vectors: [], hash, mtimeMs, size, shouldDelete: true };
|
|
258
258
|
}
|
|
259
|
-
if ((0, file_utils_1.isGeneratedContent)(buffer)) {
|
|
259
|
+
if ((0, file_utils_1.isGeneratedContent)(buffer, absolutePath)) {
|
|
260
260
|
(0, logger_1.debug)("orch", `skip ${input.path} (machine-generated header)`);
|
|
261
261
|
return { vectors: [], hash, mtimeMs, size, shouldDelete: true };
|
|
262
262
|
}
|
package/package.json
CHANGED
|
@@ -78,7 +78,23 @@ function isProjectRegistered() {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// A kill switch for the auto-start. Create ~/.gmax/autostart-disabled (or export
|
|
82
|
+
// GMAX_NO_AUTOSTART=1) to keep sessions from reviving the daemon — needed when the
|
|
83
|
+
// host is quarantined after a kernel-zone incident and the daemon's write volume is
|
|
84
|
+
// the thing under investigation. Delete the file to restore normal behavior.
|
|
85
|
+
function isAutostartDisabled() {
|
|
86
|
+
if (process.env.GMAX_NO_AUTOSTART === "1") return true;
|
|
87
|
+
try {
|
|
88
|
+
return require("node:fs").existsSync(
|
|
89
|
+
_path.join(require("node:os").homedir(), ".gmax", "autostart-disabled"),
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
function startWatcher() {
|
|
97
|
+
if (isAutostartDisabled()) return;
|
|
82
98
|
if (!isProjectRegistered()) return;
|
|
83
99
|
try {
|
|
84
100
|
execFileSync("gmax", ["watch", "--daemon", "-b"], {
|