fix(coding-agent): incremental single-flight session metadata scans - #2043
fix(coding-agent): incremental single-flight session metadata scans#2043snimu wants to merge 4 commits into
Conversation
…ne shared in-flight scan per file readSessionInfo re-read every session file from byte 0 whenever (size, mtime) changed, streamed with no end bound past the stat snapshot on actively growing files, and let concurrent callers stampede duplicate scans of the same path. Scans now fold into a per-file accumulator resumed from the last consumed byte offset (rewrites detected by shrink, same-size mtime change, or a changed prefix tail), are bounded to the size seen at scan start, and concurrent readers share one in-flight scan. A torn trailing line folds into the snapshot only, never into the resumable accumulator. Fixes the defects reported in discussions #1536 and the per-child scan cost of #1671.
…d bound resumable scan state Review fixes for the incremental scan owner: a caller arriving after an append could join an earlier in-flight scan and observe the pre-append snapshot, so per-path scans now chain instead of joining (unchanged files settle with one stat in the cached-hit path); rename rewrites that grow the file while preserving the 16-byte tail window were resumed stale, so resume now also requires an unchanged dev+ino; and resumable accumulators are LRU bounded (1024 files) with eviction when a listed directory disappears, so transcript-sized scan state cannot grow the daemon heap monotonically.
|
Addressed all three review findings in 41427fa:
Bench after the changes (72MB fixture): 15 refresh cycles 0.1MB read / 44ms (main: 396MB / 634ms); 30 concurrent hot readers still collapse to one incremental scan. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 41427fa. Configure here.
…rify inode after each scan Review fixes for the resumable scan cache: the 1024-file LRU bounded entry count, not memory (a huge transcript retains one usage record per assistant message), and a listing larger than the cap evicted its own earlier entries, re-paying full rescans every refresh. The cache is now bounded by total retained usage entries (100k, roughly a few tens of MB worst case) with whole-state LRU eviction only while over the bound, so small states never thrash regardless of catalog size. And a rename rewrite racing a scan between the pre-scan stat and the reads could mix two files' bytes into one cached accumulator: the inode is now re-verified after each scan, discarding the state and rescanning once when it changed.
|
Addressed the three findings in 8467701:
Resource bounds remain unpinned by prior agreement pattern (no behavioral observable without instrumentation); the bound and accounting are stated in the code. |
…ts pins Comment blocks collapse to one- or two-line invariants; the concurrency pins merge into one serialized-scan test, the two rewrite-detection pins become one table (rename vs truncate mode), and the torn-tail pin folds into the resume test. Every fail-unfixed behavior keeps its assertion.
) * refactor(coding-agent): move the semantic-edge ledger onto the event-log substrate The recorder's private append/replay/repair IO is deleted; EventLog owns it, the same move #1987 made for the RLM spawn ledger. One durability rule is unified in the substrate rather than dropped: an unterminated final line is an uncommitted append, skipped on read and truncated before the next append — never newline-completed and never surfaced to a consumer whose next append destroys it. * fix(coding-agent): make the explicit ledger reader's ENOENT contract atomic readSemanticEdgeLedger probed with statSync before reading through EventLog, which swallows ENOENT; a ledger deleted between the two returned [] instead of throwing. The missing-file decision now lives at the single open (replaySync missingFileThrows), so no check-then-read window exists. * docs(coding-agent): state the event-log tail rule once The unterminated-tail contract was restated four times (module doc, replaySync doc, two test comments). It now lives once in the module doc; the method doc keeps only its own parse/missing-file semantics and the test comments reference the contract. * fix(coding-agent): write event-log appends fully and gate appends on tail repair writeSync may write short (ENOSPC after a prefix); appendSync now loops until the payload is fully on disk so write-before-action callers never act on a torn record reported as success. A tail-repair failure (e.g. append-only ACL permitting O_APPEND but not r+) now propagates instead of being swallowed: writing through an unrepaired torn tail would weld it to the new record as permanent interior corruption. ENOENT and the concurrent-writer instability path keep their existing semantics. * fix(coding-agent): reclaim short event-log writes instead of completing them The rlm spawn ledger is multi-writer by documented design (supervisor plus each worker over one file), so completing a short O_APPEND write with a second write could interleave with a rival append and weld two records. A short write now truncates its own torn prefix back off (only while this writer still owns the tail) and fails the append; a torn tail is read-tolerated, a weld is permanent corruption. The append fd opens a+ so the ownership check can read the tail. * fix(coding-agent): leave the torn tail on a short write instead of reclaiming it The tail-match reclaim could truncate a rival's committed record whose final bytes coincide with our torn prefix - committed-data loss, strictly worse than the torn tail it prevented. A short write now just fails the append: the torn tail is the one tolerated shape, skipped on read and truncated by any writer's next repair (verified for both topologies: a resumed single-writer recorder repairs on its first append; every rlm-ledger writer repairs before each append). * refactor(coding-agent): compress event-log comments * fix(ai): omit the default service tier, reprice cache writes from message_delta, repoint the zai default Incorporates #2032 at f82c7fa. * fix(tui,coding-agent): survive lone surrogates in table cells and terminate the WebP EXIF scan Incorporates #2033 at a3d1139. * fix(coding-agent): restart dead kernels on ensure() and read mcp>=2 tool schemas Incorporates #2034 at 749e216. * fix: one crash-safe owner for durable state writes Incorporates #2035 at f0f02d2. * fix(coding-agent): one zombie-aware process-liveness probe Incorporates #2041 at 92a0eac. * fix(coding-agent): snapshot transfer ids from the materialized cursor; mismatches settle the transfer, not the worker channel Incorporates #2044 at 5af3bbe. * fix(coding-agent): failed workers recover on touch; roster gaps answer a structured recovering error Incorporates #2047 at 77b747a. * fix(coding-agent): seven session and IO correctness defects Incorporates #2037 at 41b5d72. * fix(coding-agent): coalesce child-usage attribution and gate agent-status persistence on real changes Incorporates #2050 at 6b0af5d. * fix(coding-agent): incremental single-flight session metadata scans Incorporates #2043 at df032c1. * fix(coding-agent): memoize the passive RLM topology derivation Incorporates #2051 at 0ee114c. * fix(coding-agent): preserve accounting and metadata across deferred updates Keep durable child-usage aggregates separate from pending sibling usage. Retry optional topology metadata after transient reads. Completes #2050 and #2051 integration. * fix: preserve session accounting and read-only persistence boundaries --------- Co-authored-by: Seth <seth@primeintellect.ai>

Purpose
Session-list metadata scans treated append-only session files as immutable documents: every (size, mtime) change triggered a full re-read from byte 0, the read stream had no end bound (so a scan of an actively-appended file chases the moving EOF and holds its FD), and concurrent callers of
readSessionInfofor the same path each opened their own duplicate stream. With ten daemon call sites (catalog list, RLM tree walks, ledger, supervisor) this is the read-amplification and FD-stampede half of the large-tree incidents.Mechanism
One owner per session file for metadata scanning, in
session-manager.ts:readLinesAsBuffersgained an optional{start, end}range), so a growing file cannot extend a scan.All ten
readSessionInfocall sites become readers of this one derivation; no call-site changes.Measurement (72MB fixture: 200 sessions + one 24MB hot session)
Validation
test/session-manager(157),rlm-ledger(31 withfile-lines) pass; rootnpm run checkpasses.LOC
Total src: +323/−138 (net +185); tests: +137/−1 (net +136).
Src +219/−139 (net +80): mechanism change in one owner — full-rescan cache replaced by resumable accumulator + single-flight; no deletions elsewhere. Tests +115.
Squashes discussion #1536 and the per-child scan cost of #1671.
Linear: RES-1272 https://linear.app/primeintellect/issue/RES-1272
Note
Medium Risk
Core daemon hot path for session catalog metadata; incorrect resume or torn-tail handling could show stale counts or usage, though rewrite detection and tests mitigate this.
Overview
Replaces full-file session-list metadata rescans on every size/mtime change with resumable per-file scan state in
session-manager.ts. Each path keeps a fold accumulator plus consumed byte offset, inode identity, and a short prefix tail so appends only read new bytes; shrink, inode/rename, or prefix mismatch forces a full rescan.readSessionInfonow serializes concurrent readers on the same path (queued follow-up scans) and caps retained scan memory via LRU eviction of whole states. Scans are bounded to the file size atstattime through optional{ start, end }onreadLinesAsBuffersand a newreadBytesSynchelper infile-lines.ts. Incomplete trailing JSONL lines contribute to the returned snapshot only, not the persistent offset, avoiding double-count when the line completes.Session directory listing drops scan state for removed or missing files. Tests cover concurrency, incremental resume, rewrite detection, and file recreation.
Reviewed by Cursor Bugbot for commit df032c1. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add incremental resumable single-flight session metadata scans in
session-managerreadLinesAsBuffersand a newreadBytesSynchelper in file-lines.ts support byte-range reads, enabling prefix validation and range-bounded line streaminglistSessionsFromDirevict scan state for deleted or missing session files to prevent recreated paths from inheriting stale metadataMacroscope summarized df032c1.