Skip to content

Commit fef6090

Browse files
fix(cli-runner): cap summary block plus marker against maxHistoryChars
ClawSweeper P2 on #83117 flagged that when `summaryRendered.length` is less than `maxHistoryChars` but `summaryBlock.length` (summary + `\n\n` separator) meets or exceeds it, the `remainingBudget <= 0` arm of `buildCliSessionHistoryPrompt` appends the truncation marker after the already-full summary block. A 199-char rendered summary under a 200-char cap produced a 257-char history block — defeating the cap that prevents reseeding fresh CLI sessions with unexpectedly huge prompts. Fix the budget edge by truncating the summary in this branch as well so `summary + separator + marker` stays within `maxHistoryChars`. The tail still drops (the summary alone consumes the budget) and the marker still leads its own line so the prompt announces what was discarded. Mirrors the existing oversize-summary branch's pattern of head-slicing the summary against an explicit budget that reserves marker + separator. Add a focused regression in `session-history.test.ts` covering exactly the gap the finding called out: `summaryRendered.length < maxHistoryChars` with a non-empty post-summary tail. Asserts the rendered history block stays within `maxHistoryChars` and the truncation marker is present.
1 parent d027bc1 commit fef6090

2 files changed

Lines changed: 49 additions & 5 deletions

File tree

src/agents/cli-runner/session-history.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,4 +696,41 @@ describe("buildCliSessionHistoryPrompt", () => {
696696
expect(prompt).toContain("POST_SUMMARY_ASSISTANT_DROPPED");
697697
expect(prompt).toContain("<next_user_message>\nnext ask\n</next_user_message>");
698698
});
699+
700+
it("honors the cap when the summary block plus marker crosses it", () => {
701+
// Edge case: `summaryRendered.length < maxHistoryChars` (the gate that
702+
// routes to the oversize-summary branch is not taken) BUT
703+
// `summaryBlock.length >= maxHistoryChars` once the `\n\n` separator
704+
// is appended, making `remainingBudget <= 0`. Without summary
705+
// truncation in that branch, the rendered history block is
706+
// `summary + separator + marker` — well over `maxHistoryChars`. A
707+
// 199-char rendered summary under a 200-char cap would otherwise
708+
// produce a 257-char history block.
709+
const maxHistoryChars = 200;
710+
// `renderHistoryMessage` prefixes "Compaction summary: " (20 chars)
711+
// before the summary text, so a 179-char summary renders to 199 chars
712+
// — strictly less than the cap, but `summaryBlock = rendered + "\n\n"`
713+
// is 201 chars and `remainingBudget` is negative.
714+
const summaryPrefix = "Compaction summary: ";
715+
const summaryText = "S".repeat(maxHistoryChars - 1 - summaryPrefix.length);
716+
const prompt = buildCliSessionHistoryPrompt({
717+
messages: [
718+
{ role: "compactionSummary", summary: summaryText },
719+
{ role: "user", content: "POST_SUMMARY_TAIL_USER" },
720+
{ role: "assistant", content: "POST_SUMMARY_TAIL_ASSISTANT" },
721+
],
722+
prompt: "next ask",
723+
maxHistoryChars,
724+
});
725+
726+
expect(prompt).toBeDefined();
727+
const historyMatch = prompt?.match(
728+
/<conversation_history>\n([\s\S]*?)\n<\/conversation_history>/,
729+
);
730+
expect(historyMatch).not.toBeNull();
731+
const renderedHistory = historyMatch?.[1] ?? "";
732+
expect(renderedHistory.length).toBeLessThanOrEqual(maxHistoryChars);
733+
// Marker is still present so the prompt announces what was discarded.
734+
expect(prompt).toContain("[OpenClaw reseed history truncated; older turns dropped]");
735+
});
699736
});

src/agents/cli-runner/session-history.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,18 @@ export function buildCliSessionHistoryPrompt(params: {
200200
const remainingBudget = maxHistoryChars - summaryBlock.length;
201201
if (remainingBudget <= 0) {
202202
// `tailRaw.slice(-0)` would return the entire tail (JS quirk:
203-
// `-0 === 0`), so guard explicitly: the summary plus its
204-
// separator already consumes the full budget. Drop the tail
205-
// entirely and lead with the marker so the prompt still announces
206-
// what was discarded.
207-
renderedHistory = `${summaryBlock}${truncationMarker}`;
203+
// `-0 === 0`), so guard explicitly. The summary plus its
204+
// separator already meets or exceeds the cap, so the tail must
205+
// drop. The summary itself must also be truncated so that
206+
// `summary + separator + marker` stays within budget — appending
207+
// the marker after a full-budget summary block would otherwise
208+
// blow past `maxHistoryChars` (a 199-char summary under a
209+
// 200-char cap would render a 257-char history block).
210+
const summaryBudget = Math.max(0, maxHistoryChars - truncationMarker.length - 2);
211+
const summaryTruncated = summaryRendered.slice(0, summaryBudget).trimEnd();
212+
renderedHistory = summaryTruncated
213+
? `${summaryTruncated}\n\n${truncationMarker}`
214+
: truncationMarker;
208215
} else if (tailRaw.length > remainingBudget) {
209216
renderedHistory = `${summaryBlock}${truncationMarker}\n${tailRaw.slice(-remainingBudget).trimStart()}`;
210217
} else {

0 commit comments

Comments
 (0)