WindowedSessionStorage class final

Append-only session storage over a byte-scanned window of the file.

Segment boundary (gh-1077): this view is bound to the ACTIVE segment (the primary file). Records archived into <path>.part-NN siblings by segment rotation are NOT reachable through loadOlder — paging stops at the primary's header copy and hasOlder goes false there, by design: the chat path only ever renders recent history. Full-chain reads go through JsonlSessionStorage.open (which loads parts + primary as one chain) or JsonlSessionRepo.readCustomRecordsOfType.

The in-memory index holds the LOADED records only; every structure (_entries, _byId, _labelsById, offsets) is pruned in the same pass when eviction drops records, so no strong reference outlives residency (issue #135 round-2 review). The branch walk (getPathToRoot) stops silently at the window edge — records outside are reachable through loadOlder/loadNewer/ jumpToOffset. Side-branch records that share a chunk with the active branch enter the index for id lookups but never render: the branch joins (_joinBranchUpward) walk the parent chain only.

Implemented types

Properties

cachedMetadata → SessionMetadata
The header metadata (parsed at open/creation).
no setteroverride
cachedTotalRecords → int?
The exact record count once countRecords completed; null before — the UI shows "more above" until then.
no setter
countBelow → int?
File records below the window — the "Load newer" banner count. null while unknown (after a jumpToOffset); 0 at the tail.
no setter
hashCode → int
The hash code for this object.
no setterinherited
hasNewer → bool
Whether records sit below the loaded window (the "Load newer" signal — the page-down path back to the live tail).
no setter
hasOlder → bool
Whether records may exist above the loaded window (the "more above" signal while cachedTotalRecords is still unknown).
no setter
reader → SessionChunkReader
The SessionChunkReader backing this window (jump support).
no setter
residentCount → int
Resident-window instrumentation (issue #135 AC1: the bound is asserted by tests, not by user behavior).
no setter
residentWindowBytes → int
no setter
runtimeType → Type
A representation of the runtime type of the object.
no setterinherited

Methods

appendEntry(SessionRecord record) → Future<void>
Appends record to the file and the in-memory index.
override
countAbove() → Future<int?>
File records above the window — the "Load earlier" banner count. null while unknown (background count running, or right after a jumpToOffset until an edge is walked); 0 at the file top.
countRecords() → Future<int>
Background record count (streams newlines, no JSON decode). The first call scans; later calls return the memo.
createEntryId() → Future<String>
Generates a record id that is unique within this storage.
override
currentBranch() → Future<List<SessionRecord>>
The active branch as currently RESIDENT, root-first — the view baseline. Bounded by the residency cap by construction (issue #135 round 4: the app layer must not re-accumulate what the storage evicted).
findEntries(String type) → Future<List<SessionRecord>>
Returns all records of the given type, in file order.
override
getEntries() → Future<List<SessionRecord>>
Returns all records in file order.
override
getEntry(String id) → Future<SessionRecord?>
Looks up a record by id.
override
getLabel(String id) → Future<String?>
Returns the current label attached to the record id, if any.
override
getLeafId() → Future<String?>
Returns the id of the active leaf record, or null at the tree root.
override
getMetadata() → Future<SessionMetadata>
Returns the session metadata (from the file header).
override
getPathToRoot(String? leafId) → Future<List<SessionRecord>>
Walks from leafId to the tree root, returning records root-first.
override
growOlderUntil(bool found(SessionRecord record), {int maxPages = 512, int? tokenBudget}) → Future<bool>
Pages older chunks until found matches a record on the active branch, the file top is reached, or maxPages pass — WITHOUT sliding the newest side out: the residency cap is suspended for the walk, so a resume keeps the tail anchored while reaching the compaction boundary (the CLI resume walks to the newest compaction — the old ensure path let loadOlder evict the leaf on the first chunk, the branch read empty, and every deep-boundary resume fell back to a full open: 10s+ on a marathon file).
ingestAppended() → Future<({List<SessionRecord> delta, bool reanchored})>
Streams externally-appended records (a running fa CLI) into the window. Returns the records that joined the active branch at the tail, oldest-first — the transcript delta to append — and whether the window was RE-ANCHORED (the file shrank: truncation or rotation; issue #135 E5): the delta is then the new window's whole branch and the caller must replace, not append. While the user is deep-paged (window bottom above the file tail) appends only advance the below-count — they page in through loadNewer.
jumpToOffset(int byteOffset, {int maxRecords = defaultChunkRecords, int maxBytes = defaultChunkBytes}) → Future<List<SessionRecord>>
Re-centers the window on the record at byteOffset (issue #135 AC6): the window becomes the chunk around the target, the leaf stays the true file leaf, and both edge counts reset to unknown until an edge is walked. Returns the branch records inside the new window, root-first — the transcript replacement delta. Offsets for repeat jumps come from offsetOf without a re-read.
jumpToRecord(String recordId, {int maxRecords = defaultChunkRecords, int maxBytes = defaultChunkBytes}) → Future<List<SessionRecord>>
Jump-to-record (issue #135 AC6, the mechanism the review asked for): the offset comes from the sparse map when the record was ever read this open — SessionChunkReader.locatePassCount then stays flat, the "instant" AC6 jump — and only unexplored records pay the one-pass SessionChunkReader.locateRecord seek. Returns the re-centered branch (root-first) or an empty list on a miss.
jumpToTail() → Future<List<SessionRecord>>
Re-centers the window on the live tail (issue #1159 E2): a deep-paged view rejoins the newest records in a bounded number of reads — one tail re-anchor plus at most two chunk fills back up to the resident cap (never the chunk-by-chunk page-down crawl) — and hasNewer clears. Returns the re-centered branch (root-first) — the transcript delta the caller re-projects. A window already at the tail re-reads the same range (idempotent).
loadAllOlder({int maxBytes = maxWalkBlockBytes, int maxWholeLedgerBytes = defaultMaxFullOpenBytes}) → Future<void>
Brings the whole file into residency with a sequential forward scan (gh-1555 W1 — the read-everything trajectory surfaces): strips read upward from the file head in bounded chunks, every byte read and parsed exactly once, spliced into the window in ONE prepend. growOlderUntil cannot do this cheaply: its doubling strips answer "where is the NEARBY boundary", and its per-strip record cap truncates each strip — a whole-ledger walk under that cap measured ~13x read+parse amplification on a 30k-record ledger (each 8 MiB strip kept only its newest few hundred records). A no-op when the window already reaches the file head.
loadNewer({int maxRecords = defaultChunkRecords, int maxBytes = defaultChunkBytes}) → Future<List<SessionRecord>>
Pages one chunk of records back in BELOW the window — the page-down path after deep paging slid the newest side out. Returns the records that rejoin the active branch at the bottom, oldest-first (the transcript delta to append).
loadOlder({int maxRecords = defaultChunkRecords, int maxBytes = defaultChunkBytes, int maxChunks = 8}) → Future<List<SessionRecord>>
Loads one chunk of records above the window. Returns the records that JOIN THE ACTIVE BRANCH, root-first — the transcript delta to prepend. Side-branch records in the chunk enter the index (id lookups) but are not returned. Reads chunks until a branch record lands (a chunk can be entirely foreign-branch), the branch root or file top is reached, or maxChunks passes. When the residency cap is exceeded the NEWEST side slides out — deep paging moves the window up; loadNewer (or jumpToOffset) brings the tail side back.
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
offsetOf(String recordId) → int?
The byte offset of recordId if its chunk was ever read this open (the sparse offset map); null for unexplored records.
setLeafId(String? leafId) → Future<void>
Persists a leaf record that moves the active leaf to leafId.
override
toString() → String
A string representation of this object.
inherited

Operators

operator ==(Object other) → bool
The equality operator.
inherited

Static Methods

open(FileSystem fs, String filePath, {int chunkRecords = defaultChunkRecords, int chunkBytes = defaultChunkBytes, int? residentRecords, int? residentBytes, SessionParseExecutor? parseExecutor, SessionIoRetryConfig ioRetry = const SessionIoRetryConfig(), SessionTimingLogger? timingLog}) → Future<WindowedSessionStorage>
Opens a session windowed: header + newest chunk only. Small sessions load completely (window == file) and behave exactly like JsonlSessionStorage.open.

Constants

defaultResidentBytes → const int
defaultResidentRecords → const int
Resident-window bounds (issue #135 AC1): memory is bounded by the cache, not the file. Defaults keep ~3 chunks alive.
maxWalkBlockBytes → const int
maxWalkBlockRecords → const int
Largest single block of the growOlderUntil walk — the per-page doubling ceiling (64 MB / 4096 records keeps one page at ~300 ms and bounds the transient strip buffer).