feat(chat): CT-B1 — bound the transcript by retail's character budget
Campaign CT slice B1.
CORRECTION TO THE PLAN: this slice was written as "the transcript grows for the
life of the session — a slow leak". That was wrong, and the plan said it
because I read the retail-side finding and inferred our side without checking.
ChatLog has always been bounded (ConcurrentQueue, maxEntries default 500, with
a dequeue loop in Append). There was no leak.
The real gap is the UNIT. Retail bounds the rendered transcript by CHARACTERS —
0x2710, beheaded toward 0x1D4C at a newline boundary — while we bounded the
model by messages. Two different things: a window of 500 messages is far more
scrollback than 10,000 characters, and the message cap is a safety limit on the
log rather than a display rule.
So the budget is applied where retail applies it: on the rendered window, not
the model. ChatLog's entry cap stays as the model-level bound.
Two deliberate simplifications, both registered as CT-1 rather than left
implicit:
- ONE threshold, not retail's two. The hysteresis exists to stop retail
re-trimming an accumulating buffer on every append; we rebuild the visible
list each time, so there is nothing to damp, and a second threshold would
only make the oldest visible line jump around as messages arrive.
- Whole-line cutting rather than a newline search near an offset — our unit
already IS the line, which is what retail's newline preference is for.
Filtered-out lines deliberately do not consume budget: a line this window
filters out is not in retail's buffer at all, so counting it would mean turning
a filter OFF silently shortened the visible history.
Solution builds clean; full hermetic gate green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
78bf62c80e
commit
0f1660d6ea
3 changed files with 126 additions and 1 deletions
|
|
@ -158,4 +158,74 @@ public sealed class ChatTranscriptRunsTests
|
|||
IReadOnlyList<UiText.TextRun> line = Assert.Single(runs)!;
|
||||
Assert.All(line, run => Assert.Equal(line[0].Color, run.Color));
|
||||
}
|
||||
|
||||
// ── CT-B1: retail's transcript character budget ─────────────────────
|
||||
|
||||
private static FormattedLine Plain(string text, uint logTextType = 0x02u)
|
||||
=> new(text, ChatKind.LocalSpeech, null, logTextType);
|
||||
|
||||
[Fact]
|
||||
public void AShortHistoryIsKeptWhole()
|
||||
{
|
||||
var detailed = new List<FormattedLine> { Plain("one"), Plain("two") };
|
||||
|
||||
Assert.Equal(0, ChatTranscriptRenderer.FirstLineWithinBudget(detailed, accept: null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheOldestLinesDropOnceTheBudgetIsExceeded()
|
||||
{
|
||||
// Four lines of 10 characters (+1 newline each = 11) against a budget
|
||||
// of 25 keeps the newest two and drops the older two.
|
||||
var detailed = new List<FormattedLine>
|
||||
{
|
||||
Plain(new string('a', 10)),
|
||||
Plain(new string('b', 10)),
|
||||
Plain(new string('c', 10)),
|
||||
Plain(new string('d', 10)),
|
||||
};
|
||||
|
||||
Assert.Equal(
|
||||
2,
|
||||
ChatTranscriptRenderer.FirstLineWithinBudget(detailed, accept: null, budget: 25));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilteredOutLinesDoNotConsumeBudget()
|
||||
{
|
||||
// A line this window filters out is not in its buffer at all, so it
|
||||
// must not push older lines off the top — otherwise turning a filter
|
||||
// OFF would silently shorten the visible history.
|
||||
var detailed = new List<FormattedLine>
|
||||
{
|
||||
Plain(new string('a', 10), logTextType: 0x02u),
|
||||
Plain(new string('x', 100), logTextType: 0x06u), // filtered
|
||||
Plain(new string('b', 10), logTextType: 0x02u),
|
||||
};
|
||||
|
||||
Assert.Equal(
|
||||
0,
|
||||
ChatTranscriptRenderer.FirstLineWithinBudget(
|
||||
detailed, accept: type => type == 0x02u, budget: 25));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildLines_RendersOnlyTheLinesInsideTheBudget()
|
||||
{
|
||||
var detailed = new List<FormattedLine>();
|
||||
for (int i = 0; i < 40; i++)
|
||||
detailed.Add(Plain(new string((char)('a' + (i % 26)), 500)));
|
||||
|
||||
List<UiText.Line> lines = ChatTranscriptRenderer.BuildLines(
|
||||
detailed, maxW: 100000f, Measure, accept: null, defaultColor: LineColor);
|
||||
|
||||
// 40 * 501 = 20,040 characters against retail's 10,000 budget, so
|
||||
// roughly half survive — and crucially the NEWEST half.
|
||||
Assert.True(lines.Count < detailed.Count, "the oldest lines should have dropped");
|
||||
Assert.Equal(detailed[^1].Text, lines[^1].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheBudgetIsRetailsOwnNumber()
|
||||
=> Assert.Equal(0x2710, ChatTranscriptRenderer.MaxTranscriptCharacters);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue