Repository navigation
[RSI] fix(coding-agent): speed up session append and fork hot paths - #2276
Conversation
Prime Agent performance — completedPR Overall: 0 regressed · 0 improved · 17 no clear change.
Python runtime
Sandbox cost: ~$0.0947 — no inference calls. Methodology and samplesMain resolved at 2026-09-13T06:23:57.563551+00:00. Harness
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6104585. Configure here.
Review fix (high): forkFrom issued one writeSync per JSONL line and ignored the returned byte count, so a short write left a torn line that the next reopen repaired as crash damage, silently dropping forked entries. Route the header and entry writes through a writeAllSync helper that loops until the whole buffer lands, matching the short-write contract writeFileAtomicSync already enforces. Regression test: inject a partial-write descriptor (at most 3 bytes land per buffer writeSync call, 1 byte for the string form) and assert the fork completes with the full header and both messages on disk and a fresh reopen loads the entries with parents intact. Against the ignore-the-count code the test fails with 1 recoverable line instead of 3. Review fix (low): retarget the forkFrom comment, the fork test title, and the bench header at the current single-descriptor write path instead of the removed per-entry appendFileSync path; the bench bullets now describe the flip-once guard cache and the one-descriptor fork.

What
SessionManager's append and fork hot paths pay per-entry costs that scale with session size (cycle B perf report, candidate C3):
_persistran an O(entries)fileEntries.some(...)scan on every appended entry to answer "has an assistant message been persisted yet" — a boolean that can only flip false→true once. Sessions before their first assistant response (goal state, queued messages, harness/custom entries) pay the full scan per append: ~100 µs per append at 20k in-memory entries, all synchronous on the worker's event loop.forkFromwrote the forked file with oneappendFileSync(open+write+close) per source entry: a 20k-entry fork stalled the caller's thread for ~0.5 s of syscalls.Every message, tool result, bash execution, and agent status append in every resident session (including all in-process subagents) goes through
_persist, so this is a per-turn, per-session tax.Change
packages/coding-agent/src/core/session-manager.tsonly:hasAssistantEntrycache._appendEntrysets it on the first assistant message (O(1)); everyfileEntriesreassignment (setSessionFile/open,newSession,materializeSessionFile, rollback pop,createBranchedSession— both paths) refreshes it via_refreshHasAssistantEntry()(cold, once per reassignment)._persistnow consults the cached flag instead of rescanning.forkFromwrites the whole fork through a single descriptor: oneopenSync+ per-entrywriteSync+ onecloseSync, constant memory (no full-file join).createUniqueSessionFileTargetguarantees the target does not exist, so"w"creates it exactly like the first append used to. Failure semantics unchanged: on error the partial file stays behind and the next fork gets a fresh unique target.existsSync(this.sessionFile)stat is deliberately kept, with a comment: it is what recovers a live session whose file was deleted underneath it — without it,appendFileSyncwould recreate a headerless stub. That recovery is pinned bytest/session-manager/session-state.test.ts("rewrites the full session if the session file disappears after flushing"). The benchmark below shows steady-state appends are dominated by the disk write, not this stat, so the candidate report's suggestion of caching it was dropped: it would break that pinned recovery for no measurable gain.Micro-benchmark
npx tsx test/session-manager-append-bench.ts(committed; same machine, main vs this PR):Guard-path appends are now flat O(1) instead of O(entries); fork writes drop from O(entries) open/close pairs to one. forkFrom rows include the constant git-context subprocess cost on both sides. The steady-state row is unchanged by design: it is dominated by the (pinned) existence stat and the disk append itself.
Validation
npm run check(biome + tsgo + installer + browser-smoke): clean.npx vitest run test/session-manager/ test/session-manager-flush.test.ts test/session-manager-git-state.test.ts: 171 passed — includes the pinned flush-order, no-assistant-guard, rollback, and deleted-file-recovery tests.npx vitest run test/agent-session-recursion.test.ts test/daemon-supervisor-lazy-subagents.test.ts test/rlm-ledger.test.ts test/atomic-persistence.test.ts test/session-info-modified-timestamp.test.ts: 190 passed (fork consumers + persistence).test/session-manager/append-hot-path.test.ts(7 tests): pre-assistant suppression with full-history rewrite on the first assistant message;session_statepersists pre-assistant while messages stay suppressed; flag recomputed on reopen; guard re-arms when branching to an assistant-less path; flag survives a rolled-back custom append; fork issues zeroappendFileSynccalls (spy) with git_state dropped and parents re-linked; forked pre-assistant sessions stay suppressed.No open PR touches
core/session-manager.ts(checked against the cycle B dedupe list and an open-PR title search).This PR was authored autonomously as part of a recursive self-improvement program (RSI). Source report:
~/rsi/reports-cycle-b/perf.md, candidate C3.Note
Medium Risk
Changes core session persistence invariants (pre-assistant guard and fork I/O); regressions could corrupt or suppress JSONL, though behavior is heavily tested and steady-state append semantics are intentionally preserved.
Overview
Speeds up session JSONL appends and forks by removing work that scaled with session size on every turn.
Append path:
_persistno longer scans all in-memory entries on each append to decide whether an assistant message exists. A flip-oncehasAssistantEntrycache is set on the first assistant append and recomputed only when the entry list is replaced (open, new session, branch, rollback, materialize). Pre-assistant suppression andsession_state/session_infoexceptions behave the same; the per-appendexistsSynccheck stays for deleted-file recovery.Fork path:
forkFromwrites the new file with oneopenSync, repeatedwriteSyncviawriteAllSync(handles short writes), and onecloseSyncinstead ofappendFileSyncper copied line. Git-state dropping and parent re-linking are unchanged.Adds a manual benchmark script, changelog note, and tests for guard/cache behavior, single-descriptor fork, and partial
writeSynchandling.Reviewed by Cursor Bugbot for commit 5246257. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Speed up
SessionManagerappend and fork hot pathsSessionManager, replacing a fullfileEntriesscan on every_persistcall with constant-time lookup; the cache is maintained across loads, branches, resets, and rollbacks.appendFileSyncloop inSessionManager.forkFromwith a singleopenSyncdescriptor and a newwriteAllSynchelper that retrieswriteSyncon partial writes and throws on no progress; the descriptor is closed in afinallyblock.writeAllSyncthrows whenwriteSyncmakes no progress; forks that previously silently produced partial files now fail instead. Rollback in_appendEntryrecomputes_refreshHasAssistantEntry, so any future code path that mutatesfileEntriesoutside the existing append/load/branch/reset flows must also refresh the cache.Macroscope summarized 5246257.