re-adopt orphaned chat sessions on reconnect
restarting officer under a live turn left the browser connected but permanently silent. the sidecars are pm2 peers, so the agent kept generating and kept committing to chat_session_events — what died was officer's binding to it. on `resume-cursor` the server only re-attached the socket when an in-memory session still existed, so after a restart there was no session and, critically, no session-scoped subscription relaying sidecar events to the client. the client got its durable replay and then nothing, which reads exactly like the agent stopping. adopt the session instead: recreate the record and re-open the subscription without spawning anything. `_claudeKill` has to be set as part of that — handleChat treats its absence as "first turn" and would open a second subscription, doubling every message. the client now echoes the model and cwd from its session:init back in the handshake, since after a restart it is the only party that still remembers them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -84,6 +84,10 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
|
||||
const streamingRef = useRef('');
|
||||
const rafRef = useRef<number | null>(null);
|
||||
const sessionIdRef = useRef<string | null>(initialSessionId ?? null);
|
||||
// Mirrors of the session:init fields, for the reconnect handshake — `onOpen` is stable by design and
|
||||
// cannot close over the state.
|
||||
const modelRef = useRef<string | null>(null);
|
||||
const cwdRef = useRef<string | null>(null);
|
||||
const saveTimerRef = useRef<number | null>(null);
|
||||
const toolCallsInTurnRef = useRef(false);
|
||||
const onTurnCompleteRef = useRef(onTurnComplete);
|
||||
@@ -216,6 +220,8 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
|
||||
switch (msg.type) {
|
||||
case 'session:init':
|
||||
sessionIdRef.current = msg.sessionId;
|
||||
modelRef.current = msg.model;
|
||||
cwdRef.current = msg.cwd;
|
||||
setSessionId(msg.sessionId);
|
||||
setModel(msg.model);
|
||||
setCwd(msg.cwd);
|
||||
@@ -367,9 +373,19 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
|
||||
|
||||
// On every (re)connect, if a session is already established, re-bind + replay via resume-cursor.
|
||||
// The first connect (no session yet) no-ops; the first turn establishes the session via session:init.
|
||||
// `model`/`cwd` come back from the session:init this client already holds. After an officer restart the
|
||||
// server has no memory of either, and it needs both to re-adopt the session rather than leave it
|
||||
// orphaned — so the client, which is now the only party that remembers, hands them back.
|
||||
const onOpen = useCallback(() => {
|
||||
const sid = sessionIdRef.current;
|
||||
if (sid) sendRef.current({ type: 'resume-cursor', sessionId: sid, cursor: cursorRef.current });
|
||||
if (!sid) return;
|
||||
sendRef.current({
|
||||
type: 'resume-cursor',
|
||||
sessionId: sid,
|
||||
cursor: cursorRef.current,
|
||||
...(modelRef.current ? { model: modelRef.current } : {}),
|
||||
...(cwdRef.current ? { cwd: cwdRef.current } : {}),
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { isConnected, send } = useChatWebSocket({ url: wsUrl, onMessage: handleMessage, onOpen });
|
||||
|
||||
Reference in New Issue
Block a user