@khalilgharbaoui/opencode-claude-code-plugin 0.4.1 → 0.4.2

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
@@ -1319,52 +1319,54 @@ function writeJson(res, body) {
1319
1319
 
1320
1320
  // src/proxy-broker.ts
1321
1321
  import { EventEmitter as EventEmitter3 } from "events";
1322
- var pendingBySession = /* @__PURE__ */ new Map();
1322
+ var pendingByCallId = /* @__PURE__ */ new Map();
1323
+ var callIdsBySession = /* @__PURE__ */ new Map();
1323
1324
  var emitter = new EventEmitter3();
1324
1325
  var PENDING_PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
1325
1326
  function eventName(sessionKey2) {
1326
1327
  return `pending:${sessionKey2}`;
1327
1328
  }
1329
+ function indexAdd(sessionKey2, callId) {
1330
+ let s = callIdsBySession.get(sessionKey2);
1331
+ if (!s) {
1332
+ s = /* @__PURE__ */ new Set();
1333
+ callIdsBySession.set(sessionKey2, s);
1334
+ }
1335
+ s.add(callId);
1336
+ }
1337
+ function indexRemove(sessionKey2, callId) {
1338
+ const s = callIdsBySession.get(sessionKey2);
1339
+ if (!s) return;
1340
+ s.delete(callId);
1341
+ if (s.size === 0) callIdsBySession.delete(sessionKey2);
1342
+ }
1328
1343
  function onPendingProxyCall(sessionKey2, handler) {
1329
1344
  const name = eventName(sessionKey2);
1330
1345
  emitter.on(name, handler);
1331
1346
  return () => emitter.off(name, handler);
1332
1347
  }
1333
1348
  function queuePendingProxyCall(sessionKey2, call) {
1334
- const existing = pendingBySession.get(sessionKey2);
1335
- if (existing) {
1336
- if (Date.now() - existing.createdAt < PENDING_PROXY_CALL_TIMEOUT_MS) {
1337
- call.reject(
1338
- new Error(`Another proxy tool call is already pending for ${sessionKey2}`)
1339
- );
1340
- log.warn("rejected overlapping proxy call", {
1341
- sessionKey: sessionKey2,
1342
- existingToolCallId: existing.toolCallId,
1343
- existingToolName: existing.toolName,
1344
- toolCallId: call.id,
1345
- toolName: call.toolName
1346
- });
1347
- return existing;
1348
- }
1349
- clearTimeout(existing.timer);
1350
- existing.reject(
1351
- new Error(
1352
- `Stale proxy tool call expired after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms for ${sessionKey2}`
1353
- )
1349
+ const previous = pendingByCallId.get(call.id);
1350
+ if (previous) {
1351
+ clearTimeout(previous.timer);
1352
+ previous.reject(
1353
+ new Error(`Replaced pending proxy call ${call.id} with a fresh one`)
1354
1354
  );
1355
- pendingBySession.delete(sessionKey2);
1355
+ pendingByCallId.delete(call.id);
1356
+ indexRemove(previous.sessionKey, call.id);
1356
1357
  }
1357
1358
  const timer = setTimeout(() => {
1358
- const current = pendingBySession.get(sessionKey2);
1359
- if (!current || current.toolCallId !== call.id) return;
1360
- pendingBySession.delete(sessionKey2);
1359
+ const current = pendingByCallId.get(call.id);
1360
+ if (!current) return;
1361
+ pendingByCallId.delete(call.id);
1362
+ indexRemove(current.sessionKey, call.id);
1361
1363
  current.reject(
1362
1364
  new Error(
1363
1365
  `Proxy tool call '${call.toolName}' timed out after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
1364
1366
  )
1365
1367
  );
1366
1368
  log.warn("timed out pending proxy call", {
1367
- sessionKey: sessionKey2,
1369
+ sessionKey: current.sessionKey,
1368
1370
  toolCallId: call.id,
1369
1371
  toolName: call.toolName,
1370
1372
  timeoutMs: PENDING_PROXY_CALL_TIMEOUT_MS
@@ -1380,7 +1382,8 @@ function queuePendingProxyCall(sessionKey2, call) {
1380
1382
  resolve: call.resolve,
1381
1383
  reject: call.reject
1382
1384
  };
1383
- pendingBySession.set(sessionKey2, pending);
1385
+ pendingByCallId.set(call.id, pending);
1386
+ indexAdd(sessionKey2, call.id);
1384
1387
  emitter.emit(eventName(sessionKey2), pending);
1385
1388
  log.info("queued pending proxy call", {
1386
1389
  sessionKey: sessionKey2,
@@ -1389,22 +1392,55 @@ function queuePendingProxyCall(sessionKey2, call) {
1389
1392
  });
1390
1393
  return pending;
1391
1394
  }
1392
- function getPendingProxyCall(sessionKey2) {
1393
- return pendingBySession.get(sessionKey2);
1395
+ function getPendingProxyCalls(sessionKey2) {
1396
+ const s = callIdsBySession.get(sessionKey2);
1397
+ if (!s || s.size === 0) return [];
1398
+ const out = [];
1399
+ for (const id of s) {
1400
+ const p = pendingByCallId.get(id);
1401
+ if (p) out.push(p);
1402
+ }
1403
+ return out;
1394
1404
  }
1395
- function resolvePendingProxyCall(sessionKey2, result) {
1396
- const pending = pendingBySession.get(sessionKey2);
1405
+ function resolvePendingProxyCallById(toolCallId, result) {
1406
+ const pending = pendingByCallId.get(toolCallId);
1397
1407
  if (!pending) return false;
1398
- pendingBySession.delete(sessionKey2);
1408
+ pendingByCallId.delete(toolCallId);
1409
+ indexRemove(pending.sessionKey, toolCallId);
1399
1410
  clearTimeout(pending.timer);
1400
1411
  pending.resolve(result);
1401
1412
  log.info("resolved pending proxy call", {
1402
- sessionKey: sessionKey2,
1413
+ sessionKey: pending.sessionKey,
1403
1414
  toolCallId: pending.toolCallId,
1404
1415
  toolName: pending.toolName
1405
1416
  });
1406
1417
  return true;
1407
1418
  }
1419
+ function rejectPendingProxyCallById(toolCallId, error) {
1420
+ const pending = pendingByCallId.get(toolCallId);
1421
+ if (!pending) return false;
1422
+ pendingByCallId.delete(toolCallId);
1423
+ indexRemove(pending.sessionKey, toolCallId);
1424
+ clearTimeout(pending.timer);
1425
+ pending.reject(error);
1426
+ log.warn("rejected pending proxy call", {
1427
+ sessionKey: pending.sessionKey,
1428
+ toolCallId: pending.toolCallId,
1429
+ toolName: pending.toolName,
1430
+ error: error.message
1431
+ });
1432
+ return true;
1433
+ }
1434
+ function rejectAllPendingProxyCallsForSession(sessionKey2, error) {
1435
+ const s = callIdsBySession.get(sessionKey2);
1436
+ if (!s) return 0;
1437
+ const ids = [...s];
1438
+ let count = 0;
1439
+ for (const id of ids) {
1440
+ if (rejectPendingProxyCallById(id, error)) count++;
1441
+ }
1442
+ return count;
1443
+ }
1408
1444
 
1409
1445
  // src/claude-code-language-model.ts
1410
1446
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
@@ -2238,8 +2274,14 @@ ${plan}
2238
2274
  );
2239
2275
  const resolvedProxy = this.resolvedProxyTools();
2240
2276
  const self = this;
2241
- const pendingProxyCall = getPendingProxyCall(sk);
2242
- const pendingProxyResult = pendingProxyCall ? this.extractPendingProxyResult(options.prompt, pendingProxyCall.toolCallId) : null;
2277
+ const previousPendingProxyCalls = getPendingProxyCalls(sk);
2278
+ const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
2279
+ call,
2280
+ result: this.extractPendingProxyResult(options.prompt, call.toolCallId)
2281
+ }));
2282
+ const hasMatchedPendingResults = previousPendingProxyMatches.some(
2283
+ (m) => m.result !== null
2284
+ );
2243
2285
  const runtimeStatus = await getRuntimeMcpStatus();
2244
2286
  log.info("doStream starting", {
2245
2287
  cwd,
@@ -2369,21 +2411,27 @@ ${plan}
2369
2411
  const skipResultForIds = /* @__PURE__ */ new Set();
2370
2412
  const toolCallsById = /* @__PURE__ */ new Map();
2371
2413
  let resultMeta = {};
2372
- const finishWithToolCall = (call) => {
2414
+ const drainBuffer = [];
2415
+ let drainTimer = null;
2416
+ const DRAIN_QUIET_MS = 100;
2417
+ const finishWithToolCalls = (calls) => {
2373
2418
  if (controllerClosed) return;
2374
- controller.enqueue({
2375
- type: "tool-input-start",
2376
- id: call.toolCallId,
2377
- toolName: call.toolName
2378
- });
2379
- controller.enqueue({
2380
- type: "tool-call",
2381
- toolCallId: call.toolCallId,
2382
- toolName: call.toolName,
2383
- input: JSON.stringify(call.input),
2384
- providerExecuted: false
2385
- });
2386
- skipResultForIds.add(call.toolCallId);
2419
+ if (calls.length === 0) return;
2420
+ for (const call of calls) {
2421
+ controller.enqueue({
2422
+ type: "tool-input-start",
2423
+ id: call.toolCallId,
2424
+ toolName: call.toolName
2425
+ });
2426
+ controller.enqueue({
2427
+ type: "tool-call",
2428
+ toolCallId: call.toolCallId,
2429
+ toolName: call.toolName,
2430
+ input: JSON.stringify(call.input),
2431
+ providerExecuted: false
2432
+ });
2433
+ skipResultForIds.add(call.toolCallId);
2434
+ }
2387
2435
  controller.enqueue({
2388
2436
  type: "finish",
2389
2437
  finishReason: toFinishReason("tool-calls"),
@@ -2399,6 +2447,21 @@ ${plan}
2399
2447
  } catch {
2400
2448
  }
2401
2449
  };
2450
+ const drainNow = () => {
2451
+ if (drainTimer) {
2452
+ clearTimeout(drainTimer);
2453
+ drainTimer = null;
2454
+ }
2455
+ if (drainBuffer.length === 0) return;
2456
+ if (controllerClosed) return;
2457
+ const batch = drainBuffer.splice(0, drainBuffer.length);
2458
+ log.info("draining pending proxy calls into stream finish", {
2459
+ sessionKey: sk,
2460
+ count: batch.length,
2461
+ toolCallIds: batch.map((c) => c.toolCallId)
2462
+ });
2463
+ finishWithToolCalls(batch);
2464
+ };
2402
2465
  let gotPartialEvents = false;
2403
2466
  const lineHandler = (line) => {
2404
2467
  if (!line.trim()) return;
@@ -2831,6 +2894,15 @@ ${plan}
2831
2894
  const closeHandler = () => {
2832
2895
  log.debug("readline closed");
2833
2896
  if (controllerClosed) return;
2897
+ if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
2898
+ rejectAllPendingProxyCallsForSession(
2899
+ sk,
2900
+ new Error(
2901
+ "Claude CLI subprocess closed before pending tool calls were resolved"
2902
+ )
2903
+ );
2904
+ drainBuffer.length = 0;
2905
+ }
2834
2906
  controllerClosed = true;
2835
2907
  cleanupTurn();
2836
2908
  endTextBlock();
@@ -2852,6 +2924,10 @@ ${plan}
2852
2924
  if (cleanedUp) return;
2853
2925
  cleanedUp = true;
2854
2926
  clearFallbackTimer();
2927
+ if (drainTimer) {
2928
+ clearTimeout(drainTimer);
2929
+ drainTimer = null;
2930
+ }
2855
2931
  lineEmitter.off("line", lineHandler);
2856
2932
  lineEmitter.off("close", closeHandler);
2857
2933
  pendingProxyUnsubscribe?.();
@@ -2861,6 +2937,15 @@ ${plan}
2861
2937
  const procErrorHandler = (err) => {
2862
2938
  log.error("process error", { error: err.message });
2863
2939
  if (controllerClosed) return;
2940
+ if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
2941
+ rejectAllPendingProxyCallsForSession(
2942
+ sk,
2943
+ new Error(
2944
+ `Claude CLI subprocess error: ${err.message}`
2945
+ )
2946
+ );
2947
+ drainBuffer.length = 0;
2948
+ }
2864
2949
  controllerClosed = true;
2865
2950
  cleanupTurn();
2866
2951
  controller.enqueue({ type: "error", error: err });
@@ -2872,12 +2957,31 @@ ${plan}
2872
2957
  lineEmitter.on("line", lineHandler);
2873
2958
  lineEmitter.on("close", closeHandler);
2874
2959
  pendingProxyUnsubscribe = onPendingProxyCall(sk, (call) => {
2960
+ if (controllerClosed) {
2961
+ log.warn(
2962
+ "pending proxy call arrived after stream close; rejecting",
2963
+ {
2964
+ sessionKey: sk,
2965
+ toolCallId: call.toolCallId,
2966
+ toolName: call.toolName
2967
+ }
2968
+ );
2969
+ rejectPendingProxyCallById(
2970
+ call.toolCallId,
2971
+ new Error(
2972
+ `Pending proxy call '${call.toolName}' arrived after the stream was already closed`
2973
+ )
2974
+ );
2975
+ return;
2976
+ }
2875
2977
  log.info("received pending proxy call for session", {
2876
2978
  sessionKey: sk,
2877
2979
  toolCallId: call.toolCallId,
2878
2980
  toolName: call.toolName
2879
2981
  });
2880
- finishWithToolCall(call);
2982
+ drainBuffer.push(call);
2983
+ if (drainTimer) clearTimeout(drainTimer);
2984
+ drainTimer = setTimeout(drainNow, DRAIN_QUIET_MS);
2881
2985
  });
2882
2986
  proc.on("error", procErrorHandler);
2883
2987
  if (options.abortSignal) {
@@ -2903,21 +3007,44 @@ ${plan}
2903
3007
  startResultFallback(5e3);
2904
3008
  });
2905
3009
  }
2906
- if (pendingProxyCall && pendingProxyResult) {
2907
- log.info("resolving pending proxy call from tool result prompt", {
2908
- sessionKey: sk,
2909
- toolCallId: pendingProxyCall.toolCallId,
2910
- toolName: pendingProxyCall.toolName
2911
- });
2912
- const resolved = resolvePendingProxyCall(sk, pendingProxyResult);
2913
- if (!resolved) {
2914
- log.warn("failed to resolve pending proxy call; no pending state", {
2915
- sessionKey: sk,
2916
- toolCallId: pendingProxyCall.toolCallId
2917
- });
3010
+ if (hasMatchedPendingResults) {
3011
+ for (const { call, result } of previousPendingProxyMatches) {
3012
+ if (result) {
3013
+ log.info("resolving pending proxy call from tool result prompt", {
3014
+ sessionKey: sk,
3015
+ toolCallId: call.toolCallId,
3016
+ toolName: call.toolName
3017
+ });
3018
+ resolvePendingProxyCallById(call.toolCallId, result);
3019
+ } else {
3020
+ log.warn(
3021
+ "pending proxy call had no matching tool-result; rejecting as orphan",
3022
+ {
3023
+ sessionKey: sk,
3024
+ toolCallId: call.toolCallId,
3025
+ toolName: call.toolName
3026
+ }
3027
+ );
3028
+ rejectPendingProxyCallById(
3029
+ call.toolCallId,
3030
+ new Error(
3031
+ `Pending proxy call '${call.toolName}' (${call.toolCallId}) was not matched in tool-result turn; rejecting as orphaned`
3032
+ )
3033
+ );
3034
+ }
2918
3035
  }
2919
3036
  return;
2920
3037
  }
3038
+ if (previousPendingProxyCalls.length > 0) {
3039
+ for (const call of previousPendingProxyCalls) {
3040
+ rejectPendingProxyCallById(
3041
+ call.toolCallId,
3042
+ new Error(
3043
+ `Pending proxy call '${call.toolName}' (${call.toolCallId}) was orphaned by a new user turn; rejecting`
3044
+ )
3045
+ );
3046
+ }
3047
+ }
2921
3048
  proc.stdin?.write(userMsg + "\n");
2922
3049
  log.debug("sent user message", { textLength: userMsg.length });
2923
3050
  };