@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/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 SSE_KEEPALIVE_MS = 15e3;
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: 60 * 60 * 1e3,
528
- // 60 min
529
- task_batch: 60 * 60 * 1e3,
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 (typeof ov === "number" && ov > 0) ms = ov;
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
- for (const v of Object.values(PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS)) {
558
- if (v > ms) ms = v;
559
- }
560
- if (overrides) {
561
- for (const v of Object.values(overrides)) {
562
- if (typeof v === "number" && v > ms) ms = v;
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 get a 60-minute proxy deadline by default (configurable via proxyToolTimeoutMs).";
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. Same 60-minute proxy deadline as task (configurable via proxyToolTimeoutMs).";
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 sse = null;
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
- if (acceptsEventStream(req.headers.accept)) {
1024
- sse = openEventStream(res);
1025
- }
1030
+ reply = acceptsEventStream(req.headers.accept) ? openEventStream(res, keepaliveMs) : openJsonStream(res, keepaliveMs);
1026
1031
  res.once("close", () => {
1027
- sse?.stop();
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
- timer = setTimeout(() => {
1053
- if (!pending.has(callId)) return;
1054
- pending.delete(callId);
1055
- log.notice("proxy-mcp tool call timed out", {
1056
- callId,
1057
- toolName,
1058
- deadlineMs
1059
- });
1060
- reject2(buildProxyTimeoutError(toolName, deadlineMs));
1061
- }, deadlineMs);
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, sse);
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
- sse
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, sse = null) {
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 (sse) {
1261
- sse.finish(envelope);
1325
+ if (reply) {
1326
+ reply.finish(envelope);
1262
1327
  return;
1263
1328
  }
1264
1329
  writeJson(res, envelope);
1265
1330
  }
1266
- function openEventStream(res) {
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(": keepalive\n\n");
1279
- }, SSE_KEEPALIVE_MS);
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
- clearTimeout(previous.timer);
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
- clearTimeout(pending.timer);
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
- clearTimeout(pending.timer);
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
- void ap.proxyServer?.close();
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)} | ${formatAge(
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 spawnInteractiveProcess(opts) {
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 liveToolInfo = taskProxyEnabled || questionProxyEnabled ? await loadLiveToolInfo() : {
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(combinedProxyTools, sk);
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: enrichedProxy?.some((t) => t.name === "compress") ?? false,
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(sk, self.config.idleProcessTimeoutMs);
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) return;
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
- if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
8351
- rejectAllPendingProxyCallsForSession(
8352
- sk,
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
- // No `event` hook: MCP config drift is detected at turn start by the
9047
- // hot-reload check in `claude-code-language-model.ts`, which respawns
9048
- // claude safely between turns. Eviction on `global.disposed` would kill
9049
- // an in-flight stream and abort the user's current turn.
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,