github-router 0.3.35 → 0.3.36

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", []);
@@ -3055,6 +3075,12 @@ async function bridgeCall(endpoint, tool, args, timeoutMs, signal) {
3055
3075
  }
3056
3076
  const timer = setTimeout(() => finish(() => reject(/* @__PURE__ */ new Error(`timeout after ${timeoutMs}ms`))), timeoutMs);
3057
3077
  ws.on("open", () => {
3078
+ if (settled) {
3079
+ try {
3080
+ ws.close();
3081
+ } catch {}
3082
+ return;
3083
+ }
3058
3084
  ws.send(JSON.stringify({
3059
3085
  id,
3060
3086
  tool,
@@ -5882,40 +5908,25 @@ function detectAgentCall(input) {
5882
5908
  const MCP_PROTOCOL_VERSION = "2025-06-18";
5883
5909
  const SERVER_NAME = "github-router-peers";
5884
5910
  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
5911
  const inflightAborts = /* @__PURE__ */ new Map();
5912
+ /**
5913
+ * Idempotent teardown for an in-flight tools/call. Aborts the upstream
5914
+ * fetch, frees the concurrency slot, and removes the registry entry.
5915
+ * Safe to call from both `notifications/cancelled` and the SSE
5916
+ * `ReadableStream.cancel()` callback, in either order, and any number
5917
+ * of times — the second call is a no-op.
5918
+ */
5919
+ function cancelInflight(key, reason) {
5920
+ const entry = inflightAborts.get(key);
5921
+ if (!entry) return;
5922
+ inflightAborts.delete(key);
5923
+ try {
5924
+ entry.aborter.abort(new Error(reason));
5925
+ } catch {}
5926
+ try {
5927
+ entry.release();
5928
+ } catch {}
5929
+ }
5919
5930
  const RPC_PARSE_ERROR = -32700;
5920
5931
  const RPC_INVALID_REQUEST = -32600;
5921
5932
  const RPC_METHOD_NOT_FOUND = -32601;
@@ -6388,9 +6399,14 @@ async function handleToolsCall(body) {
6388
6399
  const startedAt = Date.now();
6389
6400
  const abortKey = body.id !== void 0 && body.id !== null ? body.id : void 0;
6390
6401
  let aborter;
6402
+ let inflightEntry;
6391
6403
  if (abortKey !== void 0) {
6392
6404
  aborter = new AbortController();
6393
- inflightAborts.set(abortKey, aborter);
6405
+ inflightEntry = {
6406
+ aborter,
6407
+ release
6408
+ };
6409
+ inflightAborts.set(abortKey, inflightEntry);
6394
6410
  }
6395
6411
  const telemetryName = persona ? persona.agentName : nonPersonaTool.toolNameHttp;
6396
6412
  const telemetryModel = persona ? persona.model : "(non-persona)";
@@ -6421,7 +6437,9 @@ async function handleToolsCall(body) {
6421
6437
  });
6422
6438
  } finally {
6423
6439
  release();
6424
- if (abortKey !== void 0) inflightAborts.delete(abortKey);
6440
+ if (abortKey !== void 0 && inflightEntry !== void 0) {
6441
+ if (inflightAborts.get(abortKey) === inflightEntry) inflightAborts.delete(abortKey);
6442
+ }
6425
6443
  }
6426
6444
  }
6427
6445
  /**
@@ -6436,9 +6454,7 @@ function handleCancelledNotification(body) {
6436
6454
  consola.debug(`[mcp] notifications/cancelled missing or invalid requestId: ${JSON.stringify(requestId)}`);
6437
6455
  return;
6438
6456
  }
6439
- const aborter = inflightAborts.get(requestId);
6440
- if (!aborter) return;
6441
- aborter.abort(/* @__PURE__ */ new Error("client requested cancellation"));
6457
+ cancelInflight(requestId, "client requested cancellation");
6442
6458
  }
6443
6459
  async function handleRpc(_c, body) {
6444
6460
  if (body === null || typeof body !== "object" || Array.isArray(body)) return {
@@ -6637,6 +6653,7 @@ const SSE_HEARTBEAT_INTERVAL_MS = 5e3;
6637
6653
  async function handleToolsCallSSE(body) {
6638
6654
  const encoder = new TextEncoder();
6639
6655
  const callPromise = handleToolsCall(body);
6656
+ let heartbeatHandle;
6640
6657
  const stream = new ReadableStream({
6641
6658
  async start(controller) {
6642
6659
  let closed = false;
@@ -6669,23 +6686,27 @@ async function handleToolsCallSSE(body) {
6669
6686
  }
6670
6687
  });
6671
6688
  safeEnqueue(heartbeatFrame());
6672
- const heartbeatHandle = setInterval(() => safeEnqueue(heartbeatFrame()), SSE_HEARTBEAT_INTERVAL_MS);
6689
+ heartbeatHandle = setInterval(() => safeEnqueue(heartbeatFrame()), SSE_HEARTBEAT_INTERVAL_MS);
6673
6690
  try {
6674
6691
  safeEnqueue(sseFrame(await callPromise));
6675
6692
  } catch (err) {
6676
6693
  consola.error("/mcp SSE upstream error:", err);
6677
6694
  safeEnqueue(sseFrame(rpcError(body.id ?? null, RPC_INTERNAL_ERROR, err instanceof Error ? err.message : String(err))));
6678
6695
  } finally {
6679
- clearInterval(heartbeatHandle);
6696
+ if (heartbeatHandle !== void 0) {
6697
+ clearInterval(heartbeatHandle);
6698
+ heartbeatHandle = void 0;
6699
+ }
6680
6700
  safeClose();
6681
6701
  }
6682
6702
  },
6683
6703
  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"));
6704
+ if (heartbeatHandle !== void 0) {
6705
+ clearInterval(heartbeatHandle);
6706
+ heartbeatHandle = void 0;
6688
6707
  }
6708
+ const abortKey = body.id !== void 0 && body.id !== null ? body.id : void 0;
6709
+ if (abortKey !== void 0) cancelInflight(abortKey, "client disconnected SSE stream");
6689
6710
  }
6690
6711
  });
6691
6712
  return new Response(stream, {
@@ -7047,7 +7068,8 @@ function renderConversationAsText(conversation, maxChars = ADVISOR_MAX_CONVERSAT
7047
7068
  * Anthropic's own ADVISOR ("see the whole task + every tool call +
7048
7069
  * every result").
7049
7070
  */
7050
- async function runAdvisor(conversation, advisorModel, advisorEffort) {
7071
+ async function runAdvisor(conversation, advisorModel, advisorEffort, signal) {
7072
+ if (signal?.aborted) throw new Error("advisor call aborted before dispatch");
7051
7073
  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
7074
  const conversationText = renderConversationAsText(conversation);
7053
7075
  const resolvedAdvisorModel = resolveModel(advisorModel);
@@ -7064,7 +7086,7 @@ async function runAdvisor(conversation, advisorModel, advisorEffort) {
7064
7086
  }],
7065
7087
  stream: false,
7066
7088
  reasoning: { effort: advisorEffort }
7067
- });
7089
+ }, void 0, signal);
7068
7090
  const out = [];
7069
7091
  for (const item of response.output) {
7070
7092
  if (typeof item !== "object" || item === null) continue;
@@ -7091,7 +7113,7 @@ async function runAdvisor(conversation, advisorModel, advisorEffort) {
7091
7113
  content: conversationText
7092
7114
  }],
7093
7115
  stream: false
7094
- }), {})).json();
7116
+ }), {}, signal)).json();
7095
7117
  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
7118
  if (!text) throw new Error(`Advisor model ${resolvedAdvisorModel} returned empty response`);
7097
7119
  return text;
@@ -7140,284 +7162,305 @@ function sseEvent(type, data) {
7140
7162
  function buildAdvisorStream(opts) {
7141
7163
  const advisorModel = opts.advisorModel ?? ADVISOR_DEFAULT_MODEL;
7142
7164
  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;
7165
+ const aborter = new AbortController();
7166
+ let conversation = [...opts.initialConversation];
7167
+ return new ReadableStream({
7168
+ async start(controller) {
7169
+ let messageStartForwarded = false;
7170
+ let nextSyntheticIndex = 0;
7171
+ let turnsRun = 0;
7172
+ const safeEnqueue = (bytes) => {
7165
7173
  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;
7174
+ controller.enqueue(bytes);
7175
+ return true;
7176
+ } catch (err) {
7177
+ if (isControllerClosedError(err)) {
7178
+ if (!aborter.signal.aborted) aborter.abort(/* @__PURE__ */ new Error("advisor stream consumer disconnected"));
7179
+ return false;
7180
+ }
7181
+ throw err;
7173
7182
  }
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
- }
7183
+ };
7184
+ const safeEnqueueEvent = (type, data) => safeEnqueue(ENCODER$2.encode(sseEvent(type, data)));
7185
+ async function processOneTurn(response) {
7186
+ const capturedBlocks = [];
7187
+ let advisorToolUse = null;
7188
+ const indexToBlock = /* @__PURE__ */ new Map();
7189
+ for await (const ev of events(response)) {
7190
+ if (!ev.event || !ev.data) continue;
7191
+ let payload;
7192
+ try {
7193
+ payload = JSON.parse(ev.data);
7194
+ } catch {
7195
+ if (!safeEnqueue(ENCODER$2.encode(`event: ${ev.event}\ndata: ${ev.data}\n\n`))) return {
7196
+ capturedBlocks,
7197
+ advisorToolUse
7198
+ };
7183
7199
  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 {
7200
+ }
7201
+ switch (ev.event) {
7202
+ case "message_start":
7203
+ if (!messageStartForwarded) {
7204
+ if (!safeEnqueueEvent(ev.event, payload)) return {
7208
7205
  capturedBlocks,
7209
7206
  advisorToolUse
7210
7207
  };
7211
- const captured = {
7212
- block: {
7213
- type: "tool_use",
7208
+ messageStartForwarded = true;
7209
+ }
7210
+ continue;
7211
+ case "content_block_start": {
7212
+ const block = payload.content_block;
7213
+ const upstreamIndex = payload.index;
7214
+ if (block && upstreamIndex !== void 0) {
7215
+ const myIndex = nextSyntheticIndex++;
7216
+ if (block.type === "tool_use" && block.name === ADVISOR_INTERNAL_TOOL_NAME) {
7217
+ const id = typeof block.id === "string" ? block.id : `toolu_advisor_${myIndex}`;
7218
+ advisorToolUse = {
7219
+ index: myIndex,
7214
7220
  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 {
7221
+ clientId: toClientServerToolUseId(id, myIndex),
7222
+ inputJson: ""
7223
+ };
7224
+ const translated = {
7225
+ ...payload,
7226
+ index: myIndex,
7227
+ content_block: {
7228
+ type: "server_tool_use",
7229
+ id: advisorToolUse.clientId,
7230
+ name: ADVISOR_CLIENT_TOOL_NAME,
7231
+ input: {}
7232
+ }
7233
+ };
7234
+ if (!safeEnqueueEvent(ev.event, translated)) return {
7235
+ capturedBlocks,
7236
+ advisorToolUse
7237
+ };
7238
+ const captured = {
7239
+ block: {
7240
+ type: "tool_use",
7241
+ id,
7242
+ name: ADVISOR_INTERNAL_TOOL_NAME,
7243
+ input: {}
7244
+ },
7245
+ partialJson: "",
7246
+ advisorReplay: { id }
7247
+ };
7248
+ capturedBlocks.push(captured);
7249
+ indexToBlock.set(upstreamIndex, captured);
7250
+ } else {
7251
+ const reindexed = {
7252
+ ...payload,
7253
+ index: myIndex
7254
+ };
7255
+ if (!safeEnqueueEvent(ev.event, reindexed)) return {
7256
+ capturedBlocks,
7257
+ advisorToolUse
7258
+ };
7259
+ const captured = {
7260
+ block: { ...block },
7261
+ partialJson: ""
7262
+ };
7263
+ capturedBlocks.push(captured);
7264
+ indexToBlock.set(upstreamIndex, captured);
7265
+ }
7266
+ }
7267
+ continue;
7268
+ }
7269
+ case "content_block_delta": {
7270
+ const upstreamIndex = payload.index;
7271
+ const delta = payload.delta;
7272
+ if (upstreamIndex !== void 0) {
7273
+ const captured = upstreamIndex !== void 0 ? indexToBlock.get(upstreamIndex) : void 0;
7224
7274
  const reindexed = {
7225
7275
  ...payload,
7226
- index: myIndex
7276
+ index: captured ? capturedBlocks.indexOf(captured) >= 0 ? nextSyntheticIndex - capturedBlocks.length + capturedBlocks.indexOf(captured) : upstreamIndex : upstreamIndex
7227
7277
  };
7228
7278
  if (!safeEnqueueEvent(ev.event, reindexed)) return {
7229
7279
  capturedBlocks,
7230
7280
  advisorToolUse
7231
7281
  };
7232
- const captured = {
7233
- block: { ...block },
7234
- partialJson: ""
7235
- };
7236
- capturedBlocks.push(captured);
7237
- indexToBlock.set(upstreamIndex, captured);
7238
- }
7282
+ if (captured && delta) {
7283
+ if (delta.type === "text_delta" && typeof delta.text === "string") captured.block.text = (captured.block.text ?? "") + delta.text;
7284
+ else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") captured.block.thinking = (captured.block.thinking ?? "") + delta.thinking;
7285
+ else if (delta.type === "signature_delta" && typeof delta.signature === "string") captured.block.signature = (captured.block.signature ?? "") + delta.signature;
7286
+ else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") captured.partialJson += delta.partial_json;
7287
+ else if (delta.type === "citations_delta" && delta.citation) {
7288
+ if (!Array.isArray(captured.block.citations)) captured.block.citations = [];
7289
+ captured.block.citations.push(delta.citation);
7290
+ }
7291
+ }
7292
+ } else if (!safeEnqueueEvent(ev.event, payload)) return {
7293
+ capturedBlocks,
7294
+ advisorToolUse
7295
+ };
7296
+ continue;
7239
7297
  }
7240
- continue;
7241
- }
7242
- case "content_block_delta": {
7243
- const upstreamIndex = payload.index;
7244
- const delta = payload.delta;
7245
- if (upstreamIndex !== void 0) {
7298
+ case "content_block_stop": {
7299
+ const upstreamIndex = payload.index;
7246
7300
  const captured = upstreamIndex !== void 0 ? indexToBlock.get(upstreamIndex) : void 0;
7247
7301
  const reindexed = {
7248
7302
  ...payload,
7249
- index: captured ? capturedBlocks.indexOf(captured) >= 0 ? nextSyntheticIndex - capturedBlocks.length + capturedBlocks.indexOf(captured) : upstreamIndex : upstreamIndex
7303
+ index: captured ? nextSyntheticIndex - capturedBlocks.length + capturedBlocks.indexOf(captured) : upstreamIndex ?? 0
7250
7304
  };
7251
7305
  if (!safeEnqueueEvent(ev.event, reindexed)) return {
7252
7306
  capturedBlocks,
7253
7307
  advisorToolUse
7254
7308
  };
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);
7309
+ if (captured) {
7310
+ if (captured.block.type === "tool_use" && captured.partialJson.length > 0) try {
7311
+ captured.block.input = JSON.parse(captured.partialJson);
7312
+ } catch (err) {
7313
+ 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)}`);
7314
+ captured.block.input = {};
7263
7315
  }
7316
+ if (captured.block.type === "text" && (typeof captured.block.text !== "string" || captured.block.text.length === 0)) captured.dropFromReplay = true;
7264
7317
  }
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;
7318
+ continue;
7290
7319
  }
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 {
7320
+ case "message_delta":
7321
+ if (!safeEnqueueEvent(ev.event, payload)) return {
7322
+ capturedBlocks,
7323
+ advisorToolUse
7324
+ };
7325
+ continue;
7326
+ case "message_stop":
7327
+ if (advisorToolUse) return {
7328
+ capturedBlocks,
7329
+ advisorToolUse
7330
+ };
7331
+ if (!safeEnqueueEvent(ev.event, payload)) return {
7332
+ capturedBlocks,
7333
+ advisorToolUse
7334
+ };
7335
+ return {
7336
+ capturedBlocks,
7337
+ advisorToolUse
7338
+ };
7339
+ default: if (!safeEnqueueEvent(ev.event, payload)) return {
7309
7340
  capturedBlocks,
7310
7341
  advisorToolUse
7311
7342
  };
7312
- default: if (!safeEnqueueEvent(ev.event, payload)) return {
7313
- capturedBlocks,
7314
- advisorToolUse
7315
- };
7343
+ }
7316
7344
  }
7345
+ return {
7346
+ capturedBlocks,
7347
+ advisorToolUse
7348
+ };
7317
7349
  }
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
- };
7350
+ try {
7351
+ let response = opts.firstResponse;
7352
+ for (turnsRun = 0; turnsRun < ADVISOR_MAX_TURNS; turnsRun++) {
7353
+ if (aborter.signal.aborted) return;
7354
+ if (conversation === null) return;
7355
+ const { capturedBlocks, advisorToolUse } = await processOneTurn(response);
7356
+ if (!advisorToolUse) return;
7357
+ if (aborter.signal.aborted) return;
7358
+ if (conversation === null) return;
7359
+ const assistantTurn = {
7360
+ role: "assistant",
7361
+ content: capturedBlocks.filter((c) => !c.dropFromReplay).map((c) => {
7362
+ if (c.advisorReplay) {
7363
+ const input = typeof c.block.input === "object" && c.block.input !== null ? c.block.input : {};
7364
+ return {
7365
+ type: "tool_use",
7366
+ id: c.advisorReplay.id,
7367
+ name: ADVISOR_INTERNAL_TOOL_NAME,
7368
+ input
7369
+ };
7370
+ }
7371
+ return c.block;
7372
+ })
7373
+ };
7374
+ conversation.push(assistantTurn);
7375
+ let advisorText;
7376
+ try {
7377
+ advisorText = await runAdvisor(conversation, advisorModel, advisorEffort, aborter.signal);
7378
+ } catch (err) {
7379
+ if (aborter.signal.aborted) return;
7380
+ const msg = err instanceof Error ? err.message : String(err);
7381
+ consola.warn(`Advisor model call failed: ${msg}`);
7382
+ advisorText = `[Advisor unavailable: ${msg}. Continuing without external review — proceed with caution and consider self-checking against your primary-source evidence.]`;
7383
+ }
7384
+ if (aborter.signal.aborted) return;
7385
+ if (conversation === null) return;
7386
+ const resultIndex = nextSyntheticIndex++;
7387
+ if (!safeEnqueueEvent("content_block_start", {
7388
+ type: "content_block_start",
7389
+ index: resultIndex,
7390
+ content_block: {
7391
+ type: "advisor_tool_result",
7392
+ tool_use_id: advisorToolUse.clientId,
7393
+ content: {
7394
+ type: "advisor_result",
7395
+ text: advisorText
7396
+ }
7339
7397
  }
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.]`;
7398
+ })) return;
7399
+ if (!safeEnqueueEvent("content_block_stop", {
7400
+ type: "content_block_stop",
7401
+ index: resultIndex
7402
+ })) return;
7403
+ conversation.push({
7404
+ role: "user",
7405
+ content: [{
7406
+ type: "tool_result",
7407
+ tool_use_id: advisorToolUse.id,
7408
+ content: advisorText
7409
+ }]
7410
+ });
7411
+ if (aborter.signal.aborted) return;
7412
+ response = await createMessages(JSON.stringify({
7413
+ ...opts.baseBody,
7414
+ messages: conversation,
7415
+ stream: true
7416
+ }), opts.requestHeaders, aborter.signal);
7351
7417
  }
7352
- const resultIndex = nextSyntheticIndex++;
7353
- if (!safeEnqueueEvent("content_block_start", {
7418
+ if (aborter.signal.aborted) return;
7419
+ const finalIndex = nextSyntheticIndex++;
7420
+ safeEnqueueEvent("content_block_start", {
7354
7421
  type: "content_block_start",
7355
- index: resultIndex,
7422
+ index: finalIndex,
7356
7423
  content_block: {
7357
- type: "advisor_tool_result",
7358
- tool_use_id: advisorToolUse.clientId,
7359
- content: {
7360
- type: "advisor_result",
7361
- text: advisorText
7362
- }
7424
+ type: "text",
7425
+ text: ""
7426
+ }
7427
+ });
7428
+ safeEnqueueEvent("content_block_delta", {
7429
+ type: "content_block_delta",
7430
+ index: finalIndex,
7431
+ delta: {
7432
+ type: "text_delta",
7433
+ text: `\n\n[Advisor loop exceeded ${ADVISOR_MAX_TURNS} turns; halting]`
7363
7434
  }
7364
- })) return;
7365
- if (!safeEnqueueEvent("content_block_stop", {
7435
+ });
7436
+ safeEnqueueEvent("content_block_stop", {
7366
7437
  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
- }]
7438
+ index: finalIndex
7376
7439
  });
7377
- response = await createMessages(JSON.stringify({
7378
- ...opts.baseBody,
7379
- messages: conversation,
7380
- stream: true
7381
- }), opts.requestHeaders);
7440
+ safeEnqueueEvent("message_stop", { type: "message_stop" });
7441
+ } catch (err) {
7442
+ if (aborter.signal.aborted) return;
7443
+ const msg = err instanceof Error ? err.message : String(err);
7444
+ consola.error(`Advisor stream error: ${msg}`);
7445
+ safeEnqueueEvent("error", {
7446
+ type: "error",
7447
+ error: {
7448
+ type: "api_error",
7449
+ message: `advisor loop failed: ${msg}`
7450
+ }
7451
+ });
7452
+ } finally {
7453
+ conversation = null;
7454
+ try {
7455
+ controller.close();
7456
+ } catch {}
7382
7457
  }
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 {}
7458
+ },
7459
+ cancel(reason) {
7460
+ if (!aborter.signal.aborted) aborter.abort(/* @__PURE__ */ new Error(`advisor stream cancelled: ${reason instanceof Error ? reason.message : String(reason ?? "no reason")}`));
7461
+ conversation = null;
7419
7462
  }
7420
- } });
7463
+ });
7421
7464
  }
7422
7465
 
7423
7466
  //#endregion
@@ -7480,22 +7523,24 @@ function mcpHeaders(sid) {
7480
7523
  if (sid) headers["Mcp-Session-Id"] = sid;
7481
7524
  return headers;
7482
7525
  }
7483
- async function postMcp(body, sid, retry = true) {
7526
+ async function postMcp(body, sid, retry = true, signal) {
7484
7527
  const url = `${copilotBaseUrl(state)}/mcp`;
7485
7528
  const res = await fetch(url, {
7486
7529
  method: "POST",
7487
7530
  headers: mcpHeaders(sid),
7488
- body: JSON.stringify(body)
7531
+ body: JSON.stringify(body),
7532
+ signal
7489
7533
  });
7490
7534
  if (!res.ok && retry && res.status >= 500) {
7491
7535
  await sleep(500);
7492
- return postMcp(body, sid, false);
7536
+ return postMcp(body, sid, false, signal);
7493
7537
  }
7494
7538
  return res;
7495
7539
  }
7496
- async function searchWeb(query) {
7540
+ async function searchWeb(query, signal) {
7497
7541
  await throttleSearch();
7498
7542
  consola.info(`Web search (MCP): "${query.slice(0, 80)}"`);
7543
+ if (signal?.aborted) throw new Error("web search aborted before dispatch");
7499
7544
  const callId = Math.floor(Math.random() * 1e9);
7500
7545
  let sid;
7501
7546
  try {
@@ -7511,7 +7556,7 @@ async function searchWeb(query) {
7511
7556
  version: copilotVersion(state)
7512
7557
  }
7513
7558
  }
7514
- });
7559
+ }, void 0, true, signal);
7515
7560
  if (!initRes.ok) {
7516
7561
  consola.error("MCP initialize failed", initRes.status);
7517
7562
  throw new HTTPError("MCP initialize failed", initRes);
@@ -7521,7 +7566,7 @@ async function searchWeb(query) {
7521
7566
  const notifRes = await postMcp({
7522
7567
  jsonrpc: "2.0",
7523
7568
  method: "notifications/initialized"
7524
- }, sid);
7569
+ }, sid, true, signal);
7525
7570
  if (!notifRes.ok && notifRes.status !== 202) {
7526
7571
  consola.error("MCP notifications/initialized failed", notifRes.status);
7527
7572
  throw new HTTPError("MCP notifications/initialized failed", notifRes);
@@ -7534,13 +7579,14 @@ async function searchWeb(query) {
7534
7579
  name: "web_search",
7535
7580
  arguments: { query }
7536
7581
  }
7537
- }, sid);
7582
+ }, sid, true, signal);
7538
7583
  if (!callRes.ok) {
7539
7584
  consola.error("MCP tools/call failed", callRes.status);
7540
7585
  throw new HTTPError("MCP tools/call failed", callRes);
7541
7586
  }
7542
7587
  let rpc;
7543
7588
  for await (const ev of events(callRes)) {
7589
+ if (signal?.aborted) throw new Error("web search aborted during SSE stream");
7544
7590
  if (!ev.data) continue;
7545
7591
  let parsedJson;
7546
7592
  try {
@@ -9880,7 +9926,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
9880
9926
  description: "The search query string. Natural-language queries work best — the upstream provider rewrites for the search index."
9881
9927
  } }
9882
9928
  },
9883
- async handler(args, _signal) {
9929
+ async handler(args, signal) {
9884
9930
  const query = typeof args.query === "string" ? args.query : "";
9885
9931
  if (!query) return {
9886
9932
  content: [{
@@ -9892,7 +9938,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
9892
9938
  try {
9893
9939
  return { content: [{
9894
9940
  type: "text",
9895
- text: formatWebSearchResult(await searchWeb(query))
9941
+ text: formatWebSearchResult(await searchWeb(query, signal))
9896
9942
  }] };
9897
9943
  } catch (err) {
9898
9944
  return {
@@ -10752,9 +10798,11 @@ function rotateIfNeeded(filePath) {
10752
10798
  fs$1.renameSync(filePath, filePath + ".1");
10753
10799
  } catch {}
10754
10800
  }
10755
- var FileLogReporter = class {
10801
+ var FileLogReporter = class FileLogReporter {
10756
10802
  filePath;
10757
10803
  seen = /* @__PURE__ */ new Set();
10804
+ bytesSinceCheck = 0;
10805
+ static ROTATE_CHECK_BYTES = MAX_LOG_BYTES / 2;
10758
10806
  constructor(filePath) {
10759
10807
  this.filePath = filePath;
10760
10808
  rotateIfNeeded(filePath);
@@ -10766,6 +10814,11 @@ var FileLogReporter = class {
10766
10814
  if (this.seen.size >= DEDUP_MAX) this.seen.clear();
10767
10815
  this.seen.add(key);
10768
10816
  const line = formatLogLine(logObj);
10817
+ this.bytesSinceCheck += line.length;
10818
+ if (this.bytesSinceCheck >= FileLogReporter.ROTATE_CHECK_BYTES) {
10819
+ rotateIfNeeded(this.filePath);
10820
+ this.bytesSinceCheck = 0;
10821
+ }
10769
10822
  let fd;
10770
10823
  try {
10771
10824
  fd = fs$1.openSync(this.filePath, "a", 384);
@@ -10895,7 +10948,7 @@ function initProxyFromEnv() {
10895
10948
  //#endregion
10896
10949
  //#region package.json
10897
10950
  var name = "github-router";
10898
- var version = "0.3.35";
10951
+ var version = "0.3.36";
10899
10952
 
10900
10953
  //#endregion
10901
10954
  //#region src/lib/approval.ts
@@ -11859,6 +11912,89 @@ function stripAnthropicOnlyFields$1(body) {
11859
11912
 
11860
11913
  //#endregion
11861
11914
  //#region src/routes/messages/handler.ts
11915
+ const NON_STREAMING_BODY_CAP_BYTES = 10 * 1024 * 1024;
11916
+ /**
11917
+ * Read a Response body with a hard byte cap, then parse as JSON.
11918
+ *
11919
+ * Falls back to the fast path (response.json()) when Content-Length is
11920
+ * present and within the cap, avoiding the streaming-reader overhead for
11921
+ * the vast majority of normal responses.
11922
+ *
11923
+ * When the cap is hit:
11924
+ * - the reader is cancelled to release the upstream socket
11925
+ * - a structured Anthropic-format error is returned to the caller
11926
+ * (the caller wraps it in c.json(), not throws — the client gets a
11927
+ * clean 413 error, not an unhandled-rejection crash)
11928
+ *
11929
+ * Returns `{ ok: true, value }` on success or `{ ok: false, errorResponse }`
11930
+ * on cap exceeded.
11931
+ */
11932
+ async function readResponseBodyCapped(response, routePath, capBytes) {
11933
+ const contentLengthHeader = response.headers.get("content-length");
11934
+ const contentLength = contentLengthHeader ? parseInt(contentLengthHeader, 10) : NaN;
11935
+ if (!isNaN(contentLength) && contentLength <= capBytes) return {
11936
+ ok: true,
11937
+ value: await parseJsonOrDiagnose(response, routePath)
11938
+ };
11939
+ const reader = response.body?.getReader();
11940
+ if (!reader) return {
11941
+ ok: true,
11942
+ value: await parseJsonOrDiagnose(response, routePath)
11943
+ };
11944
+ const chunks = [];
11945
+ let totalBytes = 0;
11946
+ let capped = false;
11947
+ try {
11948
+ while (true) {
11949
+ const { done, value } = await reader.read();
11950
+ if (done) break;
11951
+ if (!value) continue;
11952
+ totalBytes += value.byteLength;
11953
+ if (totalBytes > capBytes) {
11954
+ capped = true;
11955
+ try {
11956
+ await reader.cancel("size_cap");
11957
+ } catch {}
11958
+ break;
11959
+ }
11960
+ chunks.push(value);
11961
+ }
11962
+ } catch (err) {
11963
+ if (!capped) consola.warn(`readResponseBodyCapped: read error at ${routePath}:`, err);
11964
+ }
11965
+ if (capped) {
11966
+ consola.warn(`Non-streaming upstream response at ${routePath} exceeded ${capBytes} bytes (10 MiB cap); dropping body to prevent OOM. Check upstream health.`);
11967
+ return {
11968
+ ok: false,
11969
+ status: 502,
11970
+ errorResponse: {
11971
+ type: "error",
11972
+ error: {
11973
+ type: "api_error",
11974
+ message: "Upstream response body exceeded the 10 MiB size cap for non-streaming /v1/messages. The upstream may be misbehaving. Try enabling streaming (stream: true) which handles large responses chunk-by-chunk."
11975
+ }
11976
+ }
11977
+ };
11978
+ }
11979
+ const merged = new Uint8Array(totalBytes);
11980
+ let offset = 0;
11981
+ for (const chunk of chunks) {
11982
+ merged.set(chunk, offset);
11983
+ offset += chunk.byteLength;
11984
+ }
11985
+ const text = new TextDecoder().decode(merged);
11986
+ try {
11987
+ return {
11988
+ ok: true,
11989
+ value: JSON.parse(text)
11990
+ };
11991
+ } catch (err) {
11992
+ const preview = text.slice(0, 200);
11993
+ const contentType = response.headers.get("content-type") ?? "(none)";
11994
+ consola.error(`Upstream JSON parse failed at ${routePath}: status=${response.status} content-type="${contentType}" body[0..200]=${JSON.stringify(preview)}`);
11995
+ throw err;
11996
+ }
11997
+ }
11862
11998
  const isWebSearchTool = (tool) => typeof tool.type === "string" && tool.type.startsWith("web_search") || tool.name === "web_search";
11863
11999
  /**
11864
12000
  * Extract whitelisted beta headers from the incoming request to forward
@@ -12072,7 +12208,9 @@ async function handleCompletion(c) {
12072
12208
  headers: streamHeaders
12073
12209
  });
12074
12210
  }
12075
- const responseBody = await parseJsonOrDiagnose(response, c.req.path);
12211
+ const cappedResult = await readResponseBodyCapped(response, c.req.path, NON_STREAMING_BODY_CAP_BYTES);
12212
+ if (!cappedResult.ok) return c.json(cappedResult.errorResponse, cappedResult.status);
12213
+ const responseBody = cappedResult.value;
12076
12214
  logRequest({
12077
12215
  method: "POST",
12078
12216
  path: c.req.path,