feat(app): add flow diagnostics and refine interface
CI / Validate manifests (push) Has been cancelled
CI / SwiftLint (push) Has been cancelled
CI / iOS / Extension (push) Has been cancelled
CI / macOS (push) Has been cancelled

Improve failure recovery visibility, strengthen account-session handling, and make the cross-platform interface more consistent.
This commit is contained in:
Rocky
2026-08-26 14:40:06 +08:00
parent 1ee032bdb9
commit 39002336c0
78 changed files with 2737 additions and 735 deletions
+100 -7
View File
@@ -302,6 +302,14 @@ final class FlowSessionManager: ObservableObject {
)
guard AppGroup.isAvailable else {
debug("cannot start flow session: App Group unavailable")
FlowDiagnostics.persistStartupFailure(
reason: "appGroupUnavailable",
context: startupFailureContext(
trigger: reason,
attemptCount: 0,
elapsed: 0
)
)
return
}
@@ -396,20 +404,35 @@ final class FlowSessionManager: ObservableObject {
reason: String = "unspecified",
startCapture: Bool = false
) {
OSGDiag.log(
FlowDiagnostics.log(
"activateOnForeground reason=\(reason) startCapture=\(startCapture) "
+ "onboardingDone=\(ProviderConfig.shared.hasCompletedOnboarding) "
+ "\(OSGDiag.memoryTag())",
category: "flow"
+ "\(OSGDiag.memoryTag())"
)
guard AppGroup.isAvailable else {
OSGDiag.log("activateOnForeground aborted reason=appGroupUnavailable", category: "flow")
FlowDiagnostics.log("activateOnForeground aborted reason=appGroupUnavailable")
FlowDiagnostics.persistStartupFailure(
reason: "appGroupUnavailable",
context: startupFailureContext(
trigger: "activateOnForeground:\(reason)",
attemptCount: 0,
elapsed: 0
)
)
return
}
guard AppPermissions.flowRequirementsMet else {
OSGDiag.log("activateOnForeground aborted reason=permissions", category: "flow")
FlowDiagnostics.log("activateOnForeground aborted reason=permissions")
sessionWarning = permissionWarningMessage()
FlowSessionBridge.setHostReady(false)
FlowDiagnostics.persistStartupFailure(
reason: "permissionsMissing",
context: startupFailureContext(
trigger: "activateOnForeground:\(reason)",
attemptCount: 0,
elapsed: 0
)
)
return
}
@@ -422,10 +445,30 @@ final class FlowSessionManager: ObservableObject {
/// User-requested recovery always discards the rejected AVKit generation.
func retryPiPRecovery() {
guard AppGroup.isAvailable else { return }
guard AppGroup.isAvailable else {
FlowDiagnostics.log("manual PiP recovery aborted reason=appGroupUnavailable")
FlowDiagnostics.persistStartupFailure(
reason: "appGroupUnavailable",
context: startupFailureContext(
trigger: FlowPiPActivationTrigger.manualRetry.rawValue,
attemptCount: 0,
elapsed: 0
)
)
return
}
guard AppPermissions.flowRequirementsMet else {
sessionWarning = permissionWarningMessage()
FlowSessionBridge.setHostReady(false)
FlowDiagnostics.log("manual PiP recovery aborted reason=permissions")
FlowDiagnostics.persistStartupFailure(
reason: "permissionsMissing",
context: startupFailureContext(
trigger: FlowPiPActivationTrigger.manualRetry.rawValue,
attemptCount: 0,
elapsed: 0
)
)
return
}
wantsActiveSession = true
@@ -933,6 +976,7 @@ final class FlowSessionManager: ObservableObject {
guard !Task.isCancelled else { return }
traceState("startSessionAsync.begin", extra: "trigger=\(trigger.rawValue)")
sessionWarning = nil
let recoveryStartedAt = Date()
guard AppPermissions.flowRequirementsMet else {
sessionWarning = permissionWarningMessage()
@@ -940,11 +984,20 @@ final class FlowSessionManager: ObservableObject {
traceState("startSessionAsync.blocked", extra: "reason=permissions")
FlowSessionBridge.setHostReady(false)
isColdStartHandoff = false
FlowDiagnostics.persistStartupFailure(
reason: "permissionsMissing",
context: startupFailureContext(
trigger: trigger.rawValue,
attemptCount: 0,
elapsed: Date().timeIntervalSince(recoveryStartedAt)
)
)
return
}
let deadline = Date().addingTimeInterval(FlowPiPRecoveryPolicy.totalBudget)
var lastFailure: FlowPiPStartFailure = .timedOut
var attemptsPerformed = 0
for attempt in 1...FlowPiPRecoveryPolicy.maxAttempts {
guard FlowPiPRecoveryPolicy.canContinue(
@@ -984,6 +1037,7 @@ final class FlowSessionManager: ObservableObject {
let remaining = deadline.timeIntervalSinceNow
guard remaining > 0 else { break }
attemptsPerformed = attempt
pipLifecycleState = isActive
? .recovering(attempt: attempt, total: FlowPiPRecoveryPolicy.maxAttempts)
: .preparing(attempt: attempt, total: FlowPiPRecoveryPolicy.maxAttempts)
@@ -1001,6 +1055,8 @@ final class FlowSessionManager: ObservableObject {
remainingBudget: deadline.timeIntervalSinceNow
)
)
let recoveryElapsed = FlowPiPRecoveryPolicy.totalBudget
- max(0, deadline.timeIntervalSinceNow)
guard FlowPiPRecoveryPolicy.canContinue(
operation: operation,
currentOperation: pipRecoveryOperation,
@@ -1029,7 +1085,9 @@ final class FlowSessionManager: ObservableObject {
lastFailure = failure
traceState(
"startSessionAsync.failed",
extra: "reason=pipUnavailable trigger=\(trigger.rawValue) attempt=\(attempt) failure=\(failure)"
extra: "reason=pipUnavailable trigger=\(trigger.rawValue) "
+ "attempt=\(attempt) failure=\(failure) "
+ "elapsed=\(String(format: "%.2f", recoveryElapsed))"
)
}
}
@@ -1040,6 +1098,14 @@ final class FlowSessionManager: ObservableObject {
FlowSessionBridge.setHostReady(false)
refreshHostReady()
debug("PiP keep-alive failed after bounded recovery: \(lastFailure)")
FlowDiagnostics.persistStartupFailure(
reason: String(describing: lastFailure),
context: startupFailureContext(
trigger: trigger.rawValue,
attemptCount: attemptsPerformed,
elapsed: Date().timeIntervalSince(recoveryStartedAt)
)
)
}
private func activateFlowSessionAfterPiPProof(duration: TimeInterval?) {
@@ -3539,6 +3605,33 @@ final class FlowSessionManager: ObservableObject {
FlowDiagnostics.log(message)
}
private func startupFailureContext(
trigger: String,
attemptCount: Int,
elapsed: TimeInterval
) -> [String: String] {
let memory = OSGDiag.memorySnapshot()
return [
"trigger": trigger,
"attemptCount": "\(attemptCount)",
"elapsedSeconds": String(format: "%.3f", elapsed),
"appForeground": isAppForeground ? "true" : "false",
"sessionActive": isActive ? "true" : "false",
"sessionIntent": wantsActiveSession ? "true" : "false",
"pipLifecycleState": String(describing: pipLifecycleState),
"pipGeneration": "\(pipController.generation)",
"pipActive": pipController.isPictureInPictureActive ? "true" : "false",
"hostReady": FlowSessionBridge.isHostReady() ? "true" : "false",
"bridgeSessionActive": FlowSessionBridge.isSessionActive() ? "true" : "false",
"heartbeatStalenessSeconds": FlowSessionBridge.heartbeatStaleness()
.map { String(format: "%.3f", $0) } ?? "nil",
"microphonePermission": String(describing: AppPermissions.micStatus),
"speechPermission": String(describing: AppPermissions.speechStatus),
"rssMB": String(format: "%.1f", memory.rssMB),
"physicalFootprintMB": String(format: "%.1f", memory.physFootprintMB)
]
}
private static func safeErrorLogMetadata(_ error: Error) -> String {
"errorCategory=\(String(reflecting: type(of: error))) "
+ "errorBytes=\(error.localizedDescription.utf8.count)"