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/browser-bridge/index.js +62 -10
- package/dist/main.js +520 -325
- 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", []);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6818
|
+
if (heartbeatHandle !== void 0) {
|
|
6819
|
+
clearInterval(heartbeatHandle);
|
|
6820
|
+
heartbeatHandle = void 0;
|
|
6821
|
+
}
|
|
6680
6822
|
safeClose();
|
|
6681
6823
|
}
|
|
6682
6824
|
},
|
|
6683
6825
|
cancel() {
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
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
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
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
|
-
|
|
7216
|
-
|
|
7217
|
-
}
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
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:
|
|
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
|
-
|
|
7233
|
-
block
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
7256
|
-
if (
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
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
|
-
|
|
7313
|
-
capturedBlocks,
|
|
7314
|
-
advisorToolUse
|
|
7315
|
-
};
|
|
7493
|
+
}
|
|
7316
7494
|
}
|
|
7495
|
+
return {
|
|
7496
|
+
capturedBlocks,
|
|
7497
|
+
advisorToolUse
|
|
7498
|
+
};
|
|
7317
7499
|
}
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
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
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
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
|
-
|
|
7353
|
-
|
|
7568
|
+
if (aborter.signal.aborted) return;
|
|
7569
|
+
const finalIndex = nextSyntheticIndex++;
|
|
7570
|
+
safeEnqueueEvent("content_block_start", {
|
|
7354
7571
|
type: "content_block_start",
|
|
7355
|
-
index:
|
|
7572
|
+
index: finalIndex,
|
|
7356
7573
|
content_block: {
|
|
7357
|
-
type: "
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
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
|
-
})
|
|
7365
|
-
|
|
7585
|
+
});
|
|
7586
|
+
safeEnqueueEvent("content_block_stop", {
|
|
7366
7587
|
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
|
-
}]
|
|
7588
|
+
index: finalIndex
|
|
7376
7589
|
});
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
})
|
|
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
|
-
|
|
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 {}
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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({
|