mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +00:00
Four verified-open issues in the subagent-orchestration family, one PR: - #1594: dream synthesize subagent job/wait timeouts promoted from hardcoded 30/35-min constants to config keys dream.synthesize.subagent_timeout_ms / subagent_wait_timeout_ms. Approach ported from stale PR #1596 (credit @ai920wisco). - #2778: add_timeline_entry joins the subagent brain-tool allowlist, fenced fail-closed by the shared enforceSubagentSlugFence (extracted from put_page's inline check — same trusted-workspace allow-list / wiki/agents/<id>/ namespace policy). The per-turn output cap is now resolveMaxOutputTokens (data.max_tokens → agent.max_output_tokens → 8192, was hardcoded 4096); a max_tokens stop surfaces as stop_reason 'max_tokens' instead of a silent end_turn, and a mid-tool-round cap hit injects a truncation note so the model re-issues the dropped call. - #2782: patterns phase status now reflects the child outcome — non-complete outcome with zero writes → fail (PATTERNS_CHILD_<OUTCOME>), partial writes → warn. Patterns timeouts get the same config-key pair (dream.patterns.subagent_timeout_ms / subagent_wait_timeout_ms). - #2113: facts extraction cap is config facts.extraction_max_tokens (default 4000, was hardcoded 1500); stopReason 'length' is checked, retried once at 2x the cap, and surfaced on stderr instead of silently extracting zero facts. Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local> Co-authored-by: ai920wisco <ai920wisco@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
/**
|
|
* #1594 — dream synthesize subagent timeouts are config keys, not hardcoded
|
|
* 30/35-minute constants. Approach ported from PR #1596 (@ai920wisco).
|
|
*/
|
|
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
|
import { createHash } from 'node:crypto';
|
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
|
import { runPhaseSynthesize } from '../src/core/cycle/synthesize.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
let schemaVersion: string;
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({ database_url: '' });
|
|
await engine.initSchema();
|
|
// resetPgliteState truncates `config`, wiping the `version` row that
|
|
// MinionQueue.ensureSchema checks. Capture it so beforeEach can restore.
|
|
schemaVersion = (await engine.getConfig('version')) ?? '7';
|
|
}, 60_000);
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await resetPgliteState(engine);
|
|
await engine.setConfig('version', schemaVersion);
|
|
});
|
|
|
|
async function seedWorthProcessingVerdict(
|
|
filePath: string,
|
|
content: string,
|
|
): Promise<void> {
|
|
const contentHash = createHash('sha256').update(content, 'utf8').digest('hex');
|
|
await engine.putDreamVerdict(filePath, contentHash, {
|
|
worth_processing: true,
|
|
reasons: ['seeded for timeout config test'],
|
|
});
|
|
}
|
|
|
|
describe('runPhaseSynthesize subagent timeout config', () => {
|
|
test('dream.synthesize.subagent_timeout_ms flows to submitted subagent job', async () => {
|
|
const brainDir = mkdtempSync(join(tmpdir(), 'gbrain-synth-timeout-brain-'));
|
|
const corpusDir = mkdtempSync(join(tmpdir(), 'gbrain-synth-timeout-corpus-'));
|
|
|
|
try {
|
|
await engine.setConfig('dream.synthesize.enabled', 'true');
|
|
await engine.setConfig('dream.synthesize.session_corpus_dir', corpusDir);
|
|
await engine.setConfig('dream.synthesize.subagent_timeout_ms', '600000');
|
|
await engine.setConfig('dream.synthesize.subagent_wait_timeout_ms', '1');
|
|
|
|
const filePath = join(corpusDir, '2026-05-28-dense-transcript.txt');
|
|
const content = 'dense transcript line\n'.repeat(250);
|
|
writeFileSync(filePath, content);
|
|
await seedWorthProcessingVerdict(filePath, content);
|
|
|
|
const result = await runPhaseSynthesize(engine, {
|
|
brainDir,
|
|
dryRun: false,
|
|
});
|
|
|
|
expect(result.status).toBe('ok');
|
|
|
|
const jobs = await engine.executeRaw<{ timeout_ms: string | number | null }>(
|
|
`SELECT timeout_ms
|
|
FROM minion_jobs
|
|
WHERE name = 'subagent'
|
|
ORDER BY id DESC
|
|
LIMIT 1`,
|
|
);
|
|
expect(jobs).toHaveLength(1);
|
|
expect(Number(jobs[0]!.timeout_ms)).toBe(600000);
|
|
} finally {
|
|
rmSync(brainDir, { recursive: true, force: true });
|
|
rmSync(corpusDir, { recursive: true, force: true });
|
|
}
|
|
}, 30_000);
|
|
});
|