fix: G1 context registry — prevent empty contexts from dropping all export events

- Add ReadingContextRegistry for SessionManager→Pipeline context sharing
- Wire registry into SessionManager (register on open, unregister on close)
- Fix Pipeline to auto-pull from registry when contexts not provided
- Fix ScenePhase to not pass empty contexts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-09 22:08:03 +08:00
parent 51b9365ece
commit f421fbb721
4 changed files with 31 additions and 6 deletions

View File

@ -71,14 +71,14 @@ struct AIStudyAppApp: App {
// Flush events: export enqueue (quick upload in background task) // Flush events: export enqueue (quick upload in background task)
Task { Task {
let pipeline = ReadingEventUploadPipeline.shared let pipeline = ReadingEventUploadPipeline.shared
pipeline.exportAndEnqueue(contexts: [:]) // contexts populated from active sessions pipeline.exportAndEnqueue() // pulls from ContextRegistry
await pipeline.flush() await pipeline.flush()
} }
case .active: case .active:
// Resume session + reload stale events from Rust // Resume session + reload stale events from Rust
ReadingRuntimeSessionManager.shared.resume() ReadingRuntimeSessionManager.shared.resume()
Task { Task {
ReadingEventUploadPipeline.shared.reloadOnLaunch(contexts: [:]) ReadingEventUploadPipeline.shared.reloadOnLaunch()
} }
case .inactive: case .inactive:
break break

View File

@ -0,0 +1,22 @@
import Foundation
/// Shared registry of active reading material contexts.
/// The SessionManager registers contexts here; the Pipeline reads them for export.
@MainActor
final class ReadingContextRegistry {
static let shared = ReadingContextRegistry()
private var contexts: [String: ReadingMaterialContext] = [:]
func register(_ context: ReadingMaterialContext) {
contexts[context.materialId] = context
}
func unregister(materialId: String) {
contexts.removeValue(forKey: materialId)
}
func allContexts() -> [String: ReadingMaterialContext] {
return contexts
}
}

View File

@ -174,12 +174,13 @@ final class ReadingEventUploadPipeline {
private init() {} private init() {}
/// Full pipeline: export from Rust enqueue. /// Full pipeline: export from Rust enqueue. Pulls contexts from registry if not provided.
func exportAndEnqueue(contexts: [String: ReadingMaterialContext]) { func exportAndEnqueue(contexts: [String: ReadingMaterialContext] = [:]) {
let effectiveContexts = contexts.isEmpty ? ReadingContextRegistry.shared.allContexts() : contexts
let rustEvents = adapter.exportEvents(limit: 100, timestampMs: nowMs()) let rustEvents = adapter.exportEvents(limit: 100, timestampMs: nowMs())
guard !rustEvents.isEmpty else { return } guard !rustEvents.isEmpty else { return }
let uploadItems = ReadingEventMapper.map(rustEvents: rustEvents, contexts: contexts) let uploadItems = ReadingEventMapper.map(rustEvents: rustEvents, contexts: effectiveContexts)
guard !uploadItems.isEmpty else { return } guard !uploadItems.isEmpty else { return }
queue.enqueue(uploadItems) queue.enqueue(uploadItems)
@ -214,7 +215,7 @@ final class ReadingEventUploadPipeline {
} }
/// Reload on app launch: reload stale Rust events, enqueue, retry failed. /// Reload on app launch: reload stale Rust events, enqueue, retry failed.
func reloadOnLaunch(contexts: [String: ReadingMaterialContext]) { func reloadOnLaunch(contexts: [String: ReadingMaterialContext] = [:]) {
_ = adapter.reloadStaleEvents() _ = adapter.reloadStaleEvents()
_ = adapter.cleanupStaleSessions(nowMs: nowMs(), maxAgeMs: 30 * 60 * 1000) _ = adapter.cleanupStaleSessions(nowMs: nowMs(), maxAgeMs: 30 * 60 * 1000)

View File

@ -52,6 +52,7 @@ final class ReadingRuntimeSessionManager {
activeSessionId = sessionId activeSessionId = sessionId
activeContext = context activeContext = context
state = .active state = .active
ReadingContextRegistry.shared.register(context)
lastPosition = nil lastPosition = nil
lastHeartbeatAtMs = now lastHeartbeatAtMs = now
@ -74,6 +75,7 @@ final class ReadingRuntimeSessionManager {
_ = try? adapter.closeSession(sessionId) _ = try? adapter.closeSession(sessionId)
activeSessionId = nil activeSessionId = nil
if let ctx = activeContext { ReadingContextRegistry.shared.unregister(materialId: ctx.materialId) }
activeContext = nil activeContext = nil
lastPosition = nil lastPosition = nil
state = .closed state = .closed