Skip to content

Commit a498a9a

Browse files
committed
refactor(macos): satisfy session catalog lint
1 parent 5c6cc5d commit a498a9a

1 file changed

Lines changed: 212 additions & 142 deletions

File tree

apps/macos/Sources/OpenClaw/NodeMode/MacNodeClaudeSessionCatalog.swift

Lines changed: 212 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,26 @@ enum MacNodeClaudeSessionCatalog {
9898
var generation: UInt64 = 0
9999
}
100100

101+
private struct CLIRecordInspection {
102+
var aiTitle: String?
103+
var record: SessionRecord?
104+
var sidechain = false
105+
var shouldStop = false
106+
}
107+
108+
private struct CLIRecordDiscoveryContext {
109+
var projectsURL: URL
110+
var resolvedProjectsURL: URL
111+
var rootPath: String
112+
}
113+
114+
private struct CLIRecordFileScan {
115+
var fileBytes: Int
116+
var record: SessionRecord?
117+
var sidechain: Bool
118+
var cacheable: Bool
119+
}
120+
101121
private final class CatalogDiscoveryCache: @unchecked Sendable {
102122
private let lock = NSLock()
103123
private var entries: [String: CatalogDiscoveryCacheEntry] = [:]
@@ -439,7 +459,10 @@ extension MacNodeClaudeSessionCatalog {
439459
var scannedBytes = 0
440460
var truncated = false
441461
var seenPaths = Set<String>()
442-
let rootPath = projectsURL.path
462+
let context = CLIRecordDiscoveryContext(
463+
projectsURL: projectsURL,
464+
resolvedProjectsURL: resolvedProjectsURL,
465+
rootPath: projectsURL.path)
443466
scan: for projectURL in self.childDirectories(projectsURL) {
444467
try Task.checkCancellation()
445468
let files = (try? FileManager.default.contentsOfDirectory(
@@ -453,157 +476,204 @@ extension MacNodeClaudeSessionCatalog {
453476
break scan
454477
}
455478
discoveredFiles += 1
456-
let sessionId = candidate.deletingPathExtension().lastPathComponent
457-
guard !sessionId.isEmpty,
458-
records[sessionId] == nil,
459-
!sidechainIds.contains(sessionId),
460-
let fileURL = self.safeSessionFile(
461-
root: projectsURL,
462-
resolvedRoot: resolvedProjectsURL,
463-
candidate: candidate,
464-
sessionId: sessionId)
465-
else { continue }
466-
let identity = self.catalogFileIdentity(fileURL)
467-
let cachePath = fileURL.path
468-
seenPaths.insert(cachePath)
469-
// Cache identity does not encode ACLs. Preserve the old open-on-every-list
470-
// authorization behavior before returning cached prompt metadata.
471-
guard FileManager.default.isReadableFile(atPath: cachePath) else { continue }
472-
if let identity,
473-
let cached = self.catalogDiscoveryCache.lookup(
474-
path: cachePath,
475-
rootPath: rootPath,
476-
identity: identity,
477-
sessionId: sessionId),
478-
scannedBytes + cached.scannedBytes <= self.maxCatalogMetadataScanBytes
479-
{
480-
if cached.sidechain {
481-
sidechainIds.insert(sessionId)
482-
}
483-
if let record = cached.record {
484-
records[sessionId] = record
485-
}
486-
// Preserve the cold-scan byte frontier so repeated pagination stays stable.
487-
scannedBytes += cached.scannedBytes
488-
if scannedBytes >= self.maxCatalogMetadataScanBytes {
489-
truncated = true
490-
break scan
491-
}
492-
continue
493-
}
494-
guard let handle = try? FileHandle(forReadingFrom: fileURL) else { continue }
495-
var aiTitle: String?
496-
let updatedAt = identity.map {
497-
Int64($0.modificationDate.timeIntervalSince1970 * 1000)
498-
} ?? (try? fileURL.resourceValues(
499-
forKeys: [.contentModificationDateKey]).contentModificationDate)
500-
.map { Int64($0.timeIntervalSince1970 * 1000) }
501-
var stopFile = false
502-
var discoveredRecord: SessionRecord?
503-
var discoveredSidechain = false
504-
func inspectLine(_ line: Data) {
505-
guard let row = try? JSONSerialization.jsonObject(with: line) as? [String: Any],
506-
self.string(row["sessionId"], maxLength: self.maxSessionIdLength) == sessionId
507-
else { return }
508-
if row["type"] as? String == "ai-title" {
509-
aiTitle = self.string(row["aiTitle"], maxLength: 500) ?? aiTitle
510-
return
511-
}
512-
if let entrypoint = row["entrypoint"] as? String, entrypoint != "sdk-cli" {
513-
stopFile = true
514-
return
515-
}
516-
if row["entrypoint"] as? String == "sdk-cli",
517-
(row["isSidechain"] as? Bool) == true
518-
{
519-
discoveredSidechain = true
520-
stopFile = true
521-
return
522-
}
523-
guard row["entrypoint"] as? String == "sdk-cli",
524-
row["type"] as? String == "user",
525-
let message = row["message"] as? [String: Any],
526-
message["role"] as? String == "user",
527-
let content = message["content"]
528-
else { return }
529-
var fragments: [String] = []
530-
self.collectText(content, into: &fragments)
531-
discoveredRecord = SessionRecord(
532-
threadId: sessionId,
533-
name: aiTitle ?? fragments.first.flatMap { self.string($0, maxLength: 500) },
534-
cwd: self.string(row["cwd"]),
535-
createdAt: self.timestampMs(row["timestamp"]),
536-
updatedAt: updatedAt,
537-
source: "claude-cli",
538-
gitBranch: self.string(row["gitBranch"], maxLength: 500),
539-
fileURL: fileURL)
540-
stopFile = true
541-
}
542-
var pending = Data()
543-
var fileBytes = 0
544-
var reachedEnd = false
545-
while !stopFile,
546-
fileBytes < self.metadataPrefixBytes,
547-
scannedBytes < self.maxCatalogMetadataScanBytes
548-
{
549-
try Task.checkCancellation()
550-
let size = min(
551-
self.metadataReadChunkBytes,
552-
self.metadataPrefixBytes - fileBytes,
553-
self.maxCatalogMetadataScanBytes - scannedBytes)
554-
guard size > 0 else { break }
555-
guard let chunk = try? handle.read(upToCount: size) else {
556-
pending.removeAll()
557-
break
558-
}
559-
if chunk.isEmpty {
560-
reachedEnd = true
561-
break
562-
}
563-
fileBytes += chunk.count
564-
scannedBytes += chunk.count
565-
pending.append(chunk)
566-
while !stopFile, let newline = pending.firstIndex(of: 0x0A) {
567-
inspectLine(Data(pending[..<newline]))
568-
pending.removeSubrange(...newline)
569-
}
570-
}
571-
if !stopFile, reachedEnd, !pending.isEmpty {
572-
inspectLine(pending)
573-
}
574-
try? handle.close()
575-
if discoveredSidechain {
576-
sidechainIds.insert(sessionId)
577-
}
578-
if let discoveredRecord {
579-
records[sessionId] = discoveredRecord
580-
}
581-
let budgetConstrained = scannedBytes >= self.maxCatalogMetadataScanBytes
582-
if let identity,
583-
!budgetConstrained,
584-
stopFile || reachedEnd || fileBytes >= self.metadataPrefixBytes
479+
if try self.discoverCLIRecord(
480+
candidate: candidate,
481+
context: context,
482+
scannedBytes: &scannedBytes,
483+
seenPaths: &seenPaths,
484+
records: &records,
485+
sidechainIds: &sidechainIds)
585486
{
586-
self.catalogDiscoveryCache.store(
587-
CatalogDiscoveryCacheEntry(
588-
rootPath: rootPath,
589-
identity: identity,
590-
sessionId: sessionId,
591-
scannedBytes: fileBytes,
592-
record: discoveredRecord,
593-
sidechain: discoveredSidechain),
594-
path: cachePath)
595-
}
596-
if scannedBytes >= self.maxCatalogMetadataScanBytes {
597487
truncated = true
598488
break scan
599489
}
600490
}
601491
}
602492
if !truncated {
603-
self.catalogDiscoveryCache.removeUnseen(rootPath: rootPath, seenPaths: seenPaths)
493+
self.catalogDiscoveryCache.removeUnseen(rootPath: context.rootPath, seenPaths: seenPaths)
604494
}
605495
}
606496

497+
private static func discoverCLIRecord(
498+
candidate: URL,
499+
context: CLIRecordDiscoveryContext,
500+
scannedBytes: inout Int,
501+
seenPaths: inout Set<String>,
502+
records: inout [String: SessionRecord],
503+
sidechainIds: inout Set<String>) throws -> Bool
504+
{
505+
let sessionId = candidate.deletingPathExtension().lastPathComponent
506+
guard !sessionId.isEmpty,
507+
records[sessionId] == nil,
508+
!sidechainIds.contains(sessionId),
509+
let fileURL = self.safeSessionFile(
510+
root: context.projectsURL,
511+
resolvedRoot: context.resolvedProjectsURL,
512+
candidate: candidate,
513+
sessionId: sessionId)
514+
else { return false }
515+
let identity = self.catalogFileIdentity(fileURL)
516+
let cachePath = fileURL.path
517+
seenPaths.insert(cachePath)
518+
// Cache identity does not encode ACLs. Preserve open-on-every-list authorization.
519+
guard FileManager.default.isReadableFile(atPath: cachePath) else { return false }
520+
if let identity,
521+
let cached = self.catalogDiscoveryCache.lookup(
522+
path: cachePath,
523+
rootPath: context.rootPath,
524+
identity: identity,
525+
sessionId: sessionId),
526+
scannedBytes + cached.scannedBytes <= self.maxCatalogMetadataScanBytes
527+
{
528+
if cached.sidechain {
529+
sidechainIds.insert(sessionId)
530+
}
531+
if let record = cached.record {
532+
records[sessionId] = record
533+
}
534+
// Preserve the cold-scan byte frontier so repeated pagination stays stable.
535+
scannedBytes += cached.scannedBytes
536+
return scannedBytes >= self.maxCatalogMetadataScanBytes
537+
}
538+
guard let handle = try? FileHandle(forReadingFrom: fileURL) else { return false }
539+
let updatedAt = identity.map {
540+
Int64($0.modificationDate.timeIntervalSince1970 * 1000)
541+
} ?? (try? fileURL.resourceValues(
542+
forKeys: [.contentModificationDateKey]).contentModificationDate)
543+
.map { Int64($0.timeIntervalSince1970 * 1000) }
544+
let scan = try self.scanCLIRecordFile(
545+
handle: handle,
546+
fileURL: fileURL,
547+
sessionId: sessionId,
548+
updatedAt: updatedAt,
549+
byteLimit: self.maxCatalogMetadataScanBytes - scannedBytes)
550+
try? handle.close()
551+
scannedBytes += scan.fileBytes
552+
if scan.sidechain {
553+
sidechainIds.insert(sessionId)
554+
}
555+
if let record = scan.record {
556+
records[sessionId] = record
557+
}
558+
let budgetConstrained = scannedBytes >= self.maxCatalogMetadataScanBytes
559+
if let identity, !budgetConstrained, scan.cacheable {
560+
self.catalogDiscoveryCache.store(
561+
CatalogDiscoveryCacheEntry(
562+
rootPath: context.rootPath,
563+
identity: identity,
564+
sessionId: sessionId,
565+
scannedBytes: scan.fileBytes,
566+
record: scan.record,
567+
sidechain: scan.sidechain),
568+
path: cachePath)
569+
}
570+
return budgetConstrained
571+
}
572+
573+
private static func scanCLIRecordFile(
574+
handle: FileHandle,
575+
fileURL: URL,
576+
sessionId: String,
577+
updatedAt: Int64?,
578+
byteLimit: Int) throws -> CLIRecordFileScan
579+
{
580+
var inspection = CLIRecordInspection()
581+
var pending = Data()
582+
var fileBytes = 0
583+
var reachedEnd = false
584+
var readFailed = false
585+
while !inspection.shouldStop,
586+
fileBytes < self.metadataPrefixBytes,
587+
fileBytes < byteLimit
588+
{
589+
try Task.checkCancellation()
590+
let size = min(
591+
self.metadataReadChunkBytes,
592+
self.metadataPrefixBytes - fileBytes,
593+
byteLimit - fileBytes)
594+
guard size > 0 else { break }
595+
guard let chunk = try? handle.read(upToCount: size) else {
596+
pending.removeAll()
597+
readFailed = true
598+
break
599+
}
600+
if chunk.isEmpty {
601+
reachedEnd = true
602+
break
603+
}
604+
fileBytes += chunk.count
605+
pending.append(chunk)
606+
while !inspection.shouldStop, let newline = pending.firstIndex(of: 0x0A) {
607+
self.inspectCLIRecordLine(
608+
Data(pending[..<newline]),
609+
sessionId: sessionId,
610+
fileURL: fileURL,
611+
updatedAt: updatedAt,
612+
inspection: &inspection)
613+
pending.removeSubrange(...newline)
614+
}
615+
}
616+
if !inspection.shouldStop, reachedEnd, !pending.isEmpty {
617+
self.inspectCLIRecordLine(
618+
pending,
619+
sessionId: sessionId,
620+
fileURL: fileURL,
621+
updatedAt: updatedAt,
622+
inspection: &inspection)
623+
}
624+
return CLIRecordFileScan(
625+
fileBytes: fileBytes,
626+
record: inspection.record,
627+
sidechain: inspection.sidechain,
628+
cacheable: !readFailed &&
629+
(inspection.shouldStop || reachedEnd || fileBytes >= self.metadataPrefixBytes))
630+
}
631+
632+
private static func inspectCLIRecordLine(
633+
_ line: Data,
634+
sessionId: String,
635+
fileURL: URL,
636+
updatedAt: Int64?,
637+
inspection: inout CLIRecordInspection)
638+
{
639+
guard let row = try? JSONSerialization.jsonObject(with: line) as? [String: Any],
640+
self.string(row["sessionId"], maxLength: self.maxSessionIdLength) == sessionId
641+
else { return }
642+
if row["type"] as? String == "ai-title" {
643+
inspection.aiTitle = self.string(row["aiTitle"], maxLength: 500) ?? inspection.aiTitle
644+
return
645+
}
646+
if let entrypoint = row["entrypoint"] as? String, entrypoint != "sdk-cli" {
647+
inspection.shouldStop = true
648+
return
649+
}
650+
if row["entrypoint"] as? String == "sdk-cli",
651+
(row["isSidechain"] as? Bool) == true
652+
{
653+
inspection.sidechain = true
654+
inspection.shouldStop = true
655+
return
656+
}
657+
guard row["entrypoint"] as? String == "sdk-cli",
658+
row["type"] as? String == "user",
659+
let message = row["message"] as? [String: Any],
660+
message["role"] as? String == "user",
661+
let content = message["content"]
662+
else { return }
663+
var fragments: [String] = []
664+
self.collectText(content, into: &fragments)
665+
inspection.record = SessionRecord(
666+
threadId: sessionId,
667+
name: inspection.aiTitle ?? fragments.first.flatMap { self.string($0, maxLength: 500) },
668+
cwd: self.string(row["cwd"]),
669+
createdAt: self.timestampMs(row["timestamp"]),
670+
updatedAt: updatedAt,
671+
source: "claude-cli",
672+
gitBranch: self.string(row["gitBranch"], maxLength: 500),
673+
fileURL: fileURL)
674+
inspection.shouldStop = true
675+
}
676+
607677
private static func sessions(homeURL: URL) throws -> [SessionRecord] {
608678
try Task.checkCancellation()
609679
let projectsURL = self.projectsURL(homeURL: homeURL)

0 commit comments

Comments
 (0)