| 1 |
/** |
| 2 |
* useReloadRecovery — Phase 4.2 |
| 3 |
* |
| 4 |
* On component mount (and whenever the session_id changes), fetch the |
| 5 |
* most recent brain-authored TurnState snapshot for this session from |
| 6 |
* Laravel and hydrate `useChatExecutionStore.turnSnapshot`. Restores: |
| 7 |
* - TurnTerminalBanner (Phase 4.3) if the last turn had errors / |
| 8 |
* was unresolved / timed out / etc. |
| 9 |
* - Tool-card terminal states via reconcileToolCallsFromSnapshot. |
| 10 |
* |
| 11 |
* Silent on failure: reload recovery is best-effort. If the request |
| 12 |
* errors (network, auth, 404), the UI continues with whatever live |
| 13 |
* state it has. A failed recovery should never break the chat. |
| 14 |
* |
| 15 |
* Why this exists: before Phase 4.2, a browser refresh after a turn |
| 16 |
* ended wiped the in-memory `turnSnapshot`, so the banner vanished |
| 17 |
* and tool-card terminal states reverted to whatever the delta-driven |
| 18 |
* store reassembled (often wrong). With this hook, the durable copy |
| 19 |
* in Laravel DB is the restore source. |
| 20 |
*/ |
| 21 |
|
| 22 |
import { useEffect } from 'react'; |
| 23 |
import { sessionLastTurn } from '../services/api'; |
| 24 |
import { useChatExecutionStore } from '../store/useChatExecutionStore'; |
| 25 |
|
| 26 |
/** |
| 27 |
* @param {string|null|undefined} sessionId Current chat session identifier. |
| 28 |
* When null/empty, the hook is a no-op (no session, nothing to restore). |
| 29 |
*/ |
| 30 |
export function useReloadRecovery(sessionId) { |
| 31 |
const applyTurnSnapshot = useChatExecutionStore((s) => s.applyTurnSnapshot); |
| 32 |
const reconcileToolCallsFromSnapshot = useChatExecutionStore((s) => s.reconcileToolCallsFromSnapshot); |
| 33 |
|
| 34 |
useEffect(() => { |
| 35 |
if (!sessionId) return; |
| 36 |
let cancelled = false; |
| 37 |
|
| 38 |
(async () => { |
| 39 |
try { |
| 40 |
const resp = await sessionLastTurn(sessionId); |
| 41 |
if (cancelled) return; |
| 42 |
|
| 43 |
const snapshot = resp?.snapshot; |
| 44 |
if (!snapshot) return; |
| 45 |
|
| 46 |
applyTurnSnapshot(snapshot); |
| 47 |
|
| 48 |
// At terminal time brain's tool_calls are authoritative — |
| 49 |
// reconcile any store cards that survived from delta-driven |
| 50 |
// replay so they match brain's final status. |
| 51 |
if (snapshot.terminal && Array.isArray(snapshot.tool_calls)) { |
| 52 |
reconcileToolCallsFromSnapshot(snapshot.tool_calls); |
| 53 |
} |
| 54 |
} catch (err) { |
| 55 |
// Best-effort — don't break the chat on recovery failure. |
| 56 |
// eslint-disable-next-line no-console |
| 57 |
} |
| 58 |
})(); |
| 59 |
|
| 60 |
return () => { |
| 61 |
cancelled = true; |
| 62 |
}; |
| 63 |
}, [sessionId, applyTurnSnapshot, reconcileToolCallsFromSnapshot]); |
| 64 |
} |
| 65 |
|