Files
gbrain/src/core/code-graph-readiness.ts
T
1036f8f752 v0.42.14.0 fix(zero-config): code-* readiness signal + init embedding-key validation + lock self-heal (#1780) (#1804)
* feat(code-intel): readiness signal on code-def/refs/callers/callees (#1780 Gap 1)

New src/core/code-graph-readiness.ts: resolveCodeReadiness() returns a typed
status (not_built | indexing | ready | unknown) + ready boolean so callers can
tell "graph not built / still indexing" apart from "genuinely no match" when
count===0. EXISTS-based (cheap), chunk-grain, resolver-version-matching pending
predicate, fail-open. Wired into the 4 CLI envelopes (+ human hint) and the 4
MCP op handlers. def/refs are 2-state brain-wide; callers/callees 3-state scoped.

* feat(db-lock): automatic same-host dead-pid cycle-lock takeover (#1780 Gap 3)

tryAcquireDbLock now reclaims a held, not-TTL-expired lock when the same-host
holder is provably dead (process.kill ESRCH) past a 60s grace, via guarded
DELETE + one normal-upsert retry returning the normal handle. New shared
injectable classifyHolderLiveness/isHolderDeadLocally (EPERM treated as ALIVE
— never steals a live lock). runBreakLock's safe path consumes the shared
predicate, fixing its prior EPERM-as-dead bug. Cross-host stays TTL-only.

* feat(init): validate the embedding key at gbrain init (#1780 Gap 2)

New src/core/init-embed-check.ts: config-only diagnoseEmbedding (missing key,
all providers) + best-effort 1-token live test-embed (invalid/expired key, 5s
timeout, never blocks). Loud warning to stderr, init still exits 0; skipped by
--no-embedding / --skip-embed-check / GBRAIN_INIT_SKIP_EMBED_CHECK=1. Builds the
effective env (process.env + file-plane keys + --key) via buildGatewayConfig,
extracted to src/core/ai/build-gateway-config.ts (cli.ts re-exports) so the
check sees the same keys + provider base URLs as runtime. embedding_check added
to --json.

* chore: bump version and changelog (v0.42.14.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document the #1780 zero-config gaps for v0.42.14.0

CLAUDE.md Key Files: add src/core/code-graph-readiness.ts, init-embed-check.ts,
ai/build-gateway-config.ts, and the db-lock auto-takeover + code-* readiness
field behaviors. Regenerate llms-full.txt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 06:43:11 -07:00

157 lines
6.5 KiB
TypeScript

/**
* Code-graph readiness signal (issue #1780 Gap 1).
*
* `code-def` / `code-refs` / `code-callers` / `code-callees` historically
* returned `count: 0` in three indistinguishable situations:
* 1. the symbol graph isn't built yet for the scope (code never synced /
* chunked, or edges not yet resolved),
* 2. the source was never synced,
* 3. the graph IS built and the symbol genuinely has no match.
*
* An agent that gets `count: 0` can't tell "wait and retry" from "trust this
* empty result." This module adds a typed readiness signal so the envelope
* carries `status` + `ready`, letting the caller distinguish those cases.
*
* Two grains, because the four commands read different data:
* - `code-def` / `code-refs` read `content_chunks.symbol_name` /
* `chunk_text`, which are populated at CHUNK time (during sync/import),
* independent of edge resolution. Their readiness is 2-state: code chunks
* exist → `ready`, else `not_built`. They never report `indexing` (edge
* resolution is irrelevant to them).
* - `code-callers` / `code-callees` read the call graph (`code_edges_*`).
* Their readiness is 3-state: no code chunks → `not_built`; code chunks
* but edges not yet resolved → `indexing`; all resolved → `ready`.
*
* The "pending edges" predicate MUST mirror the resolver
* (`symbol-resolver.ts:resolveSymbolEdgesIncremental`): a chunk is pending
* when `edges_backfilled_at IS NULL OR edges_backfilled_at <
* EDGE_EXTRACTOR_VERSION_TS`. Counting only `IS NULL` would falsely report
* `ready` after a resolver-version bump (the graph is stale, not done).
*
* Cost: callers run this ONLY when `count === 0` (see `resolveCodeReadiness`);
* a non-empty result short-circuits to `ready: true` with no query. Probes use
* `EXISTS` (short-circuits on first row) rather than `COUNT(*)` because the
* bootstrap schema has no `page_kind` index; the pending probe rides the
* partial `idx_content_chunks_edges_backfill` index. Fail-open: any DB error
* yields `status: 'unknown'` so a supplementary signal never breaks the command.
*
* Scope must match the result query exactly: `code-def` / `code-refs` do NOT
* filter `deleted_at`, so neither do these probes (else readiness could say
* `not_built` while results came from soft-deleted code pages).
*/
import type { BrainEngine } from './engine.ts';
import { EDGE_EXTRACTOR_VERSION_TS } from './chunkers/symbol-resolver.ts';
export type CodeGraphStatus = 'not_built' | 'indexing' | 'ready' | 'unknown';
export interface CodeGraphReadiness {
/** Coarse machine-readable state. */
status: CodeGraphStatus;
/** Convenience: `status === 'ready'`. */
ready: boolean;
/** Whether any code chunk exists in scope. */
has_code: boolean;
/** Whether unresolved/stale edge chunks remain in scope (edge kind only). */
pending_edges: boolean;
}
/** Scope for a readiness probe. Omit `sourceId` (or set `allSources`) for brain-wide. */
export interface ReadinessScope {
sourceId?: string;
allSources?: boolean;
}
function effectiveSourceId(scope: ReadinessScope): string | undefined {
return scope.allSources ? undefined : scope.sourceId;
}
/** EXISTS probe: does any code chunk exist in scope? Matches the def/refs result query. */
async function codeChunksExist(engine: BrainEngine, sourceId: string | undefined): Promise<boolean> {
const params: unknown[] = [];
let scopeClause = '';
if (sourceId) {
params.push(sourceId);
scopeClause = `AND p.source_id = $${params.length}`;
}
const rows = await engine.executeRaw<{ e: boolean }>(
`SELECT EXISTS(
SELECT 1 FROM content_chunks cc
JOIN pages p ON p.id = cc.page_id
WHERE p.page_kind = 'code' ${scopeClause}
) AS e`,
params,
);
return Boolean(rows[0]?.e);
}
/** EXISTS probe: does any code chunk have unresolved/stale edges (resolver predicate)? */
async function pendingEdgeChunksExist(engine: BrainEngine, sourceId: string | undefined): Promise<boolean> {
const params: unknown[] = [EDGE_EXTRACTOR_VERSION_TS];
let scopeClause = '';
if (sourceId) {
params.push(sourceId);
scopeClause = `AND p.source_id = $${params.length}`;
}
const rows = await engine.executeRaw<{ e: boolean }>(
`SELECT EXISTS(
SELECT 1 FROM content_chunks cc
JOIN pages p ON p.id = cc.page_id
WHERE p.page_kind = 'code'
AND (cc.edges_backfilled_at IS NULL
OR cc.edges_backfilled_at < $1::timestamptz)
${scopeClause}
) AS e`,
params,
);
return Boolean(rows[0]?.e);
}
/**
* Resolve the readiness signal for a code-* command.
*
* `kind: 'symbol'` for code-def/code-refs (2-state); `kind: 'edge'` for
* code-callers/code-callees (3-state). When `count > 0` the result is
* trivially `ready` and no query runs. Fail-open: any DB error → `unknown`.
*/
export async function resolveCodeReadiness(
engine: BrainEngine,
opts: { kind: 'symbol' | 'edge'; count: number } & ReadinessScope,
): Promise<CodeGraphReadiness> {
if (opts.count > 0) {
return { status: 'ready', ready: true, has_code: true, pending_edges: false };
}
const sourceId = effectiveSourceId(opts);
try {
const hasCode = await codeChunksExist(engine, sourceId);
if (!hasCode) {
return { status: 'not_built', ready: false, has_code: false, pending_edges: false };
}
if (opts.kind === 'symbol') {
// Symbol metadata is set at chunk time; code chunks exist ⇒ genuinely none.
return { status: 'ready', ready: true, has_code: true, pending_edges: false };
}
const pending = await pendingEdgeChunksExist(engine, sourceId);
return pending
? { status: 'indexing', ready: false, has_code: true, pending_edges: true }
: { status: 'ready', ready: true, has_code: true, pending_edges: false };
} catch {
// Supplementary signal: never fail the command on a readiness DB error.
return { status: 'unknown', ready: false, has_code: false, pending_edges: false };
}
}
/** Human-facing one-liner for non-TTY-less output, or null when ready. */
export function readinessHint(r: CodeGraphReadiness): string | null {
switch (r.status) {
case 'not_built':
return 'Symbol graph not built (no code indexed in scope). Run `gbrain sync` to index code.';
case 'indexing':
return 'Symbol graph still building (edges pending resolution). Re-run after the next `gbrain dream` cycle / autopilot tick.';
case 'unknown':
return 'Readiness check unavailable (DB error). Treat the empty result as best-effort.';
case 'ready':
return null;
}
}