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 static let weightedStances: [(stance: Self, weight: Double)] = [ (.standard, 63), (.southpaw, 19), (.flurry, 5), (.spin, 4), (.shadowbox, 3), (.backflip, 2), (.zen, 2), (.drummer, 1), (.peekaboo, 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, 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 }