mirror of
https://github.com/daggerhashimoto/openclaw-nerve
synced 2026-04-21 10:37:17 +00:00
chore: fix exhaustive-deps lint warnings
This commit is contained in:
parent
7700b1fd58
commit
4cf147f163
2 changed files with 140 additions and 107 deletions
|
|
@ -144,6 +144,41 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
setStream: streamHook.setStream,
|
||||
});
|
||||
|
||||
const {
|
||||
loadHistory,
|
||||
getAllMessages,
|
||||
applyMessageWindow,
|
||||
} = msgHook;
|
||||
const {
|
||||
triggerRecovery,
|
||||
clearDisconnectState,
|
||||
captureDisconnectState,
|
||||
wasGeneratingOnDisconnect,
|
||||
isRecoveryInFlight,
|
||||
isRecoveryPending,
|
||||
incrementGeneration,
|
||||
getGeneration,
|
||||
} = recoveryHook;
|
||||
const {
|
||||
lastEventTimestamp,
|
||||
setProcessingStage,
|
||||
setLastEventTimestamp,
|
||||
setActivityLog,
|
||||
addActivityEntry,
|
||||
completeActivityEntry,
|
||||
startThinking,
|
||||
captureThinkingDuration,
|
||||
scheduleStreamingUpdate,
|
||||
clearStreamBuffer,
|
||||
getThinkingDuration,
|
||||
resetThinking,
|
||||
} = streamHook;
|
||||
const {
|
||||
playCompletionPing,
|
||||
resetPlayedSounds,
|
||||
handleFinalTTS,
|
||||
} = ttsHook;
|
||||
|
||||
// ─── Reset transient state on session switch ──────────────────────────────
|
||||
useEffect(() => {
|
||||
setIsGenerating(false);
|
||||
|
|
@ -167,31 +202,29 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
const prevConnection = previousConnectionStateRef.current;
|
||||
|
||||
if (connectionState === 'connected') {
|
||||
if (prevConnection === 'reconnecting' && recoveryHook.wasGeneratingOnDisconnect()) {
|
||||
recoveryHook.triggerRecovery('reconnect');
|
||||
if (prevConnection === 'reconnecting' && wasGeneratingOnDisconnect()) {
|
||||
triggerRecovery('reconnect');
|
||||
}
|
||||
recoveryHook.clearDisconnectState();
|
||||
clearDisconnectState();
|
||||
}
|
||||
|
||||
if (connectionState === 'reconnecting' && prevConnection === 'connected') {
|
||||
recoveryHook.captureDisconnectState();
|
||||
captureDisconnectState();
|
||||
}
|
||||
|
||||
previousConnectionStateRef.current = connectionState;
|
||||
}, [
|
||||
connectionState,
|
||||
currentSession,
|
||||
msgHook.loadHistory,
|
||||
recoveryHook.wasGeneratingOnDisconnect,
|
||||
recoveryHook.triggerRecovery,
|
||||
recoveryHook.clearDisconnectState,
|
||||
recoveryHook.captureDisconnectState,
|
||||
wasGeneratingOnDisconnect,
|
||||
triggerRecovery,
|
||||
clearDisconnectState,
|
||||
captureDisconnectState,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionState !== 'connected' || !currentSession) return;
|
||||
msgHook.loadHistory(currentSession);
|
||||
}, [connectionState, currentSession, msgHook.loadHistory]);
|
||||
loadHistory(currentSession);
|
||||
}, [connectionState, currentSession, loadHistory]);
|
||||
|
||||
// ─── Periodic history poll for sub-agent sessions ─────────────────────────
|
||||
const isSubagentSession = currentSession ? isSubagentSessionKey(currentSession) : false;
|
||||
|
|
@ -212,14 +245,14 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
const sk = currentSessionRef.current;
|
||||
const result = await loadChatHistory({ rpc, sessionKey: sk, limit: 500 });
|
||||
if (sk !== currentSessionRef.current) return;
|
||||
const prev = msgHook.getAllMessages();
|
||||
const prev = getAllMessages();
|
||||
if (
|
||||
result.length === prev.length &&
|
||||
result.length > 0 &&
|
||||
result[result.length - 1]?.rawText === prev[prev.length - 1]?.rawText &&
|
||||
result[result.length - 1]?.role === prev[prev.length - 1]?.role
|
||||
) return;
|
||||
msgHook.applyMessageWindow(result, false);
|
||||
applyMessageWindow(result, false);
|
||||
} catch { /* best-effort */ } finally {
|
||||
subagentPollInFlightRef.current = false;
|
||||
}
|
||||
|
|
@ -229,26 +262,26 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
clearInterval(pollInterval);
|
||||
subagentPollInFlightRef.current = false;
|
||||
};
|
||||
}, [isSubagentActive, connectionState, currentSession, rpc, msgHook.applyMessageWindow, msgHook.getAllMessages]);
|
||||
}, [isSubagentActive, connectionState, currentSession, rpc, applyMessageWindow, getAllMessages]);
|
||||
|
||||
// ─── Watchdog: if stream stalls, recover once ─────────────────────────────
|
||||
useEffect(() => {
|
||||
if (!isGenerating || !streamHook.lastEventTimestamp) return;
|
||||
if (!isGenerating || !lastEventTimestamp) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
const elapsed = Date.now() - streamHook.lastEventTimestamp;
|
||||
if (elapsed >= 12_000 && !recoveryHook.isRecoveryInFlight() && !recoveryHook.isRecoveryPending()) {
|
||||
recoveryHook.triggerRecovery('chat-gap');
|
||||
const elapsed = Date.now() - lastEventTimestamp;
|
||||
if (elapsed >= 12_000 && !isRecoveryInFlight() && !isRecoveryPending()) {
|
||||
triggerRecovery('chat-gap');
|
||||
}
|
||||
}, 12_000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [
|
||||
isGenerating,
|
||||
streamHook.lastEventTimestamp,
|
||||
recoveryHook.isRecoveryInFlight,
|
||||
recoveryHook.isRecoveryPending,
|
||||
recoveryHook.triggerRecovery,
|
||||
lastEventTimestamp,
|
||||
isRecoveryInFlight,
|
||||
isRecoveryPending,
|
||||
triggerRecovery,
|
||||
]);
|
||||
|
||||
// ─── Subscribe to streaming events ────────────────────────────────────────
|
||||
|
|
@ -258,7 +291,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
const triggerRecoveryOnce = (reason: RecoveryReason) => {
|
||||
if (recoveryTriggeredThisEvent) return;
|
||||
recoveryTriggeredThisEvent = true;
|
||||
recoveryHook.triggerRecovery(reason);
|
||||
triggerRecovery(reason);
|
||||
};
|
||||
|
||||
const classified = classifyStreamEvent(msg);
|
||||
|
|
@ -273,7 +306,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
isRootChildSession(classified.sessionKey, getRootAgentSessionKey(currentSk) || currentSk) &&
|
||||
(classified.type === 'chat_final' || classified.type === 'lifecycle_end')
|
||||
) {
|
||||
recoveryHook.triggerRecovery('subagent-complete');
|
||||
triggerRecovery('subagent-complete');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -294,32 +327,32 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
if (type === 'lifecycle_start') {
|
||||
setIsGenerating(true);
|
||||
streamHook.setProcessingStage('thinking');
|
||||
streamHook.setLastEventTimestamp(Date.now());
|
||||
setProcessingStage('thinking');
|
||||
setLastEventTimestamp(Date.now());
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'lifecycle_end') {
|
||||
setIsGenerating(false);
|
||||
streamHook.setProcessingStage(null);
|
||||
streamHook.setActivityLog([]);
|
||||
streamHook.setLastEventTimestamp(0);
|
||||
ttsHook.playCompletionPing();
|
||||
setProcessingStage(null);
|
||||
setActivityLog([]);
|
||||
setLastEventTimestamp(0);
|
||||
playCompletionPing();
|
||||
|
||||
recoveryHook.incrementGeneration();
|
||||
incrementGeneration();
|
||||
|
||||
const activeRun = activeRunIdRef.current;
|
||||
const runFinalized = activeRun ? runsRef.current.get(activeRun)?.finalized : false;
|
||||
if (!runFinalized) {
|
||||
recoveryHook.triggerRecovery('reconnect');
|
||||
triggerRecovery('reconnect');
|
||||
}
|
||||
activeRunIdRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'assistant_stream') {
|
||||
streamHook.setProcessingStage('streaming');
|
||||
streamHook.setLastEventTimestamp(Date.now());
|
||||
setProcessingStage('streaming');
|
||||
setLastEventTimestamp(Date.now());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -328,30 +361,30 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
setIsGenerating(true);
|
||||
}
|
||||
|
||||
streamHook.setLastEventTimestamp(Date.now());
|
||||
setLastEventTimestamp(Date.now());
|
||||
|
||||
if (type === 'agent_tool_start') {
|
||||
streamHook.setProcessingStage('tool_use');
|
||||
streamHook.addActivityEntry(ap);
|
||||
setProcessingStage('tool_use');
|
||||
addActivityEntry(ap);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'agent_tool_result') {
|
||||
const completedId = ap.data?.toolCallId;
|
||||
if (completedId) streamHook.completeActivityEntry(completedId);
|
||||
if (completedId) completeActivityEntry(completedId);
|
||||
|
||||
if (toolResultRefreshRef.current) clearTimeout(toolResultRefreshRef.current);
|
||||
const capturedSession = currentSessionRef.current;
|
||||
const capturedGeneration = recoveryHook.getGeneration();
|
||||
const capturedGeneration = getGeneration();
|
||||
toolResultRefreshRef.current = setTimeout(async () => {
|
||||
toolResultRefreshRef.current = null;
|
||||
try {
|
||||
const recovered = await loadChatHistory({ rpc, sessionKey: capturedSession, limit: 100 });
|
||||
if (capturedSession !== currentSessionRef.current) return;
|
||||
if (capturedGeneration !== recoveryHook.getGeneration()) return;
|
||||
if (capturedGeneration !== getGeneration()) return;
|
||||
if (recovered.length > 0) {
|
||||
const merged = mergeRecoveredTail(msgHook.getAllMessages(), recovered);
|
||||
msgHook.applyMessageWindow(merged, false);
|
||||
const merged = mergeRecoveredTail(getAllMessages(), recovered);
|
||||
applyMessageWindow(merged, false);
|
||||
}
|
||||
} catch { /* best-effort */ }
|
||||
}, 300);
|
||||
|
|
@ -360,7 +393,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
if (type === 'agent_state' && agentState) {
|
||||
const stage = deriveProcessingStage(agentState);
|
||||
if (stage) streamHook.setProcessingStage(stage);
|
||||
if (stage) setProcessingStage(stage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -385,7 +418,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
const prevRunSeq = run.lastChatSeq;
|
||||
run.lastChatSeq = updateHighestSeq(run.lastChatSeq, classified.chatSeq);
|
||||
|
||||
streamHook.setLastEventTimestamp(Date.now());
|
||||
setLastEventTimestamp(Date.now());
|
||||
|
||||
if (type === 'chat_started') {
|
||||
activeRunIdRef.current = runId;
|
||||
|
|
@ -397,10 +430,10 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
run.bufferText = '';
|
||||
|
||||
setIsGenerating(true);
|
||||
ttsHook.resetPlayedSounds();
|
||||
streamHook.setProcessingStage('thinking');
|
||||
streamHook.setActivityLog([]);
|
||||
streamHook.startThinking(runId);
|
||||
resetPlayedSounds();
|
||||
setProcessingStage('thinking');
|
||||
setActivityLog([]);
|
||||
startThinking(runId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -411,14 +444,14 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
if (!isGeneratingRef.current) setIsGenerating(true);
|
||||
if (!activeRunIdRef.current) activeRunIdRef.current = runId;
|
||||
|
||||
streamHook.captureThinkingDuration();
|
||||
captureThinkingDuration();
|
||||
|
||||
const delta = extractStreamDelta(cp);
|
||||
if (delta) {
|
||||
run.bufferRaw = delta.text;
|
||||
run.bufferText = delta.cleaned;
|
||||
streamHook.scheduleStreamingUpdate(runId, run.bufferText);
|
||||
streamHook.setProcessingStage('streaming');
|
||||
scheduleStreamingUpdate(runId, run.bufferText);
|
||||
setProcessingStage('streaming');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -435,32 +468,32 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
run.bufferText = '';
|
||||
|
||||
if (activeRunIdRef.current === runId) activeRunIdRef.current = null;
|
||||
recoveryHook.incrementGeneration();
|
||||
incrementGeneration();
|
||||
|
||||
if (isActiveRun) {
|
||||
setIsGenerating(false);
|
||||
streamHook.setProcessingStage(null);
|
||||
streamHook.setActivityLog([]);
|
||||
streamHook.setLastEventTimestamp(0);
|
||||
streamHook.clearStreamBuffer();
|
||||
setProcessingStage(null);
|
||||
setActivityLog([]);
|
||||
setLastEventTimestamp(0);
|
||||
clearStreamBuffer();
|
||||
}
|
||||
|
||||
const finalData = extractFinalMessage(cp);
|
||||
const finalMessages = processChatMessages(extractFinalMessages(cp));
|
||||
|
||||
if (finalMessages.length > 0) {
|
||||
const merged = mergeFinalMessages(msgHook.getAllMessages(), finalMessages);
|
||||
const thinkingDuration = streamHook.getThinkingDuration(runId);
|
||||
const merged = mergeFinalMessages(getAllMessages(), finalMessages);
|
||||
const thinkingDuration = getThinkingDuration(runId);
|
||||
const withDuration = thinkingDuration
|
||||
? patchThinkingDuration(merged, thinkingDuration)
|
||||
: merged;
|
||||
msgHook.applyMessageWindow(withDuration, false);
|
||||
applyMessageWindow(withDuration, false);
|
||||
} else {
|
||||
recoveryHook.triggerRecovery('unrenderable-final');
|
||||
triggerRecovery('unrenderable-final');
|
||||
}
|
||||
|
||||
ttsHook.handleFinalTTS(finalData, isActiveRun);
|
||||
streamHook.resetThinking();
|
||||
handleFinalTTS(finalData, isActiveRun);
|
||||
resetThinking();
|
||||
pruneRunRegistry(runsRef.current, activeRunIdRef.current);
|
||||
return;
|
||||
}
|
||||
|
|
@ -477,27 +510,27 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
run.bufferText = '';
|
||||
|
||||
if (activeRunIdRef.current === runId) activeRunIdRef.current = null;
|
||||
recoveryHook.incrementGeneration();
|
||||
incrementGeneration();
|
||||
|
||||
const partialMessagesRaw = extractFinalMessages(cp);
|
||||
if (partialMessagesRaw.length > 0) {
|
||||
const partialMessages = processChatMessages(partialMessagesRaw);
|
||||
if (partialMessages.length > 0) {
|
||||
const merged = mergeFinalMessages(msgHook.getAllMessages(), partialMessages);
|
||||
msgHook.applyMessageWindow(merged, false);
|
||||
const merged = mergeFinalMessages(getAllMessages(), partialMessages);
|
||||
applyMessageWindow(merged, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (isActiveRun) {
|
||||
setIsGenerating(false);
|
||||
streamHook.setProcessingStage(null);
|
||||
streamHook.setActivityLog([]);
|
||||
streamHook.setLastEventTimestamp(0);
|
||||
streamHook.clearStreamBuffer();
|
||||
ttsHook.playCompletionPing();
|
||||
setProcessingStage(null);
|
||||
setActivityLog([]);
|
||||
setLastEventTimestamp(0);
|
||||
clearStreamBuffer();
|
||||
playCompletionPing();
|
||||
}
|
||||
|
||||
streamHook.resetThinking();
|
||||
resetThinking();
|
||||
pruneRunRegistry(runsRef.current, activeRunIdRef.current);
|
||||
return;
|
||||
}
|
||||
|
|
@ -514,44 +547,44 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
run.bufferText = '';
|
||||
|
||||
if (activeRunIdRef.current === runId) activeRunIdRef.current = null;
|
||||
recoveryHook.incrementGeneration();
|
||||
incrementGeneration();
|
||||
|
||||
if (isActiveRun) {
|
||||
setIsGenerating(false);
|
||||
streamHook.setProcessingStage(null);
|
||||
streamHook.setActivityLog([]);
|
||||
streamHook.setLastEventTimestamp(0);
|
||||
streamHook.clearStreamBuffer();
|
||||
setProcessingStage(null);
|
||||
setActivityLog([]);
|
||||
setLastEventTimestamp(0);
|
||||
clearStreamBuffer();
|
||||
}
|
||||
|
||||
if (isActiveRun) {
|
||||
recoveryHook.triggerRecovery('unrenderable-final');
|
||||
triggerRecovery('unrenderable-final');
|
||||
}
|
||||
|
||||
streamHook.resetThinking();
|
||||
resetThinking();
|
||||
pruneRunRegistry(runsRef.current, activeRunIdRef.current);
|
||||
}
|
||||
});
|
||||
}, [
|
||||
msgHook.getAllMessages,
|
||||
msgHook.applyMessageWindow,
|
||||
streamHook.setProcessingStage,
|
||||
streamHook.setLastEventTimestamp,
|
||||
streamHook.setActivityLog,
|
||||
streamHook.addActivityEntry,
|
||||
streamHook.completeActivityEntry,
|
||||
streamHook.startThinking,
|
||||
streamHook.captureThinkingDuration,
|
||||
streamHook.scheduleStreamingUpdate,
|
||||
streamHook.clearStreamBuffer,
|
||||
streamHook.getThinkingDuration,
|
||||
streamHook.resetThinking,
|
||||
recoveryHook.triggerRecovery,
|
||||
recoveryHook.incrementGeneration,
|
||||
recoveryHook.getGeneration,
|
||||
ttsHook.playCompletionPing,
|
||||
ttsHook.resetPlayedSounds,
|
||||
ttsHook.handleFinalTTS,
|
||||
getAllMessages,
|
||||
applyMessageWindow,
|
||||
setProcessingStage,
|
||||
setLastEventTimestamp,
|
||||
setActivityLog,
|
||||
addActivityEntry,
|
||||
completeActivityEntry,
|
||||
startThinking,
|
||||
captureThinkingDuration,
|
||||
scheduleStreamingUpdate,
|
||||
clearStreamBuffer,
|
||||
getThinkingDuration,
|
||||
resetThinking,
|
||||
triggerRecovery,
|
||||
incrementGeneration,
|
||||
getGeneration,
|
||||
playCompletionPing,
|
||||
resetPlayedSounds,
|
||||
handleFinalTTS,
|
||||
subscribe,
|
||||
rpc,
|
||||
]);
|
||||
|
|
@ -562,14 +595,14 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
const { msg: userMsg, tempId } = buildUserMessage({ text, images, uploadPayload });
|
||||
|
||||
recoveryHook.incrementGeneration();
|
||||
incrementGeneration();
|
||||
|
||||
// Optimistic insert (functional updater to avoid read-then-write race)
|
||||
msgHook.setAllMessages(prev => [...prev, userMsg]);
|
||||
msgHook.setMessages((prev: ChatMsg[]) => [...prev, userMsg]);
|
||||
setIsGenerating(true);
|
||||
streamHook.setStream((prev: ChatStreamState) => ({ ...prev, html: '', runId: undefined }));
|
||||
streamHook.setProcessingStage('thinking');
|
||||
setProcessingStage('thinking');
|
||||
|
||||
const idempotencyKey = crypto.randomUUID ? crypto.randomUUID() : 'ik-' + Date.now();
|
||||
try {
|
||||
|
|
@ -587,7 +620,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
run.status = ack.status;
|
||||
run.finalized = false;
|
||||
activeRunIdRef.current = ack.runId;
|
||||
streamHook.startThinking(ack.runId);
|
||||
startThinking(ack.runId);
|
||||
}
|
||||
|
||||
// Confirm the message (functional updater to avoid race after await)
|
||||
|
|
@ -612,7 +645,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
msgHook.setMessages((prev: ChatMsg[]) => [...prev, errMsgBubble]);
|
||||
setIsGenerating(false);
|
||||
}
|
||||
}, [rpc, msgHook, streamHook, ttsHook, recoveryHook]);
|
||||
}, [rpc, msgHook, streamHook, ttsHook, incrementGeneration, setProcessingStage, startThinking]);
|
||||
|
||||
// ─── Abort / Reset ────────────────────────────────────────────────────────
|
||||
const handleAbort = useCallback(async () => {
|
||||
|
|
@ -664,13 +697,13 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
isGenerating,
|
||||
stream: streamHook.stream,
|
||||
processingStage: streamHook.processingStage,
|
||||
lastEventTimestamp: streamHook.lastEventTimestamp,
|
||||
lastEventTimestamp: lastEventTimestamp,
|
||||
activityLog: streamHook.activityLog,
|
||||
currentToolDescription: streamHook.currentToolDescription,
|
||||
handleSend,
|
||||
handleAbort,
|
||||
handleReset,
|
||||
loadHistory: msgHook.loadHistory,
|
||||
loadHistory: loadHistory,
|
||||
loadMore: msgHook.loadMore,
|
||||
hasMore: msgHook.hasMore,
|
||||
showResetConfirm,
|
||||
|
|
@ -681,13 +714,13 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||
isGenerating,
|
||||
streamHook.stream,
|
||||
streamHook.processingStage,
|
||||
streamHook.lastEventTimestamp,
|
||||
lastEventTimestamp,
|
||||
streamHook.activityLog,
|
||||
streamHook.currentToolDescription,
|
||||
handleSend,
|
||||
handleAbort,
|
||||
handleReset,
|
||||
msgHook.loadHistory,
|
||||
loadHistory,
|
||||
msgHook.loadMore,
|
||||
msgHook.hasMore,
|
||||
showResetConfirm,
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ export function useDashboardData(options: DashboardDataOptions = {}): DashboardD
|
|||
clearInterval(memIv);
|
||||
clearInterval(tokIv);
|
||||
};
|
||||
}, [refreshMemories, refreshTokens]);
|
||||
}, [activeAgentId, refreshMemories, refreshTokens]);
|
||||
|
||||
return {
|
||||
memories,
|
||||
|
|
|
|||
Loading…
Reference in a new issue