mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(native-chat): contain the WSL-gate advisory emit so a throwing subscriber can't kill the poll
The unresolved-transcript notice was already contained, but its sibling emit in runAttempt's catch was not: a throw there escaped `void runAttempt()` as an unhandled rejection and skipped the reschedule below it, stranding the poll for good. Route both through one emitAdvisory helper.
This commit is contained in:
@@ -198,6 +198,29 @@ describe('native chat transcript resolve polling', () => {
|
||||
subscription.unsubscribe()
|
||||
})
|
||||
|
||||
// Sibling of the advisory case: the gate's emit runs inside runAttempt's catch,
|
||||
// so a throw there escapes `void runAttempt()` and skips the reschedule below it.
|
||||
it('keeps polling when the WSL-gate subscriber throws', async () => {
|
||||
mocks.resolve.mockRejectedValue(new WslTranscriptFsError('unavailable', 'stuck permits'))
|
||||
const subscription = await subscribeNativeChatTranscript({
|
||||
agent: 'claude',
|
||||
sessionId: 'session-id',
|
||||
resolvePollIntervalMs: 10,
|
||||
unresolvedNoticeMs: 10_000,
|
||||
onAppend: () => {},
|
||||
onInitialSnapshot: () => {
|
||||
throw new Error('subscriber blew up')
|
||||
}
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
const resolvesAtGateError = mocks.resolve.mock.calls.length
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(mocks.resolve.mock.calls.length).toBeGreaterThan(resolvesAtGateError)
|
||||
|
||||
subscription.unsubscribe()
|
||||
})
|
||||
|
||||
it('still fails subscribe on non-gate resolver errors', async () => {
|
||||
mocks.resolve.mockRejectedValueOnce(new Error('resolver crashed'))
|
||||
|
||||
|
||||
@@ -100,6 +100,17 @@ function subscribeViaResolvePoll(
|
||||
const unresolvedNoticeMs = args.unresolvedNoticeMs ?? UNRESOLVED_NOTICE_MS
|
||||
const resolveController = new AbortController()
|
||||
|
||||
/** Both advisories run under `void runAttempt()`, so a subscriber that throws
|
||||
* (an emit on a dying RPC connection) would surface as an unhandled rejection
|
||||
* and skip the rest of the attempt — including its reschedule. Contain it. */
|
||||
function emitAdvisory(message: string): void {
|
||||
try {
|
||||
args.onInitialSnapshot?.([], false, 0, message)
|
||||
} catch {
|
||||
// Advisory only: keep polling so a later real snapshot still replaces it.
|
||||
}
|
||||
}
|
||||
|
||||
/** Non-terminal: the poll keeps running and a later real snapshot replaces
|
||||
* this, so a slow-to-flush session still recovers (#8401). */
|
||||
function emitUnresolvedNotice(): void {
|
||||
@@ -110,12 +121,7 @@ function subscribeViaResolvePoll(
|
||||
return
|
||||
}
|
||||
unresolvedNoticeEmitted = true
|
||||
try {
|
||||
args.onInitialSnapshot([], false, 0, UNRESOLVED_TRANSCRIPT_MESSAGE)
|
||||
} catch {
|
||||
// A subscriber that throws (an emit on a dying RPC connection) must not
|
||||
// escape `void runAttempt()` as an unhandled rejection.
|
||||
}
|
||||
emitAdvisory(UNRESOLVED_TRANSCRIPT_MESSAGE)
|
||||
}
|
||||
|
||||
function scheduleAttempt(): void {
|
||||
@@ -189,7 +195,7 @@ function subscribeViaResolvePoll(
|
||||
// runAttempt is invoked as `void runAttempt()`.
|
||||
if (error instanceof WslTranscriptFsError && !gateErrorEmitted && args.onInitialSnapshot) {
|
||||
gateErrorEmitted = true
|
||||
args.onInitialSnapshot([], false, 0, error.message)
|
||||
emitAdvisory(error.message)
|
||||
}
|
||||
result = null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user