mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 13:21:11 +00:00
* feat(chat): replay the active plan snapshot to reconnecting clients Plan checklists were live-event-only: a client that connected or refreshed mid-run saw nothing until the next update_plan call, and iOS adoptRun actively wiped retained plan state on reconnect. The gateway now records the latest normalized plan snapshot per run beside the existing chat text buffers and replays it through the established chat.history/chat.startup inFlightRun recovery contract (runId > plan > text under the payload budget; a budget-dropped plan is sent as an explicit empty snapshot so clients never preserve a stale checklist). Web, iOS, and Android seed and reconcile the snapshot through one shared contract riding each client's existing inFlightRun adoption gate: present plan replaces, absent plan preserves (legacy gateway), a different or terminated run clears, and stale history responses cannot clobber a newer live plan. Verified live gap: openclaw/openclaw#108675 comment 5001475066. * fix(ios): extract run-snapshot adoption below the file-length lint ceiling ChatViewModel.swift crossed SwiftLint's 1500-line limit; the in-flight snapshot adoption and plan reconciliation move to a focused extension. Also refresh the native i18n inventory for shifted line anchors. * fixup: regenerate i18n anchors and test formatting after rebase * ci: retrigger after wedged queued runs * fix(ui): use Object.hasOwn for the plan field presence probe * fix(ios): annotate the plan-snapshot fixture init for periphery
73 lines
3.1 KiB
Swift
73 lines
3.1 KiB
Swift
import OpenClawKit
|
|
|
|
/// In-flight run adoption shared by history replay and live transport events.
|
|
extension OpenClawChatViewModel {
|
|
func applyInFlightRunSnapshot(
|
|
_ payload: OpenClawChatHistoryPayload,
|
|
for request: HistoryRequest)
|
|
{
|
|
guard request.runOwnershipGeneration == self.runOwnershipGeneration,
|
|
request.id >= self.latestAppliedRunSnapshotRequestID
|
|
else {
|
|
return
|
|
}
|
|
self.latestAppliedRunSnapshotRequestID = request.id
|
|
// Plan reconciliation shares run adoption: rejected history cannot clobber newer live state.
|
|
// A missing plan is version-skew unknown; replacement or explicit terminal evidence clears it.
|
|
guard let snapshot = payload.inFlightRun,
|
|
let runId = Self.normalizedRunID(snapshot.runId)
|
|
else {
|
|
guard let retainedRunId = self.planRunId else { return }
|
|
let activeRunIds = payload.sessionInfo?.activeRunIds
|
|
let confirmsAnotherRun = activeRunIds.map { !$0.contains(retainedRunId) } == true
|
|
if payload.sessionInfo?.hasActiveRun == false || confirmsAnotherRun {
|
|
self.clearPlan(for: retainedRunId)
|
|
}
|
|
return
|
|
}
|
|
|
|
self.isApplyingRunSnapshot = true
|
|
defer { self.isApplyingRunSnapshot = false }
|
|
self.updateActiveSessionRunWithoutChatSnapshot(false)
|
|
self.adoptRunState(runId: runId, bufferedText: snapshot.text, preservePlan: true)
|
|
if self.planRunId != nil, self.planRunId != runId {
|
|
self.clearPlan()
|
|
}
|
|
if let planSnapshot = snapshot.plan {
|
|
self.applyPlanSnapshot(
|
|
runId: runId,
|
|
steps: planSnapshot.steps,
|
|
explanation: planSnapshot.explanation)
|
|
}
|
|
}
|
|
|
|
func adoptRun(runId: String, bufferedText: String) {
|
|
self.adoptRunState(runId: runId, bufferedText: bufferedText, preservePlan: false)
|
|
}
|
|
|
|
private func adoptRunState(runId: String, bufferedText: String, preservePlan: Bool) {
|
|
let canonicalPendingRuns = Set([runId])
|
|
let replacedRun = self.pendingRuns != canonicalPendingRuns
|
|
if replacedRun {
|
|
// Gateway snapshots and live deltas are canonical for this session.
|
|
// Replace stale local ownership so only that run consumes later events.
|
|
clearPendingRuns(reason: nil, preservePlan: preservePlan)
|
|
self.pendingRuns.insert(runId)
|
|
self.pendingToolCallsById = [:]
|
|
self.updateStreamingAssistantText(nil)
|
|
}
|
|
if self.runMessageScopesByRunID[runId] == nil {
|
|
self.runMessageScopesByRunID[runId] = currentRunMessageScope()
|
|
}
|
|
if self.pendingRunOwnerArmIDs[runId] == nil {
|
|
armPendingRunOwner(runId: runId)
|
|
}
|
|
if !bufferedText.isEmpty {
|
|
self.updateStreamingAssistantText(bufferedText)
|
|
}
|
|
self.logDiagnostic(
|
|
"chat.ui adopted in-flight run sessionKey=\(self.sessionKey) "
|
|
+ "runId=\(runId) bufferedTextLen=\(bufferedText.count)")
|
|
}
|
|
}
|