From d4b02b68c114915407759b61af7306c7ae6d1296 Mon Sep 17 00:00:00 2001 From: izhangzhihao <12044174+izhangzhihao@users.noreply.github.com> Date: Mon, 26 Oct 2026 08:41:30 +0000 Subject: [PATCH 1/2] fix(dreamer): honor task runtime limits and improve batching Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/dreamer-mapper-step-cap-pi.test.ts | 27 +++---- packages/pi-plugin/src/dreamer/index.test.ts | 26 ++++++- packages/pi-plugin/src/dreamer/index.ts | 14 +--- .../pi-plugin/src/subagent-runner.test.ts | 10 +-- packages/pi-plugin/src/subagent-runner.ts | 2 +- .../src/agent-registration-drift.test.ts | 4 +- .../src/agents/hidden-agent-registrations.ts | 13 ++-- .../dreamer/verify-batches.test.ts | 73 ++++++++++--------- .../features/magic-context/dreamer/verify.ts | 18 ++--- 9 files changed, 99 insertions(+), 88 deletions(-) diff --git a/packages/e2e-tests/tests/dreamer-mapper-step-cap-pi.test.ts b/packages/e2e-tests/tests/dreamer-mapper-step-cap-pi.test.ts index b2e9212670..1db13a70ef 100644 --- a/packages/e2e-tests/tests/dreamer-mapper-step-cap-pi.test.ts +++ b/packages/e2e-tests/tests/dreamer-mapper-step-cap-pi.test.ts @@ -17,7 +17,7 @@ import { resolvePiHostInvocation } from "../src/pi-runner/spawn"; import { verifyPromptIds } from "./dreamer-timeout-support"; test.skipIf(process.env.MC_E2E_MAPPER_STEP_PROBE !== "1")( - "real Pi verify-broad banks a partial 20-memory manifest before the mapper step cap", + "real Pi verify-broad completes a 70-read batch under the mapper step cap", async () => { const root = realpathSync(tmpdir()); expect(root).toContain("/magic-context/mapper-step-cap/"); @@ -71,18 +71,16 @@ test.skipIf(process.env.MC_E2E_MAPPER_STEP_PROBE !== "1")( const finalize = JSON.stringify(body).includes("You're out of token budget"); if (finalize) { finalizes++; - expect(batch).toHaveLength(20); - return { text: `${batch.slice(0, 2).map((id) => ``).join("")}`, usage: { input_tokens: 10, output_tokens: 30 } }; + expect(batch).toHaveLength(10); + return { text: `${batch.map((id) => ``).join("")}`, usage: { input_tokens: 10, output_tokens: 30 } }; } - // The first batch needs four serial lookups per memory (80 calls), - // exceeding 60. A finalize request banks only the two checked claims. - if (batch.length === 20 && calls < 80) { + expect(batch).toHaveLength(10); + // Seventy serial source lookups exceed the former 60-step cap but + // should finish below the mapper's new 400-step limit. + if (calls < 70) { calls++; return { content: [{ type: "tool_use", id: `toolu_lookup_${calls}`, name: "read", input: { path: "fact.txt" } }], stop_reason: "tool_use", usage: { input_tokens: 10, output_tokens: 10 } }; } - // The next batch proves the omitted eighteen claims remain resumable. - expect(batch).toHaveLength(18); - expect(getMemoryVerifications(db, ids).get(ids[0]!)?.verifiedAt).toBeGreaterThan(1000); return { text: `${batch.map((id) => ``).join("")}`, usage: { input_tokens: 10, output_tokens: 30 } }; }); const config = DreamerConfigSchema.parse({ @@ -94,14 +92,11 @@ test.skipIf(process.env.MC_E2E_MAPPER_STEP_PROBE !== "1")( registerPiDreamerProject({ db, projectDir: work, projectIdentity: identity, registrationOwner: owner, config, harness: "pi", embeddingConfig: { provider: "off" }, memoryEnabled: true, gitCommitIndexing: { enabled: false, since_days: 30, max_commits: 200 } }); const result = await runPiDreamForProject(identity, "verify-broad", owner); console.log(`mapper-step-cap Pi ${JSON.stringify({ calls, finalizes, result, runs: getDreamRuns(db, identity) })}`); - expect(finalizes).toBe(1); - expect(calls).toBeGreaterThanOrEqual(58); - expect(calls).toBeLessThan(60); + expect(finalizes).toBe(0); + expect(calls).toBe(70); expect(result.failed).toEqual([]); - expect(ids.filter((id) => (getMemoryVerifications(db, ids).get(id)?.verifiedAt ?? 0) > 1000)).toHaveLength(2); - expect(JSON.parse(getDreamRuns(db, identity)[0]!.tasks_json)[0]).toMatchObject({ status: "completed", tokenBudget: { finalizeFired: true, banked: 2 } }); - await runPiDreamForProject(identity, "verify-broad", owner); - expect(ids.filter((id) => (getMemoryVerifications(db, ids).get(id)?.verifiedAt ?? 0) > 1000)).toHaveLength(20); + const stored = getMemoryVerifications(db, ids); + expect(ids.filter((id) => (stored.get(id)?.verifiedAt ?? 0) > 1000)).toHaveLength(20); expect(JSON.stringify(getDreamRuns(db, identity))).not.toContain("MC-D10"); } finally { __test.reset(); diff --git a/packages/pi-plugin/src/dreamer/index.test.ts b/packages/pi-plugin/src/dreamer/index.test.ts index b41772baba..19cec7096e 100644 --- a/packages/pi-plugin/src/dreamer/index.test.ts +++ b/packages/pi-plugin/src/dreamer/index.test.ts @@ -1160,14 +1160,27 @@ describe("Pi dreamer wiring", () => { db = createDb(); const attemptedModels: Array = []; const attemptedThinkingLevels: Array = []; + const forwardedRunOptions: Array<{ + fallbackModels?: unknown; + timeoutMs?: number; + }> = []; __test.setStartDreamScheduleTimerFactory(async () => mock(() => {})); __test.setPiSubagentRunnerFactory( () => ({ run: mock( - async (args: { model?: string; thinkingLevel?: string }) => { + async (args: { + model?: string; + thinkingLevel?: string; + fallbackModels?: unknown; + timeoutMs?: number; + }) => { attemptedModels.push(args.model); attemptedThinkingLevels.push(args.thinkingLevel); + forwardedRunOptions.push({ + fallbackModels: args.fallbackModels, + timeoutMs: args.timeoutMs, + }); return { ok: true, assistantText: @@ -1202,6 +1215,7 @@ describe("Pi dreamer wiring", () => { fallback_models: [ { model: "fallback/curator", thinking_level: "low" }, ], + timeout_minutes: 35, }, }, }, @@ -1217,6 +1231,16 @@ describe("Pi dreamer wiring", () => { expect(attemptedModels).toEqual(["primary/curator", "fallback/curator"]); expect(attemptedThinkingLevels).toEqual(["high", "low"]); + expect(forwardedRunOptions).toHaveLength(2); + for (const { fallbackModels, timeoutMs } of forwardedRunOptions) { + expect(timeoutMs).toBe(35 * 60 * 1000); + expect(fallbackModels).toEqual([ + expect.objectContaining({ + model: "fallback/curator", + qualifier: "low", + }), + ]); + } expect(result.failed).toEqual([]); expect(result.ran).toEqual(["curate"]); }); diff --git a/packages/pi-plugin/src/dreamer/index.ts b/packages/pi-plugin/src/dreamer/index.ts index e9a05d4ff7..054e9d893b 100644 --- a/packages/pi-plugin/src/dreamer/index.ts +++ b/packages/pi-plugin/src/dreamer/index.ts @@ -592,13 +592,6 @@ function createPiDreamerClient( const userMessage = extractUserMessage(args); const systemPrompt = extractSystemPrompt(args); - // Per-task model override (Dreamer v2): the SHARED executor - // (promptSyncWithValidatedOutputRetry) owns fallback iteration — it - // rewrites body.model to each candidate (per-task model, then the - // per-task fallback chain) and calls this facade once per attempt. So - // we use body.model as the current attempt's model and pass - // fallbackModels: undefined; passing the dreamer-level chain here would - // double-iterate and override a task's own (possibly empty) chain. const perTaskModel = extractBodyModel(args); const requestedAgent = extractBodyAgent(args) ?? "magic-context-dreamer"; const accountingTask = accountingTaskFromTitle(dreamSession.title); @@ -627,11 +620,8 @@ function createPiDreamerClient( systemPrompt, userMessage, model: perTaskModel, - fallbackModels: undefined, - // The executor enforces the per-task timeout via its abort signal; - // give the subprocess a generous ceiling so the signal is the - // authority (not a second, conflicting wall-clock here). - timeoutMs: 30 * 60 * 1000, + fallbackModels: taskConfig?.fallbackModels, + timeoutMs: (taskConfig?.timeoutMinutes ?? 30) * 60 * 1000, cwd: dreamSession.directory, signal: args.signal ? AbortSignal.any([args.signal, controller.signal]) diff --git a/packages/pi-plugin/src/subagent-runner.test.ts b/packages/pi-plugin/src/subagent-runner.test.ts index d885542af9..f19b7bc7c6 100644 --- a/packages/pi-plugin/src/subagent-runner.test.ts +++ b/packages/pi-plugin/src/subagent-runner.test.ts @@ -1580,8 +1580,8 @@ describe("PiSubagentRunner spawn lifecycle", () => { process.env.XDG_DATA_HOME = testDataDir; closeDatabase(); try { - const model = "unknown-provider/model-not-installed"; - const suggestion = "unknown-provider/model-available"; + const model = "missing-provider/model-not-installed"; + const suggestion = "missing-provider/model-available"; const resultPromise = runner.run({ ...baseOptions, model, @@ -1612,7 +1612,7 @@ describe("PiSubagentRunner spawn lifecycle", () => { ); expect(row).toMatchObject({ harness: targetHarness, - providerId: "unknown-provider", + providerId: "missing-provider", modelId: "model-not-installed", status: "empty", }); @@ -4486,7 +4486,7 @@ describe("Pi dreamer prompt-token budget", () => { it("finalizes the mapper before its step cap and retains a partial manifest below token budget", async () => { const { child, run } = await started(3_000_000); - for (let step = 1; step <= 58; step++) + for (let step = 1; step <= 398; step++) child.writeStdoutLine({ type: "message_end", message: { @@ -4511,7 +4511,7 @@ describe("Pi dreamer prompt-token budget", () => { child.emitClose(); expect(await run).toMatchObject({ ok: true, - meta: { tokenBudget: { spent: 59, finalizeFired: true } }, + meta: { tokenBudget: { spent: 399, finalizeFired: true } }, }); expect(steered).toBe(true); expect(child.stdinText.match(/dreamer-finalize/g)).toHaveLength(1); diff --git a/packages/pi-plugin/src/subagent-runner.ts b/packages/pi-plugin/src/subagent-runner.ts index a4c435788b..18b584a14b 100644 --- a/packages/pi-plugin/src/subagent-runner.ts +++ b/packages/pi-plugin/src/subagent-runner.ts @@ -1999,7 +1999,7 @@ export class PiSubagentRunner implements SubagentRunner { rpcBudget, ); // Reserve two turns: one for a queued investigation/refusal and - // one for the manifest. Keep the 60-step runaway ceiling intact. + // one for the manifest. Keep the configured step cap as the runaway ceiling. if ( decision === "continue" && !finished && diff --git a/packages/plugin/src/agent-registration-drift.test.ts b/packages/plugin/src/agent-registration-drift.test.ts index ebde6911aa..36a6802de1 100644 --- a/packages/plugin/src/agent-registration-drift.test.ts +++ b/packages/plugin/src/agent-registration-drift.test.ts @@ -344,7 +344,7 @@ describe("hidden-agent registration drift guard", () => { expect(tools).not.toContain(denied); } expect(byId(DREAMER_DOCS_AGENT)?.lockPermissions).toBe(true); - expect(byId(DREAMER_DOCS_AGENT)?.maxSteps).toBe(60); + expect(byId(DREAMER_DOCS_AGENT)?.maxSteps).toBe(400); }); test("dreamer-reviewer is a zero-tool locked JSON reviewer", () => { @@ -362,7 +362,7 @@ describe("hidden-agent registration drift guard", () => { expect(tools).not.toContain(denied); } expect(byId(DREAMER_MEMORY_MAPPER_AGENT)?.lockPermissions).toBe(true); - expect(byId(DREAMER_MEMORY_MAPPER_AGENT)?.maxSteps).toBe(60); + expect(byId(DREAMER_MEMORY_MAPPER_AGENT)?.maxSteps).toBe(400); }); test("retrospective inline allow-list is ctx_search only", () => { diff --git a/packages/plugin/src/agents/hidden-agent-registrations.ts b/packages/plugin/src/agents/hidden-agent-registrations.ts index 104b873259..81d5c8f064 100644 --- a/packages/plugin/src/agents/hidden-agent-registrations.ts +++ b/packages/plugin/src/agents/hidden-agent-registrations.ts @@ -126,9 +126,9 @@ export function buildHiddenAgentRegistrations(args: { // Documentation proposals may inspect source files but cannot mutate them. // Keep this list synchronized with DREAMER_DOCS_ALLOWED_TOOLS and locked against overrides. allowedTools: ["read", "grep", "glob", "aft_outline", "aft_zoom", "aft_search"], - // Documentation investigation reads the repository tree in a bounded loop, - // unlike whole-memory-pool tasks with a 150-step limit. - maxSteps: 60, + // Documentation investigations can span many files in larger repositories. + // Keep the read-only proposal loop bounded while allowing it to finish. + maxSteps: 400, overrides: args.dreamerOverrides, // Lock so a user override can't add the memory surface back. lockPermissions: true, @@ -203,10 +203,9 @@ export function buildHiddenAgentRegistrations(args: { // ctx_search (these tasks check against local code, not cross-session // recall). allowedTools: ["read", "grep", "glob", "aft_outline", "aft_zoom", "aft_search"], - // Bounded read-only-lookup budget (mirrors the primer investigator) — - // NOT the dreamer's 150. A map/verify batch is a targeted file-lookup - // pass, and this bounds the per-batch cost of an unsupervised run. - maxSteps: 60, + // A map/verify batch can require many file lookups across larger repositories. + // Keep the investigation bounded while allowing complex batches to finish. + maxSteps: 400, overrides: args.dreamerOverrides, // A user dreamer `permission`/`tools` override must never re-grant the // denied write/bash/ctx_memory surface to this unsupervised agent. diff --git a/packages/plugin/src/features/magic-context/dreamer/verify-batches.test.ts b/packages/plugin/src/features/magic-context/dreamer/verify-batches.test.ts index 1a73061d20..5ed3cfecc9 100644 --- a/packages/plugin/src/features/magic-context/dreamer/verify-batches.test.ts +++ b/packages/plugin/src/features/magic-context/dreamer/verify-batches.test.ts @@ -16,6 +16,7 @@ import { acquireLease } from "./lease"; import { createDreamTokenBudget, DreamTokenBudgetExceeded } from "./token-budget"; import { runVerify, + VERIFY_BATCH_SIZE, VERIFY_IN_FLIGHT_GRACE_MS, VERIFY_RUN_SLICE_MS, VERIFY_TOKENS_PER_MEMORY, @@ -86,10 +87,11 @@ function toolOnlyHistory(whitespace = false) { } test("verify allocates at most 100k prompt tokens per input including a short batch", () => { + expect(VERIFY_BATCH_SIZE).toBe(10); expect(VERIFY_TOKENS_PER_MEMORY).toBe(100_000); - expect(verifyBatchTokenBudget(5, 2_500_000)).toBe(500_000); + expect(verifyBatchTokenBudget(10, 2_500_000)).toBe(1_000_000); expect(verifyBatchTokenBudget(1)).toBe(100_000); - expect(verifyBatchTokenBudget(5, 40_000)).toBe(40_000); + expect(verifyBatchTokenBudget(10, 40_000)).toBe(40_000); const guard = createDreamTokenBudget(verifyBatchTokenBudget(1)); expect(guard.charge(80_000, 0, 0)).toBe("finalize"); expect(guard.charge(20_000, 0, 0)).toBe("stop"); @@ -149,7 +151,7 @@ test("verify prefilters identifier candidates asynchronously without replacing b } }); -test("fake provider emits five-memory progress and banks an interrupted verdict at the wall slice", async () => { +test("fake provider emits ten-memory progress and banks an interrupted verdict at the wall slice", async () => { const f = fixture(12); const sizes: number[] = []; const budgets: number[] = []; @@ -195,7 +197,7 @@ test("fake provider emits five-memory progress and banks an interrupted verdict ].map((m) => Number(m[1])); sizes.push(ids.length); text = `${ids - .slice(0, child === 1 ? 5 : 1) + .slice(0, child === 1 ? 10 : 1) .map((id) => ``) .join("")}${child === 1 ? "" : ""}`; if (child > 1) await new Promise(() => {}); @@ -208,20 +210,20 @@ test("fake provider emits five-memory progress and banks an interrupted verdict close: async () => {}, } as never, }); - expect(sizes).toEqual([5, 5]); - expect(budgets).toEqual([500_000, 500_000]); + expect(sizes).toEqual([10, 2]); + expect(budgets).toEqual([1_000_000, 200_000]); expect(calls).toBe(2); - expect(progress).toEqual([5, 6]); + expect(progress).toEqual([10, 11]); expect(result).toMatchObject({ - verified: 6, - remaining: 6, + verified: 11, + remaining: 1, complete: false, stopReason: "batch-timeout", }); const stored = getMemoryVerifications(f.db, f.ids); - expect([...stored.values()].filter((m) => m.verifiedAt > 1_000)).toHaveLength(6); + expect([...stored.values()].filter((m) => m.verifiedAt > 1_000)).toHaveLength(11); console.log( - `FAKE VERIFY: batch sizes=${sizes}; progress=${progress}; stop=${result.stopReason}; durable=6; fallback not started; settlement_tail_ms=${Date.now() - sliceFiredAt}`, + `FAKE VERIFY: batch sizes=${sizes}; progress=${progress}; stop=${result.stopReason}; durable=11; fallback not started; settlement_tail_ms=${Date.now() - sliceFiredAt}`, ); } finally { timer.mockRestore(); @@ -340,20 +342,20 @@ async function runOnFakeClock(count: number, durations: number[]) { test("verify lets a batch in flight at the run deadline finish and bank its verdicts", async () => { // The second batch starts 200 s into the run and needs 90 s, so it is still // running when the 240 s run deadline passes. - const run = await runOnFakeClock(10, [200_000, 90_000]); + const run = await runOnFakeClock(20, [200_000, 90_000]); expect(run.opened).toEqual([0, 200_000]); expect(run.abortedAt).toEqual([]); - expect(run.result).toMatchObject({ verified: 10, remaining: 0, complete: true, batches: 2 }); + expect(run.result).toMatchObject({ verified: 20, remaining: 0, complete: true, batches: 2 }); expect(run.result.stopReason).toBeUndefined(); }); test("verify does not start a batch once the run deadline has passed", async () => { // The second batch ends exactly at the run deadline; plenty of in-flight // grace remains, but that grace is only for batches already running. - const run = await runOnFakeClock(15, [120_000, VERIFY_RUN_SLICE_MS - 120_000]); + const run = await runOnFakeClock(25, [120_000, VERIFY_RUN_SLICE_MS - 120_000]); expect(run.opened).toEqual([0, 120_000]); expect(run.result).toMatchObject({ - verified: 10, + verified: 20, remaining: 5, complete: false, batches: 2, @@ -362,7 +364,7 @@ test("verify does not start a batch once the run deadline has passed", async () }); test("verify still aborts a runaway batch at the hard stop after the run deadline", async () => { - const run = await runOnFakeClock(10, [200_000, Number.POSITIVE_INFINITY]); + const run = await runOnFakeClock(20, [200_000, Number.POSITIVE_INFINITY]); const hardStop = VERIFY_RUN_SLICE_MS + VERIFY_IN_FLIGHT_GRACE_MS; expect(run.opened).toEqual([0, 200_000]); // The runaway child moves the clock in 10 s steps, so the slice timer fires @@ -370,8 +372,8 @@ test("verify still aborts a runaway batch at the hard stop after the run deadlin expect(run.abortedAt).toHaveLength(1); expect(run.abortedAt[0]).toBeLessThanOrEqual(hardStop + 10_000); expect(run.result).toMatchObject({ - verified: 5, - remaining: 5, + verified: 10, + remaining: 10, complete: false, stopReason: "batch-timeout", }); @@ -422,10 +424,10 @@ test("fixed replay fixture compares provider-reported tokens per verified memory } as never, }); const before = replayCost(20, 20); - expect(sizes).toEqual([5, 5, 5, 5]); + expect(sizes).toEqual([10, 10]); expect(result.verified).toBe(20); expect(before).toBe(1_952_000); - expect(spent).toBe(608_000); + expect(spent).toBe(1_054_000); console.log( `FIXED TRACE: before=${before / 20} tokens/verified memory; after=${spent / result.verified}; verified=20/20 (synthetic usage, not live quota)`, ); @@ -434,8 +436,8 @@ test("fixed replay fixture compares provider-reported tokens per verified memory } }); -test("verify shares the run token ceiling across five children and records each child's spend", async () => { - const f = fixture(30); +test("verify shares the run token ceiling across batches and records each child's spend", async () => { + const f = fixture(60); const budgets: number[] = []; const updates: Array<{ sessionId?: string; spent: number }> = []; let text = ""; @@ -470,11 +472,11 @@ test("verify shares the run token ceiling across five children and records each close: async () => {}, } as never, }); - expect(budgets).toEqual([500_000, 500_000, 500_000, 500_000, 500_000]); - expect(result).toMatchObject({ verified: 25, remaining: 5, stopReason: "token-budget" }); + expect(budgets).toEqual([1_000_000, 1_000_000, 1_000_000, 1_000_000, 500_000]); + expect(result).toMatchObject({ verified: 50, remaining: 10, stopReason: "token-budget" }); expect(new Set(updates.map((s) => s.sessionId)).size).toBe(5); console.log( - "FAKE RUN CAP: 5 children x 500000 = 2500000 prompt tokens; no sixth child; 25 durable verdicts", + "FAKE RUN CAP: five 500000-token spends = 2500000 prompt tokens; no sixth batch; 50 durable verdicts", ); } finally { f.db.close(); @@ -490,6 +492,7 @@ test("a failing fake provider exhausts one 500k envelope without retrying and ba try { const result = await runVerify({ ...f.args, + tokenBudget: 500_000, fallbackModels: ["fixture/fallback"], hiddenCompletionExecutor: { capabilities: { tools: true }, @@ -561,7 +564,7 @@ test("verify run slice is shared across batches, not renewed after progress", as } as never, }); expect(children).toBe(1); - expect(result).toMatchObject({ verified: 5, remaining: 20, stopReason: "deadline" }); + expect(result).toMatchObject({ verified: 10, remaining: 15, stopReason: "deadline" }); } finally { now.mockRestore(); f.db.close(); @@ -782,7 +785,7 @@ test("fixed navigation fixture measures exact replay bytes for whole reads versu close: async () => {}, } as never, }); - expect(result).toMatchObject({ verified: 20, complete: true, batches: 4 }); + expect(result).toMatchObject({ verified: 20, complete: true, batches: 2 }); return { traces, sourceBytes: Buffer.byteLength(source), @@ -800,20 +803,20 @@ test("fixed navigation fixture measures exact replay bytes for whole reads versu const whole = await measure("whole-file"); const navigation = await measure("navigation"); expect(whole.traces.map((trace) => trace.toolCalls)).toEqual( - Array.from({ length: 4 }, () => ["read"]), + Array.from({ length: 2 }, () => ["read"]), ); expect(navigation.traces.map((trace) => trace.toolCalls)).toEqual( - Array.from({ length: 4 }, () => ["aft_outline", "aft_zoom"]), + Array.from({ length: 2 }, () => ["aft_outline", "aft_zoom"]), ); expect(navigation.traces.map((trace) => [trace.systemBytes, trace.userBytes])).toEqual( whole.traces.map((trace) => [trace.systemBytes, trace.userBytes]), ); - expect(whole.totalBytes).toBe(69_970); - expect(navigation.totalBytes).toBe(62_699); - expect(whole.tokens).toBe(17_496); - expect(navigation.tokens).toBe(15_679); - expect(whole.traces.map((trace) => trace.systemBytes)).toEqual([3_448, 3_448, 3_448, 3_448]); - expect(whole.traces.map((trace) => trace.userBytes)).toEqual([1_002, 1_003, 1_012, 1_012]); + expect(whole.totalBytes).toBe(36_666); + expect(navigation.totalBytes).toBe(33_871); + expect(whole.tokens).toBe(9_168); + expect(navigation.tokens).toBe(8_470); + expect(whole.traces.map((trace) => trace.systemBytes)).toEqual([3_448, 3_448]); + expect(whole.traces.map((trace) => trace.userBytes)).toEqual([1_398, 1_417]); expect(navigation.tokens).toBeLessThan(whole.tokens); console.log( `NAVIGATION FIXTURE: ${JSON.stringify({ whole, navigation, tokensPerVerdict: { whole: whole.tokens / whole.verified, navigation: navigation.tokens / navigation.verified } })}`, diff --git a/packages/plugin/src/features/magic-context/dreamer/verify.ts b/packages/plugin/src/features/magic-context/dreamer/verify.ts index a61a0a4aa7..9ffb23375f 100644 --- a/packages/plugin/src/features/magic-context/dreamer/verify.ts +++ b/packages/plugin/src/features/magic-context/dreamer/verify.ts @@ -76,8 +76,8 @@ import { * and apply the agent's verified/update/archive manifest HOST-side. * * Per-memory verified_at (no global watermark): a timed-out batch banks what it - * checked; the next run continues. Cost is unique-file-bounded like map, but - * verify uses small batches because deeper reads replay more prompt tokens. + * checked; the next run continues. Cost is unique-file-bounded like map, and + * ten-memory batches amortize child startup while per-input budgets bound replay. * Closed individual verdicts survive interruption; unfinished claims stay pending. * * Apply is cache-NEUTRAL: update/archive route through queueMemoryMutation, @@ -85,7 +85,7 @@ import { * must never bust the prompt cache. */ -export const VERIFY_BATCH_SIZE = 5; +export const VERIFY_BATCH_SIZE = 10; // Completed nightly runs spent 22,103,259 prompt tokens for 295 banked verdicts // (~74,926 each). Round up to 100k per input, with no spare allocation for a short batch. // All memories checked in one verifier child session share its batch token budget; @@ -100,12 +100,12 @@ export const VERIFY_RUN_SLICE_MS = 240_000; /** * Extra time a batch that is already running at the run deadline may take to * finish. Cutting such a batch off throws away the tokens it already spent, and - * finishing it costs little more. On 2026-10-20 live data, five-memory batches - * finished in 64 s at the median, 90 s at p90 and 96 s at worst. Batches under - * the 500k token envelope finished in 103 s at p95. The slowest full batch - * consumed about 4.6k prompt tokens per second, so its token budget runs out - * after roughly 110 s. 120 s therefore covers nearly every batch that can still - * finish, and the slice timer stops a runaway batch at deadline + 120 s. + * finishing it costs little more. Historical five-memory batches (500k tokens) + * finished in 64 s at the median, 90 s at p90 and 96 s at worst; batches under + * that envelope finished in 103 s at p95. At the observed 4.6k prompt tokens per + * second, a ten-memory batch's 1M token envelope takes about 217 s to exhaust. + * The 240 s batch slice bounds its runtime; the grace lets an in-flight batch + * continue past the run deadline, up to that limit. */ export const VERIFY_IN_FLIGHT_GRACE_MS = 120_000; From 9e060ed35040e8914bc4cdc12fc0f91074715ce9 Mon Sep 17 00:00:00 2001 From: Zhihao Zhang <12044174+izhangzhihao@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:56:14 +0800 Subject: [PATCH 2/2] Update packages/pi-plugin/src/dreamer/index.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- packages/pi-plugin/src/dreamer/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pi-plugin/src/dreamer/index.ts b/packages/pi-plugin/src/dreamer/index.ts index 054e9d893b..97ca77e386 100644 --- a/packages/pi-plugin/src/dreamer/index.ts +++ b/packages/pi-plugin/src/dreamer/index.ts @@ -620,7 +620,7 @@ function createPiDreamerClient( systemPrompt, userMessage, model: perTaskModel, - fallbackModels: taskConfig?.fallbackModels, + fallbackModels: undefined, timeoutMs: (taskConfig?.timeoutMinutes ?? 30) * 60 * 1000, cwd: dreamSession.directory, signal: args.signal