@khalilgharbaoui/opencode-claude-code-plugin 0.34.0 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/index.js +198 -70
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +2 -1
package/README.md
CHANGED
|
@@ -469,7 +469,7 @@ reaches the CLI, so there is nothing there to fall back from.
|
|
|
469
469
|
| `mcpConfig` | string \| string[] | – | Extra `--mcp-config` paths/JSON passed alongside the bridged config. |
|
|
470
470
|
| `strictMcpConfig` | boolean | `false` | Pass `--strict-mcp-config` so Claude loads **only** the configured servers and ignores `~/.claude/settings.json`. |
|
|
471
471
|
| `hotReloadMcp` | boolean | `true` | With MCP bridging on, compare the merged MCP config and runtime status at the start of each turn and respawn the `claude` process when they drifted, so a server you just enabled, disabled or finished connecting becomes visible without restarting opencode or opening a new chat. It only ever acts at a safe boundary: never during `/compact`, never on the interactive transport, never while a proxied call is still in the air, a turn is still running or a plan-mode approval is outstanding, and the Claude session id is preserved for `--resume` so the conversation continues. A log line at INFO names which servers joined and left. A server that flaps buys at most one respawn per minute per conversation (`CLAUDE_CODE_MCP_HOT_RELOAD_COOLDOWN_MS`). Set `false` to keep a cached subprocess until the chat is reset. It does not reload other provider options and does not watch the contents of files named in `mcpConfig`. |
|
|
472
|
-
| `mcpConnectWaitMs` | number | `3000` | How long the first turn of a conversation waits for MCP servers opencode reports as still connecting before planning the `claude` spawn without them. Only opencode 2 can report that state (`pending`); opencode 1's own status call blocks until every server has decided, so this budget is what makes the two majors behave alike. Set `0` to always plan with whatever the host says at that instant. A server slower than the budget is not lost either way: it is still bridged, and `hotReloadMcp` moves the conversation onto a process that has it on the next turn. |
|
|
472
|
+
| `mcpConnectWaitMs` | number | `3000` | How long the first turn of a conversation waits for MCP servers opencode reports as still connecting before planning the `claude` spawn without them. Only opencode 2 can report that state (`pending`); opencode 1's own status call blocks until every server has decided, so this budget is what makes the two majors behave alike. Set `0` to always plan with whatever the host says at that instant. Aborting the turn also ends the wait at once. A server slower than the budget is not lost either way: it is still bridged, and `hotReloadMcp` moves the conversation onto a process that has it on the next turn. |
|
|
473
473
|
| `proxyOpencodeMcpTools` | boolean | `false` | Route opencode's MCP-backed tools through the in-process `opencode_proxy` server instead of bridging them straight into Claude's `--mcp-config`, so each call executes once, inside opencode, with its permission prompt and its tool row. **The default changed from `true` to `false` in this release, and no behaviour changed with it:** at `true` it used to route nothing at all, because discovery read opencode's tool registry, which contains built-ins and plugin-declared tools and has never contained an MCP tool. Discovery now reads the model tool set opencode passes the provider, which is where MCP tools actually are, so the option works, and turning it on is the operator's decision rather than a silent migration of traffic that the direct bridge is handling today. Two caveats before enabling it: pair it with `strictMcpConfig: true`, because a server also registered in Claude Code's own config is reached directly and bypasses the proxy entirely; and a routed call runs in opencode with the calling agent's permissions, the same trade [`proxyOpencodeTools`](#options-reference) makes. Servers whose tools are not found stay on the direct bridge, and a warning says so, so do not treat this as an exactly-once guarantee for write-capable tools. |
|
|
474
474
|
| `proxyOpencodeTools` | string[] | `[]` | Forward explicitly named opencode tools through the proxy (for example, a plugin's `compress` or V2 Code Mode `execute`). V1 uses registry ids; V2 uses the current model tool snapshot, including its real JSON Schema and agent visibility, not the registry's empty schemas. A forwarded tool runs inside opencode with the calling agent's permissions. A name already held by a proxy def is dropped with a warning. The read-only preset refuses `execute`. See [Forwarding opencode's own tools](#forwarding-opencode-s-own-tools) and [V2 Code Mode](#v2-code-mode). |
|
|
475
475
|
| `stripContextReminders` | boolean | `false` | Remove opencode-dcp's `<dcp-system-reminder>` blocks from message text when no `compress` tool is proxied, so an order the model cannot follow stops being re-sent with every message that carries it. Inert as soon as `compress` is reachable. See [Trimming unsatisfiable context reminders](#trimming-unsatisfiable-context-reminders). |
|
|
@@ -851,7 +851,7 @@ A proxied call ends when something happens to it, not when a clock runs out. The
|
|
|
851
851
|
| What happens | What the plugin does |
|
|
852
852
|
|---|---|
|
|
853
853
|
| opencode returns the tool's result | resolves the call; the CLI gets the result and carries on |
|
|
854
|
-
| you abort the turn (Esc / Ctrl+C) | sends the CLI an `interrupt`, which answers with its own result, and rejects every call the turn had pending, whether the abort lands before content, mid-turn, or while opencode is running the tool between two stream boundaries |
|
|
854
|
+
| you abort the turn (Esc / Ctrl+C) | sends the CLI an `interrupt`, which answers with its own result, and rejects every call the turn had pending, whether the abort lands before content, mid-turn, or while opencode is running the tool between two stream boundaries. A stop that lands before the turn has asked the CLI for anything is the one exception: it interrupts nothing and releases nothing, because the calls still pending there belong to the previous step and the next message orphans them as usual |
|
|
855
855
|
| you send the next message in that chat | rejects every call the previous turn left pending as orphaned, so the CLI gets an error result and the new turn starts clean |
|
|
856
856
|
| the `claude` process closes its output or exits, mid-turn or between turns | rejects its pending calls; a mid-turn death also ends the turn as a visible error |
|
|
857
857
|
| you delete the chat in opencode, or opencode exits | kills the worker and rejects its pending calls |
|
|
@@ -1145,6 +1145,7 @@ Each chat keeps a long-lived `claude` subprocess so the model retains its native
|
|
|
1145
1145
|
- **New chat** → fresh process under the new session key.
|
|
1146
1146
|
- **Resumed chat after restart** → in-memory state is gone; a new process spawns and the conversation history is summarized and prepended.
|
|
1147
1147
|
- **Abort (Esc / Ctrl+C)** → the plugin sends the Claude CLI a stream-json `interrupt` control request, so the CLI actually stops generating and running tools instead of finishing the abandoned turn on your bill. The process stays alive for the next message in that chat, and any proxied call the aborted turn left behind is released when that message arrives (see [How a proxied call ends](#how-a-proxied-call-ends)). If a turn is somehow still running when the next one starts, it is interrupted first (5 s cap). Contributed by [@broskees](https://github.com/broskees).
|
|
1148
|
+
- **Abort during the first moment of a turn** → a turn spends a little time preparing before it asks the CLI for anything: resolving the spawn directory, probing the `claude` version, reading opencode's MCP status and tool registry. A stop pressed in that window used to be dropped, and the turn spawned, ran and billed anyway. It now ends the turn there: nothing is spawned, nothing is written, no running process is interrupted, and the reply is simply empty.
|
|
1148
1149
|
- **Idle timeout** → when `idleProcessTimeoutMs` is set, a completed headless turn arms an eviction timer (unset or `0` keeps workers until LRU eviction). Reuse cancels it, a worker found mid-turn when it fires is left alone and re-timed, and eviction preserves the session id, so the next message resumes the same conversation with `--resume`. An idle `claude --print` holds around 250 MB, which is the reason to set it if you keep many chats open.
|
|
1149
1150
|
- **Cap**: 16 active processes, LRU eviction. A process that is mid-turn is never the victim: eviction takes the oldest **idle** one, and when every process is busy it evicts nothing and warns instead, so a running answer is never truncated to make room.
|
|
1150
1151
|
- **Deleted chat** → deleting a session in opencode kills its `claude` workers at once and forgets their session ids and per-chat state; there is nothing left to resume. Other chats, and the shared fallback bucket used when no session id is known, are untouched.
|
package/dist/index.js
CHANGED
|
@@ -3504,16 +3504,18 @@ async function getRuntimeMcpStatus(options = {}) {
|
|
|
3504
3504
|
const current = await fetchRuntimeMcpStatus();
|
|
3505
3505
|
const budgetMs = options.waitForPendingMs ?? 0;
|
|
3506
3506
|
if (!current || budgetMs <= 0) return current;
|
|
3507
|
-
return waitForPendingMcpServers(current, budgetMs);
|
|
3507
|
+
return waitForPendingMcpServers(current, budgetMs, options.signal);
|
|
3508
3508
|
}
|
|
3509
|
-
async function waitForPendingMcpServers(first, budgetMs) {
|
|
3509
|
+
async function waitForPendingMcpServers(first, budgetMs, signal) {
|
|
3510
3510
|
let current = first;
|
|
3511
3511
|
const waitedFor = pendingMcpServers(current);
|
|
3512
3512
|
if (waitedFor.length === 0) return current;
|
|
3513
3513
|
const started = Date.now();
|
|
3514
3514
|
for (; ; ) {
|
|
3515
3515
|
if (Date.now() - started >= budgetMs) break;
|
|
3516
|
+
if (signal?.aborted) break;
|
|
3516
3517
|
await new Promise((resolve6) => setTimeout(resolve6, MCP_CONNECT_POLL_MS));
|
|
3518
|
+
if (signal?.aborted) break;
|
|
3517
3519
|
const next = await fetchRuntimeMcpStatus();
|
|
3518
3520
|
if (!next) break;
|
|
3519
3521
|
current = next;
|
|
@@ -3524,7 +3526,8 @@ async function waitForPendingMcpServers(first, budgetMs) {
|
|
|
3524
3526
|
waitedFor,
|
|
3525
3527
|
stillPending,
|
|
3526
3528
|
budgetMs,
|
|
3527
|
-
elapsedMs: Date.now() - started
|
|
3529
|
+
elapsedMs: Date.now() - started,
|
|
3530
|
+
aborted: signal?.aborted === true
|
|
3528
3531
|
});
|
|
3529
3532
|
return current;
|
|
3530
3533
|
}
|
|
@@ -8621,6 +8624,8 @@ function createTurnState(init) {
|
|
|
8621
8624
|
reasoningIds: /* @__PURE__ */ new Map(),
|
|
8622
8625
|
reasoningStarted: /* @__PURE__ */ new Map(),
|
|
8623
8626
|
hadThinkingTextFromStream: false,
|
|
8627
|
+
cliAskedForWork: false,
|
|
8628
|
+
streamStarted: false,
|
|
8624
8629
|
turnCompleted: false,
|
|
8625
8630
|
controllerClosed: false,
|
|
8626
8631
|
cleanedUp: false,
|
|
@@ -8670,6 +8675,59 @@ function createTurnState(init) {
|
|
|
8670
8675
|
return state;
|
|
8671
8676
|
}
|
|
8672
8677
|
|
|
8678
|
+
// src/turn-abort.ts
|
|
8679
|
+
function watchTurnAbort(signal) {
|
|
8680
|
+
let aborted = signal?.aborted === true;
|
|
8681
|
+
let listeners = [];
|
|
8682
|
+
let settle;
|
|
8683
|
+
const whenAborted = new Promise((resolve6) => {
|
|
8684
|
+
settle = resolve6;
|
|
8685
|
+
});
|
|
8686
|
+
const fire = () => {
|
|
8687
|
+
if (aborted && listeners === null) return;
|
|
8688
|
+
aborted = true;
|
|
8689
|
+
const pending = listeners ?? [];
|
|
8690
|
+
listeners = null;
|
|
8691
|
+
settle();
|
|
8692
|
+
for (const fn of pending) {
|
|
8693
|
+
try {
|
|
8694
|
+
fn();
|
|
8695
|
+
} catch (error) {
|
|
8696
|
+
log.warn("turn abort listener threw", {
|
|
8697
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8698
|
+
});
|
|
8699
|
+
}
|
|
8700
|
+
}
|
|
8701
|
+
};
|
|
8702
|
+
const onSignalAbort = () => fire();
|
|
8703
|
+
if (aborted) {
|
|
8704
|
+
listeners = null;
|
|
8705
|
+
settle();
|
|
8706
|
+
} else {
|
|
8707
|
+
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
8708
|
+
}
|
|
8709
|
+
return {
|
|
8710
|
+
get aborted() {
|
|
8711
|
+
return aborted;
|
|
8712
|
+
},
|
|
8713
|
+
get reason() {
|
|
8714
|
+
return signal?.reason;
|
|
8715
|
+
},
|
|
8716
|
+
whenAborted,
|
|
8717
|
+
onAbort(fn) {
|
|
8718
|
+
if (aborted) {
|
|
8719
|
+
fn();
|
|
8720
|
+
return;
|
|
8721
|
+
}
|
|
8722
|
+
listeners?.push(fn);
|
|
8723
|
+
},
|
|
8724
|
+
dispose() {
|
|
8725
|
+
listeners = null;
|
|
8726
|
+
signal?.removeEventListener("abort", onSignalAbort);
|
|
8727
|
+
}
|
|
8728
|
+
};
|
|
8729
|
+
}
|
|
8730
|
+
|
|
8673
8731
|
// src/stream-parser.ts
|
|
8674
8732
|
import { generateId as generateId2 } from "@ai-sdk/provider-utils";
|
|
8675
8733
|
|
|
@@ -10344,6 +10402,21 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10344
10402
|
}
|
|
10345
10403
|
async doStreamForHost(options, attempt, mode = "stream") {
|
|
10346
10404
|
const warnings = [];
|
|
10405
|
+
const turnAbort = watchTurnAbort(options.abortSignal);
|
|
10406
|
+
const abortedBeforeWork = (stage) => {
|
|
10407
|
+
log.info("abort before the turn asked claude for work; nothing spawned", {
|
|
10408
|
+
stage,
|
|
10409
|
+
reason: describeAbortReason(turnAbort.reason)
|
|
10410
|
+
});
|
|
10411
|
+
turnAbort.dispose();
|
|
10412
|
+
const aborted = new ReadableStream({
|
|
10413
|
+
start(controller) {
|
|
10414
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
10415
|
+
controller.close();
|
|
10416
|
+
}
|
|
10417
|
+
});
|
|
10418
|
+
return { stream: aborted, request: { body: { text: "" } } };
|
|
10419
|
+
};
|
|
10347
10420
|
const skipPermissions = this.config.skipPermissions !== false;
|
|
10348
10421
|
const scope = this.requestScope(options);
|
|
10349
10422
|
const affinity = this.sessionAffinity(options);
|
|
@@ -10388,6 +10461,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10388
10461
|
modelId: effectiveModelId
|
|
10389
10462
|
});
|
|
10390
10463
|
let cliPath = failover.cliPath;
|
|
10464
|
+
if (turnAbort.aborted) return abortedBeforeWork("resolving the spawn cwd and account");
|
|
10391
10465
|
const asideTransportRef = { cliPath, interactive: !!useInteractive };
|
|
10392
10466
|
const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
|
|
10393
10467
|
if (doctor) {
|
|
@@ -10639,7 +10713,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10639
10713
|
const hasMatchedPendingResults = previousPendingProxyMatches.some(
|
|
10640
10714
|
(m) => m.result !== null
|
|
10641
10715
|
);
|
|
10642
|
-
const
|
|
10716
|
+
const prologueWork = Promise.all([
|
|
10643
10717
|
compactionMode ? Promise.resolve(void 0) : getRuntimeMcpStatus({
|
|
10644
10718
|
// Give a server opencode is still starting a bounded moment to
|
|
10645
10719
|
// reach a decision before the spawn is planned without it. Only
|
|
@@ -10647,10 +10721,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10647
10721
|
// call exactly as before.
|
|
10648
10722
|
waitForPendingMs: resolveMcpConnectWaitMs(
|
|
10649
10723
|
this.config.mcpConnectWaitMs
|
|
10650
|
-
)
|
|
10724
|
+
),
|
|
10725
|
+
signal: options.abortSignal
|
|
10651
10726
|
}),
|
|
10652
10727
|
detectCliVersion(cliPath)
|
|
10653
10728
|
]);
|
|
10729
|
+
prologueWork.catch(() => void 0);
|
|
10730
|
+
const prologue = await Promise.race([
|
|
10731
|
+
prologueWork.then((value) => ({ value })),
|
|
10732
|
+
turnAbort.whenAborted.then(() => void 0)
|
|
10733
|
+
]);
|
|
10734
|
+
if (!prologue) {
|
|
10735
|
+
return abortedBeforeWork("waiting for opencode's MCP status and the CLI version");
|
|
10736
|
+
}
|
|
10737
|
+
const [runtimeStatus, cliVersion] = prologue.value;
|
|
10654
10738
|
const failoverAccounts = failoverCandidates(
|
|
10655
10739
|
this.config.failoverAccounts,
|
|
10656
10740
|
sourceAccount
|
|
@@ -10681,6 +10765,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10681
10765
|
opencodeAgent: this.getOpencodeAgent(options),
|
|
10682
10766
|
providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
|
|
10683
10767
|
});
|
|
10768
|
+
if (turnAbort.aborted) return abortedBeforeWork("planning the spawn");
|
|
10684
10769
|
const stream = new ReadableStream({
|
|
10685
10770
|
start(controller) {
|
|
10686
10771
|
if (compactionMode) {
|
|
@@ -10710,7 +10795,109 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10710
10795
|
});
|
|
10711
10796
|
state.activeProcess = getActiveProcess(sk);
|
|
10712
10797
|
state.proxyServer = state.activeProcess?.proxyServer ?? null;
|
|
10798
|
+
const discardUnattachedProxyServer = () => {
|
|
10799
|
+
const own = state.proxyServer;
|
|
10800
|
+
if (!own || own === state.activeProcess?.proxyServer) return;
|
|
10801
|
+
state.proxyServer = null;
|
|
10802
|
+
void own.close().catch((error) => {
|
|
10803
|
+
log.warn("failed to close the proxy server of an aborted turn", {
|
|
10804
|
+
sessionKey: sk,
|
|
10805
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10806
|
+
});
|
|
10807
|
+
});
|
|
10808
|
+
};
|
|
10809
|
+
const releaseAbandonedProxyCalls = (reason) => {
|
|
10810
|
+
if (state.drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
|
|
10811
|
+
rejectAllPendingProxyCallsForSession(sk, new Error(reason));
|
|
10812
|
+
state.drainBuffer.length = 0;
|
|
10813
|
+
};
|
|
10814
|
+
turnAbort.onAbort(() => {
|
|
10815
|
+
state.autoContinueState.aborted = true;
|
|
10816
|
+
if (!state.cliAskedForWork) {
|
|
10817
|
+
if (state.controllerClosed) return;
|
|
10818
|
+
state.controllerClosed = true;
|
|
10819
|
+
log.info("abort while the turn was still being prepared; nothing was sent to claude", {
|
|
10820
|
+
sessionKey: sk,
|
|
10821
|
+
// A process here was spawned by this turn or reused from an
|
|
10822
|
+
// earlier one; either way nothing was asked of it.
|
|
10823
|
+
attachedProcess: !!state.activeProcess,
|
|
10824
|
+
reason: describeAbortReason(turnAbort.reason)
|
|
10825
|
+
});
|
|
10826
|
+
discardUnattachedProxyServer();
|
|
10827
|
+
state.cleanupTurn?.();
|
|
10828
|
+
if (!state.streamStarted) {
|
|
10829
|
+
state.streamStarted = true;
|
|
10830
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
10831
|
+
}
|
|
10832
|
+
try {
|
|
10833
|
+
controller.close();
|
|
10834
|
+
} catch {
|
|
10835
|
+
}
|
|
10836
|
+
return;
|
|
10837
|
+
}
|
|
10838
|
+
if (state.turnCompleted || state.controllerClosed) {
|
|
10839
|
+
const stoppedProcess = state.activeProcess;
|
|
10840
|
+
if (stoppedProcess && stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
|
|
10841
|
+
const reason = describeAbortReason(options.abortSignal?.reason);
|
|
10842
|
+
void settleSessionRunState(affinity).then((stopped) => {
|
|
10843
|
+
const stillParked = stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0;
|
|
10844
|
+
if (stopped === "busy" || !stillParked) {
|
|
10845
|
+
log.debug("abort at a tool boundary while opencode is still running the turn; keeping pending calls", {
|
|
10846
|
+
sk,
|
|
10847
|
+
session: stopped,
|
|
10848
|
+
stillParked,
|
|
10849
|
+
reason
|
|
10850
|
+
});
|
|
10851
|
+
return;
|
|
10852
|
+
}
|
|
10853
|
+
log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason });
|
|
10854
|
+
void interruptTurn(stoppedProcess).then((idle) => {
|
|
10855
|
+
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
10856
|
+
});
|
|
10857
|
+
releaseAbandonedProxyCalls(
|
|
10858
|
+
"Provider stream was aborted while opencode was running its proxy tool calls"
|
|
10859
|
+
);
|
|
10860
|
+
});
|
|
10861
|
+
}
|
|
10862
|
+
return;
|
|
10863
|
+
}
|
|
10864
|
+
if (state.activeProcess) {
|
|
10865
|
+
void interruptTurn(state.activeProcess).then((idle) => {
|
|
10866
|
+
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
10867
|
+
});
|
|
10868
|
+
}
|
|
10869
|
+
if (!state.hasReceivedContent) {
|
|
10870
|
+
log.info(
|
|
10871
|
+
"abort signal received before content, closing stream immediately",
|
|
10872
|
+
{ cwd }
|
|
10873
|
+
);
|
|
10874
|
+
releaseAbandonedProxyCalls(
|
|
10875
|
+
"Provider stream was aborted before pending proxy calls were emitted"
|
|
10876
|
+
);
|
|
10877
|
+
state.controllerClosed = true;
|
|
10878
|
+
state.cleanupTurn?.();
|
|
10879
|
+
try {
|
|
10880
|
+
controller.close();
|
|
10881
|
+
} catch {
|
|
10882
|
+
}
|
|
10883
|
+
return;
|
|
10884
|
+
}
|
|
10885
|
+
log.info(
|
|
10886
|
+
"abort signal received mid-turn, starting grace period",
|
|
10887
|
+
{ cwd }
|
|
10888
|
+
);
|
|
10889
|
+
releaseAbandonedProxyCalls(
|
|
10890
|
+
"Provider stream was aborted while proxy tool calls were pending"
|
|
10891
|
+
);
|
|
10892
|
+
startResultFallback(state, 5e3);
|
|
10893
|
+
});
|
|
10894
|
+
const stoppedBeforeWork = () => {
|
|
10895
|
+
if (!state.controllerClosed || state.cliAskedForWork) return false;
|
|
10896
|
+
discardUnattachedProxyServer();
|
|
10897
|
+
return true;
|
|
10898
|
+
};
|
|
10713
10899
|
const setup = async () => {
|
|
10900
|
+
if (stoppedBeforeWork()) return;
|
|
10714
10901
|
if (!compactionMode && state.activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
|
|
10715
10902
|
const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
|
|
10716
10903
|
const previousHash = state.activeProcess.mcpHash ?? null;
|
|
@@ -10783,6 +10970,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10783
10970
|
enabled: self.config.bridgeOpencodeSkills === true,
|
|
10784
10971
|
...self.skillBridgeSpawn(failover)
|
|
10785
10972
|
});
|
|
10973
|
+
if (stoppedBeforeWork()) return;
|
|
10786
10974
|
const ap = spawnInteractiveProcess({
|
|
10787
10975
|
cwd,
|
|
10788
10976
|
cliPath,
|
|
@@ -10989,6 +11177,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10989
11177
|
spawnMcpHash = mcp.bridgedHash;
|
|
10990
11178
|
spawnMcpServers = mcp.allEnabledServerNames;
|
|
10991
11179
|
}
|
|
11180
|
+
if (stoppedBeforeWork()) return;
|
|
10992
11181
|
if (state.activeProcess && !compactionMode) {
|
|
10993
11182
|
state.proc = state.activeProcess.proc;
|
|
10994
11183
|
state.lineEmitter = state.activeProcess.lineEmitter;
|
|
@@ -11019,6 +11208,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
11019
11208
|
log.warn("previous turn did not stop in time; this turn may see stale output", { sk });
|
|
11020
11209
|
}
|
|
11021
11210
|
}
|
|
11211
|
+
if (stoppedBeforeWork()) return;
|
|
11212
|
+
state.streamStarted = true;
|
|
11022
11213
|
controller.enqueue({ type: "stream-start", warnings });
|
|
11023
11214
|
if (failoverNote) {
|
|
11024
11215
|
controller.enqueue({
|
|
@@ -11377,72 +11568,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
11377
11568
|
state.drainTimer = setTimeout(() => drainNow(state), DRAIN_QUIET_MS);
|
|
11378
11569
|
});
|
|
11379
11570
|
state.proc.on("error", procErrorHandler);
|
|
11380
|
-
if (options.abortSignal) {
|
|
11381
|
-
const releaseAbandonedProxyCalls = (reason) => {
|
|
11382
|
-
if (state.drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
|
|
11383
|
-
rejectAllPendingProxyCallsForSession(sk, new Error(reason));
|
|
11384
|
-
state.drainBuffer.length = 0;
|
|
11385
|
-
};
|
|
11386
|
-
options.abortSignal.addEventListener("abort", () => {
|
|
11387
|
-
state.autoContinueState.aborted = true;
|
|
11388
|
-
if (state.turnCompleted || state.controllerClosed) {
|
|
11389
|
-
const stoppedProcess = state.activeProcess;
|
|
11390
|
-
if (stoppedProcess && stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
|
|
11391
|
-
const reason = describeAbortReason(options.abortSignal?.reason);
|
|
11392
|
-
void settleSessionRunState(affinity).then((stopped) => {
|
|
11393
|
-
const stillParked = stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0;
|
|
11394
|
-
if (stopped === "busy" || !stillParked) {
|
|
11395
|
-
log.debug("abort at a tool boundary while opencode is still running the turn; keeping pending calls", {
|
|
11396
|
-
sk,
|
|
11397
|
-
session: stopped,
|
|
11398
|
-
stillParked,
|
|
11399
|
-
reason
|
|
11400
|
-
});
|
|
11401
|
-
return;
|
|
11402
|
-
}
|
|
11403
|
-
log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason });
|
|
11404
|
-
void interruptTurn(stoppedProcess).then((idle) => {
|
|
11405
|
-
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
11406
|
-
});
|
|
11407
|
-
releaseAbandonedProxyCalls(
|
|
11408
|
-
"Provider stream was aborted while opencode was running its proxy tool calls"
|
|
11409
|
-
);
|
|
11410
|
-
});
|
|
11411
|
-
}
|
|
11412
|
-
return;
|
|
11413
|
-
}
|
|
11414
|
-
if (state.activeProcess) {
|
|
11415
|
-
void interruptTurn(state.activeProcess).then((idle) => {
|
|
11416
|
-
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
11417
|
-
});
|
|
11418
|
-
}
|
|
11419
|
-
if (!state.hasReceivedContent) {
|
|
11420
|
-
log.info(
|
|
11421
|
-
"abort signal received before content, closing stream immediately",
|
|
11422
|
-
{ cwd }
|
|
11423
|
-
);
|
|
11424
|
-
releaseAbandonedProxyCalls(
|
|
11425
|
-
"Provider stream was aborted before pending proxy calls were emitted"
|
|
11426
|
-
);
|
|
11427
|
-
state.controllerClosed = true;
|
|
11428
|
-
cleanupTurn();
|
|
11429
|
-
try {
|
|
11430
|
-
controller.close();
|
|
11431
|
-
} catch {
|
|
11432
|
-
}
|
|
11433
|
-
return;
|
|
11434
|
-
}
|
|
11435
|
-
log.info(
|
|
11436
|
-
"abort signal received mid-turn, starting grace period",
|
|
11437
|
-
{ cwd }
|
|
11438
|
-
);
|
|
11439
|
-
releaseAbandonedProxyCalls(
|
|
11440
|
-
"Provider stream was aborted while proxy tool calls were pending"
|
|
11441
|
-
);
|
|
11442
|
-
startResultFallback(state, 5e3);
|
|
11443
|
-
});
|
|
11444
|
-
}
|
|
11445
11571
|
if (hasMatchedPendingResults) {
|
|
11572
|
+
state.cliAskedForWork = true;
|
|
11446
11573
|
for (const { call, result } of previousPendingProxyMatches) {
|
|
11447
11574
|
if (result) {
|
|
11448
11575
|
const channelClosed = isPendingProxyCallChannelClosed(call);
|
|
@@ -11500,6 +11627,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
11500
11627
|
);
|
|
11501
11628
|
}
|
|
11502
11629
|
}
|
|
11630
|
+
state.cliAskedForWork = true;
|
|
11503
11631
|
if (state.activeProcess) noteTurnStarted(state.activeProcess);
|
|
11504
11632
|
state.proc.stdin?.write(userMsg + "\n");
|
|
11505
11633
|
log.debug("sent user message", { textLength: userMsg.length });
|