grepmax 0.26.40 → 0.26.41

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.
@@ -44,6 +44,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.handleDaemonSearch = handleDaemonSearch;
46
46
  const embedding_generation_1 = require("../index/embedding-generation");
47
+ const index_state_footer_1 = require("../output/index-state-footer");
47
48
  const searcher_1 = require("../search/searcher");
48
49
  const retriever_1 = require("../skeleton/retriever");
49
50
  const project_registry_1 = require("../utils/project-registry");
@@ -55,7 +56,7 @@ const project_registry_1 = require("../utils/project-registry");
55
56
  function handleDaemonSearch(deps, payload, signal) {
56
57
  return __awaiter(this, void 0, void 0, function* () {
57
58
  var _a, _b, _c, _d;
58
- var _e, _f;
59
+ var _e, _f, _g;
59
60
  const { vectorDb, workerPool } = deps;
60
61
  if (!vectorDb || !workerPool) {
61
62
  return { ok: false, error: "daemon not ready" };
@@ -116,11 +117,20 @@ function handleDaemonSearch(deps, payload, signal) {
116
117
  if ((_c = result.warnings) === null || _c === void 0 ? void 0 : _c.length)
117
118
  response.warnings = result.warnings;
118
119
  // Annotate partial results when the index is still catching up, so an
119
- // agent can caveat or retry. Only attached when actually indexing (the
120
- // formatter suppresses the settled case anyway).
120
+ // agent can caveat or retry. Settled failures remain visible too.
121
121
  const idx = deps.getIndexState(root);
122
- if (idx.indexing)
123
- response.indexState = idx;
122
+ response.indexState = idx;
123
+ for (const candidateRoot of roots) {
124
+ if (candidateRoot === root)
125
+ continue;
126
+ const health = (0, index_state_footer_1.formatIndexStateFooter)(deps.getIndexState(candidateRoot), {
127
+ agent: true,
128
+ });
129
+ if (health) {
130
+ (_f = response.warnings) !== null && _f !== void 0 ? _f : (response.warnings = []);
131
+ response.warnings.push(`${candidateRoot}: ${health}`);
132
+ }
133
+ }
124
134
  // --skeleton support: fetch per-file skeletons inline so the CLI doesn't
125
135
  // have to open its own VectorDB. getStoredSkeleton is a single LIMIT-1
126
136
  // lookup; cheap enough to call for the top N distinct paths.
@@ -131,7 +141,7 @@ function handleDaemonSearch(deps, payload, signal) {
131
141
  const seen = new Set();
132
142
  const skeletons = {};
133
143
  for (const chunk of result.data) {
134
- const p = (_f = chunk.path) !== null && _f !== void 0 ? _f : (_d = chunk.metadata) === null || _d === void 0 ? void 0 : _d.path;
144
+ const p = (_g = chunk.path) !== null && _g !== void 0 ? _g : (_d = chunk.metadata) === null || _d === void 0 ? void 0 : _d.path;
135
145
  if (!p || seen.has(p))
136
146
  continue;
137
147
  seen.add(p);
@@ -142,7 +152,7 @@ function handleDaemonSearch(deps, payload, signal) {
142
152
  if (sk)
143
153
  skeletons[p] = sk;
144
154
  }
145
- catch (_g) {
155
+ catch (_h) {
146
156
  // best-effort — drop the entry, keep the search result
147
157
  }
148
158
  }
@@ -157,7 +167,7 @@ function handleDaemonSearch(deps, payload, signal) {
157
167
  const builder = new GraphBuilder(vectorDb, root);
158
168
  response.graph = yield builder.buildGraphMultiHop(payload.query, 1);
159
169
  }
160
- catch (_h) {
170
+ catch (_j) {
161
171
  // best-effort — drop graph, keep results
162
172
  }
163
173
  }
@@ -104,6 +104,10 @@ class WatcherManager {
104
104
  this.pollRecoveryTimers = new Map();
105
105
  this.recoveryTimeouts = new Map();
106
106
  this.lastOverflowMs = new Map();
107
+ this.lastWatcherErrorLogMs = new Map();
108
+ this.overflowCounts = new Map();
109
+ this.reconciledAt = new Map();
110
+ this.catchupDurations = new Map();
107
111
  this.lastCatchupEndMs = new Map();
108
112
  this.deferredCatchups = new Map();
109
113
  this.catchups = new Map();
@@ -111,6 +115,24 @@ class WatcherManager {
111
115
  this.terminalFailures = new Map();
112
116
  this.watchLifecycles = new Map();
113
117
  }
118
+ health(root) {
119
+ var _a;
120
+ var _b, _c;
121
+ return {
122
+ failedFiles: (_b = (_a = this.terminalFailures.get(root)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0,
123
+ degraded: this.isRootDegraded(root),
124
+ watcherMode: this.pollIntervals.has(root)
125
+ ? "polling"
126
+ : this.pendingOps.has(`recover:${root}`) ||
127
+ this.deferredCatchups.has(root)
128
+ ? "recovering"
129
+ : "native",
130
+ catchupRunning: this.catchups.has(root),
131
+ lastReconciledAt: this.reconciledAt.get(root),
132
+ overflowCount: (_c = this.overflowCounts.get(root)) !== null && _c !== void 0 ? _c : 0,
133
+ catchupMs: this.catchupDurations.get(root),
134
+ };
135
+ }
114
136
  watchProject(root_1) {
115
137
  return __awaiter(this, arguments, void 0, function* (root, options = {}) {
116
138
  var _a, _b;
@@ -268,14 +290,20 @@ class WatcherManager {
268
290
  this.deps.subscriptions.delete(root);
269
291
  }
270
292
  const sub = yield watcher.subscribe(root, (err, events) => {
271
- var _a;
293
+ var _a, _b;
272
294
  if (err) {
273
- console.error(`[daemon:${name}] Watcher error:`, err);
295
+ this.overflowCounts.set(root, ((_a = this.overflowCounts.get(root)) !== null && _a !== void 0 ? _a : 0) + 1);
296
+ const now = Date.now();
297
+ if (!this.lastWatcherErrorLogMs.has(root) ||
298
+ now - this.lastWatcherErrorLogMs.get(root) >= 60000) {
299
+ this.lastWatcherErrorLogMs.set(root, now);
300
+ console.error(`[daemon:${name}] Watcher error (total=${this.overflowCounts.get(root)}):`, err);
301
+ }
274
302
  this.recoverWatcher(root, processor);
275
303
  return;
276
304
  }
277
305
  // Only reset fail counter after sustained health (5min since last overflow)
278
- const lastOverflow = (_a = this.lastOverflowMs.get(root)) !== null && _a !== void 0 ? _a : 0;
306
+ const lastOverflow = (_b = this.lastOverflowMs.get(root)) !== null && _b !== void 0 ? _b : 0;
279
307
  if (Date.now() - lastOverflow > 5 * 60 * 1000) {
280
308
  this.watcherFailCount.delete(root);
281
309
  }
@@ -490,6 +518,7 @@ class WatcherManager {
490
518
  var _a, e_1, _b, _c;
491
519
  var _d;
492
520
  var _e;
521
+ const scanStart = Date.now();
493
522
  const { isFileCached } = yield Promise.resolve().then(() => __importStar(require("../utils/cache-check")));
494
523
  const metaCache = this.deps.getMetaCache();
495
524
  const vectorDb = this.deps.getVectorDb();
@@ -641,7 +670,11 @@ class WatcherManager {
641
670
  parts.push(`${purged} deleted`);
642
671
  console.log(`[daemon:${path.basename(root)}] Catchup: ${parts.join(", ")} file(s) while offline`);
643
672
  }
644
- this.lastCatchupEndMs.set(root, Date.now());
673
+ const ended = Date.now();
674
+ this.lastCatchupEndMs.set(root, ended);
675
+ this.catchupDurations.set(root, ended - scanStart);
676
+ if (complete)
677
+ this.reconciledAt.set(root, ended);
645
678
  return complete;
646
679
  });
647
680
  }
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.GraphBuilder = void 0;
13
13
  const languages_1 = require("../core/languages");
14
+ const arrow_1 = require("../utils/arrow");
14
15
  const filter_builder_1 = require("../utils/filter-builder");
15
16
  const query_timeout_1 = require("../utils/query-timeout");
16
17
  const callsites_1 = require("./callsites");
@@ -118,19 +119,43 @@ class GraphBuilder {
118
119
  */
119
120
  getCallees(symbol) {
120
121
  return __awaiter(this, void 0, void 0, function* () {
122
+ var _a;
121
123
  const table = yield this.db.ensureTable();
122
124
  const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
123
125
  // Find the definition of the symbol
124
126
  const rows = yield table
125
127
  .query()
126
- .select(["referenced_symbols"])
128
+ .select([
129
+ "path",
130
+ "referenced_symbols",
131
+ "member_referenced_symbols",
132
+ "content",
133
+ ])
127
134
  .where(this.scopeWhere(`array_contains(defined_symbols, '${escaped}')`))
128
135
  .limit(1)
129
136
  .toArray();
130
137
  if (rows.length === 0)
131
138
  return [];
132
139
  const record = rows[0];
133
- return record.referenced_symbols || [];
140
+ const memberNames = (0, arrow_1.toArr)(record.member_referenced_symbols);
141
+ const content = String((_a = record.content) !== null && _a !== void 0 ? _a : "");
142
+ let localDefinitions = [];
143
+ if (record.path && memberNames.some(callsites_1.isBuiltinCallee)) {
144
+ const localRows = yield table
145
+ .query()
146
+ .select(["defined_symbols"])
147
+ .where(this.scopeWhere(`path = '${(0, filter_builder_1.escapeSqlString)(String(record.path))}'`))
148
+ .toArray();
149
+ localDefinitions = localRows.flatMap((row) => (0, arrow_1.toArr)(row.defined_symbols));
150
+ }
151
+ return (0, arrow_1.toArr)(record.referenced_symbols).filter((name) => {
152
+ if (!(0, callsites_1.isBuiltinCallee)(name) ||
153
+ !memberNames.includes(name) ||
154
+ localDefinitions.includes(name))
155
+ return true;
156
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
157
+ return new RegExp(`(?<![\\w.$])${escapedName}\\s*\\(`).test(content);
158
+ });
134
159
  });
135
160
  }
136
161
  /**
@@ -138,7 +163,8 @@ class GraphBuilder {
138
163
  */
139
164
  buildGraph(symbol) {
140
165
  return __awaiter(this, void 0, void 0, function* () {
141
- var _a;
166
+ var _a, _b;
167
+ var _c, _d;
142
168
  const table = yield this.db.ensureTable();
143
169
  const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
144
170
  // 1. Get Center (Definition)
@@ -149,6 +175,8 @@ class GraphBuilder {
149
175
  "start_line",
150
176
  "defined_symbols",
151
177
  "referenced_symbols",
178
+ "member_referenced_symbols",
179
+ "content",
152
180
  "role",
153
181
  "parent_symbol",
154
182
  "complexity",
@@ -187,6 +215,26 @@ class GraphBuilder {
187
215
  ])
188
216
  .limit(25)
189
217
  .toArray();
218
+ const memberNames = (0, arrow_1.toArr)((_a = centerRows[0]) === null || _a === void 0 ? void 0 : _a.member_referenced_symbols);
219
+ const content = String((_c = (_b = centerRows[0]) === null || _b === void 0 ? void 0 : _b.content) !== null && _c !== void 0 ? _c : "");
220
+ const bareCall = new RegExp(`(?<![\\w.$])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`).test(content);
221
+ const selfDefinition = rows.find((r) => { var _a; return String((_a = r.path) !== null && _a !== void 0 ? _a : "") === centerFile; });
222
+ if ((0, callsites_1.isBuiltinCallee)(name) &&
223
+ memberNames.includes(name) &&
224
+ !bareCall &&
225
+ !selfDefinition) {
226
+ calleeNodes.push({
227
+ symbol: name,
228
+ file: "",
229
+ line: 0,
230
+ role: "",
231
+ calls: [],
232
+ calledBy: [],
233
+ resolution: "ambiguous-member",
234
+ confidence: "INFERRED",
235
+ });
236
+ continue;
237
+ }
190
238
  if (rows.length > 0) {
191
239
  // Prefer-self-file, then same-language-family. Only fall back to the
192
240
  // first row when no candidate shares the center's file or family.
@@ -195,7 +243,7 @@ class GraphBuilder {
195
243
  ? rows.find((r) => { var _a; return (0, languages_1.languageFamilyForPath)(String((_a = r.path) !== null && _a !== void 0 ? _a : "")) ===
196
244
  centerFamily; })
197
245
  : undefined;
198
- calleeNodes.push(this.mapRowToNode(((_a = selfRow !== null && selfRow !== void 0 ? selfRow : familyRow) !== null && _a !== void 0 ? _a : rows[0]), name, "center"));
246
+ calleeNodes.push(this.mapRowToNode(((_d = selfRow !== null && selfRow !== void 0 ? selfRow : familyRow) !== null && _d !== void 0 ? _d : rows[0]), name, "center"));
199
247
  }
200
248
  else if (!(0, callsites_1.isBuiltinCallee)(name)) {
201
249
  // Unresolved + a known builtin (.map/.get/forEach) → suppress the
@@ -151,7 +151,7 @@ function formatImpactRollupHuman(rollup, opts) {
151
151
  }
152
152
  else {
153
153
  for (const test of rollup.topTests) {
154
- lines.push(` ${relativeToProject(opts.projectRoot, test.file)}:${test.line + 1} (${(0, test_hits_1.hopLabelHuman)(test.hops)}${(0, test_hits_1.formatViaHuman)(test.via)})`);
154
+ lines.push(` ${relativeToProject(opts.projectRoot, test.file)}:${test.line + 1} (${(0, test_hits_1.hopLabelHuman)(test.hops, test.evidence)}${(0, test_hits_1.formatViaHuman)(test.via)})`);
155
155
  }
156
156
  }
157
157
  }
@@ -191,7 +191,7 @@ function formatImpactRollupAgent(rollup, opts) {
191
191
  lines.push([
192
192
  "test",
193
193
  `${relativeToProject(opts.projectRoot, test.file)}:${test.line + 1}`,
194
- (0, test_hits_1.hopLabelAgent)(test.hops),
194
+ (0, test_hits_1.hopLabelAgent)(test.hops, test.evidence),
195
195
  (0, test_hits_1.formatViaAgent)(test.via).trim(),
196
196
  ]
197
197
  .filter(Boolean)
@@ -157,10 +157,20 @@ function findTests(symbols_1, vectorDb_1, projectRoot_1) {
157
157
  const expanded = yield expandFileSymbols(symbols, vectorDb, projectRoot, excludePrefixes);
158
158
  const families = yield resolveSymbolFamilies(expanded, vectorDb, projectRoot, excludePrefixes, symbolFamilies);
159
159
  for (const symbol of expanded) {
160
- yield walkCallers(symbol, graphBuilder, testHits, 0, depth, new Set(), (_a = families.get(symbol)) !== null && _a !== void 0 ? _a : null);
160
+ const candidates = new Map();
161
+ yield walkCallers(symbol, graphBuilder, candidates, 0, depth, new Set(), (_a = families.get(symbol)) !== null && _a !== void 0 ? _a : null);
162
+ for (const [key, hit] of candidates) {
163
+ if (!symbols.includes(symbol))
164
+ hit.evidence = "file-candidate";
165
+ const previous = testHits.get(key);
166
+ if (!previous ||
167
+ (previous.evidence && !hit.evidence) ||
168
+ (previous.evidence === hit.evidence && hit.hops < previous.hops))
169
+ testHits.set(key, hit);
170
+ }
161
171
  }
162
- if (testHits.size === 0) {
163
- const importFiles = yield findImportFallbackTests(expanded, symbols, vectorDb, projectRoot, excludePrefixes, families);
172
+ if (![...testHits.values()].some((hit) => !hit.evidence)) {
173
+ const importFiles = yield findImportFallbackTests(symbols, vectorDb, projectRoot, excludePrefixes, families);
164
174
  for (const file of importFiles) {
165
175
  testHits.set(`${file}:(referenced)`, {
166
176
  file,
@@ -170,17 +180,19 @@ function findTests(symbols_1, vectorDb_1, projectRoot_1) {
170
180
  });
171
181
  }
172
182
  }
173
- return [...testHits.values()].sort((a, b) => a.hops - b.hops || a.file.localeCompare(b.file));
183
+ return [...testHits.values()].sort((a, b) => Number(!!a.evidence) - Number(!!b.evidence) ||
184
+ a.hops - b.hops ||
185
+ a.file.localeCompare(b.file));
174
186
  });
175
187
  }
176
- function findImportFallbackTests(expandedSymbols, originalSymbols, vectorDb, projectRoot, excludePrefixes, symbolFamilies) {
188
+ function findImportFallbackTests(originalSymbols, vectorDb, projectRoot, excludePrefixes, symbolFamilies) {
177
189
  return __awaiter(this, void 0, void 0, function* () {
178
190
  var _a;
179
191
  const files = new Set();
180
192
  // Signal 1: referenced_symbols match (precise; works when the chunker
181
- // captured call references in test bodies). Uses the expanded set so tests
182
- // that call a method of the target class still match.
183
- const dependents = yield findDependents(expandedSymbols, vectorDb, projectRoot, undefined, 50, excludePrefixes, symbolFamilies);
193
+ // captured call references in test bodies). Co-defined symbols are already
194
+ // exposed as file candidates and must not become target coverage here.
195
+ const dependents = yield findDependents(originalSymbols, vectorDb, projectRoot, undefined, 50, excludePrefixes, symbolFamilies);
184
196
  for (const d of dependents) {
185
197
  if (isTestPath(d.file))
186
198
  files.add(d.file);
@@ -9,20 +9,22 @@ function groupTestHitsByFile(hits) {
9
9
  const byFile = new Map();
10
10
  // Sort by hops so the first hit per file carries the best line/hops and
11
11
  // `via` lists closest callers first ((referenced) fallback hits sort last).
12
- const ordered = [...hits].sort((a, b) => (a.hops === -1 ? Number.MAX_SAFE_INTEGER : a.hops) -
13
- (b.hops === -1 ? Number.MAX_SAFE_INTEGER : b.hops));
12
+ const ordered = [...hits].sort((a, b) => Number(!!a.evidence) - Number(!!b.evidence) ||
13
+ (a.hops === -1 ? Number.MAX_SAFE_INTEGER : a.hops) -
14
+ (b.hops === -1 ? Number.MAX_SAFE_INTEGER : b.hops));
14
15
  for (const h of ordered) {
15
16
  let g = byFile.get(h.file);
16
17
  if (!g) {
17
- g = { file: h.file, line: h.line, hops: h.hops, via: [] };
18
+ g = Object.assign({ file: h.file, line: h.line, hops: h.hops, via: [] }, (h.evidence ? { evidence: h.evidence } : {}));
18
19
  byFile.set(h.file, g);
19
20
  }
20
21
  if (h.symbol && h.symbol !== "(referenced)" && !g.via.includes(h.symbol)) {
21
22
  g.via.push(h.symbol);
22
23
  }
23
24
  }
24
- return [...byFile.values()].sort((a, b) => (a.hops === -1 ? Number.MAX_SAFE_INTEGER : a.hops) -
25
- (b.hops === -1 ? Number.MAX_SAFE_INTEGER : b.hops) ||
25
+ return [...byFile.values()].sort((a, b) => Number(!!a.evidence) - Number(!!b.evidence) ||
26
+ (a.hops === -1 ? Number.MAX_SAFE_INTEGER : a.hops) -
27
+ (b.hops === -1 ? Number.MAX_SAFE_INTEGER : b.hops) ||
26
28
  a.file.localeCompare(b.file));
27
29
  }
28
30
  const MAX_VIA = 3;
@@ -42,10 +44,14 @@ function formatViaHuman(via) {
42
44
  const more = via.length > MAX_VIA ? ` (+${via.length - MAX_VIA} more)` : "";
43
45
  return `, via ${shown}${more}`;
44
46
  }
45
- function hopLabelAgent(hops) {
47
+ function hopLabelAgent(hops, evidence) {
48
+ if (evidence)
49
+ return "file-candidate";
46
50
  return hops === -1 ? "via-import" : hops === 0 ? "direct" : `${hops}-hop`;
47
51
  }
48
- function hopLabelHuman(hops) {
52
+ function hopLabelHuman(hops, evidence) {
53
+ if (evidence)
54
+ return "same-file candidate";
49
55
  return hops === -1
50
56
  ? "via import"
51
57
  : hops === 0
@@ -50,6 +50,7 @@ const vector_db_1 = require("../store/vector-db");
50
50
  const cache_check_1 = require("../utils/cache-check");
51
51
  const file_utils_1 = require("../utils/file-utils");
52
52
  const logger_1 = require("../utils/logger");
53
+ const embedding_error_1 = require("../workers/embedding-error");
53
54
  const pool_1 = require("../workers/pool");
54
55
  const cache_coherence_1 = require("./cache-coherence");
55
56
  const file_policy_1 = require("./file-policy");
@@ -81,6 +82,8 @@ class ProjectBatchProcessor {
81
82
  this.closed = false;
82
83
  this.currentBatchAc = null;
83
84
  this.lastCorruptionLogMs = 0;
85
+ this.backendFailures = 0;
86
+ this.backendRetryAt = 0;
84
87
  this.policyChangedDuringBatch = false;
85
88
  /** Rolling window of recent batch outcomes — see `progress`. */
86
89
  this.recentBatches = [];
@@ -201,8 +204,11 @@ class ProjectBatchProcessor {
201
204
  });
202
205
  }
203
206
  scheduleBatch(delayMs = DEBOUNCE_MS) {
207
+ delayMs = Math.max(delayMs, this.backendRetryAt - Date.now());
204
208
  const dueMs = Date.now() + Math.max(0, delayMs);
205
- if (this.debounceTimer && this.debounceDueMs <= dueMs)
209
+ if (this.debounceTimer &&
210
+ this.debounceDueMs <= dueMs &&
211
+ this.debounceDueMs >= this.backendRetryAt)
206
212
  return;
207
213
  if (this.debounceTimer)
208
214
  clearTimeout(this.debounceTimer);
@@ -234,7 +240,10 @@ class ProjectBatchProcessor {
234
240
  const run = (this.runOperation
235
241
  ? this.runOperation(execute)
236
242
  : execute(new AbortController().signal)).catch((err) => {
237
- console.error(`[${this.wtag}] Batch processing failed:`, err);
243
+ if ((err === null || err === void 0 ? void 0 : err.name) === "AbortError")
244
+ (0, logger_1.log)(this.wtag, "Batch cancelled during shutdown");
245
+ else
246
+ console.error(`[${this.wtag}] Batch processing failed:`, err);
238
247
  });
239
248
  this.activeBatch = run;
240
249
  void run.finally(() => {
@@ -303,6 +312,7 @@ class ProjectBatchProcessor {
303
312
  const metaDeletes = [];
304
313
  const completed = new Set();
305
314
  const retryFailures = new Set();
315
+ let backendUnavailable = false;
306
316
  const requeuePath = (absPath, event, failed) => {
307
317
  var _a;
308
318
  var _b;
@@ -434,6 +444,11 @@ class ProjectBatchProcessor {
434
444
  if (batchAc.signal.aborted)
435
445
  return;
436
446
  const code = err === null || err === void 0 ? void 0 : err.code;
447
+ if ((0, embedding_error_1.isEmbeddingBackendUnavailable)(err)) {
448
+ backendUnavailable = true;
449
+ stopDispatch = true;
450
+ return;
451
+ }
437
452
  if (code === "ENOENT") {
438
453
  deletes.push(absPath);
439
454
  metaDeletes.push(absPath);
@@ -479,6 +494,16 @@ class ProjectBatchProcessor {
479
494
  yield schedule(() => processOne(absPath, event));
480
495
  }
481
496
  yield Promise.allSettled(activeTasks);
497
+ if (backendUnavailable) {
498
+ this.backendFailures++;
499
+ const backoffMs = Math.min(60000, 5000 * Math.pow(2, Math.min(this.backendFailures - 1, 4)));
500
+ this.backendRetryAt = Date.now() + backoffMs;
501
+ (0, logger_1.log)(this.wtag, `Embedding backend unavailable — preserving file retry budgets; retry in ${backoffMs / 1000}s`);
502
+ }
503
+ else {
504
+ this.backendFailures = 0;
505
+ this.backendRetryAt = 0;
506
+ }
482
507
  const pureDeleteCandidates = new Set(metaDeletes);
483
508
  const authoritativeMetaDeletes = new Set();
484
509
  for (const absPath of pureDeleteCandidates) {
@@ -56,15 +56,24 @@ for (const lang of languages_1.LANGUAGES) {
56
56
  }
57
57
  }
58
58
  const downloadFile = (url, dest) => __awaiter(void 0, void 0, void 0, function* () {
59
- const response = yield fetch(url, {
60
- headers: {
61
- "User-Agent": "gmax",
62
- },
63
- });
64
- if (!response.ok)
65
- throw new Error(`Failed to download ${url}`);
66
- const arrayBuffer = yield response.arrayBuffer();
67
- fs.writeFileSync(dest, Buffer.from(arrayBuffer));
59
+ for (let attempt = 1; attempt <= 3; attempt++) {
60
+ try {
61
+ const response = yield fetch(url, {
62
+ headers: { "User-Agent": "gmax" },
63
+ signal: AbortSignal.timeout(30000),
64
+ });
65
+ if (!response.ok)
66
+ throw new Error(`Failed to download ${url}: HTTP ${response.status}`);
67
+ const arrayBuffer = yield response.arrayBuffer();
68
+ fs.writeFileSync(dest, Buffer.from(arrayBuffer));
69
+ return;
70
+ }
71
+ catch (error) {
72
+ if (attempt === 3)
73
+ throw error;
74
+ yield new Promise((resolve) => setTimeout(resolve, attempt * 500));
75
+ }
76
+ }
68
77
  });
69
78
  function ensureGrammars() {
70
79
  return __awaiter(this, arguments, void 0, function* (log = console.log, options) {
@@ -73,6 +82,7 @@ function ensureGrammars() {
73
82
  fs.mkdirSync(exports.GRAMMARS_DIR, { recursive: true });
74
83
  }
75
84
  const silent = (_a = options === null || options === void 0 ? void 0 : options.silent) !== null && _a !== void 0 ? _a : false;
85
+ const failed = [];
76
86
  for (const [lang, url] of Object.entries(GRAMMAR_URLS)) {
77
87
  const dest = path.join(exports.GRAMMARS_DIR, `tree-sitter-${lang}.wasm`);
78
88
  if (fs.existsSync(dest)) {
@@ -99,6 +109,7 @@ function ensureGrammars() {
99
109
  }
100
110
  }
101
111
  catch (err) {
112
+ failed.push(lang);
102
113
  if (log === console.log) {
103
114
  console.log("Failed");
104
115
  }
@@ -106,5 +117,7 @@ function ensureGrammars() {
106
117
  }
107
118
  }
108
119
  }
120
+ if ((options === null || options === void 0 ? void 0 : options.strict) && failed.length)
121
+ throw new Error(`Required grammars could not be downloaded: ${failed.join(", ")}`);
109
122
  });
110
123
  }
@@ -191,7 +191,7 @@ function formatTrace(graph, options) {
191
191
  lines.push(` ${style.cyan("↓")} ${style.green(callee.symbol)} ${style.dim(`(${callee.file}:${callee.line})`)}`);
192
192
  }
193
193
  else {
194
- lines.push(` ${style.cyan("↓")} ${callee.symbol} ${style.dim("(not indexed)")}`);
194
+ lines.push(` ${style.cyan("↓")} ${callee.symbol} ${style.dim(callee.resolution === "ambiguous-member" ? "(receiver unresolved)" : "(not indexed)")}`);
195
195
  }
196
196
  });
197
197
  }
@@ -14,17 +14,37 @@ exports.formatIndexStateFooter = formatIndexStateFooter;
14
14
  * while results may actually be incomplete.
15
15
  */
16
16
  function formatIndexStateFooter(state, opts) {
17
- if (!(state === null || state === void 0 ? void 0 : state.indexing))
17
+ if (!state)
18
18
  return null;
19
- // Re-verification is not incompleteness. Saying "results may be incomplete"
20
- // while a catchup re-checks thousands of unchanged files trains agents to
21
- // distrust and retry correct answers, which is worse than saying nothing.
19
+ const issues = [];
20
+ if (state.failedFiles)
21
+ issues.push(`${state.failedFiles} files failed`);
22
+ if (state.degraded && !state.failedFiles)
23
+ issues.push("reconciliation incomplete");
24
+ if (state.watcherMode === "polling")
25
+ issues.push("polling; changes may lag up to 5min");
26
+ if (state.watcherMode === "recovering")
27
+ issues.push("watcher recovering");
28
+ if (state.catchupRunning)
29
+ issues.push("reconciling filesystem changes");
30
+ if (issues.length) {
31
+ const next = state.failedFiles || state.degraded
32
+ ? "inspect gmax watch status; retry after recovery"
33
+ : "retry after reconciliation";
34
+ return opts.agent
35
+ ? `[index: ${issues.join(" · ")} · results may be incomplete · ${next}]`
36
+ : `Index: ${issues.join("; ")} — results may be incomplete; ${next}.`;
37
+ }
38
+ if (!state.indexing)
39
+ return null;
40
+ // Cache-hit samples suggest verification, but do not prove the remaining
41
+ // files are unchanged. Keep that distinction without claiming full coverage.
22
42
  if (state.verifying) {
23
43
  const n = state.pendingFiles;
24
44
  if (opts.agent) {
25
- return `[index: verifying ~${n} unchanged files · results current]`;
45
+ return `[index: verifying ~${n} files · coverage not yet verified]`;
26
46
  }
27
- return `Index verifying ${n} unchanged file${n === 1 ? "" : "s"} — results are current.`;
47
+ return `Index verifying ${n} file${n === 1 ? "" : "s"} — coverage is not yet verified.`;
28
48
  }
29
49
  const count = state.pendingFiles > 0 ? `~${state.pendingFiles} files pending` : null;
30
50
  if (opts.agent) {
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildResourceSnapshot = buildResourceSnapshot;
4
+ function buildResourceSnapshot(counters, memory = process.memoryUsage(), at = Date.now()) {
5
+ const mb = (bytes) => Math.round(bytes / 1024 / 1024);
6
+ return {
7
+ at,
8
+ reason: counters.reason,
9
+ footprintMb: counters.footprintMb,
10
+ rssMb: mb(memory.rss),
11
+ heapUsedMb: mb(memory.heapUsed),
12
+ externalMb: mb(memory.external),
13
+ arrayBuffersMb: mb(memory.arrayBuffers),
14
+ lanceCacheMb: counters.lanceCacheBytes === null ? null : mb(counters.lanceCacheBytes),
15
+ workers: counters.workers,
16
+ pendingFiles: counters.pendingFiles,
17
+ operations: counters.operations,
18
+ maintenance: counters.maintenance,
19
+ };
20
+ }
@@ -15,6 +15,7 @@ exports.fetchTestsForFooter = fetchTestsForFooter;
15
15
  exports.renderTestsFooterAgent = renderTestsFooterAgent;
16
16
  exports.renderTestsFooterHuman = renderTestsFooterHuman;
17
17
  const impact_1 = require("../graph/impact");
18
+ const test_hits_1 = require("../graph/test-hits");
18
19
  /**
19
20
  * The footer is a courtesy line under a body the user asked for; it must never
20
21
  * make the command feel slow. Past this budget the lookup is abandoned and the
@@ -57,24 +58,10 @@ function fetchTestsForFooter(symbol, vectorDb, pathPrefix, excludePrefixes) {
57
58
  function relPath(p, projectRoot) {
58
59
  return p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
59
60
  }
60
- function hopLabelAgent(hops) {
61
- if (hops === -1)
62
- return "via-import";
63
- if (hops === 0)
64
- return "direct";
65
- return `${hops}-hop`;
66
- }
67
- function hopLabelHuman(hops) {
68
- if (hops === -1)
69
- return "via import";
70
- if (hops === 0)
71
- return "direct";
72
- return `${hops} hop${hops > 1 ? "s" : ""}`;
73
- }
74
61
  function renderTestsFooterAgent(tests, projectRoot) {
75
62
  const lines = [];
76
63
  for (const t of tests.slice(0, MAX_SHOWN)) {
77
- lines.push(`t: ${relPath(t.file, projectRoot)}:${t.line + 1}\t${t.symbol}\t${hopLabelAgent(t.hops)}`);
64
+ lines.push(`t: ${relPath(t.file, projectRoot)}:${t.line + 1}\t${t.symbol}\t${(0, test_hits_1.hopLabelAgent)(t.hops, t.evidence)}`);
78
65
  }
79
66
  if (tests.length > MAX_SHOWN) {
80
67
  lines.push(`t: ... ${tests.length - MAX_SHOWN} more`);
@@ -86,7 +73,12 @@ function renderTestsFooterHuman(tests, projectRoot) {
86
73
  lines.push("");
87
74
  lines.push(`tests (${tests.length}):`);
88
75
  for (const t of tests.slice(0, MAX_SHOWN)) {
89
- lines.push(` ${t.symbol.padEnd(25)} ${dim(`${relPath(t.file, projectRoot)}:${t.line + 1}`)} ${dim(`(${hopLabelHuman(t.hops)})`)}`);
76
+ const label = t.evidence
77
+ ? "same-file candidate"
78
+ : t.hops === -1
79
+ ? "via import"
80
+ : (0, test_hits_1.hopLabelAgent)(t.hops);
81
+ lines.push(` ${t.symbol.padEnd(25)} ${dim(`${relPath(t.file, projectRoot)}:${t.line + 1}`)} ${dim(`(${label})`)}`);
90
82
  }
91
83
  if (tests.length > MAX_SHOWN) {
92
84
  lines.push(dim(` ... and ${tests.length - MAX_SHOWN} more`));
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EmbeddingBackendUnavailableError = void 0;
4
+ exports.isEmbeddingBackendUnavailable = isEmbeddingBackendUnavailable;
5
+ /** Shared backend outages are retryable; malformed inputs/protocols are not. */
6
+ class EmbeddingBackendUnavailableError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.code = "EMBED_BACKEND_UNAVAILABLE";
10
+ this.name = "EmbeddingBackendUnavailableError";
11
+ }
12
+ }
13
+ exports.EmbeddingBackendUnavailableError = EmbeddingBackendUnavailableError;
14
+ function isEmbeddingBackendUnavailable(error) {
15
+ return ((error === null || error === void 0 ? void 0 : error.code) === "EMBED_BACKEND_UNAVAILABLE");
16
+ }