@khalilgharbaoui/opencode-claude-code-plugin 0.19.0 → 0.20.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));
@@ -1246,7 +1256,7 @@ function readBody(req) {
1246
1256
  req.on("error", reject);
1247
1257
  });
1248
1258
  }
1249
- function writeToolCallResult(res, requestId, result, sse = null) {
1259
+ function writeToolCallResult(res, requestId, result, reply = null) {
1250
1260
  const text = result.kind === "error" ? result.message : result.text;
1251
1261
  const isError = result.kind === "error" || result.isError === true;
1252
1262
  const envelope = {
@@ -1257,26 +1267,20 @@ function writeToolCallResult(res, requestId, result, sse = null) {
1257
1267
  isError
1258
1268
  }
1259
1269
  };
1260
- if (sse) {
1261
- sse.finish(envelope);
1270
+ if (reply) {
1271
+ reply.finish(envelope);
1262
1272
  return;
1263
1273
  }
1264
1274
  writeJson(res, envelope);
1265
1275
  }
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");
1276
+ function startKeepalive(res, keepaliveMs, ping) {
1273
1277
  let timer = setInterval(() => {
1274
1278
  if (res.writableEnded || res.destroyed) {
1275
1279
  stop();
1276
1280
  return;
1277
1281
  }
1278
- res.write(": keepalive\n\n");
1279
- }, SSE_KEEPALIVE_MS);
1282
+ res.write(ping);
1283
+ }, keepaliveMs);
1280
1284
  timer.unref?.();
1281
1285
  const stop = () => {
1282
1286
  if (timer) {
@@ -1284,6 +1288,16 @@ function openEventStream(res) {
1284
1288
  timer = null;
1285
1289
  }
1286
1290
  };
1291
+ return stop;
1292
+ }
1293
+ function openEventStream(res, keepaliveMs) {
1294
+ res.statusCode = 200;
1295
+ res.setHeader("Content-Type", "text/event-stream");
1296
+ res.setHeader("Cache-Control", "no-cache, no-transform");
1297
+ res.setHeader("Connection", "keep-alive");
1298
+ res.flushHeaders();
1299
+ res.write(": open\n\n");
1300
+ const stop = startKeepalive(res, keepaliveMs, ": keepalive\n\n");
1287
1301
  return {
1288
1302
  stop,
1289
1303
  finish(envelope) {
@@ -1296,6 +1310,21 @@ data: ${JSON.stringify(envelope)}
1296
1310
  }
1297
1311
  };
1298
1312
  }
1313
+ function openJsonStream(res, keepaliveMs) {
1314
+ res.statusCode = 200;
1315
+ res.setHeader("Content-Type", "application/json");
1316
+ res.setHeader("Cache-Control", "no-cache, no-transform");
1317
+ res.flushHeaders();
1318
+ const stop = startKeepalive(res, keepaliveMs, " ");
1319
+ return {
1320
+ stop,
1321
+ finish(envelope) {
1322
+ stop();
1323
+ if (res.writableEnded || res.destroyed) return;
1324
+ res.end(JSON.stringify(envelope));
1325
+ }
1326
+ };
1327
+ }
1299
1328
  function writeJson(res, body) {
1300
1329
  if (res.destroyed || res.writableEnded) return;
1301
1330
  const payload = JSON.stringify(body);
@@ -1306,6 +1335,7 @@ function writeJson(res, body) {
1306
1335
  }
1307
1336
 
1308
1337
  // src/proxy-broker.ts
1338
+ import { EventEmitter as EventEmitter2 } from "events";
1309
1339
  var pendingByCallId = /* @__PURE__ */ new Map();
1310
1340
  var callIdsBySession = /* @__PURE__ */ new Map();
1311
1341
  var emitter = new EventEmitter2();
@@ -1334,7 +1364,7 @@ function onPendingProxyCall(sessionKey2, handler) {
1334
1364
  function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
1335
1365
  const previous = pendingByCallId.get(call.id);
1336
1366
  if (previous) {
1337
- clearTimeout(previous.timer);
1367
+ if (previous.timer) clearTimeout(previous.timer);
1338
1368
  previous.reject(
1339
1369
  new Error(`Replaced pending proxy call ${call.id} with a fresh one`)
1340
1370
  );
@@ -1346,7 +1376,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
1346
1376
  call.input,
1347
1377
  timeoutOverrides
1348
1378
  );
1349
- const timer = setTimeout(() => {
1379
+ const timer = deadlineMs > PROXY_NO_DEADLINE_MS ? setTimeout(() => {
1350
1380
  const current = pendingByCallId.get(call.id);
1351
1381
  if (!current) return;
1352
1382
  pendingByCallId.delete(call.id);
@@ -1358,7 +1388,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
1358
1388
  toolName: call.toolName,
1359
1389
  deadlineMs
1360
1390
  });
1361
- }, deadlineMs);
1391
+ }, deadlineMs) : null;
1362
1392
  const pending = {
1363
1393
  sessionKey: sessionKey2,
1364
1394
  toolCallId: call.id,
@@ -1418,7 +1448,7 @@ function resolvePendingProxyCallById(toolCallId, result) {
1418
1448
  if (!pending) return false;
1419
1449
  pendingByCallId.delete(toolCallId);
1420
1450
  indexRemove(pending.sessionKey, toolCallId);
1421
- clearTimeout(pending.timer);
1451
+ if (pending.timer) clearTimeout(pending.timer);
1422
1452
  pending.resolve(result);
1423
1453
  log.info("resolved pending proxy call", {
1424
1454
  sessionKey: pending.sessionKey,
@@ -1432,7 +1462,7 @@ function rejectPendingProxyCallById(toolCallId, error) {
1432
1462
  if (!pending) return false;
1433
1463
  pendingByCallId.delete(toolCallId);
1434
1464
  indexRemove(pending.sessionKey, toolCallId);
1435
- clearTimeout(pending.timer);
1465
+ if (pending.timer) clearTimeout(pending.timer);
1436
1466
  pending.reject(error);
1437
1467
  log.notice("rejected pending proxy call", {
1438
1468
  sessionKey: pending.sessionKey,
@@ -1864,9 +1894,14 @@ var activeProcesses = /* @__PURE__ */ new Map();
1864
1894
  var claudeSessions = /* @__PURE__ */ new Map();
1865
1895
  var idleEvictionTimers = /* @__PURE__ */ new Map();
1866
1896
  var MAX_IDLE_TIMEOUT_MS = 2147483647;
1897
+ var DEFAULT_IDLE_PROCESS_TIMEOUT_MS = 0;
1898
+ function resolveIdleProcessTimeoutMs(configured) {
1899
+ return configured === void 0 ? DEFAULT_IDLE_PROCESS_TIMEOUT_MS : configured;
1900
+ }
1867
1901
  var MAX_ACTIVE_PROCESSES = 16;
1868
1902
  var PROCESS_EXIT_TIMEOUT_MS = 1500;
1869
1903
  var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
1904
+ var CHILD_EXITED_MESSAGE = "Claude CLI subprocess closed before pending tool calls were resolved";
1870
1905
  function envFlagEnabled(value) {
1871
1906
  if (value === void 0) return false;
1872
1907
  const normalized = value.trim().toLowerCase();
@@ -2016,6 +2051,11 @@ function scheduleIdleProcessEviction(key, timeoutMs) {
2016
2051
  const timer = setTimeout(() => {
2017
2052
  idleEvictionTimers.delete(key);
2018
2053
  if (activeProcesses.get(key) !== scheduledProcess) return;
2054
+ if (isTurnInFlight(scheduledProcess)) {
2055
+ log.info("idle timer found a turn in flight; re-arming", { sessionKey: key, timeoutMs });
2056
+ scheduleIdleProcessEviction(key, timeoutMs);
2057
+ return;
2058
+ }
2019
2059
  log.info("evicting idle claude process", { sessionKey: key, timeoutMs });
2020
2060
  deleteActiveProcess(key);
2021
2061
  }, timeoutMs);
@@ -2027,9 +2067,44 @@ function detachActiveProcess(key) {
2027
2067
  const ap = activeProcesses.get(key);
2028
2068
  if (!ap) return void 0;
2029
2069
  activeProcesses.delete(key);
2030
- void ap.proxyServer?.close();
2070
+ if (ap.proxyServer) {
2071
+ void ap.proxyServer.close();
2072
+ rejectAllPendingProxyCallsForSession(key, new Error(SERVER_CLOSED_MESSAGE));
2073
+ }
2031
2074
  return ap;
2032
2075
  }
2076
+ function deleteActiveProcessesForSession(sessionID) {
2077
+ if (!sessionID || sessionID === "default") return [];
2078
+ const released = [];
2079
+ for (const [key, ap] of [...activeProcesses]) {
2080
+ const owned = ap.opencodeSessionID === sessionID || describeSessionKey(key).session === sessionID;
2081
+ if (!owned) continue;
2082
+ log.info("releasing claude process for deleted session", { sessionKey: key, sessionID });
2083
+ void deleteActiveProcessAndWait(key);
2084
+ released.push(key);
2085
+ }
2086
+ for (const key of [...claudeSessions.keys()]) {
2087
+ if (describeSessionKey(key).session !== sessionID) continue;
2088
+ deleteClaudeSessionId(key);
2089
+ clearCompression(key);
2090
+ if (!released.includes(key)) released.push(key);
2091
+ }
2092
+ return released;
2093
+ }
2094
+ function killAllActiveProcesses() {
2095
+ const keys = [...activeProcesses.keys()];
2096
+ for (const key of keys) deleteActiveProcess(key);
2097
+ return keys;
2098
+ }
2099
+ var processExitCleanupWired = false;
2100
+ function ensureProcessExitCleanup() {
2101
+ if (processExitCleanupWired) return false;
2102
+ processExitCleanupWired = true;
2103
+ process.once("exit", () => {
2104
+ killAllActiveProcesses();
2105
+ });
2106
+ return true;
2107
+ }
2033
2108
  function deleteActiveProcess(key) {
2034
2109
  const ap = detachActiveProcess(key);
2035
2110
  ap?.proc.kill();
@@ -2178,6 +2253,12 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
2178
2253
  if (ownsSessionKey) {
2179
2254
  cancelIdleProcessEviction(sessionKey2);
2180
2255
  activeProcesses.delete(sessionKey2);
2256
+ if (getPendingProxyCalls(sessionKey2).length > 0) {
2257
+ rejectAllPendingProxyCallsForSession(
2258
+ sessionKey2,
2259
+ new Error(CHILD_EXITED_MESSAGE)
2260
+ );
2261
+ }
2181
2262
  }
2182
2263
  if (ownsSessionKey && code !== 0 && code !== null) {
2183
2264
  log.info("process exited with error, clearing session", {
@@ -2218,6 +2299,7 @@ function appendResumeIfNeeded(sessionKey2, cliArgs) {
2218
2299
  function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropicApiKey) {
2219
2300
  const old = activeProcesses.get(sessionKey2);
2220
2301
  if (!old) return void 0;
2302
+ const turnWasInFlight = isTurnInFlight(old);
2221
2303
  activeProcesses.delete(sessionKey2);
2222
2304
  old.proc.removeAllListeners("exit");
2223
2305
  try {
@@ -2237,6 +2319,7 @@ function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropi
2237
2319
  );
2238
2320
  replacement.pendingProxyCompletions = old.pendingProxyCompletions;
2239
2321
  delete old.pendingProxyCompletions;
2322
+ if (turnWasInFlight) noteTurnStarted(replacement);
2240
2323
  return replacement;
2241
2324
  }
2242
2325
  function buildCliArgs(opts) {
@@ -3532,10 +3615,9 @@ function formatDoctorReport(report) {
3532
3615
  lines.push("| tool | call id | age | deadline |");
3533
3616
  lines.push("|---|---|---|---|");
3534
3617
  for (const call of report.pendingCalls) {
3618
+ const deadline = call.deadlineMs > 0 ? formatAge(call.deadlineMs) : "none";
3535
3619
  lines.push(
3536
- `| ${call.toolName} | \`${call.toolCallId}\` | ${formatAge(call.ageMs)} | ${formatAge(
3537
- call.deadlineMs
3538
- )} |`
3620
+ `| ${call.toolName} | \`${call.toolCallId}\` | ${formatAge(call.ageMs)} | ${deadline} |`
3539
3621
  );
3540
3622
  }
3541
3623
  }
@@ -5059,7 +5141,7 @@ ${text}`
5059
5141
  }
5060
5142
  return parts.join("\n\n");
5061
5143
  }
5062
- function spawnInteractiveProcess(opts) {
5144
+ function interactiveExtraArgs(opts) {
5063
5145
  const extraArgs = [];
5064
5146
  if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
5065
5147
  extraArgs.push(
@@ -5068,6 +5150,9 @@ function spawnInteractiveProcess(opts) {
5068
5150
  "--strict-mcp-config"
5069
5151
  );
5070
5152
  }
5153
+ for (const dir of opts.pluginDirs ?? []) {
5154
+ extraArgs.push("--plugin-dir", dir);
5155
+ }
5071
5156
  const flagSettings = {};
5072
5157
  if (opts.permissionsAllow && opts.permissionsAllow.length > 0) {
5073
5158
  flagSettings.permissions = { allow: opts.permissionsAllow };
@@ -5088,6 +5173,10 @@ function spawnInteractiveProcess(opts) {
5088
5173
  if (opts.systemPromptFile) {
5089
5174
  extraArgs.push("--append-system-prompt-file", opts.systemPromptFile);
5090
5175
  }
5176
+ return extraArgs;
5177
+ }
5178
+ function spawnInteractiveProcess(opts) {
5179
+ const extraArgs = interactiveExtraArgs(opts);
5091
5180
  const session = new ClaudeSession({
5092
5181
  cwd: opts.cwd,
5093
5182
  cliPath: opts.cliPath,
@@ -6274,6 +6363,11 @@ var ClaudeCodeLanguageModel = class {
6274
6363
  { compressEnabled: false, compressionSummary: getCompressionSummary(sk) }
6275
6364
  );
6276
6365
  const { model: spawnModelId, fast: fastMode } = parseModelId(effectiveModelId);
6366
+ const skillPluginDirs = await resolveSkillPluginDirs({
6367
+ cwd,
6368
+ cliPath: this.config.cliPath,
6369
+ enabled: this.config.bridgeOpencodeSkills === true
6370
+ });
6277
6371
  const cliArgs = buildCliArgs({
6278
6372
  sessionKey: sk,
6279
6373
  skipPermissions: this.config.skipPermissions !== false,
@@ -6284,6 +6378,7 @@ var ClaudeCodeLanguageModel = class {
6284
6378
  strictMcpConfig: this.config.strictMcpConfig,
6285
6379
  disallowedTools: this.config.webSearch === "disabled" ? ["WebSearch"] : void 0,
6286
6380
  appendSystemPromptFile: systemPromptFile,
6381
+ pluginDirs: skillPluginDirs,
6287
6382
  ...this.thinkingCliOptions(),
6288
6383
  fastMode,
6289
6384
  cliVersion
@@ -6883,6 +6978,11 @@ ${plan}
6883
6978
  "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI"
6884
6979
  );
6885
6980
  }
6981
+ const skillPluginDirs = await resolveSkillPluginDirs({
6982
+ cwd,
6983
+ cliPath,
6984
+ enabled: self.config.bridgeOpencodeSkills === true
6985
+ });
6886
6986
  const ap = spawnInteractiveProcess({
6887
6987
  cwd,
6888
6988
  cliPath,
@@ -6890,6 +6990,7 @@ ${plan}
6890
6990
  model: spawnModelId,
6891
6991
  fastMode,
6892
6992
  mcpConfigPaths: mcp.paths,
6993
+ pluginDirs: skillPluginDirs,
6893
6994
  permissionsAllow: allow,
6894
6995
  systemPromptFile,
6895
6996
  ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
@@ -7221,6 +7322,7 @@ ${plan}
7221
7322
  if (entries.length === 0) return false;
7222
7323
  endTextBlock();
7223
7324
  watchdogMessage = makeLateProxyResultMessage(entries);
7325
+ if (activeProcess) noteTurnStarted(activeProcess);
7224
7326
  proc.stdin.write(watchdogMessage + "\n");
7225
7327
  for (const { call } of entries) pending.delete(call.toolCallId);
7226
7328
  log.warn("delivering proxy results after interrupted continuation", {
@@ -7482,7 +7584,10 @@ ${plan}
7482
7584
  controllerClosed = true;
7483
7585
  cleanupTurn();
7484
7586
  if (!useInteractive && !compactionMode) {
7485
- scheduleIdleProcessEviction(sk, self.config.idleProcessTimeoutMs);
7587
+ scheduleIdleProcessEviction(
7588
+ sk,
7589
+ resolveIdleProcessTimeoutMs(self.config.idleProcessTimeoutMs)
7590
+ );
7486
7591
  }
7487
7592
  try {
7488
7593
  controller.close();
@@ -8277,7 +8382,7 @@ ${plan}
8277
8382
  if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
8278
8383
  if (msg.is_error && msg.result) text = msg.result;
8279
8384
  }
8280
- if (text) controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: text });
8385
+ if (text) controller.enqueue({ type: "text-delta", id: currentTextId ?? startTextBlock(), delta: text });
8281
8386
  } catch {
8282
8387
  }
8283
8388
  }
@@ -8334,9 +8439,25 @@ ${plan}
8334
8439
  });
8335
8440
  proc.on("error", procErrorHandler);
8336
8441
  if (options.abortSignal) {
8442
+ const releaseAbandonedProxyCalls = (reason) => {
8443
+ if (drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
8444
+ rejectAllPendingProxyCallsForSession(sk, new Error(reason));
8445
+ drainBuffer.length = 0;
8446
+ };
8337
8447
  options.abortSignal.addEventListener("abort", () => {
8338
8448
  autoContinueState.aborted = true;
8339
- if (turnCompleted || controllerClosed) return;
8449
+ if (turnCompleted || controllerClosed) {
8450
+ if (activeProcess && activeProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
8451
+ log.info("abort between proxy tool boundaries; releasing pending calls", { sk });
8452
+ void interruptTurn(activeProcess).then((idle) => {
8453
+ log.info("interrupt sent for aborted turn", { sk, idle });
8454
+ });
8455
+ releaseAbandonedProxyCalls(
8456
+ "Provider stream was aborted while opencode was running its proxy tool calls"
8457
+ );
8458
+ }
8459
+ return;
8460
+ }
8340
8461
  if (activeProcess) {
8341
8462
  void interruptTurn(activeProcess).then((idle) => {
8342
8463
  log.info("interrupt sent for aborted turn", { sk, idle });
@@ -8347,15 +8468,9 @@ ${plan}
8347
8468
  "abort signal received before content, closing stream immediately",
8348
8469
  { cwd }
8349
8470
  );
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
- }
8471
+ releaseAbandonedProxyCalls(
8472
+ "Provider stream was aborted before pending proxy calls were emitted"
8473
+ );
8359
8474
  controllerClosed = true;
8360
8475
  cleanupTurn();
8361
8476
  try {
@@ -8368,6 +8483,9 @@ ${plan}
8368
8483
  "abort signal received mid-turn, starting grace period",
8369
8484
  { cwd }
8370
8485
  );
8486
+ releaseAbandonedProxyCalls(
8487
+ "Provider stream was aborted while proxy tool calls were pending"
8488
+ );
8371
8489
  startResultFallback(5e3);
8372
8490
  });
8373
8491
  }
@@ -9007,8 +9125,16 @@ async function buildAgentRegistry(config) {
9007
9125
  defaultSubagentModel: getDefaultSubagentModel()
9008
9126
  });
9009
9127
  }
9128
+ function extractDeletedSessionId(event) {
9129
+ const payload = event?.payload ?? event;
9130
+ if (!payload || payload.type !== "session.deleted") return void 0;
9131
+ const properties = payload.properties;
9132
+ const id = properties?.info?.id;
9133
+ return typeof id === "string" && id.length > 0 ? id : void 0;
9134
+ }
9010
9135
  var server = async (input) => {
9011
9136
  cleanupStaleUnscopedInstall();
9137
+ ensureProcessExitCleanup();
9012
9138
  const opencodeVersion = pickOpencodeVersion(input);
9013
9139
  if (input && typeof input === "object" && "client" in input) {
9014
9140
  setOpencodeClient(input.client);
@@ -9043,10 +9169,21 @@ var server = async (input) => {
9043
9169
  opencodeVersion
9044
9170
  );
9045
9171
  },
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.
9172
+ // Only `session.deleted` is acted on. MCP config drift is still detected
9173
+ // at turn start by the hot-reload check in `claude-code-language-model.ts`,
9174
+ // which respawns claude safely between turns, and eviction on
9175
+ // `global.disposed` would kill an in-flight stream and abort the user's
9176
+ // current turn. A deleted session has no turn left to abort, and its
9177
+ // `claude` child would otherwise linger until the idle timer or LRU
9178
+ // pressure took it, with its session id kept for a resume that never comes.
9179
+ event: async ({ event }) => {
9180
+ const sessionID = extractDeletedSessionId(event);
9181
+ if (!sessionID) return;
9182
+ const released = deleteActiveProcessesForSession(sessionID);
9183
+ if (released.length > 0) {
9184
+ log.info("released claude state for deleted session", { sessionID, released });
9185
+ }
9186
+ },
9050
9187
  provider: {
9051
9188
  id: PROVIDER_ID2,
9052
9189
  models: async (provider) => defaultModelsForProvider(provider.models)
@@ -9101,6 +9238,7 @@ export {
9101
9238
  createClaudeCode,
9102
9239
  index_default as default,
9103
9240
  defaultModels,
9241
+ extractDeletedSessionId,
9104
9242
  getAgentRegistry,
9105
9243
  getDefaultSubagentModel,
9106
9244
  registerDoctorCommand,