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:
Operative-001
2026-02-16 12:13:12 +01:00
committed by Peter Steinberger
parent 7bb9a7dcfc
commit e9f2e6a829
2 changed files with 254 additions and 0 deletions

View File

@@ -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",