(result: ToolResult, projectPath?: string)
| 1330 | } |
| 1331 | |
| 1332 | private withStalenessNotice(result: ToolResult, projectPath?: string): ToolResult { |
| 1333 | if (result.isError) return result; |
| 1334 | |
| 1335 | let cg: CodeGraph; |
| 1336 | try { |
| 1337 | cg = this.getCodeGraph(projectPath); |
| 1338 | } catch { |
| 1339 | return result; // no default project — leave as is |
| 1340 | } |
| 1341 | |
| 1342 | // Cross-project `projectPath` calls open a cached CodeGraph WITHOUT a |
| 1343 | // watcher (watchers are only attached to the default session project). |
| 1344 | // When the cross-project path happens to be the same project as the |
| 1345 | // default cg, the cached instance is the wrong one — its pendingFiles is |
| 1346 | // permanently empty. Detect the equal-path case and prefer the default |
| 1347 | // cg so the staleness signal still fires when an agent passes the |
| 1348 | // explicit projectPath form of its own project. |
| 1349 | if (this.cg && cg !== this.cg) { |
| 1350 | try { |
| 1351 | const sameProject = |
| 1352 | resolvePath(this.cg.getProjectRoot()) === resolvePath(cg.getProjectRoot()); |
| 1353 | if (sameProject) cg = this.cg; |
| 1354 | } catch { |
| 1355 | /* getProjectRoot may throw on a closed instance — leave cg as is */ |
| 1356 | } |
| 1357 | } |
| 1358 | |
| 1359 | // Whole-index degradation (#876): once live watching has permanently |
| 1360 | // stopped, getPendingFiles() is empty so the per-file banner below can't |
| 1361 | // fire — but the index is now FROZEN and silently drifting stale. Surface |
| 1362 | // one global notice instead, so the agent Reads for current content rather |
| 1363 | // than trusting a response off a no-longer-updating index. (Cross-project |
| 1364 | // calls open a watcher-less CodeGraph, so this is false there — correct: we |
| 1365 | // only know degraded state for the default session project.) |
| 1366 | let degraded = false; |
| 1367 | try { |
| 1368 | degraded = cg.isWatcherDegraded?.() ?? false; |
| 1369 | } catch { |
| 1370 | degraded = false; |
| 1371 | } |
| 1372 | if (degraded) { |
| 1373 | const [head, ...tail] = result.content; |
| 1374 | if (!head || head.type !== 'text') return result; |
| 1375 | let reason: string | null = null; |
| 1376 | try { |
| 1377 | reason = cg.getWatcherDegradedReason?.() ?? null; |
| 1378 | } catch { |
| 1379 | reason = null; |
| 1380 | } |
| 1381 | const composed = `${formatDegradedBanner(reason)}\n\n${head.text}`; |
| 1382 | return { ...result, content: [{ type: 'text', text: composed }, ...tail] }; |
| 1383 | } |
| 1384 | |
| 1385 | // Defensive: some test fakes inject a partial CodeGraph stub without the |
| 1386 | // newer pending-files API. Treat missing/throwing as "no pending files." |
| 1387 | let pending: PendingFile[] = []; |
| 1388 | try { |
| 1389 | pending = cg.getPendingFiles?.() ?? []; |
no test coverage detected