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/browser-bridge/index.js +62 -10
- package/dist/main.js +435 -297
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6696
|
+
if (heartbeatHandle !== void 0) {
|
|
6697
|
+
clearInterval(heartbeatHandle);
|
|
6698
|
+
heartbeatHandle = void 0;
|
|
6699
|
+
}
|
|
6680
6700
|
safeClose();
|
|
6681
6701
|
}
|
|
6682
6702
|
},
|
|
6683
6703
|
cancel() {
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
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
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
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
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
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
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
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
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
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
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
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
|
-
|
|
7216
|
-
|
|
7217
|
-
}
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
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:
|
|
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
|
-
|
|
7233
|
-
block
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
7256
|
-
if (
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
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
|
-
|
|
7313
|
-
capturedBlocks,
|
|
7314
|
-
advisorToolUse
|
|
7315
|
-
};
|
|
7343
|
+
}
|
|
7316
7344
|
}
|
|
7345
|
+
return {
|
|
7346
|
+
capturedBlocks,
|
|
7347
|
+
advisorToolUse
|
|
7348
|
+
};
|
|
7317
7349
|
}
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
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
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
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
|
-
|
|
7353
|
-
|
|
7418
|
+
if (aborter.signal.aborted) return;
|
|
7419
|
+
const finalIndex = nextSyntheticIndex++;
|
|
7420
|
+
safeEnqueueEvent("content_block_start", {
|
|
7354
7421
|
type: "content_block_start",
|
|
7355
|
-
index:
|
|
7422
|
+
index: finalIndex,
|
|
7356
7423
|
content_block: {
|
|
7357
|
-
type: "
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
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
|
-
})
|
|
7365
|
-
|
|
7435
|
+
});
|
|
7436
|
+
safeEnqueueEvent("content_block_stop", {
|
|
7366
7437
|
type: "content_block_stop",
|
|
7367
|
-
index:
|
|
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
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
})
|
|
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
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
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,
|
|
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.
|
|
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
|
|
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,
|