* On-disk drift check for a single indexed file (issue #1474). The code * renderers slice CURRENT bytes at INDEXED line ranges; when the file * changed after its last index sync those ranges can point at a DIFFERENT * symbol's code — served under the requested name with `isError: false`.
(cg: CodeGraph, relPath: string, content?: string)
| 1295 | * would needlessly push the agent back to Read. |
| 1296 | */ |
| 1297 | private isFileStaleOnDisk(cg: CodeGraph, relPath: string, content?: string): boolean { |
| 1298 | let root: string; |
| 1299 | try { |
| 1300 | root = cg.getProjectRoot(); |
| 1301 | } catch { |
| 1302 | return false; |
| 1303 | } |
| 1304 | const key = `${root}\0${relPath}`; |
| 1305 | const now = Date.now(); |
| 1306 | const hit = this.driftCache.get(key); |
| 1307 | if (hit && now - hit.at < ToolHandler.DRIFT_TTL_MS) return hit.stale; |
| 1308 | let stale = false; |
| 1309 | try { |
| 1310 | const rec = cg.getFile(relPath); |
| 1311 | const absPath = rec ? validatePathWithinRoot(root, relPath) : null; |
| 1312 | if (rec && absPath && existsSync(absPath)) { |
| 1313 | const st = statSync(absPath); |
| 1314 | // Same freshness test as the sync fast path (extraction/index.ts): |
| 1315 | // equal size + equal floored mtime ⇒ unchanged, no read needed. |
| 1316 | if (st.size !== rec.size || Math.floor(st.mtimeMs) !== Math.floor(rec.modifiedAt)) { |
| 1317 | const data = content ?? readFileSync(absPath, 'utf-8'); |
| 1318 | // Must stay byte-identical to extraction's `hashContent` (sha256 over |
| 1319 | // the utf-8 string) — the identical-rewrite test in |
| 1320 | // mcp-stale-slice.test.ts pins the parity. Inlined (not imported) |
| 1321 | // to keep the extraction module off the MCP startup path. |
| 1322 | stale = createHash('sha256').update(data).digest('hex') !== rec.contentHash; |
| 1323 | } |
| 1324 | } |
| 1325 | } catch { |
| 1326 | stale = false; |
| 1327 | } |
| 1328 | this.driftCache.set(key, { at: now, stale }); |
| 1329 | return stale; |
| 1330 | } |
| 1331 | |
| 1332 | private withStalenessNotice(result: ToolResult, projectPath?: string): ToolResult { |
| 1333 | if (result.isError) return result; |
no test coverage detected