@khalilgharbaoui/opencode-claude-code-plugin 0.9.3 → 0.11.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
@@ -733,6 +733,10 @@ import * as fs2 from "fs";
733
733
  import * as path2 from "path";
734
734
  import * as os2 from "os";
735
735
  import * as crypto from "crypto";
736
+ import {
737
+ parse as parseJsonc,
738
+ printParseErrorCode
739
+ } from "jsonc-parser";
736
740
 
737
741
  // src/tmp.ts
738
742
  import * as fs from "fs";
@@ -776,49 +780,18 @@ function dirExists(p) {
776
780
  return false;
777
781
  }
778
782
  }
779
- function stripJsonComments(text) {
780
- let out = "";
781
- let i = 0;
782
- let inString = null;
783
- while (i < text.length) {
784
- const c = text[i];
785
- if (inString) {
786
- out += c;
787
- if (c === "\\" && i + 1 < text.length) {
788
- out += text[i + 1];
789
- i += 2;
790
- continue;
791
- }
792
- if (c === inString) inString = null;
793
- i++;
794
- continue;
795
- }
796
- if (c === '"' || c === "'") {
797
- inString = c;
798
- out += c;
799
- i++;
800
- continue;
801
- }
802
- if (c === "/" && text[i + 1] === "/") {
803
- while (i < text.length && text[i] !== "\n") i++;
804
- continue;
805
- }
806
- if (c === "/" && text[i + 1] === "*") {
807
- i += 2;
808
- while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
809
- i++;
810
- i += 2;
811
- continue;
812
- }
813
- out += c;
814
- i++;
815
- }
816
- return out;
817
- }
818
783
  function readAndParse(file) {
819
784
  try {
820
785
  const raw = fs2.readFileSync(file, "utf8");
821
- return JSON.parse(stripJsonComments(raw));
786
+ const errors = [];
787
+ const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
788
+ if (errors.length > 0) {
789
+ const first = errors[0];
790
+ throw new Error(
791
+ `${printParseErrorCode(first.error)} at offset ${first.offset}`
792
+ );
793
+ }
794
+ return parsed;
822
795
  } catch (e) {
823
796
  log.warn("failed to parse opencode config", {
824
797
  file,
@@ -1001,6 +974,31 @@ function mergeMcp(target, source) {
1001
974
  return out;
1002
975
  }
1003
976
  function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
977
+ const {
978
+ servers: merged,
979
+ enabledServerNames: allEnabledServerNames,
980
+ hash
981
+ } = mergeOpencodeMcp(cwd, runtimeStatus);
982
+ const servers = {};
983
+ const bridgedServerNames = [];
984
+ for (const [name, spec] of Object.entries(merged)) {
985
+ if (!spec || typeof spec !== "object") continue;
986
+ if (excludeServers?.has(name)) continue;
987
+ const translated = translateServer(name, spec);
988
+ if (translated) {
989
+ servers[name] = translated;
990
+ bridgedServerNames.push(name);
991
+ }
992
+ }
993
+ return finishBridge({
994
+ servers,
995
+ bridgedServerNames,
996
+ allEnabledServerNames,
997
+ hash,
998
+ excludeServers
999
+ });
1000
+ }
1001
+ function mergeOpencodeMcp(cwd, runtimeStatus) {
1004
1002
  const worktree = detectWorktree(cwd);
1005
1003
  let merged = {};
1006
1004
  merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
@@ -1039,26 +1037,19 @@ function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
1039
1037
  merged[name] = { ...base, enabled: status === "connected" };
1040
1038
  }
1041
1039
  }
1042
- const allEnabledServerNames = [];
1040
+ const enabledServerNames = [];
1043
1041
  for (const [name, spec] of Object.entries(merged)) {
1044
1042
  if (!spec || typeof spec !== "object") continue;
1045
1043
  const enabled = spec.enabled;
1046
1044
  if (enabled === false) continue;
1047
- allEnabledServerNames.push(name);
1048
- }
1049
- const servers = {};
1050
- const bridgedServerNames = [];
1051
- for (const [name, spec] of Object.entries(merged)) {
1052
- if (!spec || typeof spec !== "object") continue;
1053
- if (excludeServers?.has(name)) continue;
1054
- const translated = translateServer(name, spec);
1055
- if (translated) {
1056
- servers[name] = translated;
1057
- bridgedServerNames.push(name);
1058
- }
1045
+ enabledServerNames.push(name);
1059
1046
  }
1060
1047
  const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
1061
1048
  const hash = crypto.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
1049
+ return { servers: merged, enabledServerNames, hash };
1050
+ }
1051
+ function finishBridge(input) {
1052
+ const { servers, bridgedServerNames, allEnabledServerNames, hash, excludeServers } = input;
1062
1053
  if (Object.keys(servers).length === 0) {
1063
1054
  const allEnabledServersExcluded = excludeServers && allEnabledServerNames.length > 0 && allEnabledServerNames.every((name) => excludeServers.has(name));
1064
1055
  if (!allEnabledServersExcluded) return null;
@@ -1109,6 +1100,9 @@ var opencodeProjectDirectory;
1109
1100
  function setOpencodeProjectDirectory(dir) {
1110
1101
  opencodeProjectDirectory = dir;
1111
1102
  }
1103
+ function getOpencodeProjectDirectory() {
1104
+ return opencodeProjectDirectory;
1105
+ }
1112
1106
  function isUsableDirectory(d) {
1113
1107
  return typeof d === "string" && d.length > 1 && d !== "/";
1114
1108
  }
@@ -1243,6 +1237,8 @@ function cliSupportsThinking(v) {
1243
1237
  var activeProcesses = /* @__PURE__ */ new Map();
1244
1238
  var claudeSessions = /* @__PURE__ */ new Map();
1245
1239
  var MAX_ACTIVE_PROCESSES = 16;
1240
+ var PROCESS_EXIT_TIMEOUT_MS = 1500;
1241
+ var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
1246
1242
  function envFlagEnabled(value) {
1247
1243
  if (value === void 0) return false;
1248
1244
  const normalized = value.trim().toLowerCase();
@@ -1289,13 +1285,54 @@ function getActiveProcess(key) {
1289
1285
  function setActiveProcess(key, ap) {
1290
1286
  activeProcesses.set(key, ap);
1291
1287
  }
1292
- function deleteActiveProcess(key) {
1288
+ function detachActiveProcess(key) {
1293
1289
  const ap = activeProcesses.get(key);
1294
- if (ap) {
1295
- void ap.proxyServer?.close();
1296
- ap.proc.kill();
1297
- activeProcesses.delete(key);
1298
- }
1290
+ if (!ap) return void 0;
1291
+ activeProcesses.delete(key);
1292
+ void ap.proxyServer?.close();
1293
+ return ap;
1294
+ }
1295
+ function deleteActiveProcess(key) {
1296
+ const ap = detachActiveProcess(key);
1297
+ ap?.proc.kill();
1298
+ }
1299
+ function hasProcessExited(proc) {
1300
+ return proc.exitCode !== null || proc.signalCode !== null;
1301
+ }
1302
+ function waitForProcessExit(proc, timeoutMs) {
1303
+ if (hasProcessExited(proc)) return Promise.resolve(true);
1304
+ return new Promise((resolve4) => {
1305
+ const onExit = () => {
1306
+ clearTimeout(timer);
1307
+ resolve4(true);
1308
+ };
1309
+ const timer = setTimeout(() => {
1310
+ proc.off("exit", onExit);
1311
+ resolve4(hasProcessExited(proc));
1312
+ }, timeoutMs);
1313
+ proc.once("exit", onExit);
1314
+ });
1315
+ }
1316
+ async function deleteActiveProcessAndWait(key, options = {}) {
1317
+ const ap = detachActiveProcess(key);
1318
+ if (!ap || hasProcessExited(ap.proc)) return true;
1319
+ const gracefulExit = waitForProcessExit(
1320
+ ap.proc,
1321
+ options.exitTimeoutMs ?? PROCESS_EXIT_TIMEOUT_MS
1322
+ );
1323
+ ap.proc.kill();
1324
+ if (await gracefulExit) return true;
1325
+ const forcedExit = waitForProcessExit(
1326
+ ap.proc,
1327
+ options.forceExitTimeoutMs ?? PROCESS_FORCE_EXIT_TIMEOUT_MS
1328
+ );
1329
+ ap.proc.kill("SIGKILL");
1330
+ if (await forcedExit) return true;
1331
+ log.warn("claude process did not exit; starting a fresh session", {
1332
+ sessionKey: key
1333
+ });
1334
+ deleteClaudeSessionId(key);
1335
+ return false;
1299
1336
  }
1300
1337
  function getClaudeSessionId(key) {
1301
1338
  return claudeSessions.get(key);
@@ -1343,8 +1380,9 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
1343
1380
  void unlink(systemPromptFile).catch(() => {
1344
1381
  });
1345
1382
  }
1346
- activeProcesses.delete(sessionKey2);
1347
- if (code !== 0 && code !== null) {
1383
+ const ownsSessionKey = activeProcesses.get(sessionKey2) === ap;
1384
+ if (ownsSessionKey) activeProcesses.delete(sessionKey2);
1385
+ if (ownsSessionKey && code !== 0 && code !== null) {
1348
1386
  log.info("process exited with error, clearing session", {
1349
1387
  code,
1350
1388
  sessionKey: sessionKey2
@@ -1355,16 +1393,50 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
1355
1393
  proc.stderr?.on("data", (data) => {
1356
1394
  const stderr = data.toString();
1357
1395
  log.debug("stderr", { data: stderr.slice(0, 200) });
1358
- if (stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
1359
- log.warn("claude session ID error, clearing session", {
1360
- sessionKey: sessionKey2,
1361
- error: stderr.slice(0, 200)
1362
- });
1363
- claudeSessions.delete(sessionKey2);
1396
+ if (stderr.includes("No conversation found") || stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
1397
+ if (activeProcesses.get(sessionKey2) === ap) {
1398
+ log.warn("claude session ID error, clearing session", {
1399
+ sessionKey: sessionKey2,
1400
+ error: stderr.slice(0, 200)
1401
+ });
1402
+ claudeSessions.delete(sessionKey2);
1403
+ } else {
1404
+ log.debug("ignoring session ID error from stale claude process", {
1405
+ sessionKey: sessionKey2
1406
+ });
1407
+ }
1364
1408
  }
1365
1409
  });
1366
1410
  return ap;
1367
1411
  }
1412
+ function appendResumeIfNeeded(sessionKey2, cliArgs) {
1413
+ if (cliArgs.includes("--resume") || cliArgs.includes("--session-id")) {
1414
+ return cliArgs;
1415
+ }
1416
+ const sid = claudeSessions.get(sessionKey2);
1417
+ if (!sid) return cliArgs;
1418
+ return [...cliArgs, "--resume", sid];
1419
+ }
1420
+ function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropicApiKey) {
1421
+ const old = activeProcesses.get(sessionKey2);
1422
+ if (!old) return void 0;
1423
+ activeProcesses.delete(sessionKey2);
1424
+ old.proc.removeAllListeners("exit");
1425
+ try {
1426
+ old.proc.kill();
1427
+ } catch {
1428
+ }
1429
+ return spawnClaudeProcess(
1430
+ cliPath,
1431
+ appendResumeIfNeeded(sessionKey2, cliArgs),
1432
+ cwd,
1433
+ sessionKey2,
1434
+ old.proxyServer,
1435
+ old.mcpHash,
1436
+ old.systemPromptFile,
1437
+ ignoreAnthropicApiKey
1438
+ );
1439
+ }
1368
1440
  function buildCliArgs(opts) {
1369
1441
  const {
1370
1442
  sessionKey: sessionKey2,
@@ -1398,7 +1470,7 @@ function buildCliArgs(opts) {
1398
1470
  if (includeSessionId) {
1399
1471
  const sessionId = claudeSessions.get(sessionKey2);
1400
1472
  if (sessionId && !activeProcesses.has(sessionKey2)) {
1401
- args.push("--session-id", sessionId);
1473
+ args.push("--resume", sessionId);
1402
1474
  }
1403
1475
  }
1404
1476
  if (mcpConfig) {
@@ -1990,10 +2062,61 @@ import * as fs4 from "fs";
1990
2062
  import * as path4 from "path";
1991
2063
  import * as crypto2 from "crypto";
1992
2064
  import { EventEmitter as EventEmitter3 } from "events";
2065
+ var SERVER_CLOSED_MESSAGE = "proxy MCP server closed";
2066
+ function isExpectedCleanupError(message) {
2067
+ return message.includes("timed out after") && message.includes("waiting for opencode to resolve") || message.includes("rejecting as orphaned") || message.includes("was orphaned by a new user turn") || message.includes("stream was aborted") || message.includes(SERVER_CLOSED_MESSAGE);
2068
+ }
1993
2069
  var PROTOCOL_VERSION = "2024-11-05";
1994
2070
  var SERVER_NAME = "opencode_proxy";
1995
2071
  var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
1996
- var PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
2072
+ var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
2073
+ var PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS = {
2074
+ task: 60 * 60 * 1e3
2075
+ // 60 min
2076
+ };
2077
+ var MAX_PROXY_TIMEOUT_MS = 2 ** 31 - 1;
2078
+ function resolveProxyCallTimeoutMs(toolName, input, overrides) {
2079
+ const key = toolName.toLowerCase();
2080
+ let ms = PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS[key] ?? PROXY_DEFAULT_TIMEOUT_MS;
2081
+ if (overrides) {
2082
+ const ov = lookupCaseInsensitive(overrides, key);
2083
+ if (typeof ov === "number" && ov > 0) ms = ov;
2084
+ }
2085
+ if (key === "bash") {
2086
+ const requested = input?.timeout;
2087
+ if (typeof requested === "number" && requested > ms) ms = requested;
2088
+ }
2089
+ return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
2090
+ }
2091
+ function lookupCaseInsensitive(map, key) {
2092
+ if (Object.prototype.hasOwnProperty.call(map, key)) return map[key];
2093
+ for (const k of Object.keys(map)) {
2094
+ if (k.toLowerCase() === key) return map[k];
2095
+ }
2096
+ return void 0;
2097
+ }
2098
+ function resolveProxyClientCeilingMs(overrides) {
2099
+ let ms = PROXY_DEFAULT_TIMEOUT_MS;
2100
+ for (const v of Object.values(PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS)) {
2101
+ if (v > ms) ms = v;
2102
+ }
2103
+ if (overrides) {
2104
+ for (const v of Object.values(overrides)) {
2105
+ if (typeof v === "number" && v > ms) ms = v;
2106
+ }
2107
+ }
2108
+ return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
2109
+ }
2110
+ function buildProxyTimeoutError(toolName, ms) {
2111
+ const key = toolName.toLowerCase();
2112
+ const base = `Proxy tool '${toolName}' timed out after ${ms}ms waiting for opencode to resolve the call`;
2113
+ if (key === "task") {
2114
+ return new Error(
2115
+ base + " (the subagent). The subagent may still be running but its result is no longer reachable in this session. Do not declare the dispatch failed, and do not 'schedule a wake-up' or defer -- that mechanism does not apply here. If the result is required, re-dispatch or verify it directly now."
2116
+ );
2117
+ }
2118
+ return new Error(base);
2119
+ }
1997
2120
  var DEFAULT_PROXY_TOOLS = [
1998
2121
  {
1999
2122
  name: "bash",
@@ -2086,7 +2209,7 @@ var DEFAULT_PROXY_TOOLS = [
2086
2209
  },
2087
2210
  {
2088
2211
  name: "task",
2089
- description: "Launch an opencode subagent to handle a complex multi-step task autonomously. Routed through opencode's task tool so subagent orchestration, permission, and lifecycle are handled by opencode. Use `subagent_type` to pick which configured subagent runs (e.g. `build`, `general`, `explore`, or any custom subagent declared in opencode.json). The call blocks until the subagent finishes; the 10-minute proxy timeout applies.",
2212
+ description: "Launch an opencode subagent to handle a complex multi-step task autonomously. Routed through opencode's task tool so subagent orchestration, permission, and lifecycle are handled by opencode. Use `subagent_type` to pick which configured subagent runs (e.g. `build`, `general`, `explore`, or any custom subagent declared in opencode.json). Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. Task calls get a 60-minute proxy deadline by default (configurable via proxyToolTimeoutMs).",
2090
2213
  inputSchema: {
2091
2214
  type: "object",
2092
2215
  properties: {
@@ -2109,13 +2232,17 @@ var DEFAULT_PROXY_TOOLS = [
2109
2232
  command: {
2110
2233
  type: "string",
2111
2234
  description: "The command that triggered this task"
2235
+ },
2236
+ background: {
2237
+ type: "boolean",
2238
+ description: "Run the task in the background when supported by opencode"
2112
2239
  }
2113
2240
  },
2114
2241
  required: ["description", "prompt", "subagent_type"]
2115
2242
  }
2116
2243
  }
2117
2244
  ];
2118
- async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
2245
+ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides) {
2119
2246
  const calls = new EventEmitter3();
2120
2247
  const pending = /* @__PURE__ */ new Map();
2121
2248
  const server2 = createServer(async (req, res) => {
@@ -2124,13 +2251,17 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
2124
2251
  res.end();
2125
2252
  return;
2126
2253
  }
2254
+ let requestId = null;
2255
+ let requestMethod = null;
2127
2256
  try {
2128
2257
  const body = await readBody(req);
2129
2258
  const request = JSON.parse(body);
2259
+ requestId = request?.id ?? null;
2260
+ requestMethod = typeof request?.method === "string" ? request.method : null;
2130
2261
  if (request?.jsonrpc !== "2.0" || typeof request.method !== "string") {
2131
2262
  writeJson(res, {
2132
2263
  jsonrpc: "2.0",
2133
- id: request?.id ?? null,
2264
+ id: requestId,
2134
2265
  error: { code: -32600, message: "Invalid request" }
2135
2266
  });
2136
2267
  return;
@@ -2142,7 +2273,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
2142
2273
  if (request.method === "initialize") {
2143
2274
  writeJson(res, {
2144
2275
  jsonrpc: "2.0",
2145
- id: request.id ?? null,
2276
+ id: requestId,
2146
2277
  result: {
2147
2278
  protocolVersion: PROTOCOL_VERSION,
2148
2279
  capabilities: { tools: {} },
@@ -2162,7 +2293,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
2162
2293
  if (request.method === "tools/list") {
2163
2294
  writeJson(res, {
2164
2295
  jsonrpc: "2.0",
2165
- id: request.id ?? null,
2296
+ id: requestId,
2166
2297
  result: {
2167
2298
  tools: tools.map((t) => ({
2168
2299
  name: t.name,
@@ -2180,10 +2311,10 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
2180
2311
  if (!tools.some((t) => t.name === toolName)) {
2181
2312
  writeJson(res, {
2182
2313
  jsonrpc: "2.0",
2183
- id: request.id ?? null,
2184
- error: {
2185
- code: -32601,
2186
- message: `Unknown proxy tool: ${toolName}`
2314
+ id: requestId,
2315
+ result: {
2316
+ content: [{ type: "text", text: `Unknown proxy tool: ${toolName}` }],
2317
+ isError: true
2187
2318
  }
2188
2319
  });
2189
2320
  return;
@@ -2205,20 +2336,21 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
2205
2336
  reject
2206
2337
  };
2207
2338
  pending.set(callId, entry);
2339
+ const deadlineMs = resolveProxyCallTimeoutMs(
2340
+ toolName,
2341
+ input,
2342
+ timeoutOverrides
2343
+ );
2208
2344
  timer = setTimeout(() => {
2209
2345
  if (!pending.has(callId)) return;
2210
2346
  pending.delete(callId);
2211
2347
  log.notice("proxy-mcp tool call timed out", {
2212
2348
  callId,
2213
2349
  toolName,
2214
- timeoutMs: PROXY_CALL_TIMEOUT_MS
2350
+ deadlineMs
2215
2351
  });
2216
- reject(
2217
- new Error(
2218
- `Proxy tool '${toolName}' timed out after ${PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
2219
- )
2220
- );
2221
- }, PROXY_CALL_TIMEOUT_MS);
2352
+ reject(buildProxyTimeoutError(toolName, deadlineMs));
2353
+ }, deadlineMs);
2222
2354
  calls.emit("call", entry);
2223
2355
  }
2224
2356
  ).finally(() => {
@@ -2228,17 +2360,17 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
2228
2360
  if (result.kind === "error") {
2229
2361
  writeJson(res, {
2230
2362
  jsonrpc: "2.0",
2231
- id: request.id ?? null,
2232
- error: {
2233
- code: -32e3,
2234
- message: result.message
2363
+ id: requestId,
2364
+ result: {
2365
+ content: [{ type: "text", text: result.message }],
2366
+ isError: true
2235
2367
  }
2236
2368
  });
2237
2369
  return;
2238
2370
  }
2239
2371
  writeJson(res, {
2240
2372
  jsonrpc: "2.0",
2241
- id: request.id ?? null,
2373
+ id: requestId,
2242
2374
  result: {
2243
2375
  content: [{ type: "text", text: result.text }],
2244
2376
  isError: result.isError === true
@@ -2248,20 +2380,38 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
2248
2380
  }
2249
2381
  writeJson(res, {
2250
2382
  jsonrpc: "2.0",
2251
- id: request.id ?? null,
2383
+ id: requestId,
2252
2384
  error: { code: -32601, message: `Unknown method: ${request.method}` }
2253
2385
  });
2254
2386
  } catch (error) {
2255
2387
  const errorMessage = error instanceof Error ? error.message : String(error);
2256
- const isExpectedCleanup = errorMessage.includes("timed out after") && errorMessage.includes("waiting for opencode to resolve") || errorMessage.includes("rejecting as orphaned") || errorMessage.includes("was orphaned by a new user turn");
2257
- const logFn = isExpectedCleanup ? log.notice : log.warn;
2388
+ const logFn = isExpectedCleanupError(errorMessage) ? log.notice : log.warn;
2258
2389
  logFn("proxy-mcp error handling request", {
2259
2390
  error: errorMessage
2260
2391
  });
2392
+ if (requestMethod === "tools/call") {
2393
+ try {
2394
+ writeJson(res, {
2395
+ jsonrpc: "2.0",
2396
+ id: requestId,
2397
+ result: {
2398
+ content: [{ type: "text", text: errorMessage }],
2399
+ isError: true
2400
+ }
2401
+ });
2402
+ } catch {
2403
+ try {
2404
+ res.statusCode = 500;
2405
+ res.end();
2406
+ } catch {
2407
+ }
2408
+ }
2409
+ return;
2410
+ }
2261
2411
  try {
2262
2412
  writeJson(res, {
2263
2413
  jsonrpc: "2.0",
2264
- id: null,
2414
+ id: requestId,
2265
2415
  error: {
2266
2416
  code: -32603,
2267
2417
  message: error instanceof Error ? error.message : "Internal error"
@@ -2306,7 +2456,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
2306
2456
  mcpServers: {
2307
2457
  [SERVER_NAME]: {
2308
2458
  type: "http",
2309
- url
2459
+ url,
2460
+ timeout: resolveProxyClientCeilingMs(timeoutOverrides)
2310
2461
  }
2311
2462
  }
2312
2463
  },
@@ -2324,7 +2475,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
2324
2475
  },
2325
2476
  async close() {
2326
2477
  for (const entry of pending.values()) {
2327
- entry.reject(new Error("proxy MCP server closed"));
2478
+ entry.reject(new Error(SERVER_CLOSED_MESSAGE));
2328
2479
  }
2329
2480
  pending.clear();
2330
2481
  await new Promise((resolve4) => {
@@ -2386,7 +2537,6 @@ import { EventEmitter as EventEmitter4 } from "events";
2386
2537
  var pendingByCallId = /* @__PURE__ */ new Map();
2387
2538
  var callIdsBySession = /* @__PURE__ */ new Map();
2388
2539
  var emitter = new EventEmitter4();
2389
- var PENDING_PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
2390
2540
  function eventName(sessionKey2) {
2391
2541
  return `pending:${sessionKey2}`;
2392
2542
  }
@@ -2409,7 +2559,7 @@ function onPendingProxyCall(sessionKey2, handler) {
2409
2559
  emitter.on(name, handler);
2410
2560
  return () => emitter.off(name, handler);
2411
2561
  }
2412
- function queuePendingProxyCall(sessionKey2, call) {
2562
+ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
2413
2563
  const previous = pendingByCallId.get(call.id);
2414
2564
  if (previous) {
2415
2565
  clearTimeout(previous.timer);
@@ -2419,23 +2569,24 @@ function queuePendingProxyCall(sessionKey2, call) {
2419
2569
  pendingByCallId.delete(call.id);
2420
2570
  indexRemove(previous.sessionKey, call.id);
2421
2571
  }
2572
+ const deadlineMs = resolveProxyCallTimeoutMs(
2573
+ call.toolName,
2574
+ call.input,
2575
+ timeoutOverrides
2576
+ );
2422
2577
  const timer = setTimeout(() => {
2423
2578
  const current = pendingByCallId.get(call.id);
2424
2579
  if (!current) return;
2425
2580
  pendingByCallId.delete(call.id);
2426
2581
  indexRemove(current.sessionKey, call.id);
2427
- current.reject(
2428
- new Error(
2429
- `Proxy tool call '${call.toolName}' timed out after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
2430
- )
2431
- );
2582
+ current.reject(buildProxyTimeoutError(call.toolName, deadlineMs));
2432
2583
  log.notice("timed out pending proxy call", {
2433
2584
  sessionKey: current.sessionKey,
2434
2585
  toolCallId: call.id,
2435
2586
  toolName: call.toolName,
2436
- timeoutMs: PENDING_PROXY_CALL_TIMEOUT_MS
2587
+ deadlineMs
2437
2588
  });
2438
- }, PENDING_PROXY_CALL_TIMEOUT_MS);
2589
+ }, deadlineMs);
2439
2590
  const pending = {
2440
2591
  sessionKey: sessionKey2,
2441
2592
  toolCallId: call.id,
@@ -2574,6 +2725,7 @@ function hasNewUserContent(prompt) {
2574
2725
  var AUTO_CONTINUE_MAX_ATTEMPTS = 8;
2575
2726
  var AUTO_CONTINUE_MAX_ELAPSED_MS = 10 * 60 * 1e3;
2576
2727
  var AUTO_CONTINUE_NO_PROGRESS_LIMIT = 2;
2728
+ var PROXY_RESULT_BOUNDARY_GRACE_MS = 250;
2577
2729
  var AUTO_CONTINUE_PROMPT = "Continue the task from where you stopped. Do not summarize; keep working until the requested task is complete, you need clarification, or you hit a real blocker.";
2578
2730
  function normalizeVisibleText(text) {
2579
2731
  return text.replace(/\s+/g, " ").trim();
@@ -2701,9 +2853,9 @@ function makeAutoContinueMessage() {
2701
2853
  }
2702
2854
  });
2703
2855
  }
2704
- function readPromptFileIfPresent(path6) {
2856
+ function readPromptFileIfPresent(path7) {
2705
2857
  try {
2706
- const content = readFileSync3(path6, "utf8").trim();
2858
+ const content = readFileSync3(path7, "utf8").trim();
2707
2859
  return content || void 0;
2708
2860
  } catch {
2709
2861
  return void 0;
@@ -2772,10 +2924,10 @@ function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true, extraSystem
2772
2924
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
2773
2925
  const content = parts.join("\n\n");
2774
2926
  if (!content) return void 0;
2775
- const path6 = join6(tmpdir2(), `opencode-cc-sys-${randomUUID3()}.md`);
2927
+ const path7 = join6(tmpdir2(), `opencode-cc-sys-${randomUUID3()}.md`);
2776
2928
  try {
2777
- writeFileSync3(path6, content, "utf8");
2778
- return path6;
2929
+ writeFileSync3(path7, content, "utf8");
2930
+ return path7;
2779
2931
  } catch (err) {
2780
2932
  log.warn("failed to write system prompt file", { error: String(err) });
2781
2933
  return void 0;
@@ -2911,9 +3063,10 @@ var ClaudeCodeLanguageModel = class {
2911
3063
  * The process lifecycle owns the server lifecycle via session-manager.
2912
3064
  */
2913
3065
  async ensureProxyServer(tools, sessionKeyForCalls) {
2914
- const srv = await createProxyMcpServer(tools);
3066
+ const timeoutOverrides = this.config.proxyToolTimeoutMs;
3067
+ const srv = await createProxyMcpServer(tools, timeoutOverrides);
2915
3068
  srv.calls.on("call", (call) => {
2916
- queuePendingProxyCall(sessionKeyForCalls, call);
3069
+ queuePendingProxyCall(sessionKeyForCalls, call, timeoutOverrides);
2917
3070
  });
2918
3071
  return srv;
2919
3072
  }
@@ -3668,22 +3821,32 @@ ${plan}
3668
3821
  let activeProcess = getActiveProcess(sk);
3669
3822
  let proc;
3670
3823
  let lineEmitter;
3824
+ let cliArgs;
3671
3825
  let proxyServer = activeProcess?.proxyServer ?? null;
3672
- if (!compactionMode && activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
3673
- const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
3674
- const previousHash = activeProcess.mcpHash ?? null;
3675
- if (previousHash !== probe.bridgedHash) {
3676
- log.info("opencode MCP config changed, respawning claude", {
3677
- sk,
3678
- previousHash,
3679
- currentHash: probe.bridgedHash
3680
- });
3681
- deleteActiveProcess(sk);
3682
- activeProcess = void 0;
3683
- proxyServer = null;
3684
- }
3685
- }
3686
3826
  const setup = async () => {
3827
+ if (!compactionMode && activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
3828
+ const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
3829
+ const previousHash = activeProcess.mcpHash ?? null;
3830
+ if (previousHash !== probe.bridgedHash) {
3831
+ if (previousPendingProxyCalls.length > 0) {
3832
+ log.info("deferring MCP hot reload until proxy calls resolve", {
3833
+ sk,
3834
+ previousHash,
3835
+ currentHash: probe.bridgedHash,
3836
+ pendingCalls: previousPendingProxyCalls.length
3837
+ });
3838
+ } else {
3839
+ log.info("opencode MCP config changed, respawning claude", {
3840
+ sk,
3841
+ previousHash,
3842
+ currentHash: probe.bridgedHash
3843
+ });
3844
+ await deleteActiveProcessAndWait(sk);
3845
+ activeProcess = void 0;
3846
+ proxyServer = null;
3847
+ }
3848
+ }
3849
+ }
3687
3850
  if (useInteractive && !compactionMode) {
3688
3851
  const mcp = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
3689
3852
  if (activeProcess) {
@@ -3744,7 +3907,6 @@ ${plan}
3744
3907
  });
3745
3908
  }
3746
3909
  } else {
3747
- let cliArgs;
3748
3910
  let spawnSystemPromptFile;
3749
3911
  let spawnProxyServer = null;
3750
3912
  let spawnMcpHash = null;
@@ -3847,6 +4009,7 @@ ${plan}
3847
4009
  let controllerClosed = false;
3848
4010
  let pendingProxyUnsubscribe = null;
3849
4011
  let resultFallbackTimer = null;
4012
+ let pendingResultCompletion = null;
3850
4013
  let hasReceivedContent = false;
3851
4014
  let visibleTextSinceContinue = "";
3852
4015
  let lastVisibleTextSinceContinue = "";
@@ -3877,6 +4040,103 @@ ${plan}
3877
4040
  closeHandler();
3878
4041
  }, delayMs);
3879
4042
  };
4043
+ const START_WATCHDOG_MS = (() => {
4044
+ const env = process.env.CLAUDE_CODE_START_WATCHDOG_MS;
4045
+ const parsed = env ? Number.parseInt(env, 10) : NaN;
4046
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 9e4;
4047
+ })();
4048
+ let startWatchdog = null;
4049
+ let respawnAttempted = false;
4050
+ const clearStartWatchdog = () => {
4051
+ if (startWatchdog) {
4052
+ clearTimeout(startWatchdog);
4053
+ startWatchdog = null;
4054
+ }
4055
+ };
4056
+ const onStartWatchdogFire = () => {
4057
+ startWatchdog = null;
4058
+ if (controllerClosed || hasReceivedContent) return;
4059
+ if (respawnAttempted) {
4060
+ log.error(
4061
+ "claude process still silent after respawn; ending turn",
4062
+ { sessionKey: sk }
4063
+ );
4064
+ deleteActiveProcess(sk);
4065
+ deleteClaudeSessionId(sk);
4066
+ controllerClosed = true;
4067
+ cleanupTurn();
4068
+ controller.enqueue({
4069
+ type: "error",
4070
+ error: new Error(
4071
+ "Claude process produced no output after the envelope write (start watchdog timeout)."
4072
+ )
4073
+ });
4074
+ try {
4075
+ controller.close();
4076
+ } catch {
4077
+ }
4078
+ return;
4079
+ }
4080
+ respawnAttempted = true;
4081
+ log.warn(
4082
+ "no stdout after envelope write; respawning claude process to resume conversation",
4083
+ { sessionKey: sk, startWatchdogMs: START_WATCHDOG_MS }
4084
+ );
4085
+ lineEmitter.off("line", lineHandler);
4086
+ lineEmitter.off("close", closeHandler);
4087
+ proc.off("error", procErrorHandler);
4088
+ const newAp = respawnActiveProcess(
4089
+ sk,
4090
+ cliPath,
4091
+ cliArgs,
4092
+ cwd,
4093
+ self.config.ignoreAnthropicApiKey
4094
+ );
4095
+ if (!newAp) {
4096
+ log.error(
4097
+ "no active process to respawn (start watchdog); ending turn",
4098
+ { sessionKey: sk }
4099
+ );
4100
+ controllerClosed = true;
4101
+ cleanupTurn();
4102
+ controller.enqueue({
4103
+ type: "error",
4104
+ error: new Error(
4105
+ "No active claude process to respawn after start watchdog timeout."
4106
+ )
4107
+ });
4108
+ try {
4109
+ controller.close();
4110
+ } catch {
4111
+ }
4112
+ return;
4113
+ }
4114
+ proc = newAp.proc;
4115
+ lineEmitter = newAp.lineEmitter;
4116
+ activeProcess = newAp;
4117
+ lineEmitter.on("line", lineHandler);
4118
+ lineEmitter.on("close", closeHandler);
4119
+ proc.on("error", procErrorHandler);
4120
+ try {
4121
+ proc.stdin?.write(userMsg + "\n");
4122
+ log.debug("re-sent user message after respawn", {
4123
+ textLength: userMsg.length
4124
+ });
4125
+ } catch (err) {
4126
+ log.error("failed to re-send envelope after respawn", {
4127
+ error: err instanceof Error ? err.message : String(err)
4128
+ });
4129
+ }
4130
+ startWatchdog = setTimeout(
4131
+ onStartWatchdogFire,
4132
+ START_WATCHDOG_MS
4133
+ );
4134
+ };
4135
+ const armStartWatchdog = () => {
4136
+ clearStartWatchdog();
4137
+ if (controllerClosed) return;
4138
+ startWatchdog = setTimeout(onStartWatchdogFire, START_WATCHDOG_MS);
4139
+ };
3880
4140
  const toolCallMap = /* @__PURE__ */ new Map();
3881
4141
  const skipResultForIds = /* @__PURE__ */ new Set();
3882
4142
  const toolCallsById = /* @__PURE__ */ new Map();
@@ -3932,6 +4192,28 @@ ${plan}
3932
4192
  });
3933
4193
  finishWithToolCalls(batch);
3934
4194
  };
4195
+ const settleResultBoundary = () => {
4196
+ drainTimer = null;
4197
+ const completeResult2 = pendingResultCompletion;
4198
+ pendingResultCompletion = null;
4199
+ if (!completeResult2 || controllerClosed) return;
4200
+ if (drainBuffer.length > 0) {
4201
+ drainNow();
4202
+ return;
4203
+ }
4204
+ completeResult2();
4205
+ };
4206
+ const scheduleResultBoundary = (completeResult2, delayMs) => {
4207
+ pendingResultCompletion = completeResult2;
4208
+ if (drainTimer) clearTimeout(drainTimer);
4209
+ drainTimer = setTimeout(settleResultBoundary, delayMs);
4210
+ };
4211
+ const noteResultBoundaryCall = () => {
4212
+ if (!pendingResultCompletion) return false;
4213
+ if (drainTimer) clearTimeout(drainTimer);
4214
+ drainTimer = setTimeout(settleResultBoundary, DRAIN_QUIET_MS);
4215
+ return true;
4216
+ };
3935
4217
  const noteVisibleText = (text) => {
3936
4218
  visibleTextSinceContinue += text;
3937
4219
  lastVisibleTextSinceContinue += text;
@@ -3956,11 +4238,106 @@ ${plan}
3956
4238
  hadProxyActivitySinceContinue = false;
3957
4239
  lastStopReason = null;
3958
4240
  };
4241
+ const completeResult = (msg) => {
4242
+ if (controllerClosed) return;
4243
+ if (drainBuffer.length > 0) {
4244
+ drainNow();
4245
+ return;
4246
+ }
4247
+ const pendingSiblings = getPendingProxyCalls(sk);
4248
+ if (pendingSiblings.length > 0) {
4249
+ log.info("leaving parallel proxy calls pending at result boundary", {
4250
+ sessionKey: sk,
4251
+ count: pendingSiblings.length
4252
+ });
4253
+ }
4254
+ const autoDecision = shouldAutoContinueIncompleteTurn(
4255
+ autoContinueState,
4256
+ {
4257
+ text: visibleTextSinceContinue,
4258
+ lastVisibleText: lastVisibleTextSinceContinue,
4259
+ hadReasoning: hadReasoningSinceContinue,
4260
+ hadToolActivity: hadToolActivitySinceContinue,
4261
+ hadProxyActivity: hadProxyActivitySinceContinue,
4262
+ isError: msg.is_error,
4263
+ stopReason: lastStopReason
4264
+ }
4265
+ );
4266
+ if (autoDecision.continue) {
4267
+ const signature = continuationSignature({
4268
+ text: visibleTextSinceContinue,
4269
+ lastVisibleText: lastVisibleTextSinceContinue,
4270
+ hadReasoning: hadReasoningSinceContinue,
4271
+ hadToolActivity: hadToolActivitySinceContinue,
4272
+ hadProxyActivity: hadProxyActivitySinceContinue,
4273
+ isError: msg.is_error
4274
+ });
4275
+ autoContinueState.noProgressCount = signature === autoContinueState.lastSignature ? autoContinueState.noProgressCount + 1 : 0;
4276
+ autoContinueState.lastSignature = signature;
4277
+ autoContinueState.attempts++;
4278
+ log.notice("auto-continuing incomplete claude result", {
4279
+ sessionKey: sk,
4280
+ reason: autoDecision.reason,
4281
+ attempts: autoContinueState.attempts,
4282
+ textLength: visibleTextSinceContinue.length,
4283
+ lastTextLength: lastVisibleTextSinceContinue.length,
4284
+ hadReasoning: hadReasoningSinceContinue,
4285
+ hadToolActivity: hadToolActivitySinceContinue,
4286
+ hadProxyActivity: hadProxyActivitySinceContinue
4287
+ });
4288
+ turnCompleted = false;
4289
+ resetAutoContinueWindow();
4290
+ proc.stdin?.write(makeAutoContinueMessage() + "\n");
4291
+ return;
4292
+ }
4293
+ log.notice("auto-continuation stopped", {
4294
+ sessionKey: sk,
4295
+ reason: autoDecision.reason,
4296
+ stopReason: lastStopReason,
4297
+ attempts: autoContinueState.attempts,
4298
+ textLength: visibleTextSinceContinue.length,
4299
+ lastTextLength: lastVisibleTextSinceContinue.length,
4300
+ hadReasoning: hadReasoningSinceContinue,
4301
+ hadToolActivity: hadToolActivitySinceContinue,
4302
+ hadProxyActivity: hadProxyActivitySinceContinue
4303
+ });
4304
+ for (const [idx, reasoningId] of reasoningIds) {
4305
+ if (reasoningStarted.get(idx)) {
4306
+ controller.enqueue({
4307
+ type: "reasoning-end",
4308
+ id: reasoningId
4309
+ });
4310
+ }
4311
+ }
4312
+ controller.enqueue({
4313
+ type: "finish",
4314
+ finishReason: toFinishReason("stop"),
4315
+ usage: toUsage(msg.usage),
4316
+ providerMetadata: {
4317
+ "claude-code": {
4318
+ ...resultMeta,
4319
+ ...compactionMode ? { compactionModel: effectiveModelId } : {}
4320
+ },
4321
+ ...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
4322
+ anthropic: {
4323
+ cacheCreationInputTokens: msg.usage.cache_creation_input_tokens
4324
+ }
4325
+ } : {}
4326
+ }
4327
+ });
4328
+ controllerClosed = true;
4329
+ cleanupTurn();
4330
+ try {
4331
+ controller.close();
4332
+ } catch {
4333
+ }
4334
+ };
3959
4335
  let gotPartialEvents = false;
3960
4336
  const lineHandler = (line) => {
3961
4337
  if (!line.trim()) return;
3962
4338
  if (controllerClosed) return;
3963
4339
  startResultFallback();
4340
+ clearStartWatchdog();
3964
4341
  try {
3965
4342
  const outer = JSON.parse(line);
3966
4343
  const msg = outer.type === "stream_event" && outer.event ? { ...outer.event, session_id: outer.session_id } : outer;
@@ -4150,6 +4527,7 @@ ${plan}
4150
4527
  });
4151
4528
  endTextBlock();
4152
4529
  } else if (tc.name.startsWith(PROXY_TOOL_PREFIX)) {
4530
+ noteProxyActivity();
4153
4531
  log.debug("ignoring proxy tool_use block; broker handles it", {
4154
4532
  name: tc.name,
4155
4533
  id: tc.id
@@ -4316,6 +4694,7 @@ ${plan}
4316
4694
  });
4317
4695
  endTextBlock();
4318
4696
  } else if (block.name.startsWith(PROXY_TOOL_PREFIX)) {
4697
+ noteProxyActivity();
4319
4698
  log.debug("ignoring proxy tool_use from assistant message", {
4320
4699
  name: block.name,
4321
4700
  id: block.id
@@ -4466,113 +4845,36 @@ ${plan}
4466
4845
  });
4467
4846
  turnCompleted = true;
4468
4847
  endTextBlock();
4469
- if (drainBuffer.length > 0) {
4848
+ const shouldDeferResult = !msg.is_error && !autoContinueState.aborted && !autoContinueState.sawAskUserQuestion;
4849
+ if (drainBuffer.length > 0 && shouldDeferResult) {
4470
4850
  log.info(
4471
- "draining pending proxy calls at turn-result boundary",
4851
+ "waiting for parallel proxy calls at turn-result boundary",
4472
4852
  {
4473
4853
  sessionKey: sk,
4474
4854
  count: drainBuffer.length
4475
4855
  }
4476
4856
  );
4477
- drainNow();
4857
+ scheduleResultBoundary(
4858
+ () => completeResult(msg),
4859
+ DRAIN_QUIET_MS
4860
+ );
4478
4861
  return;
4479
4862
  }
4480
- const orphanPending = getPendingProxyCalls(sk);
4481
- if (orphanPending.length > 0) {
4482
- log.warn(
4483
- "rejecting orphan pending proxy calls at turn-result boundary",
4863
+ if (drainBuffer.length === 0 && hadProxyActivitySinceContinue && shouldDeferResult) {
4864
+ log.info(
4865
+ "waiting for delayed proxy call at turn-result boundary",
4484
4866
  {
4485
4867
  sessionKey: sk,
4486
- count: orphanPending.length
4868
+ graceMs: PROXY_RESULT_BOUNDARY_GRACE_MS
4487
4869
  }
4488
4870
  );
4489
- rejectAllPendingProxyCallsForSession(
4490
- sk,
4491
- new Error(
4492
- "Claude CLI emitted result with pending proxy calls not in drain buffer"
4493
- )
4871
+ scheduleResultBoundary(
4872
+ () => completeResult(msg),
4873
+ PROXY_RESULT_BOUNDARY_GRACE_MS
4494
4874
  );
4495
- }
4496
- const autoDecision = shouldAutoContinueIncompleteTurn(
4497
- autoContinueState,
4498
- {
4499
- text: visibleTextSinceContinue,
4500
- lastVisibleText: lastVisibleTextSinceContinue,
4501
- hadReasoning: hadReasoningSinceContinue,
4502
- hadToolActivity: hadToolActivitySinceContinue,
4503
- hadProxyActivity: hadProxyActivitySinceContinue,
4504
- isError: msg.is_error,
4505
- stopReason: lastStopReason
4506
- }
4507
- );
4508
- if (autoDecision.continue) {
4509
- const signature = continuationSignature({
4510
- text: visibleTextSinceContinue,
4511
- lastVisibleText: lastVisibleTextSinceContinue,
4512
- hadReasoning: hadReasoningSinceContinue,
4513
- hadToolActivity: hadToolActivitySinceContinue,
4514
- hadProxyActivity: hadProxyActivitySinceContinue,
4515
- isError: msg.is_error
4516
- });
4517
- autoContinueState.noProgressCount = signature === autoContinueState.lastSignature ? autoContinueState.noProgressCount + 1 : 0;
4518
- autoContinueState.lastSignature = signature;
4519
- autoContinueState.attempts++;
4520
- log.notice("auto-continuing incomplete claude result", {
4521
- sessionKey: sk,
4522
- reason: autoDecision.reason,
4523
- attempts: autoContinueState.attempts,
4524
- textLength: visibleTextSinceContinue.length,
4525
- lastTextLength: lastVisibleTextSinceContinue.length,
4526
- hadReasoning: hadReasoningSinceContinue,
4527
- hadToolActivity: hadToolActivitySinceContinue,
4528
- hadProxyActivity: hadProxyActivitySinceContinue
4529
- });
4530
- turnCompleted = false;
4531
- resetAutoContinueWindow();
4532
- proc.stdin?.write(makeAutoContinueMessage() + "\n");
4533
4875
  return;
4534
4876
  }
4535
- log.notice("auto-continuation stopped", {
4536
- sessionKey: sk,
4537
- reason: autoDecision.reason,
4538
- stopReason: lastStopReason,
4539
- attempts: autoContinueState.attempts,
4540
- textLength: visibleTextSinceContinue.length,
4541
- lastTextLength: lastVisibleTextSinceContinue.length,
4542
- hadReasoning: hadReasoningSinceContinue,
4543
- hadToolActivity: hadToolActivitySinceContinue,
4544
- hadProxyActivity: hadProxyActivitySinceContinue
4545
- });
4546
- for (const [idx, reasoningId] of reasoningIds) {
4547
- if (reasoningStarted.get(idx)) {
4548
- controller.enqueue({
4549
- type: "reasoning-end",
4550
- id: reasoningId
4551
- });
4552
- }
4553
- }
4554
- controller.enqueue({
4555
- type: "finish",
4556
- finishReason: toFinishReason("stop"),
4557
- usage: toUsage(msg.usage),
4558
- providerMetadata: {
4559
- "claude-code": {
4560
- ...resultMeta,
4561
- ...compactionMode ? { compactionModel: effectiveModelId } : {}
4562
- },
4563
- ...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
4564
- anthropic: {
4565
- cacheCreationInputTokens: msg.usage.cache_creation_input_tokens
4566
- }
4567
- } : {}
4568
- }
4569
- });
4570
- controllerClosed = true;
4571
- cleanupTurn();
4572
- try {
4573
- controller.close();
4574
- } catch {
4575
- }
4877
+ completeResult(msg);
4576
4878
  }
4577
4879
  } catch (e) {
4578
4880
  log.debug("failed to parse line", {
@@ -4616,6 +4918,8 @@ ${plan}
4616
4918
  if (cleanedUp) return;
4617
4919
  cleanedUp = true;
4618
4920
  clearFallbackTimer();
4921
+ pendingResultCompletion = null;
4922
+ clearStartWatchdog();
4619
4923
  if (drainTimer) {
4620
4924
  clearTimeout(drainTimer);
4621
4925
  drainTimer = null;
@@ -4676,6 +4980,7 @@ ${plan}
4676
4980
  noteProxyActivity();
4677
4981
  noteToolActivity();
4678
4982
  drainBuffer.push(call);
4983
+ if (noteResultBoundaryCall()) return;
4679
4984
  if (drainTimer) clearTimeout(drainTimer);
4680
4985
  drainTimer = setTimeout(drainNow, DRAIN_QUIET_MS);
4681
4986
  });
@@ -4689,6 +4994,15 @@ ${plan}
4689
4994
  "abort signal received before content, closing stream immediately",
4690
4995
  { cwd }
4691
4996
  );
4997
+ if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
4998
+ rejectAllPendingProxyCallsForSession(
4999
+ sk,
5000
+ new Error(
5001
+ "Provider stream was aborted before pending proxy calls were emitted"
5002
+ )
5003
+ );
5004
+ drainBuffer.length = 0;
5005
+ }
4692
5006
  controllerClosed = true;
4693
5007
  cleanupTurn();
4694
5008
  try {
@@ -4714,20 +5028,14 @@ ${plan}
4714
5028
  });
4715
5029
  resolvePendingProxyCallById(call.toolCallId, result);
4716
5030
  } else {
4717
- log.notice(
4718
- "pending proxy call had no matching tool-result; rejecting as orphan",
5031
+ log.info(
5032
+ "leaving unmatched parallel proxy call pending",
4719
5033
  {
4720
5034
  sessionKey: sk,
4721
5035
  toolCallId: call.toolCallId,
4722
5036
  toolName: call.toolName
4723
5037
  }
4724
5038
  );
4725
- rejectPendingProxyCallById(
4726
- call.toolCallId,
4727
- new Error(
4728
- `Pending proxy call '${call.toolName}' (${call.toolCallId}) was not matched in tool-result turn; rejecting as orphaned`
4729
- )
4730
- );
4731
5039
  }
4732
5040
  }
4733
5041
  return;
@@ -4744,6 +5052,7 @@ ${plan}
4744
5052
  }
4745
5053
  proc.stdin?.write(userMsg + "\n");
4746
5054
  log.debug("sent user message", { textLength: userMsg.length });
5055
+ armStartWatchdog();
4747
5056
  };
4748
5057
  void setup().catch((err) => {
4749
5058
  log.error("failed to set up doStream", {
@@ -5227,6 +5536,107 @@ function cleanupOne(cacheRoot, ourDir) {
5227
5536
  }
5228
5537
  }
5229
5538
 
5539
+ // src/startup-diagnostics.ts
5540
+ import * as fs5 from "fs";
5541
+ import * as path6 from "path";
5542
+ import { fileURLToPath as fileURLToPath2 } from "url";
5543
+ var cachedPluginVersion;
5544
+ function pluginVersion() {
5545
+ if (cachedPluginVersion) return cachedPluginVersion;
5546
+ try {
5547
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
5548
+ const raw = fs5.readFileSync(path6.join(here, "..", "package.json"), "utf8");
5549
+ const version = JSON.parse(raw).version;
5550
+ cachedPluginVersion = typeof version === "string" ? version : "unknown";
5551
+ } catch {
5552
+ cachedPluginVersion = "unknown";
5553
+ }
5554
+ return cachedPluginVersion;
5555
+ }
5556
+ function pickOpencodeVersion(input) {
5557
+ if (!input || typeof input !== "object") return void 0;
5558
+ const app = input.app;
5559
+ if (app && typeof app === "object") {
5560
+ const version = app.version;
5561
+ if (typeof version === "string" && version.length > 0) return version;
5562
+ }
5563
+ const direct = input.version;
5564
+ if (typeof direct === "string" && direct.length > 0) return direct;
5565
+ return void 0;
5566
+ }
5567
+ function describeSpawnCwd(configured, live = process.cwd(), captured = getOpencodeProjectDirectory()) {
5568
+ if (typeof configured === "string" && configured.length > 0) {
5569
+ return { resolved: configured, source: "configured" };
5570
+ }
5571
+ if (isUsableDirectory(live)) return { resolved: live, source: "process" };
5572
+ if (isUsableDirectory(captured)) return { resolved: captured, source: "captured" };
5573
+ return { resolved: live, source: "unresolved" };
5574
+ }
5575
+ function stringList(value) {
5576
+ if (!Array.isArray(value)) return [];
5577
+ return value.filter((entry) => typeof entry === "string");
5578
+ }
5579
+ function firstOption(providers, key) {
5580
+ for (const entry of Object.values(providers)) {
5581
+ const value = entry?.options?.[key];
5582
+ if (value !== void 0) return value;
5583
+ }
5584
+ return void 0;
5585
+ }
5586
+ function collectStartupDiagnostics(providers, opencodeVersion) {
5587
+ const accounts = [];
5588
+ for (const entry of Object.values(providers)) {
5589
+ const account = entry?.options?.account;
5590
+ if (typeof account === "string" && account.length > 0) accounts.push(account);
5591
+ }
5592
+ const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
5593
+ let mcpServers = [];
5594
+ try {
5595
+ mcpServers = mergeOpencodeMcp(cwd.resolved).enabledServerNames;
5596
+ } catch (err) {
5597
+ log.debug("startup diagnostics could not read MCP config", {
5598
+ error: err instanceof Error ? err.message : String(err)
5599
+ });
5600
+ }
5601
+ return {
5602
+ plugin: pluginVersion(),
5603
+ opencode: opencodeVersion ?? process.env.OPENCODE_VERSION ?? "unknown",
5604
+ claudeCliPath: String(firstOption(providers, "cliPath") ?? "claude"),
5605
+ cwd,
5606
+ providers: Object.keys(providers),
5607
+ accounts,
5608
+ proxyTools: stringList(firstOption(providers, "proxyTools")),
5609
+ mcpServers,
5610
+ interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
5611
+ anthropicApiKeyInEnv: Boolean(
5612
+ process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
5613
+ )
5614
+ };
5615
+ }
5616
+ var logged = false;
5617
+ function logStartupDiagnostics(providers, opencodeVersion) {
5618
+ if (logged) return;
5619
+ logged = true;
5620
+ void (async () => {
5621
+ try {
5622
+ const { claudeCliPath, ...rest } = collectStartupDiagnostics(
5623
+ providers,
5624
+ opencodeVersion
5625
+ );
5626
+ const cli = await detectCliVersion(claudeCliPath);
5627
+ const diagnostics = {
5628
+ ...rest,
5629
+ claudeCli: { path: claudeCliPath, version: cli?.raw ?? "not detected" }
5630
+ };
5631
+ log.notice("claude-code plugin ready", { ...diagnostics });
5632
+ } catch (err) {
5633
+ log.debug("startup diagnostics failed", {
5634
+ error: err instanceof Error ? err.message : String(err)
5635
+ });
5636
+ }
5637
+ })();
5638
+ }
5639
+
5230
5640
  // src/index.ts
5231
5641
  function pickOpencodeDirectory(input) {
5232
5642
  if (!input || typeof input !== "object") return void 0;
@@ -5236,6 +5646,13 @@ function pickOpencodeDirectory(input) {
5236
5646
  return void 0;
5237
5647
  }
5238
5648
  var warnedAnthropicApiKey = false;
5649
+ var DEFAULT_PROXY_TOOL_NAMES = [
5650
+ "Bash",
5651
+ "Edit",
5652
+ "Write",
5653
+ "WebFetch",
5654
+ "Task"
5655
+ ];
5239
5656
  function warnIfAnthropicApiKey(ignore) {
5240
5657
  if (warnedAnthropicApiKey) return;
5241
5658
  if (!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_AUTH_TOKEN) return;
@@ -5262,7 +5679,7 @@ function createClaudeCode(settings = {}) {
5262
5679
  warnIfAnthropicApiKey(settings.ignoreAnthropicApiKey);
5263
5680
  const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
5264
5681
  const providerName = settings.providerID ?? settings.name ?? "claude-code";
5265
- const proxyTools = settings.proxyTools ?? ["Bash", "Edit", "Write", "WebFetch"];
5682
+ const proxyTools = settings.proxyTools ?? [...DEFAULT_PROXY_TOOL_NAMES];
5266
5683
  const createModel = (modelId) => {
5267
5684
  return new ClaudeCodeLanguageModel(modelId, {
5268
5685
  provider: providerName,
@@ -5280,6 +5697,7 @@ function createClaudeCode(settings = {}) {
5280
5697
  controlRequestToolBehaviors: settings.controlRequestToolBehaviors,
5281
5698
  controlRequestDenyMessage: settings.controlRequestDenyMessage,
5282
5699
  proxyTools,
5700
+ proxyToolTimeoutMs: settings.proxyToolTimeoutMs,
5283
5701
  webSearch: settings.webSearch,
5284
5702
  hotReloadMcp: settings.hotReloadMcp ?? true,
5285
5703
  proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true,
@@ -5374,7 +5792,7 @@ function configModelsForProvider(providerModels, providerID, modelSuffix) {
5374
5792
  async function providerConfig(existing, providerID = PROVIDER_ID2, optionDefaults = {}, displayName) {
5375
5793
  const mergedOptions = {
5376
5794
  cliPath: "claude",
5377
- proxyTools: ["Bash", "Edit", "Write", "WebFetch"],
5795
+ proxyTools: [...DEFAULT_PROXY_TOOL_NAMES],
5378
5796
  ...optionDefaults,
5379
5797
  ...cleanProviderOptions(existing?.options),
5380
5798
  providerID
@@ -5394,6 +5812,13 @@ async function providerConfig(existing, providerID = PROVIDER_ID2, optionDefault
5394
5812
  // opencode's config-path loader parses (and merges user variants).
5395
5813
  };
5396
5814
  }
5815
+ function claudeCodeProviders(providers) {
5816
+ const out = {};
5817
+ for (const [id, entry] of Object.entries(providers ?? {})) {
5818
+ if (id === PROVIDER_ID2 || id.startsWith(`${PROVIDER_ID2}-`)) out[id] = entry;
5819
+ }
5820
+ return out;
5821
+ }
5397
5822
  async function expandAccountProviders(config) {
5398
5823
  const seed = config.provider?.[PROVIDER_ID2];
5399
5824
  const accounts = resolveAccounts(seed?.options?.accounts);
@@ -5439,6 +5864,7 @@ async function expandAccountProviders(config) {
5439
5864
  }
5440
5865
  var server = async (input) => {
5441
5866
  cleanupStaleUnscopedInstall();
5867
+ const opencodeVersion = pickOpencodeVersion(input);
5442
5868
  if (input && typeof input === "object" && "client" in input) {
5443
5869
  setOpencodeClient(input.client);
5444
5870
  }
@@ -5448,12 +5874,10 @@ var server = async (input) => {
5448
5874
  config.provider ??= {};
5449
5875
  const expanded = await expandAccountProviders(config);
5450
5876
  if (expanded) {
5451
- const registered2 = Object.entries(config.provider).filter(([id]) => id === PROVIDER_ID2 || id.startsWith(`${PROVIDER_ID2}-`)).map(([id, p]) => ({
5452
- id,
5453
- name: p?.name ?? id,
5454
- cwd: p?.options?.cwd
5455
- }));
5456
- log.notice("registered claude-code providers", { providers: registered2 });
5877
+ logStartupDiagnostics(
5878
+ claudeCodeProviders(config.provider),
5879
+ opencodeVersion
5880
+ );
5457
5881
  return;
5458
5882
  }
5459
5883
  const existing = config.provider[PROVIDER_ID2];
@@ -5465,11 +5889,10 @@ var server = async (input) => {
5465
5889
  PROVIDER_ID2
5466
5890
  )
5467
5891
  };
5468
- log.notice("registered claude-code provider", {
5469
- id: PROVIDER_ID2,
5470
- name: config.provider[PROVIDER_ID2]?.name ?? PROVIDER_ID2,
5471
- cwd: config.provider[PROVIDER_ID2]?.options?.cwd
5472
- });
5892
+ logStartupDiagnostics(
5893
+ claudeCodeProviders(config.provider),
5894
+ opencodeVersion
5895
+ );
5473
5896
  },
5474
5897
  // No `event` hook: MCP config drift is detected at turn start by the
5475
5898
  // hot-reload check in `claude-code-language-model.ts`, which respawns
@@ -5514,6 +5937,7 @@ var index_default = {
5514
5937
  export {
5515
5938
  ClaudeCodeLanguageModel,
5516
5939
  bridgeOpencodeMcp,
5940
+ claudeCodeProviders,
5517
5941
  configModelsForProvider,
5518
5942
  createClaudeCode,
5519
5943
  index_default as default,