diff --git a/supacode/Infrastructure/AgentDetection/AgentSessionResolver.swift b/supacode/Infrastructure/AgentDetection/AgentSessionResolver.swift index 1eafa4c9..05795007 100644 --- a/supacode/Infrastructure/AgentDetection/AgentSessionResolver.swift +++ b/supacode/Infrastructure/AgentDetection/AgentSessionResolver.swift @@ -456,10 +456,12 @@ nonisolated enum AgentSessionFingerprintMatcher { // whole tail instead of just the cut first line. // swiftlint:disable:next optional_data_string_conversion let fragments = transcriptStrings(String(decoding: data, as: UTF8.self)) - if !fragments.isEmpty { sessionScoreable = true } - let score = fragments.reduce(0) { best, fragment in - let normalized = normalize(fragment) - guard normalized.count >= 12 else { return best } + // Scoreable means the session produced at least one fragment long + // enough to actually enter the comparison — fragments below the floor + // ("OK") are no testimony at all. + let comparable = fragments.map(normalize).filter { $0.count >= 12 } + if !comparable.isEmpty { sessionScoreable = true } + let score = comparable.reduce(0) { best, normalized in if screen.contains(normalized) { return max(best, min(200, normalized.count + 80)) } let suffix = String(normalized.suffix(80)) return suffix.count >= 24 && screen.contains(suffix) ? max(best, suffix.count) : best diff --git a/supacodeTests/AgentSessionProfileTests.swift b/supacodeTests/AgentSessionProfileTests.swift index 45a06cd5..29ee4a81 100644 --- a/supacodeTests/AgentSessionProfileTests.swift +++ b/supacodeTests/AgentSessionProfileTests.swift @@ -236,6 +236,37 @@ struct AgentSessionProfileTests { #expect(match == nil) } + @Test func fingerprintRefusesWhenACompetitorHasOnlyShortFragments() throws { + let root = FileManager.default.temporaryDirectory + .appending(path: "prowl-short-\(UUID().uuidString)", directoryHint: .isDirectory) + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: root) } + // Session B parses cleanly but only yields fragments below the 12-char + // comparison floor ("OK"): it never actually testified, so it must block + // uniqueness exactly like an unreadable session. + let aURL = root.appending(path: "a.jsonl") + try #"{"message":{"content":"Shared prompt visible on both panes right now."}}"# + .write(to: aURL, atomically: true, encoding: .utf8) + let bURL = root.appending(path: "b.jsonl") + try #"{"message":{"content":"OK"}}"#.write(to: bURL, atomically: true, encoding: .utf8) + let candidates = [ + AgentSessionCandidate( + session: AgentSession(id: "session-a", transcriptPath: aURL, source: .recentFile), + modifiedAt: Date(timeIntervalSince1970: 2_000) + ), + AgentSessionCandidate( + session: AgentSession(id: "session-b", transcriptPath: bURL, source: .recentFile), + modifiedAt: Date(timeIntervalSince1970: 2_001) + ), + ] + + let match = AgentSessionFingerprintMatcher.bestMatch( + activeText: "Shared prompt visible on both panes right now.", + candidates: candidates + ) + #expect(match == nil) + } + @Test func fingerprintSurvivesTailCutInsideMultibyteCharacter() throws { let root = FileManager.default.temporaryDirectory .appending(path: "prowl-utf8-\(UUID().uuidString)", directoryHint: .isDirectory)