Skip to content

[RSI, performance] count tool-result message entries from their serialized header - #2416

Merged
snimu merged 6 commits into
mainfrom
rsi/perf-agents-roster
Sep 21, 2026
Merged

snimu merged 6 commits into
mainfrom
rsi/perf-agents-roster

Conversation

@sethkarten

@sethkarten sethkarten commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Cold saved-session catalog scans (SessionManager.list/listAll -> scanSessionInfo) parse every transcript line with JSON.parse, but tool-result entries — the largest share of bytes in a transcript-heavy catalog — feed the scan fold nothing but their message count. This PR counts those entries from their serialized header instead of parsing the payload. Any layout the file writer does not produce (spacing, another key order, a nested container before the role marker, a role that runs past the bounded prefix) falls back to the full parse, so behavior only narrows.

Profiling (instrumented local run, ui.py fixture catalog: 201 sessions, 106 MB)

Reproduced the agents_roster benchmark path (saved-session list + roster composition) with a cold-daemon TUI driver on a scratch agent dir:

  • Roster-window work: 0.40 s, of which the catalog-scan child spends 0.31 s CPU; the TUI ~0.09 s. The rest of the benchmark wall is its own 3 s settle window.
  • Cold SessionManager.listAll = 206 ms: JSON.parse ~120 ms, file read+decode ~45 ms, fold/overhead ~40 ms.
  • Parse split by entry role: toolResult 52 ms (57 MB), assistant 55 ms (37 MB), user 15 ms (12 MB). Tool results are 54% of bytes / 42% of parse time, yet the fold reads nothing from them but messageCount.
  • Not candidates after measuring: ledger hydration 2 ms, agents-view row composition sub-ms, socket payload stringify+parse ~7 ms.

Change

isCountOnlyMessageLine() (session-manager.ts) inspects a 512-char serialized header prefix:

  • requires the structural markers "type":"message" and "message":{"role":" (" is escaped inside JSON strings, so a match cannot come from quoted text);
  • rejects any { before the role marker (the marker carries the message object's own brace, so an earlier container means the found role is not the entry's role);
  • rejects a role value that would run past the prefix;
  • roles user/assistant still take the full parse (they feed usage, model, search text, firstMessage, and lastActivityTime);
  • guarded on the session header being seen, so damaged-file detection still parses the first entry.

Returned SessionInfo is byte-identical before/after (digest over every field incl. allMessagesText, usage, firstMessage, modified, message count) on all three fixture catalogs.

Intentional, bounded behavior delta: a structural-but-unparseable line (torn in-flight append, truncated tail) now counts toward messageCount where a failed parse used to skip it — transient while the tail is in flight, self-correcting once the line completes. No other field can differ (count-only roles never fed any other fold output).

Numbers (5 cold scans per side, separate processes, medians)

Production-shaped catalog (type-key-first layout, as SessionManager writes):

before (main) after delta
wall 218.3 ms 182.6 ms −16.4%
CPU 287.0 ms 257.8 ms −10.2%

Fixture-shaped benchmark layout: wall 218.4 -> 182.3 ms, CPU 287.3 -> 258.0 ms. Pretty-printed catalog (no layout match, exercises the fallback): unchanged.

Expected agents_roster CI movement: the scan is the dominant roster-window cost, so the CPU metric moves by the scan's share (single-digit %); the wall metric is additionally floored by the benchmark's 3 s settle window, so expect a modest wall change. agents_open/agents_reopen and every incremental rescan share the same win.

Tests

packages/coding-agent/test/session-manager/message-count-scan.test.ts (10 tests):

  • header-based counting for both key orders and for a tool result whose payload does not parse (fail on pristine main);
  • torn tail with no trailing newline counts from its header once and settles to exactly one count after the line completes (fails on pristine main);
  • guards that pass both ways: escaping, container rejection (incl. the nested shadow-role layout), role-fit at the 512 boundary, oversize ordering, header guard, spacing fallback;
  • the two hardening tests fail on the unhardened predicate and pass after it.

Targeted suites (test/session-manager/, session-scan-retention, session-reader-persistence, saved-session-catalog, daemon-session-list): 222 passed. npm run check: pass.

No overlap with open PRs #2393 (daemon-mode/daemon-session-list), #2389 (cron-jobs), #2405/#2409 (kernel bootstrap): this touches only the session-manager scan path, its test, and a changeset.

Checklist

  • No benchmark gaming; no semantic changes for writer-produced layouts (SessionInfo digest-identical)
  • Regression tests mirror existing session-manager test patterns and fail pre-fix
  • npm run check + targeted vitest on touched areas

Test-line budget

Fleet test-budget audit (2026-09-18): this PR exceeded the repo test-line budget gate (node scripts/check-test-policy.mjs, run by CI's Build and check job: net added test lines over changed test files may not exceed meaningful added source lines, and changed test files may not add per-category violations). The user directive driving the audit: "probably needs to remove redundant and unnecessary tests throughout. Remove unnecessary tests and we should have enough." The owner policy decision authorized executing the named-vector cut menu in cost order: "nah dont grandfather them in. we should fix them now".

Both numbers, because the two views differ after a branch update with main:

  • Branch point (what the failing CI run measured): 299 net test additions vs 22 meaningful source additions. The vectors lived in a standalone message-count-scan.test.ts (299 lines) that re-declared its own temp-dir and fixture harness.
  • Main tip / post-merge (what CI enforces after the branch meets main): 27 added / 5 deleted test lines = 22 net vs 22 source, green, zero category violations. Measured at this head with GITHUB_BASE_REF=main node scripts/check-test-policy.mjs (merge base is now main itself), and the same check runs inside GITHUB_BASE_REF=main npm run check, which exits 0.

Why the views differ (worth knowing for the other PRs in this wave): the first compression pass also consolidated several pre-existing clusters in test/session-manager/file-operations.test.ts, but main had already consolidated exactly those clusters itself, so those deletions became no-ops the moment the branch met main. The budget is therefore carried only by this PR's own vectors, which now live inside main's consolidated file and reuse its tempDir/header/msg/line harness.

Surviving vectors (all in packages/coding-agent/test/session-manager/file-operations.test.ts, table rows unless noted): an unparsed tool result counted from its serialized header; an oversized tool result counted without reading its payload; tool results in the id-first key order (complete + torn); a container before the role marker falling back to the full parse; the role value starting exactly at the 512-character prefix boundary falling back too; and the damaged-session null path (readSessionInfo null + listAll empty). Two probes confirm the two subtlest pins are live: reverting src/core/session-manager.ts to the base makes the unparsed and id-first rows fail, and deleting the prefix-boundary early-out makes only the boundary row fail.

Lost-coverage ledger (owner-authorized cuts, per vector)

Each row records what the cut pinned, why it was the cheapest option per line, and any surviving partial pin. Nothing disappears silently.

Traded for the restored boundary pin (four rows that are true same-observable duplicates):

  • findMostRecentSession table, row "returns null for an empty directory" and row "returns null for a non-existent directory": both assert the same observable (no session found in a directory that holds no sessions). Surviving pins: the "ignores non-jsonl files" and "ignores jsonl files without a valid session header" rows still assert the null result, and the remaining rows still cover selection and recency.
  • loadEntriesFromFile table, row "an empty file" and row "malformed JSON": both assert the same observable (empty entry array for a file with no usable records). Surviving pins: the "a missing file" and "a file without a session header" rows assert the same empty-array result; the "malformed line between valid ones" row still covers malformed input inside a valid file.

Cuts to this PR's own new vectors:

  • standalone message-count-scan.test.ts (299 lines): deleted; its vectors were folded into file-operations.test.ts to reuse that file's existing harness instead of re-declaring it per file (the harness alone was ~50 lines).
  • table row "a spaced layout": pinned that a differently-spaced header falls back to the full parse. Cheapest of the remaining fallbacks - relaxing the exact-match spacing could only route more lines through the fast path, which counts them correctly either way. Surviving: none for that layout; the container and boundary rows still pin fallbacks that matter for text fidelity.
  • table row "a quoted header inside text": pinned that an assistant message quoting the header layout is still folded as an assistant message. The property is structural rather than a code guard: those bytes are backslash-escaped inside the JSON string, so the header search cannot match them. Surviving: the source comment states the invariant; no test.
  • table row "an embedded header inside an entry payload": pinned that a non-message entry whose payload embeds the header layout is not counted. Surviving partial pin: the container-before-the-role-marker row exercises the same "the role found is not the entry's own" branch; session_info name handling is pinned by the labels/session-state tests.
  • torn-tail-across-incremental-scans vector: pinned that a tool-result line torn mid-write is counted exactly once across a resumed scan. Surviving partial pin: the existing "resumes from the scanned offset: prefix never re-read, torn tail folded exactly once" test pins the resume path itself, but with an assistant-role torn line, so the tool-result fast path is not exercised there.
  • usage-summary assertions on the fast-path rows: the fixtures carry no assistant usage, so usage is not asserted there. Surviving pin: "scan and resident computation agree on whole-file own spend, forks and attributions included".
  • modified-timestamp assertions: dropped from the fast-path rows. Surviving pin: test/session-info-modified-timestamp.test.ts.
  • earlier pre-existing-test consolidations (the empty-array group, the truncates/rewrites pair, the preserved-path/subsequent-loads pair): reverted by the merge with main because main had already consolidated exactly those clusters into its own tables. No coverage was lost - every case still runs in main's version.

Note

Medium Risk
Changes the hot path for listing saved sessions; edge cases include dropping headerless files that start with a tool result and counting some torn/unparseable tool-result lines from headers where parse used to skip them.

Overview
Cold session catalog scans (readSessionInfo / SessionManager.listAll) no longer JSON.parse every line when a message entry only contributes messageCount.

isCountOnlyMessageLine inspects the first 512 characters for writer-shaped "type":"message" and "message":{"role":" markers, treats roles other than user / assistant as count-only (e.g. toolResult), and falls back to full parse when spacing, key order, nested containers, or a role past the prefix do not match. foldSessionScanLine uses this only after a valid session header is seen so the first line still validates damaged files.

Oversized tool payloads can increment messageCount without reading megabytes of content. Tests in file-operations.test.ts cover header counting, fallback layouts, and rejecting a file whose first line is a tool result; a few duplicate empty/malformed loader cases were removed.

Reviewed by Cursor Bugbot for commit f9c4c9c. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Count tool-result message entries from serialized header in session scan

  • Adds isCountOnlyMessageLine to session-manager.ts, a bounded classifier that inspects only the first 512 characters of a serialized line for message type and role markers. It rejects lines with earlier nested containers or roles reaching beyond the prefix.
  • Inserts a fast path in foldSessionScanLine where qualifying non-user/non-assistant entries increment messageCount and return before JSON parsing. Unsupported layouts fall through to the existing full-parse path.
  • The first entry in a session is always fully parsed so damaged files can be rejected by header validation.
  • Risk: sessions whose first entry is a tool result are now dropped and excluded from SessionManager.listAll results, since the fast path only activates after a valid session header is seen.
📊 Macroscope summarized f9c4c9c. 2 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

packages/coding-agent/src/core/session-manager.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 1305: The fast path returns before updateLastActivityTime, but every persisted message entry has its own timestamp (see appendMessage at session-manager.ts:1808-1815), and the previous full-parse path updated activity for every entry. Consequently, any normally sized tool-result, bashExecution, or custom message matching this header leaves the catalog's modified/last-activity value at the preceding entry. For example, a session whose last event is a tool result will be listed and sorted with a stale modification time. Restricting the optimization to roles whose entry timestamp is known irrelevant, or extracting/updating the entry timestamp from the header, preserves the existing catalog semantics. [ Failed validation ]

…alized header

A cold saved-session catalog scan parses every transcript line, and a
transcript-heavy catalog spends most of that parse on tool results: in the roster
fixture (201 sessions, 106 MB) tool results are 54% of the bytes and 42% of the
JSON.parse time, yet the fold reads nothing from them but the message count.

Read the entry header instead of the payload for message entries whose role can
only contribute that count. The header check is bounded to the first 512
characters, and it falls back to the full parse for every layout the file writer
does not produce: spaced JSON, another key order, a role value that runs past the
prefix, and any container before the role marker. That container guard covers
both a payload nested before the type key and a payload that quotes the header
before the entry's own message key, so a shadowed role marker can never hide a
searchable entry's text. A double quote is escaped inside a JSON string, so a
header that matches can only be structural.

Measured on the same fixture, 5 cold scans per side, medians:

  scan wall  218.4 ms -> 182.6 ms  (-16%)
  scan CPU   286.9 ms -> 257.8 ms  (-10%)

The id-first catalog layout gives the same win, the hardenings cost nothing
measurable, and the returned SessionInfo is byte-identical before and after on
every catalog measured.
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

Prime Agent performance — completed

PR f9c4c9ca compared with main e311d649.

Overall: 1 regressed · 0 improved · 40 no clear change.

Metric Main This PR Change
Cold startup 784.1 ms 763.7 ms ≈ -20.5 ms (-2.61%)
Warm startup 501.4 ms 543.4 ms ≈ +42.0 ms (+8.38%)
Installation 5.98 s 7.77 s ≈ +1.79 s (+29.93%)
Compressed release artifacts 72.84 MB 72.90 MB ≈ +0.06 MB (+0.08%)
Installed footprint 594.23 MB 593.69 MB ≈ -0.54 MB (-0.09%)
Idle memory, summed RSS 819.01 MB 817.44 MB ≈ -1.57 MB (-0.19%)

Python runtime

Metric Main This PR Change
Python kernel startup 32.8 ms 31.6 ms ≈ -1.1 ms (-3.49%)
Python cell round trip 0.105 ms 0.097 ms ≈ -0.008 ms (-7.30%)
Empty bash command 2.1 ms 2.0 ms ≈ -0.093 ms (-4.41%)
Bash git status 2.8 ms 2.6 ms ≈ -0.2 ms (-7.39%)
Bash 32 KiB output 2.2 ms 2.1 ms ≈ -0.1 ms (-4.96%)
35 cells / 9 shell calls 27.9 ms 26.0 ms ≈ -1.9 ms (-6.96%)
Python interrupt to done 0.544 ms 0.508 ms ≈ -0.036 ms (-6.65%)
Python state snapshot 9.8 ms 9.8 ms ≈ +0.075 ms (+0.77%)
Python state restore 130.1 ms 122.0 ms ≈ -8.2 ms (-6.27%)
Python idle RSS 21.08 MB 21.13 MB ≈ +0.05 MB (+0.24%)
Python RSS after pandas workload 75.46 MB 75.52 MB ≈ +0.06 MB (+0.08%)

Session transport

Metric Main This PR Change
Private frame decode, 32 MiB in 8 KiB chunks 16.5 ms 14.3 ms ≈ -2.2 ms (-13.15%)

UI interactions

Metric Main This PR Change
Resume large session (cold) 2,005.7 ms 1,816.6 ms ≈ -189.1 ms (-9.43%)
CPU, resume large session 2,180.0 ms 2,050.0 ms ≈ -130.0 ms (-5.96%)
Switch into large session 1,808.5 ms 1,595.7 ms ≈ -212.8 ms (-11.77%)
CPU, switch into large session 2,190.0 ms 1,910.0 ms ≈ -280.0 ms (-12.79%)
Open agents view from a session 504.3 ms 516.6 ms ≈ +12.2 ms (+2.43%)
CPU, open agents view 890.0 ms 870.0 ms ≈ -20.0 ms (-2.25%)
Full agents roster, many sessions 4.03 s 3.63 s ≈ -0.40 s (-9.92%)
CPU, full agents roster 0.88 s 0.77 s ≈ -0.11 s (-12.50%)
Open another session from agents view 1,861.9 ms 1,886.5 ms ≈ +24.6 ms (+1.32%)
CPU, open from agents view 970.0 ms 1,060.0 ms ≈ +90.0 ms (+9.28%)
Reopen resident large session 259.8 ms 281.5 ms ≈ +21.7 ms (+8.35%)
CPU, reopen resident session 290.0 ms 290.0 ms ≈ +8.9e-13 ms (+0.00%)
Open subagent session at depth 6 17,472.4 ms 17,571.1 ms ≈ +98.7 ms (+0.56%)
CPU, open subagent at depth 6 4,620.0 ms 4,710.0 ms ≈ +90.0 ms (+1.95%)
Open chain parent from agents view 3,040.0 ms 3,038.2 ms ≈ -1.7 ms (-0.06%)
CPU, open chain parent 1,380.0 ms 1,420.0 ms ≈ +40.0 ms (+2.90%)
Scheduled catalog, first request 407.0 ms 452.5 ms ≈ +45.5 ms (+11.18%)
CPU, scheduled catalog 830.0 ms 860.0 ms ≈ +30.0 ms (+3.61%)
Scheduled catalog, repeated request 0.5 ms 0.5 ms ≈ +0.052 ms (+11.32%)
CPU, repeated catalog 0.0 ms 0.0 ms ≈ +0.0 ms (N/A)
Cold worker with three catalog scans 368.3 ms 386.4 ms ≈ +18.1 ms (+4.93%)
CPU, cold worker and scans 360.0 ms 440.0 ms $\textcolor{#b76260}{\textsf{↑ +80.0 ms (+22.22\%)}}$
UI memory after interactions 1,738.48 MB 1,759.59 MB ≈ +21.11 MB (+1.21%)

Sandbox cost: ~$0.1004 — no inference calls.
Run, logs, and downloadable raw results

Methodology and samples

Main resolved at 2026-09-20T04:11:53.140380+00:00. Harness e311d649.
Linux x64, 4 vCPU, 8 GB RAM, 20 GB disk; region us.
Image: node:24-bookworm@sha256:be23f54a88d34e8824c741b19b91064094f92c1c97b194144bfc8b50d67258e2.
Stock tools, skills, daemon, and Python bootstrap enabled; fresh homes and a fixed Git fixture.
Onboarding is dismissed; the editor starts without a selected model or submitted prompt.
Medians shown. Arrows require a 20% timing/memory change plus absolute floors and IQR.
These practical noise floors are not a statistical significance test.
Cold means stopped Prime processes; OS filesystem caches are not flushed.
No model requests or credentials. Installation excludes build/setup time.
Installer tarballs use loopback; npm/Python downloads use the network with fresh caches.
Artifact size counts release tarballs; footprint after first use includes registry packages.
MB is decimal. Summed RSS can double-count shared pages; PSS is recorded when available.
Provisioning, setup, and build durations are recorded separately in the raw results.
Kernel probes use the installed JSONL runtime, outside the TUI/TypeScript host.
Per trial: 50 Python cells, 5 calls per shell case, and one 35-cell mix (9 git status calls).
Cell/shell values are batch means; other runtime timings are single operations.
State fixture: a 10,000-row × 8-column integer DataFrame and a 10,000-integer list.
Restore runs in a fresh kernel, including pandas imports; kernel startup is excluded.
Kernel RSS covers the isolated Python process; loaded RSS follows the pandas workload.
Transport benches run node against the prepared source build, outside the installed home.
Frame decode times one 32 MiB private frame, snapshot-chunk header, pushed in
8 KiB chunks; the wire shape of multi-MB frames on the daemon-worker channels.
UI trials use a fresh fixture set: 194 top-level sessions including one ~40 MB transcript,
40 ledger fan-out children, and a 6-deep subagent chain (~46 spawn edges).
Large fixtures hold 1,999 complete triples (~5 MB JSONL); medium 119; subagents 399 each.
Interactions: cold --resume of a large session, warm /resume switch, left-arrow to agents view,
roster settle with many saved sessions, search-and-open of another large session,
reattaching to that resident session, opening the chain parent, and drilling to depth 6.
Readiness is the rendered transcript tail plus a confirmed editor echo.
CPU metrics sum utime+stime across the whole benchmark-user process tree per interaction.
UI memory sums RSS after the interactions; PTY byte counts are in the raw results.
A separate catalog fixture has 2,300 sessions, 2,298 edges, and 13 paused scheduled-job owners.
Catalog timings cover first/repeated reads and cold worker creation under three pending scans.
All expected jobs and owner metadata are checked; worker readiness excludes TUI rendering.
Costs estimate full sandbox lifetimes at configured rates, including setup and build.
Budget target: $1; not a billing cap. Performance changes are informational.
Failed or incomplete execution fails the workflow; saved artifacts remain available.
Each side stops a phase after 2 identical consecutive failures.
Skipped trials are not attempted samples. Warm startup requires a successful cold launch.

Metric Main successful/attempted PR successful/attempted Main spread PR spread
Cold startup 10/10 10/10 IQR 301.5 ms IQR 104.6 ms
Warm startup 10/10 10/10 IQR 82.1 ms IQR 222.3 ms
Installation 3/3 3/3 range 4.70 s range 3.17 s
Compressed release artifacts 1/1 1/1
Installed footprint 1/1 1/1
Idle memory, summed RSS 10/10 10/10 IQR 9.39 MB IQR 1.85 MB
Python kernel startup 10/10 10/10 IQR 1.8 ms IQR 0.7 ms
Python cell round trip 10/10 10/10 IQR 0.019 ms IQR 0.039 ms
Empty bash command 10/10 10/10 IQR 0.3 ms IQR 0.3 ms
Bash git status 10/10 10/10 IQR 0.4 ms IQR 0.4 ms
Bash 32 KiB output 10/10 10/10 IQR 0.2 ms IQR 0.4 ms
35 cells / 9 shell calls 10/10 10/10 IQR 5.0 ms IQR 5.2 ms
Python interrupt to done 10/10 10/10 IQR 0.053 ms IQR 0.078 ms
Python state snapshot 10/10 10/10 IQR 0.5 ms IQR 0.4 ms
Python state restore 10/10 10/10 IQR 8.2 ms IQR 9.8 ms
Python idle RSS 10/10 10/10 IQR 0.15 MB IQR 0.11 MB
Python RSS after pandas workload 10/10 10/10 IQR 0.15 MB IQR 0.33 MB
Private frame decode, 32 MiB in 8 KiB chunks 10/10 10/10 IQR 1.8 ms IQR 1.2 ms
Resume large session (cold) 3/3 3/3 range 355.9 ms range 607.8 ms
CPU, resume large session 3/3 3/3 range 130.0 ms range 120.0 ms
Switch into large session 3/3 3/3 range 310.1 ms range 99.7 ms
CPU, switch into large session 3/3 3/3 range 320.0 ms range 290.0 ms
Open agents view from a session 3/3 3/3 range 348.3 ms range 235.8 ms
CPU, open agents view 3/3 3/3 range 570.0 ms range 190.0 ms
Full agents roster, many sessions 3/3 3/3 range 0.40 s range 0.0041 s
CPU, full agents roster 3/3 3/3 range 0.05 s range 0.06 s
Open another session from agents view 3/3 3/3 range 37.9 ms range 186.5 ms
CPU, open from agents view 3/3 3/3 range 60.0 ms range 120.0 ms
Reopen resident large session 3/3 3/3 range 12.3 ms range 29.5 ms
CPU, reopen resident session 3/3 3/3 range 10.0 ms range 30.0 ms
Open subagent session at depth 6 3/3 3/3 range 337.1 ms range 277.6 ms
CPU, open subagent at depth 6 3/3 3/3 range 110.0 ms range 230.0 ms
Open chain parent from agents view 3/3 3/3 range 68.0 ms range 27.6 ms
CPU, open chain parent 3/3 3/3 range 180.0 ms range 90.0 ms
Scheduled catalog, first request 3/3 3/3 range 24.5 ms range 17.4 ms
CPU, scheduled catalog 3/3 3/3 range 70.0 ms range 150.0 ms
Scheduled catalog, repeated request 3/3 3/3 range 0.3 ms range 0.091 ms
CPU, repeated catalog 3/3 3/3 range 0.0 ms range 10.0 ms
Cold worker with three catalog scans 3/3 3/3 range 55.5 ms range 116.3 ms
CPU, cold worker and scans 3/3 3/3 range 40.0 ms range 70.0 ms
UI memory after interactions 3/3 3/3 range 185.86 MB range 29.27 MB

…-manager suite

Net test additions drop 299 -> 22 (source additions 22), so the test-policy gate
passes at branch level with no category violations.

The standalone message-count-scan.test.ts file is gone: its ten vectors now live
in test/session-manager/file-operations.test.ts and reuse that file's existing
tempDir/header/msg/line harness. The three header-count vectors and the five
fallback-layout vectors are table-driven, and four pre-existing edge-case groups
in the same file are consolidated into tables with every case kept. Per-cut
lost-coverage ledger is in the PR body.
@kevinjosethomas

Copy link
Copy Markdown
Member

Second-pass review findings — holding:

  • Valid JSON loses real data: the header predicate (src/core/session-manager.ts, ~1291–1307) matches raw role bytes, so a record like {"type":"message","message":{"role":"\u0075ser","content":"hello",...}} is classified count-only even though JSON.parse decodes role user — it's dropped from firstMessage, searchable text, and activity; escaped assistant also skips model/usage. Require a complete, unescaped recognized role token (else parse the record), and add escaped-role tests.
  • Malformed-record semantics changed: malformed/truncated records are now counted where the previous parser skipped them — and this is not necessarily transient (a crash-truncated tail or malformed newline-terminated record never self-corrects). The first new test explicitly locks this in. For a pure-perf PR, restore skip-on-invalid or explicitly separate/accept the semantic change.
  • No committed microbenchmark backs the claimed 16.4% cold-scan win, and exact-head CI benchmark reporting shows 26 regressions (roster CPU +36.68%; unrelated Python probes also slower, so possibly confounding — but green benchmark execution is not confirmation of the speedup).

main had already consolidated the same pre-existing clusters this branch's
previous commit compressed, so the deletions were no-ops after the merge. The
main-tip (post-merge) budget is therefore carried by the PR's own vectors only:
they are re-added on top of main's consolidated file-operations.test.ts, reusing
that file's existing tempDir/header/msg/line harness.

Header-counting vectors kept (net 22 added test lines vs 22 meaningful source
lines, gate green against main tip):
- an unparsed tool result counted from its serialized header
- an oversized tool result counted without reading its payload
- tool results written in the id-first key order
- a container before the role marker falls back to the full parse (keeps text)
- a damaged session whose first entry is a tool result is still dropped

Per-vector lost-coverage ledger is in the PR body.
…uplicate rows

The header fast path's prefix-boundary early-out was unpinned after the budget
compression. Restore its vector and pay for it with four rows in the same file
that are true same-observable duplicates:

- findMostRecentSession: drop "returns null for an empty directory" and
  "returns null for a non-existent directory" (the null observable survives in
  the non-jsonl and headerless-jsonl rows).
- loadEntriesFromFile: drop "an empty file" and "malformed JSON" (the empty-array
  observable survives in the missing-file and headerless rows).

Reverting the early-out makes the restored row fail and nothing else. Net test
additions stay at 22 against 22 meaningful source lines, so the test-line budget
gate still passes at the main-tip view.
@sethkarten
sethkarten enabled auto-merge (squash) September 17, 2026 21:20
@sethkarten
sethkarten requested a review from xeophon September 17, 2026 21:21

@snimu snimu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved on snimu's instruction after independent read-only review (astra program, 2026-09-20). [written by prime-agent, checked by snimu]

@snimu
snimu merged commit 0597614 into main Sep 21, 2026
69 of 70 checks passed
@snimu
snimu deleted the rsi/perf-agents-roster branch September 21, 2026 12:11
KushBeaverTTV pushed a commit to KushBeaverTTV/prime-agent-windows that referenced this pull request Sep 22, 2026
18a56bf fix(coding-agent): make blocked daemon update restart warnings actionable (PrimeIntellect-ai#2515)
3fc5d96 fix(ci): harden behavioral eval token, label, and verifier trust (PrimeIntellect-ai#2449)
aa242fb add grok 4.7 to every provider that serves it (PrimeIntellect-ai#2505)
c91e6e9 [RSI, bug] fix(kernel): bound REPL protocol frame sizes in kernel and host (PrimeIntellect-ai#2423)
1272a3d [RSI, feature] feat(coding-agent): tell the model when Python skills fail to import (PrimeIntellect-ai#2381)
690e23d feat(coding-agent): per-request provider timing diagnostics (PI_REQUEST_TIMING) (PrimeIntellect-ai#2462)
27f32dd [RSI, bug] compaction: anchor summaries to kept-tail state and stop re-summarizing file lists (PrimeIntellect-ai#2385)
0498deb [RSI, performance] Cache the branch array on the per-turn hot path (PrimeIntellect-ai#2414)
a8ae626 [RSI, performance] context: keep only the newest harness digest in built contexts (PrimeIntellect-ai#2394)
e1f4ae5 [RSI, performance] start the daemon catalog on demand (PrimeIntellect-ai#2398)
c37f5eb [RSI, performance] Append catalog metadata without full transcript parses (PrimeIntellect-ai#2433)
2883a78 [RSI, feature] Park quota-blocked sessions until the provider reset and auto-resume (PrimeIntellect-ai#2375)
0597614 [RSI, performance] count tool-result message entries from their serialized header (PrimeIntellect-ai#2416)
b3e04b5 [RSI, bug] fix(coding-agent): re-park pending next-turn messages when an async-bash notice is withdrawn (PrimeIntellect-ai#2386)
2e9ab77 fix(coding-agent): reconnect attached windows when the daemon restarts (PrimeIntellect-ai#2458)
3b1aa5f feat(images): route image turns to a configured image model (PrimeIntellect-ai#2453)
41e4e0e fix(harness): validate refinement writes and skip malformed entries in the digest (PrimeIntellect-ai#2463)
e683fcb [RSI, bug] fix(ai): recover stale Codex chains after metadata (PrimeIntellect-ai#2374)
eee9d81 feat(coding-agent): add /speed command showing output tok/sec in the footer (PrimeIntellect-ai#2466)
b08f08e feat(coding-agent): hold goal/autonomous continuations while background bash() handles run (PrimeIntellect-ai#2465)

Gates: all passed
Conflicts: auto=1 agent=0
sethkarten pushed a commit that referenced this pull request Sep 22, 2026
…budget (#2501)

* test(coding-agent): add regression pins from the RSI review batch

Add tests for behaviors whose revert-mutants survive the current suite.
No source changes.

- harness digest at compaction boundaries: the compaction head keeps its
  fingerprint snapshot as the only digest on resume with unchanged state,
  and a legacy snapshot without a fingerprint self-heals with exactly one
  re-delivery that supersedes the snapshot
- the live snapshot-clearing loop on the compaction summary when a fresh
  digest is appended
- the leaf-branch cache is dropped when the same instance reloads its file
- worker recovery completes when the interrupted-session notice cannot be
  written
- the daemon catalog process routes rename/archive/mark_interrupted through
  the fast append path (real subprocess over DaemonCatalogClient)
- the fast-append parentId equals the leaf a full open computes, across
  chained, label-leaf, and blank-line transcripts
- the session scan drops a damaged file whose first entry is a tool result
  even when a valid session header follows

Each pin was verified to fail on its named mutant in a Prime sandbox.

Test-Budget-Exception: pin-only PR restoring mutant-surviving coverage found by the RSI review batch (#2394 #2400 #2414 #2416 #2433)

* chore: drop changelog fragment (test-only PR, no release-notes surface)

* test(coding-agent): fit the RSI regression pins into the test-line budget

Net test lines must not exceed source lines (0 here), so the pins are folded
into existing tests instead of added beside them:
- agent-session-prompt: the two cold-boundary resume tests become one lifecycle
  test that also pins the compaction-head fingerprint (#2400) and the
  snapshot-clearing loop (#2394).
- leaf-branch-cache: the rollback subset test folds into the held-array test,
  which now also pins the same-instance reload cache drop (#2414).
- daemon-supervisor-monitor: one refused markInterrupted in the existing
  independence test pins the advisory catch (#2433).
- append-to-existing-file: the catalog subprocess and leaf-differential tests
  are dropped (see PR body, "Coverage deliberately not restored").

Gate: TEST_POLICY_BASE=c91e6e991 node scripts/check-test-policy.mjs -> 42 added,
47 deleted test lines, 0 source lines. Passes.

* test(coding-agent): drop a comment that restates the test title
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants