Replies: 1 comment 1 reply
|
Your finding is confirmed on the static-source side at HEAD (
private readonly sessions = new Map<SessionId, Session>()Lazy-built and inserted in The design intent is written down in // Sessions remain resident after creation so their open Remote sources keep running off-screen.So "open a session and it stays" is current design, not a regression — and it's consistent with your measurement that the held-session count never returns after socket close. On config knobs: the only adjacent one is Practical mitigation today: nothing in source reclaims live sessions, so a scheduled restart of If you still have the per-session numbers from run 3's end state, a before/after-restart heap comparison would make the "restart reclaims" case airtight. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
英文版(建议作为正文)
Environment
0.1.5-rc.1(global npm install)dsh web(web profile; 4 local profile plugins)--max-old-space-sizeSummary
dsh webgrows its heap in proportion to the number of sessions the process has opened, not merely to one large log, and nothing ever evicts those live sessions. Three separate runs died the same way at three different heap caps, and raising the cap only moved the wall:Ineffective mark-compacts near heap limit--max-old-space-size=8192)Reached heap limitIn every crash the last mark-compacts reclaimed almost nothing (e.g.
4030.2 → 4014.1 MB, i.e. 0.4 %), so the heap was live retained data, not uncollected garbage.Measured: resident cost is per open session
Fresh boot with 8 sessions open (each holding an OS file lock on its
session.lock), measured at the HTTP API (heapUsed≈ 2875 MB):The RSS sum (3376.6 MB) matches the process measurement (3354 MB) almost exactly; the
heapUsedsum (1807.8 MB) accounts for ~63 % of the process's 2875 MB. Method: each transcript was streamed through the same path the host uses when it opens a session (line-wiseJSON.parse→ lossless clone →deepFreeze), one file per isolated process.Measured: opening a session is one-way
I opened one not-yet-live session through the real wire protocol (
/api/remote.mux,session/followwithmaxMessages: 1), then closed the WebSocket. Evidence is the process's held session locks, which is not affected by background noise:The count never returns. Repeating page loads/streams accumulates.
Measured: growth does not require activity
A
/proc-based sampler watched one process for its whole life (3,861 samples): anonymous memory went 1.05 GB → 4.64 GB in 16.2 h, monotonic. During that window there were zero session-log writes and zero queue/storage writes — only a handful of attachment files. So the slope is not "work done"; there is an additional time/aggregate-proportional term.Upstream source locations (checked against
master, 0.1.6-alpha.1, pushed 2026-09-15 — all still present)Verified by fetching the files and grepping the current master tree:
No live-session retention policy.
packages/api/session-controller/src/client/sessions/manager.ts:96private readonly sessions = new Map<SessionId, Session>():292this.sessions.set(sessionId, session)(lazy build, never evicted):245async drop(sessionId)exists;:247this.sessions.delete(sessionId)grep -nE "idleTimeout|maxSessions|evict|retention"over the package: no hitsA read handle pins the whole decoded log.
packages/session/session-persistence-jsonl/src/index.tsprimed— 2 occurrences on masterCOLD_LOG_MEMO— 2 occurrences (the decoded-log memo, count-bounded by entries, not bytes)decodeStoredLog3,freezeStoredEvents3,memoizeStoredLog4The observation cache can exceed its own cap.
packages/session-query/session-query/src/index.ts:122-131const preparedSessionCacheSize = config.preparedSessionCacheSize(default 5)refs > 0) are skipped by eviction, so a cache bounded by count is not a bound in bytes.The raw log is append-only and compaction does not trim it.
Session.logkeeps every streaming chunk; surfacereplaceremoves shadowed entries from model input only.Why "more RAM" is not the fix
The crash is not physical-memory exhaustion: a 64 GB host still died at ~4.1 GB (#5995), and one reporter measured that with
--max-old-space-size=16384the heap still climbs past 4 GB with no plateau (#3275). The resident term isopened sessions × per-session cost, so any fixed cap is eventually crossed.Reproduction (minimal)
What I ruled out on my host
DEFAULT_MAX_MESSAGES = 50), and the leak reproduces with no client at all (--no-open), so the renderer is not the cause of the host heap.bcahlitin Web server OOMs within ~60s of any client connecting when a workspace contains a large session #3275 showed hiding all v2 files and keeping only v3 still OOMs.session_projcacheaside makes it OOM faster.persona.prefix, so that path is not active here.SessionPersistenceCorruptionError/SessionFormatUnsupportedErrorin ~10 boots; only heap exhaustion. All sessions load.Suggested fixes (ordered by leverage)
manager.get()↔drop()), plus a public eviction entry point — today the onlydrop()caller is scope teardown.primed) for read handles; stream the fold instead of materialising every event.Related
中文版(可放在英文之后,或作为主帖)
环境
@deepseek-ai/dsh0.1.5-rc.1(全局 npm 安装)· Node v22.23.1 · Linux x64 · 物理内存 128 GB(实际可用约 64 GB)· web profile(含 4 个本地 profile 插件)· 启动方式dsh web。结论
dsh web的堆占用与**「该进程打开过的会话数量」成正比(不只是单个大日志),而且没有任何驱逐机制**。三次运行在三个不同的堆上限上以同一方式崩溃,抬高上限只是把墙往后挪:Ineffective mark-compacts near heap limit--max-old-space-size=8192)Reached heap limit每次崩溃前最后一次 mark-compact 几乎回收不到东西(例如
4030.2 → 4014.1 MB,即 0.4%),说明堆里是活着的保留数据,不是没回收的垃圾。实测一:常驻成本按「每个已打开会话」计
全新启动、8 个会话处于打开状态(各自持有
session.lock的 OS 文件锁),经 HTTP API 读到heapUsed ≈ 2875 MB:RSS 合计(3376.6 MB)与进程实测(3354 MB)几乎完全吻合;heap 合计占进程 2875 MB 的约 63%。测法:把每份日志分别喂给一个独立进程,走 host 打开会话时的同一条路径(逐行
JSON.parse→ 无损克隆 →deepFreeze)。实测二:打开会话是单向的
我用真实线协议(
/api/remote.mux+session/follow,maxMessages: 1)打开一个当时尚未 live 的会话,然后关闭 WebSocket。判据用进程持有的会话锁数——它不受后台噪声影响:计数不回落。 反复刷新页面/重连会持续累加。
实测三:增长不需要活动
用一个只读采样器盯住某次进程的完整生命周期(3,861 个样本):匿名内存 16.2 小时内从 1.05 GB 单调涨到 4.64 GB。该时间窗内会话日志零写入、队列与 storages 零写入(只有几个附件文件)。所以斜率不是「做了多少活」,另有一项与时间/累计量成正比的增长。
上游源码位置(已对照
master/ 0.1.6-alpha.1,最后推送 2026-09-15,四处都还在)packages/api/session-controller/src/client/sessions/manager.ts:96private readonly sessions = new Map<SessionId, Session>():292this.sessions.set(sessionId, session)(懒建,永不淘汰):245async drop(sessionId)存在、:247this.sessions.delete(sessionId)idleTimeout|maxSessions|evict|retention全包 grep:零命中packages/session/session-persistence-jsonl/src/index.ts——primed2 处、COLD_LOG_MEMO2 处、decodeStoredLog3 处、freezeStoredEvents3 处packages/session-query/session-query/src/index.ts:122-131,preparedSessionCacheSize默认 5,且refs > 0(被租约钉住)的条目在淘汰时被跳过 —— 一个「按条数」的上限在字节维度上不构成上限Session.log保留全部流式碎片;surfacereplace只影响模型输入,不删原始事件为什么「加内存」不是解法
这不是物理内存耗尽:一台 64 GB 的机器同样死在 ~4.1 GB(#5995);另有人实测
--max-old-space-size=16384时堆仍越过 4 GB 且无平台期(#3275)。常驻项是已打开会话数 × 单会话成本,因此任何固定上限终将被越过。已排除项(避免后来者重复踩)
DEFAULT_MAX_MESSAGES = 50),且完全没有客户端(--no-open)时同样复现。session_projcache移开反而更快 OOM(同上)。persona.prefix存在,该路径未激活。SessionPersistenceCorruptionError/SessionFormatUnsupportedError,只有堆耗尽;全部会话可加载。建议修法(按杠杆排序)
manager.get()↔drop()),并补一个公开驱逐入口——今天drop()的唯一调用者是 scope teardown。primed);把 fold 改成流式,而不是物化每个事件。相关讨论
#3275 · #5696 · #5995 · #6511 · #4722 · #5347
All reactions