@khalilgharbaoui/opencode-claude-code-plugin 0.32.0 → 0.33.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/README.md +15 -2
- package/dist/index.js +203 -52
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +25 -0
package/README.md
CHANGED
|
@@ -541,7 +541,7 @@ Anything you supply is merged on top of the defaults; you don't need to redeclar
|
|
|
541
541
|
|
|
542
542
|
## Interactive transport (experimental)
|
|
543
543
|
|
|
544
|
-
By default the plugin spawns `claude --print` (headless). The interactive transport instead drives the real interactive `claude` TUI under a native PTY inside opencode's Bun runtime, types your prompt into it, and streams the session transcript (`~/.claude/projects/<cwd>/<session-id>.jsonl`) back through the same pipeline the headless transport uses. It was built as insurance for the day headless usage is billed differently from interactive usage; today both draw from the same plan usage limits (see [Billing](#billing)), so it is not a way to change what a turn costs.
|
|
544
|
+
By default the plugin spawns `claude --print` (headless). The interactive transport instead drives the real interactive `claude` TUI under a native PTY inside opencode's Bun runtime, types your prompt into it, and streams the session transcript (`~/.claude/projects/<encoded-cwd>/<session-id>.jsonl`) back through the same pipeline the headless transport uses. Claude Code names that directory from the cwd's **resolved real path** with every non-alphanumeric character replaced by `-`, so a working directory reached through a symlink (on macOS `/tmp` is a symlink to `/private/tmp`) is named after the target: `/tmp/scratch` becomes `-private-tmp-scratch`. It was built as insurance for the day headless usage is billed differently from interactive usage; today both draw from the same plan usage limits (see [Billing](#billing)), so it is not a way to change what a turn costs.
|
|
545
545
|
|
|
546
546
|
```json
|
|
547
547
|
"options": { "interactive": true }
|
|
@@ -577,6 +577,7 @@ This is the part to read before turning it on. Three whole features of this plug
|
|
|
577
577
|
- **Input is text-only:** images and other non-text blocks are dropped (with a logged warning); tool results are rendered as labeled text.
|
|
578
578
|
- **Output granularity:** text arrives per transcript record, not token-by-token, so it can feel chunkier than headless streaming.
|
|
579
579
|
- **Token counts come from the transcript, one count per API call.** The session JSONL writes one record per content block (thinking, text, tool_use) and every record of a call repeats that call's final usage, so the transport counts each call once, keyed by its message id. The numbers then mean exactly what they do on the headless transport: [`turnStats`](#per-turn-stats) gets the turn's totals and opencode gets the last call's context plus the turn's output. Before this was fixed a four-tool turn reported 1,306 output tokens against a real 653, and its input and cache counts were one call's instead of the turn's. An all-zero `<synthetic>` record (how the CLI writes "Login expired" or a session limit into the transcript) is not counted as a call.
|
|
580
|
+
- **How a turn finishes:** a turn that reaches a terminal stop reason (`end_turn`, `stop_sequence`, `max_tokens`) finishes exactly as a headless turn does, so it is an ordinary completed reply and [`turnStats`](#per-turn-stats) applies to it. `max_tokens` is deliberately a completed turn rather than a failure: the call happened and billed, and the truncation is what auto-continue reads. Before this was fixed every interactive turn finished as an error instead, which also suppressed the stats footer.
|
|
580
581
|
- **Turn timeout:** a turn that produces no terminal stop within 30 minutes is reported honestly as an error result (visible truncation), not silently ended.
|
|
581
582
|
- **No idle eviction:** `idleProcessTimeoutMs` does not apply to interactive sessions.
|
|
582
583
|
- `/compact` always uses the headless transport regardless of this setting.
|
|
@@ -646,6 +647,14 @@ The task is working in the background. You will be notified automatically when i
|
|
|
646
647
|
|
|
647
648
|
Claude keeps working. When the subagent finishes, opencode prompts the same conversation with the result as a new message, so it arrives as its own turn rather than as that call's result. Measured end to end on opencode 1.18.33 with claude-haiku-4-5: the dispatch returned in 14 s while the child's 30-second command was still running, Claude ran another tool and ended its turn 18 s in, and the `<task ... state="completed">` message landed 43 s later. That notification is automatic, so the right thing after a background dispatch is to end the turn, not to wait or poll.
|
|
648
649
|
|
|
650
|
+
**opencode 2 uses different envelopes for the same thing**, so the plugin tells the model about its own host's. There a background dispatch answers in prose rather than XML:
|
|
651
|
+
|
|
652
|
+
```text
|
|
653
|
+
The subagent is working in the background (sessionID: ses_f0cb9005fffekrDPNa1Px8Jp0J). You will be notified automatically when it finishes.
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
and the completion arrives as `<subagent sessionID="…" state="completed" description="…">`. That `sessionID` is the `task_id` for the two tools below. Measured on opencode 2.0.16.
|
|
657
|
+
|
|
649
658
|
**The gate is enforced at the schema, not at the call.** On a host without the flag opencode rejects `background: true` outright with `Background subagents require OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`, which costs a whole dispatch. So the plugin reads the host's own `task` schema (the same registry fetch that supplies the agent-type list) and, when it has no `background` property, strips the field before Claude ever sees it. Nothing about a default install changes: the model is shown exactly `description`, `prompt`, `subagent_type`, `task_id`, `command`. Either way `plugin.log` says which:
|
|
650
659
|
|
|
651
660
|
```
|
|
@@ -659,7 +668,11 @@ background subagent gate {"supported":true,"registryResolved":true,"hostApi":"v1
|
|
|
659
668
|
| `mcp__opencode_proxy__task_status` | Reads the state of a background subagent by its `task_id` and returns its result if it has finished. A recovery path, not a progress poll: a healthy background task delivers its own result. A result is handed over once, so asking again reports the state without repeating the output. |
|
|
660
669
|
| `mcp__opencode_proxy__task_cancel` | Stops a background subagent. A cancelled subagent sends no completion notification. |
|
|
661
670
|
|
|
662
|
-
The `task_id` is the `id` in the `<task …>` envelope,
|
|
671
|
+
The `task_id` is the child's own opencode session id: the `id` in the `<task …>` envelope on opencode 1.x, the `sessionID` the dispatch reported on opencode 2. Both tools are answered inside the plugin rather than executed by opencode, because opencode has no tools of these names, and both refuse any session whose parent is not the conversation doing the asking. Neither can be named in `proxyTools`: they appear only when the host advertises background support, so upgrading changes nothing about what the model can do or spend on a default install.
|
|
672
|
+
|
|
673
|
+
Both work on opencode 1.x and on opencode 2. On opencode 2 they run over the session routes a plugin is actually given there (`session.context` and `session.interrupt`); opencode 2 gives a plugin no all-sessions run-state map, so "still running" is read off the child's own transcript instead. Verified live on 2.0.16: start, `task_status` answering `running`, `task_cancel` answering `Stopped.`, and a finished child collected once.
|
|
674
|
+
|
|
675
|
+
**What `/claude-code-doctor` says about it.** The report has a **Background subagents** section: whether `background` was offered to Claude and the two tools registered, which opencode major, and what decided it (the live `task` schema, a registry that did not answer, or opencode 2 offering it unconditionally), plus the background tasks this process has collected or cancelled. The gate is read while a turn plans its proxy tools, so in a fresh process the section reads `Not read yet this process`: send one message and run it again.
|
|
663
676
|
|
|
664
677
|
### Proxy endpoint security
|
|
665
678
|
|
package/dist/index.js
CHANGED
|
@@ -1633,12 +1633,12 @@ function setProxyDeadlineGuard(guard) {
|
|
|
1633
1633
|
var PROXY_DEADLINE_RECHECK_MS = 6e4;
|
|
1634
1634
|
var deadlineRecheckMs = PROXY_DEADLINE_RECHECK_MS;
|
|
1635
1635
|
function armProxyDeadline(opts) {
|
|
1636
|
-
let
|
|
1636
|
+
let cancelled2 = false;
|
|
1637
1637
|
let extended = false;
|
|
1638
1638
|
let timer = null;
|
|
1639
1639
|
const check = async () => {
|
|
1640
1640
|
timer = null;
|
|
1641
|
-
if (
|
|
1641
|
+
if (cancelled2) return;
|
|
1642
1642
|
let keep = false;
|
|
1643
1643
|
const guard = proxyDeadlineGuard;
|
|
1644
1644
|
if (guard) {
|
|
@@ -1648,7 +1648,7 @@ function armProxyDeadline(opts) {
|
|
|
1648
1648
|
keep = false;
|
|
1649
1649
|
}
|
|
1650
1650
|
}
|
|
1651
|
-
if (
|
|
1651
|
+
if (cancelled2) return;
|
|
1652
1652
|
if (keep) {
|
|
1653
1653
|
if (!extended && opts.logExtension) {
|
|
1654
1654
|
log.warn("proxy call past its deadline, but opencode is still serving it; waiting", {
|
|
@@ -1667,7 +1667,7 @@ function armProxyDeadline(opts) {
|
|
|
1667
1667
|
timer = setTimeout(() => void check(), opts.deadlineMs);
|
|
1668
1668
|
return {
|
|
1669
1669
|
cancel() {
|
|
1670
|
-
|
|
1670
|
+
cancelled2 = true;
|
|
1671
1671
|
if (timer) clearTimeout(timer);
|
|
1672
1672
|
timer = null;
|
|
1673
1673
|
}
|
|
@@ -1685,6 +1685,7 @@ function buildProxyTimeoutError(toolName, ms) {
|
|
|
1685
1685
|
}
|
|
1686
1686
|
var TASK_PROXY_NOTE = "This and task_batch are the ONLY tools that dispatch opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists: invalid types fail fast with a clear error. Calls block until the subagent finishes. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls have no proxy deadline by default: the call waits for the subagent to finish (a positive proxyToolTimeoutMs override adds a deadline).";
|
|
1687
1687
|
var TASK_BACKGROUND_NOTE = 'Fire-and-collect: set `background: true` and the call returns at once with `<task id="ses_..." state="running">` instead of the subagent\'s answer. Keep working on something that does not overlap it, then end your turn: when the subagent finishes, opencode delivers its result to this conversation on its own as a new message. Do NOT poll, sleep or loop waiting for it. The `id` in that envelope is the task_id: pass it to task_status to read a result the notification did not deliver, or to task_cancel to stop a background subagent you no longer want.';
|
|
1688
|
+
var TASK_BACKGROUND_NOTE_V2 = 'Fire-and-collect: set `background: true` and the call returns at once with `The subagent is working in the background (sessionID: ses_...)` instead of the subagent\'s answer. Keep working on something that does not overlap it, then end your turn: when the subagent finishes, opencode delivers its result to this conversation on its own, as a `<subagent sessionID="..." state="completed">` message. Do NOT poll, sleep or loop waiting for it. That sessionID is the task_id: pass it to task_status to read a result the notification did not deliver, or to task_cancel to stop a background subagent you no longer want.';
|
|
1688
1689
|
var TASK_BATCH_TOOL_NAME = "task_batch";
|
|
1689
1690
|
var TASK_BATCH_PROXY_NOTE = "Use this instead of several task calls in one response: Claude Code runs MCP tool calls one at a time, so separate task calls run serially even when emitted together, while one task_batch call fans them out as parallel opencode task calls. Each task takes the same fields as the task tool. Results come back in task order, each labelled. Like task it has no proxy deadline by default (a positive proxyToolTimeoutMs override adds one).";
|
|
1690
1691
|
var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
|
|
@@ -1785,7 +1786,7 @@ var BACKGROUND_TASK_TOOL_DEFS = [
|
|
|
1785
1786
|
properties: {
|
|
1786
1787
|
task_id: {
|
|
1787
1788
|
type: "string",
|
|
1788
|
-
description: 'The id
|
|
1789
|
+
description: 'The background subagent\'s own opencode session id: the `id` in the `<task id="..." state="running">` envelope on opencode 1.x, or the `sessionID` the background dispatch reported on opencode 2.'
|
|
1789
1790
|
}
|
|
1790
1791
|
},
|
|
1791
1792
|
required: ["task_id"]
|
|
@@ -1799,21 +1800,22 @@ var BACKGROUND_TASK_TOOL_DEFS = [
|
|
|
1799
1800
|
properties: {
|
|
1800
1801
|
task_id: {
|
|
1801
1802
|
type: "string",
|
|
1802
|
-
description: 'The id
|
|
1803
|
+
description: 'The background subagent\'s own opencode session id: the `id` in the `<task id="..." state="running">` envelope on opencode 1.x, or the `sessionID` the background dispatch reported on opencode 2.'
|
|
1803
1804
|
}
|
|
1804
1805
|
},
|
|
1805
1806
|
required: ["task_id"]
|
|
1806
1807
|
}
|
|
1807
1808
|
}
|
|
1808
1809
|
];
|
|
1809
|
-
function applyBackgroundSubagentSupport(tools, supported) {
|
|
1810
|
+
function applyBackgroundSubagentSupport(tools, supported, dialect = "v1") {
|
|
1810
1811
|
if (!tools.some((t) => t.name === "task")) return tools;
|
|
1812
|
+
const note = dialect === "v2" ? TASK_BACKGROUND_NOTE_V2 : TASK_BACKGROUND_NOTE;
|
|
1811
1813
|
const adjusted = tools.map((t) => {
|
|
1812
1814
|
if (t.name !== "task" && t.name !== TASK_BATCH_TOOL_NAME) return t;
|
|
1813
1815
|
if (supported) {
|
|
1814
1816
|
return { ...t, description: `${t.description}
|
|
1815
1817
|
|
|
1816
|
-
${
|
|
1818
|
+
${note}` };
|
|
1817
1819
|
}
|
|
1818
1820
|
return { ...t, inputSchema: stripBackgroundFromSchema(t.name, t.inputSchema) };
|
|
1819
1821
|
});
|
|
@@ -3132,8 +3134,8 @@ async function abortSession(sessionID) {
|
|
|
3132
3134
|
const client = opencodeClient;
|
|
3133
3135
|
if (!client?.session?.abort) return false;
|
|
3134
3136
|
try {
|
|
3135
|
-
await client.session.abort({ path: { id: sessionID } });
|
|
3136
|
-
return
|
|
3137
|
+
const res = await client.session.abort({ path: { id: sessionID } });
|
|
3138
|
+
return res.data !== false;
|
|
3137
3139
|
} catch (err) {
|
|
3138
3140
|
log.warn("failed to abort opencode session", {
|
|
3139
3141
|
sessionID,
|
|
@@ -3149,21 +3151,22 @@ var TASK_CANCEL_TOOL_NAME = "task_cancel";
|
|
|
3149
3151
|
var MAX_LEDGERS = 32;
|
|
3150
3152
|
var MAX_COLLECTED_PER_SESSION = 64;
|
|
3151
3153
|
var collected = /* @__PURE__ */ new Map();
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
+
var cancelled = /* @__PURE__ */ new Map();
|
|
3155
|
+
function ledgerIn(ledgers2, sessionKey2) {
|
|
3156
|
+
let ids = ledgers2.get(sessionKey2);
|
|
3154
3157
|
if (!ids) {
|
|
3155
3158
|
ids = /* @__PURE__ */ new Set();
|
|
3156
|
-
|
|
3157
|
-
while (
|
|
3158
|
-
const oldest =
|
|
3159
|
+
ledgers2.set(sessionKey2, ids);
|
|
3160
|
+
while (ledgers2.size > MAX_LEDGERS) {
|
|
3161
|
+
const oldest = ledgers2.keys().next();
|
|
3159
3162
|
if (oldest.done) break;
|
|
3160
|
-
|
|
3163
|
+
ledgers2.delete(oldest.value);
|
|
3161
3164
|
}
|
|
3162
3165
|
}
|
|
3163
3166
|
return ids;
|
|
3164
3167
|
}
|
|
3165
|
-
function
|
|
3166
|
-
const ids =
|
|
3168
|
+
function mark(ledgers2, sessionKey2, taskId) {
|
|
3169
|
+
const ids = ledgerIn(ledgers2, sessionKey2);
|
|
3167
3170
|
ids.add(taskId);
|
|
3168
3171
|
while (ids.size > MAX_COLLECTED_PER_SESSION) {
|
|
3169
3172
|
const oldest = ids.values().next();
|
|
@@ -3171,11 +3174,30 @@ function markCollected(sessionKey2, taskId) {
|
|
|
3171
3174
|
ids.delete(oldest.value);
|
|
3172
3175
|
}
|
|
3173
3176
|
}
|
|
3177
|
+
function markCollected(sessionKey2, taskId) {
|
|
3178
|
+
mark(collected, sessionKey2, taskId);
|
|
3179
|
+
}
|
|
3174
3180
|
function hasCollectedBackgroundTask(sessionKey2, taskId) {
|
|
3175
3181
|
return collected.get(sessionKey2)?.has(taskId) === true;
|
|
3176
3182
|
}
|
|
3177
3183
|
function clearBackgroundTasks(sessionKey2) {
|
|
3178
3184
|
collected.delete(sessionKey2);
|
|
3185
|
+
cancelled.delete(sessionKey2);
|
|
3186
|
+
}
|
|
3187
|
+
var lastGate;
|
|
3188
|
+
function recordBackgroundSubagentGate(gate, now = Date.now()) {
|
|
3189
|
+
lastGate = { ...gate, at: now };
|
|
3190
|
+
}
|
|
3191
|
+
function snapshotBackgroundSubagentGate() {
|
|
3192
|
+
return lastGate ? { ...lastGate } : void 0;
|
|
3193
|
+
}
|
|
3194
|
+
function snapshotBackgroundTasks() {
|
|
3195
|
+
const keys = /* @__PURE__ */ new Set([...collected.keys(), ...cancelled.keys()]);
|
|
3196
|
+
return [...keys].map((sessionKey2) => ({
|
|
3197
|
+
sessionKey: sessionKey2,
|
|
3198
|
+
collected: [...collected.get(sessionKey2) ?? []],
|
|
3199
|
+
cancelled: [...cancelled.get(sessionKey2) ?? []]
|
|
3200
|
+
}));
|
|
3179
3201
|
}
|
|
3180
3202
|
function readTaskId(input) {
|
|
3181
3203
|
const raw = input.task_id ?? input.taskId ?? input.sessionID;
|
|
@@ -3183,14 +3205,20 @@ function readTaskId(input) {
|
|
|
3183
3205
|
const id = raw.trim();
|
|
3184
3206
|
return id.length > 0 ? id : null;
|
|
3185
3207
|
}
|
|
3186
|
-
var MISSING_TASK_ID = 'task_id is required: pass the id from the `<task id="...">` envelope the background dispatch
|
|
3208
|
+
var MISSING_TASK_ID = 'task_id is required: pass the background subagent\'s own opencode session id, from the `<task id="...">` envelope on opencode 1.x or the `sessionID` the background dispatch reported on opencode 2.';
|
|
3187
3209
|
async function guardParent(taskId, callerSessionId) {
|
|
3188
3210
|
if (!callerSessionId || callerSessionId === "default") {
|
|
3189
3211
|
return `Cannot check that task_id ${taskId} belongs to this conversation (this turn has no opencode session id), so it is not inspected or cancelled.`;
|
|
3190
3212
|
}
|
|
3191
3213
|
const parent = await fetchSessionParentId(taskId);
|
|
3192
3214
|
if (parent === callerSessionId) return null;
|
|
3193
|
-
return `task_id ${taskId} is not a subagent of this conversation, so it cannot be inspected or cancelled from here. Use the id from a
|
|
3215
|
+
return `task_id ${taskId} is not a subagent of this conversation, so it cannot be inspected or cancelled from here. Use the session id from a background dispatch this conversation itself made.`;
|
|
3216
|
+
}
|
|
3217
|
+
function isBackgroundTaskRunning(runState, replies) {
|
|
3218
|
+
if (runState === "busy") return true;
|
|
3219
|
+
if (runState === "idle") return false;
|
|
3220
|
+
const reply = lastAssistantReply(replies);
|
|
3221
|
+
return reply !== void 0 && !reply.completed && reply.error === void 0;
|
|
3194
3222
|
}
|
|
3195
3223
|
function lastAssistantReply(replies) {
|
|
3196
3224
|
for (let index = replies.length - 1; index >= 0; index--) {
|
|
@@ -3212,7 +3240,7 @@ async function collectBackgroundTask(input, options) {
|
|
|
3212
3240
|
};
|
|
3213
3241
|
}
|
|
3214
3242
|
const runState = await fetchSessionRunState(taskId);
|
|
3215
|
-
if (runState
|
|
3243
|
+
if (isBackgroundTaskRunning(runState, replies)) {
|
|
3216
3244
|
return {
|
|
3217
3245
|
kind: "text",
|
|
3218
3246
|
text: `<task id="${taskId}" state="running">
|
|
@@ -3267,7 +3295,10 @@ async function cancelBackgroundTask(input, options) {
|
|
|
3267
3295
|
if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
|
|
3268
3296
|
const refused = await guardParent(taskId, options.callerSessionId);
|
|
3269
3297
|
if (refused) return { kind: "error", message: refused };
|
|
3270
|
-
const
|
|
3298
|
+
const running = isBackgroundTaskRunning(
|
|
3299
|
+
await fetchSessionRunState(taskId),
|
|
3300
|
+
await fetchSessionReplies(taskId) ?? []
|
|
3301
|
+
);
|
|
3271
3302
|
const aborted = await abortSession(taskId);
|
|
3272
3303
|
if (!aborted) {
|
|
3273
3304
|
return {
|
|
@@ -3276,15 +3307,16 @@ async function cancelBackgroundTask(input, options) {
|
|
|
3276
3307
|
};
|
|
3277
3308
|
}
|
|
3278
3309
|
collected.get(options.sessionKey)?.delete(taskId);
|
|
3310
|
+
mark(cancelled, options.sessionKey, taskId);
|
|
3279
3311
|
log.info("cancelled background subagent", {
|
|
3280
3312
|
sessionKey: options.sessionKey,
|
|
3281
3313
|
taskId,
|
|
3282
|
-
wasRunning:
|
|
3314
|
+
wasRunning: running
|
|
3283
3315
|
});
|
|
3284
3316
|
return {
|
|
3285
3317
|
kind: "text",
|
|
3286
3318
|
text: `<task id="${taskId}" state="cancelled">
|
|
3287
|
-
` + (
|
|
3319
|
+
` + (running ? "Stopped. No completion notification will arrive for it." : "It was not running when the cancel was sent, so nothing was interrupted. No completion notification will arrive for it.") + "\n</task>"
|
|
3288
3320
|
};
|
|
3289
3321
|
}
|
|
3290
3322
|
|
|
@@ -5219,6 +5251,10 @@ function formatDoctorReport(report) {
|
|
|
5219
5251
|
}
|
|
5220
5252
|
}
|
|
5221
5253
|
lines.push("");
|
|
5254
|
+
lines.push("**Background subagents**");
|
|
5255
|
+
lines.push("");
|
|
5256
|
+
lines.push(...formatBackgroundSubagents(report.backgroundSubagents));
|
|
5257
|
+
lines.push("");
|
|
5222
5258
|
lines.push("**Plan usage**");
|
|
5223
5259
|
lines.push("");
|
|
5224
5260
|
switch (report.planUsage.status) {
|
|
@@ -5251,6 +5287,59 @@ function formatDoctorReport(report) {
|
|
|
5251
5287
|
}
|
|
5252
5288
|
return lines.join("\n");
|
|
5253
5289
|
}
|
|
5290
|
+
function formatBackgroundSubagents(state) {
|
|
5291
|
+
const lines = [];
|
|
5292
|
+
const gate = state.gate;
|
|
5293
|
+
if (!gate) {
|
|
5294
|
+
lines.push(
|
|
5295
|
+
"Not read yet this process. The gate is resolved the first time a turn plans its proxy tools, so send one message and run this again."
|
|
5296
|
+
);
|
|
5297
|
+
} else {
|
|
5298
|
+
lines.push("| Field | Value |");
|
|
5299
|
+
lines.push("|---|---|");
|
|
5300
|
+
lines.push(`| \`background\` offered to Claude | ${gate.supported ? "yes" : "no"} |`);
|
|
5301
|
+
lines.push(
|
|
5302
|
+
`| \`task_status\` / \`task_cancel\` | ${gate.supported ? "registered" : "not registered"} |`
|
|
5303
|
+
);
|
|
5304
|
+
lines.push(`| opencode API | ${gate.hostApi} |`);
|
|
5305
|
+
lines.push(`| decided from | ${describeBackgroundGateSource(gate)} |`);
|
|
5306
|
+
lines.push(`| read | ${formatAge(Math.max(0, Date.now() - gate.at))} ago |`);
|
|
5307
|
+
if (!gate.supported && gate.hostApi === "v1") {
|
|
5308
|
+
lines.push("");
|
|
5309
|
+
lines.push(
|
|
5310
|
+
"opencode 1.x keeps background subagents behind a flag on its OWN process: set `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` (or the blanket `OPENCODE_EXPERIMENTAL`) in opencode's environment and restart it. The plugin never reads that variable itself; it reads whether opencode's advertised `task` schema carries a `background` property, which is how opencode publishes the flag."
|
|
5311
|
+
);
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
const active = state.ledgers.filter(
|
|
5315
|
+
(ledger) => ledger.collected.length > 0 || ledger.cancelled.length > 0
|
|
5316
|
+
);
|
|
5317
|
+
lines.push("");
|
|
5318
|
+
if (active.length === 0) {
|
|
5319
|
+
lines.push("No background task has been collected or cancelled by this process.");
|
|
5320
|
+
} else {
|
|
5321
|
+
lines.push("| session | collected | cancelled |");
|
|
5322
|
+
lines.push("|---|---|---|");
|
|
5323
|
+
for (const ledger of active) {
|
|
5324
|
+
lines.push(
|
|
5325
|
+
`| ${describeSessionKey(ledger.sessionKey).session} | ${list(ledger.collected)} | ${list(
|
|
5326
|
+
ledger.cancelled
|
|
5327
|
+
)} |`
|
|
5328
|
+
);
|
|
5329
|
+
}
|
|
5330
|
+
lines.push("");
|
|
5331
|
+
lines.push(
|
|
5332
|
+
"A collected task is one this conversation read back with `task_status`, or was told about by opencode's own completion notification; a result is handed over once."
|
|
5333
|
+
);
|
|
5334
|
+
}
|
|
5335
|
+
return lines;
|
|
5336
|
+
}
|
|
5337
|
+
function describeBackgroundGateSource(gate) {
|
|
5338
|
+
if (gate.hostApi === "v2") {
|
|
5339
|
+
return "opencode 2 offers `background` unconditionally, so the registry is not consulted";
|
|
5340
|
+
}
|
|
5341
|
+
return gate.registryResolved ? "opencode's live `task` schema" : "opencode's live tool registry did not answer, so the answer defaulted to no";
|
|
5342
|
+
}
|
|
5254
5343
|
async function checkProxyAuth(url, fetchImpl = fetch, timeoutMs = 3e3) {
|
|
5255
5344
|
let authority;
|
|
5256
5345
|
try {
|
|
@@ -5317,7 +5406,11 @@ async function gatherDoctorReport(options) {
|
|
|
5317
5406
|
proxyServers,
|
|
5318
5407
|
mcpServerErrors: snapshotMcpServerErrors(),
|
|
5319
5408
|
pluginLoadFailures: snapshotPluginLoadFailures(),
|
|
5320
|
-
planUsage
|
|
5409
|
+
planUsage,
|
|
5410
|
+
backgroundSubagents: {
|
|
5411
|
+
gate: snapshotBackgroundSubagentGate(),
|
|
5412
|
+
ledgers: snapshotBackgroundTasks()
|
|
5413
|
+
}
|
|
5321
5414
|
};
|
|
5322
5415
|
}
|
|
5323
5416
|
async function buildDoctorReport(options) {
|
|
@@ -5332,7 +5425,8 @@ async function buildDoctorReport(options) {
|
|
|
5332
5425
|
proxyServers: report.proxyServers.map((server2) => server2.auth.status),
|
|
5333
5426
|
mcpServerErrors: report.mcpServerErrors.length,
|
|
5334
5427
|
pluginLoadFailures: report.pluginLoadFailures.length,
|
|
5335
|
-
planUsage: report.planUsage.status
|
|
5428
|
+
planUsage: report.planUsage.status,
|
|
5429
|
+
backgroundSubagents: report.backgroundSubagents.gate?.supported ?? "not read"
|
|
5336
5430
|
});
|
|
5337
5431
|
return formatDoctorReport(report);
|
|
5338
5432
|
} catch (error) {
|
|
@@ -6820,7 +6914,13 @@ function resolveClaude(cmd = "claude") {
|
|
|
6820
6914
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
6821
6915
|
}
|
|
6822
6916
|
function encodeCwd(cwd) {
|
|
6823
|
-
|
|
6917
|
+
const resolved = path8.resolve(cwd);
|
|
6918
|
+
let real = resolved;
|
|
6919
|
+
try {
|
|
6920
|
+
real = fs6.realpathSync.native(resolved);
|
|
6921
|
+
} catch {
|
|
6922
|
+
}
|
|
6923
|
+
return real.replace(/[^a-zA-Z0-9]/g, "-");
|
|
6824
6924
|
}
|
|
6825
6925
|
function interactiveSpawnEnv(opts) {
|
|
6826
6926
|
return {
|
|
@@ -7341,16 +7441,18 @@ function spawnInteractiveProcess(opts) {
|
|
|
7341
7441
|
if (!startPromise) startPromise = session.start();
|
|
7342
7442
|
return startPromise;
|
|
7343
7443
|
};
|
|
7344
|
-
const emitResult = (
|
|
7444
|
+
const emitResult = (opts2) => {
|
|
7345
7445
|
lineEmitter.emit(
|
|
7346
7446
|
"line",
|
|
7347
7447
|
JSON.stringify({
|
|
7348
7448
|
type: "result",
|
|
7349
|
-
subtype,
|
|
7350
|
-
is_error: isError,
|
|
7351
|
-
|
|
7449
|
+
subtype: opts2.subtype,
|
|
7450
|
+
is_error: opts2.isError,
|
|
7451
|
+
stop_reason: opts2.stopReason,
|
|
7452
|
+
terminal_reason: opts2.terminalReason,
|
|
7453
|
+
result: opts2.result,
|
|
7352
7454
|
session_id: session.sessionId,
|
|
7353
|
-
usage: usage ?? {},
|
|
7455
|
+
usage: opts2.usage ?? {},
|
|
7354
7456
|
total_cost_usd: null,
|
|
7355
7457
|
duration_ms: 0
|
|
7356
7458
|
})
|
|
@@ -7373,20 +7475,24 @@ function spawnInteractiveProcess(opts) {
|
|
|
7373
7475
|
lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens
|
|
7374
7476
|
});
|
|
7375
7477
|
const timedOut = !stopReason;
|
|
7376
|
-
emitResult(
|
|
7377
|
-
timedOut ? "error_during_execution" :
|
|
7378
|
-
timedOut,
|
|
7379
|
-
timedOut ?
|
|
7478
|
+
emitResult({
|
|
7479
|
+
subtype: timedOut ? "error_during_execution" : "success",
|
|
7480
|
+
isError: timedOut,
|
|
7481
|
+
stopReason: timedOut ? null : stopReason,
|
|
7482
|
+
terminalReason: timedOut ? "error_during_execution" : "completed",
|
|
7483
|
+
result: timedOut ? "Interactive transport: the turn ended without a terminal stop_reason (turn timeout or claude exit). Output above may be incomplete." : void 0,
|
|
7380
7484
|
usage
|
|
7381
|
-
);
|
|
7485
|
+
});
|
|
7382
7486
|
} catch (err) {
|
|
7383
7487
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
7384
7488
|
log.error("interactive turn failed", { error: e.message });
|
|
7385
|
-
emitResult(
|
|
7386
|
-
"error_during_execution",
|
|
7387
|
-
true,
|
|
7388
|
-
|
|
7389
|
-
|
|
7489
|
+
emitResult({
|
|
7490
|
+
subtype: "error_during_execution",
|
|
7491
|
+
isError: true,
|
|
7492
|
+
stopReason: null,
|
|
7493
|
+
terminalReason: "error_during_execution",
|
|
7494
|
+
result: `Interactive transport failed: ${e.message}`
|
|
7495
|
+
});
|
|
7390
7496
|
if (errorHandlers.size > 0) {
|
|
7391
7497
|
for (const h of errorHandlers) h(e);
|
|
7392
7498
|
} else {
|
|
@@ -10547,18 +10653,25 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10547
10653
|
)
|
|
10548
10654
|
)
|
|
10549
10655
|
});
|
|
10656
|
+
const hostDialect = self.config.hostApi === "v2" ? "v2" : "v1";
|
|
10550
10657
|
backgroundSubagentsSupported = liveTaskSupportsBackground(
|
|
10551
10658
|
liveToolInfo.taskParameters,
|
|
10552
|
-
|
|
10659
|
+
hostDialect
|
|
10553
10660
|
);
|
|
10554
10661
|
enrichedProxy = applyBackgroundSubagentSupport(
|
|
10555
10662
|
enrichedProxy,
|
|
10556
|
-
backgroundSubagentsSupported
|
|
10663
|
+
backgroundSubagentsSupported,
|
|
10664
|
+
hostDialect
|
|
10557
10665
|
);
|
|
10666
|
+
recordBackgroundSubagentGate({
|
|
10667
|
+
supported: backgroundSubagentsSupported,
|
|
10668
|
+
hostApi: hostDialect,
|
|
10669
|
+
registryResolved: liveToolInfo.resolved
|
|
10670
|
+
});
|
|
10558
10671
|
log.info("background subagent gate", {
|
|
10559
10672
|
supported: backgroundSubagentsSupported,
|
|
10560
10673
|
registryResolved: liveToolInfo.resolved,
|
|
10561
|
-
hostApi:
|
|
10674
|
+
hostApi: hostDialect,
|
|
10562
10675
|
note: backgroundSubagentsSupported ? "task accepts `background`; task_status and task_cancel are registered" : "`background` stripped from the task schema; set OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true in opencode's environment to enable it"
|
|
10563
10676
|
});
|
|
10564
10677
|
}
|
|
@@ -11203,7 +11316,7 @@ import {
|
|
|
11203
11316
|
existsSync as existsSync3,
|
|
11204
11317
|
mkdirSync as mkdirSync4,
|
|
11205
11318
|
readFileSync as readFileSync6,
|
|
11206
|
-
realpathSync as
|
|
11319
|
+
realpathSync as realpathSync3,
|
|
11207
11320
|
rmSync as rmSync3,
|
|
11208
11321
|
writeFileSync as writeFileSync5
|
|
11209
11322
|
} from "fs";
|
|
@@ -11242,7 +11355,7 @@ function userIntendsToUseUnscoped() {
|
|
|
11242
11355
|
function ourLoadedDir() {
|
|
11243
11356
|
try {
|
|
11244
11357
|
const filePath = fileURLToPath3(import.meta.url);
|
|
11245
|
-
return
|
|
11358
|
+
return realpathSync3(resolve5(filePath, "..", ".."));
|
|
11246
11359
|
} catch {
|
|
11247
11360
|
return null;
|
|
11248
11361
|
}
|
|
@@ -11311,7 +11424,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
11311
11424
|
if (!existsSync3(stalePath)) return;
|
|
11312
11425
|
let realStalePath = stalePath;
|
|
11313
11426
|
try {
|
|
11314
|
-
realStalePath =
|
|
11427
|
+
realStalePath = realpathSync3(stalePath);
|
|
11315
11428
|
} catch {
|
|
11316
11429
|
}
|
|
11317
11430
|
if (ourDir && realStalePath === ourDir) return;
|
|
@@ -11384,6 +11497,30 @@ function toV1Session(info) {
|
|
|
11384
11497
|
const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path9.resolve(root, session.subpath) : root : void 0;
|
|
11385
11498
|
return { ...info, directory, parentID: session.parentID };
|
|
11386
11499
|
}
|
|
11500
|
+
function toV1Messages(messages) {
|
|
11501
|
+
if (!Array.isArray(messages)) return [];
|
|
11502
|
+
return messages.flatMap((entry) => {
|
|
11503
|
+
if (!entry || typeof entry !== "object") return [];
|
|
11504
|
+
const message = entry;
|
|
11505
|
+
const parts = Array.isArray(message.content) ? message.content.flatMap((part) => {
|
|
11506
|
+
if (!part || typeof part !== "object") return [];
|
|
11507
|
+
const piece = part;
|
|
11508
|
+
return piece.type === "text" && typeof piece.text === "string" ? [{ type: "text", text: piece.text }] : [];
|
|
11509
|
+
}) : typeof message.text === "string" ? [{ type: "text", text: message.text }] : [];
|
|
11510
|
+
const error = message.error;
|
|
11511
|
+
const info = {
|
|
11512
|
+
role: typeof message.type === "string" ? message.type : "",
|
|
11513
|
+
time: message.time && typeof message.time === "object" ? message.time : {},
|
|
11514
|
+
...error && typeof error === "object" ? {
|
|
11515
|
+
error: {
|
|
11516
|
+
name: typeof error.type === "string" ? error.type : "error",
|
|
11517
|
+
data: { message: typeof error.message === "string" ? error.message : void 0 }
|
|
11518
|
+
}
|
|
11519
|
+
} : {}
|
|
11520
|
+
};
|
|
11521
|
+
return [{ info, parts }];
|
|
11522
|
+
});
|
|
11523
|
+
}
|
|
11387
11524
|
function createV1ClientShim(ctx) {
|
|
11388
11525
|
const client = {};
|
|
11389
11526
|
const mcpList = ctx.mcp?.list;
|
|
@@ -11436,13 +11573,27 @@ ${agentList}` : subagent.description;
|
|
|
11436
11573
|
};
|
|
11437
11574
|
}
|
|
11438
11575
|
const sessionGet = ctx.session?.get;
|
|
11576
|
+
const sessionContext = ctx.session?.context;
|
|
11577
|
+
const sessionInterrupt = ctx.session?.interrupt;
|
|
11578
|
+
const session = {};
|
|
11439
11579
|
if (typeof sessionGet === "function") {
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11580
|
+
session.get = async (options) => ({
|
|
11581
|
+
data: toV1Session(await sessionGet.call(ctx.session, { sessionID: options.path.id }))
|
|
11582
|
+
});
|
|
11583
|
+
}
|
|
11584
|
+
if (typeof sessionContext === "function") {
|
|
11585
|
+
session.messages = async (options) => ({
|
|
11586
|
+
data: toV1Messages(await sessionContext.call(ctx.session, { sessionID: options.path.id }))
|
|
11587
|
+
});
|
|
11588
|
+
}
|
|
11589
|
+
if (typeof sessionInterrupt === "function") {
|
|
11590
|
+
session.abort = async (options) => {
|
|
11591
|
+
const result = await sessionInterrupt.call(ctx.session, { sessionID: options.path.id });
|
|
11592
|
+
const interrupted = result?.interrupted;
|
|
11593
|
+
return { data: typeof interrupted === "boolean" ? interrupted : true };
|
|
11444
11594
|
};
|
|
11445
11595
|
}
|
|
11596
|
+
if (Object.keys(session).length > 0) client.session = session;
|
|
11446
11597
|
return client;
|
|
11447
11598
|
}
|
|
11448
11599
|
|