From f90c4e31ef89d984c5769d88e79fb3681e02e5e3 Mon Sep 17 00:00:00 2001 From: yexisu <134936161+yexisu@users.noreply.github.com> Date: Sun, 4 Oct 2026 10:08:53 +0800 Subject: [PATCH 1/2] 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)); } From 530b6dbb91c82c3c8249b51a5f09ff8258e7c901 Mon Sep 17 00:00:00 2001 From: vastsa Date: Sun, 4 Oct 2026 10:57:16 +0800 Subject: [PATCH 2/2] docs(sessions): specify native session deduplication Document the identity and preservation behavior expected when native transcript files share an id, and record the full user journey for the resulting projection. --- docs/spec/03-runtime/04-data-storage.md | 5 +++++ docs/spec/06-delivery/04-e2e-test-plan.md | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/spec/03-runtime/04-data-storage.md b/docs/spec/03-runtime/04-data-storage.md index bcb12e6c04..e9bc11c0b2 100644 --- a/docs/spec/03-runtime/04-data-storage.md +++ b/docs/spec/03-runtime/04-data-storage.md @@ -1700,6 +1700,11 @@ source-discriminated transcript authority owned by the Node agent sidecar. They are never inserted into SQLite and never copied to the Desktop transcript directory. `session.list` merges their projections with Rust-owned Desktop summaries, and `session.get` routes by the opaque `native-pi:` id. +Discovery deduplicates native files that share the same JSONL `header.id`, +keeping the projection with the newest transcript `updatedAt`. The selected +file retains its path-derived opaque session id; duplicate files are not +rewritten or deleted, and their paths are omitted from the in-memory lookup +map for the current scan. Detail reads take an immutable byte snapshot, parse it into an in-memory `SessionManager`, and follow the current native branch. They must not call diff --git a/docs/spec/06-delivery/04-e2e-test-plan.md b/docs/spec/06-delivery/04-e2e-test-plan.md index bd4b516122..7bae4144ec 100644 --- a/docs/spec/06-delivery/04-e2e-test-plan.md +++ b/docs/spec/06-delivery/04-e2e-test-plan.md @@ -15440,6 +15440,19 @@ plugin-form fixtures in an isolated temporary directory at runtime. - **Specs:** IPC native routing; runtime §12; storage §12; security §12. - **Status:** Documented; run after integration into main. +### E2E-SESSION-native-pi-duplicate-id-collapses + +- **Preconditions:** An isolated Pi agent directory contains a valid native v3 + session and a copied JSONL file under a nested directory with the same + `header.id`. +- **Steps:** Start PI-Desktop with the fixture-only agent directory, refresh the + session list, open the listed native session, and search its transcript. +- **Expected:** The list contains one row for that native id. Detail and search + resolve through the selected file; duplicate files remain byte-identical and + are not rewritten or deleted. +- **Specs:** runtime §12; storage §12; ADR 0254. +- **Status:** Source-regression covered; full Desktop journey Draft. + ### E2E-SUBAGENT-ordered-model-fallback-preserves-work