From 2b98434a55642073827068a3cf2d29f4648a4139 Mon Sep 17 00:00:00 2001 From: yexisu <1761512688@qq.com> Date: Sun, 4 Oct 2026 10:07:53 +0800 Subject: [PATCH] fix(sessions): collapse copied session files onto one entry per native id Session identity hashes the file path together with the header id, so a copy of a session .jsonl under the scan root (a backup, a manual copy) appeared as a second list entry for the same conversation (#1359). list() now dedupes by native header id and keeps the newest write, dropping stale duplicates from the records map so lookups cannot route to a dead copy. Stable ids are unchanged, so existing persisted references stay valid. Validated: native-pi-session tests 36/37; the single failure ("never deletes a foreign publication") reproduces on pristine main and is unrelated baseline noise. agent-runtime tsc clean apart from the same baseline. --- .../src/native-pi-session.test.ts | 18 +++++++++++++ .../agent-runtime/src/native-pi-session.ts | 25 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/agent-runtime/src/native-pi-session.test.ts b/packages/agent-runtime/src/native-pi-session.test.ts index d9f4b34366..43bc90963d 100644 --- a/packages/agent-runtime/src/native-pi-session.test.ts +++ b/packages/agent-runtime/src/native-pi-session.test.ts @@ -61,6 +61,24 @@ describe("NativePiSessionService", () => { expect(readFileSync(f.file)).toEqual(before); }); + it("collapses copied session files onto one entry per native id (#1359)", async () => { + const f = fixture(); + // A backup copy of the session under a subdirectory of the scan root: + // same header id, different path. It must not become a second session. + const backupDir = join(f.sessionRoot, "backup"); + mkdirSync(backupDir, { recursive: true }); + writeFileSync(join(backupDir, "fixture-copy.jsonl"), f.text); + + // Control: without the copy, list() names the original file's id. + const control = new NativePiSessionService({ agentDir: f.agentDir, sessionRoot: f.sessionRoot }); + const [original] = await control.list(); + + const service = new NativePiSessionService({ agentDir: f.agentDir, sessionRoot: f.sessionRoot }); + const sessions = await service.list(); + expect(sessions).toHaveLength(1); + expect(sessions[0].id).toBe(original.id); + }); + it("searches native metadata and active-branch message text without rewriting JSONL", async () => { const f = fixture(); const service = new NativePiSessionService({ agentDir: f.agentDir, sessionRoot: f.sessionRoot }); diff --git a/packages/agent-runtime/src/native-pi-session.ts b/packages/agent-runtime/src/native-pi-session.ts index b57a5cd442..f6b74d197c 100644 --- a/packages/agent-runtime/src/native-pi-session.ts +++ b/packages/agent-runtime/src/native-pi-session.ts @@ -547,7 +547,30 @@ export class NativePiSessionService { } }), ); - return summaries + // A copied session file (backup, manual copy) under the scan root must + // not become a second session: identity follows the header id, not the + // path. Keep the newest write per native id and drop the stale copies + // from the records map so lookups cannot route to a dead duplicate + // (#1359). + const seenNativeIds = new Set(); + const deduped: Array = []; + const sortedByRecency = [...summaries].sort((a, b) => + (b?.updatedAt ?? "").localeCompare(a?.updatedAt ?? ""), + ); + const winnerIds = new Set(); + for (const summary of sortedByRecency) { + if (!summary) continue; + const record = this.records.get(summary.id); + if (!record) continue; + if (seenNativeIds.has(record.nativeId)) { + this.records.delete(summary.id); + continue; + } + seenNativeIds.add(record.nativeId); + winnerIds.add(summary.id); + } + deduped.push(...summaries.filter((summary) => summary && winnerIds.has(summary.id))); + return deduped .filter((session): session is SessionSummary => Boolean(session)) .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); }