mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 14:38:25 +00:00
fix(heartbeat): prune transcript for HEARTBEAT_OK turns
When a heartbeat run results in HEARTBEAT_OK (or empty/duplicate), the user+assistant turns are now pruned from the session transcript. This prevents context window pollution from zero-information exchanges. Implementation: - captureTranscriptState(): records transcript file path and size before heartbeat - pruneHeartbeatTranscript(): truncates file back to pre-heartbeat size - Called in ok-empty, ok-token, and duplicate cases (same places as restoreHeartbeatUpdatedAt) This extends the existing pattern where delivery is suppressed and updatedAt is restored for HEARTBEAT_OK responses - now the transcript is also cleaned up. Fixes #17804
This commit is contained in:
committed by
Peter Steinberger
parent
7bb9a7dcfc
commit
e9f2e6a829
@@ -31,6 +31,7 @@ import {
|
||||
loadSessionStore,
|
||||
resolveAgentIdFromSessionKey,
|
||||
resolveAgentMainSessionKey,
|
||||
resolveSessionFilePath,
|
||||
resolveStorePath,
|
||||
saveSessionStore,
|
||||
updateSessionStore,
|
||||
@@ -351,6 +352,58 @@ async function restoreHeartbeatUpdatedAt(params: {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune heartbeat transcript entries by truncating the file back to a previous size.
|
||||
* This removes the user+assistant turns that were written during a HEARTBEAT_OK run,
|
||||
* preventing context pollution from zero-information exchanges.
|
||||
*/
|
||||
async function pruneHeartbeatTranscript(params: {
|
||||
transcriptPath?: string;
|
||||
preHeartbeatSize?: number;
|
||||
}) {
|
||||
const { transcriptPath, preHeartbeatSize } = params;
|
||||
if (!transcriptPath || typeof preHeartbeatSize !== "number" || preHeartbeatSize < 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const stat = await fs.stat(transcriptPath);
|
||||
// Only truncate if the file has grown during the heartbeat run
|
||||
if (stat.size > preHeartbeatSize) {
|
||||
await fs.truncate(transcriptPath, preHeartbeatSize);
|
||||
}
|
||||
} catch {
|
||||
// File may not exist or may have been removed - ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transcript file path and its current size before a heartbeat run.
|
||||
* Returns undefined values if the session or transcript doesn't exist yet.
|
||||
*/
|
||||
async function captureTranscriptState(params: {
|
||||
storePath: string;
|
||||
sessionKey: string;
|
||||
agentId?: string;
|
||||
}): Promise<{ transcriptPath?: string; preHeartbeatSize?: number }> {
|
||||
const { storePath, sessionKey, agentId } = params;
|
||||
try {
|
||||
const store = loadSessionStore(storePath);
|
||||
const entry = store[sessionKey];
|
||||
if (!entry?.sessionId) {
|
||||
return {};
|
||||
}
|
||||
const transcriptPath = resolveSessionFilePath(entry.sessionId, entry, {
|
||||
agentId,
|
||||
sessionsDir: path.dirname(storePath),
|
||||
});
|
||||
const stat = await fs.stat(transcriptPath);
|
||||
return { transcriptPath, preHeartbeatSize: stat.size };
|
||||
} catch {
|
||||
// Session or transcript doesn't exist yet - nothing to prune
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHeartbeatReply(
|
||||
payload: ReplyPayload,
|
||||
responsePrefix: string | undefined,
|
||||
@@ -546,6 +599,13 @@ export async function runHeartbeatOnce(opts: {
|
||||
};
|
||||
|
||||
try {
|
||||
// Capture transcript state before the heartbeat run so we can prune if HEARTBEAT_OK
|
||||
const transcriptState = await captureTranscriptState({
|
||||
storePath,
|
||||
sessionKey,
|
||||
agentId,
|
||||
});
|
||||
|
||||
const heartbeatModelOverride = heartbeat?.model?.trim() || undefined;
|
||||
const suppressToolErrorWarnings = heartbeat?.suppressToolErrorWarnings === true;
|
||||
const replyOpts = heartbeatModelOverride
|
||||
@@ -567,6 +627,8 @@ export async function runHeartbeatOnce(opts: {
|
||||
sessionKey,
|
||||
updatedAt: previousUpdatedAt,
|
||||
});
|
||||
// Prune the transcript to remove HEARTBEAT_OK turns
|
||||
await pruneHeartbeatTranscript(transcriptState);
|
||||
const okSent = await maybeSendHeartbeatOk();
|
||||
emitHeartbeatEvent({
|
||||
status: "ok-empty",
|
||||
@@ -601,6 +663,8 @@ export async function runHeartbeatOnce(opts: {
|
||||
sessionKey,
|
||||
updatedAt: previousUpdatedAt,
|
||||
});
|
||||
// Prune the transcript to remove HEARTBEAT_OK turns
|
||||
await pruneHeartbeatTranscript(transcriptState);
|
||||
const okSent = await maybeSendHeartbeatOk();
|
||||
emitHeartbeatEvent({
|
||||
status: "ok-token",
|
||||
@@ -637,6 +701,8 @@ export async function runHeartbeatOnce(opts: {
|
||||
sessionKey,
|
||||
updatedAt: previousUpdatedAt,
|
||||
});
|
||||
// Prune the transcript to remove duplicate heartbeat turns
|
||||
await pruneHeartbeatTranscript(transcriptState);
|
||||
emitHeartbeatEvent({
|
||||
status: "skipped",
|
||||
reason: "duplicate",
|
||||
|
||||
Reference in New Issue
Block a user