## 症状(0.1.66/0.1.67,HEAD 仍在) 重启后,web 面板"最后活动"列全员显示**开机时刻**——本机实测::57645(08:03:00Z 启动)内存里 **256 个会话有 245 个 `lastSeen` 等于同一毫秒 2026-08-31T08:03:00.294Z**,而其注册表文件的 savedAt 早在 05:42:50Z、05:08:08Z。新鲜度列彻底失真;更实害的是 **`plugin/status` 的 `fallback=latest` 按这个假时间戳排序**——245 路平局里按 readdir 序任取一个,可能把新客户端挂到**毫不相干的旧会话**上。 ## 根因 `buildSession` 对每个从磁盘加载的记录打 `lastSeen: Date.now()`(`src/persist.ts:376` dist 49911 区段 / `src/session.ts:170`),而非沿用 `parsed.savedAt`。三个消费端全被污染:面板 stats(57173/`server.ts:2698-2712`)、`handlePluginStatus` 的 fallback=latest(`src/plugin.ts:309-321`,dist 53808-53818)、以及排序逻辑本身。附带发现:`initSessions` 按 **createdAt**(创建时刻)保留 top-256——"老创造、新活跃"的会话会被先丢。 ## 建议修复 - `lastSeen: parsed.savedAt ?? Date.now()`;恢复行打 `restored:true`,面板可灰显; - `fallback=latest` 仅接受**开机后有真实活动**的会话,否则明确报"无近期活动会话"而非猜; - 内存截断键从 createdAt 改为 max(createdAt, savedAt)。 ## 验收标准 - [ ] 重启后面板"最后活动"与文件 mtime/savedAt 一致(±写盘节流窗口) - [ ] 构造 245 个恢复会话 + 1 个真活跃会话:fallback=latest 必返后者 - [ ] 活跃但古老的会话不被 loadAll 截断误杀
Activity
ranxianglei commentedon Aug 31, 2026
请处理这个 issue 并回复处理结果
ranxianglei commentedon Aug 31, 2026
[bot] 🏷 已接手,正在处理。
根因确认:
src/persist.ts:376的buildSession对每个从磁盘恢复的会话打lastSeen: Date.now()(开机时刻),而非沿用文件里的savedAt。三个消费端全被污染:src/server.ts:2712)handlePluginStatus的fallback=latest排序(src/plugin.ts:313-321)initSessions按createdAt截断 top-256(src/session.ts:145),老创造新活跃的会话会被误杀修复方案(按 issue 建议):
buildSession沿用envelope.savedAt(文件写盘时刻)作为lastSeen,恢复行打restored:truefallback=latest仅接受本进程内有真实活动(restored===false)的会话,否则明确报"无近期活动会话"而非在平局里猜createdAt改为max(createdAt, lastSeen)restored行灰显正在实现 + 补验收测试,完成后回贴 PR 链接。
ranxianglei commentedon Sep 3, 2026
修复 PR:#487
按 issue 的建议实现:
buildSession:lastSeen = parsed.savedAt ?? Date.now(),恢复的会话在内存标记restored: true(不落盘,无 schema 变更),首个真实请求(getSession两条 touch 路径:命中已有会话 / 内存 miss 重载)清除标记initSessions截断键改为max(createdAt, savedAt)——老创造但活跃到最近的会话不再让位给新创造但闲置的fallback=latest只接受restored !== true的会话;全部为恢复态时 404 并明确报 "no session with activity since boot",不再按 readdir 序随机猜__bili/stats暴露restored,web 会话表灰显标注「(恢复,开机后未活动)」附带收益:
evictOldest/listSessions同样消费lastSeen,恢复会话现在按真实活跃度排序、优先被驱逐。验证:新测试 3/3;全量 977/979(2 个为 plugin-agent.test.ts 已知永久环境失败,干净 master 同样复现);build ✓;E2E 四阶段 ✓。
ranxianglei commentedon Sep 3, 2026
[bot] 🏷
已独立复核 #404 的根因与修复。结论:PR #487 完整且正确地解决了本 issue,无需另开重复 PR(避免与在途 PR 冲突/制造噪音)。以下是我的独立验证记录,非转述。
一、根因复核(与 #487 诊断一致)
buildSession对每条从磁盘恢复的记录打lastSeen: Date.now()(src/persist.ts),而非沿用文件里的savedAt。三个消费端全被污染:src/server.tssendStats)→"最后活动"列全员显示开机时刻;handlePluginStatus的fallback=latest(src/plugin.ts)→ 按假时间戳排序,~245 路平局里按 readdir 序任取,可能把新客户端挂到毫不相干的旧会话;initSessions按createdAt截断 top-N(src/session.ts)→"老创造、新活跃"的会话被先丢。二、#487 的修复(issue 三条建议全覆盖)
lastSeen = parsed.savedAt ?? Date.now(),恢复会话标记restored(内存态、不落盘、无 schema 变更);首个真实请求(getSession的"命中已有"与"内存 miss 重载"两条路径)清除标记;fallback=latest只接受restored !== true的会话;全部为恢复态时返回 404 并明确报 "no session with activity since boot",不再按 readdir 序猜;max(createdAt, lastSeen)——老创造但活跃到最近的会话不再让位给新创造但闲置的。三、我的独立验证(检出该分支到隔离 worktree 实测,非凭评论)
npm run typecheck✓tests/issue404-restore-timestamps.test.ts3/3 通过,逐条命中验收标准:lastSeen === 文件 savedAt(非恢复时刻)+writeNow → loadAll往返后lastSeen早于加载时刻;fallback=latest必返活跃者;全恢复态则 404("since boot");tests/launcher.test.ts的resolveClientCommand: codex/claude resolve to themselves——我已在**干净 master(合并基线)**上复现同样这一处失败,属预先存在的环境性失败(取决于沙箱内装了哪些 client 二进制),与本修复无关。npm run build✓(dist 产物正常生成)。四、一处设计说明:#487 取的是 payload 里的
savedAt(parsed.savedAt),而buildRecord每次写盘都会写入该字段,因此取值可靠(与 envelope 层savedAt同为写盘时刻)。用可选restored?字段也避免了大面积改动测试辅助代码,整体改动最小。结论与建议:#487 方案正确、覆盖全部验收标准、附带修正了驱逐/列表排序的消费端。建议 owner review 后合并(合并为人工操作)。我这边已把并行做的重复实现清理掉,不再另开 PR。
Merge pull request #487 from ranxianglei/2026-09-03_issue-404-restore…
fix: restart restores savedAt as lastSeen instead of fabricating boot…
test: realistic savedAt epochs in #404 boot test — #478 boot GC prune…
test(#404): pin lazy restore semantics under the warm-off default + e…
fix(web): serve /__bili lists from the unified index and bound boot p…
test: pin #3199 cold-scan behavior (shadow reconcile, bounded boot, l…
test: realistic savedAt epochs in ranxianglei#404 boot test — ranxian…