mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
fix(dream): honor a configured 0 in synthesize + auto_think config resolution (stop coercing to the default) (#3552)
Co-Authored-By: Time Attakc <89218912+time-attack@users.noreply.github.com>
This commit is contained in:
committed by
Sina Matian
co-authored by
Time Attakc
parent
69be8bb707
commit
241603aab8
@@ -56,8 +56,6 @@ async function loadConfig(engine: BrainEngine): Promise<AutoThinkConfig> {
|
||||
const enabledStr = await engine.getConfig('dream.auto_think.enabled');
|
||||
const questionsStr = await engine.getConfig('dream.auto_think.questions');
|
||||
const maxPerStr = await engine.getConfig('dream.auto_think.max_per_cycle');
|
||||
const budgetStr = await engine.getConfig('dream.auto_think.budget');
|
||||
const cooldownStr = await engine.getConfig('dream.auto_think.cooldown_days');
|
||||
const autoCommitStr = await engine.getConfig('dream.auto_think.auto_commit');
|
||||
|
||||
let questions: string[] = [];
|
||||
@@ -68,16 +66,30 @@ async function loadConfig(engine: BrainEngine): Promise<AutoThinkConfig> {
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// getNumberConfig (not `parse* || N`) so a configured 0 is honored — a bare
|
||||
// `|| N` coerces an explicit 0 back to the default (budget 0 = "spend nothing",
|
||||
// cooldown 0 = "no cooldown"). max_per_cycle stays inline: its Math.max(1, ...)
|
||||
// floor already makes 0 invalid there, so no configured value is lost.
|
||||
const budgetUsd = Math.max(0, await getNumberConfig(engine, 'dream.auto_think.budget', 2.0));
|
||||
const cooldownDays = Math.max(0, await getNumberConfig(engine, 'dream.auto_think.cooldown_days', 30));
|
||||
|
||||
return {
|
||||
enabled: enabledStr === 'true',
|
||||
questions,
|
||||
maxPerCycle: maxPerStr ? Math.max(1, parseInt(maxPerStr, 10) || 5) : 5,
|
||||
budgetUsd: budgetStr ? Math.max(0, parseFloat(budgetStr) || 2.0) : 2.0,
|
||||
cooldownDays: cooldownStr ? Math.max(0, parseInt(cooldownStr, 10) || 30) : 30,
|
||||
budgetUsd,
|
||||
cooldownDays,
|
||||
autoCommit: autoCommitStr === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
async function getNumberConfig(engine: BrainEngine, key: string, fallback: number): Promise<number> {
|
||||
const raw = await engine.getConfig(key);
|
||||
if (raw === undefined || raw === null) return fallback;
|
||||
const value = Number(raw);
|
||||
return Number.isNaN(value) ? fallback : value;
|
||||
}
|
||||
|
||||
async function isCoolingDown(engine: BrainEngine, days: number): Promise<boolean> {
|
||||
if (days <= 0) return false;
|
||||
const last = await engine.getConfig('dream.auto_think.last_completion_ts');
|
||||
@@ -201,3 +213,6 @@ export async function runPhaseAutoThink(
|
||||
duration_ms: Date.now() - start,
|
||||
};
|
||||
}
|
||||
|
||||
// Test-only export: pin config-resolution behavior at function granularity.
|
||||
export const __testing = { loadConfig };
|
||||
|
||||
@@ -804,7 +804,6 @@ async function loadSynthConfig(engine: BrainEngine): Promise<SynthConfig> {
|
||||
// Explicit enabled=false still wins for pausing synthesis without removing corpus config.
|
||||
const enabled = enabledRaw === 'false' ? false : (enabledRaw === 'true' || !!corpusDir);
|
||||
const meetingTranscriptsDir = await engine.getConfig('dream.synthesize.meeting_transcripts_dir');
|
||||
const minCharsStr = await engine.getConfig('dream.synthesize.min_chars');
|
||||
const excludeStr = await engine.getConfig('dream.synthesize.exclude_patterns');
|
||||
// v0.28: resolveModel() unifies CLI flag > new key > deprecated key > models.default > env > fallback
|
||||
const { resolveModel } = await import('../model-config.ts');
|
||||
@@ -820,7 +819,10 @@ async function loadSynthConfig(engine: BrainEngine): Promise<SynthConfig> {
|
||||
tier: 'utility',
|
||||
fallback: 'haiku',
|
||||
});
|
||||
const cooldownHoursStr = await engine.getConfig('dream.synthesize.cooldown_hours');
|
||||
// getNumberConfig (not `parseInt(str, 10) || N`) so a configured 0 is honored — a bare
|
||||
// `|| N` coerces an explicit 0 back to the default (cooldown 0 = "no cooldown").
|
||||
const cooldownHours = Math.max(0, await getNumberConfig(engine, 'dream.synthesize.cooldown_hours', 12));
|
||||
const minChars = Math.max(0, await getNumberConfig(engine, 'dream.synthesize.min_chars', 2000));
|
||||
const maxPromptTokensStr = await engine.getConfig('dream.synthesize.max_prompt_tokens');
|
||||
const maxChunksStr = await engine.getConfig('dream.synthesize.max_chunks_per_transcript');
|
||||
const subagentTimeoutMs = await getNumberConfig(
|
||||
@@ -863,11 +865,11 @@ async function loadSynthConfig(engine: BrainEngine): Promise<SynthConfig> {
|
||||
enabled,
|
||||
corpusDir: corpusDir ?? null,
|
||||
meetingTranscriptsDir: meetingTranscriptsDir ?? null,
|
||||
minChars: minCharsStr ? Math.max(0, parseInt(minCharsStr, 10) || 2000) : 2000,
|
||||
minChars,
|
||||
excludePatterns,
|
||||
model,
|
||||
verdictModel,
|
||||
cooldownHours: cooldownHoursStr ? Math.max(0, parseInt(cooldownHoursStr, 10) || 12) : 12,
|
||||
cooldownHours,
|
||||
maxPromptTokens,
|
||||
maxChunksPerTranscript,
|
||||
outputRoot: await loadOutputRoot(engine),
|
||||
@@ -1599,4 +1601,5 @@ export const __testing = {
|
||||
stampDreamProvenance,
|
||||
reverseWriteRefs,
|
||||
runPgliteSubagentsInline,
|
||||
loadSynthConfig,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { __testing } from '../src/core/cycle/auto-think.ts';
|
||||
import type { BrainEngine } from '../src/core/engine.ts';
|
||||
|
||||
// A configured 0 must survive config resolution. The pre-fix
|
||||
// `parse*(str) || <default>` coerced an explicit "0" back to the default
|
||||
// (budget 0 = "spend nothing", cooldown 0 = "no cooldown"); loadConfig now
|
||||
// routes budget + cooldown_days through getNumberConfig, which honors 0.
|
||||
function stubEngine(config: Record<string, string>): BrainEngine {
|
||||
return { getConfig: async (key: string) => config[key] ?? null } as unknown as BrainEngine;
|
||||
}
|
||||
|
||||
describe('auto_think loadConfig honors a configured 0', () => {
|
||||
test('budget = "0" resolves 0, not $2', async () => {
|
||||
const cfg = await __testing.loadConfig(stubEngine({ 'dream.auto_think.budget': '0' }));
|
||||
expect(cfg.budgetUsd).toBe(0);
|
||||
});
|
||||
|
||||
test('cooldown_days = "0" resolves 0, not the 30d default', async () => {
|
||||
const cfg = await __testing.loadConfig(stubEngine({ 'dream.auto_think.cooldown_days': '0' }));
|
||||
expect(cfg.cooldownDays).toBe(0);
|
||||
});
|
||||
|
||||
test('absent keys keep the defaults', async () => {
|
||||
const cfg = await __testing.loadConfig(stubEngine({}));
|
||||
expect(cfg.budgetUsd).toBe(2.0);
|
||||
expect(cfg.cooldownDays).toBe(30);
|
||||
});
|
||||
|
||||
test('unparseable values fall back to the defaults', async () => {
|
||||
const cfg = await __testing.loadConfig(stubEngine({
|
||||
'dream.auto_think.budget': 'abc',
|
||||
'dream.auto_think.cooldown_days': 'xyz',
|
||||
}));
|
||||
expect(cfg.budgetUsd).toBe(2.0);
|
||||
expect(cfg.cooldownDays).toBe(30);
|
||||
});
|
||||
|
||||
test('positive values round-trip (budget accepts fractions)', async () => {
|
||||
const cfg = await __testing.loadConfig(stubEngine({
|
||||
'dream.auto_think.budget': '0.5',
|
||||
'dream.auto_think.cooldown_days': '7',
|
||||
}));
|
||||
expect(cfg.budgetUsd).toBe(0.5);
|
||||
expect(cfg.cooldownDays).toBe(7);
|
||||
});
|
||||
|
||||
test('a negative value clamps to 0', async () => {
|
||||
const cfg = await __testing.loadConfig(stubEngine({ 'dream.auto_think.budget': '-1' }));
|
||||
expect(cfg.budgetUsd).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { __testing } from '../src/core/cycle/synthesize.ts';
|
||||
import type { BrainEngine } from '../src/core/engine.ts';
|
||||
|
||||
// A configured 0 must survive config resolution. The pre-fix `parseInt(str, 10)
|
||||
// || <default>` coerced an explicit "0" back to the default (cooldown 0 = "no
|
||||
// cooldown"); loadSynthConfig now routes cooldown_hours + min_chars through
|
||||
// getNumberConfig, which honors 0. Only engine.getConfig is exercised, so a
|
||||
// stub engine is sufficient (no PGLite).
|
||||
function stubEngine(config: Record<string, string>): BrainEngine {
|
||||
return { getConfig: async (key: string) => config[key] ?? null } as unknown as BrainEngine;
|
||||
}
|
||||
|
||||
describe('loadSynthConfig honors a configured 0', () => {
|
||||
test('cooldown_hours = "0" resolves 0, not the 12h default', async () => {
|
||||
const cfg = await __testing.loadSynthConfig(stubEngine({ 'dream.synthesize.cooldown_hours': '0' }));
|
||||
expect(cfg.cooldownHours).toBe(0);
|
||||
});
|
||||
|
||||
test('min_chars = "0" resolves 0, not 2000', async () => {
|
||||
const cfg = await __testing.loadSynthConfig(stubEngine({ 'dream.synthesize.min_chars': '0' }));
|
||||
expect(cfg.minChars).toBe(0);
|
||||
});
|
||||
|
||||
test('absent keys keep the defaults', async () => {
|
||||
const cfg = await __testing.loadSynthConfig(stubEngine({}));
|
||||
expect(cfg.cooldownHours).toBe(12);
|
||||
expect(cfg.minChars).toBe(2000);
|
||||
});
|
||||
|
||||
test('unparseable values fall back to the defaults', async () => {
|
||||
const cfg = await __testing.loadSynthConfig(stubEngine({
|
||||
'dream.synthesize.cooldown_hours': 'abc',
|
||||
'dream.synthesize.min_chars': 'xyz',
|
||||
}));
|
||||
expect(cfg.cooldownHours).toBe(12);
|
||||
expect(cfg.minChars).toBe(2000);
|
||||
});
|
||||
|
||||
test('positive values round-trip', async () => {
|
||||
const cfg = await __testing.loadSynthConfig(stubEngine({
|
||||
'dream.synthesize.cooldown_hours': '6',
|
||||
'dream.synthesize.min_chars': '500',
|
||||
}));
|
||||
expect(cfg.cooldownHours).toBe(6);
|
||||
expect(cfg.minChars).toBe(500);
|
||||
});
|
||||
|
||||
test('a negative value clamps to 0', async () => {
|
||||
const cfg = await __testing.loadSynthConfig(stubEngine({ 'dream.synthesize.cooldown_hours': '-5' }));
|
||||
expect(cfg.cooldownHours).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user