github-router 0.3.35 → 0.3.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -2892,8 +2892,28 @@ function buildInstallRequired(reason, autoInstalled) {
2892
2892
  * model. Side effect: when reason is `extension_not_loaded`, attempts
2893
2893
  * to install the NMH manifest for every detected browser so that the
2894
2894
  * extension can connect immediately on load.
2895
+ *
2896
+ * Single-flight: concurrent calls share one in-flight Promise so that
2897
+ * `installNativeHostForAll` (which writes files and spawns reg.exe on
2898
+ * Windows) is called exactly once per check cycle, regardless of how
2899
+ * many browser_* tool calls arrive concurrently.
2900
+ */
2901
+ let _inFlightReady;
2902
+ /**
2903
+ * @internal — counts how many times _ensureBridgeReadyImpl has started.
2904
+ * Used by regression tests for the single-flight property (Bug #6).
2905
+ * Always 0 in production (only incremented when imported by tests).
2895
2906
  */
2907
+ let __implInvocationsForTests = 0;
2896
2908
  async function ensureBridgeReady() {
2909
+ if (_inFlightReady) return _inFlightReady;
2910
+ _inFlightReady = _ensureBridgeReadyImpl().finally(() => {
2911
+ _inFlightReady = void 0;
2912
+ });
2913
+ return _inFlightReady;
2914
+ }
2915
+ async function _ensureBridgeReadyImpl() {
2916
+ __implInvocationsForTests++;
2897
2917
  const browsers = detectSupportedBrowsers();
2898
2918
  if (browsers.length === 0) return buildInstallRequired("no_supported_browser", []);
2899
2919
  if (!bridgeBundleExists()) return buildInstallRequired("bridge_bundle_missing", []);
@@ -3035,10 +3055,11 @@ async function bridgeCall(endpoint, tool, args, timeoutMs, signal) {
3035
3055
  const id = randomUUID();
3036
3056
  const ws = new WebSocket(`ws://127.0.0.1:${endpoint.port}`, { headers: { authorization: `Bearer ${endpoint.token}` } });
3037
3057
  let settled = false;
3058
+ let timer = void 0;
3038
3059
  const finish = (fn) => {
3039
3060
  if (settled) return;
3040
3061
  settled = true;
3041
- clearTimeout(timer);
3062
+ if (timer !== void 0) clearTimeout(timer);
3042
3063
  if (signal) signal.removeEventListener("abort", onAbort);
3043
3064
  try {
3044
3065
  ws.close();
@@ -3053,8 +3074,14 @@ async function bridgeCall(endpoint, tool, args, timeoutMs, signal) {
3053
3074
  }
3054
3075
  signal.addEventListener("abort", onAbort, { once: true });
3055
3076
  }
3056
- const timer = setTimeout(() => finish(() => reject(/* @__PURE__ */ new Error(`timeout after ${timeoutMs}ms`))), timeoutMs);
3077
+ timer = setTimeout(() => finish(() => reject(/* @__PURE__ */ new Error(`timeout after ${timeoutMs}ms`))), timeoutMs);
3057
3078
  ws.on("open", () => {
3079
+ if (settled) {
3080
+ try {
3081
+ ws.close();
3082
+ } catch {}
3083
+ return;
3084
+ }
3058
3085
  ws.send(JSON.stringify({
3059
3086
  id,
3060
3087
  tool,
@@ -5155,6 +5182,117 @@ async function acquireWorkerSlot(signal) {
5155
5182
  };
5156
5183
  }
5157
5184
 
5185
+ //#endregion
5186
+ //#region src/lib/diagnose-response.ts
5187
+ const PREVIEW_LIMIT = 200;
5188
+ async function parseJsonOrDiagnose(response, routePath) {
5189
+ const cloned = response.clone();
5190
+ try {
5191
+ return await response.json();
5192
+ } catch (error) {
5193
+ const contentType = response.headers.get("content-type") ?? "(none)";
5194
+ const bodyText = await cloned.text().catch(() => "(unreadable)");
5195
+ const preview = bodyText.length > PREVIEW_LIMIT ? bodyText.slice(0, PREVIEW_LIMIT) + "...(truncated)" : bodyText;
5196
+ consola.error(`Upstream JSON parse failed at ${routePath}: status=${response.status} content-type="${contentType}" body[0..${PREVIEW_LIMIT}]=${JSON.stringify(preview)}`);
5197
+ throw error;
5198
+ }
5199
+ }
5200
+
5201
+ //#endregion
5202
+ //#region src/lib/response-cap.ts
5203
+ /**
5204
+ * Hard byte cap for non-streaming upstream response bodies.
5205
+ *
5206
+ * Anthropic responses with large tool_use blocks can legitimately reach
5207
+ * several MB, but a multi-GB body is either a buggy upstream or a malicious
5208
+ * one. Buffering it would OOM the proxy and crash all in-flight requests.
5209
+ *
5210
+ * Applies to /v1/messages, /v1/chat/completions, and /v1/responses.
5211
+ */
5212
+ const MAX_RESPONSE_BODY_BYTES = 10 * 1024 * 1024;
5213
+ /**
5214
+ * Read a Response body with a hard byte cap, then parse as JSON.
5215
+ *
5216
+ * Falls back to the fast path (response.json()) when Content-Length is
5217
+ * present and within the cap, avoiding the streaming-reader overhead for
5218
+ * the vast majority of normal responses.
5219
+ *
5220
+ * When the cap is hit:
5221
+ * - the reader is cancelled to release the upstream socket
5222
+ * - a structured Anthropic-format error is returned to the caller
5223
+ * (the caller wraps it in c.json(), not throws — the client gets a
5224
+ * clean 413 error, not an unhandled-rejection crash)
5225
+ *
5226
+ * Returns `{ ok: true, value }` on success or `{ ok: false, errorResponse, status }`
5227
+ * on cap exceeded.
5228
+ */
5229
+ async function readResponseBodyCapped(response, routePath, capBytes = MAX_RESPONSE_BODY_BYTES) {
5230
+ const contentLengthHeader = response.headers.get("content-length");
5231
+ const contentLength = contentLengthHeader ? parseInt(contentLengthHeader, 10) : NaN;
5232
+ if (!isNaN(contentLength) && contentLength <= capBytes) return {
5233
+ ok: true,
5234
+ value: await parseJsonOrDiagnose(response, routePath)
5235
+ };
5236
+ const reader = response.body?.getReader();
5237
+ if (!reader) return {
5238
+ ok: true,
5239
+ value: await parseJsonOrDiagnose(response, routePath)
5240
+ };
5241
+ const chunks = [];
5242
+ let totalBytes = 0;
5243
+ let capped = false;
5244
+ try {
5245
+ while (true) {
5246
+ const { done, value } = await reader.read();
5247
+ if (done) break;
5248
+ if (!value) continue;
5249
+ totalBytes += value.byteLength;
5250
+ if (totalBytes > capBytes) {
5251
+ capped = true;
5252
+ try {
5253
+ await reader.cancel("size_cap");
5254
+ } catch {}
5255
+ break;
5256
+ }
5257
+ chunks.push(value);
5258
+ }
5259
+ } catch (err) {
5260
+ if (!capped) consola.warn(`readResponseBodyCapped: read error at ${routePath}:`, err);
5261
+ }
5262
+ if (capped) {
5263
+ consola.warn(`Non-streaming upstream response at ${routePath} exceeded ${capBytes} bytes (10 MiB cap); dropping body to prevent OOM. Check upstream health.`);
5264
+ return {
5265
+ ok: false,
5266
+ status: 502,
5267
+ errorResponse: {
5268
+ type: "error",
5269
+ error: {
5270
+ type: "api_error",
5271
+ message: `Upstream response body exceeded the 10 MiB size cap for non-streaming ${routePath}. The upstream may be misbehaving. Try enabling streaming (stream: true) which handles large responses chunk-by-chunk.`
5272
+ }
5273
+ }
5274
+ };
5275
+ }
5276
+ const merged = new Uint8Array(totalBytes);
5277
+ let offset = 0;
5278
+ for (const chunk of chunks) {
5279
+ merged.set(chunk, offset);
5280
+ offset += chunk.byteLength;
5281
+ }
5282
+ const text = new TextDecoder().decode(merged);
5283
+ try {
5284
+ return {
5285
+ ok: true,
5286
+ value: JSON.parse(text)
5287
+ };
5288
+ } catch (err) {
5289
+ const preview = text.slice(0, 200);
5290
+ const contentType = response.headers.get("content-type") ?? "(none)";
5291
+ consola.error(`Upstream JSON parse failed at ${routePath}: status=${response.status} content-type="${contentType}" body[0..200]=${JSON.stringify(preview)}`);
5292
+ throw err;
5293
+ }
5294
+ }
5295
+
5158
5296
  //#endregion
5159
5297
  //#region src/services/copilot/create-chat-completions.ts
5160
5298
  const createChatCompletions = async (payload, modelHeaders, callerSignal) => {
@@ -5196,7 +5334,12 @@ const createChatCompletions = async (payload, modelHeaders, callerSignal) => {
5196
5334
  }));
5197
5335
  }
5198
5336
  if (payload.stream) return events(response);
5199
- return await response.json();
5337
+ const cappedResult = await readResponseBodyCapped(response, "/v1/chat/completions", MAX_RESPONSE_BODY_BYTES);
5338
+ if (!cappedResult.ok) throw new HTTPError("Upstream /v1/chat/completions response exceeded 10 MiB size cap", new Response(JSON.stringify(cappedResult.errorResponse), {
5339
+ status: cappedResult.status,
5340
+ headers: { "content-type": "application/json" }
5341
+ }));
5342
+ return cappedResult.value;
5200
5343
  };
5201
5344
 
5202
5345
  //#endregion
@@ -5857,7 +6000,12 @@ const createResponses = async (payload, modelHeaders, callerSignal) => {
5857
6000
  throw new HTTPError("Failed to create responses", response);
5858
6001
  }
5859
6002
  if (payload.stream) return events(response);
5860
- return await response.json();
6003
+ const cappedResult = await readResponseBodyCapped(response, "/v1/responses", MAX_RESPONSE_BODY_BYTES);
6004
+ if (!cappedResult.ok) throw new HTTPError("Upstream /v1/responses response exceeded 10 MiB size cap", new Response(JSON.stringify(cappedResult.errorResponse), {
6005
+ status: cappedResult.status,
6006
+ headers: { "content-type": "application/json" }
6007
+ }));
6008
+ return cappedResult.value;
5861
6009
  };
5862
6010
  function detectVision(input) {
5863
6011
  if (typeof input === "string") return false;
@@ -5882,40 +6030,25 @@ function detectAgentCall(input) {
5882
6030
  const MCP_PROTOCOL_VERSION = "2025-06-18";
5883
6031
  const SERVER_NAME = "github-router-peers";
5884
6032
  const SERVER_VERSION = "1";
5885
- /** Bounded concurrency. Originally capped at 2 (commit 4317a25) as a defensive
5886
- * pre-launch guess against Opus's natural pattern of fanning out to all three
5887
- * critics at once. Raised to 8 (Phase 2D of the peer-MCP plan) so the
5888
- * decomposition pattern Phase 2B teaches Opus — "split a >20 KB artifact
5889
- * into 2-4 batches and call in parallel" — can actually run in parallel
5890
- * without the (3+)th call returning isError "queue full". The persona
5891
- * handlers (`callPersona`) hold no shared mutable state — there's no race
5892
- * the cap is hiding; the upstream Copilot's own rate-limit (surfaced as a
5893
- * per-call 429 → tool isError) is the real backpressure mechanism. 8 covers
5894
- * a 7-fork wave with one slot of headroom and is still a hard upper bound
5895
- * against runaway clients. See docs/research/peer-mcp-investigation.md
5896
- * § "Concurrency cap investigation" for the full justification.
5897
- *
5898
- * The counter itself lives in `src/lib/mcp-inflight.ts` so the
5899
- * worker-agent's nested `peer_review` / `advisor` tools share the
5900
- * same budget — otherwise a worker could fan out unboundedly to
5901
- * peers without showing up in the MCP-side cap. */
5902
- /**
5903
- * Per-request AbortController registry for `notifications/cancelled`
5904
- * (Phase D P1.5). When a client times out a tools/call before the
5905
- * upstream Copilot fetch completes, the JSON-RPC notification:
5906
- * { jsonrpc:"2.0", method:"notifications/cancelled",
5907
- * params:{ requestId: "<id>", reason?: "..." } }
5908
- * arrives. Without handling, the upstream fetch keeps running until
5909
- * natural completion, leaking the inFlightToolsCall slot for tens of
5910
- * minutes. Tracking the AbortController lets us abort the fetch and
5911
- * free the slot immediately.
5912
- *
5913
- * Important: per CLAUDE.md "Bun request-signal quirk", we use OUR own
5914
- * AbortController (NOT c.req.raw.signal which fires after request body
5915
- * is consumed). The signal is threaded into createResponses /
5916
- * createChatCompletions's `callerSignal` parameter.
5917
- */
5918
6033
  const inflightAborts = /* @__PURE__ */ new Map();
6034
+ /**
6035
+ * Idempotent teardown for an in-flight tools/call. Aborts the upstream
6036
+ * fetch, frees the concurrency slot, and removes the registry entry.
6037
+ * Safe to call from both `notifications/cancelled` and the SSE
6038
+ * `ReadableStream.cancel()` callback, in either order, and any number
6039
+ * of times — the second call is a no-op.
6040
+ */
6041
+ function cancelInflight(key, reason) {
6042
+ const entry = inflightAborts.get(key);
6043
+ if (!entry) return;
6044
+ inflightAborts.delete(key);
6045
+ try {
6046
+ entry.aborter.abort(new Error(reason));
6047
+ } catch {}
6048
+ try {
6049
+ entry.release();
6050
+ } catch {}
6051
+ }
5919
6052
  const RPC_PARSE_ERROR = -32700;
5920
6053
  const RPC_INVALID_REQUEST = -32600;
5921
6054
  const RPC_METHOD_NOT_FOUND = -32601;
@@ -6388,9 +6521,14 @@ async function handleToolsCall(body) {
6388
6521
  const startedAt = Date.now();
6389
6522
  const abortKey = body.id !== void 0 && body.id !== null ? body.id : void 0;
6390
6523
  let aborter;
6524
+ let inflightEntry;
6391
6525
  if (abortKey !== void 0) {
6392
6526
  aborter = new AbortController();
6393
- inflightAborts.set(abortKey, aborter);
6527
+ inflightEntry = {
6528
+ aborter,
6529
+ release
6530
+ };
6531
+ inflightAborts.set(abortKey, inflightEntry);
6394
6532
  }
6395
6533
  const telemetryName = persona ? persona.agentName : nonPersonaTool.toolNameHttp;
6396
6534
  const telemetryModel = persona ? persona.model : "(non-persona)";
@@ -6421,7 +6559,9 @@ async function handleToolsCall(body) {
6421
6559
  });
6422
6560
  } finally {
6423
6561
  release();
6424
- if (abortKey !== void 0) inflightAborts.delete(abortKey);
6562
+ if (abortKey !== void 0 && inflightEntry !== void 0) {
6563
+ if (inflightAborts.get(abortKey) === inflightEntry) inflightAborts.delete(abortKey);
6564
+ }
6425
6565
  }
6426
6566
  }
6427
6567
  /**
@@ -6436,9 +6576,7 @@ function handleCancelledNotification(body) {
6436
6576
  consola.debug(`[mcp] notifications/cancelled missing or invalid requestId: ${JSON.stringify(requestId)}`);
6437
6577
  return;
6438
6578
  }
6439
- const aborter = inflightAborts.get(requestId);
6440
- if (!aborter) return;
6441
- aborter.abort(/* @__PURE__ */ new Error("client requested cancellation"));
6579
+ cancelInflight(requestId, "client requested cancellation");
6442
6580
  }
6443
6581
  async function handleRpc(_c, body) {
6444
6582
  if (body === null || typeof body !== "object" || Array.isArray(body)) return {
@@ -6637,6 +6775,7 @@ const SSE_HEARTBEAT_INTERVAL_MS = 5e3;
6637
6775
  async function handleToolsCallSSE(body) {
6638
6776
  const encoder = new TextEncoder();
6639
6777
  const callPromise = handleToolsCall(body);
6778
+ let heartbeatHandle;
6640
6779
  const stream = new ReadableStream({
6641
6780
  async start(controller) {
6642
6781
  let closed = false;
@@ -6669,23 +6808,27 @@ async function handleToolsCallSSE(body) {
6669
6808
  }
6670
6809
  });
6671
6810
  safeEnqueue(heartbeatFrame());
6672
- const heartbeatHandle = setInterval(() => safeEnqueue(heartbeatFrame()), SSE_HEARTBEAT_INTERVAL_MS);
6811
+ heartbeatHandle = setInterval(() => safeEnqueue(heartbeatFrame()), SSE_HEARTBEAT_INTERVAL_MS);
6673
6812
  try {
6674
6813
  safeEnqueue(sseFrame(await callPromise));
6675
6814
  } catch (err) {
6676
6815
  consola.error("/mcp SSE upstream error:", err);
6677
6816
  safeEnqueue(sseFrame(rpcError(body.id ?? null, RPC_INTERNAL_ERROR, err instanceof Error ? err.message : String(err))));
6678
6817
  } finally {
6679
- clearInterval(heartbeatHandle);
6818
+ if (heartbeatHandle !== void 0) {
6819
+ clearInterval(heartbeatHandle);
6820
+ heartbeatHandle = void 0;
6821
+ }
6680
6822
  safeClose();
6681
6823
  }
6682
6824
  },
6683
6825
  cancel() {
6684
- const abortKey = body.id !== void 0 && body.id !== null ? body.id : void 0;
6685
- if (abortKey !== void 0) {
6686
- const aborter = inflightAborts.get(abortKey);
6687
- if (aborter) aborter.abort(/* @__PURE__ */ new Error("client disconnected SSE stream"));
6826
+ if (heartbeatHandle !== void 0) {
6827
+ clearInterval(heartbeatHandle);
6828
+ heartbeatHandle = void 0;
6688
6829
  }
6830
+ const abortKey = body.id !== void 0 && body.id !== null ? body.id : void 0;
6831
+ if (abortKey !== void 0) cancelInflight(abortKey, "client disconnected SSE stream");
6689
6832
  }
6690
6833
  });
6691
6834
  return new Response(stream, {
@@ -6826,6 +6969,34 @@ async function readWithInactivityTimeout(reader, timeoutMs) {
6826
6969
  }
6827
6970
  }
6828
6971
  /**
6972
+ * Race an `AsyncIterableIterator.next()` call against an inactivity timeout.
6973
+ *
6974
+ * Follows the same pattern as `readWithInactivityTimeout` (including the
6975
+ * noop catcher to avoid Node 24 unhandled-rejection crashes) but works
6976
+ * with typed iterators that yield parsed objects rather than raw bytes.
6977
+ *
6978
+ * On timeout, throws an `InactivityTimeout` error (same classification as
6979
+ * the byte-reader variant — surfaced to the consumer as `timeout_error` via
6980
+ * `buildOpenAIErrorEvent`).
6981
+ *
6982
+ * @param iterator - An AsyncIterableIterator whose `.next()` we want to race.
6983
+ * @param timeoutMs - Milliseconds before the timeout fires.
6984
+ */
6985
+ async function readIteratorWithTimeout(iterator, timeoutMs) {
6986
+ let timeoutHandle;
6987
+ const timeoutPromise = new Promise((_, reject) => {
6988
+ timeoutHandle = setTimeout(() => {
6989
+ reject(Object.assign(/* @__PURE__ */ new Error("upstream_inactive"), { name: "InactivityTimeout" }));
6990
+ }, timeoutMs);
6991
+ });
6992
+ timeoutPromise.catch(() => {});
6993
+ try {
6994
+ return await Promise.race([iterator.next(), timeoutPromise]);
6995
+ } finally {
6996
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
6997
+ }
6998
+ }
6999
+ /**
6829
7000
  * Build the SSE wire bytes for an Anthropic-format streaming error event.
6830
7001
  * Per Anthropic streaming spec, errors are sent as:
6831
7002
  * event: error
@@ -7047,7 +7218,8 @@ function renderConversationAsText(conversation, maxChars = ADVISOR_MAX_CONVERSAT
7047
7218
  * Anthropic's own ADVISOR ("see the whole task + every tool call +
7048
7219
  * every result").
7049
7220
  */
7050
- async function runAdvisor(conversation, advisorModel, advisorEffort) {
7221
+ async function runAdvisor(conversation, advisorModel, advisorEffort, signal) {
7222
+ if (signal?.aborted) throw new Error("advisor call aborted before dispatch");
7051
7223
  const advisorSystem = "You are an expert advisor reviewing an in-progress Claude Code session. The transcript below is the work-in-progress (turns numbered, with tool calls and results inlined). Read carefully and provide concrete, actionable advice on the next step or course-correction. Be specific — cite the parts of the transcript you're responding to. If the assistant is on the right track, say so explicitly. If they're stuck or off-track, name the specific assumption or step to revisit. Aim for 2-5 paragraphs of substantive guidance.";
7052
7224
  const conversationText = renderConversationAsText(conversation);
7053
7225
  const resolvedAdvisorModel = resolveModel(advisorModel);
@@ -7064,7 +7236,7 @@ async function runAdvisor(conversation, advisorModel, advisorEffort) {
7064
7236
  }],
7065
7237
  stream: false,
7066
7238
  reasoning: { effort: advisorEffort }
7067
- });
7239
+ }, void 0, signal);
7068
7240
  const out = [];
7069
7241
  for (const item of response.output) {
7070
7242
  if (typeof item !== "object" || item === null) continue;
@@ -7091,7 +7263,7 @@ async function runAdvisor(conversation, advisorModel, advisorEffort) {
7091
7263
  content: conversationText
7092
7264
  }],
7093
7265
  stream: false
7094
- }), {})).json();
7266
+ }), {}, signal)).json();
7095
7267
  const text = (Array.isArray(json.content) ? json.content : []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n\n");
7096
7268
  if (!text) throw new Error(`Advisor model ${resolvedAdvisorModel} returned empty response`);
7097
7269
  return text;
@@ -7140,284 +7312,305 @@ function sseEvent(type, data) {
7140
7312
  function buildAdvisorStream(opts) {
7141
7313
  const advisorModel = opts.advisorModel ?? ADVISOR_DEFAULT_MODEL;
7142
7314
  const advisorEffort = opts.advisorEffort ?? ADVISOR_DEFAULT_EFFORT;
7143
- return new ReadableStream({ async start(controller) {
7144
- const conversation = [...opts.initialConversation];
7145
- let messageStartForwarded = false;
7146
- let nextSyntheticIndex = 0;
7147
- let turnsRun = 0;
7148
- const safeEnqueue = (bytes) => {
7149
- try {
7150
- controller.enqueue(bytes);
7151
- return true;
7152
- } catch (err) {
7153
- if (isControllerClosedError(err)) return false;
7154
- throw err;
7155
- }
7156
- };
7157
- const safeEnqueueEvent = (type, data) => safeEnqueue(ENCODER$2.encode(sseEvent(type, data)));
7158
- async function processOneTurn(response) {
7159
- const capturedBlocks = [];
7160
- let advisorToolUse = null;
7161
- const indexToBlock = /* @__PURE__ */ new Map();
7162
- for await (const ev of events(response)) {
7163
- if (!ev.event || !ev.data) continue;
7164
- let payload;
7315
+ const aborter = opts.externalAborter ?? new AbortController();
7316
+ let conversation = [...opts.initialConversation];
7317
+ return new ReadableStream({
7318
+ async start(controller) {
7319
+ let messageStartForwarded = false;
7320
+ let nextSyntheticIndex = 0;
7321
+ let turnsRun = 0;
7322
+ const safeEnqueue = (bytes) => {
7165
7323
  try {
7166
- payload = JSON.parse(ev.data);
7167
- } catch {
7168
- if (!safeEnqueue(ENCODER$2.encode(`event: ${ev.event}\ndata: ${ev.data}\n\n`))) return {
7169
- capturedBlocks,
7170
- advisorToolUse
7171
- };
7172
- continue;
7324
+ controller.enqueue(bytes);
7325
+ return true;
7326
+ } catch (err) {
7327
+ if (isControllerClosedError(err)) {
7328
+ if (!aborter.signal.aborted) aborter.abort(/* @__PURE__ */ new Error("advisor stream consumer disconnected"));
7329
+ return false;
7330
+ }
7331
+ throw err;
7173
7332
  }
7174
- switch (ev.event) {
7175
- case "message_start":
7176
- if (!messageStartForwarded) {
7177
- if (!safeEnqueueEvent(ev.event, payload)) return {
7178
- capturedBlocks,
7179
- advisorToolUse
7180
- };
7181
- messageStartForwarded = true;
7182
- }
7333
+ };
7334
+ const safeEnqueueEvent = (type, data) => safeEnqueue(ENCODER$2.encode(sseEvent(type, data)));
7335
+ async function processOneTurn(response) {
7336
+ const capturedBlocks = [];
7337
+ let advisorToolUse = null;
7338
+ const indexToBlock = /* @__PURE__ */ new Map();
7339
+ for await (const ev of events(response)) {
7340
+ if (!ev.event || !ev.data) continue;
7341
+ let payload;
7342
+ try {
7343
+ payload = JSON.parse(ev.data);
7344
+ } catch {
7345
+ if (!safeEnqueue(ENCODER$2.encode(`event: ${ev.event}\ndata: ${ev.data}\n\n`))) return {
7346
+ capturedBlocks,
7347
+ advisorToolUse
7348
+ };
7183
7349
  continue;
7184
- case "content_block_start": {
7185
- const block = payload.content_block;
7186
- const upstreamIndex = payload.index;
7187
- if (block && upstreamIndex !== void 0) {
7188
- const myIndex = nextSyntheticIndex++;
7189
- if (block.type === "tool_use" && block.name === ADVISOR_INTERNAL_TOOL_NAME) {
7190
- const id = typeof block.id === "string" ? block.id : `toolu_advisor_${myIndex}`;
7191
- advisorToolUse = {
7192
- index: myIndex,
7193
- id,
7194
- clientId: toClientServerToolUseId(id, myIndex),
7195
- inputJson: ""
7196
- };
7197
- const translated = {
7198
- ...payload,
7199
- index: myIndex,
7200
- content_block: {
7201
- type: "server_tool_use",
7202
- id: advisorToolUse.clientId,
7203
- name: ADVISOR_CLIENT_TOOL_NAME,
7204
- input: {}
7205
- }
7206
- };
7207
- if (!safeEnqueueEvent(ev.event, translated)) return {
7350
+ }
7351
+ switch (ev.event) {
7352
+ case "message_start":
7353
+ if (!messageStartForwarded) {
7354
+ if (!safeEnqueueEvent(ev.event, payload)) return {
7208
7355
  capturedBlocks,
7209
7356
  advisorToolUse
7210
7357
  };
7211
- const captured = {
7212
- block: {
7213
- type: "tool_use",
7358
+ messageStartForwarded = true;
7359
+ }
7360
+ continue;
7361
+ case "content_block_start": {
7362
+ const block = payload.content_block;
7363
+ const upstreamIndex = payload.index;
7364
+ if (block && upstreamIndex !== void 0) {
7365
+ const myIndex = nextSyntheticIndex++;
7366
+ if (block.type === "tool_use" && block.name === ADVISOR_INTERNAL_TOOL_NAME) {
7367
+ const id = typeof block.id === "string" ? block.id : `toolu_advisor_${myIndex}`;
7368
+ advisorToolUse = {
7369
+ index: myIndex,
7214
7370
  id,
7215
- name: ADVISOR_INTERNAL_TOOL_NAME,
7216
- input: {}
7217
- },
7218
- partialJson: "",
7219
- advisorReplay: { id }
7220
- };
7221
- capturedBlocks.push(captured);
7222
- indexToBlock.set(upstreamIndex, captured);
7223
- } else {
7371
+ clientId: toClientServerToolUseId(id, myIndex),
7372
+ inputJson: ""
7373
+ };
7374
+ const translated = {
7375
+ ...payload,
7376
+ index: myIndex,
7377
+ content_block: {
7378
+ type: "server_tool_use",
7379
+ id: advisorToolUse.clientId,
7380
+ name: ADVISOR_CLIENT_TOOL_NAME,
7381
+ input: {}
7382
+ }
7383
+ };
7384
+ if (!safeEnqueueEvent(ev.event, translated)) return {
7385
+ capturedBlocks,
7386
+ advisorToolUse
7387
+ };
7388
+ const captured = {
7389
+ block: {
7390
+ type: "tool_use",
7391
+ id,
7392
+ name: ADVISOR_INTERNAL_TOOL_NAME,
7393
+ input: {}
7394
+ },
7395
+ partialJson: "",
7396
+ advisorReplay: { id }
7397
+ };
7398
+ capturedBlocks.push(captured);
7399
+ indexToBlock.set(upstreamIndex, captured);
7400
+ } else {
7401
+ const reindexed = {
7402
+ ...payload,
7403
+ index: myIndex
7404
+ };
7405
+ if (!safeEnqueueEvent(ev.event, reindexed)) return {
7406
+ capturedBlocks,
7407
+ advisorToolUse
7408
+ };
7409
+ const captured = {
7410
+ block: { ...block },
7411
+ partialJson: ""
7412
+ };
7413
+ capturedBlocks.push(captured);
7414
+ indexToBlock.set(upstreamIndex, captured);
7415
+ }
7416
+ }
7417
+ continue;
7418
+ }
7419
+ case "content_block_delta": {
7420
+ const upstreamIndex = payload.index;
7421
+ const delta = payload.delta;
7422
+ if (upstreamIndex !== void 0) {
7423
+ const captured = upstreamIndex !== void 0 ? indexToBlock.get(upstreamIndex) : void 0;
7224
7424
  const reindexed = {
7225
7425
  ...payload,
7226
- index: myIndex
7426
+ index: captured ? capturedBlocks.indexOf(captured) >= 0 ? nextSyntheticIndex - capturedBlocks.length + capturedBlocks.indexOf(captured) : upstreamIndex : upstreamIndex
7227
7427
  };
7228
7428
  if (!safeEnqueueEvent(ev.event, reindexed)) return {
7229
7429
  capturedBlocks,
7230
7430
  advisorToolUse
7231
7431
  };
7232
- const captured = {
7233
- block: { ...block },
7234
- partialJson: ""
7235
- };
7236
- capturedBlocks.push(captured);
7237
- indexToBlock.set(upstreamIndex, captured);
7238
- }
7432
+ if (captured && delta) {
7433
+ if (delta.type === "text_delta" && typeof delta.text === "string") captured.block.text = (captured.block.text ?? "") + delta.text;
7434
+ else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") captured.block.thinking = (captured.block.thinking ?? "") + delta.thinking;
7435
+ else if (delta.type === "signature_delta" && typeof delta.signature === "string") captured.block.signature = (captured.block.signature ?? "") + delta.signature;
7436
+ else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") captured.partialJson += delta.partial_json;
7437
+ else if (delta.type === "citations_delta" && delta.citation) {
7438
+ if (!Array.isArray(captured.block.citations)) captured.block.citations = [];
7439
+ captured.block.citations.push(delta.citation);
7440
+ }
7441
+ }
7442
+ } else if (!safeEnqueueEvent(ev.event, payload)) return {
7443
+ capturedBlocks,
7444
+ advisorToolUse
7445
+ };
7446
+ continue;
7239
7447
  }
7240
- continue;
7241
- }
7242
- case "content_block_delta": {
7243
- const upstreamIndex = payload.index;
7244
- const delta = payload.delta;
7245
- if (upstreamIndex !== void 0) {
7448
+ case "content_block_stop": {
7449
+ const upstreamIndex = payload.index;
7246
7450
  const captured = upstreamIndex !== void 0 ? indexToBlock.get(upstreamIndex) : void 0;
7247
7451
  const reindexed = {
7248
7452
  ...payload,
7249
- index: captured ? capturedBlocks.indexOf(captured) >= 0 ? nextSyntheticIndex - capturedBlocks.length + capturedBlocks.indexOf(captured) : upstreamIndex : upstreamIndex
7453
+ index: captured ? nextSyntheticIndex - capturedBlocks.length + capturedBlocks.indexOf(captured) : upstreamIndex ?? 0
7250
7454
  };
7251
7455
  if (!safeEnqueueEvent(ev.event, reindexed)) return {
7252
7456
  capturedBlocks,
7253
7457
  advisorToolUse
7254
7458
  };
7255
- if (captured && delta) {
7256
- if (delta.type === "text_delta" && typeof delta.text === "string") captured.block.text = (captured.block.text ?? "") + delta.text;
7257
- else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") captured.block.thinking = (captured.block.thinking ?? "") + delta.thinking;
7258
- else if (delta.type === "signature_delta" && typeof delta.signature === "string") captured.block.signature = (captured.block.signature ?? "") + delta.signature;
7259
- else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") captured.partialJson += delta.partial_json;
7260
- else if (delta.type === "citations_delta" && delta.citation) {
7261
- if (!Array.isArray(captured.block.citations)) captured.block.citations = [];
7262
- captured.block.citations.push(delta.citation);
7459
+ if (captured) {
7460
+ if (captured.block.type === "tool_use" && captured.partialJson.length > 0) try {
7461
+ captured.block.input = JSON.parse(captured.partialJson);
7462
+ } catch (err) {
7463
+ consola.warn(`advisor: malformed input_json_delta for tool_use id=${captured.block.id ?? "?"} name=${captured.block.name ?? "?"} partialJson.length=${captured.partialJson.length} parseError=${err instanceof Error ? err.message : String(err)}`);
7464
+ captured.block.input = {};
7263
7465
  }
7466
+ if (captured.block.type === "text" && (typeof captured.block.text !== "string" || captured.block.text.length === 0)) captured.dropFromReplay = true;
7264
7467
  }
7265
- } else if (!safeEnqueueEvent(ev.event, payload)) return {
7266
- capturedBlocks,
7267
- advisorToolUse
7268
- };
7269
- continue;
7270
- }
7271
- case "content_block_stop": {
7272
- const upstreamIndex = payload.index;
7273
- const captured = upstreamIndex !== void 0 ? indexToBlock.get(upstreamIndex) : void 0;
7274
- const reindexed = {
7275
- ...payload,
7276
- index: captured ? nextSyntheticIndex - capturedBlocks.length + capturedBlocks.indexOf(captured) : upstreamIndex ?? 0
7277
- };
7278
- if (!safeEnqueueEvent(ev.event, reindexed)) return {
7279
- capturedBlocks,
7280
- advisorToolUse
7281
- };
7282
- if (captured) {
7283
- if (captured.block.type === "tool_use" && captured.partialJson.length > 0) try {
7284
- captured.block.input = JSON.parse(captured.partialJson);
7285
- } catch (err) {
7286
- consola.warn(`advisor: malformed input_json_delta for tool_use id=${captured.block.id ?? "?"} name=${captured.block.name ?? "?"} partialJson.length=${captured.partialJson.length} parseError=${err instanceof Error ? err.message : String(err)}`);
7287
- captured.block.input = {};
7288
- }
7289
- if (captured.block.type === "text" && (typeof captured.block.text !== "string" || captured.block.text.length === 0)) captured.dropFromReplay = true;
7468
+ continue;
7290
7469
  }
7291
- continue;
7292
- }
7293
- case "message_delta":
7294
- if (!safeEnqueueEvent(ev.event, payload)) return {
7295
- capturedBlocks,
7296
- advisorToolUse
7297
- };
7298
- continue;
7299
- case "message_stop":
7300
- if (advisorToolUse) return {
7301
- capturedBlocks,
7302
- advisorToolUse
7303
- };
7304
- if (!safeEnqueueEvent(ev.event, payload)) return {
7305
- capturedBlocks,
7306
- advisorToolUse
7307
- };
7308
- return {
7470
+ case "message_delta":
7471
+ if (!safeEnqueueEvent(ev.event, payload)) return {
7472
+ capturedBlocks,
7473
+ advisorToolUse
7474
+ };
7475
+ continue;
7476
+ case "message_stop":
7477
+ if (advisorToolUse) return {
7478
+ capturedBlocks,
7479
+ advisorToolUse
7480
+ };
7481
+ if (!safeEnqueueEvent(ev.event, payload)) return {
7482
+ capturedBlocks,
7483
+ advisorToolUse
7484
+ };
7485
+ return {
7486
+ capturedBlocks,
7487
+ advisorToolUse
7488
+ };
7489
+ default: if (!safeEnqueueEvent(ev.event, payload)) return {
7309
7490
  capturedBlocks,
7310
7491
  advisorToolUse
7311
7492
  };
7312
- default: if (!safeEnqueueEvent(ev.event, payload)) return {
7313
- capturedBlocks,
7314
- advisorToolUse
7315
- };
7493
+ }
7316
7494
  }
7495
+ return {
7496
+ capturedBlocks,
7497
+ advisorToolUse
7498
+ };
7317
7499
  }
7318
- return {
7319
- capturedBlocks,
7320
- advisorToolUse
7321
- };
7322
- }
7323
- try {
7324
- let response = opts.firstResponse;
7325
- for (turnsRun = 0; turnsRun < ADVISOR_MAX_TURNS; turnsRun++) {
7326
- const { capturedBlocks, advisorToolUse } = await processOneTurn(response);
7327
- if (!advisorToolUse) return;
7328
- const assistantTurn = {
7329
- role: "assistant",
7330
- content: capturedBlocks.filter((c) => !c.dropFromReplay).map((c) => {
7331
- if (c.advisorReplay) {
7332
- const input = typeof c.block.input === "object" && c.block.input !== null ? c.block.input : {};
7333
- return {
7334
- type: "tool_use",
7335
- id: c.advisorReplay.id,
7336
- name: ADVISOR_INTERNAL_TOOL_NAME,
7337
- input
7338
- };
7500
+ try {
7501
+ let response = opts.firstResponse;
7502
+ for (turnsRun = 0; turnsRun < ADVISOR_MAX_TURNS; turnsRun++) {
7503
+ if (aborter.signal.aborted) return;
7504
+ if (conversation === null) return;
7505
+ const { capturedBlocks, advisorToolUse } = await processOneTurn(response);
7506
+ if (!advisorToolUse) return;
7507
+ if (aborter.signal.aborted) return;
7508
+ if (conversation === null) return;
7509
+ const assistantTurn = {
7510
+ role: "assistant",
7511
+ content: capturedBlocks.filter((c) => !c.dropFromReplay).map((c) => {
7512
+ if (c.advisorReplay) {
7513
+ const input = typeof c.block.input === "object" && c.block.input !== null ? c.block.input : {};
7514
+ return {
7515
+ type: "tool_use",
7516
+ id: c.advisorReplay.id,
7517
+ name: ADVISOR_INTERNAL_TOOL_NAME,
7518
+ input
7519
+ };
7520
+ }
7521
+ return c.block;
7522
+ })
7523
+ };
7524
+ conversation.push(assistantTurn);
7525
+ let advisorText;
7526
+ try {
7527
+ advisorText = await runAdvisor(conversation, advisorModel, advisorEffort, aborter.signal);
7528
+ } catch (err) {
7529
+ if (aborter.signal.aborted) return;
7530
+ const msg = err instanceof Error ? err.message : String(err);
7531
+ consola.warn(`Advisor model call failed: ${msg}`);
7532
+ advisorText = `[Advisor unavailable: ${msg}. Continuing without external review — proceed with caution and consider self-checking against your primary-source evidence.]`;
7533
+ }
7534
+ if (aborter.signal.aborted) return;
7535
+ if (conversation === null) return;
7536
+ const resultIndex = nextSyntheticIndex++;
7537
+ if (!safeEnqueueEvent("content_block_start", {
7538
+ type: "content_block_start",
7539
+ index: resultIndex,
7540
+ content_block: {
7541
+ type: "advisor_tool_result",
7542
+ tool_use_id: advisorToolUse.clientId,
7543
+ content: {
7544
+ type: "advisor_result",
7545
+ text: advisorText
7546
+ }
7339
7547
  }
7340
- return c.block;
7341
- })
7342
- };
7343
- conversation.push(assistantTurn);
7344
- let advisorText;
7345
- try {
7346
- advisorText = await runAdvisor(conversation, advisorModel, advisorEffort);
7347
- } catch (err) {
7348
- const msg = err instanceof Error ? err.message : String(err);
7349
- consola.warn(`Advisor model call failed: ${msg}`);
7350
- advisorText = `[Advisor unavailable: ${msg}. Continuing without external review — proceed with caution and consider self-checking against your primary-source evidence.]`;
7548
+ })) return;
7549
+ if (!safeEnqueueEvent("content_block_stop", {
7550
+ type: "content_block_stop",
7551
+ index: resultIndex
7552
+ })) return;
7553
+ conversation.push({
7554
+ role: "user",
7555
+ content: [{
7556
+ type: "tool_result",
7557
+ tool_use_id: advisorToolUse.id,
7558
+ content: advisorText
7559
+ }]
7560
+ });
7561
+ if (aborter.signal.aborted) return;
7562
+ response = await createMessages(JSON.stringify({
7563
+ ...opts.baseBody,
7564
+ messages: conversation,
7565
+ stream: true
7566
+ }), opts.requestHeaders, aborter.signal);
7351
7567
  }
7352
- const resultIndex = nextSyntheticIndex++;
7353
- if (!safeEnqueueEvent("content_block_start", {
7568
+ if (aborter.signal.aborted) return;
7569
+ const finalIndex = nextSyntheticIndex++;
7570
+ safeEnqueueEvent("content_block_start", {
7354
7571
  type: "content_block_start",
7355
- index: resultIndex,
7572
+ index: finalIndex,
7356
7573
  content_block: {
7357
- type: "advisor_tool_result",
7358
- tool_use_id: advisorToolUse.clientId,
7359
- content: {
7360
- type: "advisor_result",
7361
- text: advisorText
7362
- }
7574
+ type: "text",
7575
+ text: ""
7576
+ }
7577
+ });
7578
+ safeEnqueueEvent("content_block_delta", {
7579
+ type: "content_block_delta",
7580
+ index: finalIndex,
7581
+ delta: {
7582
+ type: "text_delta",
7583
+ text: `\n\n[Advisor loop exceeded ${ADVISOR_MAX_TURNS} turns; halting]`
7363
7584
  }
7364
- })) return;
7365
- if (!safeEnqueueEvent("content_block_stop", {
7585
+ });
7586
+ safeEnqueueEvent("content_block_stop", {
7366
7587
  type: "content_block_stop",
7367
- index: resultIndex
7368
- })) return;
7369
- conversation.push({
7370
- role: "user",
7371
- content: [{
7372
- type: "tool_result",
7373
- tool_use_id: advisorToolUse.id,
7374
- content: advisorText
7375
- }]
7588
+ index: finalIndex
7376
7589
  });
7377
- response = await createMessages(JSON.stringify({
7378
- ...opts.baseBody,
7379
- messages: conversation,
7380
- stream: true
7381
- }), opts.requestHeaders);
7590
+ safeEnqueueEvent("message_stop", { type: "message_stop" });
7591
+ } catch (err) {
7592
+ if (aborter.signal.aborted) return;
7593
+ const msg = err instanceof Error ? err.message : String(err);
7594
+ consola.error(`Advisor stream error: ${msg}`);
7595
+ safeEnqueueEvent("error", {
7596
+ type: "error",
7597
+ error: {
7598
+ type: "api_error",
7599
+ message: `advisor loop failed: ${msg}`
7600
+ }
7601
+ });
7602
+ } finally {
7603
+ conversation = null;
7604
+ try {
7605
+ controller.close();
7606
+ } catch {}
7382
7607
  }
7383
- const finalIndex = nextSyntheticIndex++;
7384
- safeEnqueueEvent("content_block_start", {
7385
- type: "content_block_start",
7386
- index: finalIndex,
7387
- content_block: {
7388
- type: "text",
7389
- text: ""
7390
- }
7391
- });
7392
- safeEnqueueEvent("content_block_delta", {
7393
- type: "content_block_delta",
7394
- index: finalIndex,
7395
- delta: {
7396
- type: "text_delta",
7397
- text: `\n\n[Advisor loop exceeded ${ADVISOR_MAX_TURNS} turns; halting]`
7398
- }
7399
- });
7400
- safeEnqueueEvent("content_block_stop", {
7401
- type: "content_block_stop",
7402
- index: finalIndex
7403
- });
7404
- safeEnqueueEvent("message_stop", { type: "message_stop" });
7405
- } catch (err) {
7406
- const msg = err instanceof Error ? err.message : String(err);
7407
- consola.error(`Advisor stream error: ${msg}`);
7408
- safeEnqueueEvent("error", {
7409
- type: "error",
7410
- error: {
7411
- type: "api_error",
7412
- message: `advisor loop failed: ${msg}`
7413
- }
7414
- });
7415
- } finally {
7416
- try {
7417
- controller.close();
7418
- } catch {}
7608
+ },
7609
+ cancel(reason) {
7610
+ if (!aborter.signal.aborted) aborter.abort(/* @__PURE__ */ new Error(`advisor stream cancelled: ${reason instanceof Error ? reason.message : String(reason ?? "no reason")}`));
7611
+ conversation = null;
7419
7612
  }
7420
- } });
7613
+ });
7421
7614
  }
7422
7615
 
7423
7616
  //#endregion
@@ -7480,22 +7673,24 @@ function mcpHeaders(sid) {
7480
7673
  if (sid) headers["Mcp-Session-Id"] = sid;
7481
7674
  return headers;
7482
7675
  }
7483
- async function postMcp(body, sid, retry = true) {
7676
+ async function postMcp(body, sid, retry = true, signal) {
7484
7677
  const url = `${copilotBaseUrl(state)}/mcp`;
7485
7678
  const res = await fetch(url, {
7486
7679
  method: "POST",
7487
7680
  headers: mcpHeaders(sid),
7488
- body: JSON.stringify(body)
7681
+ body: JSON.stringify(body),
7682
+ signal
7489
7683
  });
7490
7684
  if (!res.ok && retry && res.status >= 500) {
7491
7685
  await sleep(500);
7492
- return postMcp(body, sid, false);
7686
+ return postMcp(body, sid, false, signal);
7493
7687
  }
7494
7688
  return res;
7495
7689
  }
7496
- async function searchWeb(query) {
7690
+ async function searchWeb(query, signal) {
7497
7691
  await throttleSearch();
7498
7692
  consola.info(`Web search (MCP): "${query.slice(0, 80)}"`);
7693
+ if (signal?.aborted) throw new Error("web search aborted before dispatch");
7499
7694
  const callId = Math.floor(Math.random() * 1e9);
7500
7695
  let sid;
7501
7696
  try {
@@ -7511,7 +7706,7 @@ async function searchWeb(query) {
7511
7706
  version: copilotVersion(state)
7512
7707
  }
7513
7708
  }
7514
- });
7709
+ }, void 0, true, signal);
7515
7710
  if (!initRes.ok) {
7516
7711
  consola.error("MCP initialize failed", initRes.status);
7517
7712
  throw new HTTPError("MCP initialize failed", initRes);
@@ -7521,7 +7716,7 @@ async function searchWeb(query) {
7521
7716
  const notifRes = await postMcp({
7522
7717
  jsonrpc: "2.0",
7523
7718
  method: "notifications/initialized"
7524
- }, sid);
7719
+ }, sid, true, signal);
7525
7720
  if (!notifRes.ok && notifRes.status !== 202) {
7526
7721
  consola.error("MCP notifications/initialized failed", notifRes.status);
7527
7722
  throw new HTTPError("MCP notifications/initialized failed", notifRes);
@@ -7534,13 +7729,14 @@ async function searchWeb(query) {
7534
7729
  name: "web_search",
7535
7730
  arguments: { query }
7536
7731
  }
7537
- }, sid);
7732
+ }, sid, true, signal);
7538
7733
  if (!callRes.ok) {
7539
7734
  consola.error("MCP tools/call failed", callRes.status);
7540
7735
  throw new HTTPError("MCP tools/call failed", callRes);
7541
7736
  }
7542
7737
  let rpc;
7543
7738
  for await (const ev of events(callRes)) {
7739
+ if (signal?.aborted) throw new Error("web search aborted during SSE stream");
7544
7740
  if (!ev.data) continue;
7545
7741
  let parsedJson;
7546
7742
  try {
@@ -9880,7 +10076,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
9880
10076
  description: "The search query string. Natural-language queries work best — the upstream provider rewrites for the search index."
9881
10077
  } }
9882
10078
  },
9883
- async handler(args, _signal) {
10079
+ async handler(args, signal) {
9884
10080
  const query = typeof args.query === "string" ? args.query : "";
9885
10081
  if (!query) return {
9886
10082
  content: [{
@@ -9892,7 +10088,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
9892
10088
  try {
9893
10089
  return { content: [{
9894
10090
  type: "text",
9895
- text: formatWebSearchResult(await searchWeb(query))
10091
+ text: formatWebSearchResult(await searchWeb(query, signal))
9896
10092
  }] };
9897
10093
  } catch (err) {
9898
10094
  return {
@@ -10752,9 +10948,11 @@ function rotateIfNeeded(filePath) {
10752
10948
  fs$1.renameSync(filePath, filePath + ".1");
10753
10949
  } catch {}
10754
10950
  }
10755
- var FileLogReporter = class {
10951
+ var FileLogReporter = class FileLogReporter {
10756
10952
  filePath;
10757
10953
  seen = /* @__PURE__ */ new Set();
10954
+ bytesSinceCheck = 0;
10955
+ static ROTATE_CHECK_BYTES = MAX_LOG_BYTES / 2;
10758
10956
  constructor(filePath) {
10759
10957
  this.filePath = filePath;
10760
10958
  rotateIfNeeded(filePath);
@@ -10766,6 +10964,11 @@ var FileLogReporter = class {
10766
10964
  if (this.seen.size >= DEDUP_MAX) this.seen.clear();
10767
10965
  this.seen.add(key);
10768
10966
  const line = formatLogLine(logObj);
10967
+ this.bytesSinceCheck += line.length;
10968
+ if (this.bytesSinceCheck >= FileLogReporter.ROTATE_CHECK_BYTES) {
10969
+ rotateIfNeeded(this.filePath);
10970
+ this.bytesSinceCheck = 0;
10971
+ }
10769
10972
  let fd;
10770
10973
  try {
10771
10974
  fd = fs$1.openSync(this.filePath, "a", 384);
@@ -10895,7 +11098,7 @@ function initProxyFromEnv() {
10895
11098
  //#endregion
10896
11099
  //#region package.json
10897
11100
  var name = "github-router";
10898
- var version = "0.3.35";
11101
+ var version = "0.3.37";
10899
11102
 
10900
11103
  //#endregion
10901
11104
  //#region src/lib/approval.ts
@@ -11312,7 +11515,7 @@ async function handleCompletion$1(c) {
11312
11515
  return c.json(response);
11313
11516
  }
11314
11517
  const iterator = response[Symbol.asyncIterator]();
11315
- const firstResult = await iterator.next();
11518
+ const firstResult = await readIteratorWithTimeout(iterator, UPSTREAM_INACTIVITY_TIMEOUT_MS);
11316
11519
  if (firstResult.done) consola.warn(`Upstream /chat/completions returned an empty stream at ${c.req.path}`);
11317
11520
  let pendingFirstChunk = firstResult.done ? void 0 : firstResult.value;
11318
11521
  let upstreamFinished = firstResult.done;
@@ -11352,7 +11555,7 @@ async function handleCompletion$1(c) {
11352
11555
  return;
11353
11556
  }
11354
11557
  try {
11355
- const result = await iterator.next();
11558
+ const result = await readIteratorWithTimeout(iterator, UPSTREAM_INACTIVITY_TIMEOUT_MS);
11356
11559
  if (consumerCancelled) {
11357
11560
  safeClose(controller);
11358
11561
  return;
@@ -11673,22 +11876,6 @@ function sanitizeAnthropicBody(rawBody) {
11673
11876
  return JSON.stringify(parsed);
11674
11877
  }
11675
11878
 
11676
- //#endregion
11677
- //#region src/lib/diagnose-response.ts
11678
- const PREVIEW_LIMIT = 200;
11679
- async function parseJsonOrDiagnose(response, routePath) {
11680
- const cloned = response.clone();
11681
- try {
11682
- return await response.json();
11683
- } catch (error) {
11684
- const contentType = response.headers.get("content-type") ?? "(none)";
11685
- const bodyText = await cloned.text().catch(() => "(unreadable)");
11686
- const preview = bodyText.length > PREVIEW_LIMIT ? bodyText.slice(0, PREVIEW_LIMIT) + "...(truncated)" : bodyText;
11687
- consola.error(`Upstream JSON parse failed at ${routePath}: status=${response.status} content-type="${contentType}" body[0..${PREVIEW_LIMIT}]=${JSON.stringify(preview)}`);
11688
- throw error;
11689
- }
11690
- }
11691
-
11692
11879
  //#endregion
11693
11880
  //#region src/routes/messages/count-tokens-handler.ts
11694
11881
  const isWebSearchTool$1 = (tool) => typeof tool.type === "string" && tool.type.startsWith("web_search") || tool.name === "web_search";
@@ -11998,12 +12185,13 @@ async function handleCompletion(c) {
11998
12185
  const modelId = resolvedModel ?? originalModel;
11999
12186
  if (modelId) logEndpointMismatch(modelId, "/v1/messages");
12000
12187
  const effectiveBetas = applyDefaultBetas(betaHeaders, resolvedModel ?? originalModel);
12188
+ const advisorAborter = advisorEnabled ? new AbortController() : void 0;
12001
12189
  let response;
12002
12190
  try {
12003
12191
  response = await createMessages(resolvedBody, {
12004
12192
  ...selectedModel?.requestHeaders,
12005
12193
  ...effectiveBetas
12006
- });
12194
+ }, advisorAborter?.signal);
12007
12195
  } catch (error) {
12008
12196
  if (error instanceof HTTPError) {
12009
12197
  const errorBody = await error.response.clone().text().catch(() => "");
@@ -12061,7 +12249,8 @@ async function handleCompletion(c) {
12061
12249
  requestHeaders: {
12062
12250
  ...selectedModel?.requestHeaders,
12063
12251
  ...effectiveBetas
12064
- }
12252
+ },
12253
+ externalAborter: advisorAborter
12065
12254
  }), {
12066
12255
  status: response.status,
12067
12256
  headers: streamHeaders
@@ -12072,7 +12261,9 @@ async function handleCompletion(c) {
12072
12261
  headers: streamHeaders
12073
12262
  });
12074
12263
  }
12075
- const responseBody = await parseJsonOrDiagnose(response, c.req.path);
12264
+ const cappedResult = await readResponseBodyCapped(response, c.req.path, MAX_RESPONSE_BODY_BYTES);
12265
+ if (!cappedResult.ok) return c.json(cappedResult.errorResponse, cappedResult.status);
12266
+ const responseBody = cappedResult.value;
12076
12267
  logRequest({
12077
12268
  method: "POST",
12078
12269
  path: c.req.path,
@@ -12423,7 +12614,7 @@ async function handleResponses(c) {
12423
12614
  let firstChunk;
12424
12615
  let upstreamFinished = false;
12425
12616
  while (true) {
12426
- const r = await iterator.next();
12617
+ const r = await readIteratorWithTimeout(iterator, UPSTREAM_INACTIVITY_TIMEOUT_MS);
12427
12618
  if (r.done) {
12428
12619
  upstreamFinished = true;
12429
12620
  break;
@@ -12475,7 +12666,7 @@ async function handleResponses(c) {
12475
12666
  return;
12476
12667
  }
12477
12668
  try {
12478
- const result = await iterator.next();
12669
+ const result = await readIteratorWithTimeout(iterator, UPSTREAM_INACTIVITY_TIMEOUT_MS);
12479
12670
  if (consumerCancelled) {
12480
12671
  safeClose(controller);
12481
12672
  return;
@@ -12584,11 +12775,14 @@ async function handleResponsesCompact(c) {
12584
12775
  if (!state.copilotToken) throw new Error("Copilot token not found");
12585
12776
  if (state.manualApprove) await awaitApproval();
12586
12777
  const body = await c.req.json();
12587
- const response = await fetch(`${copilotBaseUrl(state)}/responses/compact`, {
12778
+ const compactUrl = `${copilotBaseUrl(state)}/responses/compact`;
12779
+ const doFetch = () => fetch(compactUrl, {
12588
12780
  method: "POST",
12589
12781
  headers: copilotHeaders(state),
12590
- body: JSON.stringify(body)
12782
+ body: JSON.stringify(body),
12783
+ signal: AbortSignal.timeout(UPSTREAM_FETCH_TIMEOUT_MS || 3e5)
12591
12784
  });
12785
+ const response = await tryRefreshAndRetry(doFetch, "/responses/compact");
12592
12786
  if (response.ok) {
12593
12787
  logRequest({
12594
12788
  method: "POST",
@@ -12599,6 +12793,7 @@ async function handleResponsesCompact(c) {
12599
12793
  }
12600
12794
  if (response.status === 404) {
12601
12795
  consola.debug("Copilot API does not support /responses/compact, using synthetic compaction");
12796
+ await response.body?.cancel().catch(() => {});
12602
12797
  return await syntheticCompact(c, body, startTime);
12603
12798
  }
12604
12799
  logRequest({