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
This commit is contained in:
YeonGyu-Kim
2026-05-17 03:40:17 +09:00
parent 169e61f775
commit 36468b118d
4 changed files with 168 additions and 2 deletions
@@ -505,4 +505,32 @@ describe("promptAsyncAfterSessionIdle", () => {
response: { accepted: true, sessionID: "ses_bound_prompt" },
})
})
test("#given session.status hangs forever #when promptAsync gate checks activity #then it dispatches after status timeout", { timeout: 10_000 }, async () => {
// given
let promptCalls = 0
const neverSettles = new Promise<never>(() => {})
const client = {
session: {
status: () => neverSettles,
promptAsync: async () => {
promptCalls += 1
},
},
}
// when
const result = await promptAsyncAfterSessionIdle({
client,
sessionID: "ses_status_timeout",
input: { path: { id: "ses_status_timeout" }, body: { parts: [] } },
source: "test:status-timeout",
settleMs: 0,
postDispatchHoldMs: 0,
})
// then
expect(result.status).toBe("dispatched")
expect(promptCalls).toBe(1)
})
})
@@ -52,4 +52,20 @@ describe("session idle prompt guard", () => {
// then
expect(shouldPrompt).toBe(true)
})
test("#given session.status hangs forever #when checking active session #then it returns false after timeout", async () => {
// given
const neverSettles = new Promise<never>(() => {})
const client = {
session: {
status: () => neverSettles,
},
}
// when
const active = await isSessionActive(client, "ses-hang", 50)
// then
expect(active).toBe(false)
})
})