* 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)
| 1817 | * would needlessly push the agent back to Read. |
| 1818 | */ |
| 1819 | private isFileStaleOnDisk(cg: CodeGraph, relPath: string, content?: string): boolean { |
| 1820 | let root: string; |
| 1821 | try { |
| 1822 | root = cg.getProjectRoot(); |
| 1823 | } catch { |
| 1824 | return false; |
| 1825 | } |
| 1826 | const key = `${root}\0${relPath}`; |
| 1827 | const now = Date.now(); |
| 1828 | const hit = this.driftCache.get(key); |
| 1829 | if (hit && now - hit.at < ToolHandler.DRIFT_TTL_MS) return hit.stale; |
| 1830 | let stale = false; |
| 1831 | try { |
| 1832 | const rec = cg.getFile(relPath); |
| 1833 | const absPath = rec ? validatePathWithinRoot(root, relPath) : null; |
| 1834 | if (rec && absPath && existsSync(absPath)) { |
| 1835 | const st = statSync(absPath); |
| 1836 | // Same freshness test as the sync fast path (extraction/index.ts): |
| 1837 | // equal size + equal floored mtime ⇒ unchanged, no read needed. |
| 1838 | if (st.size !== rec.size || Math.floor(st.mtimeMs) !== Math.floor(rec.modifiedAt)) { |
| 1839 | const data = content ?? readFileSync(absPath, 'utf-8'); |
| 1840 | // Must stay byte-identical to extraction's `hashContent` (sha256 over |
| 1841 | // the utf-8 string) — the identical-rewrite test in |
| 1842 | // mcp-stale-slice.test.ts pins the parity. Inlined (not imported) |
| 1843 | // to keep the extraction module off the MCP startup path. |
| 1844 | stale = createHash('sha256').update(data).digest('hex') !== rec.contentHash; |
| 1845 | } |
| 1846 | } |
| 1847 | } catch { |
| 1848 | stale = false; |
| 1849 | } |
| 1850 | this.driftCache.set(key, { at: now, stale }); |
| 1851 | return stale; |
| 1852 | } |
| 1853 | |
| 1854 | private withStalenessNotice(result: ToolResult, projectPath?: string): ToolResult { |
| 1855 | if (result.isError) return result; |
no test coverage detected