36468b118d
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
84 lines
2.3 KiB
TypeScript
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))
|
|
}
|