grepmax 0.26.27 → 0.26.30
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 +40 -0
- package/dist/commands/audit.js +28 -260
- package/dist/commands/context.js +42 -26
- package/dist/commands/dead.js +37 -35
- package/dist/commands/doctor.js +280 -10
- package/dist/commands/extract.js +79 -49
- package/dist/commands/impact.js +74 -20
- package/dist/commands/log.js +25 -25
- package/dist/commands/mcp.js +639 -475
- package/dist/commands/peek.js +49 -47
- package/dist/commands/project.js +29 -107
- package/dist/commands/related.js +43 -52
- package/dist/commands/search-run.js +23 -8
- package/dist/commands/search.js +7 -2
- package/dist/commands/similar.js +50 -76
- package/dist/commands/skeleton.js +137 -83
- package/dist/commands/status.js +90 -25
- package/dist/commands/surprises.js +73 -16
- package/dist/commands/symbols.js +58 -77
- package/dist/commands/test-find.js +51 -17
- package/dist/commands/trace.js +26 -11
- 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 +157 -13
- package/dist/lib/daemon/graph-handler.js +859 -0
- package/dist/lib/daemon/ipc-handler.js +36 -4
- package/dist/lib/daemon/read-verbs.js +156 -0
- package/dist/lib/daemon/rows-handler.js +665 -0
- package/dist/lib/daemon/search-handler.js +7 -4
- package/dist/lib/daemon/vector-handler.js +199 -0
- 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/daemon-client.js +11 -0
- package/dist/lib/utils/process-footprint.js +51 -0
- package/dist/lib/utils/store-access.js +381 -0
- package/dist/lib/utils/tests-footer.js +19 -2
- package/dist/lib/utils/watcher-launcher.js +46 -19
- package/dist/lib/workers/pool.js +72 -2
- package/package.json +1 -1
- 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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Daemon-side handlers for the `vector.*` read verbs — the two commands whose
|
|
4
|
+
* store work is a vector scan rather than a row select.
|
|
5
|
+
*
|
|
6
|
+
* Both ship the *analysed* answer, never the vectors:
|
|
7
|
+
*
|
|
8
|
+
* - `vector.similar` runs the source-chunk lookup, `configureAnnVectorQuery`,
|
|
9
|
+
* `table.vectorSearch`, and the self/threshold filter, and returns the ranked
|
|
10
|
+
* chunks. The 384-float source vector never crosses the socket.
|
|
11
|
+
* - `vector.surprises` runs `analyzeSurprisingConnections` and returns the
|
|
12
|
+
* summary plus the findings the caller will actually print. The raw `pairs`
|
|
13
|
+
* array is dropped: nothing renders it, and it is the largest field by far.
|
|
14
|
+
*
|
|
15
|
+
* As in rows-handler.ts, each `run*` function is the single implementation both
|
|
16
|
+
* the verb and the command's in-process fallback call, so the two paths cannot
|
|
17
|
+
* produce different output.
|
|
18
|
+
*/
|
|
19
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
20
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
21
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
22
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
23
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
24
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
25
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.runSimilar = runSimilar;
|
|
30
|
+
exports.handleVectorSimilar = handleVectorSimilar;
|
|
31
|
+
exports.runSurprises = runSurprises;
|
|
32
|
+
exports.handleVectorSurprises = handleVectorSurprises;
|
|
33
|
+
exports.registerVectorVerbs = registerVectorVerbs;
|
|
34
|
+
const surprising_connections_1 = require("../analysis/surprising-connections");
|
|
35
|
+
const ann_config_1 = require("../store/ann-config");
|
|
36
|
+
const arrow_1 = require("../utils/arrow");
|
|
37
|
+
const filter_builder_1 = require("../utils/filter-builder");
|
|
38
|
+
const path_containment_1 = require("../utils/path-containment");
|
|
39
|
+
const scope_filter_1 = require("../utils/scope-filter");
|
|
40
|
+
const read_verbs_1 = require("./read-verbs");
|
|
41
|
+
const rows_handler_1 = require("./rows-handler");
|
|
42
|
+
function runSimilar(deps, req) {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
var _a, _b;
|
|
45
|
+
if (!deps.vectorDb)
|
|
46
|
+
throw new rows_handler_1.ReadVerbError("daemon not ready");
|
|
47
|
+
(_a = deps.touchActivity) === null || _a === void 0 ? void 0 : _a.call(deps);
|
|
48
|
+
const table = yield deps.vectorDb.ensureTable();
|
|
49
|
+
const sourceSelect = ["vector", "path", "defined_symbols", "start_line"];
|
|
50
|
+
const sourceRows = yield table
|
|
51
|
+
.query()
|
|
52
|
+
.select(sourceSelect)
|
|
53
|
+
.where(req.absPath !== undefined
|
|
54
|
+
? `path = '${(0, filter_builder_1.escapeSqlString)(req.absPath)}'`
|
|
55
|
+
: `array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)((_b = req.symbol) !== null && _b !== void 0 ? _b : "")}') AND ${(0, filter_builder_1.pathStartsWith)(`${req.projectRoot}/`)}`)
|
|
56
|
+
.limit(1)
|
|
57
|
+
.toArray();
|
|
58
|
+
if (sourceRows.length === 0)
|
|
59
|
+
return { status: "not-found" };
|
|
60
|
+
const source = sourceRows[0];
|
|
61
|
+
const sourceVector = source.vector;
|
|
62
|
+
const sourcePath = String(source.path || "");
|
|
63
|
+
if (!sourceVector || sourceVector.length === 0)
|
|
64
|
+
return { status: "no-vector" };
|
|
65
|
+
const pathScope = (0, scope_filter_1.buildScopeWhere)(req.scope);
|
|
66
|
+
const results = yield (0, ann_config_1.configureAnnVectorQuery)(table.vectorSearch(sourceVector))
|
|
67
|
+
.select([
|
|
68
|
+
"path",
|
|
69
|
+
"start_line",
|
|
70
|
+
"end_line",
|
|
71
|
+
"defined_symbols",
|
|
72
|
+
"role",
|
|
73
|
+
"content",
|
|
74
|
+
"_distance",
|
|
75
|
+
])
|
|
76
|
+
.where(pathScope)
|
|
77
|
+
.limit(req.limit + 5) // fetch extra to account for self-filtering
|
|
78
|
+
.toArray();
|
|
79
|
+
// Filter out self and apply threshold
|
|
80
|
+
const filtered = results.filter((raw) => {
|
|
81
|
+
const r = raw;
|
|
82
|
+
if (r.path === sourcePath && r.start_line === source.start_line)
|
|
83
|
+
return false;
|
|
84
|
+
if (req.threshold > 0) {
|
|
85
|
+
// LanceDB returns L2 distance; convert to similarity
|
|
86
|
+
const sim = 1 / (1 + (Number(r._distance) || 0));
|
|
87
|
+
if (sim < req.threshold)
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
});
|
|
92
|
+
// `content` is selected (keeping the query identical) but never rendered, so
|
|
93
|
+
// it is dropped here rather than shipped.
|
|
94
|
+
return {
|
|
95
|
+
status: "ok",
|
|
96
|
+
results: filtered.map((raw) => {
|
|
97
|
+
var _a, _b, _c, _d, _e;
|
|
98
|
+
const r = raw;
|
|
99
|
+
return {
|
|
100
|
+
path: String((_a = r.path) !== null && _a !== void 0 ? _a : ""),
|
|
101
|
+
start_line: Number((_b = r.start_line) !== null && _b !== void 0 ? _b : 0),
|
|
102
|
+
end_line: Number((_c = r.end_line) !== null && _c !== void 0 ? _c : 0),
|
|
103
|
+
defined_symbols: (0, arrow_1.toArr)(r.defined_symbols),
|
|
104
|
+
role: String((_d = r.role) !== null && _d !== void 0 ? _d : ""),
|
|
105
|
+
_distance: Number((_e = r._distance) !== null && _e !== void 0 ? _e : 0),
|
|
106
|
+
};
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function handleVectorSimilar(deps, payload) {
|
|
112
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
return (0, rows_handler_1.asReadVerbResponse)(() => __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
const projectRoot = (0, rows_handler_1.assertReadableProject)(payload.projectRoot);
|
|
115
|
+
const scope = (0, rows_handler_1.resolveWireScope)(projectRoot, payload.scope);
|
|
116
|
+
const absPath = typeof payload.absPath === "string" && payload.absPath
|
|
117
|
+
? (0, path_containment_1.resolveContainedPath)(projectRoot, payload.absPath)
|
|
118
|
+
: undefined;
|
|
119
|
+
const symbol = typeof payload.symbol === "string" && payload.symbol
|
|
120
|
+
? payload.symbol
|
|
121
|
+
: undefined;
|
|
122
|
+
if (!absPath && !symbol)
|
|
123
|
+
throw new rows_handler_1.ReadVerbError("missing target");
|
|
124
|
+
const result = yield runSimilar(deps, {
|
|
125
|
+
projectRoot,
|
|
126
|
+
absPath,
|
|
127
|
+
symbol,
|
|
128
|
+
scope,
|
|
129
|
+
limit: (0, rows_handler_1.clampInt)(payload.limit, 5, 1, 25),
|
|
130
|
+
threshold: typeof payload.threshold === "number" &&
|
|
131
|
+
Number.isFinite(payload.threshold)
|
|
132
|
+
? payload.threshold
|
|
133
|
+
: 0,
|
|
134
|
+
});
|
|
135
|
+
return Object.assign({ ok: true }, result);
|
|
136
|
+
}));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function runSurprises(deps, req) {
|
|
140
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
141
|
+
var _a;
|
|
142
|
+
if (!deps.vectorDb)
|
|
143
|
+
throw new rows_handler_1.ReadVerbError("daemon not ready");
|
|
144
|
+
(_a = deps.touchActivity) === null || _a === void 0 ? void 0 : _a.call(deps);
|
|
145
|
+
const table = yield deps.vectorDb.ensureTable();
|
|
146
|
+
const result = yield (0, surprising_connections_1.analyzeSurprisingConnections)(table, req.projectRoot, req.options);
|
|
147
|
+
// Both formatters use only `summary` and `findings.slice(0, top)`, plus
|
|
148
|
+
// `findings.length === 0`. Trimming to `top` here preserves both and keeps
|
|
149
|
+
// `pairs` (the bulk of the object) off the wire entirely.
|
|
150
|
+
return {
|
|
151
|
+
summary: result.summary,
|
|
152
|
+
findings: result.findings.slice(0, req.top),
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function handleVectorSurprises(deps, payload) {
|
|
157
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
158
|
+
return (0, rows_handler_1.asReadVerbResponse)(() => __awaiter(this, void 0, void 0, function* () {
|
|
159
|
+
var _a;
|
|
160
|
+
const projectRoot = (0, rows_handler_1.assertReadableProject)(payload.projectRoot);
|
|
161
|
+
const raw = ((_a = payload.options) !== null && _a !== void 0 ? _a : {});
|
|
162
|
+
const toPrefixList = (value) => {
|
|
163
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
164
|
+
return undefined;
|
|
165
|
+
// Validated here rather than trusted: these reach resolveScope, which
|
|
166
|
+
// builds the WHERE clause the scan runs under.
|
|
167
|
+
return value.map((v) => (0, path_containment_1.resolveContainedPath)(projectRoot, String(v)));
|
|
168
|
+
};
|
|
169
|
+
const result = yield runSurprises(deps, {
|
|
170
|
+
projectRoot,
|
|
171
|
+
options: {
|
|
172
|
+
sample: (0, rows_handler_1.clampInt)(raw.sample, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.sample, 1, 10000),
|
|
173
|
+
neighbors: (0, rows_handler_1.clampInt)(raw.neighbors, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.neighbors, 1, 200),
|
|
174
|
+
dirDepth: (0, rows_handler_1.clampInt)(raw.dirDepth, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.dirDepth, 1, 20),
|
|
175
|
+
minSimilarity: typeof raw.minSimilarity === "number" &&
|
|
176
|
+
Number.isFinite(raw.minSimilarity) &&
|
|
177
|
+
raw.minSimilarity >= 0
|
|
178
|
+
? raw.minSimilarity
|
|
179
|
+
: surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.minSimilarity,
|
|
180
|
+
maxRows: (0, rows_handler_1.clampInt)(raw.maxRows, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.maxRows, 1, surprising_connections_1.MAX_SURPRISE_ROWS),
|
|
181
|
+
includeTests: Boolean(raw.includeTests),
|
|
182
|
+
includeEval: Boolean(raw.includeEval),
|
|
183
|
+
in: toPrefixList(raw.in),
|
|
184
|
+
exclude: toPrefixList(raw.exclude),
|
|
185
|
+
},
|
|
186
|
+
top: (0, rows_handler_1.clampInt)(payload.top, 20, 1, 100),
|
|
187
|
+
});
|
|
188
|
+
return Object.assign({ ok: true }, result);
|
|
189
|
+
}));
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// --- Registration -----------------------------------------------------------
|
|
193
|
+
/** Called from `Daemon.start()`; see registerRowsVerbs for why not at import. */
|
|
194
|
+
function registerVectorVerbs() {
|
|
195
|
+
(0, read_verbs_1.registerReadVerbs)({
|
|
196
|
+
"vector.similar": (payload, ctx) => handleVectorSimilar(ctx.daemon.storeReadDeps(), payload),
|
|
197
|
+
"vector.surprises": (payload, ctx) => handleVectorSurprises(ctx.daemon.storeReadDeps(), payload),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -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 {
|