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