Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- A stuck git command can no longer hang CodeGraph indefinitely. The git checks behind worktree detection and git-hook setup, and the installer's optional `npm install -g` step, now time out and fail gracefully instead of blocking forever — this matters most for the background MCP server, where an unbounded git hang (network filesystems, a wedged fsmonitor) could previously freeze it long enough for the safety watchdog to kill it. Thanks @inth3shadows for the report. (#1139)
- The context hook's new plain-words matching works immediately on projects indexed by an older CodeGraph version. The word lookup it relies on is built at index time, so a project indexed before the upgrade had an empty one, and the hook would silently find nothing until something else happened to refresh the index; the hook now fills it in on first use (a one-time step — normally the background MCP server's startup catch-up gets there first). Thanks @inth3shadows for the report. (#1142)
- Several accuracy fixes to the plain-words matching: a renamed symbol (for example a NestJS route after its module prefix is applied) stays findable under its new name (#1141); a word that only appears in your code as an import statement's package name is no longer presented as a matched symbol (#1144); plural words no longer generate garbled lookup keys ("services" no longer also looks up "servic") (#1145); and a name matching both the singular and plural of one word can no longer squeeze out a genuine two-word match (#1146). Thanks @inth3shadows for the reports.
- `codegraph sync` no longer fails with "Maximum call stack size exceeded" on large projects. When a sync re-checked a big set of files at once — a fresh index, a large Perforce or git changelist, or the first sync after switching branches — CodeGraph gathered the pending references for those files in a way that broke down once the count reached into the hundreds of thousands, aborting the whole sync before it could finish. Syncs of any size now complete. Thanks for the report.

## [1.2.0] - 2026-07-02

Expand Down
47 changes: 47 additions & 0 deletions __tests__/db-perf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,53 @@ describe('deleteResolvedReferences (chunking)', () => {
});
});

describe('getUnresolvedReferencesByFiles (large result set)', () => {
let dir: string;
let db: DatabaseConnection;
let q: QueryBuilder;

beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'db-perf-unresbyfiles-'));
db = DatabaseConnection.initialize(path.join(dir, 'test.db'));
q = new QueryBuilder(db.getDb());
});

afterEach(() => {
db.close();
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
});

it('does not overflow the stack when one file has more refs than the spread limit', () => {
// Regression: the method chunks the file-PATH list under SQLite's parameter
// limit, but then did `rows.push(...chunkRows)` — spreading every matched
// row as a separate call argument. On a large `codegraph sync` a single
// chunk of files matches hundreds of thousands of unresolved_refs rows,
// which blew V8's argument-stack limit with "Maximum call stack size
// exceeded" (the arg-spread threshold is ~125k). All refs here share ONE
// file_path so a single chunk returns the whole set. from_node_id has a FK
// to nodes, so a backing node must exist first.
const COUNT = 130_000; // just past the arg-spread ceiling
q.insertNode(makeNode('n1'));
q.insertUnresolvedRefsBatch(
Array.from({ length: COUNT }, (_, i) => ({
fromNodeId: 'n1',
referenceName: `ref${i}`,
referenceKind: 'calls' as const,
line: i + 1,
column: 0,
filePath: 'big.ts',
language: 'typescript' as const,
}))
);

let refs: ReturnType<typeof q.getUnresolvedReferencesByFiles> = [];
expect(() => {
refs = q.getUnresolvedReferencesByFiles(['big.ts']);
}).not.toThrow();
expect(refs.length).toBe(COUNT);
});
});

describe('insertNode cache invalidation', () => {
let dir: string;
let db: DatabaseConnection;
Expand Down
7 changes: 6 additions & 1 deletion src/db/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1856,7 +1856,12 @@ export class QueryBuilder {
const chunkRows = this.db
.prepare(`SELECT * FROM unresolved_refs WHERE file_path IN (${placeholders})`)
.all(...chunk) as UnresolvedRefRow[];
rows.push(...chunkRows);
// Append without spreading: `rows.push(...chunkRows)` passes every row as a
// separate argument, and a 500-file chunk of a large repo matches tens of
// thousands of refs — enough to blow V8's argument-stack limit with
// "Maximum call stack size exceeded" on `codegraph sync`. Chunking the
// file-path IN-list bounds the SQL params, NOT the returned row count.
for (const row of chunkRows) rows.push(row);
}

return rows.map((row) => ({
Expand Down