Skip to content

[Bug] 会话去重(#1374)平局裁决依赖文件系统遍历顺序:Windows 上副本反向胜出,新回归测试在 NTFS 必挂 #1383

Closed

Description

@shabhui

[Bug] 会话去重(#1374 / f90c4e3)在内容相同的副本场景下,胜者由文件系统遍历顺序决定——Windows 上副本会反向胜出

现象

NativePiSessionService.list()(packages/agent-runtime/src/native-pi-session.ts,f90c4e31e 引入的去重逻辑)对相同 nativeId 的多个文件按 updatedAt 降序取"最新写入"者。但 updatedAt 取自文件内容(最后一条 entry 的 timestamp):

const updatedAt = branch.at(-1)?.timestamp ?? createdAt;

字节级副本(手动复制、备份)与原件内容完全相同 → updatedAt 必然相同。这正是 #1359/#1374 要解决的典型场景,也就是说"最新写入"的裁决条件在此场景下恒为平局,实际胜者由 Array.prototype.sort 稳定排序保持的输入顺序决定,即 walkJsonl() 的遍历顺序:

const pending = [root];
while (pending.length) {
  const directory = pending.pop()!;          // LIFO
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
    if (entry.isDirectory()) pending.push(path);   // 子目录按 readdir 顺序入栈
    else if (...) files.push(path);
  }
}

栈是 LIFO:readdir 顺序靠后的目录反而先被遍历。因此:

  • Windows / NTFS(readdir 按字母序):--project-- < backup,backup 后入栈、先弹出 → 副本先进入 summaries → 平局中副本获胜,原件的 record 被 this.records.delete() 删除。
  • Linux / ext4(readdir 为哈希序):顺序不定,恰好在该测试布局下原件先遍历 → 原件获胜。

已在 Windows 11 / NTFS 上用与上游逐字相同的 walkJsonl + 稳定排序逻辑模拟验证:扫描根含 --project--/fixture.jsonl(原件)与 backup/fixture-copy.jsonl(副本)时,遍历顺序为 [backup/fixture-copy.jsonl, --project--/fixture.jsonl],平局胜者为副本。

后果

  1. 新回归测试在 Windows 上必挂:collapses copied session files onto one entry per native id (#1359) 断言 sessions[0].id === original.id(原件 id)。NTFS 上胜者是副本(stableId 含路径,两者 id 不同)→ 断言失败。CI(ubuntu/ext4)恰好通过,掩盖了平台差异。
  2. 产品行为平台不一致:Windows 用户在 sessions 扫描根下留有 .jsonl 副本(备份)时,列表指向副本;从侧栏重新打开后,后续对话追加写入副本文件,原件从此冻结(数据未丢,但"原件不再更新"违背直觉;commit message 中 "keeps the newest write" 与 "existing persisted references stay valid" 两点在平局场景均不成立——原件的 stableId 被 records 删除,持久化引用得到 NOT_FOUND)。
  3. 运行中会话边缘场景:原件有活跃 runtime 时 list() 触发去重,若副本胜出,原件 record 被删 → 该 id 的 detail()/fork()/openRuntime() 抛 NOT_FOUND;侧栏显示副本条目(canStop=false)而真实 runtime 仍在隐形运行。

复现(Windows)

  1. 按 fixture 布局:sessions/--project--/fixture.jsonl + sessions/backup/fixture-copy.jsonl(内容相同);
  2. 单跑 native-pi-session.test.ts 中的 collapses copied session files onto one entry per native id (#1359);
  3. 断言 sessions[0].id === original.id 失败(胜者为副本路径派生的 stableId)。

(Linux/ext4 下因 readdir 哈希序恰好相反,同一测试通过。)

建议修复方向

  • 平局时引入确定性、跨平台一致的 tie-break,例如:
    • 优先保留 this.runtimes 中已有活跃 runtime 的 id(保护正在进行的会话);
    • 其次按确定性键(如路径字典序)裁决,而非依赖 readdir 顺序;
  • 测试断言改为"去重后仅剩一条"(toHaveLength(1))或显式断言确定性胜者,避免隐式依赖文件系统枚举顺序;
  • 若要兑现 "keeps the newest write",可用 nativePiSnapshot(...).mtimeMs(真实写入时间)而非内容时间戳做次级比较。

相关

Activity

  1. vastsa commented on Oct 4, 2026

    @vastsa
    Owner

    hi,欢迎提交修复 pr

  2. shabhui commented on Oct 4, 2026

    @shabhui
    ContributorAuthor

    PR 提交了:#1385。平局裁决改为确定性的两级排序(先保住已打开 runtime 的文件,再按路径序),并补了两个回归测试(路径序确定性 + runtime 优先),原有 #1359 测试在 NTFS 上从必挂变为稳定通过。

  3. added a commit that references this issue on Oct 4, 2026
    00bec0c
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