@khalilgharbaoui/opencode-claude-code-plugin 0.31.1 → 0.32.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
@@ -1683,7 +1683,8 @@ function buildProxyTimeoutError(toolName, ms) {
1683
1683
  }
1684
1684
  return new Error(base);
1685
1685
  }
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. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls have no proxy deadline by default: the call waits for the subagent to finish (a positive proxyToolTimeoutMs override adds a deadline).";
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
+ 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.';
1687
1688
  var TASK_BATCH_TOOL_NAME = "task_batch";
1688
1689
  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).";
1689
1690
  var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
@@ -1769,6 +1770,75 @@ function overlayQuestionProxyDescription(tools, liveDescription) {
1769
1770
  ${QUESTION_PROXY_NOTE}` } : t
1770
1771
  );
1771
1772
  }
1773
+ function liveTaskSupportsBackground(taskParameters, dialect = "v1") {
1774
+ if (dialect === "v2") return true;
1775
+ const properties = taskParameters?.properties;
1776
+ if (!properties || typeof properties !== "object") return false;
1777
+ return "background" in properties;
1778
+ }
1779
+ var BACKGROUND_TASK_TOOL_DEFS = [
1780
+ {
1781
+ name: "task_status",
1782
+ description: "Read the current state of a background subagent started with `task` and `background: true`, and collect its result if it has finished. Use this only to recover a result opencode's automatic completion notification did not deliver (the turn was interrupted, errored, or was compacted across). It is NOT a progress poll: a healthy background task delivers its own result, so calling this in a loop spends turns and learns nothing. A result is handed over once; asking again reports the state without repeating the output.",
1783
+ inputSchema: {
1784
+ type: "object",
1785
+ properties: {
1786
+ task_id: {
1787
+ type: "string",
1788
+ description: 'The id from the `<task id="..." state="running">` envelope the background dispatch returned.'
1789
+ }
1790
+ },
1791
+ required: ["task_id"]
1792
+ }
1793
+ },
1794
+ {
1795
+ name: "task_cancel",
1796
+ description: "Stop a background subagent started with `task` and `background: true`. Use it when the work is no longer wanted, has been superseded, or is running away. A cancelled subagent sends no completion notification. This cannot stop a foreground task call: those block this conversation and end on their own.",
1797
+ inputSchema: {
1798
+ type: "object",
1799
+ properties: {
1800
+ task_id: {
1801
+ type: "string",
1802
+ description: 'The id from the `<task id="..." state="running">` envelope the background dispatch returned.'
1803
+ }
1804
+ },
1805
+ required: ["task_id"]
1806
+ }
1807
+ }
1808
+ ];
1809
+ function applyBackgroundSubagentSupport(tools, supported) {
1810
+ if (!tools.some((t) => t.name === "task")) return tools;
1811
+ const adjusted = tools.map((t) => {
1812
+ if (t.name !== "task" && t.name !== TASK_BATCH_TOOL_NAME) return t;
1813
+ if (supported) {
1814
+ return { ...t, description: `${t.description}
1815
+
1816
+ ${TASK_BACKGROUND_NOTE}` };
1817
+ }
1818
+ return { ...t, inputSchema: stripBackgroundFromSchema(t.name, t.inputSchema) };
1819
+ });
1820
+ return supported ? [...adjusted, ...BACKGROUND_TASK_TOOL_DEFS] : adjusted;
1821
+ }
1822
+ function stripBackgroundFromSchema(toolName, schema) {
1823
+ if (toolName === TASK_BATCH_TOOL_NAME) {
1824
+ return {
1825
+ ...schema,
1826
+ properties: {
1827
+ tasks: {
1828
+ type: "array",
1829
+ minItems: 2,
1830
+ description: "Independent subagent tasks to run concurrently",
1831
+ items: {
1832
+ type: "object",
1833
+ properties: TASK_INPUT_PROPERTIES_NO_BACKGROUND,
1834
+ required: TASK_INPUT_REQUIRED
1835
+ }
1836
+ }
1837
+ }
1838
+ };
1839
+ }
1840
+ return { ...schema, properties: TASK_INPUT_PROPERTIES_NO_BACKGROUND };
1841
+ }
1772
1842
  function filterQuestionProxyByOpencodeSupport(tools, opencodeHasQuestion) {
1773
1843
  if (opencodeHasQuestion) return tools;
1774
1844
  return tools.filter((t) => t.name !== "question");
@@ -1796,9 +1866,12 @@ var TASK_INPUT_PROPERTIES = {
1796
1866
  },
1797
1867
  background: {
1798
1868
  type: "boolean",
1799
- description: "Run the task in the background when supported by opencode"
1869
+ description: 'Run the subagent in the background and return immediately with a `<task id="..." state="running">` envelope instead of its answer. You are notified automatically when it completes. Do NOT sleep, poll or proactively check on its progress.'
1800
1870
  }
1801
1871
  };
1872
+ var TASK_INPUT_PROPERTIES_NO_BACKGROUND = Object.fromEntries(
1873
+ Object.entries(TASK_INPUT_PROPERTIES).filter(([name]) => name !== "background")
1874
+ );
1802
1875
  var DEFAULT_PROXY_TOOLS = [
1803
1876
  {
1804
1877
  name: "bash",
@@ -2859,6 +2932,362 @@ function clearCompression(sessionKey2) {
2859
2932
  compressions.delete(sessionKey2);
2860
2933
  }
2861
2934
 
2935
+ // src/runtime-status.ts
2936
+ var opencodeClient = null;
2937
+ function setOpencodeClient(client) {
2938
+ if (client && typeof client === "object") {
2939
+ opencodeClient = client;
2940
+ }
2941
+ }
2942
+ function getOpencodeClient() {
2943
+ return opencodeClient;
2944
+ }
2945
+ var opencodeProjectDirectory;
2946
+ function setOpencodeProjectDirectory(dir) {
2947
+ opencodeProjectDirectory = dir;
2948
+ }
2949
+ function getOpencodeProjectDirectory() {
2950
+ return opencodeProjectDirectory;
2951
+ }
2952
+ function isUsableDirectory(d) {
2953
+ return typeof d === "string" && d.length > 1 && d !== "/";
2954
+ }
2955
+ function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
2956
+ if (configured) return configured;
2957
+ if (isUsableDirectory(sessionDir)) return sessionDir;
2958
+ if (isUsableDirectory(live)) return live;
2959
+ return captured ?? live;
2960
+ }
2961
+ async function resolveSpawnCwdForSession(configured, sessionID) {
2962
+ if (configured) return configured;
2963
+ const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
2964
+ return resolveSpawnCwdFrom(
2965
+ configured,
2966
+ process.cwd(),
2967
+ opencodeProjectDirectory,
2968
+ sessionDir
2969
+ );
2970
+ }
2971
+ async function fetchSessionDirectory(sessionID) {
2972
+ if (!sessionID || sessionID === "default") return void 0;
2973
+ const client = opencodeClient;
2974
+ if (!client?.session?.get) return void 0;
2975
+ try {
2976
+ const res = await client.session.get({ path: { id: sessionID } });
2977
+ const data = res.data;
2978
+ if (!data || typeof data !== "object") return void 0;
2979
+ const dir = data.directory;
2980
+ return isUsableDirectory(dir) ? dir : void 0;
2981
+ } catch (err) {
2982
+ log.warn("failed to fetch opencode session directory", {
2983
+ sessionID,
2984
+ error: err instanceof Error ? err.message : String(err)
2985
+ });
2986
+ return void 0;
2987
+ }
2988
+ }
2989
+ async function fetchSessionParentId(sessionID) {
2990
+ if (!sessionID || sessionID === "default") return void 0;
2991
+ const client = opencodeClient;
2992
+ if (!client?.session?.get) return void 0;
2993
+ try {
2994
+ const res = await client.session.get({ path: { id: sessionID } });
2995
+ const data = res.data;
2996
+ if (!data || typeof data !== "object") return void 0;
2997
+ const parentID = data.parentID;
2998
+ return typeof parentID === "string" && parentID.length > 0 ? parentID : void 0;
2999
+ } catch (err) {
3000
+ log.warn("failed to fetch opencode session parent", {
3001
+ sessionID,
3002
+ error: err instanceof Error ? err.message : String(err)
3003
+ });
3004
+ return void 0;
3005
+ }
3006
+ }
3007
+ async function fetchSessionRunState(sessionID) {
3008
+ if (!sessionID || sessionID === "default") return "unknown";
3009
+ const status = opencodeClient?.session?.status;
3010
+ if (!status) return "unknown";
3011
+ try {
3012
+ const res = await status.call(opencodeClient.session);
3013
+ const data = res.data;
3014
+ if (!data || typeof data !== "object") return "unknown";
3015
+ const entry = data[sessionID];
3016
+ if (!entry || typeof entry !== "object") return "idle";
3017
+ const type = entry.type;
3018
+ return type === "idle" ? "idle" : "busy";
3019
+ } catch (err) {
3020
+ log.debug("failed to read opencode session status", {
3021
+ sessionID,
3022
+ error: err instanceof Error ? err.message : String(err)
3023
+ });
3024
+ return "unknown";
3025
+ }
3026
+ }
3027
+ async function settleSessionRunState(sessionID, options = {}) {
3028
+ const pollMs = options.pollMs ?? 150;
3029
+ const windowMs = options.windowMs ?? 1500;
3030
+ const started = Date.now();
3031
+ let last = "unknown";
3032
+ for (; ; ) {
3033
+ const state = await fetchSessionRunState(sessionID);
3034
+ if (state === "busy") return "busy";
3035
+ if (state !== "unknown") last = state;
3036
+ if (Date.now() - started >= windowMs) return last;
3037
+ await new Promise((resolve6) => setTimeout(resolve6, pollMs));
3038
+ }
3039
+ }
3040
+ async function getRuntimeMcpStatus() {
3041
+ const client = opencodeClient;
3042
+ if (!client?.mcp?.status) return void 0;
3043
+ try {
3044
+ const res = await client.mcp.status();
3045
+ const data = res.data;
3046
+ if (!data || typeof data !== "object") return void 0;
3047
+ const out = {};
3048
+ for (const [name, entry] of Object.entries(data)) {
3049
+ if (entry && typeof entry === "object") {
3050
+ const status = entry.status;
3051
+ if (typeof status === "string") out[name] = status;
3052
+ }
3053
+ }
3054
+ return out;
3055
+ } catch (err) {
3056
+ log.warn("failed to fetch opencode MCP runtime status", {
3057
+ error: err instanceof Error ? err.message : String(err)
3058
+ });
3059
+ return void 0;
3060
+ }
3061
+ }
3062
+ async function fetchOpencodeToolList(provider, model, directory) {
3063
+ const client = opencodeClient;
3064
+ if (!client?.tool?.list) return void 0;
3065
+ try {
3066
+ const res = await client.tool.list({
3067
+ query: { provider, model, ...directory ? { directory } : {} }
3068
+ });
3069
+ const data = res.data;
3070
+ if (!Array.isArray(data)) return void 0;
3071
+ const out = [];
3072
+ for (const entry of data) {
3073
+ if (!entry || typeof entry !== "object") continue;
3074
+ const e = entry;
3075
+ const id = typeof e.id === "string" ? e.id : null;
3076
+ const description = typeof e.description === "string" ? e.description : "";
3077
+ const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
3078
+ if (!id) continue;
3079
+ out.push({ id, description, parameters });
3080
+ }
3081
+ return out;
3082
+ } catch (err) {
3083
+ log.warn("failed to fetch opencode tool list", {
3084
+ provider,
3085
+ model,
3086
+ error: err instanceof Error ? err.message : String(err)
3087
+ });
3088
+ return void 0;
3089
+ }
3090
+ }
3091
+ async function fetchSessionReplies(sessionID) {
3092
+ if (!sessionID || sessionID === "default") return void 0;
3093
+ const client = opencodeClient;
3094
+ if (!client?.session?.messages) return void 0;
3095
+ try {
3096
+ const res = await client.session.messages({ path: { id: sessionID } });
3097
+ const data = res.data;
3098
+ if (!Array.isArray(data)) return void 0;
3099
+ const out = [];
3100
+ for (const entry of data) {
3101
+ if (!entry || typeof entry !== "object") continue;
3102
+ const info = entry.info;
3103
+ if (!info || typeof info !== "object") continue;
3104
+ const record = info;
3105
+ const parts = entry.parts;
3106
+ const text = Array.isArray(parts) ? parts.flatMap((part) => {
3107
+ if (!part || typeof part !== "object") return [];
3108
+ const p = part;
3109
+ if (p.type !== "text" || typeof p.text !== "string") return [];
3110
+ return p.synthetic === true ? [] : [p.text];
3111
+ }).join("\n").trim() : "";
3112
+ const error = record.error;
3113
+ const message = error && typeof error === "object" ? typeof error.data?.message === "string" ? error.data.message : typeof error.name === "string" ? error.name : void 0 : void 0;
3114
+ out.push({
3115
+ role: typeof record.role === "string" ? record.role : "",
3116
+ completed: typeof record.time?.completed === "number",
3117
+ error: message,
3118
+ text
3119
+ });
3120
+ }
3121
+ return out;
3122
+ } catch (err) {
3123
+ log.warn("failed to fetch opencode session messages", {
3124
+ sessionID,
3125
+ error: err instanceof Error ? err.message : String(err)
3126
+ });
3127
+ return void 0;
3128
+ }
3129
+ }
3130
+ async function abortSession(sessionID) {
3131
+ if (!sessionID || sessionID === "default") return false;
3132
+ const client = opencodeClient;
3133
+ if (!client?.session?.abort) return false;
3134
+ try {
3135
+ await client.session.abort({ path: { id: sessionID } });
3136
+ return true;
3137
+ } catch (err) {
3138
+ log.warn("failed to abort opencode session", {
3139
+ sessionID,
3140
+ error: err instanceof Error ? err.message : String(err)
3141
+ });
3142
+ return false;
3143
+ }
3144
+ }
3145
+
3146
+ // src/background-tasks.ts
3147
+ var TASK_STATUS_TOOL_NAME = "task_status";
3148
+ var TASK_CANCEL_TOOL_NAME = "task_cancel";
3149
+ var MAX_LEDGERS = 32;
3150
+ var MAX_COLLECTED_PER_SESSION = 64;
3151
+ var collected = /* @__PURE__ */ new Map();
3152
+ function ledgerFor(sessionKey2) {
3153
+ let ids = collected.get(sessionKey2);
3154
+ if (!ids) {
3155
+ ids = /* @__PURE__ */ new Set();
3156
+ collected.set(sessionKey2, ids);
3157
+ while (collected.size > MAX_LEDGERS) {
3158
+ const oldest = collected.keys().next();
3159
+ if (oldest.done) break;
3160
+ collected.delete(oldest.value);
3161
+ }
3162
+ }
3163
+ return ids;
3164
+ }
3165
+ function markCollected(sessionKey2, taskId) {
3166
+ const ids = ledgerFor(sessionKey2);
3167
+ ids.add(taskId);
3168
+ while (ids.size > MAX_COLLECTED_PER_SESSION) {
3169
+ const oldest = ids.values().next();
3170
+ if (oldest.done) break;
3171
+ ids.delete(oldest.value);
3172
+ }
3173
+ }
3174
+ function hasCollectedBackgroundTask(sessionKey2, taskId) {
3175
+ return collected.get(sessionKey2)?.has(taskId) === true;
3176
+ }
3177
+ function clearBackgroundTasks(sessionKey2) {
3178
+ collected.delete(sessionKey2);
3179
+ }
3180
+ function readTaskId(input) {
3181
+ const raw = input.task_id ?? input.taskId ?? input.sessionID;
3182
+ if (typeof raw !== "string") return null;
3183
+ const id = raw.trim();
3184
+ return id.length > 0 ? id : null;
3185
+ }
3186
+ var MISSING_TASK_ID = 'task_id is required: pass the id from the `<task id="...">` envelope the background dispatch returned.';
3187
+ async function guardParent(taskId, callerSessionId) {
3188
+ if (!callerSessionId || callerSessionId === "default") {
3189
+ 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
+ }
3191
+ const parent = await fetchSessionParentId(taskId);
3192
+ 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 \`<task id="...">\` envelope this conversation received.`;
3194
+ }
3195
+ function lastAssistantReply(replies) {
3196
+ for (let index = replies.length - 1; index >= 0; index--) {
3197
+ const reply = replies[index];
3198
+ if (reply.role === "assistant") return reply;
3199
+ }
3200
+ return void 0;
3201
+ }
3202
+ async function collectBackgroundTask(input, options) {
3203
+ const taskId = readTaskId(input);
3204
+ if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
3205
+ const refused = await guardParent(taskId, options.callerSessionId);
3206
+ if (refused) return { kind: "error", message: refused };
3207
+ const replies = await fetchSessionReplies(taskId);
3208
+ if (replies === void 0) {
3209
+ return {
3210
+ kind: "error",
3211
+ message: `Could not read background task ${taskId} from opencode. It may have been deleted, or this opencode build does not expose the session transcript. The completion notification is still the primary delivery path: end your turn and wait for it.`
3212
+ };
3213
+ }
3214
+ const runState = await fetchSessionRunState(taskId);
3215
+ if (runState === "busy") {
3216
+ return {
3217
+ kind: "text",
3218
+ text: `<task id="${taskId}" state="running">
3219
+ Still working. Do not call this again in a loop: when it finishes opencode delivers the result to this conversation on its own. End your turn, or work on something that does not overlap it.
3220
+ </task>`
3221
+ };
3222
+ }
3223
+ const reply = lastAssistantReply(replies);
3224
+ if (!reply) {
3225
+ return {
3226
+ kind: "text",
3227
+ text: `<task id="${taskId}" state="unknown">
3228
+ opencode has no assistant reply for this task yet and it is not running. It may still be starting up.
3229
+ </task>`
3230
+ };
3231
+ }
3232
+ if (reply.error) {
3233
+ markCollected(options.sessionKey, taskId);
3234
+ return {
3235
+ kind: "text",
3236
+ isError: true,
3237
+ text: `<task id="${taskId}" state="error">
3238
+ ${reply.error}
3239
+ </task>`
3240
+ };
3241
+ }
3242
+ if (hasCollectedBackgroundTask(options.sessionKey, taskId)) {
3243
+ return {
3244
+ kind: "text",
3245
+ text: `<task id="${taskId}" state="completed">
3246
+ Already delivered to this conversation earlier (by this tool or by opencode's completion notification). Scroll back for the result rather than collecting it again.
3247
+ </task>`
3248
+ };
3249
+ }
3250
+ markCollected(options.sessionKey, taskId);
3251
+ log.info("collected background subagent result", {
3252
+ sessionKey: options.sessionKey,
3253
+ taskId,
3254
+ textLength: reply.text.length
3255
+ });
3256
+ return {
3257
+ kind: "text",
3258
+ text: `<task id="${taskId}" state="completed">
3259
+ <task_result>
3260
+ ${reply.text || "(the subagent produced no text)"}
3261
+ </task_result>
3262
+ </task>`
3263
+ };
3264
+ }
3265
+ async function cancelBackgroundTask(input, options) {
3266
+ const taskId = readTaskId(input);
3267
+ if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
3268
+ const refused = await guardParent(taskId, options.callerSessionId);
3269
+ if (refused) return { kind: "error", message: refused };
3270
+ const before = await fetchSessionRunState(taskId);
3271
+ const aborted = await abortSession(taskId);
3272
+ if (!aborted) {
3273
+ return {
3274
+ kind: "error",
3275
+ message: `Could not cancel background task ${taskId}: opencode did not accept the abort. It may have already finished, or this opencode build does not expose session abort.`
3276
+ };
3277
+ }
3278
+ collected.get(options.sessionKey)?.delete(taskId);
3279
+ log.info("cancelled background subagent", {
3280
+ sessionKey: options.sessionKey,
3281
+ taskId,
3282
+ wasRunning: before === "busy"
3283
+ });
3284
+ return {
3285
+ kind: "text",
3286
+ text: `<task id="${taskId}" state="cancelled">
3287
+ ` + (before === "busy" ? "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
+ };
3289
+ }
3290
+
2862
3291
  // src/permission-presets.ts
2863
3292
  var READ_ONLY_DISALLOWED_CLI_TOOLS = [
2864
3293
  "Bash",
@@ -3395,11 +3824,13 @@ function deleteActiveProcessesForSession(sessionID) {
3395
3824
  clearCompression(key);
3396
3825
  if (!released.includes(key)) released.push(key);
3397
3826
  }
3827
+ for (const key of released) clearBackgroundTasks(key);
3398
3828
  return released;
3399
3829
  }
3400
3830
  function killAllActiveProcesses() {
3401
3831
  const keys = [...activeProcesses.keys()];
3402
3832
  for (const key of keys) deleteActiveProcess(key);
3833
+ for (const key of keys) clearBackgroundTasks(key);
3403
3834
  return keys;
3404
3835
  }
3405
3836
  var processExitCleanupWired = false;
@@ -3511,6 +3942,7 @@ function invalidateOtherEffortSessions(baseKey, effort) {
3511
3942
  deleteActiveProcess(key);
3512
3943
  deleteClaudeSessionId(key);
3513
3944
  clearCompression(key);
3945
+ clearBackgroundTasks(key);
3514
3946
  }
3515
3947
  }
3516
3948
  function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl) {
@@ -4497,163 +4929,6 @@ function finishBridge(input) {
4497
4929
  };
4498
4930
  }
4499
4931
 
4500
- // src/runtime-status.ts
4501
- var opencodeClient = null;
4502
- function setOpencodeClient(client) {
4503
- if (client && typeof client === "object") {
4504
- opencodeClient = client;
4505
- }
4506
- }
4507
- function getOpencodeClient() {
4508
- return opencodeClient;
4509
- }
4510
- var opencodeProjectDirectory;
4511
- function setOpencodeProjectDirectory(dir) {
4512
- opencodeProjectDirectory = dir;
4513
- }
4514
- function getOpencodeProjectDirectory() {
4515
- return opencodeProjectDirectory;
4516
- }
4517
- function isUsableDirectory(d) {
4518
- return typeof d === "string" && d.length > 1 && d !== "/";
4519
- }
4520
- function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
4521
- if (configured) return configured;
4522
- if (isUsableDirectory(sessionDir)) return sessionDir;
4523
- if (isUsableDirectory(live)) return live;
4524
- return captured ?? live;
4525
- }
4526
- async function resolveSpawnCwdForSession(configured, sessionID) {
4527
- if (configured) return configured;
4528
- const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
4529
- return resolveSpawnCwdFrom(
4530
- configured,
4531
- process.cwd(),
4532
- opencodeProjectDirectory,
4533
- sessionDir
4534
- );
4535
- }
4536
- async function fetchSessionDirectory(sessionID) {
4537
- if (!sessionID || sessionID === "default") return void 0;
4538
- const client = opencodeClient;
4539
- if (!client?.session?.get) return void 0;
4540
- try {
4541
- const res = await client.session.get({ path: { id: sessionID } });
4542
- const data = res.data;
4543
- if (!data || typeof data !== "object") return void 0;
4544
- const dir = data.directory;
4545
- return isUsableDirectory(dir) ? dir : void 0;
4546
- } catch (err) {
4547
- log.warn("failed to fetch opencode session directory", {
4548
- sessionID,
4549
- error: err instanceof Error ? err.message : String(err)
4550
- });
4551
- return void 0;
4552
- }
4553
- }
4554
- async function fetchSessionParentId(sessionID) {
4555
- if (!sessionID || sessionID === "default") return void 0;
4556
- const client = opencodeClient;
4557
- if (!client?.session?.get) return void 0;
4558
- try {
4559
- const res = await client.session.get({ path: { id: sessionID } });
4560
- const data = res.data;
4561
- if (!data || typeof data !== "object") return void 0;
4562
- const parentID = data.parentID;
4563
- return typeof parentID === "string" && parentID.length > 0 ? parentID : void 0;
4564
- } catch (err) {
4565
- log.warn("failed to fetch opencode session parent", {
4566
- sessionID,
4567
- error: err instanceof Error ? err.message : String(err)
4568
- });
4569
- return void 0;
4570
- }
4571
- }
4572
- async function fetchSessionRunState(sessionID) {
4573
- if (!sessionID || sessionID === "default") return "unknown";
4574
- const status = opencodeClient?.session?.status;
4575
- if (!status) return "unknown";
4576
- try {
4577
- const res = await status.call(opencodeClient.session);
4578
- const data = res.data;
4579
- if (!data || typeof data !== "object") return "unknown";
4580
- const entry = data[sessionID];
4581
- if (!entry || typeof entry !== "object") return "idle";
4582
- const type = entry.type;
4583
- return type === "idle" ? "idle" : "busy";
4584
- } catch (err) {
4585
- log.debug("failed to read opencode session status", {
4586
- sessionID,
4587
- error: err instanceof Error ? err.message : String(err)
4588
- });
4589
- return "unknown";
4590
- }
4591
- }
4592
- async function settleSessionRunState(sessionID, options = {}) {
4593
- const pollMs = options.pollMs ?? 150;
4594
- const windowMs = options.windowMs ?? 1500;
4595
- const started = Date.now();
4596
- let last = "unknown";
4597
- for (; ; ) {
4598
- const state = await fetchSessionRunState(sessionID);
4599
- if (state === "busy") return "busy";
4600
- if (state !== "unknown") last = state;
4601
- if (Date.now() - started >= windowMs) return last;
4602
- await new Promise((resolve6) => setTimeout(resolve6, pollMs));
4603
- }
4604
- }
4605
- async function getRuntimeMcpStatus() {
4606
- const client = opencodeClient;
4607
- if (!client?.mcp?.status) return void 0;
4608
- try {
4609
- const res = await client.mcp.status();
4610
- const data = res.data;
4611
- if (!data || typeof data !== "object") return void 0;
4612
- const out = {};
4613
- for (const [name, entry] of Object.entries(data)) {
4614
- if (entry && typeof entry === "object") {
4615
- const status = entry.status;
4616
- if (typeof status === "string") out[name] = status;
4617
- }
4618
- }
4619
- return out;
4620
- } catch (err) {
4621
- log.warn("failed to fetch opencode MCP runtime status", {
4622
- error: err instanceof Error ? err.message : String(err)
4623
- });
4624
- return void 0;
4625
- }
4626
- }
4627
- async function fetchOpencodeToolList(provider, model, directory) {
4628
- const client = opencodeClient;
4629
- if (!client?.tool?.list) return void 0;
4630
- try {
4631
- const res = await client.tool.list({
4632
- query: { provider, model, ...directory ? { directory } : {} }
4633
- });
4634
- const data = res.data;
4635
- if (!Array.isArray(data)) return void 0;
4636
- const out = [];
4637
- for (const entry of data) {
4638
- if (!entry || typeof entry !== "object") continue;
4639
- const e = entry;
4640
- const id = typeof e.id === "string" ? e.id : null;
4641
- const description = typeof e.description === "string" ? e.description : "";
4642
- const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
4643
- if (!id) continue;
4644
- out.push({ id, description, parameters });
4645
- }
4646
- return out;
4647
- } catch (err) {
4648
- log.warn("failed to fetch opencode tool list", {
4649
- provider,
4650
- model,
4651
- error: err instanceof Error ? err.message : String(err)
4652
- });
4653
- return void 0;
4654
- }
4655
- }
4656
-
4657
4932
  // src/startup-diagnostics.ts
4658
4933
  var cachedPluginVersion;
4659
4934
  function pluginVersion() {
@@ -6560,6 +6835,69 @@ function interactiveSpawnEnv(opts) {
6560
6835
  ...opts.effort ? { CLAUDE_CODE_EFFORT_LEVEL: opts.effort } : {}
6561
6836
  };
6562
6837
  }
6838
+ function usageInputSide(usage) {
6839
+ return (usage?.input_tokens ?? 0) + (usage?.cache_read_input_tokens ?? 0) + (usage?.cache_creation_input_tokens ?? 0);
6840
+ }
6841
+ function isApiCallRecord(rec) {
6842
+ if (!rec || rec.type !== "assistant" || !rec.message) return false;
6843
+ return usageInputSide(rec.message.usage) > 0;
6844
+ }
6845
+ var TurnUsageAccumulator = class {
6846
+ calls = /* @__PURE__ */ new Map();
6847
+ lastKey = null;
6848
+ /** Feed every transcript record of the turn, in file order. */
6849
+ add(rec) {
6850
+ if (!isApiCallRecord(rec)) return;
6851
+ const id = rec.message.id;
6852
+ const key = typeof id === "string" && id.length > 0 ? id : `uuid:${rec.uuid ?? this.calls.size}`;
6853
+ this.calls.set(key, rec.message.usage);
6854
+ this.lastKey = key;
6855
+ }
6856
+ /** Distinct API calls seen, for diagnostics. */
6857
+ get callCount() {
6858
+ return this.calls.size;
6859
+ }
6860
+ /**
6861
+ * The newest real call's usage, verbatim (its own `iterations` included):
6862
+ * the conversation's context occupancy, which is what a finish must report.
6863
+ * See `lastCallContextUsage` in `usage.ts`, the one convention for this.
6864
+ */
6865
+ get lastCall() {
6866
+ return this.lastKey === null ? null : this.calls.get(this.lastKey) ?? null;
6867
+ }
6868
+ /**
6869
+ * The turn summed over DISTINCT calls: the shape and the meaning of a
6870
+ * headless `result` frame's usage, which is what `total_cost_usd` and
6871
+ * `turnStats` are about. Deliberately carries no `iterations`: that is one
6872
+ * response's server-side field and a sum of calls has no such list, so
6873
+ * `toUsage` falls back to these flat counters, while `lastCall` keeps its
6874
+ * own for `lastCallContextUsage` to read.
6875
+ */
6876
+ get turnTotal() {
6877
+ if (this.calls.size === 0) return null;
6878
+ const total = {
6879
+ input_tokens: 0,
6880
+ output_tokens: 0,
6881
+ cache_read_input_tokens: 0,
6882
+ cache_creation_input_tokens: 0,
6883
+ cache_creation: {
6884
+ ephemeral_1h_input_tokens: 0,
6885
+ ephemeral_5m_input_tokens: 0
6886
+ },
6887
+ output_tokens_details: { thinking_tokens: 0 }
6888
+ };
6889
+ for (const u of this.calls.values()) {
6890
+ total.input_tokens += u.input_tokens ?? 0;
6891
+ total.output_tokens += u.output_tokens ?? 0;
6892
+ total.cache_read_input_tokens += u.cache_read_input_tokens ?? 0;
6893
+ total.cache_creation_input_tokens += u.cache_creation_input_tokens ?? 0;
6894
+ total.cache_creation.ephemeral_1h_input_tokens += u.cache_creation?.ephemeral_1h_input_tokens ?? 0;
6895
+ total.cache_creation.ephemeral_5m_input_tokens += u.cache_creation?.ephemeral_5m_input_tokens ?? 0;
6896
+ total.output_tokens_details.thinking_tokens += u.output_tokens_details?.thinking_tokens ?? 0;
6897
+ }
6898
+ return total;
6899
+ }
6900
+ };
6563
6901
  var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
6564
6902
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
6565
6903
  function resolveConfigDir(configDir) {
@@ -6754,8 +7092,8 @@ var ClaudeSession = class {
6754
7092
  this.proc.terminal.write(prompt);
6755
7093
  }
6756
7094
  await this.submitTurn();
6757
- const collected = [];
6758
- let lastUsage = null;
7095
+ const collected2 = [];
7096
+ const usage = new TurnUsageAccumulator();
6759
7097
  let stopReason = null;
6760
7098
  const deadline = Date.now() + timeout;
6761
7099
  while (Date.now() < deadline) {
@@ -6779,9 +7117,9 @@ var ClaudeSession = class {
6779
7117
  if (rec.type === "assistant" && rec.message) {
6780
7118
  for (const b of rec.message.content ?? []) {
6781
7119
  if (b?.type === "text" && typeof b.text === "string")
6782
- collected.push(b.text);
7120
+ collected2.push(b.text);
6783
7121
  }
6784
- if (rec.message.usage) lastUsage = rec.message.usage;
7122
+ usage.add(rec);
6785
7123
  if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
6786
7124
  stopReason = rec.message.stop_reason;
6787
7125
  }
@@ -6793,15 +7131,17 @@ var ClaudeSession = class {
6793
7131
  if (!stopReason) {
6794
7132
  throw new Error(
6795
7133
  this.failureMessage(
6796
- `turn timed out after ${timeout}ms (no terminal assistant record; collected ${collected.length} text block(s))`
7134
+ `turn timed out after ${timeout}ms (no terminal assistant record; collected ${collected2.length} text block(s))`
6797
7135
  )
6798
7136
  );
6799
7137
  }
6800
- const u = lastUsage ?? {};
7138
+ const turnTotal = usage.turnTotal;
7139
+ const u = turnTotal ?? {};
6801
7140
  return {
6802
- text: collected.join("\n").trim(),
7141
+ text: collected2.join("\n").trim(),
6803
7142
  stopReason,
6804
- usage: lastUsage,
7143
+ usage: turnTotal,
7144
+ lastCallUsage: usage.lastCall,
6805
7145
  cacheReadTokens: u.cache_read_input_tokens ?? 0,
6806
7146
  cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
6807
7147
  ephemeral1hTokens: u.cache_creation?.ephemeral_1h_input_tokens ?? 0,
@@ -6814,9 +7154,15 @@ var ClaudeSession = class {
6814
7154
  /**
6815
7155
  * Like ask(), but instead of collecting the reply text it re-emits each NEW
6816
7156
  * raw JSONL transcript line via onLine (verbatim) until a terminal
6817
- * stop_reason. Returns the terminal stop_reason + the last assistant usage.
6818
- * Used by the opencode plugin transport shim, which feeds these raw lines
6819
- * into the existing stream-json line handler unchanged.
7157
+ * stop_reason. Used by the opencode plugin transport shim, which feeds
7158
+ * these raw lines into the existing stream-json line handler unchanged.
7159
+ *
7160
+ * `usage` is the turn summed over DISTINCT API calls, which is exactly what
7161
+ * a headless `result` frame reports, so the shim can synthesize one and
7162
+ * every downstream consumer (the finish's `lastCallContextUsage`,
7163
+ * `turnStats`) behaves as it does on the headless path. `lastCallUsage` is
7164
+ * the newest real call, returned because a caller with no stream parser
7165
+ * (`askOnce`) has no other way to get the context side.
6820
7166
  */
6821
7167
  async tailTurn(prompt, onLine, perTurnTimeoutMs) {
6822
7168
  if (this.aborted) throw new Error("aborted");
@@ -6829,8 +7175,7 @@ var ClaudeSession = class {
6829
7175
  this.proc.terminal.write(prompt);
6830
7176
  }
6831
7177
  await this.submitTurn();
6832
- let lastUsage = null;
6833
- let totalOutput = 0;
7178
+ const usage = new TurnUsageAccumulator();
6834
7179
  let stopReason = null;
6835
7180
  const deadline = Date.now() + timeout;
6836
7181
  while (Date.now() < deadline) {
@@ -6854,11 +7199,8 @@ var ClaudeSession = class {
6854
7199
  } catch {
6855
7200
  continue;
6856
7201
  }
7202
+ usage.add(rec);
6857
7203
  if (rec.type === "assistant" && rec.message) {
6858
- if (rec.message.usage) {
6859
- lastUsage = rec.message.usage;
6860
- totalOutput += rec.message.usage.output_tokens ?? 0;
6861
- }
6862
7204
  if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
6863
7205
  stopReason = rec.message.stop_reason;
6864
7206
  }
@@ -6867,18 +7209,6 @@ var ClaudeSession = class {
6867
7209
  this.cursor = lastComplete;
6868
7210
  if (stopReason) break;
6869
7211
  }
6870
- let usage = lastUsage;
6871
- if (lastUsage) {
6872
- usage = { ...lastUsage, output_tokens: totalOutput };
6873
- if (Array.isArray(lastUsage.iterations) && lastUsage.iterations.length > 0) {
6874
- const iters = lastUsage.iterations.map((it) => ({ ...it }));
6875
- iters[iters.length - 1] = {
6876
- ...iters[iters.length - 1],
6877
- output_tokens: totalOutput
6878
- };
6879
- usage.iterations = iters;
6880
- }
6881
- }
6882
7212
  if (!stopReason) {
6883
7213
  throw new Error(
6884
7214
  this.failureMessage(
@@ -6886,7 +7216,12 @@ var ClaudeSession = class {
6886
7216
  )
6887
7217
  );
6888
7218
  }
6889
- return { stopReason, usage };
7219
+ return {
7220
+ stopReason,
7221
+ usage: usage.turnTotal,
7222
+ lastCallUsage: usage.lastCall,
7223
+ callCount: usage.callCount
7224
+ };
6890
7225
  }
6891
7226
  dispose() {
6892
7227
  if (this.proc) {
@@ -7025,9 +7360,18 @@ function spawnInteractiveProcess(opts) {
7025
7360
  void (async () => {
7026
7361
  try {
7027
7362
  await ensureStarted();
7028
- const { stopReason, usage } = await session.tailTurn(userMsg, (raw) => {
7363
+ const { stopReason, usage, lastCallUsage, callCount } = await session.tailTurn(userMsg, (raw) => {
7029
7364
  lineEmitter.emit("line", raw);
7030
7365
  });
7366
+ log.info("interactive turn usage", {
7367
+ apiCalls: callCount,
7368
+ turnOutputTokens: usage?.output_tokens,
7369
+ turnInputTokens: usage?.input_tokens,
7370
+ turnCacheReadTokens: usage?.cache_read_input_tokens,
7371
+ lastCallInputTokens: lastCallUsage?.input_tokens,
7372
+ lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
7373
+ lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens
7374
+ });
7031
7375
  const timedOut = !stopReason;
7032
7376
  emitResult(
7033
7377
  timedOut ? "error_during_execution" : stopReason,
@@ -7143,6 +7487,14 @@ Subagent dispatch in this environment goes through exactly two tools: \`mcp__ope
7143
7487
  - If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__task\`), then call it.
7144
7488
  - Claude Code's built-in TaskCreate/TaskUpdate/TaskList manage a local todo list. They cannot dispatch subagents; creating a task there runs nothing. Never report a subagent as dispatched unless \`mcp__opencode_proxy__task\` returned its result.
7145
7489
  - Do not verify a subagent's existence by searching config files \u2014 the tool's description lists the available agent types, and invalid types fail fast with a clear error.`;
7490
+ var BACKGROUND_SUBAGENT_HINT = `## Background subagents
7491
+
7492
+ This opencode host runs background subagents, so \`mcp__opencode_proxy__task\` takes \`background: true\`.
7493
+
7494
+ - A background dispatch returns at once with \`<task id="ses_..." state="running">\` instead of the subagent's answer. That is success, not a truncated result.
7495
+ - After starting one, keep working on something that does not overlap it, then end your turn normally. When the subagent finishes, opencode delivers its result into this conversation as a new message on its own. Do not sleep, poll, or ask the operator to wait.
7496
+ - The \`id\` in that envelope is the task_id. \`mcp__opencode_proxy__task_status\` reads a result back if the automatic delivery did not arrive (an interrupted or errored turn); \`mcp__opencode_proxy__task_cancel\` stops a background subagent you no longer want. Neither is a progress poll.
7497
+ - Foreground is still the default and still the right choice when you need the answer before you can continue. Use background only for work that genuinely runs alongside yours.`;
7146
7498
  var QUESTION_PROXY_HINT = `## Asking the operator questions
7147
7499
 
7148
7500
  Structured questions in this environment go through exactly one tool: \`mcp__opencode_proxy__question\`.
@@ -7714,9 +8066,11 @@ async function fetchLiveToolInfo(config, modelId) {
7714
8066
  config.cwd
7715
8067
  );
7716
8068
  const question = items?.find((item) => item.id === "question");
8069
+ const task = items?.find((item) => item.id === "task");
7717
8070
  return {
7718
8071
  resolved: items !== void 0,
7719
- taskDescription: items?.find((item) => item.id === "task")?.description,
8072
+ taskDescription: task?.description,
8073
+ taskParameters: task?.parameters,
7720
8074
  questionDescription: question?.description,
7721
8075
  hasQuestion: !!question,
7722
8076
  items
@@ -7759,9 +8113,14 @@ async function resolvePlanModeQuestion(config, compactionMode, loadLiveToolInfo)
7759
8113
  }
7760
8114
  return active;
7761
8115
  }
7762
- async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCompress) {
8116
+ async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCompress, callerSessionId) {
7763
8117
  const timeoutOverrides = config.proxyToolTimeoutMs;
7764
8118
  const interceptors = /* @__PURE__ */ new Map();
8119
+ if (tools.some((t) => t.name === TASK_STATUS_TOOL_NAME)) {
8120
+ const opts = { sessionKey: sessionKeyForCalls, callerSessionId };
8121
+ interceptors.set(TASK_STATUS_TOOL_NAME, (input) => collectBackgroundTask(input, opts));
8122
+ interceptors.set(TASK_CANCEL_TOOL_NAME, (input) => cancelBackgroundTask(input, opts));
8123
+ }
7765
8124
  if (interceptCompress && tools.some((t) => t.name === "compress")) {
7766
8125
  interceptors.set("compress", (input) => {
7767
8126
  const summary = typeof input.summary === "string" ? input.summary.trim() : "";
@@ -9379,12 +9738,13 @@ var ClaudeCodeLanguageModel = class {
9379
9738
  * Create a proxy MCP server for a single active Claude process/session.
9380
9739
  * The process lifecycle owns the server lifecycle via session-manager.
9381
9740
  */
9382
- async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress) {
9741
+ async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress, callerSessionId) {
9383
9742
  return ensureProxyServer(
9384
9743
  this.config,
9385
9744
  tools,
9386
9745
  sessionKeyForCalls,
9387
- interceptCompress
9746
+ interceptCompress,
9747
+ callerSessionId
9388
9748
  );
9389
9749
  }
9390
9750
  /**
@@ -10167,10 +10527,12 @@ var ClaudeCodeLanguageModel = class {
10167
10527
  const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
10168
10528
  resolved: false,
10169
10529
  taskDescription: void 0,
10530
+ taskParameters: void 0,
10170
10531
  questionDescription: void 0,
10171
10532
  hasQuestion: false
10172
10533
  };
10173
10534
  let enrichedProxy = resolvedProxy;
10535
+ let backgroundSubagentsSupported = false;
10174
10536
  if (enrichedProxy && taskProxyEnabled) {
10175
10537
  enrichedProxy = overlayTaskProxyDescription(
10176
10538
  enrichedProxy,
@@ -10185,6 +10547,20 @@ var ClaudeCodeLanguageModel = class {
10185
10547
  )
10186
10548
  )
10187
10549
  });
10550
+ backgroundSubagentsSupported = liveTaskSupportsBackground(
10551
+ liveToolInfo.taskParameters,
10552
+ self.config.hostApi === "v2" ? "v2" : "v1"
10553
+ );
10554
+ enrichedProxy = applyBackgroundSubagentSupport(
10555
+ enrichedProxy,
10556
+ backgroundSubagentsSupported
10557
+ );
10558
+ log.info("background subagent gate", {
10559
+ supported: backgroundSubagentsSupported,
10560
+ registryResolved: liveToolInfo.resolved,
10561
+ hostApi: self.config.hostApi ?? "v1",
10562
+ 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
+ });
10188
10564
  }
10189
10565
  if (enrichedProxy && questionProxyEnabled) {
10190
10566
  enrichedProxy = overlayQuestionProxyDescription(
@@ -10225,7 +10601,8 @@ var ClaudeCodeLanguageModel = class {
10225
10601
  state.proxyServer = await self.ensureProxyServer(
10226
10602
  combinedProxyTools,
10227
10603
  sk,
10228
- pluginCompressEnabled
10604
+ pluginCompressEnabled,
10605
+ affinity
10229
10606
  );
10230
10607
  }
10231
10608
  const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
@@ -10246,6 +10623,7 @@ var ClaudeCodeLanguageModel = class {
10246
10623
  [
10247
10624
  ...extractSystemMessages(options.prompt),
10248
10625
  ...taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
10626
+ ...backgroundSubagentsSupported ? [BACKGROUND_SUBAGENT_HINT] : [],
10249
10627
  ...questionProxyActive ? [QUESTION_PROXY_HINT] : []
10250
10628
  ],
10251
10629
  {