Files
oh-my-opencode/src/shared/session-idle-settle.ts
T
YeonGyu-Kim 36468b118d fix(shared): add timeout to isSessionActive to prevent infinite hang
The SDK client disables fetch timeout (req.timeout = false). When the
opencode server is slow or unresponsive, client.session.status() hangs
forever, blocking the entire dispatchToHooks chain (sequential await)
and deadlocking the plugin.

Add a 5s timeout wrapper around the status call in isSessionActive().
On timeout, the catch block returns false (treat as inactive), letting
the caller proceed instead of hanging indefinitely.

The timeout parameter is exposed for testing to avoid 5s test delays.

Refs: .debugging/status-timeout-hang.md
2026-05-17 03:40:17 +09:00

84 lines
2.3 KiB
TypeScript

export const DEFAULT_SESSION_IDLE_SETTLE_MS = 150
export const DEFAULT_SESSION_STATUS_TIMEOUT_MS = 5_000
export function settleAfterSessionIdle(ms = DEFAULT_SESSION_IDLE_SETTLE_MS): Promise<void> {
return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve()
}
function withStatusTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
let timeoutID: ReturnType<typeof setTimeout> | undefined
const timeoutPromise = new Promise<T>((_resolve, reject) => {
timeoutID = setTimeout(() => {
reject(new Error(`session.status() timed out after ${timeoutMs}ms`))
}, timeoutMs)
})
return Promise.race([promise, timeoutPromise]).finally(() => {
if (timeoutID !== undefined) {
clearTimeout(timeoutID)
}
})
}
type SessionStatusClient = {
session?: {
status?: () => Promise<unknown>
}
}
const ACTIVE_SESSION_STATUSES = new Set(["busy", "retry", "running"])
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}
function getSessionStatusPayload(response: unknown): Record<string, unknown> {
if (isRecord(response) && isRecord(response.data)) {
return response.data
}
if (isRecord(response)) {
return response
}
return {}
}
export function isActiveSessionStatusType(statusType: string): boolean {
return ACTIVE_SESSION_STATUSES.has(statusType)
}
export async function isSessionActive(
client: SessionStatusClient,
sessionID: string,
statusTimeoutMs: number = DEFAULT_SESSION_STATUS_TIMEOUT_MS,
): Promise<boolean> {
if (typeof client.session?.status !== "function") {
return false
}
try {
const statusResult = await withStatusTimeout(
client.session.status(),
statusTimeoutMs,
)
const status = getSessionStatusPayload(statusResult)[sessionID]
if (!isRecord(status)) {
return false
}
const statusType = status.type
return typeof statusType === "string" && isActiveSessionStatusType(statusType)
} catch {
return false
}
}
export async function shouldPromptAfterSessionIdle(
client: SessionStatusClient,
sessionID: string,
settleMs = DEFAULT_SESSION_IDLE_SETTLE_MS,
): Promise<boolean> {
await settleAfterSessionIdle(settleMs)
return !(await isSessionActive(client, sessionID))
}