fix(mcp): land #1624 opt-in explore dedup (#1788)

Land the #1624 approach by @danusha2345: re-serve source by default so
subagents and compacted contexts never inherit stale already-sent pointers.
Keep cross-call dedup available through explicit truthy
CODEGRAPH_EXPLORE_DEDUP values for durable contexts.

Preserve current Unreleased entries, credit the contribution, and align
the MCP server guidance with the safe default.

Validation on Linux / Node 22.23.2:
- Reproduced default-on failure before the fix; default-off now passes.
- Focused explore-cross-call-dedup suite: 26 passed.
- TypeScript: npx tsc -p tsconfig.json --noEmit passed.

Fixes #1620.

(cherry picked from commit 63992facabbbcef2797167dcdd90695d3802b533)

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
This commit is contained in:
Colby Mchenry
2026-09-08 13:13:03 -05:00
committed by GitHub
co-authored by danusha2345
parent 7be699cd91
commit b8d46d13c7
4 changed files with 55 additions and 6 deletions
@@ -28,6 +28,7 @@ import { ExploreSessionState, type ExploreProjectState } from '../src/mcp/explor
import {
EXPLORE_DEDUP,
dedupeRange,
exploreDedupEnabled,
fileFingerprint,
formatBackReference,
intersectRange,
@@ -41,6 +42,27 @@ const FIXTURE_SRC = path.join(__dirname, 'fixtures', 'payroll-go');
const QUERY = 'how does payroll cycle create and calculate payslips?';
const POINTER = 'Already sent earlier in this conversation';
describe('dedup configuration', () => {
it('defaults off and requires an explicit truthy opt-in', () => {
const previous = process.env.CODEGRAPH_EXPLORE_DEDUP;
try {
delete process.env.CODEGRAPH_EXPLORE_DEDUP;
expect(exploreDedupEnabled()).toBe(false);
for (const enabled of ['1', 'true', 'on', 'yes', ' YES ']) {
process.env.CODEGRAPH_EXPLORE_DEDUP = enabled;
expect(exploreDedupEnabled()).toBe(true);
}
for (const disabled of ['0', 'false', 'off', 'no', 'unexpected']) {
process.env.CODEGRAPH_EXPLORE_DEDUP = disabled;
expect(exploreDedupEnabled()).toBe(false);
}
} finally {
if (previous === undefined) delete process.env.CODEGRAPH_EXPLORE_DEDUP;
else process.env.CODEGRAPH_EXPLORE_DEDUP = previous;
}
});
});
/** A prior-state shaped like the session tracker's, for the algebra tests. */
function prior(files: Array<{ path: string; ranges: Array<[number, number]>; fingerprint?: string }>): ExploreProjectState {
return {
@@ -183,8 +205,11 @@ describe('a second call against a real index', () => {
let testDir: string;
let cg: CodeGraph;
let handler: ToolHandler;
let previousDedup: string | undefined;
beforeAll(async () => {
previousDedup = process.env.CODEGRAPH_EXPLORE_DEDUP;
process.env.CODEGRAPH_EXPLORE_DEDUP = '1';
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cg18-'));
fs.cpSync(FIXTURE_SRC, testDir, { recursive: true });
fs.rmSync(path.join(testDir, '.codegraph'), { recursive: true, force: true });
@@ -194,6 +219,8 @@ describe('a second call against a real index', () => {
}, 120_000);
afterAll(() => {
if (previousDedup === undefined) delete process.env.CODEGRAPH_EXPLORE_DEDUP;
else process.env.CODEGRAPH_EXPLORE_DEDUP = previousDedup;
if (cg) cg.destroy();
if (testDir && fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
});
@@ -328,6 +355,23 @@ describe('a second call against a real index', () => {
}
}, 120_000);
it('re-serves source by default when a connection may outlive the current context', async () => {
const session = new ExploreSessionState();
const previous = process.env.CODEGRAPH_EXPLORE_DEDUP;
delete process.env.CODEGRAPH_EXPLORE_DEDUP;
try {
const first = await explore(QUERY, session);
const second = await explore(QUERY, session);
expect(second).toBe(first);
expect(second).not.toContain(POINTER);
expect([...fencedLines(second).values()].reduce((sum, lines) => sum + lines.size, 0))
.toBeGreaterThan(20);
} finally {
if (previous === undefined) delete process.env.CODEGRAPH_EXPLORE_DEDUP;
else process.env.CODEGRAPH_EXPLORE_DEDUP = previous;
}
}, 120_000);
it('reports the reclaimed bytes through the CG-4 diagnostic', async () => {
const sidecar = path.join(testDir, 'cg18-diagnostic.jsonl');
const session = new ExploreSessionState();