python3 - <<'PY'
import subprocess
source = r'''
import fs from 'node:fs';
import { syncBuiltinESMExports } from 'node:module';
import { performance } from 'node:perf_hooks';
const { SessionManager } = await import('./dist/core/session-manager.js');
const path = '/__senpi_synthetic_read_only__.jsonl';
const saved = Object.fromEntries(['existsSync','openSync','readSync','closeSync'].map(k => [k,fs[k]]));
let fixture, offset = 0, opens = 0, bytes = 0;
const fd = 987654321;
fs.existsSync = p => p === path || saved.existsSync(p);
fs.openSync = (p, ...args) => { if (p !== path) return saved.openSync(p,...args); if (args[0] !== 'r') throw Error('write forbidden'); offset = 0; opens++; return fd; };
fs.readSync = (f,b,o,n,p) => { if (f !== fd) return saved.readSync(f,b,o,n,p); const count = Math.min(n, fixture.length-offset); fixture.copy(b,o,offset,offset+count); offset += count; bytes += count; return count; };
fs.closeSync = f => { if (f !== fd) return saved.closeSync(f); };
syncBuiltinESMExports();
try {
for (const count of [64,80]) {
const header = { type:'session', version:3, id:'synthetic', timestamp:'2026-09-01T00:00:00.000Z', cwd:'/tmp' };
const entries = [header, ...Array.from({length:count}, (_,i) => ({type:'message',id:String(i),parentId:i?String(i-1):null,timestamp:header.timestamp,message:{role:'user',timestamp:0,content:[{type:'image',mimeType:'image/png',data:'A'.repeat(1024*1024)}]}}))];
fixture = Buffer.from(entries.map(e=>JSON.stringify(e)).join('\n')+'\n');
const manager = new SessionManager('/tmp','',path,false,undefined,entries);
const measure = () => { opens=0; bytes=0; const start=performance.now(); const result=manager.getEntries(); return {ms:Math.round((performance.now()-start)*100)/100,fullFileReads:opens,bytesRead:bytes,entries:result.length,complete:result.slice(0,count).every(e=>e.message.content[0].data.length===1024*1024)}; };
const cold=measure();
const cached=measure();
manager.appendEntry({type:'message',id:'new',parentId:String(count-1),timestamp:header.timestamp,message:{role:'user',timestamp:0,content:[{type:'text',text:'synthetic'}]}});
const afterAppend=measure();
console.log(JSON.stringify({count,fileBytes:fixture.length,resident:manager.getResidentStoreStats(),cold,cached,afterAppend}));
}
} finally { Object.assign(fs,saved); syncBuiltinESMExports(); }
'''
result = subprocess.run(['node','--input-type=module'], input=source, text=True, timeout=90)
raise SystemExit(result.returncode)
PY
What happened?
Reopening one image-heavy session causes severe UI lag. Its JSONL is 117,277,295 bytes with 81 embedded images; base64 payloads account for 96.16%. It has no compaction entries. The private transcript and images are not attached.
An extension-free core benchmark confirms read amplification in
getEntries() -> _materializeEntry() -> loadEntriesFromFile()when the 64 MiB resident-string cache evicts entries:The 80 MiB case processes 1.34 GB through the reader. An immediate repeat is memoized, but appending one text entry causes 16 reads again (1,046.29 ms). These are in-memory synthetic timings, not disk or UI timings; the share of real UI lag attributable to this path remains unmeasured. Related but different payload: #648.
Steps to reproduce
Open/resume a session containing many embedded tool-result images. For an isolated core reproduction, run the command below from the installed
@code-yeongyu/senpipackage directory. It imports the core directly without loading extensions and uses an in-memory filesystem fixture; it does not touch a real session or call a model. The fixture uses synthetic image strings to exercise storage, not PNG rendering. A literalpi -neUI comparison has not been performed.Extension-free reproduction command
Expected behavior
Opening large sessions should remain responsive. Restoring evicted payloads should not reparse the entire history separately for each entry.
Version
@code-yeongyu/senpi 2026.9.5, viaomo-ai 5.0.0-0.beta.43; Node.js 26.8.1; macOS 26.6.2; Apple M1 Max (arm64); Ghostty.AI-assisted drafting and diagnostic analysis. The UI symptom is user-reported; the benchmark results were measured locally. This disclosure is in the body rather than a follow-up comment to avoid triggering GitHub Actions.