mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 05:51:35 +08:00
fix(mcp,extraction): exit a daemon whose install changed; never store a grammar-load failure (#2335, #2336) (#2346)
* fix(mcp,extraction): a daemon whose install changed exits; grammar-load failures are never stored (#2335, #2336) A daemon that outlived an upgrade loaded grammars lazily from an install that no longer existed, and stored every file its watcher re-indexed as a zero-node row under the file's new content hash: no hash-based sync ever revisited those rows, and `codegraph status` called the index up to date. - The daemon checks its own package.json every 30 s (CODEGRAPH_DAEMON_INSTALL_CHECK_MS, 0 turns it off) and exits once the file is gone or carries another version, so the next session starts a daemon from the current install. - A result whose grammar failed to load (`parser_error`) is never stored: the file keeps its previous data and the next sync or index retries it, and a row an older engine stored that way is re-indexed even though its hash matches. The tag-based CFML path reports a missing grammar with the same code. - A launcher whose own install was deleted no longer crashes on the spawn error of the daemon it tried to start. - `codegraph status` and `status --json` (filesNeedingReindex, filesWithParseErrors) report indexed files whose symbols are missing although their content is current (#2336), and `files --json` carries each file's recorded errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mcp): a session whose own install changed never claims the writer lock (#2335) A daemon now exits once its install is upgraded or removed, and nobody holds the writer slot for a successor at that point. Its sessions run the same replaced code and serve themselves in-process the moment it goes; one that found the slot free claimed it as its writer even though its engine could not load (a real npm upgrade: "Cannot find module './sqlite-adapter'"), and so kept a daemon from the current install from starting. Such a session now falls back read-only and says to restart it. Also adds the CHANGELOG entries for the install check and grammar-load failures (#2335) and for the status report (#2336). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
511d86e94d
commit
dea076fd1e
@@ -147,10 +147,12 @@ export class CfmlExtractor {
|
||||
private extractTagBased(): void {
|
||||
const parser = getParser('cfml');
|
||||
if (!parser) {
|
||||
// Same code as TreeSitterExtractor's missing parser, so a grammar that
|
||||
// failed to load is never stored as this file's index data (#2335).
|
||||
this.errors.push({
|
||||
message: 'cfml grammar not loaded',
|
||||
severity: 'error',
|
||||
code: 'unsupported_language',
|
||||
code: 'parser_error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import * as path from 'path';
|
||||
import * as fsp from 'fs/promises';
|
||||
import { Parser, Language as WasmLanguage } from 'web-tree-sitter';
|
||||
import { Language } from '../types';
|
||||
import { ExtractionError, Language } from '../types';
|
||||
|
||||
export type GrammarLanguage = Exclude<Language, 'svelte' | 'vue' | 'astro' | 'liquid' | 'razor' | 'yaml' | 'twig' | 'xml' | 'properties' | 'unknown'>;
|
||||
|
||||
@@ -828,6 +828,21 @@ export function getUnavailableGrammarErrors(): Partial<Record<Language, string>>
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether these extraction errors say the file was never parsed because its
|
||||
* grammar was not available (`parser_error`, recorded by the extractor when
|
||||
* `getParser` has nothing for the language).
|
||||
*
|
||||
* That is a fact about the running process, not about the file: a daemon whose
|
||||
* install was upgraded or deleted underneath it fails every lazy grammar load
|
||||
* (#2335). Such a result must never replace a file's index data, and a row an
|
||||
* older engine stored that way must be re-indexed — not treated as current
|
||||
* because its content hash still matches.
|
||||
*/
|
||||
export function hasGrammarLoadFailure(errors: readonly ExtractionError[] | undefined): boolean {
|
||||
return !!errors && errors.some((e) => e.code === 'parser_error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get language display name
|
||||
*/
|
||||
|
||||
+32
-8
@@ -25,7 +25,7 @@ import { ParseWorkerPool, resolveParsePoolSize, resolveParseTimeoutMs } from './
|
||||
import { StoreWriter, StoreBundle, finalizeStoreBundle } from './store-writer';
|
||||
import { materializeKernelResult } from './kernel';
|
||||
import { detectGeneratedFile } from './generated-detection';
|
||||
import { detectLanguage, isSourceFile, isLanguageSupported, isFileLevelOnlyLanguage, initGrammars, loadGrammarsForLanguages, readGrammarWasmBytes, isMpegTransportStream, hasMpegTsExtension, MPEG_TS_SNIFF_BYTES } from './grammars';
|
||||
import { detectLanguage, isSourceFile, isLanguageSupported, isFileLevelOnlyLanguage, initGrammars, loadGrammarsForLanguages, readGrammarWasmBytes, isMpegTransportStream, hasMpegTsExtension, MPEG_TS_SNIFF_BYTES, hasGrammarLoadFailure } from './grammars';
|
||||
import { loadExtensionOverrides, loadIncludeIgnoredPatterns, loadExcludePatterns, loadIncludePatterns, PROJECT_CONFIG_FILENAME } from '../project-config';
|
||||
import { isCodeGraphDataDir } from '../directory';
|
||||
import { logDebug, logWarn } from '../errors';
|
||||
@@ -2289,11 +2289,17 @@ export class ExtractionOrchestrator {
|
||||
const nodeCount = result.kernelCounts?.nodes ?? result.nodes.length;
|
||||
const edgeCount = result.kernelCounts?.edges ?? result.edges.length;
|
||||
|
||||
// A file whose grammar failed to load was never parsed (#2335).
|
||||
const grammarUnavailable = hasGrammarLoadFailure(result.errors);
|
||||
|
||||
// Store: on the writer thread when active (fresh DB — bundles applied
|
||||
// in the same file order this chain dispatches them), else on the main
|
||||
// thread (SQLite connections are per-thread).
|
||||
const language = detectLanguage(filePath, content, overrides);
|
||||
if (storeWriter) {
|
||||
if (grammarUnavailable) {
|
||||
// Store nothing: a row from an earlier run keeps its data, and with no
|
||||
// row (or an older hash) the next sync or index retries the file.
|
||||
} else if (storeWriter) {
|
||||
if (result.kernelBuffers) {
|
||||
// Buffers go to the writer as-is; the worker decodes + finalizes.
|
||||
// The main thread's only per-file work stays O(1) + the content hash.
|
||||
@@ -2321,7 +2327,9 @@ export class ExtractionOrchestrator {
|
||||
errors.push(...result.errors);
|
||||
}
|
||||
|
||||
if (nodeCount > 0) {
|
||||
if (grammarUnavailable) {
|
||||
filesErrored++;
|
||||
} else if (nodeCount > 0) {
|
||||
filesIndexed++;
|
||||
totalNodes += nodeCount;
|
||||
totalEdges += edgeCount;
|
||||
@@ -2735,7 +2743,9 @@ export class ExtractionOrchestrator {
|
||||
errors.push(...result.errors);
|
||||
}
|
||||
|
||||
if (result.nodes.length > 0) {
|
||||
if (hasGrammarLoadFailure(result.errors)) {
|
||||
filesErrored++; // nothing was stored (#2335)
|
||||
} else if (result.nodes.length > 0) {
|
||||
filesIndexed++;
|
||||
totalNodes += result.nodes.length;
|
||||
totalEdges += result.edges.length;
|
||||
@@ -2918,6 +2928,12 @@ export class ExtractionOrchestrator {
|
||||
result: ExtractionResult,
|
||||
onYield?: MaybeYield
|
||||
): Promise<void> {
|
||||
// The file was never parsed: its grammar failed to load (#2335). Storing
|
||||
// this would replace the file's symbols with an empty row under the new
|
||||
// content hash, which no hash-based sync revisits. Keep whatever the index
|
||||
// has; the stale (or missing) row makes the next sync retry the file.
|
||||
if (hasGrammarLoadFailure(result.errors)) return;
|
||||
|
||||
// A kernel result can arrive as an undecoded buffer transport (empty
|
||||
// node/edge arrays, tables riding in kernelBuffers). Decode it before
|
||||
// storing — persisting the transport as-is records the file as having no
|
||||
@@ -2944,7 +2960,10 @@ export class ExtractionOrchestrator {
|
||||
const existingIsMarker =
|
||||
existingFile.nodeCount === 0 && (existingFile.errors?.length ?? 0) > 0;
|
||||
const incomingHasContent = result.nodes.length > 0;
|
||||
if (!existingIsMarker || !incomingHasContent) {
|
||||
// A row an older engine stored while the grammar could not load
|
||||
// (#2335) records no parse at all: any real result replaces it.
|
||||
const existingNeverParsed = hasGrammarLoadFailure(existingFile.errors);
|
||||
if (!existingNeverParsed && (!existingIsMarker || !incomingHasContent)) {
|
||||
return; // No changes
|
||||
}
|
||||
}
|
||||
@@ -3400,13 +3419,18 @@ export class ExtractionOrchestrator {
|
||||
}
|
||||
const fullPath = path.join(this.rootDir, filePath);
|
||||
const tracked = trackedMap.get(filePath);
|
||||
// A row an older engine stored while the file's grammar could not load
|
||||
// (#2335) holds no parse of these bytes: re-index it even though its
|
||||
// size, mtime and hash all match. Rows with a real parse error are
|
||||
// deterministic and are not retried.
|
||||
const neverParsed = tracked !== undefined && hasGrammarLoadFailure(tracked.errors);
|
||||
|
||||
// Cheap pre-filter: an already-indexed file whose size AND mtime both match
|
||||
// the DB is unchanged — skip it without reading or hashing. (A content
|
||||
// change that preserves both exactly is the blind spot every mtime-based
|
||||
// incremental tool accepts; `index --force` is the escape hatch. Git bumps
|
||||
// mtime on every file it writes during checkout/merge, so pulls are caught.)
|
||||
if (tracked) {
|
||||
if (tracked && !neverParsed) {
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.size === tracked.size && Math.floor(stat.mtimeMs) === Math.floor(tracked.modifiedAt)) {
|
||||
@@ -3442,7 +3466,7 @@ export class ExtractionOrchestrator {
|
||||
filesToIndex.push(filePath);
|
||||
changedFilePaths.push(filePath);
|
||||
filesAdded++;
|
||||
} else if (tracked.contentHash !== contentHash) {
|
||||
} else if (tracked.contentHash !== contentHash || neverParsed) {
|
||||
onFileChange?.(filePath, content);
|
||||
filesToIndex.push(filePath);
|
||||
changedFilePaths.push(filePath);
|
||||
@@ -3477,7 +3501,7 @@ export class ExtractionOrchestrator {
|
||||
|
||||
const result = await this.indexFile(filePath);
|
||||
if (result.errors.some(e => e.severity === 'error')) failedFilePaths.push(filePath);
|
||||
nodesUpdated += result.nodes.length;
|
||||
if (!hasGrammarLoadFailure(result.errors)) nodesUpdated += result.nodes.length; // else nothing was stored (#2335)
|
||||
|
||||
const pause = backpressure?.();
|
||||
if (pause) await pause;
|
||||
|
||||
Reference in New Issue
Block a user