fix: surface disconnect reason, switch health probe to /global/health (#978)
- event-pipeline: onDisconnect now carries a reason tag (ws_closed:code=N, ws_error_frame, ws_closed_before_ready, sse_error) - useConfigStore: store lastDisconnectReason, clear on reconnect - send guards: embed reason in Connection lost toast so we can tell which path tripped - lifecycle: isOpenCodeProcessHealthy hits /global/health (healthy flag) with 5s timeout instead of /session with 2s, avoids false restarts under stream load
This commit is contained in:
committed by
GitHub
parent
8e86891d8e
commit
5ec7f46105
@@ -34,7 +34,7 @@ export type EventPipelineInput = {
|
||||
/** Called after stream reconnects (visibility restore or heartbeat timeout). */
|
||||
onReconnect?: () => void
|
||||
/** Called when the stream disconnects (heartbeat timeout, network error, or transport failure). */
|
||||
onDisconnect?: () => void
|
||||
onDisconnect?: (reason: string) => void
|
||||
transport?: "auto" | "ws" | "sse"
|
||||
}
|
||||
|
||||
@@ -429,6 +429,7 @@ export function createEventPipeline(input: EventPipelineInput) {
|
||||
|
||||
if (frame.type === "error") {
|
||||
const error = new Error(frame.message || "Message stream WebSocket error")
|
||||
;(error as Error & { reason?: string }).reason = `ws_error_frame:${frame.message || "unknown"}`
|
||||
setFallbackCode(error)
|
||||
settleReject(error)
|
||||
try {
|
||||
@@ -463,13 +464,16 @@ export function createEventPipeline(input: EventPipelineInput) {
|
||||
void 0
|
||||
}
|
||||
|
||||
socket.onclose = () => {
|
||||
socket.onclose = (event) => {
|
||||
if (signal.aborted) {
|
||||
settleResolve()
|
||||
return
|
||||
}
|
||||
|
||||
const error = new Error("Global message stream WebSocket closed")
|
||||
;(error as Error & { reason?: string }).reason = opened
|
||||
? `ws_closed:code=${event?.code ?? "?"}`
|
||||
: "ws_closed_before_ready"
|
||||
setFallbackCode(error)
|
||||
settleReject(error)
|
||||
}
|
||||
@@ -521,7 +525,18 @@ export function createEventPipeline(input: EventPipelineInput) {
|
||||
// setState calls on every failed retry attempt.
|
||||
if (!disconnected) {
|
||||
disconnected = true
|
||||
onDisconnect?.()
|
||||
const taggedReason = typeof error === "object" && error !== null
|
||||
? (error as { reason?: unknown }).reason
|
||||
: undefined
|
||||
const message = typeof error === "object" && error !== null
|
||||
? (error as { message?: unknown }).message
|
||||
: undefined
|
||||
const reason = typeof taggedReason === "string" && taggedReason.length > 0
|
||||
? taggedReason
|
||||
: typeof message === "string" && message.length > 0
|
||||
? `${currentTransport}_error:${message.slice(0, 80)}`
|
||||
: `${currentTransport}_error:unknown`
|
||||
onDisconnect?.(reason)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -54,6 +54,12 @@ function dir() {
|
||||
return _getDirectory() || undefined
|
||||
}
|
||||
|
||||
function connectionLostError(): Error {
|
||||
const reason = useConfigStore.getState().lastDisconnectReason
|
||||
const suffix = reason ? ` (${reason})` : " (never connected)"
|
||||
return new Error(`Connection lost${suffix}. Please wait for reconnection.`)
|
||||
}
|
||||
|
||||
function getSessionDirectory(sessionId: string): string | undefined {
|
||||
return useSessionUIStore.getState().getDirectoryForSession(sessionId) || dir()
|
||||
}
|
||||
@@ -327,7 +333,7 @@ export async function optimisticSend(input: {
|
||||
}
|
||||
|
||||
if (!useConfigStore.getState().isConnected) {
|
||||
throw new Error("Connection lost. Please wait for reconnection.")
|
||||
throw connectionLostError()
|
||||
}
|
||||
|
||||
const store = dirStore()
|
||||
@@ -414,7 +420,7 @@ export async function respondToPermission(
|
||||
response: "once" | "always" | "reject",
|
||||
): Promise<void> {
|
||||
if (!useConfigStore.getState().isConnected) {
|
||||
throw new Error("Connection lost. Please wait for reconnection.")
|
||||
throw connectionLostError()
|
||||
}
|
||||
const result = await getRequestReplyClient("permission", sessionId, requestId).permission.reply({
|
||||
requestID: requestId,
|
||||
@@ -430,7 +436,7 @@ export async function dismissPermission(
|
||||
requestId: string,
|
||||
): Promise<void> {
|
||||
if (!useConfigStore.getState().isConnected) {
|
||||
throw new Error("Connection lost. Please wait for reconnection.")
|
||||
throw connectionLostError()
|
||||
}
|
||||
const result = await getRequestReplyClient("permission", sessionId, requestId).permission.reply({
|
||||
requestID: requestId,
|
||||
@@ -451,7 +457,7 @@ export async function respondToQuestion(
|
||||
answers: string[] | string[][],
|
||||
): Promise<void> {
|
||||
if (!useConfigStore.getState().isConnected) {
|
||||
throw new Error("Connection lost. Please wait for reconnection.")
|
||||
throw connectionLostError()
|
||||
}
|
||||
const result = await getRequestReplyClient("question", sessionId, requestId).question.reply({
|
||||
requestID: requestId,
|
||||
@@ -467,7 +473,7 @@ export async function rejectQuestion(
|
||||
requestId: string,
|
||||
): Promise<void> {
|
||||
if (!useConfigStore.getState().isConnected) {
|
||||
throw new Error("Connection lost. Please wait for reconnection.")
|
||||
throw connectionLostError()
|
||||
}
|
||||
const result = await getRequestReplyClient("question", sessionId, requestId).question.reject({
|
||||
requestID: requestId,
|
||||
|
||||
@@ -1347,7 +1347,7 @@ export function SyncProvider(props: {
|
||||
handleEvent(directory, payload, childStores, routingIndex)
|
||||
},
|
||||
onReconnect: () => {
|
||||
useConfigStore.setState({ isConnected: true })
|
||||
useConfigStore.setState({ isConnected: true, lastDisconnectReason: null })
|
||||
for (const [dir, store] of childStores.children) {
|
||||
if (reconnectResyncing.has(dir)) continue
|
||||
if (getReconnectCandidateSessionIds(store.getState()).length === 0) continue
|
||||
@@ -1362,8 +1362,8 @@ export function SyncProvider(props: {
|
||||
})
|
||||
}
|
||||
},
|
||||
onDisconnect: () => {
|
||||
useConfigStore.setState({ isConnected: false })
|
||||
onDisconnect: (reason) => {
|
||||
useConfigStore.setState({ isConnected: false, lastDisconnectReason: reason })
|
||||
},
|
||||
})
|
||||
return cleanup
|
||||
|
||||
Reference in New Issue
Block a user