fix(mobile): recover pending questions after cold start

This commit is contained in:
Bohdan Triapitsyn
2026-08-10 15:10:36 +03:00
parent 87f2d21054
commit dfa7b45dd0
7 changed files with 257 additions and 12 deletions
+42 -11
View File
@@ -1178,23 +1178,24 @@ const updateRoutingIndexFromEvent = (
* recovery paths only; normal session switches rely on primary SSE reducer
* state for `question.asked` / `permission.asked` events. When
* `candidateSessionIds` is omitted, every session known to the directory store
* is treated as a candidate.
* is treated as a candidate; when provided, recovery is limited to those IDs.
*/
export async function resyncBlockingRequestsForDirectory(
directory: string,
store: StoreApi<DirectoryStore>,
candidateSessionIds?: string[],
options?: { includePermissions?: boolean },
) {
const before = store.getState()
const knownSessionIds = new Set<string>([
const candidateIds = new Set<string>(candidateSessionIds ?? [
...before.session.map((session) => session.id),
...Object.keys(before.message ?? {}),
...Object.keys(before.session_status ?? {}),
...Object.keys(before.question ?? {}),
...Object.keys(before.permission ?? {}),
])
const candidates = candidateSessionIds ?? Array.from(knownSessionIds)
if (candidates.length === 0) return
if (candidateIds.size === 0) return
const candidates = Array.from(candidateIds)
// Re-fetch pending questions that may have been asked during an SSE gap,
// reconnect window, or directory materialization gap.
@@ -1204,12 +1205,12 @@ export async function resyncBlockingRequestsForDirectory(
)
const pendingQuestions = await opencodeClient.listPendingQuestions({ directories: [directory] })
const grouped: Record<string, QuestionRequest[]> = {}
for (const q of pendingQuestions) {
if (!q?.id || !q.sessionID) continue
if (!knownSessionIds.has(q.sessionID)) continue
const list = grouped[q.sessionID]
if (list) list.push(q)
else grouped[q.sessionID] = [q]
for (const question of pendingQuestions) {
if (!question?.id || !question.sessionID) continue
if (!candidateIds.has(question.sessionID)) continue
const list = grouped[question.sessionID]
if (list) list.push(question)
else grouped[question.sessionID] = [question]
}
for (const sessionId of Object.keys(grouped)) {
grouped[sessionId].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
@@ -1256,6 +1257,8 @@ export async function resyncBlockingRequestsForDirectory(
// Non-fatal: question resync best-effort
}
if (options?.includePermissions === false) return
// Re-fetch pending permissions — same rationale as questions.
try {
const beforeSignatures = new Map(
@@ -1265,7 +1268,7 @@ export async function resyncBlockingRequestsForDirectory(
const grouped: Record<string, PermissionRequest[]> = {}
for (const permission of pendingPermissions) {
if (!permission?.id || !permission.sessionID) continue
if (!knownSessionIds.has(permission.sessionID)) continue
if (!candidateIds.has(permission.sessionID)) continue
const list = grouped[permission.sessionID]
if (list) list.push(permission)
else grouped[permission.sessionID] = [permission]
@@ -1330,6 +1333,15 @@ export async function resyncBlockingRequestsForDirectory(
}
}
export async function resyncBlockingRequestsForActiveDirectory(
directory: string,
childStores: ChildStoreManager,
) {
const store = childStores.getChild(directory)
if (!store) return
await resyncBlockingRequestsForDirectory(directory, store)
}
async function resyncDirectoryAfterReconnect(
directory: string,
store: StoreApi<DirectoryStore>,
@@ -1928,6 +1940,7 @@ export function SyncProvider(props: {
const lastFullResyncAtByDirectoryRef = useRef(new Map<string, number>())
const lastChildDiscoveryAtByDirectoryRef = useRef(new Map<string, number>())
const resyncingDirectoriesRef = useRef(new Set<string>())
const blockingRequestResyncingDirectoriesRef = useRef(new Set<string>())
const statusPollingDirectoriesRef = useRef(new Set<string>())
const pipelineReconnectRef = useRef<((reason?: string) => void) | null>(null)
const pipelineHasConnectedRef = useRef(false)
@@ -1961,6 +1974,24 @@ export function SyncProvider(props: {
})
}, [childStores, routingIndex])
useEffect(() => {
if (typeof window === "undefined") return
const onSystemResume = () => {
const directory = currentDirectoryRef.current
if (!directory || !childStores.getChild(directory)) return
const resyncing = blockingRequestResyncingDirectoriesRef.current
if (resyncing.has(directory)) return
resyncing.add(directory)
void resyncBlockingRequestsForActiveDirectory(directory, childStores)
.finally(() => resyncing.delete(directory))
}
window.addEventListener("openchamber:system-resume", onSystemResume)
return () => window.removeEventListener("openchamber:system-resume", onSystemResume)
}, [childStores])
// Configure child store manager
useEffect(() => {
void usePermissionStore.getState().hydrate().catch(() => undefined)