@khalilgharbaoui/opencode-claude-code-plugin 0.9.3 → 0.10.0
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/README.md +32 -5
- package/dist/index.d.ts +18 -0
- package/dist/index.js +507 -211
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -733,6 +733,10 @@ import * as fs2 from "fs";
|
|
|
733
733
|
import * as path2 from "path";
|
|
734
734
|
import * as os2 from "os";
|
|
735
735
|
import * as crypto from "crypto";
|
|
736
|
+
import {
|
|
737
|
+
parse as parseJsonc,
|
|
738
|
+
printParseErrorCode
|
|
739
|
+
} from "jsonc-parser";
|
|
736
740
|
|
|
737
741
|
// src/tmp.ts
|
|
738
742
|
import * as fs from "fs";
|
|
@@ -776,49 +780,18 @@ function dirExists(p) {
|
|
|
776
780
|
return false;
|
|
777
781
|
}
|
|
778
782
|
}
|
|
779
|
-
function stripJsonComments(text) {
|
|
780
|
-
let out = "";
|
|
781
|
-
let i = 0;
|
|
782
|
-
let inString = null;
|
|
783
|
-
while (i < text.length) {
|
|
784
|
-
const c = text[i];
|
|
785
|
-
if (inString) {
|
|
786
|
-
out += c;
|
|
787
|
-
if (c === "\\" && i + 1 < text.length) {
|
|
788
|
-
out += text[i + 1];
|
|
789
|
-
i += 2;
|
|
790
|
-
continue;
|
|
791
|
-
}
|
|
792
|
-
if (c === inString) inString = null;
|
|
793
|
-
i++;
|
|
794
|
-
continue;
|
|
795
|
-
}
|
|
796
|
-
if (c === '"' || c === "'") {
|
|
797
|
-
inString = c;
|
|
798
|
-
out += c;
|
|
799
|
-
i++;
|
|
800
|
-
continue;
|
|
801
|
-
}
|
|
802
|
-
if (c === "/" && text[i + 1] === "/") {
|
|
803
|
-
while (i < text.length && text[i] !== "\n") i++;
|
|
804
|
-
continue;
|
|
805
|
-
}
|
|
806
|
-
if (c === "/" && text[i + 1] === "*") {
|
|
807
|
-
i += 2;
|
|
808
|
-
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
|
|
809
|
-
i++;
|
|
810
|
-
i += 2;
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
out += c;
|
|
814
|
-
i++;
|
|
815
|
-
}
|
|
816
|
-
return out;
|
|
817
|
-
}
|
|
818
783
|
function readAndParse(file) {
|
|
819
784
|
try {
|
|
820
785
|
const raw = fs2.readFileSync(file, "utf8");
|
|
821
|
-
|
|
786
|
+
const errors = [];
|
|
787
|
+
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
788
|
+
if (errors.length > 0) {
|
|
789
|
+
const first = errors[0];
|
|
790
|
+
throw new Error(
|
|
791
|
+
`${printParseErrorCode(first.error)} at offset ${first.offset}`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
return parsed;
|
|
822
795
|
} catch (e) {
|
|
823
796
|
log.warn("failed to parse opencode config", {
|
|
824
797
|
file,
|
|
@@ -1243,6 +1216,8 @@ function cliSupportsThinking(v) {
|
|
|
1243
1216
|
var activeProcesses = /* @__PURE__ */ new Map();
|
|
1244
1217
|
var claudeSessions = /* @__PURE__ */ new Map();
|
|
1245
1218
|
var MAX_ACTIVE_PROCESSES = 16;
|
|
1219
|
+
var PROCESS_EXIT_TIMEOUT_MS = 1500;
|
|
1220
|
+
var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
|
|
1246
1221
|
function envFlagEnabled(value) {
|
|
1247
1222
|
if (value === void 0) return false;
|
|
1248
1223
|
const normalized = value.trim().toLowerCase();
|
|
@@ -1289,13 +1264,54 @@ function getActiveProcess(key) {
|
|
|
1289
1264
|
function setActiveProcess(key, ap) {
|
|
1290
1265
|
activeProcesses.set(key, ap);
|
|
1291
1266
|
}
|
|
1292
|
-
function
|
|
1267
|
+
function detachActiveProcess(key) {
|
|
1293
1268
|
const ap = activeProcesses.get(key);
|
|
1294
|
-
if (ap)
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1269
|
+
if (!ap) return void 0;
|
|
1270
|
+
activeProcesses.delete(key);
|
|
1271
|
+
void ap.proxyServer?.close();
|
|
1272
|
+
return ap;
|
|
1273
|
+
}
|
|
1274
|
+
function deleteActiveProcess(key) {
|
|
1275
|
+
const ap = detachActiveProcess(key);
|
|
1276
|
+
ap?.proc.kill();
|
|
1277
|
+
}
|
|
1278
|
+
function hasProcessExited(proc) {
|
|
1279
|
+
return proc.exitCode !== null || proc.signalCode !== null;
|
|
1280
|
+
}
|
|
1281
|
+
function waitForProcessExit(proc, timeoutMs) {
|
|
1282
|
+
if (hasProcessExited(proc)) return Promise.resolve(true);
|
|
1283
|
+
return new Promise((resolve4) => {
|
|
1284
|
+
const onExit = () => {
|
|
1285
|
+
clearTimeout(timer);
|
|
1286
|
+
resolve4(true);
|
|
1287
|
+
};
|
|
1288
|
+
const timer = setTimeout(() => {
|
|
1289
|
+
proc.off("exit", onExit);
|
|
1290
|
+
resolve4(hasProcessExited(proc));
|
|
1291
|
+
}, timeoutMs);
|
|
1292
|
+
proc.once("exit", onExit);
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
async function deleteActiveProcessAndWait(key, options = {}) {
|
|
1296
|
+
const ap = detachActiveProcess(key);
|
|
1297
|
+
if (!ap || hasProcessExited(ap.proc)) return true;
|
|
1298
|
+
const gracefulExit = waitForProcessExit(
|
|
1299
|
+
ap.proc,
|
|
1300
|
+
options.exitTimeoutMs ?? PROCESS_EXIT_TIMEOUT_MS
|
|
1301
|
+
);
|
|
1302
|
+
ap.proc.kill();
|
|
1303
|
+
if (await gracefulExit) return true;
|
|
1304
|
+
const forcedExit = waitForProcessExit(
|
|
1305
|
+
ap.proc,
|
|
1306
|
+
options.forceExitTimeoutMs ?? PROCESS_FORCE_EXIT_TIMEOUT_MS
|
|
1307
|
+
);
|
|
1308
|
+
ap.proc.kill("SIGKILL");
|
|
1309
|
+
if (await forcedExit) return true;
|
|
1310
|
+
log.warn("claude process did not exit; starting a fresh session", {
|
|
1311
|
+
sessionKey: key
|
|
1312
|
+
});
|
|
1313
|
+
deleteClaudeSessionId(key);
|
|
1314
|
+
return false;
|
|
1299
1315
|
}
|
|
1300
1316
|
function getClaudeSessionId(key) {
|
|
1301
1317
|
return claudeSessions.get(key);
|
|
@@ -1343,8 +1359,9 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
1343
1359
|
void unlink(systemPromptFile).catch(() => {
|
|
1344
1360
|
});
|
|
1345
1361
|
}
|
|
1346
|
-
activeProcesses.
|
|
1347
|
-
if (
|
|
1362
|
+
const ownsSessionKey = activeProcesses.get(sessionKey2) === ap;
|
|
1363
|
+
if (ownsSessionKey) activeProcesses.delete(sessionKey2);
|
|
1364
|
+
if (ownsSessionKey && code !== 0 && code !== null) {
|
|
1348
1365
|
log.info("process exited with error, clearing session", {
|
|
1349
1366
|
code,
|
|
1350
1367
|
sessionKey: sessionKey2
|
|
@@ -1355,16 +1372,50 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
1355
1372
|
proc.stderr?.on("data", (data) => {
|
|
1356
1373
|
const stderr = data.toString();
|
|
1357
1374
|
log.debug("stderr", { data: stderr.slice(0, 200) });
|
|
1358
|
-
if (stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1375
|
+
if (stderr.includes("No conversation found") || stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
|
|
1376
|
+
if (activeProcesses.get(sessionKey2) === ap) {
|
|
1377
|
+
log.warn("claude session ID error, clearing session", {
|
|
1378
|
+
sessionKey: sessionKey2,
|
|
1379
|
+
error: stderr.slice(0, 200)
|
|
1380
|
+
});
|
|
1381
|
+
claudeSessions.delete(sessionKey2);
|
|
1382
|
+
} else {
|
|
1383
|
+
log.debug("ignoring session ID error from stale claude process", {
|
|
1384
|
+
sessionKey: sessionKey2
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1364
1387
|
}
|
|
1365
1388
|
});
|
|
1366
1389
|
return ap;
|
|
1367
1390
|
}
|
|
1391
|
+
function appendResumeIfNeeded(sessionKey2, cliArgs) {
|
|
1392
|
+
if (cliArgs.includes("--resume") || cliArgs.includes("--session-id")) {
|
|
1393
|
+
return cliArgs;
|
|
1394
|
+
}
|
|
1395
|
+
const sid = claudeSessions.get(sessionKey2);
|
|
1396
|
+
if (!sid) return cliArgs;
|
|
1397
|
+
return [...cliArgs, "--resume", sid];
|
|
1398
|
+
}
|
|
1399
|
+
function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropicApiKey) {
|
|
1400
|
+
const old = activeProcesses.get(sessionKey2);
|
|
1401
|
+
if (!old) return void 0;
|
|
1402
|
+
activeProcesses.delete(sessionKey2);
|
|
1403
|
+
old.proc.removeAllListeners("exit");
|
|
1404
|
+
try {
|
|
1405
|
+
old.proc.kill();
|
|
1406
|
+
} catch {
|
|
1407
|
+
}
|
|
1408
|
+
return spawnClaudeProcess(
|
|
1409
|
+
cliPath,
|
|
1410
|
+
appendResumeIfNeeded(sessionKey2, cliArgs),
|
|
1411
|
+
cwd,
|
|
1412
|
+
sessionKey2,
|
|
1413
|
+
old.proxyServer,
|
|
1414
|
+
old.mcpHash,
|
|
1415
|
+
old.systemPromptFile,
|
|
1416
|
+
ignoreAnthropicApiKey
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1368
1419
|
function buildCliArgs(opts) {
|
|
1369
1420
|
const {
|
|
1370
1421
|
sessionKey: sessionKey2,
|
|
@@ -1398,7 +1449,7 @@ function buildCliArgs(opts) {
|
|
|
1398
1449
|
if (includeSessionId) {
|
|
1399
1450
|
const sessionId = claudeSessions.get(sessionKey2);
|
|
1400
1451
|
if (sessionId && !activeProcesses.has(sessionKey2)) {
|
|
1401
|
-
args.push("--
|
|
1452
|
+
args.push("--resume", sessionId);
|
|
1402
1453
|
}
|
|
1403
1454
|
}
|
|
1404
1455
|
if (mcpConfig) {
|
|
@@ -1990,10 +2041,61 @@ import * as fs4 from "fs";
|
|
|
1990
2041
|
import * as path4 from "path";
|
|
1991
2042
|
import * as crypto2 from "crypto";
|
|
1992
2043
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
2044
|
+
var SERVER_CLOSED_MESSAGE = "proxy MCP server closed";
|
|
2045
|
+
function isExpectedCleanupError(message) {
|
|
2046
|
+
return message.includes("timed out after") && message.includes("waiting for opencode to resolve") || message.includes("rejecting as orphaned") || message.includes("was orphaned by a new user turn") || message.includes("stream was aborted") || message.includes(SERVER_CLOSED_MESSAGE);
|
|
2047
|
+
}
|
|
1993
2048
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
1994
2049
|
var SERVER_NAME = "opencode_proxy";
|
|
1995
2050
|
var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
|
|
1996
|
-
var
|
|
2051
|
+
var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2052
|
+
var PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS = {
|
|
2053
|
+
task: 60 * 60 * 1e3
|
|
2054
|
+
// 60 min
|
|
2055
|
+
};
|
|
2056
|
+
var MAX_PROXY_TIMEOUT_MS = 2 ** 31 - 1;
|
|
2057
|
+
function resolveProxyCallTimeoutMs(toolName, input, overrides) {
|
|
2058
|
+
const key = toolName.toLowerCase();
|
|
2059
|
+
let ms = PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS[key] ?? PROXY_DEFAULT_TIMEOUT_MS;
|
|
2060
|
+
if (overrides) {
|
|
2061
|
+
const ov = lookupCaseInsensitive(overrides, key);
|
|
2062
|
+
if (typeof ov === "number" && ov > 0) ms = ov;
|
|
2063
|
+
}
|
|
2064
|
+
if (key === "bash") {
|
|
2065
|
+
const requested = input?.timeout;
|
|
2066
|
+
if (typeof requested === "number" && requested > ms) ms = requested;
|
|
2067
|
+
}
|
|
2068
|
+
return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
|
|
2069
|
+
}
|
|
2070
|
+
function lookupCaseInsensitive(map, key) {
|
|
2071
|
+
if (Object.prototype.hasOwnProperty.call(map, key)) return map[key];
|
|
2072
|
+
for (const k of Object.keys(map)) {
|
|
2073
|
+
if (k.toLowerCase() === key) return map[k];
|
|
2074
|
+
}
|
|
2075
|
+
return void 0;
|
|
2076
|
+
}
|
|
2077
|
+
function resolveProxyClientCeilingMs(overrides) {
|
|
2078
|
+
let ms = PROXY_DEFAULT_TIMEOUT_MS;
|
|
2079
|
+
for (const v of Object.values(PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS)) {
|
|
2080
|
+
if (v > ms) ms = v;
|
|
2081
|
+
}
|
|
2082
|
+
if (overrides) {
|
|
2083
|
+
for (const v of Object.values(overrides)) {
|
|
2084
|
+
if (typeof v === "number" && v > ms) ms = v;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
|
|
2088
|
+
}
|
|
2089
|
+
function buildProxyTimeoutError(toolName, ms) {
|
|
2090
|
+
const key = toolName.toLowerCase();
|
|
2091
|
+
const base = `Proxy tool '${toolName}' timed out after ${ms}ms waiting for opencode to resolve the call`;
|
|
2092
|
+
if (key === "task") {
|
|
2093
|
+
return new Error(
|
|
2094
|
+
base + " (the subagent). The subagent may still be running but its result is no longer reachable in this session. Do not declare the dispatch failed, and do not 'schedule a wake-up' or defer -- that mechanism does not apply here. If the result is required, re-dispatch or verify it directly now."
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
return new Error(base);
|
|
2098
|
+
}
|
|
1997
2099
|
var DEFAULT_PROXY_TOOLS = [
|
|
1998
2100
|
{
|
|
1999
2101
|
name: "bash",
|
|
@@ -2086,7 +2188,7 @@ var DEFAULT_PROXY_TOOLS = [
|
|
|
2086
2188
|
},
|
|
2087
2189
|
{
|
|
2088
2190
|
name: "task",
|
|
2089
|
-
description: "Launch an opencode subagent to handle a complex multi-step task autonomously. Routed through opencode's task tool so subagent orchestration, permission, and lifecycle are handled by opencode. Use `subagent_type` to pick which configured subagent runs (e.g. `build`, `general`, `explore`, or any custom subagent declared in opencode.json).
|
|
2191
|
+
description: "Launch an opencode subagent to handle a complex multi-step task autonomously. Routed through opencode's task tool so subagent orchestration, permission, and lifecycle are handled by opencode. Use `subagent_type` to pick which configured subagent runs (e.g. `build`, `general`, `explore`, or any custom subagent declared in opencode.json). Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. Task calls get a 60-minute proxy deadline by default (configurable via proxyToolTimeoutMs).",
|
|
2090
2192
|
inputSchema: {
|
|
2091
2193
|
type: "object",
|
|
2092
2194
|
properties: {
|
|
@@ -2109,13 +2211,17 @@ var DEFAULT_PROXY_TOOLS = [
|
|
|
2109
2211
|
command: {
|
|
2110
2212
|
type: "string",
|
|
2111
2213
|
description: "The command that triggered this task"
|
|
2214
|
+
},
|
|
2215
|
+
background: {
|
|
2216
|
+
type: "boolean",
|
|
2217
|
+
description: "Run the task in the background when supported by opencode"
|
|
2112
2218
|
}
|
|
2113
2219
|
},
|
|
2114
2220
|
required: ["description", "prompt", "subagent_type"]
|
|
2115
2221
|
}
|
|
2116
2222
|
}
|
|
2117
2223
|
];
|
|
2118
|
-
async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
2224
|
+
async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides) {
|
|
2119
2225
|
const calls = new EventEmitter3();
|
|
2120
2226
|
const pending = /* @__PURE__ */ new Map();
|
|
2121
2227
|
const server2 = createServer(async (req, res) => {
|
|
@@ -2124,13 +2230,17 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
2124
2230
|
res.end();
|
|
2125
2231
|
return;
|
|
2126
2232
|
}
|
|
2233
|
+
let requestId = null;
|
|
2234
|
+
let requestMethod = null;
|
|
2127
2235
|
try {
|
|
2128
2236
|
const body = await readBody(req);
|
|
2129
2237
|
const request = JSON.parse(body);
|
|
2238
|
+
requestId = request?.id ?? null;
|
|
2239
|
+
requestMethod = typeof request?.method === "string" ? request.method : null;
|
|
2130
2240
|
if (request?.jsonrpc !== "2.0" || typeof request.method !== "string") {
|
|
2131
2241
|
writeJson(res, {
|
|
2132
2242
|
jsonrpc: "2.0",
|
|
2133
|
-
id:
|
|
2243
|
+
id: requestId,
|
|
2134
2244
|
error: { code: -32600, message: "Invalid request" }
|
|
2135
2245
|
});
|
|
2136
2246
|
return;
|
|
@@ -2142,7 +2252,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
2142
2252
|
if (request.method === "initialize") {
|
|
2143
2253
|
writeJson(res, {
|
|
2144
2254
|
jsonrpc: "2.0",
|
|
2145
|
-
id:
|
|
2255
|
+
id: requestId,
|
|
2146
2256
|
result: {
|
|
2147
2257
|
protocolVersion: PROTOCOL_VERSION,
|
|
2148
2258
|
capabilities: { tools: {} },
|
|
@@ -2162,7 +2272,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
2162
2272
|
if (request.method === "tools/list") {
|
|
2163
2273
|
writeJson(res, {
|
|
2164
2274
|
jsonrpc: "2.0",
|
|
2165
|
-
id:
|
|
2275
|
+
id: requestId,
|
|
2166
2276
|
result: {
|
|
2167
2277
|
tools: tools.map((t) => ({
|
|
2168
2278
|
name: t.name,
|
|
@@ -2180,10 +2290,10 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
2180
2290
|
if (!tools.some((t) => t.name === toolName)) {
|
|
2181
2291
|
writeJson(res, {
|
|
2182
2292
|
jsonrpc: "2.0",
|
|
2183
|
-
id:
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2293
|
+
id: requestId,
|
|
2294
|
+
result: {
|
|
2295
|
+
content: [{ type: "text", text: `Unknown proxy tool: ${toolName}` }],
|
|
2296
|
+
isError: true
|
|
2187
2297
|
}
|
|
2188
2298
|
});
|
|
2189
2299
|
return;
|
|
@@ -2205,20 +2315,21 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
2205
2315
|
reject
|
|
2206
2316
|
};
|
|
2207
2317
|
pending.set(callId, entry);
|
|
2318
|
+
const deadlineMs = resolveProxyCallTimeoutMs(
|
|
2319
|
+
toolName,
|
|
2320
|
+
input,
|
|
2321
|
+
timeoutOverrides
|
|
2322
|
+
);
|
|
2208
2323
|
timer = setTimeout(() => {
|
|
2209
2324
|
if (!pending.has(callId)) return;
|
|
2210
2325
|
pending.delete(callId);
|
|
2211
2326
|
log.notice("proxy-mcp tool call timed out", {
|
|
2212
2327
|
callId,
|
|
2213
2328
|
toolName,
|
|
2214
|
-
|
|
2329
|
+
deadlineMs
|
|
2215
2330
|
});
|
|
2216
|
-
reject(
|
|
2217
|
-
|
|
2218
|
-
`Proxy tool '${toolName}' timed out after ${PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
|
|
2219
|
-
)
|
|
2220
|
-
);
|
|
2221
|
-
}, PROXY_CALL_TIMEOUT_MS);
|
|
2331
|
+
reject(buildProxyTimeoutError(toolName, deadlineMs));
|
|
2332
|
+
}, deadlineMs);
|
|
2222
2333
|
calls.emit("call", entry);
|
|
2223
2334
|
}
|
|
2224
2335
|
).finally(() => {
|
|
@@ -2228,17 +2339,17 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
2228
2339
|
if (result.kind === "error") {
|
|
2229
2340
|
writeJson(res, {
|
|
2230
2341
|
jsonrpc: "2.0",
|
|
2231
|
-
id:
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2342
|
+
id: requestId,
|
|
2343
|
+
result: {
|
|
2344
|
+
content: [{ type: "text", text: result.message }],
|
|
2345
|
+
isError: true
|
|
2235
2346
|
}
|
|
2236
2347
|
});
|
|
2237
2348
|
return;
|
|
2238
2349
|
}
|
|
2239
2350
|
writeJson(res, {
|
|
2240
2351
|
jsonrpc: "2.0",
|
|
2241
|
-
id:
|
|
2352
|
+
id: requestId,
|
|
2242
2353
|
result: {
|
|
2243
2354
|
content: [{ type: "text", text: result.text }],
|
|
2244
2355
|
isError: result.isError === true
|
|
@@ -2248,20 +2359,38 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
2248
2359
|
}
|
|
2249
2360
|
writeJson(res, {
|
|
2250
2361
|
jsonrpc: "2.0",
|
|
2251
|
-
id:
|
|
2362
|
+
id: requestId,
|
|
2252
2363
|
error: { code: -32601, message: `Unknown method: ${request.method}` }
|
|
2253
2364
|
});
|
|
2254
2365
|
} catch (error) {
|
|
2255
2366
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2256
|
-
const
|
|
2257
|
-
const logFn = isExpectedCleanup ? log.notice : log.warn;
|
|
2367
|
+
const logFn = isExpectedCleanupError(errorMessage) ? log.notice : log.warn;
|
|
2258
2368
|
logFn("proxy-mcp error handling request", {
|
|
2259
2369
|
error: errorMessage
|
|
2260
2370
|
});
|
|
2371
|
+
if (requestMethod === "tools/call") {
|
|
2372
|
+
try {
|
|
2373
|
+
writeJson(res, {
|
|
2374
|
+
jsonrpc: "2.0",
|
|
2375
|
+
id: requestId,
|
|
2376
|
+
result: {
|
|
2377
|
+
content: [{ type: "text", text: errorMessage }],
|
|
2378
|
+
isError: true
|
|
2379
|
+
}
|
|
2380
|
+
});
|
|
2381
|
+
} catch {
|
|
2382
|
+
try {
|
|
2383
|
+
res.statusCode = 500;
|
|
2384
|
+
res.end();
|
|
2385
|
+
} catch {
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2261
2390
|
try {
|
|
2262
2391
|
writeJson(res, {
|
|
2263
2392
|
jsonrpc: "2.0",
|
|
2264
|
-
id:
|
|
2393
|
+
id: requestId,
|
|
2265
2394
|
error: {
|
|
2266
2395
|
code: -32603,
|
|
2267
2396
|
message: error instanceof Error ? error.message : "Internal error"
|
|
@@ -2306,7 +2435,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
2306
2435
|
mcpServers: {
|
|
2307
2436
|
[SERVER_NAME]: {
|
|
2308
2437
|
type: "http",
|
|
2309
|
-
url
|
|
2438
|
+
url,
|
|
2439
|
+
timeout: resolveProxyClientCeilingMs(timeoutOverrides)
|
|
2310
2440
|
}
|
|
2311
2441
|
}
|
|
2312
2442
|
},
|
|
@@ -2324,7 +2454,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
2324
2454
|
},
|
|
2325
2455
|
async close() {
|
|
2326
2456
|
for (const entry of pending.values()) {
|
|
2327
|
-
entry.reject(new Error(
|
|
2457
|
+
entry.reject(new Error(SERVER_CLOSED_MESSAGE));
|
|
2328
2458
|
}
|
|
2329
2459
|
pending.clear();
|
|
2330
2460
|
await new Promise((resolve4) => {
|
|
@@ -2386,7 +2516,6 @@ import { EventEmitter as EventEmitter4 } from "events";
|
|
|
2386
2516
|
var pendingByCallId = /* @__PURE__ */ new Map();
|
|
2387
2517
|
var callIdsBySession = /* @__PURE__ */ new Map();
|
|
2388
2518
|
var emitter = new EventEmitter4();
|
|
2389
|
-
var PENDING_PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2390
2519
|
function eventName(sessionKey2) {
|
|
2391
2520
|
return `pending:${sessionKey2}`;
|
|
2392
2521
|
}
|
|
@@ -2409,7 +2538,7 @@ function onPendingProxyCall(sessionKey2, handler) {
|
|
|
2409
2538
|
emitter.on(name, handler);
|
|
2410
2539
|
return () => emitter.off(name, handler);
|
|
2411
2540
|
}
|
|
2412
|
-
function queuePendingProxyCall(sessionKey2, call) {
|
|
2541
|
+
function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
|
|
2413
2542
|
const previous = pendingByCallId.get(call.id);
|
|
2414
2543
|
if (previous) {
|
|
2415
2544
|
clearTimeout(previous.timer);
|
|
@@ -2419,23 +2548,24 @@ function queuePendingProxyCall(sessionKey2, call) {
|
|
|
2419
2548
|
pendingByCallId.delete(call.id);
|
|
2420
2549
|
indexRemove(previous.sessionKey, call.id);
|
|
2421
2550
|
}
|
|
2551
|
+
const deadlineMs = resolveProxyCallTimeoutMs(
|
|
2552
|
+
call.toolName,
|
|
2553
|
+
call.input,
|
|
2554
|
+
timeoutOverrides
|
|
2555
|
+
);
|
|
2422
2556
|
const timer = setTimeout(() => {
|
|
2423
2557
|
const current = pendingByCallId.get(call.id);
|
|
2424
2558
|
if (!current) return;
|
|
2425
2559
|
pendingByCallId.delete(call.id);
|
|
2426
2560
|
indexRemove(current.sessionKey, call.id);
|
|
2427
|
-
current.reject(
|
|
2428
|
-
new Error(
|
|
2429
|
-
`Proxy tool call '${call.toolName}' timed out after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
|
|
2430
|
-
)
|
|
2431
|
-
);
|
|
2561
|
+
current.reject(buildProxyTimeoutError(call.toolName, deadlineMs));
|
|
2432
2562
|
log.notice("timed out pending proxy call", {
|
|
2433
2563
|
sessionKey: current.sessionKey,
|
|
2434
2564
|
toolCallId: call.id,
|
|
2435
2565
|
toolName: call.toolName,
|
|
2436
|
-
|
|
2566
|
+
deadlineMs
|
|
2437
2567
|
});
|
|
2438
|
-
},
|
|
2568
|
+
}, deadlineMs);
|
|
2439
2569
|
const pending = {
|
|
2440
2570
|
sessionKey: sessionKey2,
|
|
2441
2571
|
toolCallId: call.id,
|
|
@@ -2574,6 +2704,7 @@ function hasNewUserContent(prompt) {
|
|
|
2574
2704
|
var AUTO_CONTINUE_MAX_ATTEMPTS = 8;
|
|
2575
2705
|
var AUTO_CONTINUE_MAX_ELAPSED_MS = 10 * 60 * 1e3;
|
|
2576
2706
|
var AUTO_CONTINUE_NO_PROGRESS_LIMIT = 2;
|
|
2707
|
+
var PROXY_RESULT_BOUNDARY_GRACE_MS = 250;
|
|
2577
2708
|
var AUTO_CONTINUE_PROMPT = "Continue the task from where you stopped. Do not summarize; keep working until the requested task is complete, you need clarification, or you hit a real blocker.";
|
|
2578
2709
|
function normalizeVisibleText(text) {
|
|
2579
2710
|
return text.replace(/\s+/g, " ").trim();
|
|
@@ -2911,9 +3042,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
2911
3042
|
* The process lifecycle owns the server lifecycle via session-manager.
|
|
2912
3043
|
*/
|
|
2913
3044
|
async ensureProxyServer(tools, sessionKeyForCalls) {
|
|
2914
|
-
const
|
|
3045
|
+
const timeoutOverrides = this.config.proxyToolTimeoutMs;
|
|
3046
|
+
const srv = await createProxyMcpServer(tools, timeoutOverrides);
|
|
2915
3047
|
srv.calls.on("call", (call) => {
|
|
2916
|
-
queuePendingProxyCall(sessionKeyForCalls, call);
|
|
3048
|
+
queuePendingProxyCall(sessionKeyForCalls, call, timeoutOverrides);
|
|
2917
3049
|
});
|
|
2918
3050
|
return srv;
|
|
2919
3051
|
}
|
|
@@ -3668,22 +3800,32 @@ ${plan}
|
|
|
3668
3800
|
let activeProcess = getActiveProcess(sk);
|
|
3669
3801
|
let proc;
|
|
3670
3802
|
let lineEmitter;
|
|
3803
|
+
let cliArgs;
|
|
3671
3804
|
let proxyServer = activeProcess?.proxyServer ?? null;
|
|
3672
|
-
if (!compactionMode && activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
|
|
3673
|
-
const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
|
|
3674
|
-
const previousHash = activeProcess.mcpHash ?? null;
|
|
3675
|
-
if (previousHash !== probe.bridgedHash) {
|
|
3676
|
-
log.info("opencode MCP config changed, respawning claude", {
|
|
3677
|
-
sk,
|
|
3678
|
-
previousHash,
|
|
3679
|
-
currentHash: probe.bridgedHash
|
|
3680
|
-
});
|
|
3681
|
-
deleteActiveProcess(sk);
|
|
3682
|
-
activeProcess = void 0;
|
|
3683
|
-
proxyServer = null;
|
|
3684
|
-
}
|
|
3685
|
-
}
|
|
3686
3805
|
const setup = async () => {
|
|
3806
|
+
if (!compactionMode && activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
|
|
3807
|
+
const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
|
|
3808
|
+
const previousHash = activeProcess.mcpHash ?? null;
|
|
3809
|
+
if (previousHash !== probe.bridgedHash) {
|
|
3810
|
+
if (previousPendingProxyCalls.length > 0) {
|
|
3811
|
+
log.info("deferring MCP hot reload until proxy calls resolve", {
|
|
3812
|
+
sk,
|
|
3813
|
+
previousHash,
|
|
3814
|
+
currentHash: probe.bridgedHash,
|
|
3815
|
+
pendingCalls: previousPendingProxyCalls.length
|
|
3816
|
+
});
|
|
3817
|
+
} else {
|
|
3818
|
+
log.info("opencode MCP config changed, respawning claude", {
|
|
3819
|
+
sk,
|
|
3820
|
+
previousHash,
|
|
3821
|
+
currentHash: probe.bridgedHash
|
|
3822
|
+
});
|
|
3823
|
+
await deleteActiveProcessAndWait(sk);
|
|
3824
|
+
activeProcess = void 0;
|
|
3825
|
+
proxyServer = null;
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3687
3829
|
if (useInteractive && !compactionMode) {
|
|
3688
3830
|
const mcp = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
|
|
3689
3831
|
if (activeProcess) {
|
|
@@ -3744,7 +3886,6 @@ ${plan}
|
|
|
3744
3886
|
});
|
|
3745
3887
|
}
|
|
3746
3888
|
} else {
|
|
3747
|
-
let cliArgs;
|
|
3748
3889
|
let spawnSystemPromptFile;
|
|
3749
3890
|
let spawnProxyServer = null;
|
|
3750
3891
|
let spawnMcpHash = null;
|
|
@@ -3847,6 +3988,7 @@ ${plan}
|
|
|
3847
3988
|
let controllerClosed = false;
|
|
3848
3989
|
let pendingProxyUnsubscribe = null;
|
|
3849
3990
|
let resultFallbackTimer = null;
|
|
3991
|
+
let pendingResultCompletion = null;
|
|
3850
3992
|
let hasReceivedContent = false;
|
|
3851
3993
|
let visibleTextSinceContinue = "";
|
|
3852
3994
|
let lastVisibleTextSinceContinue = "";
|
|
@@ -3877,6 +4019,103 @@ ${plan}
|
|
|
3877
4019
|
closeHandler();
|
|
3878
4020
|
}, delayMs);
|
|
3879
4021
|
};
|
|
4022
|
+
const START_WATCHDOG_MS = (() => {
|
|
4023
|
+
const env = process.env.CLAUDE_CODE_START_WATCHDOG_MS;
|
|
4024
|
+
const parsed = env ? Number.parseInt(env, 10) : NaN;
|
|
4025
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 9e4;
|
|
4026
|
+
})();
|
|
4027
|
+
let startWatchdog = null;
|
|
4028
|
+
let respawnAttempted = false;
|
|
4029
|
+
const clearStartWatchdog = () => {
|
|
4030
|
+
if (startWatchdog) {
|
|
4031
|
+
clearTimeout(startWatchdog);
|
|
4032
|
+
startWatchdog = null;
|
|
4033
|
+
}
|
|
4034
|
+
};
|
|
4035
|
+
const onStartWatchdogFire = () => {
|
|
4036
|
+
startWatchdog = null;
|
|
4037
|
+
if (controllerClosed || hasReceivedContent) return;
|
|
4038
|
+
if (respawnAttempted) {
|
|
4039
|
+
log.error(
|
|
4040
|
+
"claude process still silent after respawn; ending turn",
|
|
4041
|
+
{ sessionKey: sk }
|
|
4042
|
+
);
|
|
4043
|
+
deleteActiveProcess(sk);
|
|
4044
|
+
deleteClaudeSessionId(sk);
|
|
4045
|
+
controllerClosed = true;
|
|
4046
|
+
cleanupTurn();
|
|
4047
|
+
controller.enqueue({
|
|
4048
|
+
type: "error",
|
|
4049
|
+
error: new Error(
|
|
4050
|
+
"Claude process produced no output after the envelope write (start watchdog timeout)."
|
|
4051
|
+
)
|
|
4052
|
+
});
|
|
4053
|
+
try {
|
|
4054
|
+
controller.close();
|
|
4055
|
+
} catch {
|
|
4056
|
+
}
|
|
4057
|
+
return;
|
|
4058
|
+
}
|
|
4059
|
+
respawnAttempted = true;
|
|
4060
|
+
log.warn(
|
|
4061
|
+
"no stdout after envelope write; respawning claude process to resume conversation",
|
|
4062
|
+
{ sessionKey: sk, startWatchdogMs: START_WATCHDOG_MS }
|
|
4063
|
+
);
|
|
4064
|
+
lineEmitter.off("line", lineHandler);
|
|
4065
|
+
lineEmitter.off("close", closeHandler);
|
|
4066
|
+
proc.off("error", procErrorHandler);
|
|
4067
|
+
const newAp = respawnActiveProcess(
|
|
4068
|
+
sk,
|
|
4069
|
+
cliPath,
|
|
4070
|
+
cliArgs,
|
|
4071
|
+
cwd,
|
|
4072
|
+
self.config.ignoreAnthropicApiKey
|
|
4073
|
+
);
|
|
4074
|
+
if (!newAp) {
|
|
4075
|
+
log.error(
|
|
4076
|
+
"no active process to respawn (start watchdog); ending turn",
|
|
4077
|
+
{ sessionKey: sk }
|
|
4078
|
+
);
|
|
4079
|
+
controllerClosed = true;
|
|
4080
|
+
cleanupTurn();
|
|
4081
|
+
controller.enqueue({
|
|
4082
|
+
type: "error",
|
|
4083
|
+
error: new Error(
|
|
4084
|
+
"No active claude process to respawn after start watchdog timeout."
|
|
4085
|
+
)
|
|
4086
|
+
});
|
|
4087
|
+
try {
|
|
4088
|
+
controller.close();
|
|
4089
|
+
} catch {
|
|
4090
|
+
}
|
|
4091
|
+
return;
|
|
4092
|
+
}
|
|
4093
|
+
proc = newAp.proc;
|
|
4094
|
+
lineEmitter = newAp.lineEmitter;
|
|
4095
|
+
activeProcess = newAp;
|
|
4096
|
+
lineEmitter.on("line", lineHandler);
|
|
4097
|
+
lineEmitter.on("close", closeHandler);
|
|
4098
|
+
proc.on("error", procErrorHandler);
|
|
4099
|
+
try {
|
|
4100
|
+
proc.stdin?.write(userMsg + "\n");
|
|
4101
|
+
log.debug("re-sent user message after respawn", {
|
|
4102
|
+
textLength: userMsg.length
|
|
4103
|
+
});
|
|
4104
|
+
} catch (err) {
|
|
4105
|
+
log.error("failed to re-send envelope after respawn", {
|
|
4106
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4107
|
+
});
|
|
4108
|
+
}
|
|
4109
|
+
startWatchdog = setTimeout(
|
|
4110
|
+
onStartWatchdogFire,
|
|
4111
|
+
START_WATCHDOG_MS
|
|
4112
|
+
);
|
|
4113
|
+
};
|
|
4114
|
+
const armStartWatchdog = () => {
|
|
4115
|
+
clearStartWatchdog();
|
|
4116
|
+
if (controllerClosed) return;
|
|
4117
|
+
startWatchdog = setTimeout(onStartWatchdogFire, START_WATCHDOG_MS);
|
|
4118
|
+
};
|
|
3880
4119
|
const toolCallMap = /* @__PURE__ */ new Map();
|
|
3881
4120
|
const skipResultForIds = /* @__PURE__ */ new Set();
|
|
3882
4121
|
const toolCallsById = /* @__PURE__ */ new Map();
|
|
@@ -3932,6 +4171,28 @@ ${plan}
|
|
|
3932
4171
|
});
|
|
3933
4172
|
finishWithToolCalls(batch);
|
|
3934
4173
|
};
|
|
4174
|
+
const settleResultBoundary = () => {
|
|
4175
|
+
drainTimer = null;
|
|
4176
|
+
const completeResult2 = pendingResultCompletion;
|
|
4177
|
+
pendingResultCompletion = null;
|
|
4178
|
+
if (!completeResult2 || controllerClosed) return;
|
|
4179
|
+
if (drainBuffer.length > 0) {
|
|
4180
|
+
drainNow();
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
completeResult2();
|
|
4184
|
+
};
|
|
4185
|
+
const scheduleResultBoundary = (completeResult2, delayMs) => {
|
|
4186
|
+
pendingResultCompletion = completeResult2;
|
|
4187
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
4188
|
+
drainTimer = setTimeout(settleResultBoundary, delayMs);
|
|
4189
|
+
};
|
|
4190
|
+
const noteResultBoundaryCall = () => {
|
|
4191
|
+
if (!pendingResultCompletion) return false;
|
|
4192
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
4193
|
+
drainTimer = setTimeout(settleResultBoundary, DRAIN_QUIET_MS);
|
|
4194
|
+
return true;
|
|
4195
|
+
};
|
|
3935
4196
|
const noteVisibleText = (text) => {
|
|
3936
4197
|
visibleTextSinceContinue += text;
|
|
3937
4198
|
lastVisibleTextSinceContinue += text;
|
|
@@ -3956,11 +4217,106 @@ ${plan}
|
|
|
3956
4217
|
hadProxyActivitySinceContinue = false;
|
|
3957
4218
|
lastStopReason = null;
|
|
3958
4219
|
};
|
|
4220
|
+
const completeResult = (msg) => {
|
|
4221
|
+
if (controllerClosed) return;
|
|
4222
|
+
if (drainBuffer.length > 0) {
|
|
4223
|
+
drainNow();
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
const pendingSiblings = getPendingProxyCalls(sk);
|
|
4227
|
+
if (pendingSiblings.length > 0) {
|
|
4228
|
+
log.info("leaving parallel proxy calls pending at result boundary", {
|
|
4229
|
+
sessionKey: sk,
|
|
4230
|
+
count: pendingSiblings.length
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
const autoDecision = shouldAutoContinueIncompleteTurn(
|
|
4234
|
+
autoContinueState,
|
|
4235
|
+
{
|
|
4236
|
+
text: visibleTextSinceContinue,
|
|
4237
|
+
lastVisibleText: lastVisibleTextSinceContinue,
|
|
4238
|
+
hadReasoning: hadReasoningSinceContinue,
|
|
4239
|
+
hadToolActivity: hadToolActivitySinceContinue,
|
|
4240
|
+
hadProxyActivity: hadProxyActivitySinceContinue,
|
|
4241
|
+
isError: msg.is_error,
|
|
4242
|
+
stopReason: lastStopReason
|
|
4243
|
+
}
|
|
4244
|
+
);
|
|
4245
|
+
if (autoDecision.continue) {
|
|
4246
|
+
const signature = continuationSignature({
|
|
4247
|
+
text: visibleTextSinceContinue,
|
|
4248
|
+
lastVisibleText: lastVisibleTextSinceContinue,
|
|
4249
|
+
hadReasoning: hadReasoningSinceContinue,
|
|
4250
|
+
hadToolActivity: hadToolActivitySinceContinue,
|
|
4251
|
+
hadProxyActivity: hadProxyActivitySinceContinue,
|
|
4252
|
+
isError: msg.is_error
|
|
4253
|
+
});
|
|
4254
|
+
autoContinueState.noProgressCount = signature === autoContinueState.lastSignature ? autoContinueState.noProgressCount + 1 : 0;
|
|
4255
|
+
autoContinueState.lastSignature = signature;
|
|
4256
|
+
autoContinueState.attempts++;
|
|
4257
|
+
log.notice("auto-continuing incomplete claude result", {
|
|
4258
|
+
sessionKey: sk,
|
|
4259
|
+
reason: autoDecision.reason,
|
|
4260
|
+
attempts: autoContinueState.attempts,
|
|
4261
|
+
textLength: visibleTextSinceContinue.length,
|
|
4262
|
+
lastTextLength: lastVisibleTextSinceContinue.length,
|
|
4263
|
+
hadReasoning: hadReasoningSinceContinue,
|
|
4264
|
+
hadToolActivity: hadToolActivitySinceContinue,
|
|
4265
|
+
hadProxyActivity: hadProxyActivitySinceContinue
|
|
4266
|
+
});
|
|
4267
|
+
turnCompleted = false;
|
|
4268
|
+
resetAutoContinueWindow();
|
|
4269
|
+
proc.stdin?.write(makeAutoContinueMessage() + "\n");
|
|
4270
|
+
return;
|
|
4271
|
+
}
|
|
4272
|
+
log.notice("auto-continuation stopped", {
|
|
4273
|
+
sessionKey: sk,
|
|
4274
|
+
reason: autoDecision.reason,
|
|
4275
|
+
stopReason: lastStopReason,
|
|
4276
|
+
attempts: autoContinueState.attempts,
|
|
4277
|
+
textLength: visibleTextSinceContinue.length,
|
|
4278
|
+
lastTextLength: lastVisibleTextSinceContinue.length,
|
|
4279
|
+
hadReasoning: hadReasoningSinceContinue,
|
|
4280
|
+
hadToolActivity: hadToolActivitySinceContinue,
|
|
4281
|
+
hadProxyActivity: hadProxyActivitySinceContinue
|
|
4282
|
+
});
|
|
4283
|
+
for (const [idx, reasoningId] of reasoningIds) {
|
|
4284
|
+
if (reasoningStarted.get(idx)) {
|
|
4285
|
+
controller.enqueue({
|
|
4286
|
+
type: "reasoning-end",
|
|
4287
|
+
id: reasoningId
|
|
4288
|
+
});
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
controller.enqueue({
|
|
4292
|
+
type: "finish",
|
|
4293
|
+
finishReason: toFinishReason("stop"),
|
|
4294
|
+
usage: toUsage(msg.usage),
|
|
4295
|
+
providerMetadata: {
|
|
4296
|
+
"claude-code": {
|
|
4297
|
+
...resultMeta,
|
|
4298
|
+
...compactionMode ? { compactionModel: effectiveModelId } : {}
|
|
4299
|
+
},
|
|
4300
|
+
...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
|
|
4301
|
+
anthropic: {
|
|
4302
|
+
cacheCreationInputTokens: msg.usage.cache_creation_input_tokens
|
|
4303
|
+
}
|
|
4304
|
+
} : {}
|
|
4305
|
+
}
|
|
4306
|
+
});
|
|
4307
|
+
controllerClosed = true;
|
|
4308
|
+
cleanupTurn();
|
|
4309
|
+
try {
|
|
4310
|
+
controller.close();
|
|
4311
|
+
} catch {
|
|
4312
|
+
}
|
|
4313
|
+
};
|
|
3959
4314
|
let gotPartialEvents = false;
|
|
3960
4315
|
const lineHandler = (line) => {
|
|
3961
4316
|
if (!line.trim()) return;
|
|
3962
4317
|
if (controllerClosed) return;
|
|
3963
4318
|
startResultFallback();
|
|
4319
|
+
clearStartWatchdog();
|
|
3964
4320
|
try {
|
|
3965
4321
|
const outer = JSON.parse(line);
|
|
3966
4322
|
const msg = outer.type === "stream_event" && outer.event ? { ...outer.event, session_id: outer.session_id } : outer;
|
|
@@ -4150,6 +4506,7 @@ ${plan}
|
|
|
4150
4506
|
});
|
|
4151
4507
|
endTextBlock();
|
|
4152
4508
|
} else if (tc.name.startsWith(PROXY_TOOL_PREFIX)) {
|
|
4509
|
+
noteProxyActivity();
|
|
4153
4510
|
log.debug("ignoring proxy tool_use block; broker handles it", {
|
|
4154
4511
|
name: tc.name,
|
|
4155
4512
|
id: tc.id
|
|
@@ -4316,6 +4673,7 @@ ${plan}
|
|
|
4316
4673
|
});
|
|
4317
4674
|
endTextBlock();
|
|
4318
4675
|
} else if (block.name.startsWith(PROXY_TOOL_PREFIX)) {
|
|
4676
|
+
noteProxyActivity();
|
|
4319
4677
|
log.debug("ignoring proxy tool_use from assistant message", {
|
|
4320
4678
|
name: block.name,
|
|
4321
4679
|
id: block.id
|
|
@@ -4466,113 +4824,36 @@ ${plan}
|
|
|
4466
4824
|
});
|
|
4467
4825
|
turnCompleted = true;
|
|
4468
4826
|
endTextBlock();
|
|
4469
|
-
|
|
4827
|
+
const shouldDeferResult = !msg.is_error && !autoContinueState.aborted && !autoContinueState.sawAskUserQuestion;
|
|
4828
|
+
if (drainBuffer.length > 0 && shouldDeferResult) {
|
|
4470
4829
|
log.info(
|
|
4471
|
-
"
|
|
4830
|
+
"waiting for parallel proxy calls at turn-result boundary",
|
|
4472
4831
|
{
|
|
4473
4832
|
sessionKey: sk,
|
|
4474
4833
|
count: drainBuffer.length
|
|
4475
4834
|
}
|
|
4476
4835
|
);
|
|
4477
|
-
|
|
4836
|
+
scheduleResultBoundary(
|
|
4837
|
+
() => completeResult(msg),
|
|
4838
|
+
DRAIN_QUIET_MS
|
|
4839
|
+
);
|
|
4478
4840
|
return;
|
|
4479
4841
|
}
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
"rejecting orphan pending proxy calls at turn-result boundary",
|
|
4842
|
+
if (drainBuffer.length === 0 && hadProxyActivitySinceContinue && shouldDeferResult) {
|
|
4843
|
+
log.info(
|
|
4844
|
+
"waiting for delayed proxy call at turn-result boundary",
|
|
4484
4845
|
{
|
|
4485
4846
|
sessionKey: sk,
|
|
4486
|
-
|
|
4847
|
+
graceMs: PROXY_RESULT_BOUNDARY_GRACE_MS
|
|
4487
4848
|
}
|
|
4488
4849
|
);
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
"Claude CLI emitted result with pending proxy calls not in drain buffer"
|
|
4493
|
-
)
|
|
4850
|
+
scheduleResultBoundary(
|
|
4851
|
+
() => completeResult(msg),
|
|
4852
|
+
PROXY_RESULT_BOUNDARY_GRACE_MS
|
|
4494
4853
|
);
|
|
4495
|
-
}
|
|
4496
|
-
const autoDecision = shouldAutoContinueIncompleteTurn(
|
|
4497
|
-
autoContinueState,
|
|
4498
|
-
{
|
|
4499
|
-
text: visibleTextSinceContinue,
|
|
4500
|
-
lastVisibleText: lastVisibleTextSinceContinue,
|
|
4501
|
-
hadReasoning: hadReasoningSinceContinue,
|
|
4502
|
-
hadToolActivity: hadToolActivitySinceContinue,
|
|
4503
|
-
hadProxyActivity: hadProxyActivitySinceContinue,
|
|
4504
|
-
isError: msg.is_error,
|
|
4505
|
-
stopReason: lastStopReason
|
|
4506
|
-
}
|
|
4507
|
-
);
|
|
4508
|
-
if (autoDecision.continue) {
|
|
4509
|
-
const signature = continuationSignature({
|
|
4510
|
-
text: visibleTextSinceContinue,
|
|
4511
|
-
lastVisibleText: lastVisibleTextSinceContinue,
|
|
4512
|
-
hadReasoning: hadReasoningSinceContinue,
|
|
4513
|
-
hadToolActivity: hadToolActivitySinceContinue,
|
|
4514
|
-
hadProxyActivity: hadProxyActivitySinceContinue,
|
|
4515
|
-
isError: msg.is_error
|
|
4516
|
-
});
|
|
4517
|
-
autoContinueState.noProgressCount = signature === autoContinueState.lastSignature ? autoContinueState.noProgressCount + 1 : 0;
|
|
4518
|
-
autoContinueState.lastSignature = signature;
|
|
4519
|
-
autoContinueState.attempts++;
|
|
4520
|
-
log.notice("auto-continuing incomplete claude result", {
|
|
4521
|
-
sessionKey: sk,
|
|
4522
|
-
reason: autoDecision.reason,
|
|
4523
|
-
attempts: autoContinueState.attempts,
|
|
4524
|
-
textLength: visibleTextSinceContinue.length,
|
|
4525
|
-
lastTextLength: lastVisibleTextSinceContinue.length,
|
|
4526
|
-
hadReasoning: hadReasoningSinceContinue,
|
|
4527
|
-
hadToolActivity: hadToolActivitySinceContinue,
|
|
4528
|
-
hadProxyActivity: hadProxyActivitySinceContinue
|
|
4529
|
-
});
|
|
4530
|
-
turnCompleted = false;
|
|
4531
|
-
resetAutoContinueWindow();
|
|
4532
|
-
proc.stdin?.write(makeAutoContinueMessage() + "\n");
|
|
4533
4854
|
return;
|
|
4534
4855
|
}
|
|
4535
|
-
|
|
4536
|
-
sessionKey: sk,
|
|
4537
|
-
reason: autoDecision.reason,
|
|
4538
|
-
stopReason: lastStopReason,
|
|
4539
|
-
attempts: autoContinueState.attempts,
|
|
4540
|
-
textLength: visibleTextSinceContinue.length,
|
|
4541
|
-
lastTextLength: lastVisibleTextSinceContinue.length,
|
|
4542
|
-
hadReasoning: hadReasoningSinceContinue,
|
|
4543
|
-
hadToolActivity: hadToolActivitySinceContinue,
|
|
4544
|
-
hadProxyActivity: hadProxyActivitySinceContinue
|
|
4545
|
-
});
|
|
4546
|
-
for (const [idx, reasoningId] of reasoningIds) {
|
|
4547
|
-
if (reasoningStarted.get(idx)) {
|
|
4548
|
-
controller.enqueue({
|
|
4549
|
-
type: "reasoning-end",
|
|
4550
|
-
id: reasoningId
|
|
4551
|
-
});
|
|
4552
|
-
}
|
|
4553
|
-
}
|
|
4554
|
-
controller.enqueue({
|
|
4555
|
-
type: "finish",
|
|
4556
|
-
finishReason: toFinishReason("stop"),
|
|
4557
|
-
usage: toUsage(msg.usage),
|
|
4558
|
-
providerMetadata: {
|
|
4559
|
-
"claude-code": {
|
|
4560
|
-
...resultMeta,
|
|
4561
|
-
...compactionMode ? { compactionModel: effectiveModelId } : {}
|
|
4562
|
-
},
|
|
4563
|
-
...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
|
|
4564
|
-
anthropic: {
|
|
4565
|
-
cacheCreationInputTokens: msg.usage.cache_creation_input_tokens
|
|
4566
|
-
}
|
|
4567
|
-
} : {}
|
|
4568
|
-
}
|
|
4569
|
-
});
|
|
4570
|
-
controllerClosed = true;
|
|
4571
|
-
cleanupTurn();
|
|
4572
|
-
try {
|
|
4573
|
-
controller.close();
|
|
4574
|
-
} catch {
|
|
4575
|
-
}
|
|
4856
|
+
completeResult(msg);
|
|
4576
4857
|
}
|
|
4577
4858
|
} catch (e) {
|
|
4578
4859
|
log.debug("failed to parse line", {
|
|
@@ -4616,6 +4897,8 @@ ${plan}
|
|
|
4616
4897
|
if (cleanedUp) return;
|
|
4617
4898
|
cleanedUp = true;
|
|
4618
4899
|
clearFallbackTimer();
|
|
4900
|
+
pendingResultCompletion = null;
|
|
4901
|
+
clearStartWatchdog();
|
|
4619
4902
|
if (drainTimer) {
|
|
4620
4903
|
clearTimeout(drainTimer);
|
|
4621
4904
|
drainTimer = null;
|
|
@@ -4676,6 +4959,7 @@ ${plan}
|
|
|
4676
4959
|
noteProxyActivity();
|
|
4677
4960
|
noteToolActivity();
|
|
4678
4961
|
drainBuffer.push(call);
|
|
4962
|
+
if (noteResultBoundaryCall()) return;
|
|
4679
4963
|
if (drainTimer) clearTimeout(drainTimer);
|
|
4680
4964
|
drainTimer = setTimeout(drainNow, DRAIN_QUIET_MS);
|
|
4681
4965
|
});
|
|
@@ -4689,6 +4973,15 @@ ${plan}
|
|
|
4689
4973
|
"abort signal received before content, closing stream immediately",
|
|
4690
4974
|
{ cwd }
|
|
4691
4975
|
);
|
|
4976
|
+
if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
|
|
4977
|
+
rejectAllPendingProxyCallsForSession(
|
|
4978
|
+
sk,
|
|
4979
|
+
new Error(
|
|
4980
|
+
"Provider stream was aborted before pending proxy calls were emitted"
|
|
4981
|
+
)
|
|
4982
|
+
);
|
|
4983
|
+
drainBuffer.length = 0;
|
|
4984
|
+
}
|
|
4692
4985
|
controllerClosed = true;
|
|
4693
4986
|
cleanupTurn();
|
|
4694
4987
|
try {
|
|
@@ -4714,20 +5007,14 @@ ${plan}
|
|
|
4714
5007
|
});
|
|
4715
5008
|
resolvePendingProxyCallById(call.toolCallId, result);
|
|
4716
5009
|
} else {
|
|
4717
|
-
log.
|
|
4718
|
-
"
|
|
5010
|
+
log.info(
|
|
5011
|
+
"leaving unmatched parallel proxy call pending",
|
|
4719
5012
|
{
|
|
4720
5013
|
sessionKey: sk,
|
|
4721
5014
|
toolCallId: call.toolCallId,
|
|
4722
5015
|
toolName: call.toolName
|
|
4723
5016
|
}
|
|
4724
5017
|
);
|
|
4725
|
-
rejectPendingProxyCallById(
|
|
4726
|
-
call.toolCallId,
|
|
4727
|
-
new Error(
|
|
4728
|
-
`Pending proxy call '${call.toolName}' (${call.toolCallId}) was not matched in tool-result turn; rejecting as orphaned`
|
|
4729
|
-
)
|
|
4730
|
-
);
|
|
4731
5018
|
}
|
|
4732
5019
|
}
|
|
4733
5020
|
return;
|
|
@@ -4744,6 +5031,7 @@ ${plan}
|
|
|
4744
5031
|
}
|
|
4745
5032
|
proc.stdin?.write(userMsg + "\n");
|
|
4746
5033
|
log.debug("sent user message", { textLength: userMsg.length });
|
|
5034
|
+
armStartWatchdog();
|
|
4747
5035
|
};
|
|
4748
5036
|
void setup().catch((err) => {
|
|
4749
5037
|
log.error("failed to set up doStream", {
|
|
@@ -5236,6 +5524,13 @@ function pickOpencodeDirectory(input) {
|
|
|
5236
5524
|
return void 0;
|
|
5237
5525
|
}
|
|
5238
5526
|
var warnedAnthropicApiKey = false;
|
|
5527
|
+
var DEFAULT_PROXY_TOOL_NAMES = [
|
|
5528
|
+
"Bash",
|
|
5529
|
+
"Edit",
|
|
5530
|
+
"Write",
|
|
5531
|
+
"WebFetch",
|
|
5532
|
+
"Task"
|
|
5533
|
+
];
|
|
5239
5534
|
function warnIfAnthropicApiKey(ignore) {
|
|
5240
5535
|
if (warnedAnthropicApiKey) return;
|
|
5241
5536
|
if (!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_AUTH_TOKEN) return;
|
|
@@ -5262,7 +5557,7 @@ function createClaudeCode(settings = {}) {
|
|
|
5262
5557
|
warnIfAnthropicApiKey(settings.ignoreAnthropicApiKey);
|
|
5263
5558
|
const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
|
|
5264
5559
|
const providerName = settings.providerID ?? settings.name ?? "claude-code";
|
|
5265
|
-
const proxyTools = settings.proxyTools ?? [
|
|
5560
|
+
const proxyTools = settings.proxyTools ?? [...DEFAULT_PROXY_TOOL_NAMES];
|
|
5266
5561
|
const createModel = (modelId) => {
|
|
5267
5562
|
return new ClaudeCodeLanguageModel(modelId, {
|
|
5268
5563
|
provider: providerName,
|
|
@@ -5280,6 +5575,7 @@ function createClaudeCode(settings = {}) {
|
|
|
5280
5575
|
controlRequestToolBehaviors: settings.controlRequestToolBehaviors,
|
|
5281
5576
|
controlRequestDenyMessage: settings.controlRequestDenyMessage,
|
|
5282
5577
|
proxyTools,
|
|
5578
|
+
proxyToolTimeoutMs: settings.proxyToolTimeoutMs,
|
|
5283
5579
|
webSearch: settings.webSearch,
|
|
5284
5580
|
hotReloadMcp: settings.hotReloadMcp ?? true,
|
|
5285
5581
|
proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true,
|
|
@@ -5374,7 +5670,7 @@ function configModelsForProvider(providerModels, providerID, modelSuffix) {
|
|
|
5374
5670
|
async function providerConfig(existing, providerID = PROVIDER_ID2, optionDefaults = {}, displayName) {
|
|
5375
5671
|
const mergedOptions = {
|
|
5376
5672
|
cliPath: "claude",
|
|
5377
|
-
proxyTools: [
|
|
5673
|
+
proxyTools: [...DEFAULT_PROXY_TOOL_NAMES],
|
|
5378
5674
|
...optionDefaults,
|
|
5379
5675
|
...cleanProviderOptions(existing?.options),
|
|
5380
5676
|
providerID
|