From b626f10206ecc503ad174ef6da488dbe0d5baa5f Mon Sep 17 00:00:00 2001 From: Dhruva Sahrawat <21278423+dhruva77@users.noreply.github.com> Date: Sun, 18 Oct 2026 21:28:50 +0530 Subject: [PATCH] fix(pi): make the session roots fingerprint independent of discovery order The fingerprint sorted each root's retention keys but joined the roots in discovery order. That order follows working directories and process contexts, so the same roots found in another order invalidated the cached scope and forced a rescan. Sort the components before joining; traversal order is unchanged. --- .../CodexBarCore/PiSessionCostScanner.swift | 2 ++ .../PiSessionProcessContextTests.swift | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/Sources/CodexBarCore/PiSessionCostScanner.swift b/Sources/CodexBarCore/PiSessionCostScanner.swift index 2951ec36eed..e4821f2a222 100644 --- a/Sources/CodexBarCore/PiSessionCostScanner.swift +++ b/Sources/CodexBarCore/PiSessionCostScanner.swift @@ -579,6 +579,8 @@ enum PiSessionCostScanner { ].joined(separator: "\u{1F}") } } + // Discovery order follows working directories and process contexts; it is not part of the scope. + .sorted() .joined(separator: "\u{1E}") } diff --git a/Tests/CodexBarTests/PiSessionProcessContextTests.swift b/Tests/CodexBarTests/PiSessionProcessContextTests.swift index cd33e50cc6c..cea327ed2eb 100644 --- a/Tests/CodexBarTests/PiSessionProcessContextTests.swift +++ b/Tests/CodexBarTests/PiSessionProcessContextTests.swift @@ -837,4 +837,35 @@ struct PiSessionProcessContextTests { #expect(PiSessionCostCacheIO.load(cacheRoot: env.cacheRoot).sessionRootsFingerprint? .contains(firstRoot.path) != true) } + + @Test + func `pi cost scope ignores the discovery order of identical session roots`() throws { + let env = try CostUsageTestEnvironment() + defer { env.cleanup() } + + let projects = try ["alpha", "beta", "gamma"].map { name in + let project = env.root.appendingPathComponent("pi-project-\(name)", isDirectory: true) + let sessionRoot = env.root.appendingPathComponent("pi-sessions-\(name)", isDirectory: true) + let settingsDirectory = project.appendingPathComponent(".pi", isDirectory: true) + try [sessionRoot, settingsDirectory].forEach { + try FileManager.default.createDirectory(at: $0, withIntermediateDirectories: true) + } + try Data(("{\"sessionDir\":\"" + sessionRoot.path + "\"}").utf8).write( + to: settingsDirectory.appendingPathComponent("settings.json"), + options: .atomic) + return project + } + func scope(_ workingDirectories: [URL]) -> String { + PiSessionCostScanner.scopeFingerprint(options: PiSessionCostScanner.Options( + cacheRoot: env.cacheRoot, + environment: ["HOME": env.root.path], + workingDirectories: workingDirectories)) + } + + let forward = scope([projects[0], projects[1]]) + #expect(forward.contains("pi-sessions-alpha")) + #expect(forward.contains("pi-sessions-beta")) + #expect(scope([projects[1], projects[0]]) == forward) + #expect(scope([projects[0], projects[2]]) != forward) + } }