Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions packages/e2e-tests/tests/dreamer-mapper-step-cap-pi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/");
Expand Down Expand Up @@ -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: `<verify>${batch.slice(0, 2).map((id) => `<verified id="${id}"/>`).join("")}</verify>`, usage: { input_tokens: 10, output_tokens: 30 } };
expect(batch).toHaveLength(10);
return { text: `<verify>${batch.map((id) => `<verified id="${id}"/>`).join("")}</verify>`, 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: `<verify>${batch.map((id) => `<verified id="${id}"/>`).join("")}</verify>`, usage: { input_tokens: 10, output_tokens: 30 } };
});
const config = DreamerConfigSchema.parse({
Expand All @@ -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();
Expand Down
26 changes: 25 additions & 1 deletion packages/pi-plugin/src/dreamer/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1160,14 +1160,27 @@ describe("Pi dreamer wiring", () => {
db = createDb();
const attemptedModels: Array<string | undefined> = [];
const attemptedThinkingLevels: Array<string | undefined> = [];
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:
Expand Down Expand Up @@ -1202,6 +1215,7 @@ describe("Pi dreamer wiring", () => {
fallback_models: [
{ model: "fallback/curator", thinking_level: "low" },
],
timeout_minutes: 35,
},
},
},
Expand All @@ -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"]);
});
Expand Down
12 changes: 1 addition & 11 deletions packages/pi-plugin/src/dreamer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -628,10 +621,7 @@ function createPiDreamerClient(
userMessage,
model: perTaskModel,
fallbackModels: undefined,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This change leaves the existing Dreamer forwarding test expecting the old nested fallback array, so the focused Pi Dreamer test now fails on both runner calls. Update that test to assert that the outer retry owns the fallback chain and the runner receives no nested fallbacks, while retaining its failure-path coverage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/pi-plugin/src/dreamer/index.ts, line 623:

<comment>This change leaves the existing Dreamer forwarding test expecting the old nested fallback array, so the focused Pi Dreamer test now fails on both runner calls. Update that test to assert that the outer retry owns the fallback chain and the runner receives no nested fallbacks, while retaining its failure-path coverage.</comment>

<file context>
@@ -620,7 +620,7 @@ function createPiDreamerClient(
 				userMessage,
 				model: perTaskModel,
-				fallbackModels: taskConfig?.fallbackModels,
+				fallbackModels: undefined,
 				timeoutMs: (taskConfig?.timeoutMinutes ?? 30) * 60 * 1000,
 				cwd: dreamSession.directory,
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Fallback test now fails

Setting fallbackModels to undefined fixes the nested retries, but shared curate validation retries Pi pseudo-tool-call text with the fallback model still expects the runner to receive a fallback array. That assertion at index.test.ts:1237 now fails.

Update it to expect undefined. Keep the model-order checks to verify that the shared executor still tries the fallback.

// 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,
timeoutMs: (taskConfig?.timeoutMinutes ?? 30) * 60 * 1000,
cwd: dreamSession.directory,
signal: args.signal
? AbortSignal.any([args.signal, controller.signal])
Expand Down
10 changes: 5 additions & 5 deletions packages/pi-plugin/src/subagent-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
});
Expand Down Expand Up @@ -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: {
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/pi-plugin/src/subagent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin/src/agent-registration-drift.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand Down
13 changes: 6 additions & 7 deletions packages/plugin/src/agents/hidden-agent-registrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This raises dreamer-docs to 400 steps, but OpenCode 2 still stops it at 60. AGENT_STEPS in v2/hooks/hidden-child.ts keeps DREAMER_DOCS_AGENT and DREAMER_MEMORY_MAPPER_AGENT at 60, and the step guard throws once steps > cap. Update those entries to 400 too, or the new cap has no effect on that harness.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/plugin/src/agents/hidden-agent-registrations.ts, line 131:

<comment>This raises `dreamer-docs` to 400 steps, but OpenCode 2 still stops it at 60. `AGENT_STEPS` in `v2/hooks/hidden-child.ts` keeps `DREAMER_DOCS_AGENT` and `DREAMER_MEMORY_MAPPER_AGENT` at 60, and the step guard throws once `steps > cap`. Update those entries to 400 too, or the new cap has no effect on that harness.</comment>

<file context>
@@ -126,9 +126,9 @@ export function buildHiddenAgentRegistrations(args: {
-            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.
</file context>

overrides: args.dreamerOverrides,
// Lock so a user override can't add the memory surface back.
lockPermissions: true,
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading