diff --git a/supacode/Infrastructure/AgentDetection/AgentPidArtifacts.swift b/supacode/Infrastructure/AgentDetection/AgentPidArtifacts.swift index 2e8b385c..0355efa7 100644 --- a/supacode/Infrastructure/AgentDetection/AgentPidArtifacts.swift +++ b/supacode/Infrastructure/AgentDetection/AgentPidArtifacts.swift @@ -145,10 +145,13 @@ nonisolated enum GrokActiveSessions { let sessionsRoot = home.appending(path: ".grok/sessions") let candidates: [URL] if let cwd { + // Prefer chat_history.jsonl: it is the conversation transcript + // (messages incl. system prompt); events.jsonl only logs + // MCP/infrastructure events. let encoded = percentEncodedPath(cwd) candidates = [ - sessionsRoot.appending(path: "\(encoded)/\(sessionID)/events.jsonl"), sessionsRoot.appending(path: "\(encoded)/\(sessionID)/chat_history.jsonl"), + sessionsRoot.appending(path: "\(encoded)/\(sessionID)/events.jsonl"), ] } else { candidates = [] @@ -159,10 +162,10 @@ nonisolated enum GrokActiveSessions { // Fall back to a shallow scan when cwd is missing or encoding diverged. let projectDirs = (try? fileManager.contentsOfDirectory(at: sessionsRoot, includingPropertiesForKeys: nil)) ?? [] for project in projectDirs { - let events = project.appending(path: "\(sessionID)/events.jsonl") - if fileManager.fileExists(atPath: events.path) { return events } let chat = project.appending(path: "\(sessionID)/chat_history.jsonl") if fileManager.fileExists(atPath: chat.path) { return chat } + let events = project.appending(path: "\(sessionID)/events.jsonl") + if fileManager.fileExists(atPath: events.path) { return events } } return nil } diff --git a/supacode/Infrastructure/AgentDetection/AgentSessionProfile.swift b/supacode/Infrastructure/AgentDetection/AgentSessionProfile.swift index 78592e8f..104d0211 100644 --- a/supacode/Infrastructure/AgentDetection/AgentSessionProfile.swift +++ b/supacode/Infrastructure/AgentDetection/AgentSessionProfile.swift @@ -269,11 +269,12 @@ nonisolated extension AgentSessionProfile { let components = url.pathComponents // Anchor on a `.grok` component whose next component is `sessions` // (not the first bare `sessions`, and not an earlier unrelated `.grok`). - guard let grokIndex = components.indices.first(where: { index in - components[index] == ".grok" - && components.indices.contains(index + 1) - && components[index + 1] == "sessions" - }), + guard + let grokIndex = components.indices.first(where: { index in + components[index] == ".grok" + && components.indices.contains(index + 1) + && components[index + 1] == "sessions" + }), components.count > grokIndex + 3 else { return nil } // Layout: …/.grok/sessions///… @@ -281,18 +282,18 @@ nonisolated extension AgentSessionProfile { guard uuid(in: id) != nil else { return nil } // Walk up from the open path to the session root // (…/sessions///). Nested paths like `terminal/.log` - // still resolve; transcript prefers the opened file when it is a - // known session transcript, otherwise events.jsonl under the root. + // still resolve. The transcript is always canonicalized to + // chat_history.jsonl (the conversation log): the process also holds + // events.jsonl open, which only logs MCP/infrastructure events. var dir = url.deletingLastPathComponent() while dir.lastPathComponent != id, dir.pathComponents.count > grokIndex + 3 { dir = dir.deletingLastPathComponent() } guard dir.lastPathComponent == id else { return nil } - let name = url.lastPathComponent let transcript: URL = - (name == "events.jsonl" || name == "chat_history.jsonl") + url.lastPathComponent == "chat_history.jsonl" ? url - : dir.appending(path: "events.jsonl") + : dir.appending(path: "chat_history.jsonl") return AgentSession(id: id, transcriptPath: transcript, source: .recentFile) }, candidateRoots: { home, cwd, _, _ in diff --git a/supacodeTests/AgentSessionProfileTests.swift b/supacodeTests/AgentSessionProfileTests.swift index 3caee428..76dca0b6 100644 --- a/supacodeTests/AgentSessionProfileTests.swift +++ b/supacodeTests/AgentSessionProfileTests.swift @@ -456,11 +456,24 @@ struct AgentSessionProfileTests { #expect(session?.id == id) #expect(session?.source == .processLog) #expect(session?.confidence == .exact) + // Only events.jsonl exists — the transcript falls back to it. #expect( session?.transcriptPath?.resolvingSymlinksInPath() == sessionDir.appending(path: "events.jsonl").resolvingSymlinksInPath() ) #expect(GrokActiveSessions.session(home: root, pid: 9999, processStartedAt: processStartedAt) == nil) + + // chat_history.jsonl is the conversation log; prefer it once present. + try "{}".write(to: sessionDir.appending(path: "chat_history.jsonl"), atomically: true, encoding: .utf8) + let refreshed = GrokActiveSessions.session( + home: root, + pid: 4242, + processStartedAt: processStartedAt + ) + #expect( + refreshed?.transcriptPath?.resolvingSymlinksInPath() + == sessionDir.appending(path: "chat_history.jsonl").resolvingSymlinksInPath() + ) } @Test func grokActiveSessionsRejectsStaleClaimsFromReusedPids() throws { @@ -501,13 +514,13 @@ struct AgentSessionProfileTests { let processStartedAt = Date(timeIntervalSince1970: 1_783_800_000) try """ - [{"session_id":"11111111-2222-3333-4444-555555555555","pid":4242,"cwd":"/Users/me/App"}] - """.write(to: root.appending(path: ".grok/active_sessions.json"), atomically: true, encoding: .utf8) + [{"session_id":"11111111-2222-3333-4444-555555555555","pid":4242,"cwd":"/Users/me/App"}] + """.write(to: root.appending(path: ".grok/active_sessions.json"), atomically: true, encoding: .utf8) #expect(GrokActiveSessions.session(home: root, pid: 4242, processStartedAt: processStartedAt) == nil) try """ - [{"session_id":"11111111-2222-3333-4444-555555555555","pid":4242,"cwd":"/Users/me/App","opened_at":"not-a-date"}] - """.write(to: root.appending(path: ".grok/active_sessions.json"), atomically: true, encoding: .utf8) + [{"session_id":"11111111-2222-3333-4444-555555555555","pid":4242,"cwd":"/Users/me/App","opened_at":"not-a-date"}] + """.write(to: root.appending(path: ".grok/active_sessions.json"), atomically: true, encoding: .utf8) #expect(GrokActiveSessions.session(home: root, pid: 4242, processStartedAt: processStartedAt) == nil) } @@ -534,6 +547,8 @@ struct AgentSessionProfileTests { let parsedEvents = profile.parsePath(sessionDir.appending(path: "events.jsonl").path) #expect(parsedEvents?.id == id) + // Every non-chat open file canonicalizes to the conversation log. + #expect(parsedEvents?.transcriptPath?.lastPathComponent == "chat_history.jsonl") try? FileManager.default.createDirectory( at: sessionDir.appending(path: "terminal"), @@ -543,6 +558,9 @@ struct AgentSessionProfileTests { FileManager.default.createFile(atPath: nestedPath, contents: Data()) let parsedNested = profile.parsePath(nestedPath) #expect(parsedNested?.id == id) + // Non-transcript files resolve to the conversation log at the root. + #expect(parsedNested?.transcriptPath?.lastPathComponent == "chat_history.jsonl") + #expect(parsedNested?.transcriptPath?.deletingLastPathComponent().lastPathComponent == id) #expect(profile.parsePath("/Users/me/.grok/sessions/%2FUsers%2Fme%2FApp/not-a-uuid/events.jsonl") == nil) } diff --git a/supacodeTests/AgentSessionResolverTests.swift b/supacodeTests/AgentSessionResolverTests.swift index 04a6a857..5e6e05c4 100644 --- a/supacodeTests/AgentSessionResolverTests.swift +++ b/supacodeTests/AgentSessionResolverTests.swift @@ -61,7 +61,7 @@ struct AgentSessionResolverTests { ( .grok, ( - "/Users/me/.grok/sessions/%2FUsers%2Fme%2FApp/019f5e7e-4269-7e33-9eaf-d535ff8ebafb/events.jsonl", + "/Users/me/.grok/sessions/%2FUsers%2Fme%2FApp/019f5e7e-4269-7e33-9eaf-d535ff8ebafb/chat_history.jsonl", "019f5e7e-4269-7e33-9eaf-d535ff8ebafb" ) ), @@ -69,7 +69,8 @@ struct AgentSessionResolverTests { .grok, ( // Earlier `sessions` path component must not be used as the marker. - "/Volumes/sessions/home/.grok/sessions/%2FUsers%2Fme%2FApp/019f5e7e-4269-7e33-9eaf-d535ff8ebafb/events.jsonl", + "/Volumes/sessions/home/.grok/sessions/%2FUsers%2Fme%2FApp/019f5e7e-4269-7e33-9eaf-d535ff8ebafb/" + + "chat_history.jsonl", "019f5e7e-4269-7e33-9eaf-d535ff8ebafb" ) ),