Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Sources/Vifty/AppPreferencesStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ final class AppPreferencesStore: @unchecked Sendable {
return preferences
}

// Preserve the unreadable original before any overwrite so a decode
// failure never silently destroys the last recoverable copy.
if FileManager.default.fileExists(atPath: url.path) {
let backup = url.appendingPathExtension("bak")
try? FileManager.default.removeItem(at: backup)
try? FileManager.default.copyItem(at: url, to: backup)
}

let migrated = migratedPreferences()
if migrated != .defaults {
try? saveThrowing(migrated)
Expand Down
19 changes: 19 additions & 0 deletions Sources/ViftyCore/AgentControlModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,25 @@ public struct AgentControlStatus: Codable, Equatable, Sendable {
public var lastErrorCode: AgentControlErrorCode?
public var policy: AgentControlPolicySnapshot?

private enum CodingKeys: String, CodingKey {
case enabled
case activeLease
case lastDecision
case lastErrorCode
case policy
}

// Emit nil optionals as explicit JSON nulls so strict schema consumers see
// the full contract shape even in the idle state.
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(enabled, forKey: .enabled)
try container.encode(activeLease, forKey: .activeLease)
try container.encode(lastDecision, forKey: .lastDecision)
try container.encode(lastErrorCode, forKey: .lastErrorCode)
try container.encode(policy, forKey: .policy)
}

public init(
enabled: Bool,
activeLease: AgentCoolingLease?,
Expand Down
25 changes: 25 additions & 0 deletions Sources/ViftyCore/ViftyCtlRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,31 @@ public struct ViftyCtlStatusReport: Codable, Equatable, Sendable {
public var lastErrorCode: AgentControlErrorCode?
public var policy: AgentControlPolicySnapshot?

private enum CodingKeys: String, CodingKey {
case schemaVersion
case schemaID
case generatedAt
case enabled
case activeLease
case lastDecision
case lastErrorCode
case policy
}

// Emit nil optionals as explicit JSON nulls so the status schema's required
// keys are present in the idle payload.
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(schemaVersion, forKey: .schemaVersion)
try container.encode(schemaID, forKey: .schemaID)
try container.encode(generatedAt, forKey: .generatedAt)
try container.encode(enabled, forKey: .enabled)
try container.encode(activeLease, forKey: .activeLease)
try container.encode(lastDecision, forKey: .lastDecision)
try container.encode(lastErrorCode, forKey: .lastErrorCode)
try container.encode(policy, forKey: .policy)
}

public init(
schemaVersion: Int = 1,
schemaID: String = ViftyCtlSchemaReferences.schemaIDs.status,
Expand Down