Replies: 3 comments
Follow-up: a community "bounded cold-history reads" patch already exists — here's how it maps to this roadmapWhile checking #1550, I found that @ernel135790 posted a bounded cold-history reads patch (2026-08-23) that implements a large part of this roadmap's short-term items. Worth aligning explicitly so the discussion doesn't fork into parallel proposals: What the patch covers (vs. roadmap items)
What the patch adds beyond our roadmap
My takeThe patch is a solid memory-bound solution for the short term. The remaining gap is exactly roadmap A's second half: without a persistent frame/message index, every cold open still decodes the whole artifact (CPU/IO-bound, just not memory-bound) — which is what makes a 460K-event session take ~9s even after this patch. So the natural next step after adopting (or cherry-picking) this patch is the index sidecar, plus B (frame merging) to shrink the decode surface further. Would be great to hear from the maintainers whether |
|
感谢点名。划一下边界,避免路线图和离线修文件搅在一起: dsh-session-surgeon 不改 大会话但 health= |
|
Thanks @xiaoshenming for the clear boundary — and for confirming that healthy-but-large sessions belong on the bounded-read path rather than file surgery. That's exactly the split this post intends:
One small overlap worth noting: your loopback "scan header first" approach is conceptually the same trick as roadmap item E (session-list header cache) and argszero's "detect corruption at the cheap scan" — scanning metadata before touching the full artifact. If the maintainers adopt a bounded-read API, your surgeon could also reuse Either way, no |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
中文摘要
DeepSeek Harness(DSH)首次打开一个较大的历史会话(几千条消息、大量 tool 结果,或含损坏提交历史的会话)时,Web 服务可能整体卡死数秒甚至超时。官方社区讨论 #1550(2026-08-14,报告人 wellorbetter,维护者 argszero 已确认)已经记录了这个现象与初步定位。本文不是重复报 bug,而是在 #1550 基础上做一次更系统的根因分析,并把问题从"损坏会话的 availability amplifier"扩展为所有大会话冷加载的通病:
maxMessages只限制返回 payload,不限制任何 IO/解压/解析/校验/物化成本。state.nodes.indexOf线性扫描节点数组,带 compaction 的大会话折叠为 O(n²)。本机实测(8.84MB 压缩 / 20.2MB JSON / 2.77 万事件 / 2.4 万 zstd 帧的真实日志):冷读一次(读盘+全帧解码+逐行 parse+逐事件快照)约 430ms(private decoder)/ 880ms(public),其中 JSON.parse ~200ms、事件快照 ~148ms、解码 ~78ms——而这只是"取最后 50 条消息"的前置成本。优化方向按性价比分三档:短期(A 尾部索引 sidecar+JSONL seek 冷开 <50ms、B 合并写帧减帧数 10-50x、C detached 投影接 coldSnapshot、D 事件快照按需、E 列表 header 缓存,以及社区 fork 已验证的 writer lease / yield 16ms / revision 负缓存)、中期(F 客户端骨架屏+预取、流式分页、LRU 自适应、list/inspect 隔离)、长期(G 默认 SQLite、存储格式演进、结构化损坏诊断)。预期可将冷开一个 46 万事件会话从 ~9s 降到 <100ms,并结构性消除 #1550 的全局可用性事故。
1. Problem Statement
1.1 Community anchor: Discussion #1550
Official discussion #1550 — "[Bug] Cold history loading fully materializes large/corrupt logs and can stall the entire Web server" — reported by wellorbetter on 2026-08-14 and confirmed by maintainer argszero against master, captures the community's first-hand account:
The incident: with one structurally valid large session plus one session with a committed-seq rollback in the tree, the Web server accepted TCP connections on 127.0.0.1:3080 but GET / timed out; after isolating the two session trees and restarting, it recovered. During the outage: ~163 MB working set, ~0.95 CPU-seconds per 2 wall-clock seconds, and a pile of CLOSE_WAIT sockets on port 3080.
Measured artifacts from #1550:
argszero's framing is worth quoting verbatim:
and his layered fix direction: bound the read (seekable/bounded persistence reads that stop at a page boundary instead of decode-everything-then-slice), cheap corruption detection (seq monotonicity at the frame scan level), and isolation (unreadable sessions quarantined at list/inspect time, never entering the full-scan path).
1.2 Why this document is not a duplicate bug report
#1550 was filed with corruption as the trigger and "availability amplifier" as the consequence. This document is a more systematic root-cause analysis plus an optimization roadmap that:
1.3 Scope and method
1.4 Community status (as of 2026-08-25)
The issue is live and unfixed upstream. Cross-checking #1550's own source citations against current master shows the same mechanism, with line-number drift only (see §2.2):
historySourceForgets full events before paginationhistoryPagepaginates afterwardsreadPrefix→readZstdPrefixdecodes every framestructuredClonein adoptZSTD_DECODE_YIELD_INTERVAL_MSThe reporter already prototyped a fix: wellorbetter's fork branch
fix/session-history-responsiveness(commit 159c5e4) contains (1) a cross-process writer lease (attacking the #1333/#1452 double-writer root cause), (2)ZSTD_DECODE_YIELD_INTERVAL_MS500→16 ms so large decodes yield to other requests, and (3) a revision-keyed negative cache (readFailures) so unchanged corrupt artifacts fail immediately instead of re-decoding. argszero's review: all three mechanisms pass their tests (2/2 + 152/152 + 81/81), but the patch does not cover the first valid large session's cold open — it still fully materializes; no bounded/seekable read, and the #1473 boot-block path is untouched. None of it is merged upstream as of 2026-08-25.The problem is not corruption-only: community member 7889545 reproduced a pure scale case without any corruption — 15 valid sessions (1.8–2.7 MB compressed each / ~300k tokens / 5–7k events) grew the process to ~3 GB heap within 60–90 s of boot with no client interaction; CPU profile ~60 % in
structuredClone; heap snapshot 21.6M live objects (assistant/chunk 59 MB, reasoning-delta 38 MB, …). Moving 11 session directories out ofsessions/dropped the heap to ~306 MB. NotablyarchivedSessionIdsdoes not reduce memory — archived sessions are still fully materialized, just hidden. This shows the materialization surface is not only the history RPC: the adopt path (boot-time and on-demand open) materializes just as eagerly.2. Root Cause Analysis
2.0 The full cold-load path (current master)
Key fact: pagination is applied after the full event array exists.
maxMessageslimits the returned payload only — it does not bound any I/O, decompression, parsing, validation, or materialization.2.1 Root cause R1 — Full materialization before pagination
historyRPC →historySourceFor(packages/host/apiproxy/src/api-proxy.ts:2154-2170 → 1474-1479). For a detached (cold) session this goes toinspectApiRemoteSession(packages/api/remotes/src/agent-lookup.ts:94-111), which callspersistence.list()(full enumeration) andpersistence.inspect(sessionId).inspect→prepareCore(packages/session/session-persistence/src/coordinator.ts:892-931):backend.loadStored(id)loads the entire stored log;adoptStoredEvents(coordinator.ts:560-573) migrates and deep-freezes every event;sessions.prepare(id, { seed: balanced })rebuilds the Session event-by-event (packages/core/session/src/index.ts:495-548).historyPage()applypaginate(events, beforeSeq, maxMessages)(api-proxy.ts:228-254, 745-761).Evidence that maxMessages does not bound work: the client requests history immediately on open (client/runtime/src/client/sessions/session.ts:618-648) with PAGE_MESSAGES = 50, yet the host still reads, decodes, parses, validates and materializes all 444K+ events before slicing 50 messages out.
2.2 Root cause R2 — JSONL physical read: whole file + all frames (no seek)
readStableFile(packages/session/session-persistence-jsonl/src/index.ts:292-304):stat → readFile(whole file) → stat, re-reading on revision change.readZstdPrefix(index.ts:348-419):scanZstdFramesscans frame boundaries (header-only, no decompression) then decodes every frame;SessionLogScanner(packages/session/session-persistence-jsonl/src/format.ts:272-378) JSON.parses every line and expands packed chunk rows (chunk-rows.ts:293-328).scheduler.yield()every 500 ms (index.ts:374-378) — the decode block itself is synchronous on the main thread.loadStoredFrom(fromSeq)is not implemented for JSONL (index.ts:196-197 documents "JSONL is sequential media"); the coordinator'sreadFromCore(coordinator.ts:841-870) therefore falls back toreadStoredPrefix+whole.events.slice(fromSeq)— i.e. full materialization, then slice (coordinator.ts:867-869). AnyfromSeq>0read on JSONL pays the full read/decode/parse cost.loadStoredFrom(packages/session/session-persistence-sqlite/src/store.ts:160-170, bounded suffix read via physicalSpanFrom) — but coldinspectstill calls the fullloadStored(store.ts:134-151, full-table select-events), so even the SQLite backend fully materializes on cold open; and when a suffix contains legacy shapes,readFromCore(coordinator.ts:859-861) falls back to a fullreadStoredPrefixanyway.2.3 Root cause R3 — Per-event rebuild: validation + deep copy + O(n²) surface folding
snapshotJsonValue(serialize-and-validate),assertSessionEventEnvelope,surfaceManager.validateNext; plusfreezeRestoredObjectdeep-freezing the whole JSON tree iteratively (index.ts:197-210). coordinator.ts:545-557 additionallystructuredClones each stored event on the inspect path.replacementRange(packages/core/session/src/surface.ts:337) usesstate.nodes.indexOf(op.start)/indexOf(op.end)— a linear scan of the node array per replacement. Sessions with compaction carry many replacements →foldSurface(surface.ts:387-395) becomes O(n²). With ~20K nodes and several indexOf passes, that is hundreds of millions of comparisons (hundreds of ms to seconds).2.4 Root cause R4 — Projection full re-fold on every history tail page (cache bypass)
detachedProjectionsFor→registry.restore({}, events, 0)(api-proxy.ts:810-818): with no checkpoint rows, every registered celldef.init()+ re-applies all events — O(cells × n).coldSnapshot(packages/session/session-projection-cache/src/index.ts:166-196) supports incremental restore from a floor seq — but the history path never uses it; the cache only serves session.list cold rows (api-proxy.ts:798-808). Cold history = full materialization + full projection re-fold (double O(n)).2.5 Root cause R5 — Amplifiers: 5-slot LRU, no negative cache, main-thread blocking
DEFAULT_PREPARED_SESSION_CACHE_SIZE = 5(coordinator.ts:27); a large session evicted from the LRU is re-read in full on reopen (revision re-check via stat).structuredCloneand 21.6M live objects, with no client interaction — see §1.4.archivedSessionIdsdoes not reduce this (hidden ≠ unmaterialized).2.6 What is NOT the bottleneck (client-side counter-evidence)
loadOlder50 messages per page (session.ts:380-414). The conversation assembler rebuilds incrementally per window (conversation-assembler.ts:223-248).order.maprenders the whole window; the trajectory view does use @tanstack/react-virtual). This matters only after thousands of messages are expanded, not on first open.2.7 Complexity model (summary)
3. Quantified impact (measured & estimated)
3.1 Local measurement (T2, Node v22.22.3, private zstd decoder)
Real largest session log on this machine (8.84 MB compressed / 20.2 MB JSON / 27,698 events / 24,005 zstd frames, avg ~369 B per frame):
snapshotSessionEvent— measures ~430 ms (private decoder) / ~880 ms (public one-shot fallback).3.2 Cross-validation with #1550 and the complexity model
4. Optimization roadmap (staged, prioritized by cost-effectiveness)
T2 ranked the candidates A–G by payoff/cost; the table maps them onto the staged plan (short / mid / long) together with the community-validated items from #1550 and the fork.
loadStoredFromon JSONL soreadFrom(seq)seeks to the tail framesloadStoredFromhook already exists (coordinator.ts:841-870)detachedProjectionsFor→coldSnapshotincremental instead ofrestore({}, events, 0)structuredCloneper eventZSTD_DECODE_YIELD_INTERVAL_MS500→16 ms, revision-keyedreadFailuresnegative cacheloadStoredFromseek already implemented; cold read becomes independent of log sizeAdditional #1550-aligned items folded into the plan: frame-level seq-monotonicity scan for fail-fast corruption detection (cheap scan instead of full decode; argszero #2), worker-thread decode/replay for main-thread responsiveness (#1550 suggestion 4, complements 0/yield-16ms).
5. Expected impact
6. Open questions
Sources: team T1 code-path report (tmp/20260825/team-t1-code-path.md, incl. §9 community cross-check), T2 perf analysis with local measurements (tmp/20260827/perf-analysis.md, bench-.cjs), captain's source notes & quantification (tmp/20260825/captain-notes.md, quantification.md), official discussion #1550 (raw + quotes). Inspected deepseek-ai/deepseek-harness current master 2026-08-25; finalized 2026-08-27. FINAL — ready for posting.*
All reactions