import Foundation public struct AgentSession: Codable, Equatable, Sendable, Identifiable { public enum Provider: String, Codable, Sendable { case codex case claude } public enum Source: String, Codable, Sendable { case cli case desktopApp case ide case unknown } public enum State: String, Codable, Sendable { case active case idle } public var id: String public var provider: Provider public var source: Source public var state: State public var pid: Int32? public var cwd: String? public var projectName: String? public var startedAt: Date? public var lastActivityAt: Date? public var transcriptPath: String? public var host: String public init( id: String, provider: Provider, source: Source, state: State, pid: Int32?, cwd: String?, projectName: String?, startedAt: Date?, lastActivityAt: Date?, transcriptPath: String?, host: String) { self.id = id self.provider = provider self.source = source self.state = state self.pid = pid self.cwd = cwd self.projectName = projectName self.startedAt = startedAt self.lastActivityAt = lastActivityAt self.transcriptPath = transcriptPath self.host = host }} public struct SessionScanConfig: Equatable, Sendable { public var activeWindow: TimeInterval public var fileOnlyWindow: TimeInterval public init(activeWindow: TimeInterval = 120, fileOnlyWindow: TimeInterval = 30 * 60) { self.activeWindow = activeWindow self.fileOnlyWindow = fileOnlyWindow } public func state(lastActivityAt: Date?, now: Date, hasLiveProcess: Bool) -> AgentSession.State { guard let lastActivityAt else { return hasLiveProcess ? .active : .idle } return now.timeIntervalSince(lastActivityAt) <= self.activeWindow ? .active : .idle }} public struct AgentProcessRecord: Equatable, Sendable { public let pid: Int32 public let ppid: Int32 public let startedAt: Date? public let command: String public init(pid: Int32, ppid: Int32, startedAt: Date?, command: String) { self.pid = pid self.ppid = ppid self.startedAt = startedAt self.command = command } public var executableBasename: String { let firstToken = self.command.split(whereSeparator: \ .isWhitespace).first.map(String.init) ?? "" let firstBasename = URL(fileURLWithPath: firstToken).lastPathComponent if firstBasename == "disclaimer" { return firstBasename } if self.command.contains("Application Support/Claude/claude-code/claude") { return "claude" } return firstBasename }} public enum AgentPSOutputParser { public static func parse(_ output: String) -> [AgentProcessRecord] { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "EEE MMM d HH:mm:ss yyyy" return output.split(whereSeparator: \ .isNewline).compactMap { rawLine -> AgentProcessRecord? in let fields = rawLine.split(maxSplits: 7, omittingEmptySubsequences: true, whereSeparator: \ .isWhitespace) guard fields.count == 8, let pid = Int32(fields[0]), let ppid = Int32(fields[1]) else { return nil } let dateText = fields[2...6].joined(separator: " ") return AgentProcessRecord( pid: pid, ppid: ppid, startedAt: formatter.date(from: dateText), command: String(fields[7])) } } public static func agentProcesses(from records: [AgentProcessRecord]) -> [AgentProcessRecord] { let candidates = records.filter { record in let basename = record.executableBasename.lowercased() if basename == "codex" { let arguments = self.arguments(record.command) return self.isCodexAgentExecutable(record.command) && !arguments.contains("app-server") && !arguments.contains("--help") && !arguments.contains("--version") } if basename == "claude" { return self.isClaudeAgentExecutable(record.command) && !self.isObviousClaudeHelper(record.command) } return basename == "disclaimer" && record.command.contains("claude") } let recordsByPID = Dictionary(uniqueKeysWithValues: candidates.map { ($0.pid, $0) }) return candidates.filter { record in guard record.executableBasename.lowercased() == "disclaimer" else { return true } return !candidates.contains { child in child.ppid == record.pid && child.executableBasename.lowercased() == "claude" && self.normalizedClaudeArguments(child.command) == self.normalizedClaudeArguments(record.command) } && recordsByPID[record.ppid] == nil } } public static func provider(for record: AgentProcessRecord) -> AgentSession.Provider? { let basename = record.executableBasename.lowercased() if basename == "codex" { return .codex } if basename == "claude" || basename == "disclaimer" { return .claude } return nil } public static func source(for record: AgentProcessRecord) -> AgentSession.Source { guard self.provider(for: record) == .claude else { return .cli } return record.command.contains("Application Support/Claude/claude-code") ? .desktopApp : .cli } public static func hasCodexAppServer(in records: [AgentProcessRecord]) -> Bool { records.contains { record in record.executableBasename.lowercased() == "codex" && self.isCodexAgentExecutable(record.command) && self.arguments(record.command).contains("app-server") } } private static func arguments(_ command: String) -> [String] { command.split(whereSeparator: \ .isWhitespace).dropFirst().map(String.init) } private static func normalizedClaudeArguments(_ command: String) -> [String] { let arguments = self.arguments(command) if let index = arguments.firstIndex(where: { URL(fileURLWithPath: $0).lastPathComponent == "claude" }) { return Array(arguments.suffix(from: arguments.index(after: index))) } return arguments } private static func isObviousClaudeHelper(_ command: String) -> Bool { let lowercased = command.lowercased() return lowercased.contains("--version") || lowercased.contains("--help") || lowercased.contains("claude-code-acp") } private static func isCodexAgentExecutable(_ command: String) -> Bool { let lowercased = command.lowercased() guard lowercased.contains(".app/") else { return true } return lowercased.hasPrefix("/applications/codex.app/contents/resources/codex ") || lowercased.hasPrefix("/applications/codex.app/contents/resources/codex\t") } private static func isClaudeAgentExecutable(_ command: String) -> Bool { let lowercased = command.lowercased() return !lowercased.contains(".app/") || lowercased.contains("application support/claude/claude-code/claude") }} public enum LSOFCWDOutputParser { public static func parse(_ output: String) -> [Int32: String] { var result: [Int32: String] = [:] var currentPID: Int32? for line in output.split(whereSeparator: \ .isNewline).map(String.init) { guard let prefix = line.first else { continue } switch prefix { case "p": currentPID = Int32(line.dropFirst()) case "n": if let currentPID { result[currentPID] = String(line.dropFirst()) } default: continue } } return result }} public enum ClaudeSessionProjectMapper { public struct Transcript: Equatable, Sendable { public let url: URL public let modifiedAt: Date public init(url: URL, modifiedAt: Date) { self.url = url self.modifiedAt = modifiedAt } } public static func escapedCWD(_ cwd: String) -> String { String(cwd.unicodeScalars.map { scalar in let value = scalar.value let isASCIIAlphanumeric = (48...57).contains(value) || (65...90).contains(value) || (97...122).contains(value) return isASCIIAlphanumeric ? Character(scalar) : "-" }) } public static func projectDirectories( cwd: String, homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser, fileManager: FileManager = .default) -> [URL] { let standardRoot = homeDirectory .appendingPathComponent(".claude", isDirectory: true) .appendingPathComponent("projects", isDirectory: true) return ([standardRoot] + ClaudeDesktopProjectsLocator.roots( homeDirectory: homeDirectory, fileManager: fileManager)) .map { $0.appendingPathComponent(self.escapedCWD(cwd), isDirectory: true) } } public static func newestTranscript( cwd: String, homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser, fileManager: FileManager = .default) -> (url: URL, modifiedAt: Date)? { self.transcripts(cwd: cwd, homeDirectory: homeDirectory, fileManager: fileManager).first .map { (url: $0.url, modifiedAt: $0.modifiedAt) } } public static func transcripts( cwd: String, homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser, fileManager: FileManager = .default) -> [Transcript] { self.projectDirectories(cwd: cwd, homeDirectory: homeDirectory, fileManager: fileManager) .flatMap { directory -> [(URL, Date)] in guard let files = try? fileManager.contentsOfDirectory( at: directory, includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles]) else { return [] } return files.compactMap { file in guard file.pathExtension == "jsonl", let modifiedAt = try? file.resourceValues( forKeys: [.contentModificationDateKey]).contentModificationDate else { return nil } return (file, modifiedAt) } } .sorted { $0.1 > $1.1 } .map { Transcript(url: $0.0, modifiedAt: $0.1) } }} public enum AgentSessionCorrelation { public static func newestProcessesFirst(_ processes: [AgentProcessRecord]) -> [AgentProcessRecord] { processes.sorted { lhs, rhs in let lhsDate = lhs.startedAt ?? .distantPast let rhsDate = rhs.startedAt ?? .distantPast if lhsDate != rhsDate { return lhsDate > rhsDate } return lhs.pid > rhs.pid } } public static func assignClaudeTranscripts( processes: [AgentProcessRecord], cwdByPID: [Int32: String], transcriptsByCWD: [String: [ClaudeSessionProjectMapper.Transcript]]) -> [Int32: ClaudeSessionProjectMapper.Transcript] { var assigned: [Int32: ClaudeSessionProjectMapper.Transcript] = [:] var usedPaths = Set<String>() let unambiguousPIDs = self.unambiguousProcessIDs(processes: processes, cwdByPID: cwdByPID) for process in self.newestProcessesFirst(processes) { guard unambiguousPIDs.contains(process.pid), let cwd = cwdByPID[process.pid], let candidates = transcriptsByCWD[cwd] else { continue } let candidate = candidates.first { transcript in guard !usedPaths.contains(transcript.url.path) else { return false } guard let startedAt = process.startedAt else { return true } return transcript.modifiedAt >= startedAt } if let candidate { assigned[process.pid] = candidate usedPaths.insert(candidate.url.path) } } return assigned } public static func unambiguousProcessIDs( processes: [AgentProcessRecord], cwdByPID: [Int32: String]) -> Set<Int32> { let grouped = Dictionary(grouping: processes.compactMap { process -> (Int32, String)? in guard let cwd = cwdByPID[process.pid] else { return nil } return (process.pid, URL(fileURLWithPath: cwd).standardizedFileURL.path) }, by: { $0.1 }) return Set(grouped.values.compactMap { records in records.count == 1 ? records[0].0 : nil }) } public static func codexWorkingDirectoriesMatch(_ lhs: String?, _ rhs: String?) -> Bool { guard let lhs, let rhs else { return false } return URL(fileURLWithPath: lhs).standardizedFileURL.path == URL(fileURLWithPath: rhs).standardizedFileURL.path } public static func fileOnlyCodexSource( metadataSource: AgentSession.Source, appServerPresent: Bool) -> AgentSession.Source { metadataSource == .unknown && appServerPresent ? .desktopApp : metadataSource }} public struct CodexRolloutMetadata: Equatable, Sendable { public let sessionID: String public let cwd: String? public let originator: String? public let source: String? public init(sessionID: String, cwd: String?, originator: String?, source: String?) { self.sessionID = sessionID self.cwd = cwd self.originator = originator self.source = source } public var sessionSource: AgentSession.Source { let value = [self.originator, self.source] .compactMap { $0?.lowercased() } .joined(separator: " ") if value.contains("desktop") || value.contains("app-server") { return .desktopApp } if value.contains("ide") || value.contains("vscode") || value.contains("cursor") || value.contains("zed") { return .ide } if value.contains("codex_exec") || value.contains("exec") || value.contains("cli") { return .cli } return .unknown }} public enum CodexRolloutFirstLineParser { public static func parse(_ line: String) -> CodexRolloutMetadata? { guard let data = line.data(using: .utf8), let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], object["type"] as? String == "session_meta", let payload = object["payload"] as? [String: Any], let sessionID = (payload["session_id"] as? String) ?? (payload["id"] as? String) else { return nil } return CodexRolloutMetadata( sessionID: sessionID, cwd: payload["cwd"] as? String, originator: payload["originator"] as? String, source: payload["source"] as? String) } public static func read(from url: URL) -> CodexRolloutMetadata? { guard let handle = try? FileHandle(forReadingFrom: url) else { return nil } defer { try? handle.close() } var data = Data() while data.count < 256 * 1024 { guard let chunk = try? handle.read(upToCount: 4096), !chunk.isEmpty else { break } if let newline = chunk.firstIndex(of: 0x0A) { data.append(chunk.prefix(upTo: newline)) break } data.append(chunk) } guard let line = String(data: data, encoding: .utf8) else { return nil } return self.parse(line) } public static func makeSession( metadata: CodexRolloutMetadata, transcriptURL: URL, modifiedAt: Date, pid: Int32? = nil, startedAt: Date? = nil, host: String, config: SessionScanConfig = SessionScanConfig(), now: Date = Date()) -> AgentSession? { guard pid != nil || now.timeIntervalSince(modifiedAt) <= config.fileOnlyWindow else { return nil } let cwd = metadata.cwd return AgentSession( id: metadata.sessionID, provider: .codex, source: metadata.sessionSource, state: config.state(lastActivityAt: modifiedAt, now: now, hasLiveProcess: pid != nil), pid: pid, cwd: cwd, projectName: cwd.map { URL(fileURLWithPath: $0).lastPathComponent }, startedAt: startedAt, lastActivityAt: modifiedAt, transcriptPath: transcriptURL.path, host: host) }}
import Foundation
public struct AgentSession: Codable, Equatable, Sendable, Identifiable {
public enum Provider: String, Codable, Sendable {
case codex
case claude
}
public enum Source: String, Codable, Sendable {
case cli
case desktopApp
case ide
case unknown
}
public enum State: String, Codable, Sendable {
case active
case idle
}
public var id: String
public var provider: Provider
public var source: Source
public var state: State
public var pid: Int32?
public var cwd: String?
public var projectName: String?
public var startedAt: Date?
public var lastActivityAt: Date?
public var transcriptPath: String?
public var host: String
public init(
id: String,
provider: Provider,
source: Source,
state: State,
pid: Int32?,
cwd: String?,
projectName: String?,
startedAt: Date?,
lastActivityAt: Date?,
transcriptPath: String?,
host: String)
{
self.id = id
self.provider = provider
self.source = source
self.state = state
self.pid = pid
self.cwd = cwd
self.projectName = projectName
self.startedAt = startedAt
self.lastActivityAt = lastActivityAt
self.transcriptPath = transcriptPath
self.host = host
}
}
public struct SessionScanConfig: Equatable, Sendable {
public var activeWindow: TimeInterval
public var fileOnlyWindow: TimeInterval
public init(activeWindow: TimeInterval = 120, fileOnlyWindow: TimeInterval = 30 * 60) {
self.activeWindow = activeWindow
self.fileOnlyWindow = fileOnlyWindow
}
public func state(lastActivityAt: Date?, now: Date, hasLiveProcess: Bool) -> AgentSession.State {
guard let lastActivityAt else { return hasLiveProcess ? .active : .idle }
return now.timeIntervalSince(lastActivityAt) <= self.activeWindow ? .active : .idle
}
}
public struct AgentProcessRecord: Equatable, Sendable {
public let pid: Int32
public let ppid: Int32
public let startedAt: Date?
public let command: String
public init(pid: Int32, ppid: Int32, startedAt: Date?, command: String) {
self.pid = pid
self.ppid = ppid
self.startedAt = startedAt
self.command = command
}
public var executableBasename: String {
let firstToken = self.command.split(whereSeparator: \ .isWhitespace).first.map(String.init) ?? ""
let firstBasename = URL(fileURLWithPath: firstToken).lastPathComponent
if firstBasename == "disclaimer" {
return firstBasename
}
if self.command.contains("Application Support/Claude/claude-code/claude") {
return "claude"
}
return firstBasename
}
}
public enum AgentPSOutputParser {
public static func parse(_ output: String) -> [AgentProcessRecord] {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "EEE MMM d HH:mm:ss yyyy"
return output.split(whereSeparator: \ .isNewline).compactMap { rawLine -> AgentProcessRecord? in
let fields = rawLine.split(maxSplits: 7, omittingEmptySubsequences: true, whereSeparator: \ .isWhitespace)
guard fields.count == 8,
let pid = Int32(fields[0]),
let ppid = Int32(fields[1])
else { return nil }
let dateText = fields[2...6].joined(separator: " ")
return AgentProcessRecord(
pid: pid,
ppid: ppid,
startedAt: formatter.date(from: dateText),
command: String(fields[7]))
}
}
public static func agentProcesses(from records: [AgentProcessRecord]) -> [AgentProcessRecord] {
let candidates = records.filter { record in
let basename = record.executableBasename.lowercased()
if basename == "codex" {
let arguments = self.arguments(record.command)
return self.isCodexAgentExecutable(record.command) &&
!arguments.contains("app-server") &&
!arguments.contains("--help") &&
!arguments.contains("--version")
}
if basename == "claude" {
return self.isClaudeAgentExecutable(record.command) && !self.isObviousClaudeHelper(record.command)
}
return basename == "disclaimer" && record.command.contains("claude")
}
let recordsByPID = Dictionary(uniqueKeysWithValues: candidates.map { ($0.pid, $0) })
return candidates.filter { record in
guard record.executableBasename.lowercased() == "disclaimer" else { return true }
return !candidates.contains { child in
child.ppid == record.pid &&
child.executableBasename.lowercased() == "claude" &&
self.normalizedClaudeArguments(child.command) == self.normalizedClaudeArguments(record.command)
} && recordsByPID[record.ppid] == nil
}
}
public static func provider(for record: AgentProcessRecord) -> AgentSession.Provider? {
let basename = record.executableBasename.lowercased()
if basename == "codex" {
return .codex
}
if basename == "claude" || basename == "disclaimer" {
return .claude
}
return nil
}
public static func source(for record: AgentProcessRecord) -> AgentSession.Source {
guard self.provider(for: record) == .claude else { return .cli }
return record.command.contains("Application Support/Claude/claude-code") ? .desktopApp : .cli
}
public static func hasCodexAppServer(in records: [AgentProcessRecord]) -> Bool {
records.contains { record in
record.executableBasename.lowercased() == "codex" &&
self.isCodexAgentExecutable(record.command) &&
self.arguments(record.command).contains("app-server")
}
}
private static func arguments(_ command: String) -> [String] {
command.split(whereSeparator: \ .isWhitespace).dropFirst().map(String.init)
}
private static func normalizedClaudeArguments(_ command: String) -> [String] {
let arguments = self.arguments(command)
if let index = arguments.firstIndex(where: { URL(fileURLWithPath: $0).lastPathComponent == "claude" }) {
return Array(arguments.suffix(from: arguments.index(after: index)))
}
return arguments
}
private static func isObviousClaudeHelper(_ command: String) -> Bool {
let lowercased = command.lowercased()
return lowercased.contains("--version") ||
lowercased.contains("--help") ||
lowercased.contains("claude-code-acp")
}
private static func isCodexAgentExecutable(_ command: String) -> Bool {
let lowercased = command.lowercased()
guard lowercased.contains(".app/") else { return true }
return lowercased.hasPrefix("/applications/codex.app/contents/resources/codex ") ||
lowercased.hasPrefix("/applications/codex.app/contents/resources/codex\t")
}
private static func isClaudeAgentExecutable(_ command: String) -> Bool {
let lowercased = command.lowercased()
return !lowercased.contains(".app/") || lowercased.contains("application support/claude/claude-code/claude")
}
}
public enum LSOFCWDOutputParser {
public static func parse(_ output: String) -> [Int32: String] {
var result: [Int32: String] = [:]
var currentPID: Int32?
for line in output.split(whereSeparator: \ .isNewline).map(String.init) {
guard let prefix = line.first else { continue }
switch prefix {
case "p":
currentPID = Int32(line.dropFirst())
case "n":
if let currentPID {
result[currentPID] = String(line.dropFirst())
}
default:
continue
}
}
return result
}
}
public enum ClaudeSessionProjectMapper {
public struct Transcript: Equatable, Sendable {
public let url: URL
public let modifiedAt: Date
public init(url: URL, modifiedAt: Date) {
self.url = url
self.modifiedAt = modifiedAt
}
}
public static func escapedCWD(_ cwd: String) -> String {
String(cwd.unicodeScalars.map { scalar in
let value = scalar.value
let isASCIIAlphanumeric = (48...57).contains(value) ||
(65...90).contains(value) ||
(97...122).contains(value)
return isASCIIAlphanumeric ? Character(scalar) : "-"
})
}
public static func projectDirectories(
cwd: String,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser,
fileManager: FileManager = .default) -> [URL]
{
let standardRoot = homeDirectory
.appendingPathComponent(".claude", isDirectory: true)
.appendingPathComponent("projects", isDirectory: true)
return ([standardRoot] + ClaudeDesktopProjectsLocator.roots(
homeDirectory: homeDirectory,
fileManager: fileManager))
.map { $0.appendingPathComponent(self.escapedCWD(cwd), isDirectory: true) }
}
public static func newestTranscript(
cwd: String,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser,
fileManager: FileManager = .default) -> (url: URL, modifiedAt: Date)?
{
self.transcripts(cwd: cwd, homeDirectory: homeDirectory, fileManager: fileManager).first
.map { (url: $0.url, modifiedAt: $0.modifiedAt) }
}
public static func transcripts(
cwd: String,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser,
fileManager: FileManager = .default) -> [Transcript]
{
self.projectDirectories(cwd: cwd, homeDirectory: homeDirectory, fileManager: fileManager)
.flatMap { directory -> [(URL, Date)] in
guard let files = try? fileManager.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: [.contentModificationDateKey],
options: [.skipsHiddenFiles])
else { return [] }
return files.compactMap { file in
guard file.pathExtension == "jsonl",
let modifiedAt = try? file.resourceValues(
forKeys: [.contentModificationDateKey]).contentModificationDate
else { return nil }
return (file, modifiedAt)
}
}
.sorted { $0.1 > $1.1 }
.map { Transcript(url: $0.0, modifiedAt: $0.1) }
}
}
public enum AgentSessionCorrelation {
public static func newestProcessesFirst(_ processes: [AgentProcessRecord]) -> [AgentProcessRecord] {
processes.sorted { lhs, rhs in
let lhsDate = lhs.startedAt ?? .distantPast
let rhsDate = rhs.startedAt ?? .distantPast
if lhsDate != rhsDate { return lhsDate > rhsDate }
return lhs.pid > rhs.pid
}
}
public static func assignClaudeTranscripts(
processes: [AgentProcessRecord],
cwdByPID: [Int32: String],
transcriptsByCWD: [String: [ClaudeSessionProjectMapper.Transcript]])
-> [Int32: ClaudeSessionProjectMapper.Transcript]
{
var assigned: [Int32: ClaudeSessionProjectMapper.Transcript] = [:]
var usedPaths = Set<String>()
let unambiguousPIDs = self.unambiguousProcessIDs(processes: processes, cwdByPID: cwdByPID)
for process in self.newestProcessesFirst(processes) {
guard unambiguousPIDs.contains(process.pid),
let cwd = cwdByPID[process.pid],
let candidates = transcriptsByCWD[cwd]
else { continue }
let candidate = candidates.first { transcript in
guard !usedPaths.contains(transcript.url.path) else { return false }
guard let startedAt = process.startedAt else { return true }
return transcript.modifiedAt >= startedAt
}
if let candidate {
assigned[process.pid] = candidate
usedPaths.insert(candidate.url.path)
}
}
return assigned
}
public static func unambiguousProcessIDs(
processes: [AgentProcessRecord],
cwdByPID: [Int32: String]) -> Set<Int32>
{
let grouped = Dictionary(grouping: processes.compactMap { process -> (Int32, String)? in
guard let cwd = cwdByPID[process.pid] else { return nil }
return (process.pid, URL(fileURLWithPath: cwd).standardizedFileURL.path)
}, by: { $0.1 })
return Set(grouped.values.compactMap { records in
records.count == 1 ? records[0].0 : nil
})
}
public static func codexWorkingDirectoriesMatch(_ lhs: String?, _ rhs: String?) -> Bool {
guard let lhs, let rhs else { return false }
return URL(fileURLWithPath: lhs).standardizedFileURL.path ==
URL(fileURLWithPath: rhs).standardizedFileURL.path
}
public static func fileOnlyCodexSource(
metadataSource: AgentSession.Source,
appServerPresent: Bool) -> AgentSession.Source
{
metadataSource == .unknown && appServerPresent ? .desktopApp : metadataSource
}
}
public struct CodexRolloutMetadata: Equatable, Sendable {
public let sessionID: String
public let cwd: String?
public let originator: String?
public let source: String?
public init(sessionID: String, cwd: String?, originator: String?, source: String?) {
self.sessionID = sessionID
self.cwd = cwd
self.originator = originator
self.source = source
}
public var sessionSource: AgentSession.Source {
let value = [self.originator, self.source]
.compactMap { $0?.lowercased() }
.joined(separator: " ")
if value.contains("desktop") || value.contains("app-server") {
return .desktopApp
}
if value.contains("ide") || value.contains("vscode") || value.contains("cursor") || value.contains("zed") {
return .ide
}
if value.contains("codex_exec") || value.contains("exec") || value.contains("cli") {
return .cli
}
return .unknown
}
}
public enum CodexRolloutFirstLineParser {
public static func parse(_ line: String) -> CodexRolloutMetadata? {
guard let data = line.data(using: .utf8),
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
object["type"] as? String == "session_meta",
let payload = object["payload"] as? [String: Any],
let sessionID = (payload["session_id"] as? String) ?? (payload["id"] as? String)
else { return nil }
return CodexRolloutMetadata(
sessionID: sessionID,
cwd: payload["cwd"] as? String,
originator: payload["originator"] as? String,
source: payload["source"] as? String)
}
public static func read(from url: URL) -> CodexRolloutMetadata? {
guard let handle = try? FileHandle(forReadingFrom: url) else { return nil }
defer { try? handle.close() }
var data = Data()
while data.count < 256 * 1024 {
guard let chunk = try? handle.read(upToCount: 4096), !chunk.isEmpty else { break }
if let newline = chunk.firstIndex(of: 0x0A) {
data.append(chunk.prefix(upTo: newline))
break
}
data.append(chunk)
}
guard let line = String(data: data, encoding: .utf8) else { return nil }
return self.parse(line)
}
public static func makeSession(
metadata: CodexRolloutMetadata,
transcriptURL: URL,
modifiedAt: Date,
pid: Int32? = nil,
startedAt: Date? = nil,
host: String,
config: SessionScanConfig = SessionScanConfig(),
now: Date = Date()) -> AgentSession?
{
guard pid != nil || now.timeIntervalSince(modifiedAt) <= config.fileOnlyWindow else { return nil }
let cwd = metadata.cwd
return AgentSession(
id: metadata.sessionID,
provider: .codex,
source: metadata.sessionSource,
state: config.state(lastActivityAt: modifiedAt, now: now, hasLiveProcess: pid != nil),
pid: pid,
cwd: cwd,
projectName: cwd.map { URL(fileURLWithPath: $0).lastPathComponent },
startedAt: startedAt,
lastActivityAt: modifiedAt,
transcriptPath: transcriptURL.path,
host: host)
}
}