45 lines
2.2 KiB
Swift
45 lines
2.2 KiB
Swift
import CryptoKit
|
|
import Foundation
|
|
|
|
struct AgentOutboxRow: Codable, Equatable, Sendable {
|
|
var requestID: String
|
|
var content: String
|
|
var createdAt: Date
|
|
var status: String = "sending"
|
|
var seenOnServer = false
|
|
var confirmed = false
|
|
var error: String?
|
|
var attachments: [AgentAttachment]? = nil // Optional for legacy text-only outboxes.
|
|
var draftID: String? = nil // Recovers a crash between outbox persistence and clearing the composer.
|
|
var id: String { "inbound:sybil:\(requestID)" }
|
|
var message: Message {
|
|
var info: [String: JSONValue] = ["kind": .string("message"), "status": .string(status), "requestId": .string(requestID)]
|
|
if let error { info["error"] = .string(error) }
|
|
if let attachments, !attachments.isEmpty { info["agentAttachments"] = .array(attachments.map(\.metadataValue)) }
|
|
return Message(id: id, createdAt: createdAt, role: .user, content: content, metadata: .object(info))
|
|
}
|
|
}
|
|
|
|
struct AgentOutboxStorage: Sendable {
|
|
var read: @Sendable () throws -> [AgentOutboxRow]
|
|
var write: @Sendable ([AgentOutboxRow]) throws -> Void
|
|
|
|
static func file(baseURL: URL) -> Self {
|
|
// Separate installations, but never persist tokens in a filename or payload.
|
|
let key = SHA256.hash(data: Data(baseURL.absoluteString.utf8)).map { String(format: "%02x", $0) }.joined()
|
|
let directory = URL.applicationSupportDirectory.appendingPathComponent("AgentOutbox", isDirectory: true)
|
|
let url = directory.appendingPathComponent("\(key).json")
|
|
return Self(read: {
|
|
guard FileManager.default.fileExists(atPath: url.path) else { return [] }
|
|
return try JSONDecoder().decode([AgentOutboxRow].self, from: Data(contentsOf: url))
|
|
}, write: { rows in
|
|
let data = try JSONEncoder().encode(rows)
|
|
guard rows.count <= 100, data.count <= 2 * 1024 * 1024 else {
|
|
throw AgentFailure(message: "Retry pending messages before sending more.")
|
|
}
|
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
try data.write(to: url, options: [.atomic, .completeFileProtectionUntilFirstUserAuthentication])
|
|
})
|
|
}
|
|
}
|