Skip to content

fix(server): sidecar 会话事件日志内存上限收敛(#445) - #447

Closed
Ver-zhzh wants to merge 2 commits into
huiliyi37:mainfrom
Ver-zhzh:fix/issue-445-sidecar-mem
Closed

Ver-zhzh wants to merge 2 commits into
huiliyi37:mainfrom
Ver-zhzh:fix/issue-445-sidecar-mem

Conversation

@Ver-zhzh

@Ver-zhzh Ver-zhzh commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

修复:sidecar watched-idle 会话事件日志可淘汰(#445 差集)

关联 issue #445(reporter @wangbin100608-eng,深度诊断报告;已由维护者以 completed 关闭——「会话常驻淘汰被跳过」一半的修复在本 PR 差集里,合并前保持追踪)。

收编对账:本 PR 原含三项改动,其中 maxEventBytes 64MB→8MB、PUT /config/runtime-lean 响应 effectiveAfterRestart 已由上游 abb7628(v3.30.0 republish)自行收编。按评审要求已 rebase 到最新 main 并收窄 diff,本 PR 现只剩第三项。

1. evictLoadedBeyondCap 的 listeners 跳过条件收敛

现象:淘汰函数遇到 s.agent || s.running || s.listeners.size > 0 的会话就跳过——SSE listener 摘不干净时会话永不淘汰,只进不出;长生命周期 SSE 标签页把完整内存环永久钉住(内存泄漏)。
根因:src/server/session-manager.ts 的 evictLoadedBeyondCap 跳过条件把「被 SSE 观察但长期静置」的会话也视为不可淘汰。
修复:收敛为——!s.agent && !s.running 且事件已静置超过 idleAgentTtlMs 的被观察会话允许淘汰(先 flushDeltaBuf/flushToolResultBuf 再清 events/eventsLoaded,语义与既有 unloadSession 同);重连走 getEvents→ensureEvents 从盘回放,不丢事件;idleAgentTtlMs <= 0 保留旧行为(永不淘汰被观察会话)。另加 exceptId 参数:ensureEvents 刚载入的会话不受同一次淘汰扫描影响,避免刚读回的日志被同一调用立即丢弃。

未修观察(如实报告,非本批引入)

  • rehydrate 一次性回填:当前 main 已是懒加载(loadRecords 只读轻量记录、loadedOrder 为空),无需改动。
  • compaction 覆盖率(12600 条仅压掉 1970 条):压缩逻辑在 replay-compaction.ts 及 agent 层,根因不在本批改动范围,未深挖。
  • 真实堆增长行为 headless 无法验证(issue 已知前提);建议维护者/报告人在 Windows 桌面端复测。

验证

项 结果
npx tsc --noEmit 0 新增报错
新增回归测试 session-evict-watched-idle.test.ts(评审要求的 3 场景:超 TTL 被观察会话被淘汰且可磁盘重放 / 未超 TTL 不淘汰 / idleAgentTtlMs<=0 永不淘汰) 3/3 通过
npm test session-manager 相关套件 133/133 通过,无回归
git diff(rebase 收窄后) 1 文件代码改动 + 1 新测试文件,最小改动

CI 说明(非本批引入的红)

本 PR 基线(rebase 到 v3.30.0 republish 后)在同一基线上的 main CI 同样失败:windows-smoke failure、ci (24) failure(上游基线预存问题,本地已核验)。本批改动只涉及 server 侧 session-manager.ts 的淘汰逻辑 + 1 个新测试文件,与 windows-smoke 跑的 bash/path-expression 测试无交集;tsc + session 单测本地全绿。故两处红为上游 CI 环境/基线问题,非本批引入。

Closes #445

huiliyi37 added a commit that referenced this pull request Oct 11, 2026
内容已经 sync 流程合入(dev 收编记录:未检索到),本提交为作者署名落账(提交页归属)。

Co-authored-by: Ver-zhzh <225828202+Ver-zhzh@users.noreply.github.com>
@sky-mirrors

Copy link
Copy Markdown
Collaborator

致 @Ver-zhzh

感谢这份修复——注释里把「为什么被观察会话是泄漏源」(listener 摘不干净 → 长期 pin 住整个环)和「为什么淘汰是安全的」(每次 append 都已 flush 到磁盘,重放走 getEvents→ensureEvents)论证得很完整,idleAgentTtlMs <= 0 的逃生门也保留了旧语义,这些都没问题。

审了一遍现状,有一个收编对账和两个请求:

收编对账:三项改动里的前两项已随 abb7628(v3.30.0 republish)进了 main——

  • maxEventBytes 64MB → 8MB(session-manager.ts:1433,注释同步改写);
  • effectiveAfterRestart: true(config-routes.ts:858)。

所以本 PR 目前真正待收编的只剩 evictLoadedBeyondCap 的 watched-idle 淘汰差集。

请求 1(rebase 收窄):abb7628 动了 PR 的同一区域(maxEventBytes 行及其注释),当前分支直接合并必然冲突;且前两项已收编,全量套用会重复改写。请 rebase 到最新 main 并把 diff 收窄到只剩第三项。

请求 2(补回归测试):两个文件里没有任何测试——淘汰条件收敛是会话生命周期逻辑,值得一个回归测试钉住行为,建议覆盖至少三个场景:① 无 agent/running + 有 listener + 静默超 idleAgentTtlMs → 被淘汰且事件可从磁盘重放;② 同状态但未超 TTL → 不淘汰;③ idleAgentTtlMs <= 0 → 观察中会话永不淘汰(旧行为)。可参照 claim-registry-mutex.test.ts 的多会话构造方式。

另外说明一下账目:#445 已以 completed 关闭,但其标题里「会话常驻淘汰被跳过」这一半的修复就在本 PR 的差集里——在本 PR 合并前,这个缺口不要丢。如果后续以单独 issue 立账或直接收编本 PR 差集都可以,保持追踪即可。这两项补完后我这边可以立刻核验。再次感谢!

- evictLoadedBeyondCap:被 SSE 观察但静置超 idleAgentTtlMs 的会话允许淘汰
  (此前一律跳过,长生命周期 SSE 标签页把完整内存环永久钉住);
  淘汰前 flushDeltaBuf/flushToolResultBuf,重连走 getEvents→ensureEvents
  从磁盘重放,不丢事件;idleAgentTtlMs<=0 保持旧的被观察永不淘汰。
- evictLoadedBeyondCap(exceptId?):ensureEvents 刚载入的会话不受同一次
  淘汰扫描影响,避免刚读回的日志被同一调用立即丢弃。
- 新增回归测试 session-evict-watched-idle.test.ts(3 场景)。

注:本 PR 原含 maxEventBytes 64MB→8MB、effectiveAfterRestart 两项,
已由上游 abb7628(v3.30.0 republish)自行收编;rebase 到最新 main 后
diff 已收窄到只剩本项(按评审要求)。

Closes huiliyi37#445
@Ver-zhzh
Ver-zhzh force-pushed the fix/issue-445-sidecar-mem branch from 7b99cad to 4dffbed Compare October 11, 2026 18:00
@Ver-zhzh

Copy link
Copy Markdown
Collaborator Author

两项已补完,请核验:

请求 1(rebase 收窄):已 rebase 到最新 main(含 abb7628)。diff 已收窄到只剩第三项——evictLoadedBeyondCap 的 watched-idle 淘汰差集(src/server/session-manager.ts 1 个文件);maxEventBytes 8MB 与 effectiveAfterRestart 两处重复改动已丢弃(保留 main 版)。PR 正文已同步改写为收编对账 + 差集说明。

请求 2(回归测试):新增 src/server/__tests__/session-evict-watched-idle.test.ts,覆盖全部三个场景——① 超 TTL 的被观察会话被淘汰且淘汰后事件可从磁盘重放;② 未超 TTL 不淘汰;③ idleAgentTtlMs<=0 时被观察会话永不淘汰。3/3 通过;session-manager 相关套件 133/133 无回归;tsc --noEmit 0 新增报错。

另:实现测试时发现并顺手修了一个边界——evictLoadedBeyondCap 新增可选参数 exceptId,ensureEvents 刚载入的会话不受同一次淘汰扫描影响,避免刚读回的日志被同一调用立即丢弃(有 docstring 说明)。

#445 的「会话常驻淘汰被跳过」缺口仍在本 PR 差集里,未丢。

evictLoadedBeyondCap 淘汰 watched-idle 会话后 eventsLoaded=false,
但 adoptExternalSessions 的漂移合并与 mergeExternalEvents 的
await 后守卫都只看 eventsLoaded,导致活着的 SSE listener 永远
收不到外部新事件(session-rehydrate LRU 压力测试回归)。

放宽两处资格判定为 eventsLoaded || listeners.size > 0:
无 listener 的已淘汰会话仍不参与合并(内存继续释放);
只合并 seq > session.seq 的尾部新事件,不全量常驻;
getEvents→ensureEvents 磁盘重放路径未动。
@Ver-zhzh

Copy link
Copy Markdown
Collaborator Author

rework 后 CI 新红已定位并修复(commit 107a56d,已 push,CI 重跑中)。

回归:rework 让 watched-idle 超 TTL 会话被淘汰(eventsLoaded=false、listeners 保留),但 adoptExternalSessions 的漂移合并与 mergeExternalEvents 的 await 后守卫都只看 eventsLoaded——被淘汰会话的活 SSE listener 永远收不到外部新事件,session-rehydrate.test.ts 的 watched old history preserves external under sync/async LRU pressure 2 用例失败(clean main 73/73 全过实证为 rework 引入)。

修复(仅 src/server/session-manager.ts,+13/−3):两处资格判定放宽为 eventsLoaded || listeners.size > 0。无 listener 的已淘汰会话仍不参与合并(内存继续释放);只合并 seq > session.seq 的尾部新事件,不把全量历史重新常驻;getEvents→ensureEvents 磁盘重放路径与淘汰语义本身均未动。

验证:tsc 0 报错;session-rehydrate 73/73、session-evict-watched-idle 3/3、session-manager 133/133;另跑 session-tail-memory/replay-batch/events-trim/replay-compaction/manager-goal/runtime-pool 44/44 全过。

设计说明:淘汰 watched 会话只丢事件日志、保留订阅;有外部进度到达时走漂移合并做尾部重读+扇出,SSE 语义恢复为“不断连即可实时收到”,同时 #445 的常驻内存泄漏目标不受影响。

@Ver-zhzh

Copy link
Copy Markdown
Collaborator Author

rework 后 CI(run 38182650980,commit 107a56d)已跑完,汇报结论:

  • 回归已修复:session-rehydrate 73/73、session-evict-watched-idle 3/3、session-manager 133/133 本地全绿,CI 里 session-rehydrate 不再失败 ✅
  • 仍红:ci(24) 7 个 + windows-smoke,全部逐项核对为上游 main 预存失败(干净 main run 38137254300 相同集合):cognitive-capsule-router capsule-recall 2 项、max-lines ratchet、release-catalog(DRY RUN / AtomGit / old publishers)、startup memory RSS 128MB;windows-smoke 同样是 runner EPERM 环境问题。均与本批改动(evictLoadedBeyondCap 资格放宽 + 合并尾部策略)无关。

无需返工,等待核验。

@huiliyi37

Copy link
Copy Markdown
Owner

已完成审查,并按本项目的收编流程移植合入开发主仓 main,提交 97a7905ce,保留了 @Ver-zhzh 的共同作者署名。感谢补齐差集和回归测试。

本次采纳了 watched-idle 超 TTL 淘汰、当前打开会话保护,以及淘汰后 SSE 外部进度推送。审查后另补强了两处边界:无持久化读取能力时保留被观察历史;已淘汰会话接收外部进度时不重新积累脱离 LRU 管理的事件环。还增加了其他会话都受保护时,同步/异步打开历史的压力用例。

验证:会话相关 517 项测试、类型检查通过;6 类隔离缺陷还原均触发行为断言。行数预算仍有既有失败,本次未新增违规;Windows 桌面长期堆增长仍需实机复测。

当前完成的是开发主仓收编,尚未同步公开仓,因此此 PR 暂保持打开;公开仓同步和收编关闭流程完成后再更新状态。

@huiliyi37

Copy link
Copy Markdown
Owner

已通过 sync 流程合入 dev,感谢 @Ver-zhzh 的贡献 🎉

本 PR 的改动(fix(server): sidecar 会话事件日志内存上限收敛(#445))已经 sync 流程合入主仓(本次 sync),并已收录进 CONTRIBUTORS.md。

按仓库的双仓流程,公开仓 PR 不经 merge 按钮合入(保持 dev 主仓唯一事实源),现以 sync-merged 标记关闭。欢迎继续贡献!

@huiliyi37 huiliyi37 added the sync-merged 内容已通过 sync 流程合入 dev 主仓(PR 形态留痕,作者计入 CONTRIBUTORS) label Oct 12, 2026
@huiliyi37 huiliyi37 closed this Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sync-merged 内容已通过 sync 流程合入 dev 主仓(PR 形态留痕,作者计入 CONTRIBUTORS)

Projects

None yet

3 participants