From 5e053e51b50fdfab8e97bd9abdde71542b700749 Mon Sep 17 00:00:00 2001 From: d-robotics Date: Sat, 13 Jun 2026 20:57:36 +0800 Subject: [PATCH] fix compaction replay silently dropping kept history on a missing firstKeptEntryId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildSessionContext walked entries [0, compactionIdx) and only began appending once it saw compaction.firstKeptEntryId. If that id was missing from the path (corruption/race — the JSONL codec only warns on load, it does not repair), foundFirstKept never flipped, so EVERY pre-compaction entry was dropped on replay — losing recent history the compaction summary does not cover. Fix: when firstKeptEntryId is absent, keep all pre-compaction entries instead of dropping them (preserve data over the kept-tail optimization). Normal replays, where the id exists, are unchanged. Verified red-before-green (a session with a bogus firstKeptEntryId loses PRECOMP messages before, keeps them after) and npm run verify green (agent 190). The other dry-run-blocked candidate (ROS_DOMAIN_ID NaN) was a false positive — device-ssh.ts already guards with Number.isInteger && >= 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/core/session/session-manager.ts | 9 +++- .../test/session-compaction-replay.spec.mjs | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 packages/dmoss-agent/test/session-compaction-replay.spec.mjs diff --git a/packages/dmoss-agent/src/core/session/session-manager.ts b/packages/dmoss-agent/src/core/session/session-manager.ts index 705ac93..97f6560 100644 --- a/packages/dmoss-agent/src/core/session/session-manager.ts +++ b/packages/dmoss-agent/src/core/session/session-manager.ts @@ -579,7 +579,14 @@ function buildSessionContext(state: SessionState): Message[] { const compactionIdx = path.findIndex( (entry) => entry.type === "compaction" && entry.id === compaction.id, ); - let foundFirstKept = false; + // If firstKeptEntryId is missing from the path (corruption/race — the codec + // only warns on load), the kept tail would otherwise be silently dropped on + // replay, losing recent history the compaction summary does NOT cover. Fall + // back to keeping every pre-compaction entry instead of dropping them. + const firstKeptExists = path + .slice(0, compactionIdx < 0 ? 0 : compactionIdx) + .some((entry) => entry.id === compaction.firstKeptEntryId); + let foundFirstKept = !firstKeptExists; for (let i = 0; i < compactionIdx; i++) { const entry = path[i]; if (entry.id === compaction.firstKeptEntryId) { diff --git a/packages/dmoss-agent/test/session-compaction-replay.spec.mjs b/packages/dmoss-agent/test/session-compaction-replay.spec.mjs new file mode 100644 index 0000000..bba68be --- /dev/null +++ b/packages/dmoss-agent/test/session-compaction-replay.spec.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +/** + * Compaction replay must not silently drop kept history when + * compaction.firstKeptEntryId is missing from the path (corruption/race — the + * JSONL codec only warns on load). Before the fix, buildSessionContext never + * flipped foundFirstKept, so every pre-compaction entry was dropped, losing + * recent history the summary does NOT cover. Red before / green after. + * + * Run: + * npm run build -w @rdk-moss/agent + * node packages/dmoss-agent/test/session-compaction-replay.spec.mjs + */ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { SessionManager } from '../dist/core/session/session-manager.js'; + +const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dmoss-compaction-replay-')); +try { + const sm = new SessionManager(tmpDir); + const key = 's1'; + + await sm.append(key, { role: 'user', content: 'PRECOMP-1' }); + await sm.append(key, { role: 'assistant', content: 'PRECOMP-2' }); + await sm.append(key, { role: 'user', content: 'PRECOMP-3' }); + // Compaction whose firstKeptEntryId does NOT exist in the path. + await sm.appendCompaction(key, 'THE-COMPACTION-SUMMARY', 'nonexistent-kept-id', 100); + await sm.append(key, { role: 'assistant', content: 'POSTCOMP-1' }); + + const ctx = await sm.load(key); + const text = JSON.stringify(ctx); + + assert.match(text, /THE-COMPACTION-SUMMARY/, 'compaction summary must be replayed'); + assert.match(text, /POSTCOMP-1/, 'post-compaction message must be present'); + // The core bug: pre-compaction kept messages must NOT vanish when + // firstKeptEntryId is missing. + assert.match(text, /PRECOMP-1/, 'pre-compaction history must not be lost when firstKeptEntryId is missing'); + assert.match(text, /PRECOMP-3/, 'all pre-compaction messages preserved'); + + console.log(' [PASS] compaction replay preserves history when firstKeptEntryId is missing'); +} finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); +}