Skip to content

Dashboard transcript saves bypass the 2 MB session rotation cap (grew to 2.87x the limit) #6128

Description

@SebastianYuSun

Summary

_SESSION_MAX_BYTES (2 MB) is enforced in exactly one place — ConversationLog.append, which calls _maybe_rotate. The dashboard persists a slot by rewriting the whole transcript through atomic_write in _save_slot_to_history and never reaches that check, so a transcript written only by the dashboard grows without bound.

docs/system-specs/modules/history.md documents the cap unqualified ("Rotation at 2MB (keeps metadata + last 200 messages, atomic write)"), so the spec and the dashboard write path disagree.

Where

Anchor What
src/kiro_crew/history.py _SESSION_MAX_BYTES = 2 * 1024 * 1024
src/kiro_crew/history.py _maybe_rotate — the enforcement
src/kiro_crew/history.py its only call site, inside append
src/kiro_crew/dashboard/chat_persistence.py atomic_write(path, payload, fsync=True) — the dashboard write, with no rotation after it

_MAX_SLOT_MESSAGES in dashboard/state.py bounds the in-memory window only; trimmed rows become frozen-prefix bytes on disk, so it is not a size bound on the file.

Reproduction

Control/experiment, so "no rotation" cannot be confused with "the cap does not work". Both arms write the same shape of data to the same kind of file; only the writer differs.

import pathlib, sys, tempfile
sys.path.insert(0, "src"); sys.path.insert(0, "test")

import kiro_crew.history as H
from kiro_crew.history import ConversationLog

BIG = "z" * 100_000      # 30 messages ~= 3 MB, over the 2 MB cap
N = 30
tmp = pathlib.Path(tempfile.mkdtemp())

# CONTROL: the append path
ctrl = ConversationLog(tmp / "ctrl")
for i in range(N):
    ctrl.append("dashboard:control", "user", f"{i}:{BIG}")
print("append   :", ctrl._path("dashboard:control").stat().st_size, "bytes")

# EXPERIMENT: the dashboard whole-file save path
import kiro_crew.dashboard.state as dstate
dtmp = tmp / "dash"; dtmp.mkdir(parents=True, exist_ok=True)
dstate.config_dir = lambda: dtmp
from chat_test_helpers import _make_state
from kiro_crew.dashboard.chat_persistence import _save_slot_to_history, slot_history_key

state = _make_state(dtmp); slot = state.get_or_create_slot("bigsession")
for i in range(N):
    slot.append("user", f"{i}:{BIG}", "msg")
slot.drain(); _save_slot_to_history(state, slot, force=True)
path = state.conversation_log._path(slot_history_key(slot))
print("dashboard:", path.stat().st_size, "bytes")

Observed:

_SESSION_MAX_BYTES = 2097152

append   : 2001763 bytes   rotated_at set,     under cap
dashboard: 3005449 bytes   rotated_at absent,  OVER cap

Taking one more ordinary save reaches 6,010,744 bytes = 2.87x the cap, and rotated_at is never set — rotation was not consulted and declined, it was never consulted. The control arm rotating under identical load is what makes this a writer gap rather than a broken cap.

Note on the "200 messages" figure

_SESSION_KEEP_LINES = 200 is the retain count applied after the byte check gates, not an independent cap. 600 small messages through append produce a 600-line file and no rotation. So the bypassed invariant here is the 2 MB byte cap only; a high message count alone does not trigger rotation on any path.

Why a fix is not a one-liner

Calling _maybe_rotate after the dashboard write is necessary but not sufficient. Rotation removes leading messages, which moves the frozen-prefix boundary the slot tracks in _disk_older_count. Left unreconciled, every later save rebuilds its payload from a prefix that is no longer on disk, resurrecting the dropped messages so rotation drops them again.

That damage is invisible in the file — rotation re-trims what was resurrected, so the transcript stays byte-identical and duplicate-free. What changes is the work: measured over 5 ordinary post-rotation saves, the unreconciled version rotates on all 5 and re-archives the same 10 lines each time (50 archived lines vs 0), turning an O(window) steady state into O(file) with unbounded archive growth.

Also noticed

src/kiro_crew/history.py's module docstring says the session cap is 512KB while the constant is 2 MB. Separate stale-doc defect, mentioned only so it is not lost.

rewrite_session is a second whole-file writer with no rotation, but it currently has no callers outside history.py, so it is latent rather than live.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreCore runtime utilities, config/paths, process singletonsarea: dashboardDashboard UI and its backend handlersbugSomething is not working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions