/**
 * Creates a managed stream that wraps the source stream with lifecycle management.
 *
 * The managed stream:
 * - Emits "chunk" events for each chunk
 * - Throws errors from error chunks
 * - Races against idle monitor to detect stalled streams
 * - Finalizes the session on complete or cancel (but NOT on error - see below)
 *
 * IMPORTANT: This stream intentionally does NOT finalize the session on errors.
 * If you call session.finalize("error") here, the session becomes non-pending,
 * which means new managed streams created during retry will immediately close
 * (see the `!session.isPending()` check in pull).
 *
 * The retry logic in wrapWithRetry is responsible for finalizing the session
 * when all retries are exhausted. Don't be a hero and add finalization here.
 *
 * @param source - The source async iterable stream
 * @param session - The session to update with events
 * @param idleMonitor - Idle monitor to detect stalled streams
 * @returns A managed stream with lifecycle hooks
 */
export function createManagedStream(source, session, idleMonitor) {
    const iterator = source[Symbol.asyncIterator]();
    let closed = false;
    const readable = new ReadableStream({
        async pull(controller) {
            if (closed || !session.isPending()) {
                controller.close();
                return;
            }
            // Loop over raw chunks (provider SSE keep-alives / message metadata):
            // these exist only to feed IdleMonitor during thinking silences so the
            // watchdog doesn't stall a healthy stream. For each raw chunk we:
            //   - call `session.resetActivityTimer()` so the retry engine's
            //     max-time check (retry-engine.ts:112, vs session.getTimeSinceLast-
            //     Activity()) treats keep-alives as progress. Without this, a long
            //     thinking block + transient error would refuse retries with
            //     reason="max-time" even though the stream was alive.
            //   - emit the `chunk` event so IdleMonitor's timer resets.
            //   - skip `recordChunk()` so `metrics.chunkCount` / `timeToFirst-
            //     TokenMs` stay content-meaningful, not keep-alive noise.
            //   - drop the value at this boundary so downstream consumers never
            //     see it. ReadableStream's pull contract requires we enqueue or
            //     close before returning, so we keep draining until a non-raw
            //     chunk arrives. See APPS-3460.
            while (true) {
                try {
                    const result = await raceWithStallDetection(iterator.next(), idleMonitor);
                    if (result.done) {
                        closed = true;
                        session.finalize("complete");
                        controller.close();
                        return;
                    }
                    if (result.value.type === "raw") {
                        session.resetActivityTimer();
                        void session.events.emit("chunk", result.value, session);
                        continue;
                    }
                    session.recordChunk();
                    void session.events.emit("chunk", result.value, session);
                    if (result.value.type === "error") {
                        const chunkError = result.value
                            .error;
                        closed = true;
                        // don't finalize here, wrapWithRetry catches this and decides whether to retry
                        controller.error(chunkError);
                        return;
                    }
                    controller.enqueue(result.value);
                    return;
                }
                catch (error) {
                    closed = true;
                    // no finalization, let wrapWithRetry handle it
                    controller.error(error);
                    return;
                }
            }
        },
        cancel() {
            if (!closed) {
                closed = true;
                if (session.isPending()) {
                    session.finalize("aborted");
                }
            }
            void iterator.return?.();
        },
    });
    return readable;
}
/**
 * Races a promise against stall detection. Rejects with StallTimeoutError if stall fires first.
 */
async function raceWithStallDetection(promise, idleMonitor) {
    const existingError = idleMonitor.getStallError();
    if (existingError) {
        throw existingError;
    }
    return Promise.race([promise, idleMonitor.waitForStall()]);
}
//# sourceMappingURL=managed-stream.js.map