From 9d3e87af868c38bcf23899c5b4ecc772745fde9c Mon Sep 17 00:00:00 2001 From: onevcat Date: Sat, 11 Jul 2026 19:25:47 +0900 Subject: [PATCH] Align the scoreable threshold with the comparison floor Fifth review round: sessionScoreable flipped on any non-empty fragment list while scoring only considers fragments whose normalized length reaches 12 characters. A competing session whose transcript held only short fragments ("OK") therefore counted as having testified without ever entering the comparison, letting another session win high on shared text. Scoreable now requires at least one fragment above the comparison floor, using the same normalized values for scoring. --- .../AgentDetection/AgentSessionResolver.swift | 10 +++--- supacodeTests/AgentSessionProfileTests.swift | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) 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) -- 2.51.2