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)
Task {
let pipeline = ReadingEventUploadPipeline.shared
pipeline.exportAndEnqueue(contexts: [:]) // contexts populated from active sessions
pipeline.exportAndEnqueue() // pulls from ContextRegistry
await pipeline.flush()
}
case .active:
// Resume session + reload stale events from Rust
ReadingRuntimeSessionManager.shared.resume()
Task {
ReadingEventUploadPipeline.shared.reloadOnLaunch(contexts: [:])
ReadingEventUploadPipeline.shared.reloadOnLaunch()
}
case .inactive:
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() {}
/// Full pipeline: export from Rust enqueue.
func exportAndEnqueue(contexts: [String: ReadingMaterialContext]) {
/// Full pipeline: export from Rust enqueue. Pulls contexts from registry if not provided.
func exportAndEnqueue(contexts: [String: ReadingMaterialContext] = [:]) {
let effectiveContexts = contexts.isEmpty ? ReadingContextRegistry.shared.allContexts() : contexts
let rustEvents = adapter.exportEvents(limit: 100, timestampMs: nowMs())
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 }
queue.enqueue(uploadItems)
@ -214,7 +215,7 @@ final class ReadingEventUploadPipeline {
}
/// Reload on app launch: reload stale Rust events, enqueue, retry failed.
func reloadOnLaunch(contexts: [String: ReadingMaterialContext]) {
func reloadOnLaunch(contexts: [String: ReadingMaterialContext] = [:]) {
_ = adapter.reloadStaleEvents()
_ = adapter.cleanupStaleSessions(nowMs: nowMs(), maxAgeMs: 30 * 60 * 1000)

View File

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