fix(gateway): report omitted chat-history messages in truncation log#96788
Conversation
enforceChatHistoryFinalBudget reported placeholderCount: 0 when it kept only the last message and silently dropped all earlier ones, so the chat.history caller's "log only when count > 0" gate never fired and operators had no signal that history was truncated. Rename the field to omittedCount and have every omitting branch report the number of input messages that lost their verbatim representation. Also derive the front byte-cap drop count at the caller so that omission source is logged too.
|
Codex review: passed. Reviewed June 25, 2026, 4:16 PM ET / 20:16 UTC. Summary PR surface: Source +35, Tests +219. Total +254 across 4 files. Reproducibility: yes. Current-main source shows the zero-count keep-last helper branch and the positive-count caller gate, and the PR body includes a negative-control real WebSocket run where the same request test fails before the fix. Review metrics: 1 noteworthy metric.
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. Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land this whole-pipeline Gateway omission-accounting fix as the canonical path, then close the linked issue and supersede the remaining narrower duplicate PR. Do we have a high-confidence way to reproduce the issue? Yes. Current-main source shows the zero-count keep-last helper branch and the positive-count caller gate, and the PR body includes a negative-control real WebSocket run where the same request test fails before the fix. Is this the best way to solve the issue? Yes. Counting omitted originals after replacement, front-capping, and final-budget enforcement is the best fix I saw because it covers the production caller path and avoids double-counting partial-stage accounting. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against d2da8c79d9b8. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +35, Tests +219. Total +254 across 4 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
|
…nostic Address review: move omission accounting to a single owner that sees the whole replace/cap/final pipeline. enforceChatHistoryFinalBudget now returns only its surviving messages; the new reportOmittedChatHistory helper counts, by message identity, how many original messages lost their verbatim form and emits the truncated diagnostic. Identity membership counts a message that is first replaced and then trimmed exactly once, fixing the double-count in the previous additive sum. Add a real-behavior test that runs the production helpers and asserts the real payload.large/truncated diagnostic event fires with the unique count.
|
@clawsweeper re-review Addressed both findings:
|
|
🦞🧹 I asked ClawSweeper to review this item again. |
Drive a real chat.history request over a booted Gateway WebSocket server against a real on-disk transcript and assert the payload.large/truncated diagnostic fires with the unique omitted count. Imports only the WS harness and diagnostic bus, so the same test reproduces the missing diagnostic on pre-fix main.
|
@clawsweeper automerge |
|
🦞✅ Source: What merged:
Automerge notes:
The automerge loop is complete. Automerge progress:
|
…penclaw#96788) Summary: - The PR moves Gateway `chat.history` omission accounting to a whole-pipeline reporter and adds focused helper plus real WebSocket request regression tests. - PR surface: Source +35, Tests +219. Total +254 across 4 files. - Reproducibility: yes. Current-main source shows the zero-count keep-last helper branch and the positive-coun ... he PR body includes a negative-control real WebSocket run where the same request test fails before the fix. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(gateway): count unique omitted chat-history messages + prove diag… - PR branch already contained follow-up commit before automerge: test(gateway): prove chat.history request emits omission diagnostic Validation: - ClawSweeper review passed for head 414f885. - Required merge gates passed before the squash merge. Prepared head SHA: 414f885 Review: openclaw#96788 (comment) Co-authored-by: ZengWen-DT <ceng.wen@xydigit.com> Approved-by: takhoffman
…penclaw#96788) Summary: - The PR moves Gateway `chat.history` omission accounting to a whole-pipeline reporter and adds focused helper plus real WebSocket request regression tests. - PR surface: Source +35, Tests +219. Total +254 across 4 files. - Reproducibility: yes. Current-main source shows the zero-count keep-last helper branch and the positive-coun ... he PR body includes a negative-control real WebSocket run where the same request test fails before the fix. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(gateway): count unique omitted chat-history messages + prove diag… - PR branch already contained follow-up commit before automerge: test(gateway): prove chat.history request emits omission diagnostic Validation: - ClawSweeper review passed for head 414f885. - Required merge gates passed before the squash merge. Prepared head SHA: 414f885 Review: openclaw#96788 (comment) Co-authored-by: ZengWen-DT <ceng.wen@xydigit.com> Approved-by: takhoffman
What Problem This Solves
enforceChatHistoryFinalBudget(the final byte-budget step of thechat.historyGateway response) returnedplaceholderCount: 0in the branch that keeps only the last message while silently dropping every earlier one:The caller logs truncation only when that count is positive:
So whenever history was trimmed down to its last message, the count was
0, the gate never fired, and the truncation was invisible to operators. The front byte cap (capArrayByJsonBytes) also drops the oldest messages without reporting a count, so that omission went unlogged too. Closes #96783.Why This Change Was Made
The count was being assembled from per-step partial counts that did not compose: a step's "placeholders inserted" is not the same as "history omitted", and summing
replacedCount + front-cap drops + final-budget countdouble-counts a message that is first replaced with a placeholder and then trimmed by the byte cap.The fix gives the omission count a single owner that sees the whole pipeline.
enforceChatHistoryFinalBudgetnow returns only its surviving messages (its message behavior is unchanged). A newreportOmittedChatHistoryhelper counts, by message identity, how many of the original messages lost their verbatim representation anywhere in the replace → cap → final-budget pipeline, and emits the diagnostic. Identity membership counts each omitted original exactly once.User Impact
Operators now get the
payload.large/truncateddiagnostic (and the debug line) wheneverchat.historydiscards older history for any reason — drop-to-last, placeholder replacement, or front byte-cap trimming — with an accurate, non-inflated count. No API/response shape change for clients; this is purely diagnostic accounting. The internal field rename touched only the helper, its single Gateway caller, and the focused test (the TUI / embedded-stub callers read onlymessages).Evidence
Real Gateway request (after fix). A full
chat.historyrequest issued over a booted Gateway WebSocket server against a real on-disk transcript now emits the truncation diagnostic. The new regression testchat-history-omission-request.test.tsboots the gateway via the standard server harness, seeds a session store + transcript on disk (12 messages, budget lowered to 8 KB through the existingsetMaxChatHistoryMessagesBytesForTestseam), then drivesrpcReq(ws, "chat.history", …)end-to-end through the realhandleChatHistoryRequest(not the budget helpers in isolation) and captures the real diagnostic event bus output:12 messages in, 3 survive the byte budget, 9 older messages omitted →
count: 9.Before (negative control). Checking out pre-fix
main'schat.tsand running the same real-WS test reproduces the bug: the request still omits 9 messages but emits no diagnostic, so the assertion fails. The test imports only the WebSocket harness and the diagnostic bus (no changed symbols), so it compiles and runs against bothmainand the PR head — the only difference is whether the diagnostic fires.Premise. On
mainthe caller computedplaceholderCount = replaced.replacedCount + bounded.placeholderCount. The front byte cap (capArrayByJsonBytes) and the keep-last branch ofenforceChatHistoryFinalBudgetboth omit messages without contributing to either term, so theif (placeholderCount > 0)gate never fired for those paths. The fix routes all omission accounting throughreportOmittedChatHistory, which counts originals missing from the final survivor set by identity (each omitted original once).Focused tests exercise the real exported functions and the real WS request path:
chat-history-omission-request.test.ts: a real Gateway WSchat.historyrequest emitspayload.large/truncatedwith the unique omitted count (output above); reproduces the missing diagnostic on pre-fixmain.chat-history-omission-logging.test.ts: the diagnostic fires when history is trimmed, does not fire when nothing is omitted, and a message that is replaced and then front-capped is counted once (strictly less than the old additivereplacedCount + frontCapDroppedsum).chat-history-budget.test.ts:enforceChatHistoryFinalBudgetreturns the right surviving messages (pass-through, keep-last preserving the original reference, oversized placeholder, never-empty sentinel).Formatting verified with
oxfmt.AI-assisted.