Replies: 2 comments
|
@zhangys11 你的根因判断我在 确认: live Session 只增不减, 无自动 eviction会话的 live 实例由
所以你观察到的"打开越多会话堆越高"完全吻合—— 为什么 3.47GB 之后才死V8 默认 heap ceiling 约 3.6GB(我机器无 override 时也是这个数)。最后的 mark-compact 从 3479.5MB 只回收 14MB 证明对象是真驻留 GC 根引用强持有, 不是漏 GC——所以是 OOM 而不是 leak, 你的"monotonic 增长"判断正确。 建议的落地方向(供参考, 非 PR)
优先做 1(idle-timeout)——它最小且直接命中你的复现路径; 2 是锦上添花。要注意的是 live 如果你愿意, 我可以基于这个路径出一个具体的 idle-eviction 参考 diff(含 manager idle-check + 回归测试: 打开会话后无活动 N 分钟触发 drop、重开会话回填正常)。 |
|
Git switched changes fixedkeep feedback DeepSeek all COANTOMI keeping100%swift Code all work set to chanies New maniging 💪📢🚀💯 |
Uh oh!
There was an error while loading. Please reload this page.
V8 heap OOM after ~8h: opened Conversations are fully hydrated and never evicted (no live-session retention policy)
Summary
@deepseek-ai/dsh0.1.2-rc.1 aborts with a V8 heap OOM (FATAL ERROR: Ineffective mark-compacts near heap limit) after~8 h 16 m of continuous use on a 32 GiB machine with no heap override. Opening a conversation in the web UI cold-reads
that session's entire stored event log, promotes it into a fully hydrated live
Sessionplus a live, residentAgent,and nothing in the harness ever evicts, caps, or idle-times-out those live sessions — heap usage grows monotonically
with the number of conversations opened until the process dies. Root cause: missing eviction/retention policy for live
Sessions/Agents. Data is not at risk — every session is persisted append-only to disk — this is purely an in-memory
lifecycle gap.
Environment
VERSIONfile:node v24.16.0,pnpm 11.5.0)@deepseek-ai/dsh/@deepseek-ai/dsh-session0.1.2-rc.1, installed vianpm i -g @deepseek-ai/dsh@deepseek-ai/dsh-base,@deepseek-ai/dsh-web-app,@anionex/dsh-vision-toolkit@0.1.40,@linxin666/dsh-web-all@0.3.14,dsh-better-sidebar@0.18.0NODE_OPTIONS/--max-old-space-sizeset (default V8 heap ceiling ~3.6 GB)Crash evidence
Fatal abort:
FATAL ERROR: Ineffective mark-compacts near heap limit — Allocation failed - JavaScript heap out of memory— V8abort via
node::OOMErrorHandler, delivered as SIGABRT (crash report shows the abort).GC preamble from the aborting isolate (verbatim):
Uptime 29,779,168 ms ≈ 8 h 16 m. The final mark-compact shrank the live set only from 3479.5 MB to 3465.4 MB
(~3.47 GB) — reclaiming ~14 MB — so the objects were genuinely retained and reachable, not garbage or fragmentation.
Crash report on disk:
~/Library/Logs/DiagnosticReports/node-2026-09-05-072536.ips(PID 12336).The heap total reached 3603.9 MB — the ~3.6 GB default V8 ceiling — at the time of the abort.
Session scale involved
The store holds a small number of very large single sessions. The heaviest file decompresses to ~127 MB (126.7 MB) of
JSON in 151,184 JSONL rows, yet its event
seqcounter is ~3,432,683 — rows are packed chunk runs that expand ondecode; when last observed it was at turn 421. Per-session sizes of the 6 largest (paths omitted; only the workspace
context is named):
35 files exceed 2 MB compressed, and the whole store decompresses to ~1.13 GB raw. Opening even a handful of these
resident at once is enough to exhaust a default-sized V8 heap.
Root-cause analysis (code references are from the installed 0.1.2-rc.1 build)
All paths relative to
@deepseek-ai/dsh/node_modules/@deepseek-ai/:dsh-session/lib/index.js:SessionStoreholds live sessions in an unboundedMap(
store = new Map(), line ~1544);create()is prepare + enter + announce folded into actx.effect(~1579–1586);the only removal path is the
detachdisposer returned byenter()and its paireddetachEntered(~1652–1692);list()returns everything (~1786–1788). No eviction/LRU/idle-timeout/capacity check exists anywhere in the package.Sessionstarts withlog = [](~1229);append()deep-freezes each event and pushes it (~1403–1443);snapshotEvents()caches a second frozen copy ofthe entire log on the first full-range call (
eventsSnapshot ??= Object.freeze([...this.log]), ~1342–1348), and thatcache is invalidated and rebuilt on every append (~1434). Resident cost is therefore roughly one large live object
graph per opened session plus a duplicate full-length reference array — and neither is ever released while the
process runs.
followwith{kind: "session", maxMessages: 50}):dsh-api-session-controller/lib/index.jsfollow(~2815) →history.follow(~1239) reads the observation and, whenthe session was cold/prepared, promotes it (~1296–1304) →
promote()firesagents.resolveObservedAgent(~2635–2660)→
resolveshort-circuits if the agent is already live, otherwiseresumes (~213–236) →dsh-agent-loop/lib/index.jsresume(~1349–1370) → persistenceprepareseeds theSessionwith the entirestored log (
dsh-session-persistence/lib/index.js~951–965 and ~1161–1178 — wholestoredEventsplus repair closers)→ agent-loop
publishenters the liveSessionand liveAgentinto their registries (dsh-agent-loop/lib/index.js~1249–1255). So merely opening (even previewing) a conversation fully hydrates it.
kick()returns the agent to{kind: "idle"}and itwaits for the next prompt (~449–496 / ~605–609); nothing bounds how many such live agents/sessions accumulate (the
agent
AgentRegistryis likewise a barestore = new Map(),dsh-agent/lib/index.js~416).owner context (
dsh-agent-loop/lib/index.jsdispose~1201–1221, armed ~1225–1229). Web-driven resumes run underthe app-root context, so
disposeeffectively fires only at process teardown[inference — the web-resume owner context is the app root; not directly verified in the controller code]. Switchingconversations, archiving, and idling never dispose:
dsh-workspace/lib/index.jsarchiveSession(~422–432) onlyappends to
archivedSessionIds; there is no host "active session" concept and no dispose-on-switch.projection snapshots with no log read (
dsh-api-session-controller/lib/index.js~1728–1752;dsh-session-projection-cachecachedSnapshot~178–195); full-text search is SQLite FTS. The cold read that decodesthe whole JSONL happens only on open (
dsh-session-persistence-jsonl/lib/index.jsreadPrefix~977–1078). Memorytherefore grows with each conversation you open, not with listing or searching.
dsh-agent-loopConfigonly carries
maxParallelToolCalls(~1049–1061);dsh-api-session-controllerConfig only carriescold-probe/native-open settings (~2581–2584);
dsh-session-projection-cacheanddsh-workspacehave no Config atall. The only LRU in the stack is
preparedSessionCacheSize = 5, which bounds detached/prepared (not-yet-published)cold sessions (
dsh-session-persistence/lib/index.js~488) — published/live sessions are never evicted.promote → resume), which means evicting idle live sessions is fully recoverable from disk.
Reproduction
dsh webwith default settings (no--max-old-space-size/NODE_OPTIONSoverride) on a 32 GiB macOS machine.JSONL is 50–120 MB — millions of decoded events each). Each file is a concatenated multi-frame zstd JSONL, so no
file-content
details are needed — only the decompressed size matters.
Runtime.getHeapUsage/HeapProfiler) observethat
heapUsedgrows monotonically with each session opened and is not reclaimed after switching away,archiving, or leaving the conversation idle.
the process aborts with
FATAL ERROR: Ineffective mark-compacts near heap limit.until OOM.
Suggested fixes (for maintainers)
detachthe liveSessionafter an inactivity threshold,excluding the currently-followed session. Safe because re-opening cold-hydrates from disk (already a supported path).
Suggest config knobs such as
idleTimeoutMsandmaxResidentSessions.Session+Agentfor a mere follow/preview of a conversation — page reads alreadywork cold (
list, projection snapshots, and message-page reads all avoid the log). Hydrate only when the useractually sends a message; this alone would bound memory to actively-used sessions.
Session.log(windowing / spill) and not caching a second full frozen copy ineventsSnapshotfor multi-million-event sessions.Additional context
This was observed while investigating an unrelated plugin regression, not during a stress test. No data was lost:
sessions are persisted to disk as append-only zstd JSONL, so the crash only discarded in-memory state — the on-disk
files are intact.
All reactions