feat(cron): enhance delivery modes and job configuration

- Updated isolated cron jobs to support new delivery modes: `announce` and `none`, improving output management.
- Refactored job configuration to remove legacy fields and streamline delivery settings.
- Enhanced the `CronJobEditor` UI to reflect changes in delivery options, including a new segmented control for delivery mode selection.
- Updated documentation to clarify the new delivery configurations and their implications for job execution.
- Improved tests to validate the new delivery behavior and ensure backward compatibility with legacy settings.

This update provides users with greater flexibility in managing how isolated jobs deliver their outputs, enhancing overall usability and clarity in job configurations.
This commit is contained in:
Tyler Yust
2026-02-03 16:53:46 -08:00
committed by Peter Steinberger
parent ab9f06f4ff
commit 3f82daefd8
56 changed files with 917 additions and 1150 deletions
@@ -20,9 +20,11 @@ extension CronJobEditor {
self.wakeMode = job.wakeMode
switch job.schedule {
case let .at(atMs):
case let .at(at):
self.scheduleKind = .at
self.atDate = Date(timeIntervalSince1970: TimeInterval(atMs) / 1000)
if let date = CronSchedule.parseAtDate(at) {
self.atDate = date
}
case let .every(everyMs, _):
self.scheduleKind = .every
self.everyText = self.formatDuration(ms: everyMs)
@@ -36,19 +38,22 @@ extension CronJobEditor {
case let .systemEvent(text):
self.payloadKind = .systemEvent
self.systemEventText = text
case let .agentTurn(message, thinking, timeoutSeconds, deliver, channel, to, bestEffortDeliver):
case let .agentTurn(message, thinking, timeoutSeconds, _, _, _, _):
self.payloadKind = .agentTurn
self.agentMessage = message
self.thinking = thinking ?? ""
self.timeoutSeconds = timeoutSeconds.map(String.init) ?? ""
self.deliver = deliver ?? false
let trimmed = (channel ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
self.channel = trimmed.isEmpty ? "last" : trimmed
self.to = to ?? ""
self.bestEffortDeliver = bestEffortDeliver ?? false
}
self.postPrefix = job.isolation?.postToMainPrefix ?? "Cron"
if let delivery = job.delivery {
self.deliveryMode = delivery.mode == .announce ? .announce : .none
let trimmed = (delivery.channel ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
self.channel = trimmed.isEmpty ? "last" : trimmed
self.to = delivery.to ?? ""
self.bestEffortDeliver = delivery.bestEffort ?? false
} else if self.sessionTarget == .isolated {
self.deliveryMode = .announce
}
}
func save() {
@@ -88,15 +93,25 @@ extension CronJobEditor {
}
if self.sessionTarget == .isolated {
let trimmed = self.postPrefix.trimmingCharacters(in: .whitespacesAndNewlines)
root["isolation"] = [
"postToMainPrefix": trimmed.isEmpty ? "Cron" : trimmed,
]
root["delivery"] = self.buildDelivery()
}
return root.mapValues { AnyCodable($0) }
}
func buildDelivery() -> [String: Any] {
let mode = self.deliveryMode == .announce ? "announce" : "none"
var delivery: [String: Any] = ["mode": mode]
if self.deliveryMode == .announce {
let trimmed = self.channel.trimmingCharacters(in: .whitespacesAndNewlines)
delivery["channel"] = trimmed.isEmpty ? "last" : trimmed
let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines)
if !to.isEmpty { delivery["to"] = to }
if self.bestEffortDeliver { delivery["bestEffort"] = true }
}
return delivery
}
func trimmed(_ value: String) -> String {
value.trimmingCharacters(in: .whitespacesAndNewlines)
}
@@ -115,7 +130,7 @@ extension CronJobEditor {
func buildSchedule() throws -> [String: Any] {
switch self.scheduleKind {
case .at:
return ["kind": "at", "atMs": Int(self.atDate.timeIntervalSince1970 * 1000)]
return ["kind": "at", "at": CronSchedule.formatIsoDate(self.atDate)]
case .every:
guard let ms = Self.parseDurationMs(self.everyText) else {
throw NSError(
@@ -209,14 +224,6 @@ extension CronJobEditor {
let thinking = self.thinking.trimmingCharacters(in: .whitespacesAndNewlines)
if !thinking.isEmpty { payload["thinking"] = thinking }
if let n = Int(self.timeoutSeconds), n > 0 { payload["timeoutSeconds"] = n }
payload["deliver"] = self.deliver
if self.deliver {
let trimmed = self.channel.trimmingCharacters(in: .whitespacesAndNewlines)
payload["channel"] = trimmed.isEmpty ? "last" : trimmed
let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines)
if !to.isEmpty { payload["to"] = to }
payload["bestEffortDeliver"] = self.bestEffortDeliver
}
return payload
}