@khalilgharbaoui/opencode-claude-code-plugin 0.19.0 → 0.21.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 +92 -22
- package/dist/index.d.ts +76 -14
- package/dist/index.js +389 -84
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +66 -18
package/dist/index.js
CHANGED
|
@@ -475,9 +475,6 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
475
475
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
476
476
|
import { unlink } from "fs/promises";
|
|
477
477
|
|
|
478
|
-
// src/proxy-broker.ts
|
|
479
|
-
import { EventEmitter as EventEmitter2 } from "events";
|
|
480
|
-
|
|
481
478
|
// src/proxy-mcp.ts
|
|
482
479
|
import { createServer } from "http";
|
|
483
480
|
import * as fs2 from "fs";
|
|
@@ -511,7 +508,7 @@ function pluginTmpDir() {
|
|
|
511
508
|
}
|
|
512
509
|
|
|
513
510
|
// src/proxy-mcp.ts
|
|
514
|
-
var
|
|
511
|
+
var PROXY_KEEPALIVE_MS = 15e3;
|
|
515
512
|
function acceptsEventStream(acceptHeader) {
|
|
516
513
|
return typeof acceptHeader === "string" && acceptHeader.toLowerCase().includes("text/event-stream");
|
|
517
514
|
}
|
|
@@ -523,11 +520,11 @@ var PROTOCOL_VERSION = "2024-11-05";
|
|
|
523
520
|
var SERVER_NAME = "opencode_proxy";
|
|
524
521
|
var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
|
|
525
522
|
var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
523
|
+
var PROXY_NO_DEADLINE_MS = 0;
|
|
526
524
|
var PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS = {
|
|
527
|
-
task:
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
// 60 min, same reasoning: it IS task calls
|
|
525
|
+
task: PROXY_NO_DEADLINE_MS,
|
|
526
|
+
task_batch: PROXY_NO_DEADLINE_MS,
|
|
527
|
+
// same reasoning: it IS task calls
|
|
531
528
|
question: 30 * 60 * 1e3
|
|
532
529
|
// 30 min
|
|
533
530
|
};
|
|
@@ -537,7 +534,7 @@ function resolveProxyCallTimeoutMs(toolName, input, overrides) {
|
|
|
537
534
|
let ms = PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS[key] ?? PROXY_DEFAULT_TIMEOUT_MS;
|
|
538
535
|
if (overrides) {
|
|
539
536
|
const ov = lookupCaseInsensitive(overrides, key);
|
|
540
|
-
if (
|
|
537
|
+
if (isDeadlineOverride(ov)) ms = ov;
|
|
541
538
|
}
|
|
542
539
|
if (key === "bash") {
|
|
543
540
|
const requested = input?.timeout;
|
|
@@ -545,6 +542,9 @@ function resolveProxyCallTimeoutMs(toolName, input, overrides) {
|
|
|
545
542
|
}
|
|
546
543
|
return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
|
|
547
544
|
}
|
|
545
|
+
function isDeadlineOverride(value) {
|
|
546
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
547
|
+
}
|
|
548
548
|
function lookupCaseInsensitive(map, key) {
|
|
549
549
|
if (Object.prototype.hasOwnProperty.call(map, key)) return map[key];
|
|
550
550
|
for (const k of Object.keys(map)) {
|
|
@@ -554,14 +554,20 @@ function lookupCaseInsensitive(map, key) {
|
|
|
554
554
|
}
|
|
555
555
|
function resolveProxyClientCeilingMs(overrides) {
|
|
556
556
|
let ms = PROXY_DEFAULT_TIMEOUT_MS;
|
|
557
|
-
|
|
558
|
-
if (
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
557
|
+
const consider = (deadlineMs) => {
|
|
558
|
+
if (deadlineMs === PROXY_NO_DEADLINE_MS) return true;
|
|
559
|
+
if (deadlineMs > ms) ms = deadlineMs;
|
|
560
|
+
return false;
|
|
561
|
+
};
|
|
562
|
+
for (const [toolName, defaultMs] of Object.entries(PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS)) {
|
|
563
|
+
const override = overrides ? lookupCaseInsensitive(overrides, toolName) : void 0;
|
|
564
|
+
if (consider(isDeadlineOverride(override) ? override : defaultMs)) {
|
|
565
|
+
return MAX_PROXY_TIMEOUT_MS;
|
|
563
566
|
}
|
|
564
567
|
}
|
|
568
|
+
for (const value of Object.values(overrides ?? {})) {
|
|
569
|
+
if (isDeadlineOverride(value) && consider(value)) return MAX_PROXY_TIMEOUT_MS;
|
|
570
|
+
}
|
|
565
571
|
return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
|
|
566
572
|
}
|
|
567
573
|
function buildProxyTimeoutError(toolName, ms) {
|
|
@@ -574,9 +580,9 @@ function buildProxyTimeoutError(toolName, ms) {
|
|
|
574
580
|
}
|
|
575
581
|
return new Error(base);
|
|
576
582
|
}
|
|
577
|
-
var TASK_PROXY_NOTE = "This and task_batch are the ONLY tools that dispatch opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists: invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls
|
|
583
|
+
var TASK_PROXY_NOTE = "This and task_batch are the ONLY tools that dispatch opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists: invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls have no proxy deadline by default: the call waits for the subagent to finish (a positive proxyToolTimeoutMs override adds a deadline).";
|
|
578
584
|
var TASK_BATCH_TOOL_NAME = "task_batch";
|
|
579
|
-
var TASK_BATCH_PROXY_NOTE = "Use this instead of several task calls in one response: Claude Code runs MCP tool calls one at a time, so separate task calls run serially even when emitted together, while one task_batch call fans them out as parallel opencode task calls. Each task takes the same fields as the task tool. Results come back in task order, each labelled.
|
|
585
|
+
var TASK_BATCH_PROXY_NOTE = "Use this instead of several task calls in one response: Claude Code runs MCP tool calls one at a time, so separate task calls run serially even when emitted together, while one task_batch call fans them out as parallel opencode task calls. Each task takes the same fields as the task tool. Results come back in task order, each labelled. Like task it has no proxy deadline by default (a positive proxyToolTimeoutMs override adds one).";
|
|
580
586
|
var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
|
|
581
587
|
function taskBatchInputError(input) {
|
|
582
588
|
const tasks = input?.tasks;
|
|
@@ -874,9 +880,10 @@ var DEFAULT_PROXY_TOOLS = [
|
|
|
874
880
|
}
|
|
875
881
|
}
|
|
876
882
|
];
|
|
877
|
-
async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides, interceptors) {
|
|
883
|
+
async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides, interceptors, options = {}) {
|
|
878
884
|
const calls = new EventEmitter();
|
|
879
885
|
const pending = /* @__PURE__ */ new Map();
|
|
886
|
+
const keepaliveMs = options.keepaliveMs ?? PROXY_KEEPALIVE_MS;
|
|
880
887
|
const authToken = crypto.randomBytes(32).toString("hex");
|
|
881
888
|
const expectedAuth = Buffer.from(`Bearer ${authToken}`);
|
|
882
889
|
let boundAuthority = "";
|
|
@@ -925,7 +932,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
925
932
|
}
|
|
926
933
|
let requestId = null;
|
|
927
934
|
let requestMethod = null;
|
|
928
|
-
let
|
|
935
|
+
let reply = null;
|
|
929
936
|
try {
|
|
930
937
|
const body = await readBody(req);
|
|
931
938
|
const request = JSON.parse(body);
|
|
@@ -1020,11 +1027,9 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1020
1027
|
sse: acceptsEventStream(req.headers.accept)
|
|
1021
1028
|
});
|
|
1022
1029
|
const channel = { closed: false };
|
|
1023
|
-
|
|
1024
|
-
sse = openEventStream(res);
|
|
1025
|
-
}
|
|
1030
|
+
reply = acceptsEventStream(req.headers.accept) ? openEventStream(res, keepaliveMs) : openJsonStream(res, keepaliveMs);
|
|
1026
1031
|
res.once("close", () => {
|
|
1027
|
-
|
|
1032
|
+
reply?.stop();
|
|
1028
1033
|
if (res.writableFinished) return;
|
|
1029
1034
|
channel.closed = true;
|
|
1030
1035
|
log.notice("proxy-mcp client closed a tool call before its result", {
|
|
@@ -1049,16 +1054,18 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1049
1054
|
input,
|
|
1050
1055
|
timeoutOverrides
|
|
1051
1056
|
);
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1057
|
+
if (deadlineMs > PROXY_NO_DEADLINE_MS) {
|
|
1058
|
+
timer = setTimeout(() => {
|
|
1059
|
+
if (!pending.has(callId)) return;
|
|
1060
|
+
pending.delete(callId);
|
|
1061
|
+
log.notice("proxy-mcp tool call timed out", {
|
|
1062
|
+
callId,
|
|
1063
|
+
toolName,
|
|
1064
|
+
deadlineMs
|
|
1065
|
+
});
|
|
1066
|
+
reject2(buildProxyTimeoutError(toolName, deadlineMs));
|
|
1067
|
+
}, deadlineMs);
|
|
1068
|
+
}
|
|
1062
1069
|
calls.emit("call", entry);
|
|
1063
1070
|
}
|
|
1064
1071
|
).finally(() => {
|
|
@@ -1072,7 +1079,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1072
1079
|
});
|
|
1073
1080
|
return;
|
|
1074
1081
|
}
|
|
1075
|
-
writeToolCallResult(res, requestId, result,
|
|
1082
|
+
writeToolCallResult(res, requestId, result, reply);
|
|
1076
1083
|
return;
|
|
1077
1084
|
}
|
|
1078
1085
|
writeJson(res, {
|
|
@@ -1092,7 +1099,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1092
1099
|
res,
|
|
1093
1100
|
requestId,
|
|
1094
1101
|
{ kind: "error", message: errorMessage },
|
|
1095
|
-
|
|
1102
|
+
reply
|
|
1096
1103
|
);
|
|
1097
1104
|
} catch {
|
|
1098
1105
|
try {
|
|
@@ -1174,6 +1181,9 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1174
1181
|
configFilePath = outPath;
|
|
1175
1182
|
return outPath;
|
|
1176
1183
|
},
|
|
1184
|
+
pendingCallIds() {
|
|
1185
|
+
return [...pending.keys()];
|
|
1186
|
+
},
|
|
1177
1187
|
async close() {
|
|
1178
1188
|
for (const entry of pending.values()) {
|
|
1179
1189
|
entry.reject(new Error(SERVER_CLOSED_MESSAGE));
|
|
@@ -1238,6 +1248,61 @@ function resolveDisallowedTools(options) {
|
|
|
1238
1248
|
if (options.disableWebSearch) push("WebSearch");
|
|
1239
1249
|
return out;
|
|
1240
1250
|
}
|
|
1251
|
+
function resolveProxyOpencodeToolDefs(options) {
|
|
1252
|
+
const requested = options.requested ?? [];
|
|
1253
|
+
if (requested.length === 0) return [];
|
|
1254
|
+
const items = options.items;
|
|
1255
|
+
if (!items) {
|
|
1256
|
+
log.warn(
|
|
1257
|
+
"proxyOpencodeTools is set but opencode's tool registry did not answer; forwarding nothing this spawn",
|
|
1258
|
+
{ requested: requested.map(String) }
|
|
1259
|
+
);
|
|
1260
|
+
return [];
|
|
1261
|
+
}
|
|
1262
|
+
const byLowerId = /* @__PURE__ */ new Map();
|
|
1263
|
+
for (const item of items) {
|
|
1264
|
+
const key = item.id.toLowerCase();
|
|
1265
|
+
if (!byLowerId.has(key)) byLowerId.set(key, item);
|
|
1266
|
+
}
|
|
1267
|
+
const taken = options.taken ?? /* @__PURE__ */ new Set();
|
|
1268
|
+
const out = [];
|
|
1269
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1270
|
+
const unknown = [];
|
|
1271
|
+
const collided = [];
|
|
1272
|
+
for (const raw of requested) {
|
|
1273
|
+
const name = String(raw).trim();
|
|
1274
|
+
if (!name) continue;
|
|
1275
|
+
const item = byLowerId.get(name.toLowerCase());
|
|
1276
|
+
if (!item) {
|
|
1277
|
+
unknown.push(name);
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
if (taken.has(item.id)) {
|
|
1281
|
+
collided.push(item.id);
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (seen.has(item.id)) continue;
|
|
1285
|
+
seen.add(item.id);
|
|
1286
|
+
out.push({
|
|
1287
|
+
name: item.id,
|
|
1288
|
+
description: typeof item.description === "string" ? item.description : "",
|
|
1289
|
+
inputSchema: item.parameters && typeof item.parameters === "object" ? item.parameters : { type: "object", properties: {} }
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
if (unknown.length > 0) {
|
|
1293
|
+
log.warn("ignoring unknown proxyOpencodeTools entries", {
|
|
1294
|
+
unknown,
|
|
1295
|
+
known: [...byLowerId.values()].map((item) => item.id).join(", ")
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
if (collided.length > 0) {
|
|
1299
|
+
log.warn(
|
|
1300
|
+
"proxyOpencodeTools entry dropped: a proxy tool already holds that name, and it keeps it",
|
|
1301
|
+
{ collided }
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
return out;
|
|
1305
|
+
}
|
|
1241
1306
|
function readBody(req) {
|
|
1242
1307
|
return new Promise((resolve5, reject) => {
|
|
1243
1308
|
const chunks = [];
|
|
@@ -1246,7 +1311,7 @@ function readBody(req) {
|
|
|
1246
1311
|
req.on("error", reject);
|
|
1247
1312
|
});
|
|
1248
1313
|
}
|
|
1249
|
-
function writeToolCallResult(res, requestId, result,
|
|
1314
|
+
function writeToolCallResult(res, requestId, result, reply = null) {
|
|
1250
1315
|
const text = result.kind === "error" ? result.message : result.text;
|
|
1251
1316
|
const isError = result.kind === "error" || result.isError === true;
|
|
1252
1317
|
const envelope = {
|
|
@@ -1257,26 +1322,20 @@ function writeToolCallResult(res, requestId, result, sse = null) {
|
|
|
1257
1322
|
isError
|
|
1258
1323
|
}
|
|
1259
1324
|
};
|
|
1260
|
-
if (
|
|
1261
|
-
|
|
1325
|
+
if (reply) {
|
|
1326
|
+
reply.finish(envelope);
|
|
1262
1327
|
return;
|
|
1263
1328
|
}
|
|
1264
1329
|
writeJson(res, envelope);
|
|
1265
1330
|
}
|
|
1266
|
-
function
|
|
1267
|
-
res.statusCode = 200;
|
|
1268
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
1269
|
-
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1270
|
-
res.setHeader("Connection", "keep-alive");
|
|
1271
|
-
res.flushHeaders();
|
|
1272
|
-
res.write(": open\n\n");
|
|
1331
|
+
function startKeepalive(res, keepaliveMs, ping) {
|
|
1273
1332
|
let timer = setInterval(() => {
|
|
1274
1333
|
if (res.writableEnded || res.destroyed) {
|
|
1275
1334
|
stop();
|
|
1276
1335
|
return;
|
|
1277
1336
|
}
|
|
1278
|
-
res.write(
|
|
1279
|
-
},
|
|
1337
|
+
res.write(ping);
|
|
1338
|
+
}, keepaliveMs);
|
|
1280
1339
|
timer.unref?.();
|
|
1281
1340
|
const stop = () => {
|
|
1282
1341
|
if (timer) {
|
|
@@ -1284,6 +1343,16 @@ function openEventStream(res) {
|
|
|
1284
1343
|
timer = null;
|
|
1285
1344
|
}
|
|
1286
1345
|
};
|
|
1346
|
+
return stop;
|
|
1347
|
+
}
|
|
1348
|
+
function openEventStream(res, keepaliveMs) {
|
|
1349
|
+
res.statusCode = 200;
|
|
1350
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
1351
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1352
|
+
res.setHeader("Connection", "keep-alive");
|
|
1353
|
+
res.flushHeaders();
|
|
1354
|
+
res.write(": open\n\n");
|
|
1355
|
+
const stop = startKeepalive(res, keepaliveMs, ": keepalive\n\n");
|
|
1287
1356
|
return {
|
|
1288
1357
|
stop,
|
|
1289
1358
|
finish(envelope) {
|
|
@@ -1296,6 +1365,21 @@ data: ${JSON.stringify(envelope)}
|
|
|
1296
1365
|
}
|
|
1297
1366
|
};
|
|
1298
1367
|
}
|
|
1368
|
+
function openJsonStream(res, keepaliveMs) {
|
|
1369
|
+
res.statusCode = 200;
|
|
1370
|
+
res.setHeader("Content-Type", "application/json");
|
|
1371
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1372
|
+
res.flushHeaders();
|
|
1373
|
+
const stop = startKeepalive(res, keepaliveMs, " ");
|
|
1374
|
+
return {
|
|
1375
|
+
stop,
|
|
1376
|
+
finish(envelope) {
|
|
1377
|
+
stop();
|
|
1378
|
+
if (res.writableEnded || res.destroyed) return;
|
|
1379
|
+
res.end(JSON.stringify(envelope));
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1299
1383
|
function writeJson(res, body) {
|
|
1300
1384
|
if (res.destroyed || res.writableEnded) return;
|
|
1301
1385
|
const payload = JSON.stringify(body);
|
|
@@ -1306,6 +1390,12 @@ function writeJson(res, body) {
|
|
|
1306
1390
|
}
|
|
1307
1391
|
|
|
1308
1392
|
// src/proxy-broker.ts
|
|
1393
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
1394
|
+
var PROXY_STALL_WARNING_MS = 5 * 6e4;
|
|
1395
|
+
function clearPendingTimers(pending) {
|
|
1396
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1397
|
+
if (pending.stallTimer) clearInterval(pending.stallTimer);
|
|
1398
|
+
}
|
|
1309
1399
|
var pendingByCallId = /* @__PURE__ */ new Map();
|
|
1310
1400
|
var callIdsBySession = /* @__PURE__ */ new Map();
|
|
1311
1401
|
var emitter = new EventEmitter2();
|
|
@@ -1331,10 +1421,10 @@ function onPendingProxyCall(sessionKey2, handler) {
|
|
|
1331
1421
|
emitter.on(name, handler);
|
|
1332
1422
|
return () => emitter.off(name, handler);
|
|
1333
1423
|
}
|
|
1334
|
-
function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
|
|
1424
|
+
function queuePendingProxyCall(sessionKey2, call, timeoutOverrides, stallWarningMs = PROXY_STALL_WARNING_MS) {
|
|
1335
1425
|
const previous = pendingByCallId.get(call.id);
|
|
1336
1426
|
if (previous) {
|
|
1337
|
-
|
|
1427
|
+
clearPendingTimers(previous);
|
|
1338
1428
|
previous.reject(
|
|
1339
1429
|
new Error(`Replaced pending proxy call ${call.id} with a fresh one`)
|
|
1340
1430
|
);
|
|
@@ -1346,11 +1436,12 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
|
|
|
1346
1436
|
call.input,
|
|
1347
1437
|
timeoutOverrides
|
|
1348
1438
|
);
|
|
1349
|
-
const timer = setTimeout(() => {
|
|
1439
|
+
const timer = deadlineMs > PROXY_NO_DEADLINE_MS ? setTimeout(() => {
|
|
1350
1440
|
const current = pendingByCallId.get(call.id);
|
|
1351
1441
|
if (!current) return;
|
|
1352
1442
|
pendingByCallId.delete(call.id);
|
|
1353
1443
|
indexRemove(current.sessionKey, call.id);
|
|
1444
|
+
clearPendingTimers(current);
|
|
1354
1445
|
current.reject(buildProxyTimeoutError(call.toolName, deadlineMs));
|
|
1355
1446
|
log.notice("timed out pending proxy call", {
|
|
1356
1447
|
sessionKey: current.sessionKey,
|
|
@@ -1358,7 +1449,21 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
|
|
|
1358
1449
|
toolName: call.toolName,
|
|
1359
1450
|
deadlineMs
|
|
1360
1451
|
});
|
|
1361
|
-
}, deadlineMs);
|
|
1452
|
+
}, deadlineMs) : null;
|
|
1453
|
+
const stallTimer = deadlineMs === PROXY_NO_DEADLINE_MS && stallWarningMs > 0 ? setInterval(() => {
|
|
1454
|
+
const current = pendingByCallId.get(call.id);
|
|
1455
|
+
if (!current) return;
|
|
1456
|
+
log.warn("proxy call still waiting, no deadline", {
|
|
1457
|
+
sessionKey: current.sessionKey,
|
|
1458
|
+
toolCallId: current.toolCallId,
|
|
1459
|
+
toolName: current.toolName,
|
|
1460
|
+
waitedMs: Date.now() - current.createdAt,
|
|
1461
|
+
emitted: current.emitted === true,
|
|
1462
|
+
channelClosed: current.channel?.closed === true,
|
|
1463
|
+
note: "nothing will time this out; it ends when opencode returns a result, you abort, you send another message, or the claude process goes"
|
|
1464
|
+
});
|
|
1465
|
+
}, stallWarningMs) : null;
|
|
1466
|
+
stallTimer?.unref?.();
|
|
1362
1467
|
const pending = {
|
|
1363
1468
|
sessionKey: sessionKey2,
|
|
1364
1469
|
toolCallId: call.id,
|
|
@@ -1368,6 +1473,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
|
|
|
1368
1473
|
createdAt: Date.now(),
|
|
1369
1474
|
deadlineMs,
|
|
1370
1475
|
timer,
|
|
1476
|
+
stallTimer,
|
|
1371
1477
|
resolve: call.resolve,
|
|
1372
1478
|
reject: call.reject
|
|
1373
1479
|
};
|
|
@@ -1418,7 +1524,7 @@ function resolvePendingProxyCallById(toolCallId, result) {
|
|
|
1418
1524
|
if (!pending) return false;
|
|
1419
1525
|
pendingByCallId.delete(toolCallId);
|
|
1420
1526
|
indexRemove(pending.sessionKey, toolCallId);
|
|
1421
|
-
|
|
1527
|
+
clearPendingTimers(pending);
|
|
1422
1528
|
pending.resolve(result);
|
|
1423
1529
|
log.info("resolved pending proxy call", {
|
|
1424
1530
|
sessionKey: pending.sessionKey,
|
|
@@ -1432,7 +1538,7 @@ function rejectPendingProxyCallById(toolCallId, error) {
|
|
|
1432
1538
|
if (!pending) return false;
|
|
1433
1539
|
pendingByCallId.delete(toolCallId);
|
|
1434
1540
|
indexRemove(pending.sessionKey, toolCallId);
|
|
1435
|
-
|
|
1541
|
+
clearPendingTimers(pending);
|
|
1436
1542
|
pending.reject(error);
|
|
1437
1543
|
log.notice("rejected pending proxy call", {
|
|
1438
1544
|
sessionKey: pending.sessionKey,
|
|
@@ -1864,9 +1970,14 @@ var activeProcesses = /* @__PURE__ */ new Map();
|
|
|
1864
1970
|
var claudeSessions = /* @__PURE__ */ new Map();
|
|
1865
1971
|
var idleEvictionTimers = /* @__PURE__ */ new Map();
|
|
1866
1972
|
var MAX_IDLE_TIMEOUT_MS = 2147483647;
|
|
1973
|
+
var DEFAULT_IDLE_PROCESS_TIMEOUT_MS = 0;
|
|
1974
|
+
function resolveIdleProcessTimeoutMs(configured) {
|
|
1975
|
+
return configured === void 0 ? DEFAULT_IDLE_PROCESS_TIMEOUT_MS : configured;
|
|
1976
|
+
}
|
|
1867
1977
|
var MAX_ACTIVE_PROCESSES = 16;
|
|
1868
1978
|
var PROCESS_EXIT_TIMEOUT_MS = 1500;
|
|
1869
1979
|
var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
|
|
1980
|
+
var CHILD_EXITED_MESSAGE = "Claude CLI subprocess closed before pending tool calls were resolved";
|
|
1870
1981
|
function envFlagEnabled(value) {
|
|
1871
1982
|
if (value === void 0) return false;
|
|
1872
1983
|
const normalized = value.trim().toLowerCase();
|
|
@@ -2016,6 +2127,11 @@ function scheduleIdleProcessEviction(key, timeoutMs) {
|
|
|
2016
2127
|
const timer = setTimeout(() => {
|
|
2017
2128
|
idleEvictionTimers.delete(key);
|
|
2018
2129
|
if (activeProcesses.get(key) !== scheduledProcess) return;
|
|
2130
|
+
if (isTurnInFlight(scheduledProcess)) {
|
|
2131
|
+
log.info("idle timer found a turn in flight; re-arming", { sessionKey: key, timeoutMs });
|
|
2132
|
+
scheduleIdleProcessEviction(key, timeoutMs);
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2019
2135
|
log.info("evicting idle claude process", { sessionKey: key, timeoutMs });
|
|
2020
2136
|
deleteActiveProcess(key);
|
|
2021
2137
|
}, timeoutMs);
|
|
@@ -2027,9 +2143,44 @@ function detachActiveProcess(key) {
|
|
|
2027
2143
|
const ap = activeProcesses.get(key);
|
|
2028
2144
|
if (!ap) return void 0;
|
|
2029
2145
|
activeProcesses.delete(key);
|
|
2030
|
-
|
|
2146
|
+
if (ap.proxyServer) {
|
|
2147
|
+
void ap.proxyServer.close();
|
|
2148
|
+
rejectAllPendingProxyCallsForSession(key, new Error(SERVER_CLOSED_MESSAGE));
|
|
2149
|
+
}
|
|
2031
2150
|
return ap;
|
|
2032
2151
|
}
|
|
2152
|
+
function deleteActiveProcessesForSession(sessionID) {
|
|
2153
|
+
if (!sessionID || sessionID === "default") return [];
|
|
2154
|
+
const released = [];
|
|
2155
|
+
for (const [key, ap] of [...activeProcesses]) {
|
|
2156
|
+
const owned = ap.opencodeSessionID === sessionID || describeSessionKey(key).session === sessionID;
|
|
2157
|
+
if (!owned) continue;
|
|
2158
|
+
log.info("releasing claude process for deleted session", { sessionKey: key, sessionID });
|
|
2159
|
+
void deleteActiveProcessAndWait(key);
|
|
2160
|
+
released.push(key);
|
|
2161
|
+
}
|
|
2162
|
+
for (const key of [...claudeSessions.keys()]) {
|
|
2163
|
+
if (describeSessionKey(key).session !== sessionID) continue;
|
|
2164
|
+
deleteClaudeSessionId(key);
|
|
2165
|
+
clearCompression(key);
|
|
2166
|
+
if (!released.includes(key)) released.push(key);
|
|
2167
|
+
}
|
|
2168
|
+
return released;
|
|
2169
|
+
}
|
|
2170
|
+
function killAllActiveProcesses() {
|
|
2171
|
+
const keys = [...activeProcesses.keys()];
|
|
2172
|
+
for (const key of keys) deleteActiveProcess(key);
|
|
2173
|
+
return keys;
|
|
2174
|
+
}
|
|
2175
|
+
var processExitCleanupWired = false;
|
|
2176
|
+
function ensureProcessExitCleanup() {
|
|
2177
|
+
if (processExitCleanupWired) return false;
|
|
2178
|
+
processExitCleanupWired = true;
|
|
2179
|
+
process.once("exit", () => {
|
|
2180
|
+
killAllActiveProcesses();
|
|
2181
|
+
});
|
|
2182
|
+
return true;
|
|
2183
|
+
}
|
|
2033
2184
|
function deleteActiveProcess(key) {
|
|
2034
2185
|
const ap = detachActiveProcess(key);
|
|
2035
2186
|
ap?.proc.kill();
|
|
@@ -2178,6 +2329,12 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
2178
2329
|
if (ownsSessionKey) {
|
|
2179
2330
|
cancelIdleProcessEviction(sessionKey2);
|
|
2180
2331
|
activeProcesses.delete(sessionKey2);
|
|
2332
|
+
if (getPendingProxyCalls(sessionKey2).length > 0) {
|
|
2333
|
+
rejectAllPendingProxyCallsForSession(
|
|
2334
|
+
sessionKey2,
|
|
2335
|
+
new Error(CHILD_EXITED_MESSAGE)
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2181
2338
|
}
|
|
2182
2339
|
if (ownsSessionKey && code !== 0 && code !== null) {
|
|
2183
2340
|
log.info("process exited with error, clearing session", {
|
|
@@ -2218,6 +2375,7 @@ function appendResumeIfNeeded(sessionKey2, cliArgs) {
|
|
|
2218
2375
|
function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropicApiKey) {
|
|
2219
2376
|
const old = activeProcesses.get(sessionKey2);
|
|
2220
2377
|
if (!old) return void 0;
|
|
2378
|
+
const turnWasInFlight = isTurnInFlight(old);
|
|
2221
2379
|
activeProcesses.delete(sessionKey2);
|
|
2222
2380
|
old.proc.removeAllListeners("exit");
|
|
2223
2381
|
try {
|
|
@@ -2237,6 +2395,7 @@ function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropi
|
|
|
2237
2395
|
);
|
|
2238
2396
|
replacement.pendingProxyCompletions = old.pendingProxyCompletions;
|
|
2239
2397
|
delete old.pendingProxyCompletions;
|
|
2398
|
+
if (turnWasInFlight) noteTurnStarted(replacement);
|
|
2240
2399
|
return replacement;
|
|
2241
2400
|
}
|
|
2242
2401
|
function buildCliArgs(opts) {
|
|
@@ -3532,10 +3691,9 @@ function formatDoctorReport(report) {
|
|
|
3532
3691
|
lines.push("| tool | call id | age | deadline |");
|
|
3533
3692
|
lines.push("|---|---|---|---|");
|
|
3534
3693
|
for (const call of report.pendingCalls) {
|
|
3694
|
+
const deadline = call.deadlineMs > 0 ? formatAge(call.deadlineMs) : "none";
|
|
3535
3695
|
lines.push(
|
|
3536
|
-
`| ${call.toolName} | \`${call.toolCallId}\` | ${formatAge(call.ageMs)} | ${
|
|
3537
|
-
call.deadlineMs
|
|
3538
|
-
)} |`
|
|
3696
|
+
`| ${call.toolName} | \`${call.toolCallId}\` | ${formatAge(call.ageMs)} | ${deadline} |`
|
|
3539
3697
|
);
|
|
3540
3698
|
}
|
|
3541
3699
|
}
|
|
@@ -3758,6 +3916,35 @@ function filterSideQuestionHistory(prompt) {
|
|
|
3758
3916
|
(message) => message.role === "assistant" ? { ...message, content: stripPluginNotes(message.content) } : message
|
|
3759
3917
|
);
|
|
3760
3918
|
}
|
|
3919
|
+
var DCP_REMINDER_BLOCK = /<dcp-system-reminder\b[^>]*>[\s\S]*?<\/dcp-system-reminder>/gi;
|
|
3920
|
+
function stripContextReminderBlocks(text) {
|
|
3921
|
+
if (!text.includes("<dcp-system-reminder")) return text;
|
|
3922
|
+
return text.replace(DCP_REMINDER_BLOCK, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
3923
|
+
}
|
|
3924
|
+
function shouldStripContextReminders(options) {
|
|
3925
|
+
if (options.enabled !== true) return false;
|
|
3926
|
+
const namesCompress = (list2) => (list2 ?? []).some((name) => String(name).trim().toLowerCase() === "compress");
|
|
3927
|
+
return !namesCompress(options.proxyTools) && !namesCompress(options.proxyOpencodeTools);
|
|
3928
|
+
}
|
|
3929
|
+
function stripContextReminders(prompt) {
|
|
3930
|
+
let removed = 0;
|
|
3931
|
+
const countIn = (text) => (text.match(DCP_REMINDER_BLOCK) ?? []).length;
|
|
3932
|
+
const out = prompt.map((message) => {
|
|
3933
|
+
if (message.role !== "user" && message.role !== "assistant") return message;
|
|
3934
|
+
if (!Array.isArray(message.content)) return message;
|
|
3935
|
+
let touched = false;
|
|
3936
|
+
const parts = message.content.map((part) => {
|
|
3937
|
+
if (!part || part.type !== "text" || typeof part.text !== "string") return part;
|
|
3938
|
+
const hits = countIn(part.text);
|
|
3939
|
+
if (hits === 0) return part;
|
|
3940
|
+
removed += hits;
|
|
3941
|
+
touched = true;
|
|
3942
|
+
return { ...part, text: stripContextReminderBlocks(part.text) };
|
|
3943
|
+
});
|
|
3944
|
+
return touched ? { ...message, content: parts } : message;
|
|
3945
|
+
});
|
|
3946
|
+
return removed > 0 ? { prompt: out, removed } : { prompt, removed: 0 };
|
|
3947
|
+
}
|
|
3761
3948
|
var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
3762
3949
|
"image/jpeg",
|
|
3763
3950
|
"image/png",
|
|
@@ -3957,6 +4144,15 @@ function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {})
|
|
|
3957
4144
|
const compactionMode = opts.compactionMode === true;
|
|
3958
4145
|
const cliToolCallIds = opts.cliToolCallIds;
|
|
3959
4146
|
const content = [];
|
|
4147
|
+
if (opts.stripContextReminders) {
|
|
4148
|
+
const stripped = stripContextReminders(prompt);
|
|
4149
|
+
if (stripped.removed > 0) {
|
|
4150
|
+
log.info("stripped unsatisfiable context reminders", {
|
|
4151
|
+
blocks: stripped.removed
|
|
4152
|
+
});
|
|
4153
|
+
prompt = stripped.prompt;
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
3960
4156
|
const pushToolResult = (part) => {
|
|
3961
4157
|
const id = part.toolCallId;
|
|
3962
4158
|
const text = getToolResultText(part);
|
|
@@ -5059,7 +5255,7 @@ ${text}`
|
|
|
5059
5255
|
}
|
|
5060
5256
|
return parts.join("\n\n");
|
|
5061
5257
|
}
|
|
5062
|
-
function
|
|
5258
|
+
function interactiveExtraArgs(opts) {
|
|
5063
5259
|
const extraArgs = [];
|
|
5064
5260
|
if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
|
|
5065
5261
|
extraArgs.push(
|
|
@@ -5068,6 +5264,9 @@ function spawnInteractiveProcess(opts) {
|
|
|
5068
5264
|
"--strict-mcp-config"
|
|
5069
5265
|
);
|
|
5070
5266
|
}
|
|
5267
|
+
for (const dir of opts.pluginDirs ?? []) {
|
|
5268
|
+
extraArgs.push("--plugin-dir", dir);
|
|
5269
|
+
}
|
|
5071
5270
|
const flagSettings = {};
|
|
5072
5271
|
if (opts.permissionsAllow && opts.permissionsAllow.length > 0) {
|
|
5073
5272
|
flagSettings.permissions = { allow: opts.permissionsAllow };
|
|
@@ -5088,6 +5287,10 @@ function spawnInteractiveProcess(opts) {
|
|
|
5088
5287
|
if (opts.systemPromptFile) {
|
|
5089
5288
|
extraArgs.push("--append-system-prompt-file", opts.systemPromptFile);
|
|
5090
5289
|
}
|
|
5290
|
+
return extraArgs;
|
|
5291
|
+
}
|
|
5292
|
+
function spawnInteractiveProcess(opts) {
|
|
5293
|
+
const extraArgs = interactiveExtraArgs(opts);
|
|
5091
5294
|
const session = new ClaudeSession({
|
|
5092
5295
|
cwd: opts.cwd,
|
|
5093
5296
|
cliPath: opts.cliPath,
|
|
@@ -5503,6 +5706,14 @@ You are running via the Claude Code CLI (not a direct API call). This affects co
|
|
|
5503
5706
|
- Everything outside the summary is gone after the reset \u2014 tool output, files you read, and the earlier conversation are not replayed. Write the summary as the authoritative record.
|
|
5504
5707
|
- The \`distill\`, \`prune\`, and \`extract\` tools are NOT available.
|
|
5505
5708
|
- DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
|
|
5709
|
+
var CLAUDE_CLI_OPENCODE_COMPRESS_NOTE = `## Runtime environment: Claude Code CLI
|
|
5710
|
+
|
|
5711
|
+
You are running via the Claude Code CLI (not a direct API call). This affects context management:
|
|
5712
|
+
|
|
5713
|
+
- To compress context, call \`mcp__opencode_proxy__compress\`. Use that exact full name. It runs opencode's own \`compress\` tool, which is what a "MAX CONTEXT LIMIT REACHED" reminder is asking you to do.
|
|
5714
|
+
- It compresses opencode's stored conversation, NOT this Claude Code session. Your current session keeps the context it already has, so do not assume earlier detail is gone after the call.
|
|
5715
|
+
- The \`distill\`, \`prune\`, and \`extract\` tools are NOT available.
|
|
5716
|
+
- DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
|
|
5506
5717
|
function extractSystemMessages(prompt) {
|
|
5507
5718
|
const out = [];
|
|
5508
5719
|
for (const msg of prompt) {
|
|
@@ -5529,7 +5740,7 @@ ${options.compressionSummary.trim()}`
|
|
|
5529
5740
|
);
|
|
5530
5741
|
}
|
|
5531
5742
|
parts.push(
|
|
5532
|
-
options.compressEnabled ? CLAUDE_CLI_COMPRESS_NOTE : CLAUDE_CLI_CONTEXT_NOTE
|
|
5743
|
+
options.compressEnabled ? CLAUDE_CLI_COMPRESS_NOTE : options.opencodeCompressEnabled ? CLAUDE_CLI_OPENCODE_COMPRESS_NOTE : CLAUDE_CLI_CONTEXT_NOTE
|
|
5533
5744
|
);
|
|
5534
5745
|
for (const s of extraSystemContent) {
|
|
5535
5746
|
if (s.trim()) parts.push(s.trim());
|
|
@@ -5776,9 +5987,22 @@ var ClaudeCodeLanguageModel = class {
|
|
|
5776
5987
|
resolved: items !== void 0,
|
|
5777
5988
|
taskDescription: items?.find((item) => item.id === "task")?.description,
|
|
5778
5989
|
questionDescription: question?.description,
|
|
5779
|
-
hasQuestion: !!question
|
|
5990
|
+
hasQuestion: !!question,
|
|
5991
|
+
items
|
|
5780
5992
|
};
|
|
5781
5993
|
}
|
|
5994
|
+
/**
|
|
5995
|
+
* Whether dcp-style context reminders should be stripped from this turn's
|
|
5996
|
+
* messages. Config-only and synchronous, so it can be answered before the
|
|
5997
|
+
* spawn block resolves anything: `userMsg` is built well ahead of it.
|
|
5998
|
+
*/
|
|
5999
|
+
stripContextRemindersEnabled() {
|
|
6000
|
+
return shouldStripContextReminders({
|
|
6001
|
+
enabled: this.config.stripContextReminders,
|
|
6002
|
+
proxyTools: this.config.proxyTools,
|
|
6003
|
+
proxyOpencodeTools: this.config.proxyOpencodeTools
|
|
6004
|
+
});
|
|
6005
|
+
}
|
|
5782
6006
|
/** Share one lazy registry request within a turn without making it stale. */
|
|
5783
6007
|
createLiveToolInfoLoader() {
|
|
5784
6008
|
let pending;
|
|
@@ -5814,10 +6038,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
5814
6038
|
* Create a proxy MCP server for a single active Claude process/session.
|
|
5815
6039
|
* The process lifecycle owns the server lifecycle via session-manager.
|
|
5816
6040
|
*/
|
|
5817
|
-
async ensureProxyServer(tools, sessionKeyForCalls) {
|
|
6041
|
+
async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress) {
|
|
5818
6042
|
const timeoutOverrides = this.config.proxyToolTimeoutMs;
|
|
5819
6043
|
const interceptors = /* @__PURE__ */ new Map();
|
|
5820
|
-
if (tools.some((t) => t.name === "compress")) {
|
|
6044
|
+
if (interceptCompress && tools.some((t) => t.name === "compress")) {
|
|
5821
6045
|
interceptors.set("compress", (input) => {
|
|
5822
6046
|
const summary = typeof input.summary === "string" ? input.summary.trim() : "";
|
|
5823
6047
|
if (!summary) {
|
|
@@ -6258,7 +6482,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6258
6482
|
// at all: every tool result reaching it belongs to opencode and must be
|
|
6259
6483
|
// rendered as text rather than an orphaned `tool_result` (issue #29).
|
|
6260
6484
|
getClaudeUserMessage(options.prompt, includeHistoryContext, {
|
|
6261
|
-
cliToolCallIds: /* @__PURE__ */ new Set()
|
|
6485
|
+
cliToolCallIds: /* @__PURE__ */ new Set(),
|
|
6486
|
+
stripContextReminders: this.stripContextRemindersEnabled()
|
|
6262
6487
|
});
|
|
6263
6488
|
const [runtimeStatus, cliVersion, planModeQuestionActive] = await Promise.all([
|
|
6264
6489
|
getRuntimeMcpStatus(),
|
|
@@ -6274,6 +6499,11 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6274
6499
|
{ compressEnabled: false, compressionSummary: getCompressionSummary(sk) }
|
|
6275
6500
|
);
|
|
6276
6501
|
const { model: spawnModelId, fast: fastMode } = parseModelId(effectiveModelId);
|
|
6502
|
+
const skillPluginDirs = await resolveSkillPluginDirs({
|
|
6503
|
+
cwd,
|
|
6504
|
+
cliPath: this.config.cliPath,
|
|
6505
|
+
enabled: this.config.bridgeOpencodeSkills === true
|
|
6506
|
+
});
|
|
6277
6507
|
const cliArgs = buildCliArgs({
|
|
6278
6508
|
sessionKey: sk,
|
|
6279
6509
|
skipPermissions: this.config.skipPermissions !== false,
|
|
@@ -6284,6 +6514,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6284
6514
|
strictMcpConfig: this.config.strictMcpConfig,
|
|
6285
6515
|
disallowedTools: this.config.webSearch === "disabled" ? ["WebSearch"] : void 0,
|
|
6286
6516
|
appendSystemPromptFile: systemPromptFile,
|
|
6517
|
+
pluginDirs: skillPluginDirs,
|
|
6287
6518
|
...this.thinkingCliOptions(),
|
|
6288
6519
|
fastMode,
|
|
6289
6520
|
cliVersion
|
|
@@ -6771,7 +7002,8 @@ ${plan}
|
|
|
6771
7002
|
const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
|
|
6772
7003
|
const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(options.prompt, includeHistoryContext, {
|
|
6773
7004
|
compactionMode,
|
|
6774
|
-
cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId))
|
|
7005
|
+
cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId)),
|
|
7006
|
+
stripContextReminders: this.stripContextRemindersEnabled()
|
|
6775
7007
|
});
|
|
6776
7008
|
const resolvedProxy = compactionMode ? null : this.resolvedProxyTools();
|
|
6777
7009
|
const loadLiveToolInfo = this.createLiveToolInfoLoader();
|
|
@@ -6883,6 +7115,11 @@ ${plan}
|
|
|
6883
7115
|
"interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI"
|
|
6884
7116
|
);
|
|
6885
7117
|
}
|
|
7118
|
+
const skillPluginDirs = await resolveSkillPluginDirs({
|
|
7119
|
+
cwd,
|
|
7120
|
+
cliPath,
|
|
7121
|
+
enabled: self.config.bridgeOpencodeSkills === true
|
|
7122
|
+
});
|
|
6886
7123
|
const ap = spawnInteractiveProcess({
|
|
6887
7124
|
cwd,
|
|
6888
7125
|
cliPath,
|
|
@@ -6890,6 +7127,7 @@ ${plan}
|
|
|
6890
7127
|
model: spawnModelId,
|
|
6891
7128
|
fastMode,
|
|
6892
7129
|
mcpConfigPaths: mcp.paths,
|
|
7130
|
+
pluginDirs: skillPluginDirs,
|
|
6893
7131
|
permissionsAllow: allow,
|
|
6894
7132
|
systemPromptFile,
|
|
6895
7133
|
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
@@ -6933,7 +7171,12 @@ ${plan}
|
|
|
6933
7171
|
const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
|
|
6934
7172
|
const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
|
|
6935
7173
|
const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
|
|
6936
|
-
const
|
|
7174
|
+
const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
|
|
7175
|
+
log.debug("opencode tool forwarding gate", {
|
|
7176
|
+
requested: self.config.proxyOpencodeTools ?? null,
|
|
7177
|
+
opencodeToolsRequested
|
|
7178
|
+
});
|
|
7179
|
+
const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
|
|
6937
7180
|
resolved: false,
|
|
6938
7181
|
taskDescription: void 0,
|
|
6939
7182
|
questionDescription: void 0,
|
|
@@ -6969,13 +7212,33 @@ ${plan}
|
|
|
6969
7212
|
kept: liveToolInfo.hasQuestion
|
|
6970
7213
|
});
|
|
6971
7214
|
}
|
|
7215
|
+
const opencodeToolDefs = resolveProxyOpencodeToolDefs({
|
|
7216
|
+
requested: self.config.proxyOpencodeTools,
|
|
7217
|
+
items: liveToolInfo.items,
|
|
7218
|
+
taken: new Set(
|
|
7219
|
+
[...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
|
|
7220
|
+
(t) => t.name
|
|
7221
|
+
)
|
|
7222
|
+
)
|
|
7223
|
+
});
|
|
7224
|
+
if (opencodeToolDefs.length > 0) {
|
|
7225
|
+
log.info("forwarding opencode tools through the proxy", {
|
|
7226
|
+
tools: opencodeToolDefs.map((t) => t.name)
|
|
7227
|
+
});
|
|
7228
|
+
}
|
|
6972
7229
|
const combinedList = [
|
|
6973
7230
|
...enrichedProxy ?? [],
|
|
6974
|
-
...proxyMcpTools ?? []
|
|
7231
|
+
...proxyMcpTools ?? [],
|
|
7232
|
+
...opencodeToolDefs
|
|
6975
7233
|
];
|
|
6976
7234
|
const combinedProxyTools = combinedList.length > 0 ? combinedList : null;
|
|
7235
|
+
const pluginCompressEnabled = enrichedProxy?.some((t) => t.name === "compress") ?? false;
|
|
6977
7236
|
if (!proxyServer && combinedProxyTools) {
|
|
6978
|
-
proxyServer = await self.ensureProxyServer(
|
|
7237
|
+
proxyServer = await self.ensureProxyServer(
|
|
7238
|
+
combinedProxyTools,
|
|
7239
|
+
sk,
|
|
7240
|
+
pluginCompressEnabled
|
|
7241
|
+
);
|
|
6979
7242
|
}
|
|
6980
7243
|
const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
|
|
6981
7244
|
const allDisallowed = resolveDisallowedTools({
|
|
@@ -6998,7 +7261,10 @@ ${plan}
|
|
|
6998
7261
|
...questionProxyActive ? [QUESTION_PROXY_HINT] : []
|
|
6999
7262
|
],
|
|
7000
7263
|
{
|
|
7001
|
-
compressEnabled:
|
|
7264
|
+
compressEnabled: pluginCompressEnabled,
|
|
7265
|
+
opencodeCompressEnabled: opencodeToolDefs.some(
|
|
7266
|
+
(t) => t.name === "compress"
|
|
7267
|
+
),
|
|
7002
7268
|
compressionSummary: getCompressionSummary(sk)
|
|
7003
7269
|
}
|
|
7004
7270
|
);
|
|
@@ -7221,6 +7487,7 @@ ${plan}
|
|
|
7221
7487
|
if (entries.length === 0) return false;
|
|
7222
7488
|
endTextBlock();
|
|
7223
7489
|
watchdogMessage = makeLateProxyResultMessage(entries);
|
|
7490
|
+
if (activeProcess) noteTurnStarted(activeProcess);
|
|
7224
7491
|
proc.stdin.write(watchdogMessage + "\n");
|
|
7225
7492
|
for (const { call } of entries) pending.delete(call.toolCallId);
|
|
7226
7493
|
log.warn("delivering proxy results after interrupted continuation", {
|
|
@@ -7482,7 +7749,10 @@ ${plan}
|
|
|
7482
7749
|
controllerClosed = true;
|
|
7483
7750
|
cleanupTurn();
|
|
7484
7751
|
if (!useInteractive && !compactionMode) {
|
|
7485
|
-
scheduleIdleProcessEviction(
|
|
7752
|
+
scheduleIdleProcessEviction(
|
|
7753
|
+
sk,
|
|
7754
|
+
resolveIdleProcessTimeoutMs(self.config.idleProcessTimeoutMs)
|
|
7755
|
+
);
|
|
7486
7756
|
}
|
|
7487
7757
|
try {
|
|
7488
7758
|
controller.close();
|
|
@@ -8277,7 +8547,7 @@ ${plan}
|
|
|
8277
8547
|
if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
|
|
8278
8548
|
if (msg.is_error && msg.result) text = msg.result;
|
|
8279
8549
|
}
|
|
8280
|
-
if (text) controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: text });
|
|
8550
|
+
if (text) controller.enqueue({ type: "text-delta", id: currentTextId ?? startTextBlock(), delta: text });
|
|
8281
8551
|
} catch {
|
|
8282
8552
|
}
|
|
8283
8553
|
}
|
|
@@ -8334,9 +8604,25 @@ ${plan}
|
|
|
8334
8604
|
});
|
|
8335
8605
|
proc.on("error", procErrorHandler);
|
|
8336
8606
|
if (options.abortSignal) {
|
|
8607
|
+
const releaseAbandonedProxyCalls = (reason) => {
|
|
8608
|
+
if (drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
|
|
8609
|
+
rejectAllPendingProxyCallsForSession(sk, new Error(reason));
|
|
8610
|
+
drainBuffer.length = 0;
|
|
8611
|
+
};
|
|
8337
8612
|
options.abortSignal.addEventListener("abort", () => {
|
|
8338
8613
|
autoContinueState.aborted = true;
|
|
8339
|
-
if (turnCompleted || controllerClosed)
|
|
8614
|
+
if (turnCompleted || controllerClosed) {
|
|
8615
|
+
if (activeProcess && activeProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
|
|
8616
|
+
log.info("abort between proxy tool boundaries; releasing pending calls", { sk });
|
|
8617
|
+
void interruptTurn(activeProcess).then((idle) => {
|
|
8618
|
+
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
8619
|
+
});
|
|
8620
|
+
releaseAbandonedProxyCalls(
|
|
8621
|
+
"Provider stream was aborted while opencode was running its proxy tool calls"
|
|
8622
|
+
);
|
|
8623
|
+
}
|
|
8624
|
+
return;
|
|
8625
|
+
}
|
|
8340
8626
|
if (activeProcess) {
|
|
8341
8627
|
void interruptTurn(activeProcess).then((idle) => {
|
|
8342
8628
|
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
@@ -8347,15 +8633,9 @@ ${plan}
|
|
|
8347
8633
|
"abort signal received before content, closing stream immediately",
|
|
8348
8634
|
{ cwd }
|
|
8349
8635
|
);
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
new Error(
|
|
8354
|
-
"Provider stream was aborted before pending proxy calls were emitted"
|
|
8355
|
-
)
|
|
8356
|
-
);
|
|
8357
|
-
drainBuffer.length = 0;
|
|
8358
|
-
}
|
|
8636
|
+
releaseAbandonedProxyCalls(
|
|
8637
|
+
"Provider stream was aborted before pending proxy calls were emitted"
|
|
8638
|
+
);
|
|
8359
8639
|
controllerClosed = true;
|
|
8360
8640
|
cleanupTurn();
|
|
8361
8641
|
try {
|
|
@@ -8368,6 +8648,9 @@ ${plan}
|
|
|
8368
8648
|
"abort signal received mid-turn, starting grace period",
|
|
8369
8649
|
{ cwd }
|
|
8370
8650
|
);
|
|
8651
|
+
releaseAbandonedProxyCalls(
|
|
8652
|
+
"Provider stream was aborted while proxy tool calls were pending"
|
|
8653
|
+
);
|
|
8371
8654
|
startResultFallback(5e3);
|
|
8372
8655
|
});
|
|
8373
8656
|
}
|
|
@@ -8805,6 +9088,8 @@ function createClaudeCode(settings = {}) {
|
|
|
8805
9088
|
controlRequestToolBehaviors: settings.controlRequestToolBehaviors,
|
|
8806
9089
|
controlRequestDenyMessage: settings.controlRequestDenyMessage,
|
|
8807
9090
|
proxyTools,
|
|
9091
|
+
proxyOpencodeTools: settings.proxyOpencodeTools,
|
|
9092
|
+
stripContextReminders: settings.stripContextReminders === true,
|
|
8808
9093
|
extraDisallowedTools: settings.extraDisallowedTools,
|
|
8809
9094
|
proxyToolTimeoutMs: settings.proxyToolTimeoutMs,
|
|
8810
9095
|
planModeQuestion: settings.planModeQuestion ?? false,
|
|
@@ -9007,8 +9292,16 @@ async function buildAgentRegistry(config) {
|
|
|
9007
9292
|
defaultSubagentModel: getDefaultSubagentModel()
|
|
9008
9293
|
});
|
|
9009
9294
|
}
|
|
9295
|
+
function extractDeletedSessionId(event) {
|
|
9296
|
+
const payload = event?.payload ?? event;
|
|
9297
|
+
if (!payload || payload.type !== "session.deleted") return void 0;
|
|
9298
|
+
const properties = payload.properties;
|
|
9299
|
+
const id = properties?.info?.id;
|
|
9300
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
9301
|
+
}
|
|
9010
9302
|
var server = async (input) => {
|
|
9011
9303
|
cleanupStaleUnscopedInstall();
|
|
9304
|
+
ensureProcessExitCleanup();
|
|
9012
9305
|
const opencodeVersion = pickOpencodeVersion(input);
|
|
9013
9306
|
if (input && typeof input === "object" && "client" in input) {
|
|
9014
9307
|
setOpencodeClient(input.client);
|
|
@@ -9043,10 +9336,21 @@ var server = async (input) => {
|
|
|
9043
9336
|
opencodeVersion
|
|
9044
9337
|
);
|
|
9045
9338
|
},
|
|
9046
|
-
//
|
|
9047
|
-
// hot-reload check in `claude-code-language-model.ts`,
|
|
9048
|
-
// claude safely between turns
|
|
9049
|
-
// an in-flight stream and abort the user's
|
|
9339
|
+
// Only `session.deleted` is acted on. MCP config drift is still detected
|
|
9340
|
+
// at turn start by the hot-reload check in `claude-code-language-model.ts`,
|
|
9341
|
+
// which respawns claude safely between turns, and eviction on
|
|
9342
|
+
// `global.disposed` would kill an in-flight stream and abort the user's
|
|
9343
|
+
// current turn. A deleted session has no turn left to abort, and its
|
|
9344
|
+
// `claude` child would otherwise linger until the idle timer or LRU
|
|
9345
|
+
// pressure took it, with its session id kept for a resume that never comes.
|
|
9346
|
+
event: async ({ event }) => {
|
|
9347
|
+
const sessionID = extractDeletedSessionId(event);
|
|
9348
|
+
if (!sessionID) return;
|
|
9349
|
+
const released = deleteActiveProcessesForSession(sessionID);
|
|
9350
|
+
if (released.length > 0) {
|
|
9351
|
+
log.info("released claude state for deleted session", { sessionID, released });
|
|
9352
|
+
}
|
|
9353
|
+
},
|
|
9050
9354
|
provider: {
|
|
9051
9355
|
id: PROVIDER_ID2,
|
|
9052
9356
|
models: async (provider) => defaultModelsForProvider(provider.models)
|
|
@@ -9101,6 +9405,7 @@ export {
|
|
|
9101
9405
|
createClaudeCode,
|
|
9102
9406
|
index_default as default,
|
|
9103
9407
|
defaultModels,
|
|
9408
|
+
extractDeletedSessionId,
|
|
9104
9409
|
getAgentRegistry,
|
|
9105
9410
|
getDefaultSubagentModel,
|
|
9106
9411
|
registerDoctorCommand,
|