-
Notifications
You must be signed in to change notification settings - Fork 843
fix(web): preserve slash commands on reconnect and add initialize retry logic #1359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -324,6 +324,9 @@ export function useSessionStream( | |
|
|
||
| // Initialize message tracking | ||
| const initializeIdRef = useRef<string | null>(null); | ||
| const initializeRetryCountRef = useRef(0); // Track retry attempts for initialize | ||
| const MAX_INITIALIZE_RETRIES = 5; // Maximum retry attempts | ||
| const usingCachedCommandsRef = useRef(false); // Track if using cached slash commands | ||
|
|
||
| // Current state accumulators | ||
| const currentThinkingRef = useRef(""); | ||
|
|
@@ -776,7 +779,7 @@ export function useSessionStream( | |
| }, []); | ||
|
|
||
| // Reset all state | ||
| const resetState = useCallback(() => { | ||
| const resetState = useCallback((preserveSlashCommands = false) => { | ||
| resetStepState(); | ||
| currentToolCallsRef.current.clear(); | ||
| currentToolCallIdRef.current = null; | ||
|
|
@@ -792,7 +795,6 @@ export function useSessionStream( | |
| isReplayingRef.current = true; | ||
| setIsReplayingHistory(true); | ||
| setAwaitingFirstResponse(false); | ||
| setSlashCommands([]); | ||
| // Reset first turn tracking | ||
| hasTurnStartedRef.current = false; | ||
| firstTurnCompleteCalledRef.current = false; | ||
|
|
@@ -803,7 +805,14 @@ export function useSessionStream( | |
| window.clearTimeout(historyCompleteTimeoutRef.current); | ||
| historyCompleteTimeoutRef.current = null; | ||
| } | ||
| }, [resetStepState, setAwaitingFirstResponse]); | ||
| // Handle slashCommands: preserve or clear | ||
| if (!preserveSlashCommands) { | ||
| setSlashCommands([]); | ||
| usingCachedCommandsRef.current = false; | ||
| } else if (slashCommands.length > 0) { | ||
| usingCachedCommandsRef.current = true; | ||
| } | ||
| }, [resetStepState, setAwaitingFirstResponse, slashCommands.length]); | ||
|
|
||
| // Process a SubagentEvent: accumulate inner events into parent Task tool's subagentSteps | ||
| const processSubagentEvent = useCallback( | ||
|
|
@@ -1805,20 +1814,56 @@ export function useSessionStream( | |
| ], | ||
| ); | ||
|
|
||
| // Helper to send initialize message | ||
| const sendInitialize = useCallback((ws: WebSocket) => { | ||
| const id = uuidV4(); | ||
| initializeIdRef.current = id; | ||
| const message = { | ||
| jsonrpc: "2.0", | ||
| method: "initialize", | ||
| id, | ||
| params: { | ||
| protocol_version: "1.3", | ||
| client: { | ||
| name: "kiwi", | ||
| version: kimiCliVersion, | ||
| }, | ||
| capabilities: { | ||
| supports_question: true, | ||
| }, | ||
| }, | ||
| }; | ||
| ws.send(JSON.stringify(message)); | ||
| console.log("[SessionStream] Sent initialize message"); | ||
| }, []); | ||
|
|
||
| // Handle incoming WebSocket message | ||
| const handleMessage = useCallback( | ||
| (data: string) => { | ||
| try { | ||
| console.log("[SessionStream] Received raw message:", data); | ||
| const message: WireMessage = JSON.parse(data); | ||
| console.log("[SessionStream] Parsed message:", message); | ||
|
|
||
| // Check for JSON-RPC error response | ||
| if (message.error) { | ||
| // Initialize failure during busy session is non-fatal - just skip | ||
| // Initialize failure during busy session is non-fatal - retry after delay | ||
| if (message.id === initializeIdRef.current) { | ||
| console.warn("[SessionStream] Initialize rejected (session busy), continuing..."); | ||
| initializeRetryCountRef.current += 1; | ||
|
|
||
| if (initializeRetryCountRef.current > MAX_INITIALIZE_RETRIES) { | ||
| initializeIdRef.current = null; | ||
| initializeRetryCountRef.current = 0; | ||
| return; | ||
| } | ||
|
|
||
| initializeIdRef.current = null; | ||
|
|
||
| // Auto-retry initialize after 2 seconds | ||
| setTimeout(() => { | ||
| if (wsRef.current?.readyState === WebSocket.OPEN) { | ||
| sendInitialize(wsRef.current); | ||
| } | ||
| }, 2000); | ||
|
Comment on lines
+1861
to
+1865
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Initialize retry The Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| return; | ||
| } | ||
|
|
||
|
|
@@ -1897,13 +1942,14 @@ export function useSessionStream( | |
|
|
||
| // Handle initialize response | ||
| if (message.id && message.id === initializeIdRef.current && message.result) { | ||
| console.log("[SessionStream] Initialize response received:", message.result); | ||
| initializeIdRef.current = null; | ||
| initializeRetryCountRef.current = 0; | ||
|
|
||
| // Extract slash commands | ||
| const { slash_commands } = message.result; | ||
| if (slash_commands) { | ||
|
|
||
| if (slash_commands && slash_commands.length > 0) { | ||
| setSlashCommands(slash_commands); | ||
| usingCachedCommandsRef.current = false; | ||
| } | ||
| return; | ||
| } | ||
|
|
@@ -1961,6 +2007,7 @@ export function useSessionStream( | |
| setAwaitingFirstResponse, | ||
| applySessionStatus, | ||
| completeStreamingMessages, | ||
| sendInitialize, | ||
| ], | ||
| ); | ||
|
|
||
|
|
@@ -2010,29 +2057,6 @@ export function useSessionStream( | |
| [setAwaitingFirstResponse], | ||
| ); | ||
|
|
||
| // Helper to send initialize message | ||
| const sendInitialize = useCallback((ws: WebSocket) => { | ||
| const id = uuidV4(); | ||
| initializeIdRef.current = id; | ||
| const message = { | ||
| jsonrpc: "2.0", | ||
| method: "initialize", | ||
| id, | ||
| params: { | ||
| protocol_version: "1.3", | ||
| client: { | ||
| name: "kiwi", | ||
| version: kimiCliVersion, | ||
| }, | ||
| capabilities: { | ||
| supports_question: true, | ||
| }, | ||
| }, | ||
| }; | ||
| ws.send(JSON.stringify(message)); | ||
| console.log("[SessionStream] Sent initialize message"); | ||
| }, []); | ||
|
|
||
| const respondToApproval = useCallback( | ||
| async ( | ||
| requestId: string, | ||
|
|
@@ -2196,8 +2220,11 @@ export function useSessionStream( | |
| const connect = useCallback(() => { | ||
| if (!sessionId) return; | ||
|
|
||
| initializeRetryCountRef.current = 0; // Reset retry count for new connection | ||
|
|
||
| // Close existing connection | ||
| if (wsRef.current) { | ||
| console.log("[SessionStream] Closing existing WebSocket"); | ||
| wsRef.current.close(); | ||
| wsRef.current = null; | ||
| } | ||
|
|
@@ -2207,7 +2234,7 @@ export function useSessionStream( | |
| } | ||
|
|
||
| awaitingIdleRef.current = false; | ||
| resetState(); | ||
| resetState(true); // preserve slashCommands on reconnect | ||
| setMessages([]); | ||
| setStatus("submitted"); | ||
| setAwaitingFirstResponse(Boolean(pendingMessageRef.current)); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Adding
slashCommands.lengthtoresetStatedeps causes infinite WebSocket reconnect loopThe
resetStatecallback now includesslashCommands.lengthin its dependency array (line 815). SinceresetStateis a direct dependency of theuseLayoutEffectatweb/src/hooks/useSessionStream.ts:2653, any change toslashCommands.lengthrecreatesresetState, which re-triggers the effect. The effect disconnects the current WebSocket (web/src/hooks/useSessionStream.ts:2628-2630), callsresetState()without arguments (defaultingpreserveSlashCommands=false, which clears slash commands back to[]), then reconnects viaconnectRef.current()(line 2641). On reconnect, the initialize response sets slash commands again (setSlashCommands(slash_commands)at line 1951), changingslashCommands.lengthfrom 0→N, which recreatesresetState, re-fires the effect, and the cycle repeats infinitely.Cycle trace
setSlashCommands(commands)(length 0→N)slashCommands.lengthchanges →resetStateidentity changesuseLayoutEffectdepresetStatechanged → effect re-runsdisconnectRef.current()(line 2629) → WebSocket closedresetState()(no args →preserveSlashCommands=false) →setSlashCommands([])(length N→0)connectRef.current()(line 2641) → new WebSocketPrompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.