Why
一次 upstream sync 会话把 37.8MB 的全量 git diff 注入 user 消息的 summary.diffs(message.data = 37,872,527 字节),进程随即进入忙循环:CPU ~142% 持续燃烧、RSS 稳定 9GB、JSC GC 线程(libpas scavenger + heap helpers)满负荷。事件循环被饿死后,ESC 双击的 abort 请求永远得不到调度——SessionPrompt.cancel 第一行日志都打不出来,用户只能 kill -9。此问题此前极难复现,触发条件是单条消息达几十 MB(正常使用到不了的量级)。
证据链(2026-08-28 现场采集)
- 会话
ses_fe5fb9f843e3IWKjNgbDA0otgN("Sync opencode-official upstream and merge diffs"),run 68f76d00
- step=4 的 LLM stream 耗时 49.5 分钟(13:22:06 → 14:11:38)才返回,期间进程已在高 CPU
- step=5 于 14:11:40.573 打出
llm runtime selected(ai-sdk runtime)后:零日志、零事件写入、零 token 产出
- 进程无任何 TCP 连接(LLM 请求未在网络上),但 CPU/GC 持续燃烧 → 纯 JS 分配循环,非网络等待
- macOS sample:主线程 ~21% JS 执行 + 其余 kevent,GC 线程满负荷 = 稳态分配风暴(高速分配+回收,RSS 不涨)
- 注意:会话全部 137 个 parts 仅 695KB —— 发给 LLM 的是小头,37.8MB 集中在 message.data 的 summary.diffs 元数据
根因(已确认)
放大链路:SessionSummary.summarize(packages/opencode/src/session/summary.ts:124-125)把 snapshot.diffFull 的全量 diff 无上限写入 user 消息 summary.diffs → sessions.updateMessage 每次都把 37.8MB 的 message.data 写入 message 行,并以 message.updated 事件形式再写一份进 event 表 + SSE 广播(实测 event 表单条 message.updated 达 37.87MB;全局 opencode.db 已被撑到 10GB)→ 同进程 TUI 接收并处理巨型事件 → 序列化/渲染路径分配风暴 → GC 满负荷 → 事件循环饿死 → abort HTTP 请求死在队列里(ESC 双击无效的直接机理)。13:21:19 的"成功 cancel"实为用户强退进程时走的清理路径。
修复方案(交付分支 feat/458-giant-summary-guard,PR #459)
三层防御,diffs 是纯元数据(TUI 展示用),截断不影响发给 LLM 的内容:
- 源头截断
summary.ts:summarize 写回前按总预算(256KB)截断文件列表
- 读护栏
session.ts 读路径:DB 读出超限 summary_diffs 即剥离(保留 additions/deletions/files 统计)——存量治愈:已存在的 37.8MB 消息重新加载时被卸载,旧会话可安全继续
- 写护栏
session.ts 写路径:toRow 写库前同样截断,保证任何路径写入 DB 的 summary_diffs 不超预算
后续拆分项(不在本 PR):abort 路径对事件循环饿死的韧性(cancel 不应依赖被饿死的 loop)、事件循环 lag watchdog。
环境
- opencode 1.0.34,macOS,provider=local-proxy-compatible/glm-5.3-flash(ai-sdk runtime)
- 现场证据:sample 输出已存
/var/folders/jm/w5g1j5td4vl8164rqwl96fn80000gn/T/opencode/stuck-sample.txt(临时目录,如需保留请及时转存)
Why
一次 upstream sync 会话把 37.8MB 的全量 git diff 注入 user 消息的
summary.diffs(message.data= 37,872,527 字节),进程随即进入忙循环:CPU ~142% 持续燃烧、RSS 稳定 9GB、JSC GC 线程(libpas scavenger + heap helpers)满负荷。事件循环被饿死后,ESC 双击的 abort 请求永远得不到调度——SessionPrompt.cancel第一行日志都打不出来,用户只能kill -9。此问题此前极难复现,触发条件是单条消息达几十 MB(正常使用到不了的量级)。证据链(2026-08-28 现场采集)
ses_fe5fb9f843e3IWKjNgbDA0otgN("Sync opencode-official upstream and merge diffs"),run68f76d00llm runtime selected(ai-sdk runtime)后:零日志、零事件写入、零 token 产出根因(已确认)
放大链路:
SessionSummary.summarize(packages/opencode/src/session/summary.ts:124-125)把snapshot.diffFull的全量 diff 无上限写入 user 消息summary.diffs→sessions.updateMessage每次都把 37.8MB 的message.data写入message行,并以message.updated事件形式再写一份进 event 表 + SSE 广播(实测 event 表单条message.updated达 37.87MB;全局 opencode.db 已被撑到 10GB)→ 同进程 TUI 接收并处理巨型事件 → 序列化/渲染路径分配风暴 → GC 满负荷 → 事件循环饿死 → abort HTTP 请求死在队列里(ESC 双击无效的直接机理)。13:21:19 的"成功 cancel"实为用户强退进程时走的清理路径。修复方案(交付分支
feat/458-giant-summary-guard,PR #459)三层防御,diffs 是纯元数据(TUI 展示用),截断不影响发给 LLM 的内容:
summary.ts:summarize写回前按总预算(256KB)截断文件列表session.ts读路径:DB 读出超限summary_diffs即剥离(保留 additions/deletions/files 统计)——存量治愈:已存在的 37.8MB 消息重新加载时被卸载,旧会话可安全继续session.ts写路径:toRow写库前同样截断,保证任何路径写入 DB 的 summary_diffs 不超预算后续拆分项(不在本 PR):abort 路径对事件循环饿死的韧性(cancel 不应依赖被饿死的 loop)、事件循环 lag watchdog。
环境
/var/folders/jm/w5g1j5td4vl8164rqwl96fn80000gn/T/opencode/stuck-sample.txt(临时目录,如需保留请及时转存)