fix(acp): plan-only streams enforce the per-turn cap; add category frontmatter
handlePlan charged the budget but never checked the ceiling or set the flag, so a plan-ONLY stream kept emitting after crossing the cap (caught by both review bots). It now flags + truncates exactly like text/thinking. Adds the plan-only flood regression test (185 total) and the category frontmatter field to the new solutions doc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -202,3 +202,29 @@ describe("plan output bounds (S5)", () => {
|
||||
expect(thinking.length).toBe(before);
|
||||
});
|
||||
});
|
||||
|
||||
it("a plan-ONLY stream stops emitting once the per-turn cap is crossed", async () => {
|
||||
const { createEventBridge, PER_CHUNK_CAP_CHARS, PER_TURN_OUTPUT_CAP_CHARS, MAX_PLAN_ENTRIES } =
|
||||
await import("../event-bridge.js");
|
||||
const thinking: string[] = [];
|
||||
const bridge = createEventBridge({ onThinking: (t) => thinking.push(t) });
|
||||
// Each plan line is bounded by PER_CHUNK_CAP_CHARS; flood plan events only.
|
||||
const bigEntry = "p".repeat(PER_CHUNK_CAP_CHARS);
|
||||
const entries = Array.from({ length: MAX_PLAN_ENTRIES }, () => ({
|
||||
content: bigEntry,
|
||||
priority: "low",
|
||||
status: "pending",
|
||||
}));
|
||||
const floods = Math.ceil(PER_TURN_OUTPUT_CAP_CHARS / PER_CHUNK_CAP_CHARS) + 3;
|
||||
for (let i = 0; i < floods; i += 1) {
|
||||
bridge.handleSessionUpdate({ sessionUpdate: "plan", entries } as never);
|
||||
}
|
||||
// The flag line is emitted exactly once, then nothing further.
|
||||
const flagged = thinking.filter((t) => t.includes("output truncated"));
|
||||
expect(flagged).toHaveLength(1);
|
||||
const after = thinking.length;
|
||||
bridge.handleSessionUpdate({ sessionUpdate: "plan", entries } as never);
|
||||
expect(thinking.length).toBe(after);
|
||||
// And the total emitted is bounded near the cap, not floods * cap.
|
||||
expect(thinking.length).toBeLessThan(floods);
|
||||
});
|
||||
|
||||
@@ -235,6 +235,16 @@ export function createEventBridge(callbacks: AcpCallbacks): EventBridge {
|
||||
// agent-controlled — without the cap below, one plan event with thousands
|
||||
// of entries bypasses the per-turn ceiling entirely.
|
||||
if (outputCapFlagged) return;
|
||||
// Enforce the ceiling on the plan path too: without this check a plan-ONLY
|
||||
// stream (no text/thinking ever entering forwardBounded) would keep
|
||||
// emitting forever after crossing the budget.
|
||||
if (cumulativeOutputChars >= PER_TURN_OUTPUT_CAP_CHARS) {
|
||||
outputCapFlagged = true;
|
||||
callbacks.onThinking?.(
|
||||
"[output truncated: per-turn limit reached — further agent output suppressed]",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const list = Array.isArray(entries) ? entries : [];
|
||||
const capped = list.slice(0, MAX_PLAN_ENTRIES);
|
||||
let line = formatPlan(capped);
|
||||
|
||||
Reference in New Issue
Block a user