@@ -51,14 +51,23 @@ function resolveTimeoutMs(model: unknown, fallbackMs: number): number {
5151 return validTimeoutMs ( ( model as { requestTimeoutMs ?: unknown } ) ?. requestTimeoutMs ) ?? fallbackMs ;
5252}
5353
54- function isProviderProgressEvent ( event : AssistantMessageEvent ) : boolean {
54+ function isProviderDeltaEvent ( event : AssistantMessageEvent ) : boolean {
5555 return (
5656 event . type === "text_delta" ||
5757 event . type === "thinking_delta" ||
5858 event . type === "toolcall_delta"
5959 ) ;
6060}
6161
62+ function isProviderBoundaryLivenessEvent ( event : AssistantMessageEvent ) : boolean {
63+ return (
64+ event . type === "text_end" ||
65+ event . type === "thinking_end" ||
66+ event . type === "toolcall_start" ||
67+ event . type === "toolcall_end"
68+ ) ;
69+ }
70+
6271function combineAbortSignals ( signals : ( AbortSignal | undefined ) [ ] ) : {
6372 signal : AbortSignal ;
6473 cleanup ( ) : void ;
@@ -200,8 +209,8 @@ function synthesizeMinimalAssistantMessage(
200209 *
201210 * Behavior:
202211 * - Provider-scoped: only applies when `model.provider === options.provider`.
203- * - Idle-based: the timer covers stream creation, first event delivery , and
204- * every gap after provider progress begins; if no event arrives within
212+ * - Idle-based: the timer covers stream creation, first provider delta , and
213+ * every gap after provider progress begins; if no live event arrives within
205214 * `idleTimeoutMs`, the wrapper calls `controller.abort()` on the AbortSignal
206215 * injected into the underlying call (so the OpenAI SDK request is genuinely
207216 * interrupted, not just the iterator) and pushes a terminal `error` event
@@ -210,7 +219,8 @@ function synthesizeMinimalAssistantMessage(
210219 * wrapper forwards the event, clears all timers, and ends the stream.
211220 *
212221 * The wrapper never shortens the natural end of a normal completion, because
213- * provider progress refreshes the idle timer and a terminal event cancels it entirely.
222+ * provider deltas and later block boundaries refresh the idle timer and a terminal event cancels
223+ * it entirely.
214224 */
215225export function createOpencodeGoStalledStreamWrapper (
216226 underlying : ProviderStreamFn ,
@@ -246,6 +256,7 @@ export function createOpencodeGoStalledStreamWrapper(
246256 let idleTimer : ReturnType < typeof setTimeout > | undefined ;
247257 let lastSeenPartial : AssistantMessage | undefined ;
248258 let settled = false ;
259+ let sawProviderDelta = false ;
249260 let baseIterator : AsyncIterator < AssistantMessageEvent > | undefined ;
250261
251262 const clearIdleTimer = ( ) => {
@@ -353,7 +364,10 @@ export function createOpencodeGoStalledStreamWrapper(
353364 }
354365 trackPartial ( event ) ;
355366 output . push ( event ) ;
356- if ( isProviderProgressEvent ( event ) ) {
367+ if ( isProviderDeltaEvent ( event ) ) {
368+ sawProviderDelta = true ;
369+ armIdleTimer ( ) ;
370+ } else if ( sawProviderDelta && isProviderBoundaryLivenessEvent ( event ) ) {
357371 armIdleTimer ( ) ;
358372 }
359373 }
0 commit comments