grepmax 0.26.28 → 0.26.32
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 +4 -2
- package/dist/commands/mcp.js +17 -1
- package/dist/commands/watch.js +1 -1
- package/dist/config.js +8 -2
- package/dist/lib/analysis/surprising-connections.js +135 -15
- package/dist/lib/daemon/daemon.js +131 -13
- package/dist/lib/daemon/graph-handler.js +12 -6
- package/dist/lib/daemon/ipc-handler.js +18 -3
- package/dist/lib/daemon/read-verbs.js +52 -1
- package/dist/lib/daemon/search-handler.js +7 -4
- package/dist/lib/daemon/watch-leases.js +179 -0
- package/dist/lib/index/batch-processor.js +7 -1
- package/dist/lib/index/watcher.js +4 -0
- package/dist/lib/search/pagerank.js +80 -19
- package/dist/lib/store/vector-db.js +35 -1
- package/dist/lib/utils/async-semaphore.js +141 -0
- package/dist/lib/utils/process-footprint.js +51 -0
- package/dist/lib/utils/watcher-launcher.js +36 -19
- package/dist/lib/workers/pool.js +72 -2
- package/package.json +12 -13
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/hooks/cwd-changed.js +19 -36
- package/plugins/grepmax/hooks/start.js +15 -19
- package/plugins/grepmax/hooks/stop.js +13 -4
- package/plugins/grepmax/hooks/watch-lease.js +163 -0
|
@@ -150,20 +150,28 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
150
150
|
capabilities: {
|
|
151
151
|
exclusiveGenerationRebuild: exports.EXCLUSIVE_GENERATION_REBUILD_PROTOCOL,
|
|
152
152
|
readVerbs: read_verbs_1.READ_VERBS_PROTOCOL,
|
|
153
|
+
// `watch`/`unwatch` accept holder/pid/ttlMs. A daemon without this
|
|
154
|
+
// treats `unwatch` as unwatch-for-everyone, so clients releasing a
|
|
155
|
+
// single lease must check it first.
|
|
156
|
+
watchLeases: 1,
|
|
153
157
|
},
|
|
154
158
|
};
|
|
155
159
|
case "watch": {
|
|
156
160
|
const root = String(cmd.root || "");
|
|
157
161
|
if (!root)
|
|
158
162
|
return { ok: false, error: "missing root" };
|
|
159
|
-
yield daemon.
|
|
163
|
+
yield daemon.requestWatch(root, {
|
|
164
|
+
holder: typeof cmd.holder === "string" ? cmd.holder : undefined,
|
|
165
|
+
pid: typeof cmd.pid === "number" ? cmd.pid : undefined,
|
|
166
|
+
ttlMs: typeof cmd.ttlMs === "number" ? cmd.ttlMs : undefined,
|
|
167
|
+
});
|
|
160
168
|
return { ok: true, pid: process.pid };
|
|
161
169
|
}
|
|
162
170
|
case "unwatch": {
|
|
163
171
|
const root = String(cmd.root || "");
|
|
164
172
|
if (!root)
|
|
165
173
|
return { ok: false, error: "missing root" };
|
|
166
|
-
yield daemon.
|
|
174
|
+
yield daemon.releaseWatch(root, typeof cmd.holder === "string" ? cmd.holder : undefined);
|
|
167
175
|
return { ok: true };
|
|
168
176
|
}
|
|
169
177
|
case "status":
|
|
@@ -172,6 +180,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
172
180
|
pid: process.pid,
|
|
173
181
|
uptime: daemon.uptime(),
|
|
174
182
|
projects: daemon.listProjects(),
|
|
183
|
+
leases: daemon.listWatchLeases(),
|
|
175
184
|
diskPressure: daemon.getDiskPressure(),
|
|
176
185
|
mlx: daemon.getMlxStatus(),
|
|
177
186
|
};
|
|
@@ -304,6 +313,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
304
313
|
const root = String(cmd.root || "");
|
|
305
314
|
if (!root)
|
|
306
315
|
return { ok: false, error: "missing root" };
|
|
316
|
+
daemon.leaseForCli(root);
|
|
307
317
|
startStreaming(conn, () => daemon.addProject(root, conn));
|
|
308
318
|
return null;
|
|
309
319
|
}
|
|
@@ -311,6 +321,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
311
321
|
const root = String(cmd.root || "");
|
|
312
322
|
if (!root)
|
|
313
323
|
return { ok: false, error: "missing root" };
|
|
324
|
+
daemon.leaseForCli(root);
|
|
314
325
|
startStreaming(conn, () => daemon.ensureProject(root, conn));
|
|
315
326
|
return null;
|
|
316
327
|
}
|
|
@@ -318,6 +329,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
318
329
|
const root = String(cmd.root || "");
|
|
319
330
|
if (!root)
|
|
320
331
|
return { ok: false, error: "missing root" };
|
|
332
|
+
daemon.leaseForCli(root);
|
|
321
333
|
startStreaming(conn, () => daemon.indexProject(root, conn, {
|
|
322
334
|
reset: !!cmd.reset,
|
|
323
335
|
dryRun: !!cmd.dryRun,
|
|
@@ -385,8 +397,11 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
385
397
|
const ac = new AbortController();
|
|
386
398
|
const onClose = () => ac.abort();
|
|
387
399
|
conn.on("close", onClose);
|
|
400
|
+
const name = String(cmd.cmd);
|
|
388
401
|
try {
|
|
389
|
-
|
|
402
|
+
// Heavy verbs queue on a global gate before admission; see
|
|
403
|
+
// HEAVY_READ_VERBS in read-verbs.ts.
|
|
404
|
+
return yield (0, read_verbs_1.runReadVerb)(name, ac.signal, () => daemon.runSharedOperation(name, ac.signal, (signal) => verb(cmd, { daemon, conn, signal })));
|
|
390
405
|
}
|
|
391
406
|
finally {
|
|
392
407
|
conn.off("close", onClose);
|
|
@@ -20,12 +20,16 @@
|
|
|
20
20
|
* so `ipc-handler` tests can drive them with a fake `Daemon`.
|
|
21
21
|
*/
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.READ_VERBS_PROTOCOL = void 0;
|
|
23
|
+
exports.heavyReadGate = exports.DEFAULT_HEAVY_READ_CONCURRENCY = exports.HEAVY_READ_VERBS = exports.READ_VERBS_PROTOCOL = void 0;
|
|
24
24
|
exports.registerReadVerbs = registerReadVerbs;
|
|
25
25
|
exports.getReadVerb = getReadVerb;
|
|
26
26
|
exports.readVerbNames = readVerbNames;
|
|
27
|
+
exports.resolveHeavyReadConcurrency = resolveHeavyReadConcurrency;
|
|
28
|
+
exports.isHeavyReadVerb = isHeavyReadVerb;
|
|
29
|
+
exports.runReadVerb = runReadVerb;
|
|
27
30
|
exports.clearReadVerbs = clearReadVerbs;
|
|
28
31
|
exports.registerGraphVerbs = registerGraphVerbs;
|
|
32
|
+
const async_semaphore_1 = require("../utils/async-semaphore");
|
|
29
33
|
const graph_handler_1 = require("./graph-handler");
|
|
30
34
|
/** Bumped when the verb wire shapes change incompatibly. */
|
|
31
35
|
exports.READ_VERBS_PROTOCOL = 1;
|
|
@@ -50,6 +54,53 @@ function getReadVerb(name) {
|
|
|
50
54
|
function readVerbNames() {
|
|
51
55
|
return [...registry.keys()].sort();
|
|
52
56
|
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Heavy-verb admission.
|
|
59
|
+
//
|
|
60
|
+
// `runSharedOperation` only tracks a verb; it never limits how many run. These
|
|
61
|
+
// verbs each scan a large slice of the table (tens to hundreds of thousands of
|
|
62
|
+
// rows, or one vector search per sampled anchor), and their Arrow buffers sit
|
|
63
|
+
// in native memory until the answer is built. With one MCP session per Claude
|
|
64
|
+
// Code window, N sessions asking at once meant N such scans resident together.
|
|
65
|
+
// They now share a small global semaphore; everything else stays unbounded,
|
|
66
|
+
// because point lookups (`graph.resolve`, `rows.locate`, ...) are cheap and
|
|
67
|
+
// must not queue behind an audit.
|
|
68
|
+
//
|
|
69
|
+
// A queued request waits *before* `runSharedOperation` admits it (see
|
|
70
|
+
// `runReadVerb`), so an exclusive rebuild does not have to sit behind scans
|
|
71
|
+
// that have not started yet, and a client that disconnects while queued
|
|
72
|
+
// leaves without ever touching the store.
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
exports.HEAVY_READ_VERBS = new Set([
|
|
75
|
+
"graph.audit", // up to AUDIT_ROW_LIMIT rows with symbol arrays
|
|
76
|
+
"graph.dead", // whole caller set
|
|
77
|
+
"graph.risk", // up to MAX_RISK_SYMBOLS x three content scans
|
|
78
|
+
"graph.subgraph", // per-file symbol scans over up to MAX_SUBGRAPH_FILES
|
|
79
|
+
"graph.trace", // multi-hop caller/callee expansion
|
|
80
|
+
"rows.project", // up to 200k rows with two symbol-array columns
|
|
81
|
+
"vector.similar", // vector search plus content
|
|
82
|
+
"vector.surprises", // up to 100k-row scan plus one search per anchor
|
|
83
|
+
]);
|
|
84
|
+
exports.DEFAULT_HEAVY_READ_CONCURRENCY = 2;
|
|
85
|
+
/** `GMAX_HEAVY_READ_CONCURRENCY`, or the default when unset or not a positive integer. */
|
|
86
|
+
function resolveHeavyReadConcurrency(raw = process.env.GMAX_HEAVY_READ_CONCURRENCY) {
|
|
87
|
+
const n = Number(raw);
|
|
88
|
+
return raw !== undefined && Number.isInteger(n) && n >= 1
|
|
89
|
+
? n
|
|
90
|
+
: exports.DEFAULT_HEAVY_READ_CONCURRENCY;
|
|
91
|
+
}
|
|
92
|
+
exports.heavyReadGate = new async_semaphore_1.AsyncSemaphore(resolveHeavyReadConcurrency());
|
|
93
|
+
function isHeavyReadVerb(name) {
|
|
94
|
+
return typeof name === "string" && exports.HEAVY_READ_VERBS.has(name);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Admission wrapper `ipc-handler` puts around a verb's shared operation: heavy
|
|
98
|
+
* verbs wait for a gate slot first (abortable through `signal`), light verbs
|
|
99
|
+
* run straight through.
|
|
100
|
+
*/
|
|
101
|
+
function runReadVerb(name, signal, run, gate = exports.heavyReadGate) {
|
|
102
|
+
return isHeavyReadVerb(name) ? gate.run(signal, run) : run();
|
|
103
|
+
}
|
|
53
104
|
/** Test hook: drop every registration. */
|
|
54
105
|
function clearReadVerbs() {
|
|
55
106
|
registry.clear();
|
|
@@ -76,10 +76,13 @@ function handleDaemonSearch(deps, payload, signal) {
|
|
|
76
76
|
if (!deps.processors.has(root)) {
|
|
77
77
|
// A full index (--reset) or the initial index removes/defers the
|
|
78
78
|
// processor while (re)building. The partial index is still queryable, so
|
|
79
|
-
// answer the search and flag it partial (below) rather than erroring
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
// answer the search and flag it partial (below) rather than erroring.
|
|
80
|
+
// An indexed project nobody holds a watch lease on is served from its
|
|
81
|
+
// last-synced vectors — watching is per session now, not a precondition
|
|
82
|
+
// for reading. Only unknown, never-indexed roots get "not watched".
|
|
83
|
+
const status = (_c = (0, project_registry_1.getProject)(root)) === null || _c === void 0 ? void 0 : _c.status;
|
|
84
|
+
const indexingNow = deps.indexProgress.has(root) || status === "pending";
|
|
85
|
+
if (!indexingNow && status !== "indexed") {
|
|
83
86
|
return {
|
|
84
87
|
ok: false,
|
|
85
88
|
error: "project not watched",
|
|
@@ -0,0 +1,179 @@
|
|
|
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.WatchLeases = exports.MAX_LEASE_TTL_MS = exports.DEFAULT_LEASE_TTL_MS = void 0;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
exports.DEFAULT_LEASE_TTL_MS = 30 * 60 * 1000;
|
|
40
|
+
/** Upper bound on a requested TTL — a lease must not outlive its holder by days. */
|
|
41
|
+
exports.MAX_LEASE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
42
|
+
function defaultIsPidAlive(pid) {
|
|
43
|
+
try {
|
|
44
|
+
process.kill(pid, 0);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
// EPERM: the process exists but belongs to someone else.
|
|
49
|
+
return err.code === "EPERM";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
class WatchLeases {
|
|
53
|
+
constructor(file, opts = {}) {
|
|
54
|
+
var _a, _b;
|
|
55
|
+
this.file = file;
|
|
56
|
+
this.leases = new Map();
|
|
57
|
+
this.now = (_a = opts.now) !== null && _a !== void 0 ? _a : Date.now;
|
|
58
|
+
this.isPidAlive = (_b = opts.isPidAlive) !== null && _b !== void 0 ? _b : defaultIsPidAlive;
|
|
59
|
+
}
|
|
60
|
+
static key(root, holder) {
|
|
61
|
+
return `${root}\0${holder}`;
|
|
62
|
+
}
|
|
63
|
+
/** Take or renew a lease. Returns true if the root was not wanted before. */
|
|
64
|
+
acquire(root, req) {
|
|
65
|
+
const wasWanted = this.isWanted(root);
|
|
66
|
+
const ttl = Math.min(Math.max(req.ttlMs, 1000), exports.MAX_LEASE_TTL_MS);
|
|
67
|
+
this.leases.set(WatchLeases.key(root, req.holder), {
|
|
68
|
+
root,
|
|
69
|
+
holder: req.holder,
|
|
70
|
+
pid: req.pid,
|
|
71
|
+
expiresAt: this.now() + ttl,
|
|
72
|
+
});
|
|
73
|
+
this.persist();
|
|
74
|
+
return !wasWanted;
|
|
75
|
+
}
|
|
76
|
+
/** Drop one holder's lease, or every lease on the root when holder is omitted. */
|
|
77
|
+
release(root, holder) {
|
|
78
|
+
let changed = false;
|
|
79
|
+
for (const [key, lease] of this.leases) {
|
|
80
|
+
if (lease.root !== root)
|
|
81
|
+
continue;
|
|
82
|
+
if (holder !== undefined && lease.holder !== holder)
|
|
83
|
+
continue;
|
|
84
|
+
this.leases.delete(key);
|
|
85
|
+
changed = true;
|
|
86
|
+
}
|
|
87
|
+
if (changed)
|
|
88
|
+
this.persist();
|
|
89
|
+
}
|
|
90
|
+
isLive(lease) {
|
|
91
|
+
if (lease.expiresAt <= this.now())
|
|
92
|
+
return false;
|
|
93
|
+
if (lease.pid !== undefined && !this.isPidAlive(lease.pid))
|
|
94
|
+
return false;
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
isWanted(root) {
|
|
98
|
+
for (const lease of this.leases.values()) {
|
|
99
|
+
if (lease.root === root && this.isLive(lease))
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
/** Drop dead leases. Returns roots that were wanted before and are not now. */
|
|
105
|
+
sweep() {
|
|
106
|
+
// Every stored lease was live when last swept or acquired, so the stored
|
|
107
|
+
// roots — not the currently-live ones — are what "wanted before" means.
|
|
108
|
+
const before = new Set([...this.leases.values()].map((l) => l.root));
|
|
109
|
+
let changed = false;
|
|
110
|
+
for (const [key, lease] of this.leases) {
|
|
111
|
+
if (!this.isLive(lease)) {
|
|
112
|
+
this.leases.delete(key);
|
|
113
|
+
changed = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!changed)
|
|
117
|
+
return [];
|
|
118
|
+
this.persist();
|
|
119
|
+
const after = this.wantedRoots();
|
|
120
|
+
return [...before].filter((root) => !after.has(root));
|
|
121
|
+
}
|
|
122
|
+
wantedRoots() {
|
|
123
|
+
const roots = new Set();
|
|
124
|
+
for (const lease of this.leases.values()) {
|
|
125
|
+
if (this.isLive(lease))
|
|
126
|
+
roots.add(lease.root);
|
|
127
|
+
}
|
|
128
|
+
return roots;
|
|
129
|
+
}
|
|
130
|
+
/** Live leases, for `gmax status`. */
|
|
131
|
+
list() {
|
|
132
|
+
return [...this.leases.values()].filter((lease) => this.isLive(lease));
|
|
133
|
+
}
|
|
134
|
+
load() {
|
|
135
|
+
if (!this.file)
|
|
136
|
+
return;
|
|
137
|
+
let raw;
|
|
138
|
+
try {
|
|
139
|
+
raw = JSON.parse(fs.readFileSync(this.file, "utf-8"));
|
|
140
|
+
}
|
|
141
|
+
catch (_a) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!Array.isArray(raw))
|
|
145
|
+
return;
|
|
146
|
+
for (const entry of raw) {
|
|
147
|
+
if (!entry ||
|
|
148
|
+
typeof entry.root !== "string" ||
|
|
149
|
+
typeof entry.holder !== "string" ||
|
|
150
|
+
typeof entry.expiresAt !== "number") {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const lease = {
|
|
154
|
+
root: entry.root,
|
|
155
|
+
holder: entry.holder,
|
|
156
|
+
pid: typeof entry.pid === "number" ? entry.pid : undefined,
|
|
157
|
+
expiresAt: Math.min(entry.expiresAt, this.now() + exports.MAX_LEASE_TTL_MS),
|
|
158
|
+
};
|
|
159
|
+
if (this.isLive(lease)) {
|
|
160
|
+
this.leases.set(WatchLeases.key(lease.root, lease.holder), lease);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
persist() {
|
|
165
|
+
if (!this.file)
|
|
166
|
+
return;
|
|
167
|
+
try {
|
|
168
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true });
|
|
169
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
170
|
+
fs.writeFileSync(tmp, JSON.stringify([...this.leases.values()]));
|
|
171
|
+
fs.renameSync(tmp, this.file);
|
|
172
|
+
}
|
|
173
|
+
catch (_a) {
|
|
174
|
+
// Persistence only shortens the gap after a restart; the next renewal
|
|
175
|
+
// re-establishes the lease either way.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
exports.WatchLeases = WatchLeases;
|
|
@@ -59,6 +59,11 @@ const MAX_BATCH_SIZE = 50;
|
|
|
59
59
|
// Pool priority cannot preempt an active processFile, so keep one worker free
|
|
60
60
|
// for latency-sensitive encodeQuery/rerank work during indexing bursts.
|
|
61
61
|
const DEFAULT_BATCH_CONCURRENCY = Math.max(1, config_1.CONFIG.WORKER_THREADS - 1);
|
|
62
|
+
// Files per concurrent task slot. A watcher batch of a handful of small files
|
|
63
|
+
// finishes in a second or two on one worker, so fanning it out only makes the
|
|
64
|
+
// pool cold-spawn workers (each loads the models, ~1 GB) that are reaped
|
|
65
|
+
// unused 60s later. Scale fan-out with the batch instead.
|
|
66
|
+
const FILES_PER_BATCH_SLOT = 4;
|
|
62
67
|
class ProjectBatchProcessor {
|
|
63
68
|
constructor(opts) {
|
|
64
69
|
var _a, _b, _c;
|
|
@@ -436,6 +441,7 @@ class ProjectBatchProcessor {
|
|
|
436
441
|
}
|
|
437
442
|
}
|
|
438
443
|
});
|
|
444
|
+
const batchConcurrency = Math.min(this.concurrency, Math.max(1, Math.ceil(batch.size / FILES_PER_BATCH_SLOT)));
|
|
439
445
|
const activeTasks = [];
|
|
440
446
|
const schedule = (task) => __awaiter(this, void 0, void 0, function* () {
|
|
441
447
|
const taskPromise = task();
|
|
@@ -446,7 +452,7 @@ class ProjectBatchProcessor {
|
|
|
446
452
|
activeTasks.splice(index, 1);
|
|
447
453
|
};
|
|
448
454
|
void taskPromise.then(removeActiveTask, removeActiveTask);
|
|
449
|
-
if (activeTasks.length >=
|
|
455
|
+
if (activeTasks.length >= batchConcurrency) {
|
|
450
456
|
yield Promise.race(activeTasks);
|
|
451
457
|
}
|
|
452
458
|
});
|
|
@@ -96,6 +96,10 @@ exports.WATCHER_IGNORE_GLOBS = [
|
|
|
96
96
|
// "**/<name>" ignores the directory entry itself, "**/<name>/**" its contents.
|
|
97
97
|
...IGNORED_DIR_NAMES.flatMap((name) => [`**/${name}`, `**/${name}/**`]),
|
|
98
98
|
"**/*.tmp.*", // editor atomic save artifacts
|
|
99
|
+
// Atomic-save temp files named `.!<pid>!<name>` (e.g. `.!8903!dotmd.md`).
|
|
100
|
+
// picomatch treats `!` as a literal unless it leads the pattern or opens
|
|
101
|
+
// an `!(...)` extglob, so this matches only that shape, at any depth.
|
|
102
|
+
"**/.!*!*",
|
|
99
103
|
"**/*.sb-*", // Xcode swap files
|
|
100
104
|
// Codegen output never reaches the index (file policy drops it), so don't
|
|
101
105
|
// let it reach the watcher either — it is the dominant source of FSEvents
|
|
@@ -42,12 +42,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
42
42
|
});
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.PAGERANK_MEMORY_CACHE_MAX = exports.PAGERANK_ROW_LIMIT = void 0;
|
|
45
46
|
exports.computePageRank = computePageRank;
|
|
46
47
|
exports.buildGraphFromDb = buildGraphFromDb;
|
|
47
48
|
exports.writeDiskCache = writeDiskCache;
|
|
48
49
|
exports.loadOrComputePageRank = loadOrComputePageRank;
|
|
49
50
|
exports.pageRankBoostForSymbols = pageRankBoostForSymbols;
|
|
50
51
|
exports._clearMemoryCacheForTests = _clearMemoryCacheForTests;
|
|
52
|
+
exports._memoryCacheKeysForTests = _memoryCacheKeysForTests;
|
|
51
53
|
exports._cachePathForTests = _cachePathForTests;
|
|
52
54
|
const crypto = __importStar(require("node:crypto"));
|
|
53
55
|
const fs = __importStar(require("node:fs"));
|
|
@@ -58,7 +60,51 @@ const DEFAULT_DAMPING = 0.85;
|
|
|
58
60
|
const DEFAULT_MAX_ITER = 50;
|
|
59
61
|
const DEFAULT_TOL = 1e-6;
|
|
60
62
|
const DEFAULT_TTL_MS = 60 * 60 * 1000;
|
|
63
|
+
/**
|
|
64
|
+
* Row cap for the graph scan. Matches `graph.audit`'s AUDIT_ROW_LIMIT: the
|
|
65
|
+
* largest indexed project holds ~270k chunks, so this never truncates a real
|
|
66
|
+
* project, but a runaway prefix (a home directory, a monorepo root) can no
|
|
67
|
+
* longer pull millions of symbol-array rows into daemon memory at once. A
|
|
68
|
+
* truncated scan ranks the symbols it saw; the ranking is a search boost, not
|
|
69
|
+
* an answer, so a partial graph degrades gracefully.
|
|
70
|
+
*/
|
|
71
|
+
exports.PAGERANK_ROW_LIMIT = 500000;
|
|
72
|
+
/**
|
|
73
|
+
* Score maps stay in the daemon for its lifetime, one per scoped prefix a
|
|
74
|
+
* search has used (`--in` makes those open-ended). Keep the most recently
|
|
75
|
+
* used few; each is a Map over every defined symbol in its scope.
|
|
76
|
+
*/
|
|
77
|
+
exports.PAGERANK_MEMORY_CACHE_MAX = 8;
|
|
78
|
+
// Map iteration order is insertion order, so re-inserting on a hit makes the
|
|
79
|
+
// first key the least recently used.
|
|
61
80
|
const memoryCache = new Map();
|
|
81
|
+
// Concurrent searches on a cold prefix share one scan instead of each running
|
|
82
|
+
// their own.
|
|
83
|
+
const inFlight = new Map();
|
|
84
|
+
function memoryGet(pathPrefix) {
|
|
85
|
+
const entry = memoryCache.get(pathPrefix);
|
|
86
|
+
if (!entry)
|
|
87
|
+
return undefined;
|
|
88
|
+
memoryCache.delete(pathPrefix);
|
|
89
|
+
if (Date.now() - entry.computedAt >= getTtlMs())
|
|
90
|
+
return undefined;
|
|
91
|
+
memoryCache.set(pathPrefix, entry);
|
|
92
|
+
return entry;
|
|
93
|
+
}
|
|
94
|
+
function memorySet(pathPrefix, entry) {
|
|
95
|
+
memoryCache.delete(pathPrefix);
|
|
96
|
+
memoryCache.set(pathPrefix, entry);
|
|
97
|
+
const ttl = getTtlMs();
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
for (const [key, value] of memoryCache) {
|
|
100
|
+
if (now - value.computedAt >= ttl)
|
|
101
|
+
memoryCache.delete(key);
|
|
102
|
+
}
|
|
103
|
+
while (memoryCache.size > exports.PAGERANK_MEMORY_CACHE_MAX) {
|
|
104
|
+
const oldest = memoryCache.keys().next().value;
|
|
105
|
+
memoryCache.delete(oldest);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
62
108
|
function computePageRank(graph, damping = DEFAULT_DAMPING, maxIter = DEFAULT_MAX_ITER, tol = DEFAULT_TOL) {
|
|
63
109
|
const N = graph.nodes.length;
|
|
64
110
|
const result = new Map();
|
|
@@ -148,6 +194,7 @@ function buildGraphFromDb(db, pathPrefix) {
|
|
|
148
194
|
.query()
|
|
149
195
|
.select(["defined_symbols", "referenced_symbols"])
|
|
150
196
|
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
197
|
+
.limit(exports.PAGERANK_ROW_LIMIT)
|
|
151
198
|
.toArray();
|
|
152
199
|
const nodes = new Set();
|
|
153
200
|
const edges = new Map();
|
|
@@ -224,28 +271,38 @@ function writeDiskCache(pathPrefix, scores) {
|
|
|
224
271
|
}
|
|
225
272
|
function loadOrComputePageRank(db, pathPrefix) {
|
|
226
273
|
return __awaiter(this, void 0, void 0, function* () {
|
|
227
|
-
const mem =
|
|
228
|
-
if (mem
|
|
274
|
+
const mem = memoryGet(pathPrefix);
|
|
275
|
+
if (mem)
|
|
229
276
|
return { scores: mem.scores, max: mem.max };
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
277
|
+
const pending = inFlight.get(pathPrefix);
|
|
278
|
+
if (pending)
|
|
279
|
+
return pending;
|
|
280
|
+
const task = (() => __awaiter(this, void 0, void 0, function* () {
|
|
281
|
+
const disk = readDiskCache(pathPrefix);
|
|
282
|
+
if (disk) {
|
|
283
|
+
memorySet(pathPrefix, disk);
|
|
284
|
+
return { scores: disk.scores, max: disk.max };
|
|
285
|
+
}
|
|
286
|
+
const graph = yield buildGraphFromDb(db, pathPrefix);
|
|
287
|
+
const scores = computePageRank(graph);
|
|
288
|
+
let max = 0;
|
|
289
|
+
for (const v of scores.values())
|
|
290
|
+
if (v > max)
|
|
291
|
+
max = v;
|
|
292
|
+
memorySet(pathPrefix, { scores, max, computedAt: Date.now() });
|
|
293
|
+
try {
|
|
294
|
+
writeDiskCache(pathPrefix, scores);
|
|
295
|
+
}
|
|
296
|
+
catch (_a) { }
|
|
297
|
+
return { scores, max };
|
|
298
|
+
}))();
|
|
299
|
+
inFlight.set(pathPrefix, task);
|
|
244
300
|
try {
|
|
245
|
-
|
|
301
|
+
return yield task;
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
inFlight.delete(pathPrefix);
|
|
246
305
|
}
|
|
247
|
-
catch (_a) { }
|
|
248
|
-
return { scores, max };
|
|
249
306
|
});
|
|
250
307
|
}
|
|
251
308
|
function pageRankBoostForSymbols(symbols, scores, max) {
|
|
@@ -261,6 +318,10 @@ function pageRankBoostForSymbols(symbols, scores, max) {
|
|
|
261
318
|
}
|
|
262
319
|
function _clearMemoryCacheForTests() {
|
|
263
320
|
memoryCache.clear();
|
|
321
|
+
inFlight.clear();
|
|
322
|
+
}
|
|
323
|
+
function _memoryCacheKeysForTests() {
|
|
324
|
+
return [...memoryCache.keys()];
|
|
264
325
|
}
|
|
265
326
|
function _cachePathForTests(pathPrefix) {
|
|
266
327
|
return cachePathFor(pathPrefix);
|
|
@@ -187,11 +187,33 @@ const COMPACTION_MIN_INTERVAL_MS = 30 * 60 * 1000;
|
|
|
187
187
|
* so a store that cannot be improved stops paying for the attempt.
|
|
188
188
|
*/
|
|
189
189
|
const COMPACTION_MAX_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
190
|
+
/**
|
|
191
|
+
* Bounds for LanceDB's per-connection caches.
|
|
192
|
+
*
|
|
193
|
+
* `lancedb.connect()` without a Session gets a 6 GB index cache and a 1 GB
|
|
194
|
+
* metadata cache, and a connection lives as long as the daemon. On a 48 GB host
|
|
195
|
+
* that was already swapping, the daemon reached a 4 GB footprint (9.6 GB peak),
|
|
196
|
+
* almost all native malloc and invisible to the RSS-based recycle because most of
|
|
197
|
+
* it had been compressed or swapped. The whole on-disk index directory for a
|
|
198
|
+
* 430k-row store is ~650 MB, so 1 GB still holds every index; the metadata cache
|
|
199
|
+
* only needs the latest manifests, not every retained version.
|
|
200
|
+
*/
|
|
201
|
+
const LANCE_INDEX_CACHE_MB = envMb("GMAX_LANCE_INDEX_CACHE_MB", 1024);
|
|
202
|
+
const LANCE_METADATA_CACHE_MB = envMb("GMAX_LANCE_METADATA_CACHE_MB", 256);
|
|
203
|
+
function envMb(name, fallback) {
|
|
204
|
+
const parsed = Number(process.env[name]);
|
|
205
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
206
|
+
}
|
|
207
|
+
function createLanceSession() {
|
|
208
|
+
const mb = (n) => BigInt(n * 1024 * 1024);
|
|
209
|
+
return new lancedb.Session(mb(LANCE_INDEX_CACHE_MB), mb(LANCE_METADATA_CACHE_MB));
|
|
210
|
+
}
|
|
190
211
|
class VectorDB {
|
|
191
212
|
constructor(lancedbDir, vectorDim, suppliedLease) {
|
|
192
213
|
this.lancedbDir = lancedbDir;
|
|
193
214
|
this.suppliedLease = suppliedLease;
|
|
194
215
|
this.db = null;
|
|
216
|
+
this.session = null;
|
|
195
217
|
this.closed = false;
|
|
196
218
|
this.maintenanceRunning = false;
|
|
197
219
|
this.maintenancePromise = null;
|
|
@@ -291,6 +313,16 @@ class VectorDB {
|
|
|
291
313
|
}
|
|
292
314
|
/** True iff a maintenance tick is currently running. Used by the daemon to
|
|
293
315
|
* defer idle shutdown so we don't tear down LanceDB mid-optimize. */
|
|
316
|
+
/** Bytes held by LanceDB's index + metadata caches (0 before first connect). */
|
|
317
|
+
cacheSizeBytes() {
|
|
318
|
+
var _a, _b;
|
|
319
|
+
try {
|
|
320
|
+
return Number((_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.sizeBytes()) !== null && _b !== void 0 ? _b : 0);
|
|
321
|
+
}
|
|
322
|
+
catch (_c) {
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
294
326
|
isMaintenanceActive() {
|
|
295
327
|
return this.maintenancePromise !== null;
|
|
296
328
|
}
|
|
@@ -324,7 +356,8 @@ class VectorDB {
|
|
|
324
356
|
}
|
|
325
357
|
if (!this.db) {
|
|
326
358
|
fs.mkdirSync(this.lancedbDir, { recursive: true });
|
|
327
|
-
this.
|
|
359
|
+
this.session = createLanceSession();
|
|
360
|
+
this.db = yield lancedb.connect(this.lancedbDir, {}, this.session);
|
|
328
361
|
}
|
|
329
362
|
return this.db;
|
|
330
363
|
});
|
|
@@ -1578,6 +1611,7 @@ class VectorDB {
|
|
|
1578
1611
|
}
|
|
1579
1612
|
}
|
|
1580
1613
|
this.db = null;
|
|
1614
|
+
this.session = null;
|
|
1581
1615
|
if (this.leasePromise) {
|
|
1582
1616
|
let lease = null;
|
|
1583
1617
|
try {
|