Skip to content

重启伪造时间戳:buildSession 用 Date.now() 覆盖 lastSeen,面板"最后活动"全员开机时刻、fallback=latest 在 ~245 路平局里随机挂会话 #404

Closed

Description

@wosnxn123

症状(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

  1. ranxianglei commented on Aug 31, 2026

    @ranxianglei
    Owner

    请处理这个 issue 并回复处理结果

  2. ranxianglei commented on Aug 31, 2026

    @ranxianglei
    Owner

    🤖 ework agent · vllm-qwen/qwen3.8-27b

    [bot] 🏷 已接手,正在处理。

    根因确认:src/persist.ts:376 的 buildSession 对每个从磁盘恢复的会话打 lastSeen: Date.now()(开机时刻),而非沿用文件里的 savedAt。三个消费端全被污染:

    • 面板 stats(src/server.ts:2712)
    • handlePluginStatus 的 fallback=latest 排序(src/plugin.ts:313-321)
    • 附带:initSessions 按 createdAt 截断 top-256(src/session.ts:145),老创造新活跃的会话会被误杀

    修复方案(按 issue 建议):

    1. buildSession 沿用 envelope.savedAt(文件写盘时刻)作为 lastSeen,恢复行打 restored:true
    2. fallback=latest 仅接受本进程内有真实活动(restored===false)的会话,否则明确报"无近期活动会话"而非在平局里猜
    3. 内存截断键从 createdAt 改为 max(createdAt, lastSeen)
    4. 面板对 restored 行灰显

    正在实现 + 补验收测试,完成后回贴 PR 链接。

  3. ranxianglei commented on Sep 3, 2026

    @ranxianglei
    Owner

    修复 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 四阶段 ✓。

  4. ranxianglei commented on Sep 3, 2026

    @ranxianglei
    Owner

    🤖 ework agent · qwen3.8-27b

    [bot] 🏷

    已独立复核 #404 的根因与修复。结论:PR #487 完整且正确地解决了本 issue,无需另开重复 PR(避免与在途 PR 冲突/制造噪音)。以下是我的独立验证记录,非转述。

    一、根因复核(与 #487 诊断一致)
    buildSession 对每条从磁盘恢复的记录打 lastSeen: Date.now()(src/persist.ts),而非沿用文件里的 savedAt。三个消费端全被污染:

    • 面板 stats(src/server.ts sendStats)→"最后活动"列全员显示开机时刻;
    • handlePluginStatus 的 fallback=latest(src/plugin.ts)→ 按假时间戳排序,~245 路平局里按 readdir 序任取,可能把新客户端挂到毫不相干的旧会话;
    • initSessions 按 createdAt 截断 top-N(src/session.ts)→"老创造、新活跃"的会话被先丢。

    二、#487 的修复(issue 三条建议全覆盖)

    1. lastSeen = parsed.savedAt ?? Date.now(),恢复会话标记 restored(内存态、不落盘、无 schema 变更);首个真实请求(getSession 的"命中已有"与"内存 miss 重载"两条路径)清除标记;
    2. fallback=latest 只接受 restored !== true 的会话;全部为恢复态时返回 404 并明确报 "no session with activity since boot",不再按 readdir 序猜;
    3. 启动截断键改为 max(createdAt, lastSeen)——老创造但活跃到最近的会话不再让位给新创造但闲置的。

    三、我的独立验证(检出该分支到隔离 worktree 实测,非凭评论)

    • npm run typecheck ✓
    • 新增 tests/issue404-restore-timestamps.test.ts 3/3 通过,逐条命中验收标准:
      • 恢复会话 lastSeen === 文件 savedAt(非恢复时刻)+ writeNow → loadAll 往返后 lastSeen 早于加载时刻;
      • 1 个真活跃会话 vs 若干恢复会话:fallback=latest 必返活跃者;全恢复态则 404("since boot");
      • 老创造(createdAt 更早)但 recently-saved 的会话在 top-N 截断中存活,而新创造但闲置的被丢弃。
    • 全量测试 978/979;唯一失败是 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。

  5. added a commit that references this issue on Sep 3, 2026
  6. added a commit that references this issue on Sep 4, 2026
  7. added 3 commits that reference this issue on Oct 14, 2026
  8. added a commit that references this issue on Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions