import Foundation final class FutureModificationDateClamp: @unchecked Sendable { private let lock = NSLock() private var clampDate: Date? init(clampDate: Date? = nil) { self.clampDate = clampDate } func clamp(url _: URL, modifiedAt: Date, now: Date) -> Date { guard modifiedAt > now else { return modifiedAt } return self.lock.withLock { if let clampDate = self.clampDate { return min(clampDate, now) } self.clampDate = now return now } } } public struct LocalAgentSessionScanner: Sendable { typealias ProcessOutputProvider = @Sendable ([String: String]) async -> String typealias CWDProvider = @Sendable ([Int32], [String: String]) async -> [Int32: String] private struct Rollout: Sendable { let url: URL let modifiedAt: Date let metadata: CodexRolloutMetadata } private struct ScanContext: Sendable { let homeDirectory: URL let host: String let now: Date let codexAppServerPresent: Bool let includeFileOnlySessions: Bool let threadMetadata: [String: CodexThreadMetadata] } public let config: SessionScanConfig private let futureModificationDateClamp = FutureModificationDateClamp() private let processOutputProvider: ProcessOutputProvider? private let cwdProvider: CWDProvider? private let didVisitDirectoryEntry: (@Sendable () -> Void)? public init(config: SessionScanConfig = SessionScanConfig()) { self.config = config self.processOutputProvider = nil self.cwdProvider = nil self.didVisitDirectoryEntry = nil } init( config: SessionScanConfig = SessionScanConfig(), processOutputProvider: @escaping ProcessOutputProvider, cwdProvider: @escaping CWDProvider, didVisitDirectoryEntry: (@Sendable () -> Void)? = nil) { self.config = config self.processOutputProvider = processOutputProvider self.cwdProvider = cwdProvider self.didVisitDirectoryEntry = didVisitDirectoryEntry } @concurrent public func scan( now: Date = Date(), environment: [String: String] = ProcessInfo.processInfo.environment, includeFileOnlySessions: Bool = true) async -> [AgentSession] { let processOutput = if let processOutputProvider = self.processOutputProvider { await processOutputProvider(environment) } else { await self.processOutput(environment: environment) } let allProcesses = AgentPSOutputParser.parse(processOutput) let processes = Array(AgentSessionCorrelation.newestProcessesFirst( AgentPSOutputParser.agentProcesses(from: allProcesses)) .prefix(max(0, self.config.maxProcessCount))) guard Self.shouldScanSessionMetadata( hasAgentProcesses: !processes.isEmpty, includeFileOnlySessions: includeFileOnlySessions) else { return [] } let codexAppServerPresent = AgentPSOutputParser.hasCodexAppServer(in: allProcesses) let cwdByPID = if let cwdProvider = self.cwdProvider { await cwdProvider(processes.map(\ .pid), environment) } else { await self.cwdByPID(processes.map(\ .pid), environment: environment) } let codexCWDs = processes.compactMap { process -> String? in guard AgentPSOutputParser.provider(for: process) == .codex else { return nil } return cwdByPID[process.pid] } let homeDirectory = URL(fileURLWithPath: environment["HOME"] ?? NSHomeDirectory(), isDirectory: true) let codexHomeDirectory = URL( fileURLWithPath: environment["CODEX_HOME"] ?? homeDirectory.appendingPathComponent(".codex").path, isDirectory: true) let host = ProcessInfo.processInfo.hostName var directoryBudget = DirectoryMetadataScanBudget( maxEntryCount: self.config.maxDirectoryEntryCount, maxDepth: self.config.maxDirectoryDepth, timeLimit: includeFileOnlySessions ? self.config.directoryScanBudget : min(self.config.directoryScanBudget, self.config.adaptiveDirectoryScanBudget), didVisitEntry: self.didVisitDirectoryEntry) let rollouts: [Rollout] = if includeFileOnlySessions || !codexCWDs.isEmpty { self.codexRollouts( now: now, codexHomeDirectory: codexHomeDirectory, matchingCWDs: includeFileOnlySessions ? nil : codexCWDs, directoryBudget: &directoryBudget) } else { [] } let threadMetadata = Self.codexThreadMetadata( rollouts: rollouts, codexHomeDirectory: codexHomeDirectory, environment: environment) return self.sessions( processes: processes, cwdByPID: cwdByPID, rollouts: rollouts, context: ScanContext( homeDirectory: homeDirectory, host: host, now: now, codexAppServerPresent: codexAppServerPresent, includeFileOnlySessions: includeFileOnlySessions, threadMetadata: threadMetadata), directoryBudget: &directoryBudget) } public static func shouldScanSessionMetadata( hasAgentProcesses: Bool, includeFileOnlySessions: Bool) -> Bool { hasAgentProcesses || includeFileOnlySessions } private static func codexThreadMetadata( rollouts: [Rollout], codexHomeDirectory: URL, environment: [String: String]) -> [String: CodexThreadMetadata] { let sessionIDs = Set(rollouts.map(\.metadata.sessionID)) let indexedNames = CodexThreadMetadataReader.indexedThreadNames( codexHomeDirectory: codexHomeDirectory, sessionIDs: sessionIDs) var groups: [String: (reader: CodexThreadMetadataReader, sessionIDs: Set)] = [:] for rollout in rollouts { let resolvedWorkingDirectory = rollout.metadata.cwd.map { URL(fileURLWithPath: $0, isDirectory: true).standardizedFileURL } let reader = CodexThreadMetadataReader( codexHomeDirectory: codexHomeDirectory, environment: environment, resolvedWorkingDirectory: resolvedWorkingDirectory) let key = reader.databaseURL.path if var group = groups[key] { group.sessionIDs.insert(rollout.metadata.sessionID) groups[key] = group } else { groups[key] = (reader, [rollout.metadata.sessionID]) } } var metadata: [String: CodexThreadMetadata] = [:] for group in groups.values { metadata.merge(group.reader.metadata(for: group.sessionIDs, indexedNames: indexedNames)) { _, latest in latest } } return metadata } private func sessions( processes: [AgentProcessRecord], cwdByPID: [Int32: String], rollouts: [Rollout], context: ScanContext, directoryBudget: inout DirectoryMetadataScanBudget) -> [AgentSession] { var sessions: [AgentSession] = [] var matchedRolloutPaths = Set() let claudeProcesses = processes.filter { AgentPSOutputParser.provider(for: $0) == .claude } let claudeCWDs = Set(claudeProcesses.compactMap { cwdByPID[$0.pid] }) var claudeTranscriptsByCWD: [String: [ClaudeSessionProjectMapper.Transcript]] = [:] for cwd in claudeCWDs { claudeTranscriptsByCWD[cwd] = ClaudeSessionProjectMapper.transcripts( cwd: cwd, homeDirectory: context.homeDirectory, limit: self.config.maxClaudeTranscriptCountPerProject, now: context.now, budget: &directoryBudget, clampModificationDate: self.futureModificationDateClamp.clamp) } let claudeTranscripts = AgentSessionCorrelation.assignClaudeTranscripts( processes: claudeProcesses, cwdByPID: cwdByPID, transcriptsByCWD: claudeTranscriptsByCWD) let codexProcesses = processes.filter { AgentPSOutputParser.provider(for: $0) == .codex } let codexDescriptiveNamePIDs = AgentSessionCorrelation.unambiguousProcessIDs( processes: codexProcesses, cwdByPID: cwdByPID) for process in processes { guard let provider = AgentPSOutputParser.provider(for: process) else { continue } let cwd = cwdByPID[process.pid] switch provider { case .claude: let transcript = claudeTranscripts[process.pid] sessions.append(AgentSession( id: transcript?.url.deletingPathExtension().lastPathComponent ?? "pid:\(process.pid)", provider: .claude, source: AgentPSOutputParser.source(for: process), state: self.config.state( lastActivityAt: transcript?.modifiedAt, now: context.now, hasLiveProcess: true), pid: process.pid, cwd: cwd, projectName: Self.projectName(cwd), startedAt: process.startedAt, lastActivityAt: transcript?.modifiedAt, transcriptPath: transcript?.url.path, host: context.host)) case .codex: let rollout = rollouts.first { candidate in !matchedRolloutPaths.contains(candidate.url.path) && AgentSessionCorrelation.codexWorkingDirectoriesMatch(candidate.metadata.cwd, cwd) } if let rollout { matchedRolloutPaths.insert(rollout.url.path) } let rolloutSource = rollout?.metadata.sessionSource sessions.append(AgentSession( id: rollout?.metadata.sessionID ?? "pid:\(process.pid)", provider: .codex, source: rolloutSource == nil || rolloutSource == .unknown ? .cli : rolloutSource ?? .cli, state: self.config.state( lastActivityAt: rollout?.modifiedAt, now: context.now, hasLiveProcess: true), pid: process.pid, cwd: cwd ?? rollout?.metadata.cwd, projectName: Self.projectName(cwd ?? rollout?.metadata.cwd), sessionName: codexDescriptiveNamePIDs.contains(process.pid) ? rollout?.metadata.descriptiveName( threadMetadata: rollout.flatMap { context.threadMetadata[$0.metadata.sessionID] }) : nil, startedAt: process.startedAt, lastActivityAt: rollout?.modifiedAt, transcriptPath: rollout?.url.path, host: context.host)) } } for rollout in rollouts where context.includeFileOnlySessions && !matchedRolloutPaths.contains(rollout.url.path) { guard var session = CodexRolloutFirstLineParser.makeSession( metadata: rollout.metadata, transcriptURL: rollout.url, modifiedAt: rollout.modifiedAt, host: context.host, config: self.config, now: context.now) else { continue } session.sessionName = rollout.metadata.descriptiveName( threadMetadata: context.threadMetadata[rollout.metadata.sessionID]) session.source = AgentSessionCorrelation.fileOnlyCodexSource( metadataSource: session.source, appServerPresent: context.codexAppServerPresent) sessions.append(session) } var seen = Set() return sessions .sorted { lhs, rhs in if lhs.state != rhs.state { return lhs.state == .active } return (lhs.lastActivityAt ?? lhs.startedAt ?? .distantPast) > (rhs.lastActivityAt ?? rhs.startedAt ?? .distantPast) } .filter { seen.insert("\($0.host):\($0.id)").inserted } } private func processOutput(environment: [String: String]) async -> String { let binary = ["/bin/ps", "/usr/bin/ps"].first { FileManager.default.isExecutableFile(atPath: $0) } guard let binary, let result = try? await SubprocessRunner.run( binary: binary, arguments: ["-axo", "pid=,ppid=,lstart=,command="], environment: environment, timeout: 5, label: "agent session process scan") else { return "" } return result.stdout } private func cwdByPID(_ pids: [Int32], environment: [String: String]) async -> [Int32: String] { guard !pids.isEmpty else { return [:] } if let lsof = self.findExecutable("lsof", environment: environment) { let joinedPIDs = pids.map(String.init).joined(separator: ",") if let result = try? await SubprocessRunner.run( binary: lsof, arguments: ["-a", "-d", "cwd", "-Fn", "-p", joinedPIDs], environment: environment, timeout: 5, acceptsNonZeroExit: true, label: "agent session cwd scan") { return LSOFCWDOutputParser.parse(result.stdout) } } #if os(Linux) return Dictionary(uniqueKeysWithValues: pids.compactMap { pid in let path = "/proc/\(pid)/cwd" guard let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: path) else { return nil } return (pid, destination) }) #else return [:] #endif } private func codexRollouts( now: Date, codexHomeDirectory: URL, matchingCWDs: [String]?, directoryBudget: inout DirectoryMetadataScanBudget) -> [Rollout] { let root = codexHomeDirectory.appendingPathComponent("sessions", isDirectory: true) let calendar = Calendar(identifier: .gregorian) let days = [now, calendar.date(byAdding: .day, value: -1, to: now)].compactMap(\.self) let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "yyyy/MM/dd" let fileManager = FileManager.default let candidates = days.flatMap { day -> [(url: URL, modifiedAt: Date)] in let directory = root.appendingPathComponent(formatter.string(from: day), isDirectory: true) return directoryBudget.files(in: directory, fileManager: fileManager).compactMap { file in guard file.lastPathComponent.hasPrefix("rollout-"), file.pathExtension == "jsonl", let modifiedAt = try? file.resourceValues( forKeys: [.contentModificationDateKey]).contentModificationDate else { return nil } return (file, self.futureModificationDateClamp.clamp( url: file, modifiedAt: modifiedAt, now: now)) } }.sorted { $0.modifiedAt > $1.modifiedAt } var remainingCWDs = matchingCWDs ?? [] var rollouts: [Rollout] = [] for candidate in candidates.prefix(max(0, self.config.maxCodexRolloutCount)) { guard directoryBudget.hasTimeRemaining() else { break } guard let metadata = CodexRolloutFirstLineParser.read(from: candidate.url) else { continue } rollouts.append(Rollout(url: candidate.url, modifiedAt: candidate.modifiedAt, metadata: metadata)) if let index = remainingCWDs.firstIndex(where: { AgentSessionCorrelation.codexWorkingDirectoriesMatch(metadata.cwd, $0) }) { remainingCWDs.remove(at: index) if matchingCWDs != nil, remainingCWDs.isEmpty { break } } } return rollouts } private func findExecutable(_ name: String, environment: [String: String]) -> String? { let path = environment["PATH"] ?? "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin" return path.split(separator: ":") .map { String($0) + "/" + name } .first { FileManager.default.isExecutableFile(atPath: $0) } } private static func standardized(_ path: String?) -> String? { path.map { URL(fileURLWithPath: $0).standardizedFileURL.path } } private static func projectName(_ cwd: String?) -> String? { guard let cwd, !cwd.isEmpty else { return nil } return URL(fileURLWithPath: cwd).lastPathComponent } }