























@@ -577,13 +577,163 @@ describe("buildCliSessionHistoryPrompt", () => {
577577578578it("caps rendered reseed history before adding the next user message", () => {
579579const prompt = buildCliSessionHistoryPrompt({
580-messages: [{ role: "compactionSummary", summary: "x".repeat(100) }],
580+messages: [
581+{ role: "user", content: "x".repeat(100) },
582+{ role: "assistant", content: "y".repeat(100) },
583+],
581584prompt: "current ask must survive",
582585maxHistoryChars: 20,
583586});
584587585-expect(prompt).toContain("[OpenClaw reseed history truncated]");
588+expect(prompt).toContain("[OpenClaw reseed history truncated; older turns dropped]");
586589expect(prompt).toContain("<next_user_message>\ncurrent ask must survive\n</next_user_message>");
590+// Older 100-char prefix must be dropped by the tail slice; the
591+// post-cap rendered tail is shorter than the dropped prefix.
587592expect(prompt).not.toContain("x".repeat(80));
588593});
594+595+it("keeps the most recent turns when rendered history exceeds the cap", () => {
596+// Older turns plus a final marker turn whose content is exactly what a
597+// head-slice would drop first. Asserting the marker survives in the
598+// rendered prompt locks in tail-slice semantics: a session-recovery
599+// feature must keep the latest context, not the oldest.
600+const prompt = buildCliSessionHistoryPrompt({
601+messages: [
602+{ role: "user", content: "x".repeat(8000) },
603+{ role: "assistant", content: "y".repeat(8000) },
604+{ role: "user", content: "FINAL_USER_MARKER" },
605+{ role: "assistant", content: "FINAL_ASSISTANT_MARKER" },
606+],
607+prompt: "next ask",
608+});
609+610+expect(prompt).toBeDefined();
611+expect(prompt).toContain("FINAL_USER_MARKER");
612+expect(prompt).toContain("FINAL_ASSISTANT_MARKER");
613+expect(prompt).toContain("[OpenClaw reseed history truncated; older turns dropped]");
614+// The oldest 8000-char block must have been dropped — a head-slice
615+// would have kept it instead of the recent tail.
616+expect(prompt).not.toContain("x".repeat(8000));
617+expect(prompt).toContain("<next_user_message>\nnext ask\n</next_user_message>");
618+});
619+620+it("preserves the compaction summary when the post-summary transcript exceeds the cap", () => {
621+// loadCliSessionReseedMessages places a compactionSummary entry first
622+// so the compacted prior context survives reseed. A blind tail slice
623+// of the joined history would drop that summary whenever the
624+// post-summary tail alone exceeds the cap. The structure-aware
625+// truncation pins the summary as a prefix and caps only the tail.
626+const prompt = buildCliSessionHistoryPrompt({
627+messages: [
628+{ role: "compactionSummary", summary: "COMPACTION_SUMMARY_MARKER pinned context" },
629+{ role: "user", content: "z".repeat(8000) },
630+{ role: "assistant", content: "w".repeat(8000) },
631+{ role: "user", content: "POST_SUMMARY_FINAL_USER" },
632+{ role: "assistant", content: "POST_SUMMARY_FINAL_ASSISTANT" },
633+],
634+prompt: "next ask",
635+});
636+637+expect(prompt).toBeDefined();
638+// Compaction summary must be pinned as a prefix, not sliced away.
639+expect(prompt).toContain("Compaction summary: COMPACTION_SUMMARY_MARKER pinned context");
640+// Recent tail still preserved within the post-summary budget.
641+expect(prompt).toContain("POST_SUMMARY_FINAL_USER");
642+expect(prompt).toContain("POST_SUMMARY_FINAL_ASSISTANT");
643+expect(prompt).toContain("[OpenClaw reseed history truncated; older turns dropped]");
644+// Head of post-summary tail (oldest 8000-char `z` block) must be
645+// dropped so the cap is honored.
646+expect(prompt).not.toContain("z".repeat(8000));
647+expect(prompt).toContain("<next_user_message>\nnext ask\n</next_user_message>");
648+});
649+650+it("caps oversize compaction summary while preserving recent post-summary tail", () => {
651+// Two regressions covered here:
652+// 1. `tailRaw.slice(-0)` would return the entire tail (JS quirk:
653+// `String.prototype.slice(-0) === slice(0)`), defeating the cap when
654+// the summary block consumes the budget.
655+// 2. Pinning the full summary as-is when the summary itself exceeds
656+// `maxHistoryChars` would blow past the cap that prevents
657+// reseeding fresh CLI sessions with unexpectedly huge prompts.
658+// The summary must itself be truncated to fit the budget while still
659+// preserving the recent post-summary exact turns.
660+const summaryText = "OVERSIZE_SUMMARY_MARKER ".repeat(50).trim();
661+const maxHistoryChars = 200;
662+const prompt = buildCliSessionHistoryPrompt({
663+messages: [
664+{ role: "compactionSummary", summary: summaryText },
665+{ role: "user", content: "POST_SUMMARY_USER_DROPPED" },
666+{ role: "assistant", content: "POST_SUMMARY_ASSISTANT_DROPPED" },
667+],
668+prompt: "next ask",
669+// Cap well below the rendered summary block so the summary itself
670+// must be truncated and the tail budget would naively be 0.
671+ maxHistoryChars,
672+});
673+674+expect(prompt).toBeDefined();
675+// The truncated summary still leads with recognizable load-bearing
676+// text — head-slicing preserves the orientation/intro of the summary.
677+expect(prompt).toContain("OVERSIZE_SUMMARY_MARKER");
678+expect(prompt).toContain("Compaction summary:");
679+// The leading truncation marker is present so the prompt announces
680+// what was discarded.
681+expect(prompt).toContain("[OpenClaw reseed history truncated; older turns dropped]");
682+// The cap is honored: the rendered <conversation_history> block
683+// must not blow past `maxHistoryChars` plus a small wrapper allowance.
684+const historyMatch = prompt?.match(
685+/<conversation_history>\n([\s\S]*?)\n<\/conversation_history>/,
686+);
687+expect(historyMatch).not.toBeNull();
688+const renderedHistory = historyMatch?.[1] ?? "";
689+expect(renderedHistory.length).toBeLessThanOrEqual(maxHistoryChars);
690+// The full untruncated summary must NOT appear — that would defeat
691+// the cap.
692+expect(prompt).not.toContain(summaryText);
693+// Post-summary exact turns are newer than the summary and must still
694+// survive inside the reserved tail budget.
695+expect(prompt).toContain("POST_SUMMARY_USER_DROPPED");
696+expect(prompt).toContain("POST_SUMMARY_ASSISTANT_DROPPED");
697+expect(prompt).toContain("<next_user_message>\nnext ask\n</next_user_message>");
698+});
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+// Near-cap summaries still reserve room for the newest exact turns.
736+expect(prompt).toContain("POST_SUMMARY_TAIL_USER");
737+expect(prompt).toContain("POST_SUMMARY_TAIL_ASSISTANT");
738+});
589739});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。