mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* fix(minions): self-identifying RSS watchdog + cgroup-aware default + pooler-reap self-heal (#1678) Problem 1: distinct WORKER_EXIT_RSS_WATCHDOG exit code + cause-keyed supervisor breaker (bypasses the stable-run reset that hid the 400x/24h loop) + rss_watchdog audit bucket + 80% soft-warn; cgroup-aware resolveDefaultMaxRssMb replaces the flat 2048 default at every spawn site. Problem 2: CONNECTION_ENDED classified retryable; postgres-engine sql getter throws a retryable error on a reaped instance pool instead of the misleading module-singleton fallthrough; promoteDelayed reconnect-retry; claim recovers on the next poll tick (no double-claim); lock-renewal tick reconnect-once dep. * feat(cycle): surface silent extract_atoms backlog + bounded --drain + fix lint clobbering the shared DB connection (#1678) Problem 3: extract_atoms_backlog doctor check + pack_gated skip marker + shared countExtractAtomsBacklog; `gbrain dream --phase extract_atoms --drain [--window N]` single-hold bounded drain (same cycleLockIdFor, rediscover each batch, reports remaining, exits non-zero while work remains). Also fixes a real production bug found via E2E: the cycle lint phase's resolveLintContentSanity created + disconnected a module-style engine that nulled the shared db singleton mid-cycle, breaking every later phase with "connect() has not been called". Lint now reuses the caller's live engine (cycle + Minion handlers thread it; standalone CLI keeps the create-own path). * chore: bump version and changelog (v0.41.39.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#1678): pre-landing review — route transaction/withReservedConnection through the sql getter + drain treats failed count as incomplete Codex adversarial review findings: - #2: transaction(), withReservedConnection(), and one other site bypassed the v0.42.2.0 sql-getter self-heal via `this._sql || db.getConnection()`, so a reaped instance pool fell through to the module singleton there. Route all three through `this.sql` so they throw the retryable instance-pool error and recover consistently (MinionQueue.transaction hits this). - #4: `gbrain dream --drain` treated a null backlog count (query failure) as success via `remaining ?? 0`; now null exits EXIT_DRAIN_INCOMPLETE so automation never believes an unverified backlog drained. - #1 (claim orphan) + #3 (PGLite drain lock) documented as follow-ups in TODOS. * docs: document v0.42.2.0 #1678 modules + behavior in CLAUDE.md Adds Key Files entries for worker-exit-codes.ts, rss-default.ts, and extract-atoms-drain.ts, plus v0.42.2.0 annotations on worker.ts, child-worker-supervisor.ts, lock-renewal-tick.ts, and dream.ts. Regenerated llms-full.txt to match (test/build-llms.test.ts gate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: re-version v0.42.2.0 → v0.42.5.0 across VERSION/package.json/CHANGELOG/docs/comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
99 lines
3.8 KiB
TypeScript
99 lines
3.8 KiB
TypeScript
/**
|
|
* issue #1678 — extract_atoms backlog count + doctor check.
|
|
*
|
|
* Pins:
|
|
* - countExtractAtomsBacklog counts eligible-but-unextracted pages (scoped +
|
|
* brain-wide) and excludes pages that already have an atom (NOT EXISTS).
|
|
* - computeExtractAtomsBacklogCheck WARNs with a `--drain` hint when the pack
|
|
* doesn't run the phase and the backlog is real; OK at 0.
|
|
*
|
|
* Real in-memory PGLite (canonical block, R3+R4). GBRAIN_HOME is pointed at an
|
|
* empty tmpdir for the doctor-check cases so packDeclaresPhase resolves the
|
|
* bundled base pack (which does NOT declare extract_atoms) deterministically,
|
|
* independent of the developer's real ~/.gbrain config.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
|
import { mkdtempSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
|
import { withEnv } from './helpers/with-env.ts';
|
|
import { countExtractAtomsBacklog } from '../src/core/cycle/extract-atoms.ts';
|
|
import { computeExtractAtomsBacklogCheck } from '../src/commands/doctor.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
const EMPTY_HOME = mkdtempSync(join(tmpdir(), 'gbrain-xa-backlog-home-'));
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await resetPgliteState(engine);
|
|
});
|
|
|
|
const BODY = 'x'.repeat(600); // >= MIN_PAGE_CHARS_FOR_EXTRACTION (500)
|
|
|
|
async function seedArticle(slug: string) {
|
|
return engine.putPage(slug, { type: 'article', title: slug, compiled_truth: BODY });
|
|
}
|
|
|
|
describe('countExtractAtomsBacklog (issue #1678)', () => {
|
|
it('counts eligible pages with no atom (scoped + brain-wide)', async () => {
|
|
await seedArticle('article-a');
|
|
await seedArticle('article-b');
|
|
await seedArticle('article-c');
|
|
expect(await countExtractAtomsBacklog(engine)).toBe(3);
|
|
expect(await countExtractAtomsBacklog(engine, 'default')).toBe(3);
|
|
});
|
|
|
|
it('excludes a page that already has a matching atom (NOT EXISTS)', async () => {
|
|
const p = await seedArticle('article-x');
|
|
const h16 = (p.content_hash ?? '').slice(0, 16);
|
|
expect(h16.length).toBe(16);
|
|
await engine.putPage('atoms/a1', {
|
|
type: 'atom',
|
|
title: 'a1',
|
|
compiled_truth: 'an extracted nugget',
|
|
frontmatter: { source_hash: h16 },
|
|
});
|
|
expect(await countExtractAtomsBacklog(engine)).toBe(0);
|
|
});
|
|
|
|
it('ignores short pages and dream-generated pages', async () => {
|
|
await engine.putPage('article-short', { type: 'article', title: 's', compiled_truth: 'too short' });
|
|
await engine.putPage('article-dream', {
|
|
type: 'article', title: 'd', compiled_truth: BODY,
|
|
frontmatter: { dream_generated: 'true' },
|
|
});
|
|
expect(await countExtractAtomsBacklog(engine)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('computeExtractAtomsBacklogCheck (issue #1678)', () => {
|
|
it('OK with no backlog', async () => {
|
|
const check = await withEnv({ GBRAIN_HOME: EMPTY_HOME }, () =>
|
|
computeExtractAtomsBacklogCheck(engine));
|
|
expect(check.status).toBe('ok');
|
|
expect((check.details as { backlog: number }).backlog).toBe(0);
|
|
});
|
|
|
|
it('WARNs with a --drain hint when the pack does not run the phase and backlog > 10', async () => {
|
|
for (let i = 0; i < 11; i++) await seedArticle(`article-${i}`);
|
|
const check = await withEnv({ GBRAIN_HOME: EMPTY_HOME }, () =>
|
|
computeExtractAtomsBacklogCheck(engine));
|
|
expect(check.status).toBe('warn');
|
|
expect(check.message).toContain('--drain');
|
|
expect((check.details as { pack_declares_phase: boolean }).pack_declares_phase).toBe(false);
|
|
expect((check.details as { known_approximation: string }).known_approximation).toContain('page backlog only');
|
|
});
|
|
});
|