mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 03:12:32 +00:00
fix: 8 root-cause fixes from /investigate (v0.14.2) (#259)
* fix: 8 root-cause fixes from /investigate wave
Consolidated bundle of bug fixes from /investigate on the 8 deferred bugs.
Each fix was designed to go at the structural gap, not the symptom. Codex
verified 20 load-bearing claims on the plan; 12 triggered plan revisions.
Bug 2 — GBRAIN_POOL_SIZE env knob + init finally blocks (no auto-detect).
Covers both the singleton pool (db.ts) and instance pool (import.ts:140).
Bug 3 — Centralize migration ledger writes in apply-migrations runner.
Removed appendCompletedMigration from v0_11_0, v0_12_0, v0_12_2,
v0_13_0, v0_13_1. Added 3-partial wedge cap + --force-retry reset.
'complete wins' preserved; no partial can regress a completed migration.
Bug 5 — v0.14.0 migration registered. src/commands/migrations/v0_14_0.ts
ships Phase A (ALTER minion_jobs.max_stalled SET DEFAULT 3) + Phase B
(pending-host-work ping for shell-jobs adoption).
Bug 6/10 — jsonb_agg(DISTINCT ...) in legacy traverseGraph (both engines).
Presentation-level dedup; schema still preserves provenance rows.
Bug 7 — doctor --fast reads DB URL source via getDbUrlSource() in config.ts.
Precise message: 'Skipping DB checks (--fast mode, URL present from env)'
replaces the misleading 'No database configured'.
Bug 8 — max_stalled default bumped 1→3 in schema-embedded.ts, pglite-schema.ts,
schema.sql (new installs). v0_14_0 Phase A ALTER for existing installs.
autopilot-cycle handler yields to event loop between phases so the
worker's lock-renewal timer fires on huge brains. (Deep AbortSignal
threading through runEmbedCore/runExtractCore/runBacklinksCore/performSync
deferred to v0.15 queue polish.)
Bug 9 — Gate sync.last_commit on no-failures across all three sync paths
(incremental, full via runImport, gbrain import git continuity).
recordSyncFailures() helper + ~/.gbrain/sync-failures.jsonl with
dedup key path+commit+error-hash. New flags: --skip-failed (ack) +
--retry-failed (re-attempt). Doctor surfaces unacknowledged failures.
Bug 11 — brain_score breakdown fields on BrainHealth (embed_coverage_score,
link_density_score, timeline_coverage_score, no_orphans_score,
no_dead_links_score); sum equals brain_score by construction.
dead_links now on the type (resolves featuresTeaserForDoctor drift).
orphan_pages kept as 'islanded' (no inbound AND no outbound) and
docs updated to match — explicit semantic instead of doc drift.
New tests: test/traverse-graph-dedup.test.ts, test/sync-failures.test.ts,
test/brain-score-breakdown.test.ts, test/migration-resume.test.ts,
test/migrations-v0_14_0.test.ts. Extended: migrate, doctor, apply-migrations.
All 1696 unit tests pass locally. postgres-jsonb E2E regression unchanged
(none of these touch the JSONB write surface).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: v0.14.2 CHANGELOG + CLAUDE.md; align migration-flow E2E with runner-owned ledger
CHANGELOG: v0.14.2 entry in the standard release-summary format
(two-line headline + lead + numbers table + "what this means" +
"To take advantage of v0.14.2" self-repair block + itemized
changes grouped by reliability / observability / graph correctness /
new migration / tests / deferred-to-v0.15).
CLAUDE.md: new "Key commands added in v0.14.2" section covers
--skip-failed, --retry-failed, --force-retry, GBRAIN_POOL_SIZE env,
and the new doctor checks (sync_failures, brain_score breakdown).
Migration orchestrator docs updated to describe v0_14_0.ts + the
runner-owned ledger contract from Bug 3.
test/e2e/migration-flow.test.ts: three assertions updated to match
the Bug 3 contract — orchestrators no longer append to completed.jsonl
directly, so direct-orchestrator E2E calls leave the ledger empty.
Preferences assertions remain (that's still the orchestrator's side
of the contract). Runner's ledger write is covered by the unit suite
(test/apply-migrations.test.ts + test/migration-resume.test.ts).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ebfbd5e6f7
commit
b5fa3d044a
@@ -133,3 +133,127 @@ export function pathToSlug(filePath: string, repoPrefix?: string): string {
|
||||
if (repoPrefix) slug = `${repoPrefix}/${slug}`;
|
||||
return slug.toLowerCase();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Sync failure tracking — Bug 9
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// When a sync run catches a per-file parse error (YAML with unquoted
|
||||
// colons, malformed frontmatter, etc.), we record it here instead of just
|
||||
// logging and moving on. Three goals:
|
||||
// 1. Gate the sync.last_commit bookmark advance in all three sync paths
|
||||
// (incremental, full/runImport, `gbrain import` git continuity).
|
||||
// 2. Give users a visible record of what failed, with the commit hash
|
||||
// they can use to re-attempt after fixing the source file.
|
||||
// 3. Let `gbrain sync --skip-failed` acknowledge a known-bad set so
|
||||
// repos with many broken files aren't permanently stuck.
|
||||
|
||||
import { existsSync as _existsSync, readFileSync as _readFileSync, appendFileSync as _appendFileSync, mkdirSync as _mkdirSync } from 'fs';
|
||||
import { join as _joinPath } from 'path';
|
||||
import { homedir as _homedir } from 'os';
|
||||
import { createHash as _createHash } from 'crypto';
|
||||
|
||||
export interface SyncFailure {
|
||||
path: string;
|
||||
error: string;
|
||||
commit: string;
|
||||
line?: number;
|
||||
ts: string;
|
||||
acknowledged?: boolean;
|
||||
acknowledged_at?: string;
|
||||
}
|
||||
|
||||
function _failuresDir(): string {
|
||||
return _joinPath(_homedir(), '.gbrain');
|
||||
}
|
||||
|
||||
export function syncFailuresPath(): string {
|
||||
return _joinPath(_failuresDir(), 'sync-failures.jsonl');
|
||||
}
|
||||
|
||||
function _hashError(msg: string): string {
|
||||
return _createHash('sha256').update(msg).digest('hex').slice(0, 12);
|
||||
}
|
||||
|
||||
function _dedupKey(f: { path: string; commit: string; error: string }): string {
|
||||
return `${f.path}|${f.commit}|${_hashError(f.error)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the failures JSONL, skipping malformed lines with a warning to stderr.
|
||||
* Returns empty array if the file doesn't exist.
|
||||
*/
|
||||
export function loadSyncFailures(): SyncFailure[] {
|
||||
const path = syncFailuresPath();
|
||||
if (!_existsSync(path)) return [];
|
||||
const raw = _readFileSync(path, 'utf-8');
|
||||
const out: SyncFailure[] = [];
|
||||
for (const line of raw.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
out.push(JSON.parse(trimmed) as SyncFailure);
|
||||
} catch {
|
||||
console.warn(`[sync-failures] skipping malformed line: ${trimmed.slice(0, 120)}`);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append failure entries to the JSONL. Dedups by (path, commit, error-hash) —
|
||||
* the same file failing with the same error on the same commit writes ONCE
|
||||
* to the log, not once per sync run.
|
||||
*/
|
||||
export function recordSyncFailures(
|
||||
failures: Array<{ path: string; error: string; line?: number }>,
|
||||
commit: string,
|
||||
): void {
|
||||
if (failures.length === 0) return;
|
||||
const existing = loadSyncFailures();
|
||||
const seen = new Set(existing.map(f => _dedupKey(f)));
|
||||
|
||||
_mkdirSync(_failuresDir(), { recursive: true });
|
||||
const now = new Date().toISOString();
|
||||
for (const f of failures) {
|
||||
const entry: SyncFailure = {
|
||||
path: f.path,
|
||||
error: f.error,
|
||||
commit,
|
||||
line: f.line,
|
||||
ts: now,
|
||||
};
|
||||
if (seen.has(_dedupKey(entry))) continue;
|
||||
_appendFileSync(syncFailuresPath(), JSON.stringify(entry) + '\n');
|
||||
seen.add(_dedupKey(entry));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all unacknowledged failures as acknowledged. Used by
|
||||
* `gbrain sync --skip-failed`. Returns the number newly acknowledged.
|
||||
*
|
||||
* We do not delete — acknowledged entries stay as historical record so
|
||||
* doctor can still show them under a "previously skipped" bucket.
|
||||
*/
|
||||
export function acknowledgeSyncFailures(): number {
|
||||
const entries = loadSyncFailures();
|
||||
if (entries.length === 0) return 0;
|
||||
const now = new Date().toISOString();
|
||||
let changed = 0;
|
||||
const updated = entries.map(e => {
|
||||
if (e.acknowledged) return e;
|
||||
changed++;
|
||||
return { ...e, acknowledged: true, acknowledged_at: now };
|
||||
});
|
||||
if (changed === 0) return 0;
|
||||
_mkdirSync(_failuresDir(), { recursive: true });
|
||||
const fd = require('fs').writeFileSync;
|
||||
fd(syncFailuresPath(), updated.map(e => JSON.stringify(e)).join('\n') + '\n');
|
||||
return changed;
|
||||
}
|
||||
|
||||
/** Return only unacknowledged failures. */
|
||||
export function unacknowledgedSyncFailures(): SyncFailure[] {
|
||||
return loadSyncFailures().filter(f => !f.acknowledged);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user