fix(session-memory): skip transcript-only assistant messages in getRecentSessionContent#94401
Conversation
|
Thanks for the thorough PR — bug is real, consumer-layer filter is the right call (storage-layer fix would break replay/compaction, which other surfaces rely on), and the risk analysis on the narrow model-value set is sound. Two small asks before merge: 1. Reuse the canonical helper instead of inlining the predicate.
Suggested replacement: import { isTranscriptOnlyOpenClawAssistantMessage } from "../../../shared/transcript-only-openclaw-assistant.js";
// ...
if (isTranscriptOnlyOpenClawAssistantMessage(msg)) {
continue;
}You can also drop the 2. Add a regression test in This fixes a real user-reported regression (duplicates from thinking models) and the test data is already written in your PR body. A ~25-line test covering it("skips delivery-mirror and gateway-injected assistant rows (fixes #92563)", async () => {
const sessionContent = [
JSON.stringify({ type: "message", message: { role: "user", content: "What is 2+2?" } }),
JSON.stringify({
type: "message",
message: {
role: "assistant",
provider: "openclaw",
model: "claude",
content: [{ type: "thinking", text: "..." }, { type: "text", text: "2+2 = 4" }],
},
}),
JSON.stringify({
type: "message",
message: {
role: "assistant",
provider: "openclaw",
model: "delivery-mirror",
content: [{ type: "text", text: "2+2 = 4" }],
},
}),
JSON.stringify({
type: "message",
message: {
role: "assistant",
provider: "openclaw",
model: "gateway-injected",
content: [{ type: "text", text: "should be filtered" }],
},
}),
].join("\n");
const memoryContent = await readSessionTranscript({ sessionContent });
const assistantLines = memoryContent!.split("\n").filter((l) => l.startsWith("assistant:"));
expect(assistantLines).toEqual(["assistant: 2+2 = 4"]);
expect(memoryContent).not.toContain("should be filtered");
});Happy to push the test commit directly to the branch if you'd prefer. |
c52190e to
c26f572
Compare
|
Codex review: needs maintainer review before merge. Reviewed June 29, 2026, 11:03 AM ET / 15:03 UTC. Summary PR surface: Source +18, Tests +146. Total +164 across 2 files. Reproducibility: yes. Current main and v2026.6.10 both extract text from every assistant row and do not special-case delivery-mirror rows, so raw assistant text plus a delivery-mirror copy can duplicate session-memory output; I did not run tests because this review is read-only. Review metrics: none identified. Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Review and land this branch as the canonical consumer-layer session-memory fix if maintainers accept text-matched delivery-mirror filtering; then close the linked canonical issue and supersede the broader duplicate PR attempts. Do we have a high-confidence way to reproduce the issue? Yes. Current main and v2026.6.10 both extract text from every assistant row and do not special-case delivery-mirror rows, so raw assistant text plus a delivery-mirror copy can duplicate session-memory output; I did not run tests because this review is read-only. Is this the best way to solve the issue? Yes, this is an acceptable narrow fix. It repairs the session-memory consumer instead of changing persisted transcript storage, which the discussion and replay code support because other surfaces intentionally keep transcript bookkeeping rows. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against d21ddd3f7a88. Label changesLabel justifications:
Evidence reviewedPR surface: Source +18, Tests +146. Total +164 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
c26f572 to
8f48f5e
Compare
d39e729 to
04b6c81
Compare
lastAssistantText persisted across user messages, causing delivery-mirror rows that echoed a previous turn's assistant text to be incorrectly filtered. Reset lastAssistantText to undefined when a visible user message is emitted, so cross-turn delivery-mirror duplicates are preserved while same-turn duplicates are still skipped. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
Hey @clawsweeper — the two issues flagged in the June 18 review have been addressed in the latest commits:
All tests pass (26/26 handler, 3/3 transcript), CI is green, and the @clawsweeper re-review — please re-evaluate and remove the |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Signed-off-by: sallyom <[email protected]>
|
Maintainer note: I pushed The commit keeps the delivery-mirror dedupe state from leaking across slash-command turns. Session-memory still omits command text like Local proof before push:
|
|
Merge-ready at latest head Known risk: this changes session-memory projection for persisted assistant transcript bookkeeping rows, so a wrong predicate could omit or duplicate recalled session context. I’m accepting that risk because the fix stays at the consumer boundary, skips only matching Clean local and ClawSweeper review, green CI, no breaking changes, no config/migration impact, no user-facing change beyond the intended fix. |
…centSessionContent (openclaw#94401) * fix(session-memory): only skip delivery-mirror duplicates, preserve unique DM rows - Skip delivery-mirror rows only when their text duplicates the preceding assistant text (fixes openclaw#92563) - Delivery-mirror rows with unique visible content (e.g., message-tool replies) are preserved - Gateway-injected standalone assistant replies are preserved - Combined with upstream sanitizeSessionMemoryTranscriptText Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(session-memory): reset assistant-text tracking across user turns lastAssistantText persisted across user messages, causing delivery-mirror rows that echoed a previous turn's assistant text to be incorrectly filtered. Reset lastAssistantText to undefined when a visible user message is emitted, so cross-turn delivery-mirror duplicates are preserved while same-turn duplicates are still skipped. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(session-memory): reset mirror dedupe on command turns Signed-off-by: sallyom <[email protected]> --------- Signed-off-by: sallyom <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: sallyom <[email protected]>
…centSessionContent (openclaw#94401) * fix(session-memory): only skip delivery-mirror duplicates, preserve unique DM rows - Skip delivery-mirror rows only when their text duplicates the preceding assistant text (fixes openclaw#92563) - Delivery-mirror rows with unique visible content (e.g., message-tool replies) are preserved - Gateway-injected standalone assistant replies are preserved - Combined with upstream sanitizeSessionMemoryTranscriptText Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(session-memory): reset assistant-text tracking across user turns lastAssistantText persisted across user messages, causing delivery-mirror rows that echoed a previous turn's assistant text to be incorrectly filtered. Reset lastAssistantText to undefined when a visible user message is emitted, so cross-turn delivery-mirror duplicates are preserved while same-turn duplicates are still skipped. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(session-memory): reset mirror dedupe on command turns Signed-off-by: sallyom <[email protected]> --------- Signed-off-by: sallyom <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: sallyom <[email protected]>
Summary
Session-memory hook produces duplicate assistant lines in memory files when the model uses thinking/reasoning, because the session JSONL stores both the raw assistant message (with thinking blocks + text) and a transcript-only
delivery-mirrorcopy of the same response, andgetRecentSessionContent()reads both as valid text messages.getRecentSessionContent()→extractTextMessageContent()extracts text from every message entry. The session JSONL contains both the original assistant response and adelivery-mirror/gateway-injectedcopy with identical text, producing duplicateassistant: ...lines.getRecentSessionContent(), skip delivery-mirror rows only when their text duplicates the preceding assistant text. ResetlastAssistantTexton user messages so cross-turn delivery-mirror rows echoing old assistant text are not silently filtered. Delivery-mirror rows with unique visible content (e.g., message-tool replies) are preserved.src/hooks/bundled/session-memory/transcript.ts— tracklastAssistantText, reset on user messages.src/hooks/bundled/session-memory/handler.test.ts— add message-tool preservation and cross-turn delivery-mirror regression tests.message_endpersistence), handler.ts (hook lifecycle), streaming subscriber, test coverage for existing behavior.Change Type (select all)
Scope (select all)
Linked Issue/PR
Motivation
When using a model with thinking/reasoning (e.g., DeepSeek), the session JSONL persists two copies of each assistant response:
thinking+textcontent blocksdelivery-mirrormodel) containing only thetextblockThe session-memory hook's
getRecentSessionContent()processes all JSONL entries equally viaextractTextMessageContent(), which extracts the firsttype: "text"block from each. Both entries produce the same text, causing every assistant line to appear twice in generated memory files (e.g., after/newor/reset).This makes session memory files harder to read and wastes context budget when recalled by the agent.
Real behavior proof (required for external PRs)
Behavior addressed:
getRecentSessionContent()no longer emits duplicate assistant lines from delivery-mirror transcript entries, while preserving standalone gateway-injected replies.Real environment tested: Linux x64, Node 22.11.0, branch
fix/session-memory-dup-assistant-92563, OpenClawc58e1abf6a(origin/main)Exact steps or command run after this patch:
Evidence after fix:
Terminal console output from running the reproduction script with the patch applied:
Input matrix (8 JSONL entries → 5 output lines):
Vitest regression test result (hooks test suite including the new gateway-injected preservation test):
Updated test suite (commit 79a20ac):
Rebased against upstream/main (which added
sanitizeSessionMemoryTranscriptText). Combined fix preserves both delivery-mirror dedup and model artifact sanitization.Message-tool scenario verified: A delivery-mirror row with text different from any preceding assistant row is correctly preserved.
E2E proof (4 scenarios):
Cross-turn delivery-mirror fix (commit
faaa8211b1):When a delivery-mirror row echoes a previous turn's assistant text after a user message, the fix ensures it is preserved — not incorrectly filtered. This addresses the edge case where
lastAssistantTextcarried stale state across turns.Updated test results (hooks CI-like shard):
oxlint: exit 0, no warnings.Format check:
oxfmt --check— all matched files use the correct format.Observed result after fix:
What was not tested: Live OpenClaw agent session with a thinking model (requires provider API key and gateway). The unit test in
handler.test.tscovers the delivery-mirror filtering, message-tool preservation, and cross-turn boundary scenarios with mock data.Root Cause (if applicable)
The session JSONL stores two entries per assistant response when thinking is used: the raw message (with
thinking+textblocks) and adelivery-mirrormessage (cleaned text-only copy,parentpointing to the raw entry).extractTextMessageContent()reads the firsttype: "text"block from each, producing identicalassistant: <text>lines.Provenance:
introduced by:faba508fe0(feat: add internal hooks system) — initialgetRecentSessionContentmade visible by:bb46b79d3c1(refactor: internalize OpenClaw agent runtime #85341, internalize agent runtime) — delivery-mirror entries appear alongside raw messagescarried forward by:c109a7623b(refactor lint) —extractTextMessageContentextracted as standalone functionRegression Test Plan (if applicable)
handler.test.ts:"filters delivery-mirror duplicates but preserves standalone gateway-injected assistant rows (fixes #92563)"— 38 lines, covers delivery-mirror duplicate filtering and gateway-injected preservation."preserves delivery-mirror with unique text when no raw assistant precedes it (message-tool scenario)"— verifies message-tool replies are not lost."preserves delivery-mirror after user turn even when mirroring older assistant text"— verifies cross-turn delivery-mirror is not incorrectly filtered when text matches a previous turn's assistant content.User-visible / Behavior Changes
Session memory files will no longer contain duplicate assistant lines when using thinking/reasoning models. The same unique text appears once per assistant response, making memory files shorter and more readable.
Diagram (if applicable)
Security Impact (required)
Repro + Verification
Environment
fix/session-memory-dup-assistant-92563Steps
getRecentSessionContent()on the fileExpected
No duplicate
assistant:lines in the extracted content.Actual
Before fix: duplicate assistant lines. After fix: each assistant response appears once.
Evidence
Human Verification (required)
Review Conversations
Compatibility / Migration
Best-fix Verdict
modelat the consumer boundary is the correct layer. Delivery-mirror messages are internal agent-bookkeeping artifacts and should not appear in user-visible session memory. Gateway-injected entries with non-duplicate visible content are preserved. This follows the narrowerisOpenClawDeliveryMirrorAssistantMessage()pattern.lastAssistantTextis reset on user messages, so cross-turn duplicates are preserved while same-turn duplicates are still filtered.AI Assistance 🤖
Risks and Mitigations
Risk: A legitimate
openclawprovider assistant message could be misidentified as transcript-only.Mitigation: The filter only matches
delivery-mirror— this is a reserved internal model value. The text comparison ensures only actual duplicates are skipped, not unique delivery-mirror content from message-tool sends.Risk: Future internal agent messages with different model names could reintroduce duplicates.
Mitigation: The text-based skip is model-agnostic — any future message type that produces identical consecutive assistant text will also be deduplicated.
Fixes #92563