{"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\nimport {\n  attendanceOf,\n  attendanceReason,\n  firstFrameTimeoutFor,\n  PENDING_NOTICE_MS,\n  ProtocolTimeoutError,\n  STREAM_IDLE_TIMEOUT_MS,\n  type BoundedCallOptions,\n} from './protocolDeadline';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n  | { kind: 'event'; value: unknown }\n  | { kind: 'done'; value: unknown }\n  | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n  // Fire the request that starts the stream. The host replies with frames tagged\n  // by the same `msgId`.\n  send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n  // Subscribe to inbound frames for `type`; returns an unsubscribe.\n  subscribe: (type: string, handler: (msg: { msgId?: number; stream?: StreamFrame }) => void) => () => void;\n  // Tell the host to STOP the stream early — abort the in-flight generation (and,\n  // for `llm:chat`, the upstream provider fetch so it stops BILLING). Sent when the\n  // consumer bails before a terminal frame: an early `break`/`return` out of the\n  // `for await`, or an `AbortSignal` firing. Carries the same `(type, msgId)` the\n  // host tagged its frames with, so the host aborts the matching generator\n  // (LLM_AND_AGENTS_SPEC §3.3 \"abort the in-flight LLM request\"). Optional — a\n  // transport predating the cancel frame simply omits it and the old behavior\n  // (host runs to completion) stands.\n  cancel?: (msg: { type: string; msgId: number; cancel: true }) => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n  code: string;\n  constructor(code: string, message: string) {\n    super(message);\n    this.name = 'StreamError';\n    this.code = code;\n  }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n  // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n  // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n  // reply (which has `result`, not `stream`) can't be misread.\n  streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n  return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n *\n * `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it\n * fires (or the consumer `break`s before a terminal frame), the `finally` sends a\n * `cancel` frame back over `transport.cancel` so the HOST stops generating — without\n * it, aborting only stops the app-side iterator while the upstream provider keeps\n * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n  transport: StreamTransport,\n  type: string,\n  method: string,\n  params: unknown[],\n  msgId: number = nextMsgId(),\n  signal?: AbortSignal,\n  opts?: BoundedCallOptions & { idleTimeoutMs?: number },\n): AsyncGenerator<T, R, void> {\n  const queue: StreamFrame[] = [];\n  let wake: (() => void) | null = null;\n  // True once a terminal (`done`/`error`) frame arrived — so the `finally` knows the\n  // host already stopped and must NOT send a redundant cancel. Left false when the\n  // consumer bails early or the signal aborts (the two cases that DO need a cancel).\n  let settled = false;\n  // True once the request frame went out. A cancel is only meaningful for a stream\n  // the host actually started — an abort BEFORE `send` sends nothing to cancel.\n  let started = false;\n  const push = (frame: StreamFrame) => {\n    queue.push(frame);\n    const w = wake;\n    wake = null;\n    w?.();\n  };\n\n  const unsubscribe = transport.subscribe(type, (msg) => {\n    if (msg.msgId !== msgId || !msg.stream) return;\n    push(msg.stream);\n  });\n\n  // An abort wakes the pull loop out of its idle `await`; the loop then throws.\n  const onAbort = () => {\n    const w = wake;\n    wake = null;\n    w?.();\n  };\n  if (signal) signal.addEventListener('abort', onAbort);\n\n  // R3-298 — a stream is bounded by SILENCE, not by duration.\n  //\n  // A total-duration deadline would be wrong here: a long generation that is streaming\n  // normally is healthy, and aborting it would be a worse bug than the hang. The hang has a\n  // distinct shape — no frames at all — so that is what is bounded: a time-to-FIRST-FRAME\n  // deadline, then an idle-gap deadline once frames are flowing. Together they fire exactly\n  // on a wedged stream and never on a slow one.\n  //\n  // The scheme's `type` is `protocol-llm`; the classification table is keyed on the bare\n  // scheme, so strip the prefix. The chat stream is classified ATTENDED because its first\n  // frame can sit behind the session's first passkey unseal — the dogfood hang.\n  const scheme = type.startsWith('protocol-') ? type.slice('protocol-'.length) : type;\n  const call = `${scheme}:${method}`;\n  const attendance = attendanceOf(scheme, method);\n  const firstFrameMs = opts?.timeoutMs ?? firstFrameTimeoutFor(scheme, method);\n  const idleMs = opts?.idleTimeoutMs ?? STREAM_IDLE_TIMEOUT_MS;\n  let sawFrame = false;\n  let noticed = false;\n\n  /** Wait for the next frame, bounded. Rejects with a typed timeout rather than parking. */\n  const awaitFrame = async (): Promise<void> => {\n    const budget = sawFrame ? idleMs : firstFrameMs;\n    if (!Number.isFinite(budget)) {\n      await new Promise<void>((resolve) => {\n        wake = resolve;\n      });\n      return;\n    }\n    const startedAt = Date.now();\n    let deadline: ReturnType<typeof setTimeout> | undefined;\n    let notice: ReturnType<typeof setTimeout> | undefined;\n    try {\n      await new Promise<void>((resolve, reject) => {\n        wake = resolve;\n        deadline = setTimeout(() => reject(new ProtocolTimeoutError(call, budget, attendance)), budget);\n        if (opts?.onPending && !noticed) {\n          notice = setTimeout(() => {\n            noticed = true;\n            try {\n              opts.onPending?.({\n                call,\n                attendance,\n                elapsedMs: Date.now() - startedAt,\n                ...(attendanceReason(scheme, method) ? { reason: attendanceReason(scheme, method) as string } : {}),\n              });\n            } catch {\n              /* a caller's render callback must never break the stream it describes */\n            }\n          }, PENDING_NOTICE_MS);\n        }\n      });\n    } finally {\n      if (deadline !== undefined) clearTimeout(deadline);\n      if (notice !== undefined) clearTimeout(notice);\n      wake = null;\n    }\n  };\n\n  try {\n    if (signal?.aborted) throw new StreamError('aborted', 'stream aborted before start');\n    transport.send({ type, method, params, msgId, stream: true });\n    started = true;\n    while (true) {\n      if (signal?.aborted) throw new StreamError('aborted', 'stream aborted');\n      if (queue.length === 0) {\n        // A timeout throws out of here into the `finally`, which sends the host a real\n        // cancel frame — so a wedged stream stops generating and BILLING rather than being\n        // merely abandoned by the app. Streams can do this because they own their `msgId`.\n        await awaitFrame();\n        continue;\n      }\n      sawFrame = true;\n      const frame = queue.shift() as StreamFrame;\n      if (frame.kind === 'event') {\n        yield frame.value as T;\n      } else if (frame.kind === 'done') {\n        settled = true;\n        return frame.value as R;\n      } else {\n        settled = true;\n        throw new StreamError(frame.code, frame.message);\n      }\n    }\n  } finally {\n    unsubscribe();\n    if (signal) signal.removeEventListener('abort', onAbort);\n    // Consumer stopped pulling before a terminal frame (early break/return, or the\n    // signal aborted): tell the host to stop the in-flight generation + billing.\n    if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });\n  }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers. `cancel`\n// rides the same messageBus as `send` — a `{type, msgId, cancel:true}` frame the\n// host dispatcher routes to the in-flight generator's AbortController.\nconst bundlerTransport: StreamTransport = {\n  send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n  subscribe: (type, handler) => addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n  cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n *\n * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.\n */\nexport function protocolStream<T = unknown, R = unknown>(\n  protocolName: string,\n  method: string,\n  params: unknown[],\n  signal?: AbortSignal,\n  opts?: BoundedCallOptions & { idleTimeoutMs?: number },\n): AsyncGenerator<T, R, void> {\n  return consumeStream<T, R>(bundlerTransport, protocolName, method, params, undefined, signal, opts);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,0BAAyC;AACzC,8BAQO;AA2BA,MAAM,oBAAoB,MAAM;AAAA,EAErC,YAAY,MAAc,SAAiB;AACzC,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAI,gBAAgB;AACpB,MAAM,YAAY,MAAc;AAI9B,mBAAiB,gBAAgB,KAAK,OAAO;AAC7C,SAAO;AACT;AAeA,gBAAuB,cACrB,WACA,MACA,QACA,QACA,QAAgB,UAAU,GAC1B,QACA,MAC4B;AAC5B,QAAM,QAAuB,CAAC;AAC9B,MAAI,OAA4B;AAIhC,MAAI,UAAU;AAGd,MAAI,UAAU;AACd,QAAM,OAAO,CAAC,UAAuB;AACnC,UAAM,KAAK,KAAK;AAChB,UAAM,IAAI;AACV,WAAO;AACP,QAAI;AAAA,EACN;AAEA,QAAM,cAAc,UAAU,UAAU,MAAM,CAAC,QAAQ;AACrD,QAAI,IAAI,UAAU,SAAS,CAAC,IAAI,OAAQ;AACxC,SAAK,IAAI,MAAM;AAAA,EACjB,CAAC;AAGD,QAAM,UAAU,MAAM;AACpB,UAAM,IAAI;AACV,WAAO;AACP,QAAI;AAAA,EACN;AACA,MAAI,OAAQ,QAAO,iBAAiB,SAAS,OAAO;AAapD,QAAM,SAAS,KAAK,WAAW,WAAW,IAAI,KAAK,MAAM,YAAY,MAAM,IAAI;AAC/E,QAAM,OAAO,GAAG,MAAM,IAAI,MAAM;AAChC,QAAM,iBAAa,sCAAa,QAAQ,MAAM;AAC9C,QAAM,eAAe,MAAM,iBAAa,8CAAqB,QAAQ,MAAM;AAC3E,QAAM,SAAS,MAAM,iBAAiB;AACtC,MAAI,WAAW;AACf,MAAI,UAAU;AAGd,QAAM,aAAa,YAA2B;AAC5C,UAAM,SAAS,WAAW,SAAS;AACnC,QAAI,CAAC,OAAO,SAAS,MAAM,GAAG;AAC5B,YAAM,IAAI,QAAc,CAAC,YAAY;AACnC,eAAO;AAAA,MACT,CAAC;AACD;AAAA,IACF;AACA,UAAM,YAAY,KAAK,IAAI;AAC3B,QAAI;AACJ,QAAI;AACJ,QAAI;AACF,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,eAAO;AACP,mBAAW,WAAW,MAAM,OAAO,IAAI,6CAAqB,MAAM,QAAQ,UAAU,CAAC,GAAG,MAAM;AAC9F,YAAI,MAAM,aAAa,CAAC,SAAS;AAC/B,mBAAS,WAAW,MAAM;AACxB,sBAAU;AACV,gBAAI;AACF,mBAAK,YAAY;AAAA,gBACf;AAAA,gBACA;AAAA,gBACA,WAAW,KAAK,IAAI,IAAI;AAAA,gBACxB,OAAI,0CAAiB,QAAQ,MAAM,IAAI,EAAE,YAAQ,0CAAiB,QAAQ,MAAM,EAAY,IAAI,CAAC;AAAA,cACnG,CAAC;AAAA,YACH,QAAQ;AAAA,YAER;AAAA,UACF,GAAG,yCAAiB;AAAA,QACtB;AAAA,MACF,CAAC;AAAA,IACH,UAAE;AACA,UAAI,aAAa,OAAW,cAAa,QAAQ;AACjD,UAAI,WAAW,OAAW,cAAa,MAAM;AAC7C,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI;AACF,QAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,WAAW,6BAA6B;AACnF,cAAU,KAAK,EAAE,MAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAC5D,cAAU;AACV,WAAO,MAAM;AACX,UAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,WAAW,gBAAgB;AACtE,UAAI,MAAM,WAAW,GAAG;AAItB,cAAM,WAAW;AACjB;AAAA,MACF;AACA,iBAAW;AACX,YAAM,QAAQ,MAAM,MAAM;AAC1B,UAAI,MAAM,SAAS,SAAS;AAC1B,cAAM,MAAM;AAAA,MACd,WAAW,MAAM,SAAS,QAAQ;AAChC,kBAAU;AACV,eAAO,MAAM;AAAA,MACf,OAAO;AACL,kBAAU;AACV,cAAM,IAAI,YAAY,MAAM,MAAM,MAAM,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF,UAAE;AACA,gBAAY;AACZ,QAAI,OAAQ,QAAO,oBAAoB,SAAS,OAAO;AAGvD,QAAI,WAAW,CAAC,QAAS,WAAU,SAAS,EAAE,MAAM,OAAO,QAAQ,KAAK,CAAC;AAAA,EAC3E;AACF;AAKA,MAAM,mBAAoC;AAAA,EACxC,MAAM,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,gBAAY,iCAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AAAA,EACjH,QAAQ,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAClF;AASO,SAAS,eACd,cACA,QACA,QACA,QACA,MAC4B;AAC5B,SAAO,cAAoB,kBAAkB,cAAc,QAAQ,QAAQ,QAAW,QAAQ,IAAI;AACpG;","names":[]}