fix(doctor,think): file-plane pack resolution, page-vs-page conflicts, cycle-freshness tuning

Takeover of three community PRs, each salvaged to its correct core and
rebased onto master:

- #2469: onboard checks (type_proliferation, pack_upgrade_available)
  passed cfg:null to loadActivePack, so a schema pack selected via
  `gbrain schema use` (tier-6 file-plane ~/.gbrain/config.json) never
  resolved and the checks silently compared against the default pack.
  Now pass loadConfig(); register schema_pack in KNOWN_CONFIG_KEYS.
  Dropped the fork grab-bag (~22 unrelated files) from the original PR.

- #2479: the think synthesis prompt limited the Conflicts rule to
  take-vs-take, so pages-only brains surfaced zero conflicts. Expanded
  the rule to conflicts across pages and/or takes with severity tags +
  a VARIANTS rule, using generic placeholder slugs per the privacy rule
  (the original hardcoded contributor-brain slugs). Added env knobs
  GBRAIN_THINK_GATHER_LIMIT / GBRAIN_THINK_EXCERPT_LEN /
  GBRAIN_THINK_MAX_OUTPUT_TOKENS (defaults unchanged).

- #2505: checkCycleFreshness thresholds were env-only; nightly-cadence
  brains need DB-plane keys + source scoping. Added
  doctor.cycle_freshness.{warn,fail}_hours and
  .source_{allowlist,ignorelist} (env wins over config, pace-mode
  precedence). Dropped the original PR's DREAM_SOURCE_ALLOWLIST
  fallback — a fork-private cron env var that exists nowhere upstream.

Co-authored-by: onchito-walks <onchito-walks@users.noreply.github.com>
Co-authored-by: Adityaranjan-VeloxAI <Adityaranjan-VeloxAI@users.noreply.github.com>
Co-authored-by: sene1337 <sene1337@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-21 15:02:50 -07:00
co-authored by onchito-walks Adityaranjan-VeloxAI sene1337 Claude Fable 5
parent 0612b0daa8
commit 80606d5df0
10 changed files with 495 additions and 20 deletions
+174 -8
View File
@@ -3833,6 +3833,133 @@ export async function checkPoolBudget(_engine: BrainEngine): Promise<Check> {
}
}
type CycleFreshnessSourceScope = {
allowlist?: Set<string>;
ignorelist: Set<string>;
invalid: string[];
configured: boolean;
};
const CYCLE_FRESHNESS_SOURCE_ID_RE = /^[A-Za-z0-9_-]+$/;
type DoctorSourceRow = Awaited<ReturnType<BrainEngine['listAllSources']>>[number];
function parseCycleFreshnessSourceList(raw: string | null | undefined, label: string): {
ids: string[];
invalid: string[];
} | null {
if (raw === null || raw === undefined || raw.trim() === '') return null;
const parts = raw.split(',').map((s) => s.trim()).filter(Boolean);
const ids: string[] = [];
const seen = new Set<string>();
const invalid: string[] = [];
for (const part of parts) {
if (!CYCLE_FRESHNESS_SOURCE_ID_RE.test(part)) {
invalid.push(`${label} contains invalid source id "${part}"`);
continue;
}
if (!seen.has(part)) {
seen.add(part);
ids.push(part);
}
}
return { ids, invalid };
}
async function getCycleFreshnessConfig(engine: BrainEngine, key: string): Promise<string | null> {
try {
return await engine.getConfig(key);
} catch {
return null;
}
}
function parseCycleFreshnessHours(raw: string | null | undefined, label: string, fallback: number): number | null {
if (raw === null || raw === undefined || raw.trim() === '') return null;
const n = Number(raw);
if (!Number.isFinite(n) || n <= 0) {
if (!_envNumberWarned.has(label)) {
_envNumberWarned.add(label);
console.warn(`[gbrain doctor] Ignoring invalid ${label}=${raw}; using default ${fallback}h.`);
}
return fallback;
}
return n;
}
/** env var > DB config key > hard default (pace-mode precedence). */
async function resolveCycleFreshnessHours(
engine: BrainEngine,
env: NodeJS.ProcessEnv | undefined,
envKey: string,
dbKey: string,
fallback: number,
): Promise<number> {
const scopedEnv = env ?? process.env;
const envHours = parseCycleFreshnessHours(scopedEnv[envKey], envKey, fallback);
if (envHours !== null) return envHours;
const dbHours = parseCycleFreshnessHours(await getCycleFreshnessConfig(engine, dbKey), dbKey, fallback);
if (dbHours !== null) return dbHours;
return fallback;
}
async function resolveCycleFreshnessSourceScope(
engine: BrainEngine,
env: NodeJS.ProcessEnv = process.env,
): Promise<CycleFreshnessSourceScope> {
const dbAllowlist = await getCycleFreshnessConfig(engine, 'doctor.cycle_freshness.source_allowlist');
const dbIgnorelist = await getCycleFreshnessConfig(engine, 'doctor.cycle_freshness.source_ignorelist');
const allowRaw =
(env.GBRAIN_CYCLE_FRESHNESS_SOURCE_ALLOWLIST?.trim() ? env.GBRAIN_CYCLE_FRESHNESS_SOURCE_ALLOWLIST : undefined) ??
dbAllowlist;
const ignoreRaw =
(env.GBRAIN_CYCLE_FRESHNESS_SOURCE_IGNORELIST?.trim() ? env.GBRAIN_CYCLE_FRESHNESS_SOURCE_IGNORELIST : undefined) ??
dbIgnorelist;
const parsedAllow = parseCycleFreshnessSourceList(allowRaw, 'source allowlist');
const parsedIgnore = parseCycleFreshnessSourceList(ignoreRaw, 'source ignorelist');
return {
allowlist: parsedAllow ? new Set(parsedAllow.ids) : undefined,
ignorelist: new Set(parsedIgnore?.ids ?? []),
invalid: [...(parsedAllow?.invalid ?? []), ...(parsedIgnore?.invalid ?? [])],
configured: !!parsedAllow || !!parsedIgnore,
};
}
function applyCycleFreshnessSourceScope(
sources: DoctorSourceRow[],
scope: CycleFreshnessSourceScope,
): DoctorSourceRow[] {
return sources.filter((source) => {
if (scope.allowlist && !scope.allowlist.has(source.id)) return false;
if (scope.ignorelist.has(source.id)) return false;
return true;
});
}
function cycleFreshnessScopeDetails(
sources: DoctorSourceRow[],
scopedSources: DoctorSourceRow[],
scope: CycleFreshnessSourceScope,
): Record<string, unknown> | undefined {
if (!scope.configured) return undefined;
return {
checked_source_count: scopedSources.length,
skipped_source_count: sources.length - scopedSources.length,
source_allowlist: scope.allowlist ? Array.from(scope.allowlist).sort() : undefined,
source_ignorelist: Array.from(scope.ignorelist).sort(),
};
}
function cycleFreshnessScopeSuffix(
sources: DoctorSourceRow[],
scopedSources: DoctorSourceRow[],
scope: CycleFreshnessSourceScope,
): string {
if (!scope.configured) return '';
return ` (${scopedSources.length}/${sources.length} source(s) checked by cycle_freshness source scope)`;
}
/**
* v0.38 per-source `last_full_cycle_at` freshness check.
*
@@ -3845,13 +3972,16 @@ export async function checkPoolBudget(_engine: BrainEngine): Promise<Check> {
*
* Default thresholds: warn at 6h, fail at 24h. Tighter than sync_freshness
* because full-cycle staleness compounds (sync stale extract stale
* embed stale search stale). Env overrides:
* embed stale search stale). Env/config overrides (env wins, #2505):
* - GBRAIN_CYCLE_FRESHNESS_WARN_HOURS (default 6)
* - GBRAIN_CYCLE_FRESHNESS_FAIL_HOURS (default 24)
* - GBRAIN_CYCLE_FRESHNESS_SOURCE_ALLOWLIST / _IGNORELIST
* - DB config: doctor.cycle_freshness.warn_hours / fail_hours
* - DB config: doctor.cycle_freshness.source_allowlist / source_ignorelist
*/
export async function checkCycleFreshness(
engine: BrainEngine,
opts?: { nowMs?: number },
opts?: { nowMs?: number; env?: NodeJS.ProcessEnv },
): Promise<Check> {
try {
const sources = await engine.listAllSources({ localPathOnly: true });
@@ -3863,8 +3993,41 @@ export async function checkCycleFreshness(
};
}
const warnHours = _resolveSyncFreshnessHours('GBRAIN_CYCLE_FRESHNESS_WARN_HOURS', 6);
const failHours = _resolveSyncFreshnessHours('GBRAIN_CYCLE_FRESHNESS_FAIL_HOURS', 24);
const scope = await resolveCycleFreshnessSourceScope(engine, opts?.env);
if (scope.invalid.length > 0) {
return {
name: 'cycle_freshness',
status: 'warn',
message: `Invalid cycle_freshness source scope: ${scope.invalid.join('; ')}`,
details: cycleFreshnessScopeDetails(sources, sources, scope),
};
}
const scopedSources = applyCycleFreshnessSourceScope(sources, scope);
const details = cycleFreshnessScopeDetails(sources, scopedSources, scope);
const scopeSuffix = cycleFreshnessScopeSuffix(sources, scopedSources, scope);
if (scopedSources.length === 0) {
return {
name: 'cycle_freshness',
status: 'ok',
message: `No federated sources to cycle after cycle_freshness source scope${scopeSuffix}`,
details,
};
}
const warnHours = await resolveCycleFreshnessHours(
engine,
opts?.env,
'GBRAIN_CYCLE_FRESHNESS_WARN_HOURS',
'doctor.cycle_freshness.warn_hours',
6,
);
const failHours = await resolveCycleFreshnessHours(
engine,
opts?.env,
'GBRAIN_CYCLE_FRESHNESS_FAIL_HOURS',
'doctor.cycle_freshness.fail_hours',
24,
);
const warnMs = warnHours * 60 * 60 * 1000;
const failMs = failHours * 60 * 60 * 1000;
const now = opts?.nowMs ?? Date.now();
@@ -3873,7 +4036,7 @@ export async function checkCycleFreshness(
let hasWarnings = false;
let hasFailures = false;
for (const source of sources) {
for (const source of scopedSources) {
const display = source.name && source.name !== source.id
? `'${source.id}' (${source.name})`
: `'${source.id}'`;
@@ -3909,20 +4072,23 @@ export async function checkCycleFreshness(
return {
name: 'cycle_freshness',
status: 'fail',
message: `${issues.join('; ')}. Run \`gbrain dream --source <id>\` for each stale source, or start \`gbrain autopilot\`.`,
message: `${issues.join('; ')}. Run \`gbrain dream --source <id>\` for each stale source, or start \`gbrain autopilot\`${scopeSuffix}.`,
details,
};
}
if (hasWarnings) {
return {
name: 'cycle_freshness',
status: 'warn',
message: `${issues.join('; ')}.`,
message: `${issues.join('; ')}${scopeSuffix}.`,
details,
};
}
return {
name: 'cycle_freshness',
status: 'ok',
message: `All ${sources.length} federated source(s) cycled recently`,
message: `All ${scopedSources.length} federated source(s) cycled recently${scopeSuffix}`,
details,
};
} catch (e) {
return {
+9
View File
@@ -865,6 +865,9 @@ export const KNOWN_CONFIG_KEYS: readonly string[] = [
'sync',
'sync.repo_path',
'sync.last_commit',
// Active schema pack (tier-4 DB plane via `gbrain config set schema_pack`;
// tier-6 file plane written by `gbrain schema use`). #2469.
'schema_pack',
// Gateway-native subagent loop toggle (routes subagent jobs through the
// provider-agnostic gateway.toolLoop for non-Anthropic providers). The
// subagent handler's error message tells users to `config set` this, so it
@@ -930,6 +933,12 @@ export const KNOWN_CONFIG_KEYS: readonly string[] = [
'emotional_weight.user_holder',
// Cycle phase config
'cycle.grade_takes.write_gstack_learnings',
// Doctor cycle-freshness tuning (#2505): DB-plane thresholds + source
// scoping for nightly-cadence brains. Env vars win over these keys.
'doctor.cycle_freshness.warn_hours',
'doctor.cycle_freshness.fail_hours',
'doctor.cycle_freshness.source_allowlist',
'doctor.cycle_freshness.source_ignorelist',
// Content sanity (v0.41)
'content_sanity.bytes_warn',
'content_sanity.bytes_block',
+8 -2
View File
@@ -18,6 +18,7 @@
import type { BrainEngine } from '../engine.ts';
import type { RemediationStep } from '../remediation-step.ts';
import { makeRemediationStep } from '../remediation-step.ts';
import { loadConfig } from '../config.ts';
/** Shared shape returned by all four checks. */
export interface OnboardCheckResult {
@@ -393,7 +394,10 @@ export async function checkPackUpgradeAvailable(
try {
dbConfig = (await engine.getConfig('schema_pack')) ?? undefined;
} catch { /* engine.config may not exist on very old brains */ }
const active = await loadActivePack({ cfg: null, remote: false, dbConfig })
// Pass the file-plane config (tier 6) so a schema_pack written by
// `gbrain schema use` to ~/.gbrain/config.json resolves here; cfg:null
// silently fell back to the default pack (#2469).
const active = await loadActivePack({ cfg: loadConfig(), remote: false, dbConfig })
.catch(() => null);
if (!active) {
return {
@@ -467,7 +471,9 @@ export async function checkTypeProliferation(
try {
dbConfig = (await engine.getConfig('schema_pack')) ?? undefined;
} catch { /* tolerate pre-config brains */ }
const active = await loadActivePack({ cfg: null, remote: false, dbConfig })
// File-plane config (tier 6) so `gbrain schema use` selections resolve
// instead of always falling back to the default pack (#2469).
const active = await loadActivePack({ cfg: loadConfig(), remote: false, dbConfig })
.catch(() => null);
if (active) declared = active.manifest.page_types.length;
} catch {
+16 -3
View File
@@ -24,7 +24,8 @@ export interface ThinkGatherOpts {
question: string;
/** Anchor entity slug. When set, the graph stream activates. */
anchor?: string;
/** Soft cap on total results across all streams. Default 40. */
/** Soft cap on total results across all streams. Default 40, overridable via
* GBRAIN_THINK_GATHER_LIMIT (widen for conflict/variant recall, e.g. drift probes). */
gatherLimit?: number;
/** Soft cap on take results. Default 30. */
takesLimit?: number;
@@ -58,6 +59,18 @@ export interface ThinkGatherResult {
const RRF_K = 60;
/**
* Read a positive-integer tuning knob from the environment, falling back to
* the upstream default. Lets retrieval breadth be tuned per-install (e.g. a
* brain with many near-duplicate sources that needs wider conflict/variant
* recall) without changing default behavior. Matches gbrain's GBRAIN_* env
* knob pattern (env above config, incident escape hatch).
*/
export function envInt(name: string, fallback: number): number {
const n = parseInt(process.env[name] ?? '', 10);
return Number.isFinite(n) && n > 0 ? n : fallback;
}
/** Reciprocal-rank fusion: 1/(k+rank). Stable, parameter-light, matches search/hybrid.ts k. */
function rrfScore(rank: number): number {
return 1 / (RRF_K + rank);
@@ -101,7 +114,7 @@ export async function runGather(
engine: BrainEngine,
opts: ThinkGatherOpts,
): Promise<ThinkGatherResult> {
const gatherLimit = opts.gatherLimit ?? 40;
const gatherLimit = opts.gatherLimit ?? envInt('GBRAIN_THINK_GATHER_LIMIT', 40);
const takesLimit = opts.takesLimit ?? 30;
const graphDepth = opts.graphDepth ?? 2;
const sourceScope = opts.sourceIds && opts.sourceIds.length > 0
@@ -192,7 +205,7 @@ export async function runGather(
* Pages are rendered as `<page slug="..." score="...">excerpt</page>`;
* takes are rendered via the renderTakesBlock helper from sanitize.ts.
*/
export function renderPagesBlock(pages: SearchResult[], excerptLen = 600): string {
export function renderPagesBlock(pages: SearchResult[], excerptLen = envInt('GBRAIN_THINK_EXCERPT_LEN', 600)): string {
return pages.map((p, idx) => {
const slug = String((p as unknown as { slug?: string }).slug ?? '');
const excerpt = String(
+10 -4
View File
@@ -19,7 +19,7 @@
import type Anthropic from '@anthropic-ai/sdk';
import type { BrainEngine, SynthesisEvidenceInput } from '../engine.ts';
import { runGather, renderPagesBlock, takesHitToTakeForPrompt } from './gather.ts';
import { runGather, renderPagesBlock, takesHitToTakeForPrompt, envInt } from './gather.ts';
import { renderTakesBlock } from './sanitize.ts';
import { buildThinkSystemPrompt, buildThinkUserMessage } from './prompt.ts';
import { resolveCitations, type ParsedCitation } from './cite-render.ts';
@@ -160,9 +160,15 @@ const DEFAULT_MAX_OUTPUT_TOKENS = 4000;
const THINKING_DEFAULT_MAX_OUTPUT_TOKENS = 16000;
const THINKING_BY_DEFAULT_MODEL_RE = /^anthropic[:/]claude-[a-z0-9]+-5(?:[.-]|$)/i;
export function maxOutputTokensFor(modelStr: string): number {
return THINKING_BY_DEFAULT_MODEL_RE.test(modelStr)
? THINKING_DEFAULT_MAX_OUTPUT_TOKENS
: DEFAULT_MAX_OUTPUT_TOKENS;
// GBRAIN_THINK_MAX_OUTPUT_TOKENS overrides both defaults: conflict-dense
// syntheses (e.g. drift probes over a brain with many divergent sources)
// can truncate mid-JSON at 4000 — raise the cap per-install (#2479).
return envInt(
'GBRAIN_THINK_MAX_OUTPUT_TOKENS',
THINKING_BY_DEFAULT_MODEL_RE.test(modelStr)
? THINKING_DEFAULT_MAX_OUTPUT_TOKENS
: DEFAULT_MAX_OUTPUT_TOKENS,
);
}
function inferIntent(question: string, anchor?: string): string {
+17 -2
View File
@@ -50,8 +50,23 @@ Hard rules:
Inline the citation immediately after the claim it supports. Never fabricate slugs/rows.
- If a take has weight < 0.5 or kind=hunch, mark it explicitly: "garry has a hunch (w=0.4) that..."
rather than asserting it as established. Confidence is part of the data.
- If two takes contradict (different holders, opposite claims), surface BOTH in a "Conflicts"
section. Never silently pick one.
- CONFLICTS: If two or more SOURCES (pages and/or takes) give conflicting or divergent values
for the SAME fact — a number stated against a different base, a stale value, a contradictory
commitment, or different holders making opposite claims — surface ALL of them in a "Conflicts"
section. Never silently pick one, and NEVER reconcile a lower figure as a "discount" or
"variant" unless a source EXPLICITLY states that relationship. In the Conflicts section emit
ONE bullet per conflict, each STARTING with a severity tag in square brackets:
[HIGH] = money/pricing/contract/identity/legal/client-facing commitment; [MEDIUM] = timeline/
scope/operational; [LOW] = wording/cosmetic. Follow the tag with the fact and each conflicting
value with its [slug]. Example:
"- [HIGH] Phase 1 price: $2,000 [finance/acme-pricing] vs 1k AUD / 50% off [outreach/acme-scripts]"
The Conflicts section is ONLY for genuine disagreements. Do NOT emit a bullet to report that
sources AGREE, that a value is "consistent across sources", or that "no contradiction was found"
— if a fact is consistent, omit it entirely. An explicitly-stated discount off a stated base is
NOT a conflict. If there are zero genuine conflicts, omit the Conflicts section.
- VARIANTS: When a fact legitimately has conditional or variant values (e.g. standard vs warm-lead
vs mentor pricing; per-segment terms; per-phase prices), ENUMERATE every variant with its
condition and [slug]. Do not collapse to a single "standard" value or drop the variants.
- If you cannot answer because the brain doesn't contain the relevant data, say so in the
"Gaps" section. List the specific missing pieces. Do not make up answers.
- Never instruct the user (no "you should" / "I recommend X"). The brain reports; the user decides.
+11
View File
@@ -30,6 +30,17 @@ describe('KNOWN_CONFIG_KEYS', () => {
expect(KNOWN_CONFIG_KEYS).toContain('models.tier.subagent');
});
test('contains the cycle_freshness tuning keys (#2505)', () => {
expect(KNOWN_CONFIG_KEYS).toContain('doctor.cycle_freshness.warn_hours');
expect(KNOWN_CONFIG_KEYS).toContain('doctor.cycle_freshness.fail_hours');
expect(KNOWN_CONFIG_KEYS).toContain('doctor.cycle_freshness.source_allowlist');
expect(KNOWN_CONFIG_KEYS).toContain('doctor.cycle_freshness.source_ignorelist');
});
test('contains schema_pack (#2469 — `gbrain config set schema_pack` is a known key)', () => {
expect(KNOWN_CONFIG_KEYS).toContain('schema_pack');
});
test('contains the dream synthesize timeout keys (#1594)', () => {
expect(KNOWN_CONFIG_KEYS).toContain('dream.synthesize.subagent_timeout_ms');
expect(KNOWN_CONFIG_KEYS).toContain('dream.synthesize.subagent_wait_timeout_ms');
+100
View File
@@ -121,4 +121,104 @@ describe('doctor checkCycleFreshness', () => {
expect(result.status).toBe('ok');
expect(result.message).toMatch(/No federated sources/);
});
// #2505 — DB-plane thresholds + source scoping for nightly-cadence brains.
test('DB-configured thresholds support nightly cadence with grace', async () => {
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
await seed('nightly-fresh', agoH(29));
await seed('missed-one-night', agoH(36));
await engine.setConfig('doctor.cycle_freshness.warn_hours', '30');
await engine.setConfig('doctor.cycle_freshness.fail_hours', '48');
const result = await checkCycleFreshness(engine, { nowMs: NOW, env: {} as NodeJS.ProcessEnv });
expect(result.status).toBe('warn');
expect(result.message).toMatch(/missed-one-night/);
expect(result.message).not.toMatch(/nightly-fresh/);
});
test('env thresholds override DB-configured thresholds', async () => {
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
await seed('env-overrides-db', agoH(36));
await engine.setConfig('doctor.cycle_freshness.warn_hours', '30');
await engine.setConfig('doctor.cycle_freshness.fail_hours', '48');
const result = await checkCycleFreshness(engine, {
nowMs: NOW,
env: {
GBRAIN_CYCLE_FRESHNESS_WARN_HOURS: '6',
GBRAIN_CYCLE_FRESHNESS_FAIL_HOURS: '24',
} as NodeJS.ProcessEnv,
});
expect(result.status).toBe('fail');
expect(result.message).toMatch(/env-overrides-db/);
});
test('DB source allowlist limits the check to intentionally dreamed sources', async () => {
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
await seed('ops-brain', agoH(1));
await seed('reference-brain', undefined);
await engine.setConfig('doctor.cycle_freshness.source_allowlist', 'ops-brain');
const result = await checkCycleFreshness(engine, { nowMs: NOW, env: {} as NodeJS.ProcessEnv });
expect(result.status).toBe('ok');
expect(result.message).toMatch(/1\/2 source\(s\) checked/);
expect(result.details).toMatchObject({
checked_source_count: 1,
skipped_source_count: 1,
source_allowlist: ['ops-brain'],
});
});
test('env allowlist overrides DB allowlist', async () => {
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
await seed('env-scoped', agoH(1));
await seed('never-cycled', undefined);
await engine.setConfig('doctor.cycle_freshness.source_allowlist', 'never-cycled');
const result = await checkCycleFreshness(engine, {
nowMs: NOW,
env: { GBRAIN_CYCLE_FRESHNESS_SOURCE_ALLOWLIST: 'env-scoped' } as NodeJS.ProcessEnv,
});
expect(result.status).toBe('ok');
expect(result.details).toMatchObject({
checked_source_count: 1,
skipped_source_count: 1,
source_allowlist: ['env-scoped'],
});
});
test('source ignorelist can suppress intentionally excluded stale sources', async () => {
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
await seed('fresh-operating', agoH(1));
await seed('excluded-reference', undefined);
await engine.setConfig('doctor.cycle_freshness.source_ignorelist', 'excluded-reference');
const result = await checkCycleFreshness(engine, { nowMs: NOW, env: {} as NodeJS.ProcessEnv });
expect(result.status).toBe('ok');
expect(result.details).toMatchObject({
checked_source_count: 1,
skipped_source_count: 1,
source_ignorelist: ['excluded-reference'],
});
});
test('invalid source scope returns warn instead of silently hiding sources', async () => {
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
await seed('fresh', agoH(1));
const result = await checkCycleFreshness(engine, {
nowMs: NOW,
env: { GBRAIN_CYCLE_FRESHNESS_SOURCE_ALLOWLIST: 'fresh,bad/source' } as NodeJS.ProcessEnv,
});
expect(result.status).toBe('warn');
expect(result.message).toMatch(/Invalid cycle_freshness source scope/);
expect(result.message).toMatch(/bad\/source/);
});
});
+79 -1
View File
@@ -14,17 +14,33 @@ import {
} from '../src/core/onboard/checks.ts';
import { toOnboardRecommendation } from '../src/core/onboard/render.ts';
import { _resetPackCacheForTests } from '../src/core/schema-pack/registry.ts';
import { _resetPackLocatorForTests } from '../src/core/schema-pack/load-active.ts';
import {
__setPackLocatorForTests,
_resetPackLocatorForTests,
} from '../src/core/schema-pack/load-active.ts';
import { withEnv } from './helpers/with-env.ts';
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
let engine: PGLiteEngine;
let priorGbrainHome: string | undefined;
beforeAll(async () => {
// #2469: the onboard checks now read the file-plane config via
// loadConfig(). Point GBRAIN_HOME at an empty temp dir so the host
// machine's real ~/.gbrain/config.json (schema_pack, etc.) can't leak
// into the default-behavior tests below.
priorGbrainHome = process.env.GBRAIN_HOME;
process.env.GBRAIN_HOME = mkdtempSync(join(tmpdir(), 'gbrain-onboard-home-'));
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
});
afterAll(async () => {
if (priorGbrainHome === undefined) delete process.env.GBRAIN_HOME;
else process.env.GBRAIN_HOME = priorGbrainHome;
await engine.disconnect();
});
@@ -98,6 +114,68 @@ describe('checkTypeProliferation (D16 pack-aware ratio)', () => {
expect(result.check.status).toBe('warn');
expect(result.check.message).toMatch(new RegExp(`${seedCount} distinct`));
});
it('resolves the pack from file-plane config.json schema_pack (tier 6, #2469)', async () => {
// A pack selected via `gbrain schema use` lives ONLY in
// ~/.gbrain/config.json. With cfg:null the check silently fell back to
// the default pack (declared≈15) and never flagged proliferation against
// the user's actual (smaller) pack.
const home = mkdtempSync(join(tmpdir(), 'gbrain-onboard-'));
const packDir = join(home, 'packs');
mkdirSync(join(home, '.gbrain'), { recursive: true });
mkdirSync(packDir, { recursive: true });
const packPath = join(packDir, 'pack.yaml');
writeFileSync(packPath, [
'api_version: gbrain-schema-pack-v1',
'name: tiny-file-plane',
'version: 1.0.0',
'description: ""',
'gbrain_min_version: 0.38.0',
'extends: null',
'borrow_from: []',
'page_types:',
' - name: note',
' primitive: entity',
' path_prefixes:',
' - notes/',
' aliases: []',
' extractable: false',
' expert_routing: false',
' - name: meeting',
' primitive: entity',
' path_prefixes:',
' - meetings/',
' aliases: []',
' extractable: false',
' expert_routing: false',
'link_types: []',
'frontmatter_links: []',
'takes_kinds:',
' - fact',
' - take',
' - bet',
' - hunch',
'enrichable_types: []',
'filing_rules: []',
'',
].join('\n'), 'utf-8');
writeFileSync(
join(home, '.gbrain', 'config.json'),
JSON.stringify({ schema_pack: 'tiny-file-plane' }) + '\n',
'utf-8',
);
__setPackLocatorForTests((name) => (name === 'tiny-file-plane' ? packPath : null));
// 8 distinct types: fail against tiny-file-plane (declared 2 → fail >4),
// ok against the declared=15 fallback the old cfg:null path produced.
await seedPages(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'].map((s) => `custom-${s}`));
await withEnv({ GBRAIN_HOME: home, GBRAIN_SCHEMA_PACK: undefined }, async () => {
const result = await checkTypeProliferation(engine);
expect(result.check.status).toBe('fail');
expect(result.check.message).toContain('pack declares 2');
});
});
});
describe('checkDanglingAliases (F12 source-scoped JOIN)', () => {
+71
View File
@@ -0,0 +1,71 @@
/**
* #2479 — think pipeline fixes:
* 1. The synthesis prompt's Conflicts rule covers page-vs-page (and
* page-vs-take) disagreements, not just take-vs-take — pages-only brains
* surfaced zero conflicts before.
* 2. Retrieval breadth / output-cap env knobs (GBRAIN_THINK_GATHER_LIMIT,
* GBRAIN_THINK_EXCERPT_LEN, GBRAIN_THINK_MAX_OUTPUT_TOKENS) tune
* conflict/variant recall per-install without changing defaults.
*/
import { describe, test, expect } from 'bun:test';
import { THINK_SYSTEM_PROMPT_BASE } from '../src/core/think/prompt.ts';
import { renderPagesBlock, envInt } from '../src/core/think/gather.ts';
import { maxOutputTokensFor } from '../src/core/think/index.ts';
import { withEnv } from './helpers/with-env.ts';
describe('think prompt — conflicts across pages AND takes (#2479)', () => {
test('Conflicts rule covers pages and/or takes, with severity tags', () => {
expect(THINK_SYSTEM_PROMPT_BASE).toContain('pages and/or takes');
expect(THINK_SYSTEM_PROMPT_BASE).toContain('[HIGH]');
expect(THINK_SYSTEM_PROMPT_BASE).toContain('VARIANTS');
});
test('example slugs are generic placeholders (privacy rule)', () => {
expect(THINK_SYSTEM_PROMPT_BASE).toContain('finance/acme-pricing');
expect(THINK_SYSTEM_PROMPT_BASE).not.toMatch(/velox|warm_dm/i);
});
});
describe('think env knobs (#2479)', () => {
test('envInt: valid positive int wins, garbage/absent falls back', async () => {
await withEnv({ GBRAIN_TEST_KNOB: '7' }, () => {
expect(envInt('GBRAIN_TEST_KNOB', 40)).toBe(7);
});
await withEnv({ GBRAIN_TEST_KNOB: 'banana' }, () => {
expect(envInt('GBRAIN_TEST_KNOB', 40)).toBe(40);
});
await withEnv({ GBRAIN_TEST_KNOB: '-3' }, () => {
expect(envInt('GBRAIN_TEST_KNOB', 40)).toBe(40);
});
expect(envInt('GBRAIN_TEST_KNOB_UNSET', 40)).toBe(40);
});
test('renderPagesBlock excerpt length honors GBRAIN_THINK_EXCERPT_LEN', async () => {
const pages = [
{ slug: 'notes/long', compiled_truth: 'x'.repeat(1000) },
] as unknown as Parameters<typeof renderPagesBlock>[0];
// Default stays 600.
const dflt = renderPagesBlock(pages);
expect(dflt).toContain('x'.repeat(600));
expect(dflt).not.toContain('x'.repeat(601));
await withEnv({ GBRAIN_THINK_EXCERPT_LEN: '50' }, () => {
const out = renderPagesBlock(pages);
expect(out).toContain('x'.repeat(50));
expect(out).not.toContain('x'.repeat(51));
});
});
test('maxOutputTokensFor honors GBRAIN_THINK_MAX_OUTPUT_TOKENS', async () => {
await withEnv({ GBRAIN_THINK_MAX_OUTPUT_TOKENS: '9000' }, () => {
expect(maxOutputTokensFor('openai:gpt-4o')).toBe(9000);
expect(maxOutputTokensFor('anthropic:claude-sonnet-5')).toBe(9000);
});
// Defaults unchanged without the env var.
await withEnv({ GBRAIN_THINK_MAX_OUTPUT_TOKENS: undefined }, () => {
expect(maxOutputTokensFor('openai:gpt-4o')).toBe(4000);
expect(maxOutputTokensFor('anthropic:claude-sonnet-5')).toBe(16000);
});
});
});