File size: 10,497 Bytes
b8370f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | import Foundation
enum ChatWorkingClawStance: CaseIterable, Equatable, Sendable {
case standard
case southpaw
case flurry
case spin
case shadowbox
case backflip
case zen
case drummer
case peekaboo
case nodOff
case curious
case omNom
case fakeOut
static let weightedStances: [(stance: Self, weight: Double)] = [
(.standard, 55),
(.southpaw, 18),
(.flurry, 5),
(.spin, 4),
(.shadowbox, 3),
(.backflip, 2),
(.zen, 2),
(.drummer, 2),
(.peekaboo, 2),
(.nodOff, 2),
(.curious, 2),
(.omNom, 2),
(.fakeOut, 1),
]
static func seeded(_ key: String, salt: UInt32) -> Self {
let totalWeight = self.weightedStances.reduce(0) { $0 + $1.weight }
var roll = Double((fnv1a(key) ^ salt) % 1000) / 1000 * totalWeight
for entry in self.weightedStances {
roll -= entry.weight
if roll <= 0 {
return entry.stance
}
}
return .standard
}
}
enum ChatWorkingPhrase {
static let showAfterMilliseconds = 30000
static let rotateEveryMilliseconds = 45000
/// Keep source phrases localizable; the post-merge native locale refresh
/// owns the generated catalog entries.
static let resources: [String] = [
String(localized: "Shelling"),
String(localized: "Scuttling"),
String(localized: "Clawing"),
String(localized: "Pinching"),
String(localized: "Molting"),
String(localized: "Bubbling"),
String(localized: "Tiding"),
String(localized: "Reefing"),
String(localized: "Cracking"),
String(localized: "Sifting"),
String(localized: "Brining"),
String(localized: "Nautiling"),
String(localized: "Krilling"),
String(localized: "Barnacling"),
String(localized: "Lobstering"),
String(localized: "Tidepooling"),
String(localized: "Pearling"),
String(localized: "Snapping"),
String(localized: "Surfacing"),
]
/// A constant-time stride walk keeps adjacent phrases distinct. The prime
/// list length makes every non-zero stride visit all phrases before repeating.
static func index(seed: String, elapsedMilliseconds: Int) -> Int? {
guard elapsedMilliseconds >= self.showAfterMilliseconds else { return nil }
let bucket = (elapsedMilliseconds - self.showAfterMilliseconds) / self.rotateEveryMilliseconds
return self.index(seed: seed, bucket: bucket)
}
static func index(seed: String, bucket: Int) -> Int {
let count = self.resources.count
let offset = Int(fnv1a("\(seed):offset") % UInt32(count))
let stride = 1 + Int(fnv1a("\(seed):stride") % UInt32(count - 1))
return (offset + bucket * stride) % count
}
}
enum ChatWorkingDurationFormatter {
private static let compactUnits = [
(suffix: "d", seconds: 86400),
(suffix: "h", seconds: 3600),
(suffix: "m", seconds: 60),
(suffix: "s", seconds: 1),
]
static func compact(milliseconds: Double) -> String {
let totalSeconds = self.totalSeconds(milliseconds: milliseconds)
var remainder = totalSeconds
var parts: [String] = []
for unit in self.compactUnits {
let value = remainder / unit.seconds
remainder %= unit.seconds
if value > 0 {
parts.append("\(value)\(unit.suffix)")
}
if parts.count == 2 {
break
}
}
return parts.joined(separator: " ")
}
static func full(milliseconds: Double, locale: Locale = .current) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.day, .hour, .minute, .second]
formatter.unitsStyle = .full
formatter.maximumUnitCount = 2
formatter.zeroFormattingBehavior = .dropAll
var calendar = Calendar(identifier: .gregorian)
calendar.locale = locale
formatter.calendar = calendar
return formatter.string(from: TimeInterval(self.totalSeconds(milliseconds: milliseconds))) ??
String(localized: "1 second", locale: locale)
}
private static func totalSeconds(milliseconds: Double) -> Int {
let finiteMilliseconds = milliseconds.isFinite ? milliseconds : 1000
let roundedSeconds = (max(1000, finiteMilliseconds) / 1000).rounded()
return max(1, Int(min(roundedSeconds, Double(Int.max / 2))))
}
}
enum ChatWorkingIdentity {
static func resolve(
sessionKey: String,
pendingRunIDs: Set<String>,
localUserMessageIDsByRunID: [String: UUID],
fallbackGeneration: UInt64) -> String
{
if let messageID = pendingRunIDs.compactMap({ localUserMessageIDsByRunID[$0]?.uuidString }).min() {
return "\(sessionKey):user:\(messageID)"
}
if let runID = pendingRunIDs.min() {
return "\(sessionKey):run:\(runID)"
}
return "\(sessionKey):generation:\(fallbackGeneration)"
}
}
struct ChatTurnRecap: Equatable, Sendable {
let runtimeMs: Double
let outputTokens: Int?
}
enum ChatTurnRecapText {
static func done(runtimeMs: Double, locale: Locale = .current) -> String {
String(
format: String(localized: "Done in %@", locale: locale),
locale: locale,
ChatWorkingDurationFormatter.full(milliseconds: runtimeMs, locale: locale))
}
static func tokens(_ outputTokens: Int?, locale: Locale = .current) -> String? {
guard let outputTokens else { return nil }
// Mirrors the web and American-English source contract: exactly one
// uses the singular key; zero and every other count use the generic key.
if outputTokens == 1 {
return String(localized: "1 token", locale: locale)
}
return String(
format: String(localized: "%@ tokens", locale: locale),
locale: locale,
outputTokens.formatted(.number.locale(locale)))
}
}
struct ChatTurnRecapSessionRow: Equatable, Sendable {
let status: String?
let endedAt: Double?
let runtimeMs: Double?
let outputTokens: Int?
init(
status: String? = nil,
endedAt: Double? = nil,
runtimeMs: Double? = nil,
outputTokens: Int? = nil)
{
self.status = status
self.endedAt = endedAt
self.runtimeMs = runtimeMs
self.outputTokens = outputTokens
}
init(_ entry: OpenClawChatSessionEntry) {
self.init(
status: entry.status,
endedAt: entry.endedAt,
runtimeMs: entry.runtimeMs,
outputTokens: entry.outputTokens)
}
}
/// Watches the turn whose working indicator was visible. Most anomalous
/// interleavings fail quiet; without a row run ID, an unrelated completion
/// inside the settle window remains an accepted cosmetic attribution risk.
struct ChatTurnRecapResolver {
private struct Watch {
var watching: Bool
var baselineKnown: Bool
var baselineEndedAt: Double?
var absorbedTerminal: Bool
var settleStartedAt: Date?
var settled: ChatTurnRecap?
}
private static let settleWindow: TimeInterval = 30
private var watches: [String: Watch] = [:]
mutating func resolve(
sessionKey: String,
indicatorVisible: Bool,
row: ChatTurnRecapSessionRow?,
now: Date = Date()) -> ChatTurnRecap?
{
var watch = self.watches[sessionKey]
let rowEndedAt = row?.endedAt
if indicatorVisible {
if watch == nil || watch?.watching == false {
watch = Watch(
watching: true,
baselineKnown: row != nil,
baselineEndedAt: rowEndedAt,
absorbedTerminal: false,
settleStartedAt: nil,
settled: nil)
} else if watch?.baselineKnown == false {
if row != nil {
watch?.baselineKnown = true
watch?.baselineEndedAt = rowEndedAt
}
} else if let rowEndedAt, rowEndedAt != watch?.baselineEndedAt {
watch?.baselineEndedAt = rowEndedAt
watch?.absorbedTerminal = true
}
self.watches[sessionKey] = watch
return nil
}
guard var watch else { return nil }
watch.watching = false
if let settled = watch.settled {
self.watches[sessionKey] = watch
return settled
}
if watch.absorbedTerminal || !watch.baselineKnown {
// Without an attributable baseline, later terminal rows could belong
// to background work, so consume this watch unresolved.
self.watches[sessionKey] = nil
return nil
}
if let settleStartedAt = watch.settleStartedAt {
if now.timeIntervalSince(settleStartedAt) > Self.settleWindow {
self.watches[sessionKey] = nil
return nil
}
} else {
watch.settleStartedAt = now
}
let isStale: Bool = if let rowEndedAt, let baselineEndedAt = watch.baselineEndedAt {
rowEndedAt <= baselineEndedAt
} else {
rowEndedAt == nil
}
if isStale {
// Terminal stamps are monotonic. Equality and regression both mean
// the watched run's terminal patch has not arrived yet.
self.watches[sessionKey] = watch
return nil
}
// Any fresh terminal concludes the watch. Only clean done rows with a
// finite runtime produce a recap; waiting longer could attach another run.
self.watches[sessionKey] = nil
guard row?.status == "done", let runtimeMs = row?.runtimeMs, runtimeMs.isFinite else {
return nil
}
let settled = ChatTurnRecap(runtimeMs: runtimeMs, outputTokens: row?.outputTokens)
watch.settled = settled
self.watches[sessionKey] = watch
return settled
}
}
private func fnv1a(_ key: String) -> UInt32 {
var hash: UInt32 = 0x811C_9DC5
for codeUnit in key.utf16 {
hash ^= UInt32(codeUnit)
hash = hash &* 0x0100_0193
}
return hash
}
|