Skip to content

Resuming image-heavy sessions stalls the TUI; evicted strings cause repeated full-history reads #1407

Description

@petergrapher

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:

Synthetic payload First getEntries() Full-history reads
64 MiB 0.56 ms 0
80 MiB 1,087.81 ms 16

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/senpi package 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 literal pi -ne UI comparison has not been performed.

Extension-free reproduction command
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

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, via omo-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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions