Repository navigation
[Bug] 会话去重(#1374)平局裁决依赖文件系统遍历顺序:Windows 上副本反向胜出,新回归测试在 NTFS 必挂 #1383
Copy link
Copy link
Closed
Description
[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],平局胜者为副本。
后果
- 新回归测试在 Windows 上必挂:
collapses copied session files onto one entry per native id (#1359)断言sessions[0].id === original.id(原件 id)。NTFS 上胜者是副本(stableId 含路径,两者 id 不同)→ 断言失败。CI(ubuntu/ext4)恰好通过,掩盖了平台差异。 - 产品行为平台不一致:Windows 用户在 sessions 扫描根下留有
.jsonl副本(备份)时,列表指向副本;从侧栏重新打开后,后续对话追加写入副本文件,原件从此冻结(数据未丢,但"原件不再更新"违背直觉;commit message 中 "keeps the newest write" 与 "existing persisted references stay valid" 两点在平局场景均不成立——原件的 stableId 被 records 删除,持久化引用得到 NOT_FOUND)。 - 运行中会话边缘场景:原件有活跃 runtime 时
list()触发去重,若副本胜出,原件 record 被删 → 该 id 的detail()/fork()/openRuntime()抛NOT_FOUND;侧栏显示副本条目(canStop=false)而真实 runtime 仍在隐形运行。
复现(Windows)
- 按 fixture 布局:
sessions/--project--/fixture.jsonl+sessions/backup/fixture-copy.jsonl(内容相同); - 单跑
native-pi-session.test.ts中的collapses copied session files onto one entry per native id (#1359); - 断言
sessions[0].id === original.id失败(胜者为副本路径派生的 stableId)。
(Linux/ext4 下因 readdir 哈希序恰好相反,同一测试通过。)
建议修复方向
- 平局时引入确定性、跨平台一致的 tie-break,例如:
- 优先保留
this.runtimes中已有活跃 runtime 的 id(保护正在进行的会话); - 其次按确定性键(如路径字典序)裁决,而非依赖 readdir 顺序;
- 优先保留
- 测试断言改为"去重后仅剩一条"(toHaveLength(1))或显式断言确定性胜者,避免隐式依赖文件系统枚举顺序;
- 若要兑现 "keeps the newest write",可用
nativePiSnapshot(...).mtimeMs(真实写入时间)而非内容时间戳做次级比较。
相关
- 引入提交:f90c4e31e(经 PR fix(sessions): specify native duplicate identity handling #1374 合并,修复 [Bug] 会话列表出现重复会话:递归扫描 + 按路径生成 ID,子目录副本变成第二条会话 #1359,取代未合并的 fix(sessions): collapse copied session files onto one entry per native id #1368)。
- 我此前上报的 [Bug] 会话列表出现重复会话:递归扫描 + 按路径生成 ID,子目录副本变成第二条会话 #1359(副本会话双条目)即由此修复;本 issue 是该修复在平局场景下的遗留缺陷,非新功能请求。
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
vastsa commentedon Oct 4, 2026
hi,欢迎提交修复 pr
shabhui commentedon Oct 4, 2026
PR 提交了:#1385。平局裁决改为确定性的两级排序(先保住已打开 runtime 的文件,再按路径序),并补了两个回归测试(路径序确定性 + runtime 优先),原有 #1359 测试在 NTFS 上从必挂变为稳定通过。
fix(sessions): settle native dedupe ties deterministically