Skip to content

feat(channel-watch): 频道订阅保护(不被自动 GC 卸载)+ 持久游标与离线补投 - #67

Merged
Jaxton07 merged 8 commits into
mainfrom
feat/channel-watch-retention
Sep 20, 2026
Merged

Jaxton07 merged 8 commits into
mainfrom
feat/channel-watch-retention

Conversation

@Jaxton07

@Jaxton07 Jaxton07 commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

问题

跨会话频道协作(channel-watch)里,订阅是会话实例内的内存态(watcher + 订阅集),而 renderer 的内存策略会把「空闲且不受保护」的会话 dispose() 掉。后果:

  • 会话被自动卸载(或用户关掉)后,订阅静默消失——对端以为「已经通知过」,消息永久丢失,而用户看不出来(左栏那个会话还在,只是它不再收唤醒);
  • 会话重新打开后无法知道自己离线期间漏了哪些消息(没有持久版本基线)。

改动

P0 · 有订阅的会话不被自动 GC

  • 扩展把「有效运行态订阅」上报 backend(onSubscriptionsChanged:enabled + trusted 才计入;disabled/untrusted/shutdown 上报空集;只传副本、回调异常只记日志,不阻断工具与生命周期)
  • backend 保存快照(channelSubscriptionIds)并提供查询通道 session:getChannelSubscriptionSessionIds(只返回仍在 registry 的会话;dispose 兜底清理)
  • renderer 每轮 GC 前先拉快照 → 映射进 isProtected(订阅 = 明确驻留语义,且不占 K=3 保留名额);快照拉取失败 = 保护范围未知 → 整轮跳过一个都不卸(不退化成「让 backend 拒绝一次」)
  • 自动卸载与用户意图分开:closeSession(id, intent),仅 intent:"gc" 且有订阅时拒绝({closed:false});用户主动关/删不受此限(含 delete 仍生效,退订最后一个 topic 即恢复 GC 资格)

P1 · 持久游标 + 离线补投

  • 订阅快照与 payload 同一条落盘(channel-subs):{topics, cursors};cursors 里 key 缺失 = 旧载荷未知基线(只记基线不补历史),与 null(确认时文件不存在)语义不同
  • 首次订阅先记基线(不补历史)→ 起 watcher(single-flight,失败可重试、shutdown 不留孤儿)→ 二次对账封住「读基线 ↔ watcher 就绪」竞态
  • session_start 恢复后逐 topic 对账:已知 cursor 与当前版本不同 → 补投恰好一次并推进落盘;重开不重复
  • paused 期间不推进 cursor(显式重新 subscribe 合并补投一次);自写抑制仅当写前磁盘版本 == cursor 才推进,否则保留 pending(避免吞掉对端已写、我们还没提醒的内容);未订阅/已退订在异步续体里一律复查(不产生幽灵投递/幽灵游标/孤儿 watcher)
  • 只接受 <topic>/MESSAGES.md(嵌套同名文件不唤醒也不污染 cursor)

验证

npm run typecheck   ✅
npm run lint        ✅(biome check .,434 files)
npm run test        ✅ backend 55 files / 543 passed;desktop 44 files / 474 passed(合计 1017 passed, 0 failed)
npx tsx scripts/smoke-channel-watch.mts v7   ✅ 真 PiBackend:订阅恢复 → 快照上报 → intent:'gc' 拒绝 → intent:'user' 放行 → dispose 无残留
npx tsx scripts/smoke-channel-watch.mts v8   ✅ 真 session_start + 真 appendEntry + 真 fs.watch:离线变化补投恰好一次、游标落盘、重开不重复
  • 单测覆盖 P0/P1 全部契约(含并发 subscribe 单 watcher、启动失败重试、启动期间 shutdown 不留孤儿、跨 await 退订/shutdown、嵌套同名文件、自写 pending);关键竞态用例都用注入的 hash 读取/短防抖做确定性复现,不靠真实时序等待。
  • 两个冒烟均零凭证(无需 provider),临时目录自清理。
  • 真机 UI 体验确认(订阅会话在长空闲后仍收唤醒)建议在合并前由维护者做一次;本轮未做(不与用户正在运行的正式版抢数据)。

合并顺序说明(已 rebase 到 #68 之后)

本 PR 已 rebase 到 #68(sidebar 第 2 轮)合并后的 main(d38c4e6)之上:冲突只出现在 docs/INDEX.md 与 docs/PITFALLS.md,按「保留 main 的侧栏内容 + 只落本任务增量」解掉(解完两个文档与原分支的版本逐字节一致),rebase 后在 d38c4e6 基上重跑了 typecheck / lint / test(543 + 474 = 1017 passed)/ V7+V8 冒烟,CI check 通过(3m39s)。PR 的改动面未变:20 文件 +1986/−144。

文档

  • docs/INDEX.md:channel-watch / session-gc / session-gc-run / use-session-gc / stores / pi-backend / smoke 行同步
  • docs/PITFALLS.md:新增「长生命周期订阅不能挂在可被自动 GC 的会话实例上(2026-09-20)」(4 条不易自己想到的事实 + 症状索引 3 行)

已知限制(本期不修,已在文档标明)

  • 轮询降级模式的删除检测与 watcher root 目录被删后重建:两类异常场景会漏事件(订阅会话仍在,但那一次变化不投递)
  • 自写推进的写前版本在 tool_call 钩子里读取,外部写入若完全落在「读版本 ↔ 本会话随即落盘」的微秒窗口内,仍可能跟过头一次

Checklist

  • npm run typecheck passes
  • npm run test passes
  • New user-facing strings were added to both zh and en dictionaries(本 PR 无新增用户可见文案;唯一的运行时提示是中英混排的现有 notify 文案风格,与既有 channel-watch 提示一致)
  • No API keys or sensitive data included

@Jaxton07
Jaxton07 force-pushed the feat/channel-watch-retention branch from aeef297 to 5cadff6 Compare September 20, 2026 10:44
@Jaxton07
Jaxton07 merged commit 0798045 into main Sep 20, 2026
1 check passed
@Jaxton07
Jaxton07 deleted the feat/channel-watch-retention branch September 20, 2026 10:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant