fix(prompt-async-gate): timeout isSessionActive to prevent infinite hang on stale SDK status

- Wrap isSessionActive in withDispatchTimeout (capped at 5s) so a
  stuck OpenCode SDK status() call cannot block internal prompts forever.
- Catch the timeout and treat session as inactive so the prompt can
  proceed rather than hanging indefinitely.
- Add regression test: session.status that never resolves now times out
  and allows dispatch instead of hanging the test (and production).

Refs: AGENTS.md internal-message-injection safety note
This commit is contained in:
YeonGyu-Kim
2026-05-17 03:41:02 +09:00
parent 169e61f775
commit 2613de522f
2 changed files with 41 additions and 1 deletions
@@ -476,6 +476,34 @@ describe("promptAsyncAfterSessionIdle", () => {
expect(promptCalls).toBe(1)
})
test("#given session.status never resolves #when promptAsync is requested #then isSessionActive times out and dispatch is attempted", async () => {
// given
let promptCalls = 0
const client = {
session: {
status: async () => new Promise(() => {}),
promptAsync: async () => {
promptCalls += 1
},
},
}
// when
const result = await promptAsyncAfterSessionIdle({
client,
sessionID: "ses_status_hang",
input: { path: { id: "ses_status_hang" }, body: { parts: [] } },
source: "test:status-hang",
settleMs: 0,
postDispatchHoldMs: 0,
dispatchTimeoutMs: 50,
})
// then
expect(result.status).toBe("dispatched")
expect(promptCalls).toBe(1)
}, 2000)
test("#given SDK prompt depends on its session receiver #when the gate dispatches #then method binding is preserved", async () => {
// given
const session = {
+13 -1
View File
@@ -169,7 +169,19 @@ async function dispatchAfterSessionIdle<TInput>(args: {
await settleAfterSessionIdle(settleMs)
}
if (canReadStatus && await isSessionActive(client, sessionID)) {
let sessionActive = false
if (canReadStatus) {
try {
sessionActive = await withDispatchTimeout(
isSessionActive(client, sessionID),
Math.min(dispatchTimeoutMs, 5000),
`[prompt-async-gate] ${sessionName} isSessionActive`,
)
} catch {
sessionActive = false
}
}
if (sessionActive) {
log(`[prompt-async-gate] ${sessionName} skipped because session is active`, { sessionID, source })
return { status: "active" }
}