mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(agents): timestamp recovered Claude CLI history (#119528)
* fix(agents): timestamp recovered Claude CLI history * fix(agents): reject noncanonical recovered timestamps Punchcard-Session: brisk-meadow-river-2e --------- Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -4104,10 +4104,11 @@ describe("prepareCliRunContext", () => {
|
||||
});
|
||||
|
||||
it("arms raw-transcript reseed for a missing claude-cli transcript so prior conversation is redelivered", async () => {
|
||||
const recoveredAt = "2020-01-02T03:04:05.000Z";
|
||||
fixture.appendTranscript({
|
||||
id: "msg-1",
|
||||
parentId: null,
|
||||
timestamp: new Date(1).toISOString(),
|
||||
timestamp: recoveredAt,
|
||||
message: {
|
||||
role: "user",
|
||||
content: "prior claude-cli ask",
|
||||
@@ -4138,8 +4139,16 @@ describe("prepareCliRunContext", () => {
|
||||
mode: "invalidate",
|
||||
invalidatedReason: "missing-transcript",
|
||||
});
|
||||
expect(context.openClawHistoryPrompt).toContain("prior claude-cli ask");
|
||||
expect(context.openClawHistoryPrompt).toContain("latest ask");
|
||||
expect(context.openClawHistoryPrompt).toContain(`[${recoveredAt}] User: prior claude-cli ask`);
|
||||
expect(context.openClawHistoryPrompt).not.toContain(
|
||||
"[1970-01-01T00:00:00.001Z] User: prior claude-cli ask",
|
||||
);
|
||||
expect(context.openClawHistoryPrompt).toContain(
|
||||
"Recovered history may be stale; verify current and time-sensitive facts before acting.",
|
||||
);
|
||||
expect(context.openClawHistoryPrompt).toContain(
|
||||
"<next_user_message>\nlatest ask\n</next_user_message>",
|
||||
);
|
||||
});
|
||||
|
||||
it("prepares node-placed Claude resumes without Gateway MCP, skills, or transcript checks", async () => {
|
||||
|
||||
@@ -23,8 +23,18 @@ const MAX_CLI_SESSION_HISTORY_FILE_BYTES = 5 * 1024 * 1024;
|
||||
const MAX_CLI_SESSION_HISTORY_MESSAGES = MAX_AGENT_HOOK_HISTORY_MESSAGES;
|
||||
const MAX_CLI_SESSION_RESEED_HISTORY_CHARS = 12 * 1024;
|
||||
const MAX_AUTO_CLI_SESSION_RESEED_HISTORY_CHARS = 256 * 1024;
|
||||
const RESEED_CURRENCY_GUIDANCE =
|
||||
"[Recovered history may be stale; verify current and time-sensitive facts before acting.]";
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
function withReseedGuidanceBudget(historyChars: number): number {
|
||||
return RESEED_CURRENCY_GUIDANCE.length + "\n".length + historyChars;
|
||||
}
|
||||
|
||||
function extractReseedHistory(prompt: string | undefined): string {
|
||||
return prompt?.match(/<conversation_history>\n([\s\S]*?)\n<\/conversation_history>/)?.[1] ?? "";
|
||||
}
|
||||
|
||||
function createSessionTranscript(params: {
|
||||
rootDir: string;
|
||||
sessionId: string;
|
||||
@@ -703,9 +713,12 @@ describe("loadCliSessionReseedMessages", () => {
|
||||
role: "user",
|
||||
content: `raw-${MAX_CLI_SESSION_HISTORY_MESSAGES + 24}`,
|
||||
});
|
||||
expect(buildCliSessionHistoryPrompt({ messages: reseed, prompt: "next" })).toContain(
|
||||
"raw-25",
|
||||
expect(requireRecord(reseed[0], "first raw reseed message").timestamp).toBe(
|
||||
"1970-01-01T00:00:00.026Z",
|
||||
);
|
||||
const prompt = buildCliSessionHistoryPrompt({ messages: reseed, prompt: "next" });
|
||||
expect(prompt).toContain("[1970-01-01T00:00:00.026Z] User: raw-25");
|
||||
expect(prompt).toContain(RESEED_CURRENCY_GUIDANCE);
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(stateDir, { recursive: true, force: true });
|
||||
@@ -826,9 +839,15 @@ describe("loadCliSessionReseedMessages", () => {
|
||||
expect(reseed).toHaveLength(2);
|
||||
expectCompactionSummary(reseed[0], "safe compacted summary");
|
||||
expectMessageFields(reseed[1], { role: "user", content: "post-compaction ask" });
|
||||
expect(buildCliSessionHistoryPrompt({ messages: reseed, prompt: "next" })).toContain(
|
||||
"Compaction summary: safe compacted summary",
|
||||
expect(reseed.map((message) => requireRecord(message, "reseed message").timestamp)).toEqual(
|
||||
["1970-01-01T00:00:00.002Z", "1970-01-01T00:00:00.003Z"],
|
||||
);
|
||||
const prompt = buildCliSessionHistoryPrompt({ messages: reseed, prompt: "next" });
|
||||
expect(prompt).toContain(
|
||||
"[1970-01-01T00:00:00.002Z] Compaction summary: safe compacted summary",
|
||||
);
|
||||
expect(prompt).toContain("[1970-01-01T00:00:00.003Z] User: post-compaction ask");
|
||||
expect(prompt).toContain(RESEED_CURRENCY_GUIDANCE);
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(stateDir, { recursive: true, force: true });
|
||||
@@ -851,6 +870,29 @@ describe("buildCliSessionHistoryPrompt", () => {
|
||||
expect(prompt).toContain("<next_user_message>\nnew ask\n</next_user_message>");
|
||||
});
|
||||
|
||||
it("renders canonical saved timestamps and omits invalid or noncanonical timestamps", () => {
|
||||
const prompt = buildCliSessionHistoryPrompt({
|
||||
messages: [
|
||||
{ role: "user", content: "dated ask", timestamp: "2026-06-17T16:00:00.000Z" },
|
||||
{ role: "assistant", content: "zero date answer", timestamp: "0" },
|
||||
{ role: "user", content: "year-only ask", timestamp: "2026" },
|
||||
{ role: "assistant", content: "invalid date answer", timestamp: "not-a-date" },
|
||||
{ role: "user", content: "offset date ask", timestamp: "2026-06-17T12:00:00-04:00" },
|
||||
{ role: "assistant", content: "undated answer" },
|
||||
],
|
||||
prompt: "new ask",
|
||||
});
|
||||
|
||||
expect(prompt).toContain("[2026-06-17T16:00:00.000Z] User: dated ask");
|
||||
expect(prompt).toMatch(
|
||||
/Assistant: zero date answer[\s\S]*User: year-only ask[\s\S]*Assistant: invalid date answer[\s\S]*User: offset date ask[\s\S]*Assistant: undated answer/u,
|
||||
);
|
||||
expect(prompt).not.toMatch(
|
||||
/\[(?:2000-01-01T00:00:00\.000Z|2026-01-01T00:00:00\.000Z|not-a-date|2026-06-17T12:00:00-04:00)\]/u,
|
||||
);
|
||||
expect(prompt).toContain(RESEED_CURRENCY_GUIDANCE);
|
||||
});
|
||||
|
||||
it("skips reseed text when the transcript has no renderable conversation", () => {
|
||||
expect(
|
||||
buildCliSessionHistoryPrompt({
|
||||
@@ -861,13 +903,14 @@ describe("buildCliSessionHistoryPrompt", () => {
|
||||
});
|
||||
|
||||
it("caps rendered reseed history before adding the next user message", () => {
|
||||
const maxHistoryChars = withReseedGuidanceBudget(80);
|
||||
const prompt = buildCliSessionHistoryPrompt({
|
||||
messages: [
|
||||
{ role: "user", content: "x".repeat(100) },
|
||||
{ role: "assistant", content: "y".repeat(100) },
|
||||
],
|
||||
prompt: "current ask must survive",
|
||||
maxHistoryChars: 20,
|
||||
maxHistoryChars,
|
||||
});
|
||||
|
||||
expect(prompt).toContain("[OpenClaw reseed history truncated; older turns dropped]");
|
||||
@@ -875,17 +918,18 @@ describe("buildCliSessionHistoryPrompt", () => {
|
||||
// Older 100-char prefix must be dropped by the tail slice; the
|
||||
// post-cap rendered tail is shorter than the dropped prefix.
|
||||
expect(prompt).not.toContain("x".repeat(80));
|
||||
expect(extractReseedHistory(prompt).length).toBeLessThanOrEqual(maxHistoryChars);
|
||||
});
|
||||
|
||||
it("keeps a whole code point when the retained history tail starts inside an emoji", () => {
|
||||
const prompt = buildCliSessionHistoryPrompt({
|
||||
messages: [{ role: "user", content: "prefix😀tail" }],
|
||||
prompt: "next",
|
||||
maxHistoryChars: 5,
|
||||
maxHistoryChars: withReseedGuidanceBudget(5),
|
||||
});
|
||||
|
||||
expect(prompt).toContain(
|
||||
"<conversation_history>\n[OpenClaw reseed history truncated; older turns dropped]\ntail\n</conversation_history>",
|
||||
`<conversation_history>\n${RESEED_CURRENCY_GUIDANCE}\ntail\n</conversation_history>`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -953,6 +997,9 @@ describe("buildCliSessionHistoryPrompt", () => {
|
||||
// dropped so the cap is honored.
|
||||
expect(prompt).not.toContain("z".repeat(8000));
|
||||
expect(prompt).toContain("<next_user_message>\nnext ask\n</next_user_message>");
|
||||
expect(extractReseedHistory(prompt).length).toBeLessThanOrEqual(
|
||||
MAX_CLI_SESSION_RESEED_HISTORY_CHARS,
|
||||
);
|
||||
});
|
||||
|
||||
it("caps oversize compaction summary while preserving recent post-summary tail", () => {
|
||||
@@ -966,7 +1013,8 @@ describe("buildCliSessionHistoryPrompt", () => {
|
||||
// The summary must itself be truncated to fit the budget while still
|
||||
// preserving the recent post-summary exact turns.
|
||||
const summaryText = "OVERSIZE_SUMMARY_MARKER ".repeat(50).trim();
|
||||
const maxHistoryChars = 200;
|
||||
const historyBudget = 200;
|
||||
const maxHistoryChars = withReseedGuidanceBudget(historyBudget);
|
||||
const prompt = buildCliSessionHistoryPrompt({
|
||||
messages: [
|
||||
{ role: "compactionSummary", summary: summaryText },
|
||||
@@ -1009,30 +1057,25 @@ describe("buildCliSessionHistoryPrompt", () => {
|
||||
const prompt = buildCliSessionHistoryPrompt({
|
||||
messages: [{ role: "compactionSummary", summary: `aa😀${"z".repeat(100)}` }],
|
||||
prompt: "next",
|
||||
maxHistoryChars: 80,
|
||||
maxHistoryChars: withReseedGuidanceBudget(80),
|
||||
});
|
||||
|
||||
expect(prompt).toContain(
|
||||
"<conversation_history>\n[OpenClaw reseed history truncated; older turns dropped]\nCompaction summary: aa\n</conversation_history>",
|
||||
`<conversation_history>\n${RESEED_CURRENCY_GUIDANCE}\n[OpenClaw reseed history truncated; older turns dropped]\nCompaction summary: aa\n</conversation_history>`,
|
||||
);
|
||||
});
|
||||
|
||||
it("honors the cap when the summary block plus marker crosses it", () => {
|
||||
// Edge case: `summaryRendered.length < maxHistoryChars` (the gate that
|
||||
// routes to the oversize-summary branch is not taken) BUT
|
||||
// `summaryBlock.length >= maxHistoryChars` once the `\n\n` separator
|
||||
// is appended, making `remainingBudget <= 0`. Without summary
|
||||
// truncation in that branch, the rendered history block is
|
||||
// `summary + separator + marker` — well over `maxHistoryChars`. A
|
||||
// 199-char rendered summary under a 200-char cap would otherwise
|
||||
// produce a 257-char history block.
|
||||
const maxHistoryChars = 200;
|
||||
// `renderHistoryMessage` prefixes "Compaction summary: " (20 chars)
|
||||
// before the summary text, so a 179-char summary renders to 199 chars
|
||||
// — strictly less than the cap, but `summaryBlock = rendered + "\n\n"`
|
||||
// is 201 chars and `remainingBudget` is negative.
|
||||
// Edge case: the summary fits but leaves too little room for the
|
||||
// truncation marker plus a useful exact tail. Rebalance the summary and
|
||||
// tail instead of exceeding the cap or silently dropping the marker.
|
||||
const historyBudget = 200;
|
||||
const maxHistoryChars = withReseedGuidanceBudget(historyBudget);
|
||||
const remainingBudget = 10;
|
||||
const summaryPrefix = "Compaction summary: ";
|
||||
const summaryText = "S".repeat(maxHistoryChars - 1 - summaryPrefix.length);
|
||||
const summaryText = "S".repeat(
|
||||
historyBudget - remainingBudget - "\n\n".length - summaryPrefix.length,
|
||||
);
|
||||
const prompt = buildCliSessionHistoryPrompt({
|
||||
messages: [
|
||||
{ role: "compactionSummary", summary: summaryText },
|
||||
@@ -1056,4 +1099,25 @@ describe("buildCliSessionHistoryPrompt", () => {
|
||||
expect(prompt).toContain("POST_SUMMARY_TAIL_USER");
|
||||
expect(prompt).toContain("POST_SUMMARY_TAIL_ASSISTANT");
|
||||
});
|
||||
|
||||
it("keeps fitting post-summary history without a false truncation marker", () => {
|
||||
const historyBudget = 200;
|
||||
const remainingBudget = 10;
|
||||
const summaryPrefix = "Compaction summary: ";
|
||||
const summaryText = "S".repeat(
|
||||
historyBudget - remainingBudget - "\n\n".length - summaryPrefix.length,
|
||||
);
|
||||
const prompt = buildCliSessionHistoryPrompt({
|
||||
messages: [
|
||||
{ role: "compactionSummary", summary: summaryText },
|
||||
{ role: "user", content: "tail" },
|
||||
],
|
||||
prompt: "next ask",
|
||||
maxHistoryChars: withReseedGuidanceBudget(historyBudget),
|
||||
});
|
||||
|
||||
expect(prompt).toContain(`Compaction summary: ${summaryText}`);
|
||||
expect(prompt).toContain("User: tail");
|
||||
expect(prompt).not.toContain("[OpenClaw reseed history truncated; older turns dropped]");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
*/
|
||||
import fsp from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { sliceUtf16Safe, truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import {
|
||||
resolveSessionFilePathCore,
|
||||
@@ -38,11 +40,14 @@ const MAX_AUTO_CLI_SESSION_RESEED_HISTORY_CHARS = 256 * 1024;
|
||||
const CLI_SESSION_RESEED_HISTORY_CONTEXT_SHARE = 0.08;
|
||||
const CHARS_PER_TOKEN_ESTIMATE = 4;
|
||||
const CLI_SESSION_HISTORY_HEADER_READ_BYTES = 64 * 1024;
|
||||
const CLI_SESSION_RESEED_CURRENCY_GUIDANCE =
|
||||
"[Recovered history may be stale; verify current and time-sensitive facts before acting.]";
|
||||
|
||||
type HistoryMessage = {
|
||||
role?: unknown;
|
||||
content?: unknown;
|
||||
summary?: unknown;
|
||||
timestamp?: unknown;
|
||||
};
|
||||
type HistoryEntry = {
|
||||
type?: unknown;
|
||||
@@ -123,6 +128,20 @@ function coerceHistoryTimestamp(value: unknown): number | string {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function projectReseedMessage(message: unknown, timestamp: unknown): unknown {
|
||||
// The transcript row owns persistence time; nested provider timestamps can
|
||||
// be stale or absent when history is recovered into a fresh CLI session.
|
||||
return isRecord(message) ? { ...message, timestamp } : message;
|
||||
}
|
||||
|
||||
function formatHistoryTimestamp(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const timestamp = timestampMsToIsoString(Date.parse(value));
|
||||
return timestamp === value ? timestamp : undefined;
|
||||
}
|
||||
|
||||
function historyEntryToContextEngineMessage(entry: HistoryEntry): AgentMessage | undefined {
|
||||
if (entry.type === "message") {
|
||||
return entry.message as AgentMessage;
|
||||
@@ -175,7 +194,11 @@ function renderHistoryMessage(message: unknown): string | undefined {
|
||||
entry.role === "compactionSummary" && typeof entry.summary === "string"
|
||||
? entry.summary.trim()
|
||||
: coerceHistoryText(entry.content);
|
||||
return text ? `${role}: ${text}` : undefined;
|
||||
if (!text) {
|
||||
return undefined;
|
||||
}
|
||||
const timestamp = formatHistoryTimestamp(entry.timestamp);
|
||||
return `${timestamp ? `[${timestamp}] ` : ""}${role}: ${text}`;
|
||||
}
|
||||
|
||||
/** Builds a reseed prompt that carries prior OpenClaw transcript context. */
|
||||
@@ -185,6 +208,10 @@ export function buildCliSessionHistoryPrompt(params: {
|
||||
maxHistoryChars?: number;
|
||||
}): string | undefined {
|
||||
const maxHistoryChars = params.maxHistoryChars ?? MAX_CLI_SESSION_RESEED_HISTORY_CHARS;
|
||||
const historyBudget = maxHistoryChars - CLI_SESSION_RESEED_CURRENCY_GUIDANCE.length - "\n".length;
|
||||
if (historyBudget <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// loadCliSessionReseedMessages deliberately places a `compactionSummary`
|
||||
// entry first when the session was compacted, so the compacted prior
|
||||
@@ -209,13 +236,25 @@ export function buildCliSessionHistoryPrompt(params: {
|
||||
.trim();
|
||||
|
||||
const truncationMarker = "[OpenClaw reseed history truncated; older turns dropped]";
|
||||
const renderTruncatedTail = (raw: string, budget: number): string => {
|
||||
if (budget <= truncationMarker.length + "\n".length) {
|
||||
return sliceUtf16Safe(raw, -budget).trimStart();
|
||||
}
|
||||
const tailBudget = budget - truncationMarker.length - "\n".length;
|
||||
return `${truncationMarker}\n${sliceUtf16Safe(raw, -tailBudget).trimStart()}`;
|
||||
};
|
||||
const renderTruncatedSummaryWithTail = (renderedSummary: string): string => {
|
||||
if (historyBudget <= truncationMarker.length + "\n".length) {
|
||||
return tailRaw.length > 0
|
||||
? sliceUtf16Safe(tailRaw, -historyBudget).trimStart()
|
||||
: truncateUtf16Safe(renderedSummary, historyBudget).trimEnd();
|
||||
}
|
||||
const tailBudget =
|
||||
tailRaw.length > 0 ? Math.min(tailRaw.length, Math.floor(maxHistoryChars / 2)) : 0;
|
||||
tailRaw.length > 0 ? Math.min(tailRaw.length, Math.floor(historyBudget / 2)) : 0;
|
||||
const separatorBudget = tailBudget > 0 ? 2 : 1;
|
||||
const summaryBudget = Math.max(
|
||||
0,
|
||||
maxHistoryChars - truncationMarker.length - separatorBudget - tailBudget,
|
||||
historyBudget - truncationMarker.length - separatorBudget - tailBudget,
|
||||
);
|
||||
const summaryTruncated = truncateUtf16Safe(renderedSummary, summaryBudget).trimEnd();
|
||||
const tailTruncated = tailBudget > 0 ? sliceUtf16Safe(tailRaw, -tailBudget).trimStart() : "";
|
||||
@@ -229,7 +268,7 @@ export function buildCliSessionHistoryPrompt(params: {
|
||||
// cap, the summary itself must be truncated — pinning a summary that
|
||||
// blows past `maxHistoryChars` would defeat the cap that prevents
|
||||
// reseeding fresh CLI sessions with unexpectedly huge prompts.
|
||||
if (summaryRendered.length >= maxHistoryChars) {
|
||||
if (summaryRendered.length >= historyBudget) {
|
||||
// Truncate the summary to fit the budget (less the marker line),
|
||||
// keeping the head. Still reserve budget for the post-summary tail so
|
||||
// recent exact turns survive even when the summary itself is oversize.
|
||||
@@ -238,16 +277,16 @@ export function buildCliSessionHistoryPrompt(params: {
|
||||
renderedHistory = summaryRendered;
|
||||
} else {
|
||||
const summaryBlock = `${summaryRendered}\n\n`;
|
||||
const remainingBudget = maxHistoryChars - summaryBlock.length;
|
||||
if (remainingBudget <= 0) {
|
||||
// The summary plus separator already consumes the cap. Reuse the
|
||||
// oversize-summary path so recent post-summary turns still get
|
||||
// reserved tail budget instead of being dropped wholesale.
|
||||
renderedHistory = renderTruncatedSummaryWithTail(summaryRendered);
|
||||
} else if (tailRaw.length > remainingBudget) {
|
||||
renderedHistory = `${summaryBlock}${truncationMarker}\n${sliceUtf16Safe(tailRaw, -remainingBudget).trimStart()}`;
|
||||
} else {
|
||||
const remainingBudget = historyBudget - summaryBlock.length;
|
||||
if (tailRaw.length <= remainingBudget) {
|
||||
renderedHistory = `${summaryBlock}${tailRaw}`;
|
||||
} else if (remainingBudget <= truncationMarker.length + "\n".length) {
|
||||
// The summary leaves too little room to announce truncation. Reuse
|
||||
// the oversize-summary path so the marker and recent exact turns
|
||||
// both retain budget.
|
||||
renderedHistory = renderTruncatedSummaryWithTail(summaryRendered);
|
||||
} else {
|
||||
renderedHistory = `${summaryBlock}${renderTruncatedTail(tailRaw, remainingBudget)}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -255,9 +294,7 @@ export function buildCliSessionHistoryPrompt(params: {
|
||||
// and lead with the marker so it correctly describes what follows
|
||||
// (older turns dropped, recent tail retained).
|
||||
renderedHistory =
|
||||
tailRaw.length > maxHistoryChars
|
||||
? `${truncationMarker}\n${sliceUtf16Safe(tailRaw, -maxHistoryChars).trimStart()}`
|
||||
: tailRaw;
|
||||
tailRaw.length > historyBudget ? renderTruncatedTail(tailRaw, historyBudget) : tailRaw;
|
||||
}
|
||||
|
||||
if (!renderedHistory) {
|
||||
@@ -269,6 +306,7 @@ export function buildCliSessionHistoryPrompt(params: {
|
||||
"Treat it as authoritative context for this fresh CLI session.",
|
||||
"",
|
||||
"<conversation_history>",
|
||||
CLI_SESSION_RESEED_CURRENCY_GUIDANCE,
|
||||
renderedHistory,
|
||||
"</conversation_history>",
|
||||
"",
|
||||
@@ -640,7 +678,9 @@ export async function loadCliSessionReseedMessages(params: {
|
||||
}
|
||||
const rawTail = entries.flatMap((entry) => {
|
||||
const candidate = entry as HistoryEntry;
|
||||
return candidate.type === "message" ? [candidate.message] : [];
|
||||
return candidate.type === "message"
|
||||
? [projectReseedMessage(candidate.message, candidate.timestamp)]
|
||||
: [];
|
||||
});
|
||||
return limitAgentHookHistoryMessages(rawTail, MAX_CLI_SESSION_HISTORY_MESSAGES);
|
||||
};
|
||||
@@ -660,12 +700,15 @@ export async function loadCliSessionReseedMessages(params: {
|
||||
|
||||
const tailMessages = entries.slice(latestCompactionIndex + 1).flatMap((entry) => {
|
||||
const candidate = entry as HistoryEntry;
|
||||
return candidate.type === "message" ? [candidate.message] : [];
|
||||
return candidate.type === "message"
|
||||
? [projectReseedMessage(candidate.message, candidate.timestamp)]
|
||||
: [];
|
||||
});
|
||||
return [
|
||||
{
|
||||
role: "compactionSummary",
|
||||
summary,
|
||||
timestamp: compaction.timestamp,
|
||||
},
|
||||
...limitAgentHookHistoryMessages(tailMessages, MAX_CLI_SESSION_HISTORY_MESSAGES - 1),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user