@khalilgharbaoui/opencode-claude-code-plugin 0.31.1 → 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 +53 -3
- package/dist/index.js +753 -224
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +64 -1
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
|
}
|
|
@@ -1683,7 +1683,9 @@ 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.
|
|
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.';
|
|
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.';
|
|
1687
1689
|
var TASK_BATCH_TOOL_NAME = "task_batch";
|
|
1688
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).";
|
|
1689
1691
|
var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
|
|
@@ -1769,6 +1771,76 @@ function overlayQuestionProxyDescription(tools, liveDescription) {
|
|
|
1769
1771
|
${QUESTION_PROXY_NOTE}` } : t
|
|
1770
1772
|
);
|
|
1771
1773
|
}
|
|
1774
|
+
function liveTaskSupportsBackground(taskParameters, dialect = "v1") {
|
|
1775
|
+
if (dialect === "v2") return true;
|
|
1776
|
+
const properties = taskParameters?.properties;
|
|
1777
|
+
if (!properties || typeof properties !== "object") return false;
|
|
1778
|
+
return "background" in properties;
|
|
1779
|
+
}
|
|
1780
|
+
var BACKGROUND_TASK_TOOL_DEFS = [
|
|
1781
|
+
{
|
|
1782
|
+
name: "task_status",
|
|
1783
|
+
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.",
|
|
1784
|
+
inputSchema: {
|
|
1785
|
+
type: "object",
|
|
1786
|
+
properties: {
|
|
1787
|
+
task_id: {
|
|
1788
|
+
type: "string",
|
|
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.'
|
|
1790
|
+
}
|
|
1791
|
+
},
|
|
1792
|
+
required: ["task_id"]
|
|
1793
|
+
}
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
name: "task_cancel",
|
|
1797
|
+
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.",
|
|
1798
|
+
inputSchema: {
|
|
1799
|
+
type: "object",
|
|
1800
|
+
properties: {
|
|
1801
|
+
task_id: {
|
|
1802
|
+
type: "string",
|
|
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.'
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
required: ["task_id"]
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
];
|
|
1810
|
+
function applyBackgroundSubagentSupport(tools, supported, dialect = "v1") {
|
|
1811
|
+
if (!tools.some((t) => t.name === "task")) return tools;
|
|
1812
|
+
const note = dialect === "v2" ? TASK_BACKGROUND_NOTE_V2 : TASK_BACKGROUND_NOTE;
|
|
1813
|
+
const adjusted = tools.map((t) => {
|
|
1814
|
+
if (t.name !== "task" && t.name !== TASK_BATCH_TOOL_NAME) return t;
|
|
1815
|
+
if (supported) {
|
|
1816
|
+
return { ...t, description: `${t.description}
|
|
1817
|
+
|
|
1818
|
+
${note}` };
|
|
1819
|
+
}
|
|
1820
|
+
return { ...t, inputSchema: stripBackgroundFromSchema(t.name, t.inputSchema) };
|
|
1821
|
+
});
|
|
1822
|
+
return supported ? [...adjusted, ...BACKGROUND_TASK_TOOL_DEFS] : adjusted;
|
|
1823
|
+
}
|
|
1824
|
+
function stripBackgroundFromSchema(toolName, schema) {
|
|
1825
|
+
if (toolName === TASK_BATCH_TOOL_NAME) {
|
|
1826
|
+
return {
|
|
1827
|
+
...schema,
|
|
1828
|
+
properties: {
|
|
1829
|
+
tasks: {
|
|
1830
|
+
type: "array",
|
|
1831
|
+
minItems: 2,
|
|
1832
|
+
description: "Independent subagent tasks to run concurrently",
|
|
1833
|
+
items: {
|
|
1834
|
+
type: "object",
|
|
1835
|
+
properties: TASK_INPUT_PROPERTIES_NO_BACKGROUND,
|
|
1836
|
+
required: TASK_INPUT_REQUIRED
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
return { ...schema, properties: TASK_INPUT_PROPERTIES_NO_BACKGROUND };
|
|
1843
|
+
}
|
|
1772
1844
|
function filterQuestionProxyByOpencodeSupport(tools, opencodeHasQuestion) {
|
|
1773
1845
|
if (opencodeHasQuestion) return tools;
|
|
1774
1846
|
return tools.filter((t) => t.name !== "question");
|
|
@@ -1796,9 +1868,12 @@ var TASK_INPUT_PROPERTIES = {
|
|
|
1796
1868
|
},
|
|
1797
1869
|
background: {
|
|
1798
1870
|
type: "boolean",
|
|
1799
|
-
description:
|
|
1871
|
+
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
1872
|
}
|
|
1801
1873
|
};
|
|
1874
|
+
var TASK_INPUT_PROPERTIES_NO_BACKGROUND = Object.fromEntries(
|
|
1875
|
+
Object.entries(TASK_INPUT_PROPERTIES).filter(([name]) => name !== "background")
|
|
1876
|
+
);
|
|
1802
1877
|
var DEFAULT_PROXY_TOOLS = [
|
|
1803
1878
|
{
|
|
1804
1879
|
name: "bash",
|
|
@@ -2859,6 +2934,392 @@ function clearCompression(sessionKey2) {
|
|
|
2859
2934
|
compressions.delete(sessionKey2);
|
|
2860
2935
|
}
|
|
2861
2936
|
|
|
2937
|
+
// src/runtime-status.ts
|
|
2938
|
+
var opencodeClient = null;
|
|
2939
|
+
function setOpencodeClient(client) {
|
|
2940
|
+
if (client && typeof client === "object") {
|
|
2941
|
+
opencodeClient = client;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
function getOpencodeClient() {
|
|
2945
|
+
return opencodeClient;
|
|
2946
|
+
}
|
|
2947
|
+
var opencodeProjectDirectory;
|
|
2948
|
+
function setOpencodeProjectDirectory(dir) {
|
|
2949
|
+
opencodeProjectDirectory = dir;
|
|
2950
|
+
}
|
|
2951
|
+
function getOpencodeProjectDirectory() {
|
|
2952
|
+
return opencodeProjectDirectory;
|
|
2953
|
+
}
|
|
2954
|
+
function isUsableDirectory(d) {
|
|
2955
|
+
return typeof d === "string" && d.length > 1 && d !== "/";
|
|
2956
|
+
}
|
|
2957
|
+
function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
|
|
2958
|
+
if (configured) return configured;
|
|
2959
|
+
if (isUsableDirectory(sessionDir)) return sessionDir;
|
|
2960
|
+
if (isUsableDirectory(live)) return live;
|
|
2961
|
+
return captured ?? live;
|
|
2962
|
+
}
|
|
2963
|
+
async function resolveSpawnCwdForSession(configured, sessionID) {
|
|
2964
|
+
if (configured) return configured;
|
|
2965
|
+
const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
|
|
2966
|
+
return resolveSpawnCwdFrom(
|
|
2967
|
+
configured,
|
|
2968
|
+
process.cwd(),
|
|
2969
|
+
opencodeProjectDirectory,
|
|
2970
|
+
sessionDir
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
async function fetchSessionDirectory(sessionID) {
|
|
2974
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
2975
|
+
const client = opencodeClient;
|
|
2976
|
+
if (!client?.session?.get) return void 0;
|
|
2977
|
+
try {
|
|
2978
|
+
const res = await client.session.get({ path: { id: sessionID } });
|
|
2979
|
+
const data = res.data;
|
|
2980
|
+
if (!data || typeof data !== "object") return void 0;
|
|
2981
|
+
const dir = data.directory;
|
|
2982
|
+
return isUsableDirectory(dir) ? dir : void 0;
|
|
2983
|
+
} catch (err) {
|
|
2984
|
+
log.warn("failed to fetch opencode session directory", {
|
|
2985
|
+
sessionID,
|
|
2986
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2987
|
+
});
|
|
2988
|
+
return void 0;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
async function fetchSessionParentId(sessionID) {
|
|
2992
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
2993
|
+
const client = opencodeClient;
|
|
2994
|
+
if (!client?.session?.get) return void 0;
|
|
2995
|
+
try {
|
|
2996
|
+
const res = await client.session.get({ path: { id: sessionID } });
|
|
2997
|
+
const data = res.data;
|
|
2998
|
+
if (!data || typeof data !== "object") return void 0;
|
|
2999
|
+
const parentID = data.parentID;
|
|
3000
|
+
return typeof parentID === "string" && parentID.length > 0 ? parentID : void 0;
|
|
3001
|
+
} catch (err) {
|
|
3002
|
+
log.warn("failed to fetch opencode session parent", {
|
|
3003
|
+
sessionID,
|
|
3004
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3005
|
+
});
|
|
3006
|
+
return void 0;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
async function fetchSessionRunState(sessionID) {
|
|
3010
|
+
if (!sessionID || sessionID === "default") return "unknown";
|
|
3011
|
+
const status = opencodeClient?.session?.status;
|
|
3012
|
+
if (!status) return "unknown";
|
|
3013
|
+
try {
|
|
3014
|
+
const res = await status.call(opencodeClient.session);
|
|
3015
|
+
const data = res.data;
|
|
3016
|
+
if (!data || typeof data !== "object") return "unknown";
|
|
3017
|
+
const entry = data[sessionID];
|
|
3018
|
+
if (!entry || typeof entry !== "object") return "idle";
|
|
3019
|
+
const type = entry.type;
|
|
3020
|
+
return type === "idle" ? "idle" : "busy";
|
|
3021
|
+
} catch (err) {
|
|
3022
|
+
log.debug("failed to read opencode session status", {
|
|
3023
|
+
sessionID,
|
|
3024
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3025
|
+
});
|
|
3026
|
+
return "unknown";
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
async function settleSessionRunState(sessionID, options = {}) {
|
|
3030
|
+
const pollMs = options.pollMs ?? 150;
|
|
3031
|
+
const windowMs = options.windowMs ?? 1500;
|
|
3032
|
+
const started = Date.now();
|
|
3033
|
+
let last = "unknown";
|
|
3034
|
+
for (; ; ) {
|
|
3035
|
+
const state = await fetchSessionRunState(sessionID);
|
|
3036
|
+
if (state === "busy") return "busy";
|
|
3037
|
+
if (state !== "unknown") last = state;
|
|
3038
|
+
if (Date.now() - started >= windowMs) return last;
|
|
3039
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
async function getRuntimeMcpStatus() {
|
|
3043
|
+
const client = opencodeClient;
|
|
3044
|
+
if (!client?.mcp?.status) return void 0;
|
|
3045
|
+
try {
|
|
3046
|
+
const res = await client.mcp.status();
|
|
3047
|
+
const data = res.data;
|
|
3048
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3049
|
+
const out = {};
|
|
3050
|
+
for (const [name, entry] of Object.entries(data)) {
|
|
3051
|
+
if (entry && typeof entry === "object") {
|
|
3052
|
+
const status = entry.status;
|
|
3053
|
+
if (typeof status === "string") out[name] = status;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
return out;
|
|
3057
|
+
} catch (err) {
|
|
3058
|
+
log.warn("failed to fetch opencode MCP runtime status", {
|
|
3059
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3060
|
+
});
|
|
3061
|
+
return void 0;
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
async function fetchOpencodeToolList(provider, model, directory) {
|
|
3065
|
+
const client = opencodeClient;
|
|
3066
|
+
if (!client?.tool?.list) return void 0;
|
|
3067
|
+
try {
|
|
3068
|
+
const res = await client.tool.list({
|
|
3069
|
+
query: { provider, model, ...directory ? { directory } : {} }
|
|
3070
|
+
});
|
|
3071
|
+
const data = res.data;
|
|
3072
|
+
if (!Array.isArray(data)) return void 0;
|
|
3073
|
+
const out = [];
|
|
3074
|
+
for (const entry of data) {
|
|
3075
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3076
|
+
const e = entry;
|
|
3077
|
+
const id = typeof e.id === "string" ? e.id : null;
|
|
3078
|
+
const description = typeof e.description === "string" ? e.description : "";
|
|
3079
|
+
const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
|
|
3080
|
+
if (!id) continue;
|
|
3081
|
+
out.push({ id, description, parameters });
|
|
3082
|
+
}
|
|
3083
|
+
return out;
|
|
3084
|
+
} catch (err) {
|
|
3085
|
+
log.warn("failed to fetch opencode tool list", {
|
|
3086
|
+
provider,
|
|
3087
|
+
model,
|
|
3088
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3089
|
+
});
|
|
3090
|
+
return void 0;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
async function fetchSessionReplies(sessionID) {
|
|
3094
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
3095
|
+
const client = opencodeClient;
|
|
3096
|
+
if (!client?.session?.messages) return void 0;
|
|
3097
|
+
try {
|
|
3098
|
+
const res = await client.session.messages({ path: { id: sessionID } });
|
|
3099
|
+
const data = res.data;
|
|
3100
|
+
if (!Array.isArray(data)) return void 0;
|
|
3101
|
+
const out = [];
|
|
3102
|
+
for (const entry of data) {
|
|
3103
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3104
|
+
const info = entry.info;
|
|
3105
|
+
if (!info || typeof info !== "object") continue;
|
|
3106
|
+
const record = info;
|
|
3107
|
+
const parts = entry.parts;
|
|
3108
|
+
const text = Array.isArray(parts) ? parts.flatMap((part) => {
|
|
3109
|
+
if (!part || typeof part !== "object") return [];
|
|
3110
|
+
const p = part;
|
|
3111
|
+
if (p.type !== "text" || typeof p.text !== "string") return [];
|
|
3112
|
+
return p.synthetic === true ? [] : [p.text];
|
|
3113
|
+
}).join("\n").trim() : "";
|
|
3114
|
+
const error = record.error;
|
|
3115
|
+
const message = error && typeof error === "object" ? typeof error.data?.message === "string" ? error.data.message : typeof error.name === "string" ? error.name : void 0 : void 0;
|
|
3116
|
+
out.push({
|
|
3117
|
+
role: typeof record.role === "string" ? record.role : "",
|
|
3118
|
+
completed: typeof record.time?.completed === "number",
|
|
3119
|
+
error: message,
|
|
3120
|
+
text
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
return out;
|
|
3124
|
+
} catch (err) {
|
|
3125
|
+
log.warn("failed to fetch opencode session messages", {
|
|
3126
|
+
sessionID,
|
|
3127
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3128
|
+
});
|
|
3129
|
+
return void 0;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
async function abortSession(sessionID) {
|
|
3133
|
+
if (!sessionID || sessionID === "default") return false;
|
|
3134
|
+
const client = opencodeClient;
|
|
3135
|
+
if (!client?.session?.abort) return false;
|
|
3136
|
+
try {
|
|
3137
|
+
const res = await client.session.abort({ path: { id: sessionID } });
|
|
3138
|
+
return res.data !== false;
|
|
3139
|
+
} catch (err) {
|
|
3140
|
+
log.warn("failed to abort opencode session", {
|
|
3141
|
+
sessionID,
|
|
3142
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3143
|
+
});
|
|
3144
|
+
return false;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// src/background-tasks.ts
|
|
3149
|
+
var TASK_STATUS_TOOL_NAME = "task_status";
|
|
3150
|
+
var TASK_CANCEL_TOOL_NAME = "task_cancel";
|
|
3151
|
+
var MAX_LEDGERS = 32;
|
|
3152
|
+
var MAX_COLLECTED_PER_SESSION = 64;
|
|
3153
|
+
var collected = /* @__PURE__ */ new Map();
|
|
3154
|
+
var cancelled = /* @__PURE__ */ new Map();
|
|
3155
|
+
function ledgerIn(ledgers2, sessionKey2) {
|
|
3156
|
+
let ids = ledgers2.get(sessionKey2);
|
|
3157
|
+
if (!ids) {
|
|
3158
|
+
ids = /* @__PURE__ */ new Set();
|
|
3159
|
+
ledgers2.set(sessionKey2, ids);
|
|
3160
|
+
while (ledgers2.size > MAX_LEDGERS) {
|
|
3161
|
+
const oldest = ledgers2.keys().next();
|
|
3162
|
+
if (oldest.done) break;
|
|
3163
|
+
ledgers2.delete(oldest.value);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
return ids;
|
|
3167
|
+
}
|
|
3168
|
+
function mark(ledgers2, sessionKey2, taskId) {
|
|
3169
|
+
const ids = ledgerIn(ledgers2, sessionKey2);
|
|
3170
|
+
ids.add(taskId);
|
|
3171
|
+
while (ids.size > MAX_COLLECTED_PER_SESSION) {
|
|
3172
|
+
const oldest = ids.values().next();
|
|
3173
|
+
if (oldest.done) break;
|
|
3174
|
+
ids.delete(oldest.value);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
function markCollected(sessionKey2, taskId) {
|
|
3178
|
+
mark(collected, sessionKey2, taskId);
|
|
3179
|
+
}
|
|
3180
|
+
function hasCollectedBackgroundTask(sessionKey2, taskId) {
|
|
3181
|
+
return collected.get(sessionKey2)?.has(taskId) === true;
|
|
3182
|
+
}
|
|
3183
|
+
function clearBackgroundTasks(sessionKey2) {
|
|
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
|
+
}));
|
|
3201
|
+
}
|
|
3202
|
+
function readTaskId(input) {
|
|
3203
|
+
const raw = input.task_id ?? input.taskId ?? input.sessionID;
|
|
3204
|
+
if (typeof raw !== "string") return null;
|
|
3205
|
+
const id = raw.trim();
|
|
3206
|
+
return id.length > 0 ? id : null;
|
|
3207
|
+
}
|
|
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.';
|
|
3209
|
+
async function guardParent(taskId, callerSessionId) {
|
|
3210
|
+
if (!callerSessionId || callerSessionId === "default") {
|
|
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.`;
|
|
3212
|
+
}
|
|
3213
|
+
const parent = await fetchSessionParentId(taskId);
|
|
3214
|
+
if (parent === callerSessionId) return null;
|
|
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;
|
|
3222
|
+
}
|
|
3223
|
+
function lastAssistantReply(replies) {
|
|
3224
|
+
for (let index = replies.length - 1; index >= 0; index--) {
|
|
3225
|
+
const reply = replies[index];
|
|
3226
|
+
if (reply.role === "assistant") return reply;
|
|
3227
|
+
}
|
|
3228
|
+
return void 0;
|
|
3229
|
+
}
|
|
3230
|
+
async function collectBackgroundTask(input, options) {
|
|
3231
|
+
const taskId = readTaskId(input);
|
|
3232
|
+
if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
|
|
3233
|
+
const refused = await guardParent(taskId, options.callerSessionId);
|
|
3234
|
+
if (refused) return { kind: "error", message: refused };
|
|
3235
|
+
const replies = await fetchSessionReplies(taskId);
|
|
3236
|
+
if (replies === void 0) {
|
|
3237
|
+
return {
|
|
3238
|
+
kind: "error",
|
|
3239
|
+
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.`
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
const runState = await fetchSessionRunState(taskId);
|
|
3243
|
+
if (isBackgroundTaskRunning(runState, replies)) {
|
|
3244
|
+
return {
|
|
3245
|
+
kind: "text",
|
|
3246
|
+
text: `<task id="${taskId}" state="running">
|
|
3247
|
+
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.
|
|
3248
|
+
</task>`
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
const reply = lastAssistantReply(replies);
|
|
3252
|
+
if (!reply) {
|
|
3253
|
+
return {
|
|
3254
|
+
kind: "text",
|
|
3255
|
+
text: `<task id="${taskId}" state="unknown">
|
|
3256
|
+
opencode has no assistant reply for this task yet and it is not running. It may still be starting up.
|
|
3257
|
+
</task>`
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
if (reply.error) {
|
|
3261
|
+
markCollected(options.sessionKey, taskId);
|
|
3262
|
+
return {
|
|
3263
|
+
kind: "text",
|
|
3264
|
+
isError: true,
|
|
3265
|
+
text: `<task id="${taskId}" state="error">
|
|
3266
|
+
${reply.error}
|
|
3267
|
+
</task>`
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
if (hasCollectedBackgroundTask(options.sessionKey, taskId)) {
|
|
3271
|
+
return {
|
|
3272
|
+
kind: "text",
|
|
3273
|
+
text: `<task id="${taskId}" state="completed">
|
|
3274
|
+
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.
|
|
3275
|
+
</task>`
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
markCollected(options.sessionKey, taskId);
|
|
3279
|
+
log.info("collected background subagent result", {
|
|
3280
|
+
sessionKey: options.sessionKey,
|
|
3281
|
+
taskId,
|
|
3282
|
+
textLength: reply.text.length
|
|
3283
|
+
});
|
|
3284
|
+
return {
|
|
3285
|
+
kind: "text",
|
|
3286
|
+
text: `<task id="${taskId}" state="completed">
|
|
3287
|
+
<task_result>
|
|
3288
|
+
${reply.text || "(the subagent produced no text)"}
|
|
3289
|
+
</task_result>
|
|
3290
|
+
</task>`
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
async function cancelBackgroundTask(input, options) {
|
|
3294
|
+
const taskId = readTaskId(input);
|
|
3295
|
+
if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
|
|
3296
|
+
const refused = await guardParent(taskId, options.callerSessionId);
|
|
3297
|
+
if (refused) return { kind: "error", message: refused };
|
|
3298
|
+
const running = isBackgroundTaskRunning(
|
|
3299
|
+
await fetchSessionRunState(taskId),
|
|
3300
|
+
await fetchSessionReplies(taskId) ?? []
|
|
3301
|
+
);
|
|
3302
|
+
const aborted = await abortSession(taskId);
|
|
3303
|
+
if (!aborted) {
|
|
3304
|
+
return {
|
|
3305
|
+
kind: "error",
|
|
3306
|
+
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.`
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
collected.get(options.sessionKey)?.delete(taskId);
|
|
3310
|
+
mark(cancelled, options.sessionKey, taskId);
|
|
3311
|
+
log.info("cancelled background subagent", {
|
|
3312
|
+
sessionKey: options.sessionKey,
|
|
3313
|
+
taskId,
|
|
3314
|
+
wasRunning: running
|
|
3315
|
+
});
|
|
3316
|
+
return {
|
|
3317
|
+
kind: "text",
|
|
3318
|
+
text: `<task id="${taskId}" state="cancelled">
|
|
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>"
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
|
|
2862
3323
|
// src/permission-presets.ts
|
|
2863
3324
|
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
2864
3325
|
"Bash",
|
|
@@ -3395,11 +3856,13 @@ function deleteActiveProcessesForSession(sessionID) {
|
|
|
3395
3856
|
clearCompression(key);
|
|
3396
3857
|
if (!released.includes(key)) released.push(key);
|
|
3397
3858
|
}
|
|
3859
|
+
for (const key of released) clearBackgroundTasks(key);
|
|
3398
3860
|
return released;
|
|
3399
3861
|
}
|
|
3400
3862
|
function killAllActiveProcesses() {
|
|
3401
3863
|
const keys = [...activeProcesses.keys()];
|
|
3402
3864
|
for (const key of keys) deleteActiveProcess(key);
|
|
3865
|
+
for (const key of keys) clearBackgroundTasks(key);
|
|
3403
3866
|
return keys;
|
|
3404
3867
|
}
|
|
3405
3868
|
var processExitCleanupWired = false;
|
|
@@ -3511,6 +3974,7 @@ function invalidateOtherEffortSessions(baseKey, effort) {
|
|
|
3511
3974
|
deleteActiveProcess(key);
|
|
3512
3975
|
deleteClaudeSessionId(key);
|
|
3513
3976
|
clearCompression(key);
|
|
3977
|
+
clearBackgroundTasks(key);
|
|
3514
3978
|
}
|
|
3515
3979
|
}
|
|
3516
3980
|
function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl) {
|
|
@@ -4497,163 +4961,6 @@ function finishBridge(input) {
|
|
|
4497
4961
|
};
|
|
4498
4962
|
}
|
|
4499
4963
|
|
|
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
4964
|
// src/startup-diagnostics.ts
|
|
4658
4965
|
var cachedPluginVersion;
|
|
4659
4966
|
function pluginVersion() {
|
|
@@ -4944,6 +5251,10 @@ function formatDoctorReport(report) {
|
|
|
4944
5251
|
}
|
|
4945
5252
|
}
|
|
4946
5253
|
lines.push("");
|
|
5254
|
+
lines.push("**Background subagents**");
|
|
5255
|
+
lines.push("");
|
|
5256
|
+
lines.push(...formatBackgroundSubagents(report.backgroundSubagents));
|
|
5257
|
+
lines.push("");
|
|
4947
5258
|
lines.push("**Plan usage**");
|
|
4948
5259
|
lines.push("");
|
|
4949
5260
|
switch (report.planUsage.status) {
|
|
@@ -4976,6 +5287,59 @@ function formatDoctorReport(report) {
|
|
|
4976
5287
|
}
|
|
4977
5288
|
return lines.join("\n");
|
|
4978
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
|
+
}
|
|
4979
5343
|
async function checkProxyAuth(url, fetchImpl = fetch, timeoutMs = 3e3) {
|
|
4980
5344
|
let authority;
|
|
4981
5345
|
try {
|
|
@@ -5042,7 +5406,11 @@ async function gatherDoctorReport(options) {
|
|
|
5042
5406
|
proxyServers,
|
|
5043
5407
|
mcpServerErrors: snapshotMcpServerErrors(),
|
|
5044
5408
|
pluginLoadFailures: snapshotPluginLoadFailures(),
|
|
5045
|
-
planUsage
|
|
5409
|
+
planUsage,
|
|
5410
|
+
backgroundSubagents: {
|
|
5411
|
+
gate: snapshotBackgroundSubagentGate(),
|
|
5412
|
+
ledgers: snapshotBackgroundTasks()
|
|
5413
|
+
}
|
|
5046
5414
|
};
|
|
5047
5415
|
}
|
|
5048
5416
|
async function buildDoctorReport(options) {
|
|
@@ -5057,7 +5425,8 @@ async function buildDoctorReport(options) {
|
|
|
5057
5425
|
proxyServers: report.proxyServers.map((server2) => server2.auth.status),
|
|
5058
5426
|
mcpServerErrors: report.mcpServerErrors.length,
|
|
5059
5427
|
pluginLoadFailures: report.pluginLoadFailures.length,
|
|
5060
|
-
planUsage: report.planUsage.status
|
|
5428
|
+
planUsage: report.planUsage.status,
|
|
5429
|
+
backgroundSubagents: report.backgroundSubagents.gate?.supported ?? "not read"
|
|
5061
5430
|
});
|
|
5062
5431
|
return formatDoctorReport(report);
|
|
5063
5432
|
} catch (error) {
|
|
@@ -6545,7 +6914,13 @@ function resolveClaude(cmd = "claude") {
|
|
|
6545
6914
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
6546
6915
|
}
|
|
6547
6916
|
function encodeCwd(cwd) {
|
|
6548
|
-
|
|
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, "-");
|
|
6549
6924
|
}
|
|
6550
6925
|
function interactiveSpawnEnv(opts) {
|
|
6551
6926
|
return {
|
|
@@ -6560,6 +6935,69 @@ function interactiveSpawnEnv(opts) {
|
|
|
6560
6935
|
...opts.effort ? { CLAUDE_CODE_EFFORT_LEVEL: opts.effort } : {}
|
|
6561
6936
|
};
|
|
6562
6937
|
}
|
|
6938
|
+
function usageInputSide(usage) {
|
|
6939
|
+
return (usage?.input_tokens ?? 0) + (usage?.cache_read_input_tokens ?? 0) + (usage?.cache_creation_input_tokens ?? 0);
|
|
6940
|
+
}
|
|
6941
|
+
function isApiCallRecord(rec) {
|
|
6942
|
+
if (!rec || rec.type !== "assistant" || !rec.message) return false;
|
|
6943
|
+
return usageInputSide(rec.message.usage) > 0;
|
|
6944
|
+
}
|
|
6945
|
+
var TurnUsageAccumulator = class {
|
|
6946
|
+
calls = /* @__PURE__ */ new Map();
|
|
6947
|
+
lastKey = null;
|
|
6948
|
+
/** Feed every transcript record of the turn, in file order. */
|
|
6949
|
+
add(rec) {
|
|
6950
|
+
if (!isApiCallRecord(rec)) return;
|
|
6951
|
+
const id = rec.message.id;
|
|
6952
|
+
const key = typeof id === "string" && id.length > 0 ? id : `uuid:${rec.uuid ?? this.calls.size}`;
|
|
6953
|
+
this.calls.set(key, rec.message.usage);
|
|
6954
|
+
this.lastKey = key;
|
|
6955
|
+
}
|
|
6956
|
+
/** Distinct API calls seen, for diagnostics. */
|
|
6957
|
+
get callCount() {
|
|
6958
|
+
return this.calls.size;
|
|
6959
|
+
}
|
|
6960
|
+
/**
|
|
6961
|
+
* The newest real call's usage, verbatim (its own `iterations` included):
|
|
6962
|
+
* the conversation's context occupancy, which is what a finish must report.
|
|
6963
|
+
* See `lastCallContextUsage` in `usage.ts`, the one convention for this.
|
|
6964
|
+
*/
|
|
6965
|
+
get lastCall() {
|
|
6966
|
+
return this.lastKey === null ? null : this.calls.get(this.lastKey) ?? null;
|
|
6967
|
+
}
|
|
6968
|
+
/**
|
|
6969
|
+
* The turn summed over DISTINCT calls: the shape and the meaning of a
|
|
6970
|
+
* headless `result` frame's usage, which is what `total_cost_usd` and
|
|
6971
|
+
* `turnStats` are about. Deliberately carries no `iterations`: that is one
|
|
6972
|
+
* response's server-side field and a sum of calls has no such list, so
|
|
6973
|
+
* `toUsage` falls back to these flat counters, while `lastCall` keeps its
|
|
6974
|
+
* own for `lastCallContextUsage` to read.
|
|
6975
|
+
*/
|
|
6976
|
+
get turnTotal() {
|
|
6977
|
+
if (this.calls.size === 0) return null;
|
|
6978
|
+
const total = {
|
|
6979
|
+
input_tokens: 0,
|
|
6980
|
+
output_tokens: 0,
|
|
6981
|
+
cache_read_input_tokens: 0,
|
|
6982
|
+
cache_creation_input_tokens: 0,
|
|
6983
|
+
cache_creation: {
|
|
6984
|
+
ephemeral_1h_input_tokens: 0,
|
|
6985
|
+
ephemeral_5m_input_tokens: 0
|
|
6986
|
+
},
|
|
6987
|
+
output_tokens_details: { thinking_tokens: 0 }
|
|
6988
|
+
};
|
|
6989
|
+
for (const u of this.calls.values()) {
|
|
6990
|
+
total.input_tokens += u.input_tokens ?? 0;
|
|
6991
|
+
total.output_tokens += u.output_tokens ?? 0;
|
|
6992
|
+
total.cache_read_input_tokens += u.cache_read_input_tokens ?? 0;
|
|
6993
|
+
total.cache_creation_input_tokens += u.cache_creation_input_tokens ?? 0;
|
|
6994
|
+
total.cache_creation.ephemeral_1h_input_tokens += u.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
6995
|
+
total.cache_creation.ephemeral_5m_input_tokens += u.cache_creation?.ephemeral_5m_input_tokens ?? 0;
|
|
6996
|
+
total.output_tokens_details.thinking_tokens += u.output_tokens_details?.thinking_tokens ?? 0;
|
|
6997
|
+
}
|
|
6998
|
+
return total;
|
|
6999
|
+
}
|
|
7000
|
+
};
|
|
6563
7001
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
6564
7002
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6565
7003
|
function resolveConfigDir(configDir) {
|
|
@@ -6754,8 +7192,8 @@ var ClaudeSession = class {
|
|
|
6754
7192
|
this.proc.terminal.write(prompt);
|
|
6755
7193
|
}
|
|
6756
7194
|
await this.submitTurn();
|
|
6757
|
-
const
|
|
6758
|
-
|
|
7195
|
+
const collected2 = [];
|
|
7196
|
+
const usage = new TurnUsageAccumulator();
|
|
6759
7197
|
let stopReason = null;
|
|
6760
7198
|
const deadline = Date.now() + timeout;
|
|
6761
7199
|
while (Date.now() < deadline) {
|
|
@@ -6779,9 +7217,9 @@ var ClaudeSession = class {
|
|
|
6779
7217
|
if (rec.type === "assistant" && rec.message) {
|
|
6780
7218
|
for (const b of rec.message.content ?? []) {
|
|
6781
7219
|
if (b?.type === "text" && typeof b.text === "string")
|
|
6782
|
-
|
|
7220
|
+
collected2.push(b.text);
|
|
6783
7221
|
}
|
|
6784
|
-
|
|
7222
|
+
usage.add(rec);
|
|
6785
7223
|
if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
|
|
6786
7224
|
stopReason = rec.message.stop_reason;
|
|
6787
7225
|
}
|
|
@@ -6793,15 +7231,17 @@ var ClaudeSession = class {
|
|
|
6793
7231
|
if (!stopReason) {
|
|
6794
7232
|
throw new Error(
|
|
6795
7233
|
this.failureMessage(
|
|
6796
|
-
`turn timed out after ${timeout}ms (no terminal assistant record; collected ${
|
|
7234
|
+
`turn timed out after ${timeout}ms (no terminal assistant record; collected ${collected2.length} text block(s))`
|
|
6797
7235
|
)
|
|
6798
7236
|
);
|
|
6799
7237
|
}
|
|
6800
|
-
const
|
|
7238
|
+
const turnTotal = usage.turnTotal;
|
|
7239
|
+
const u = turnTotal ?? {};
|
|
6801
7240
|
return {
|
|
6802
|
-
text:
|
|
7241
|
+
text: collected2.join("\n").trim(),
|
|
6803
7242
|
stopReason,
|
|
6804
|
-
usage:
|
|
7243
|
+
usage: turnTotal,
|
|
7244
|
+
lastCallUsage: usage.lastCall,
|
|
6805
7245
|
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
6806
7246
|
cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
|
|
6807
7247
|
ephemeral1hTokens: u.cache_creation?.ephemeral_1h_input_tokens ?? 0,
|
|
@@ -6814,9 +7254,15 @@ var ClaudeSession = class {
|
|
|
6814
7254
|
/**
|
|
6815
7255
|
* Like ask(), but instead of collecting the reply text it re-emits each NEW
|
|
6816
7256
|
* raw JSONL transcript line via onLine (verbatim) until a terminal
|
|
6817
|
-
* stop_reason.
|
|
6818
|
-
*
|
|
6819
|
-
*
|
|
7257
|
+
* stop_reason. Used by the opencode plugin transport shim, which feeds
|
|
7258
|
+
* these raw lines into the existing stream-json line handler unchanged.
|
|
7259
|
+
*
|
|
7260
|
+
* `usage` is the turn summed over DISTINCT API calls, which is exactly what
|
|
7261
|
+
* a headless `result` frame reports, so the shim can synthesize one and
|
|
7262
|
+
* every downstream consumer (the finish's `lastCallContextUsage`,
|
|
7263
|
+
* `turnStats`) behaves as it does on the headless path. `lastCallUsage` is
|
|
7264
|
+
* the newest real call, returned because a caller with no stream parser
|
|
7265
|
+
* (`askOnce`) has no other way to get the context side.
|
|
6820
7266
|
*/
|
|
6821
7267
|
async tailTurn(prompt, onLine, perTurnTimeoutMs) {
|
|
6822
7268
|
if (this.aborted) throw new Error("aborted");
|
|
@@ -6829,8 +7275,7 @@ var ClaudeSession = class {
|
|
|
6829
7275
|
this.proc.terminal.write(prompt);
|
|
6830
7276
|
}
|
|
6831
7277
|
await this.submitTurn();
|
|
6832
|
-
|
|
6833
|
-
let totalOutput = 0;
|
|
7278
|
+
const usage = new TurnUsageAccumulator();
|
|
6834
7279
|
let stopReason = null;
|
|
6835
7280
|
const deadline = Date.now() + timeout;
|
|
6836
7281
|
while (Date.now() < deadline) {
|
|
@@ -6854,11 +7299,8 @@ var ClaudeSession = class {
|
|
|
6854
7299
|
} catch {
|
|
6855
7300
|
continue;
|
|
6856
7301
|
}
|
|
7302
|
+
usage.add(rec);
|
|
6857
7303
|
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
7304
|
if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
|
|
6863
7305
|
stopReason = rec.message.stop_reason;
|
|
6864
7306
|
}
|
|
@@ -6867,18 +7309,6 @@ var ClaudeSession = class {
|
|
|
6867
7309
|
this.cursor = lastComplete;
|
|
6868
7310
|
if (stopReason) break;
|
|
6869
7311
|
}
|
|
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
7312
|
if (!stopReason) {
|
|
6883
7313
|
throw new Error(
|
|
6884
7314
|
this.failureMessage(
|
|
@@ -6886,7 +7316,12 @@ var ClaudeSession = class {
|
|
|
6886
7316
|
)
|
|
6887
7317
|
);
|
|
6888
7318
|
}
|
|
6889
|
-
return {
|
|
7319
|
+
return {
|
|
7320
|
+
stopReason,
|
|
7321
|
+
usage: usage.turnTotal,
|
|
7322
|
+
lastCallUsage: usage.lastCall,
|
|
7323
|
+
callCount: usage.callCount
|
|
7324
|
+
};
|
|
6890
7325
|
}
|
|
6891
7326
|
dispose() {
|
|
6892
7327
|
if (this.proc) {
|
|
@@ -7006,16 +7441,18 @@ function spawnInteractiveProcess(opts) {
|
|
|
7006
7441
|
if (!startPromise) startPromise = session.start();
|
|
7007
7442
|
return startPromise;
|
|
7008
7443
|
};
|
|
7009
|
-
const emitResult = (
|
|
7444
|
+
const emitResult = (opts2) => {
|
|
7010
7445
|
lineEmitter.emit(
|
|
7011
7446
|
"line",
|
|
7012
7447
|
JSON.stringify({
|
|
7013
7448
|
type: "result",
|
|
7014
|
-
subtype,
|
|
7015
|
-
is_error: isError,
|
|
7016
|
-
|
|
7449
|
+
subtype: opts2.subtype,
|
|
7450
|
+
is_error: opts2.isError,
|
|
7451
|
+
stop_reason: opts2.stopReason,
|
|
7452
|
+
terminal_reason: opts2.terminalReason,
|
|
7453
|
+
result: opts2.result,
|
|
7017
7454
|
session_id: session.sessionId,
|
|
7018
|
-
usage: usage ?? {},
|
|
7455
|
+
usage: opts2.usage ?? {},
|
|
7019
7456
|
total_cost_usd: null,
|
|
7020
7457
|
duration_ms: 0
|
|
7021
7458
|
})
|
|
@@ -7025,24 +7462,37 @@ function spawnInteractiveProcess(opts) {
|
|
|
7025
7462
|
void (async () => {
|
|
7026
7463
|
try {
|
|
7027
7464
|
await ensureStarted();
|
|
7028
|
-
const { stopReason, usage } = await session.tailTurn(userMsg, (raw) => {
|
|
7465
|
+
const { stopReason, usage, lastCallUsage, callCount } = await session.tailTurn(userMsg, (raw) => {
|
|
7029
7466
|
lineEmitter.emit("line", raw);
|
|
7030
7467
|
});
|
|
7468
|
+
log.info("interactive turn usage", {
|
|
7469
|
+
apiCalls: callCount,
|
|
7470
|
+
turnOutputTokens: usage?.output_tokens,
|
|
7471
|
+
turnInputTokens: usage?.input_tokens,
|
|
7472
|
+
turnCacheReadTokens: usage?.cache_read_input_tokens,
|
|
7473
|
+
lastCallInputTokens: lastCallUsage?.input_tokens,
|
|
7474
|
+
lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
|
|
7475
|
+
lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens
|
|
7476
|
+
});
|
|
7031
7477
|
const timedOut = !stopReason;
|
|
7032
|
-
emitResult(
|
|
7033
|
-
timedOut ? "error_during_execution" :
|
|
7034
|
-
timedOut,
|
|
7035
|
-
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,
|
|
7036
7484
|
usage
|
|
7037
|
-
);
|
|
7485
|
+
});
|
|
7038
7486
|
} catch (err) {
|
|
7039
7487
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
7040
7488
|
log.error("interactive turn failed", { error: e.message });
|
|
7041
|
-
emitResult(
|
|
7042
|
-
"error_during_execution",
|
|
7043
|
-
true,
|
|
7044
|
-
|
|
7045
|
-
|
|
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
|
+
});
|
|
7046
7496
|
if (errorHandlers.size > 0) {
|
|
7047
7497
|
for (const h of errorHandlers) h(e);
|
|
7048
7498
|
} else {
|
|
@@ -7143,6 +7593,14 @@ Subagent dispatch in this environment goes through exactly two tools: \`mcp__ope
|
|
|
7143
7593
|
- 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
7594
|
- 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
7595
|
- 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.`;
|
|
7596
|
+
var BACKGROUND_SUBAGENT_HINT = `## Background subagents
|
|
7597
|
+
|
|
7598
|
+
This opencode host runs background subagents, so \`mcp__opencode_proxy__task\` takes \`background: true\`.
|
|
7599
|
+
|
|
7600
|
+
- 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.
|
|
7601
|
+
- 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.
|
|
7602
|
+
- 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.
|
|
7603
|
+
- 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
7604
|
var QUESTION_PROXY_HINT = `## Asking the operator questions
|
|
7147
7605
|
|
|
7148
7606
|
Structured questions in this environment go through exactly one tool: \`mcp__opencode_proxy__question\`.
|
|
@@ -7714,9 +8172,11 @@ async function fetchLiveToolInfo(config, modelId) {
|
|
|
7714
8172
|
config.cwd
|
|
7715
8173
|
);
|
|
7716
8174
|
const question = items?.find((item) => item.id === "question");
|
|
8175
|
+
const task = items?.find((item) => item.id === "task");
|
|
7717
8176
|
return {
|
|
7718
8177
|
resolved: items !== void 0,
|
|
7719
|
-
taskDescription:
|
|
8178
|
+
taskDescription: task?.description,
|
|
8179
|
+
taskParameters: task?.parameters,
|
|
7720
8180
|
questionDescription: question?.description,
|
|
7721
8181
|
hasQuestion: !!question,
|
|
7722
8182
|
items
|
|
@@ -7759,9 +8219,14 @@ async function resolvePlanModeQuestion(config, compactionMode, loadLiveToolInfo)
|
|
|
7759
8219
|
}
|
|
7760
8220
|
return active;
|
|
7761
8221
|
}
|
|
7762
|
-
async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCompress) {
|
|
8222
|
+
async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCompress, callerSessionId) {
|
|
7763
8223
|
const timeoutOverrides = config.proxyToolTimeoutMs;
|
|
7764
8224
|
const interceptors = /* @__PURE__ */ new Map();
|
|
8225
|
+
if (tools.some((t) => t.name === TASK_STATUS_TOOL_NAME)) {
|
|
8226
|
+
const opts = { sessionKey: sessionKeyForCalls, callerSessionId };
|
|
8227
|
+
interceptors.set(TASK_STATUS_TOOL_NAME, (input) => collectBackgroundTask(input, opts));
|
|
8228
|
+
interceptors.set(TASK_CANCEL_TOOL_NAME, (input) => cancelBackgroundTask(input, opts));
|
|
8229
|
+
}
|
|
7765
8230
|
if (interceptCompress && tools.some((t) => t.name === "compress")) {
|
|
7766
8231
|
interceptors.set("compress", (input) => {
|
|
7767
8232
|
const summary = typeof input.summary === "string" ? input.summary.trim() : "";
|
|
@@ -9379,12 +9844,13 @@ var ClaudeCodeLanguageModel = class {
|
|
|
9379
9844
|
* Create a proxy MCP server for a single active Claude process/session.
|
|
9380
9845
|
* The process lifecycle owns the server lifecycle via session-manager.
|
|
9381
9846
|
*/
|
|
9382
|
-
async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress) {
|
|
9847
|
+
async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress, callerSessionId) {
|
|
9383
9848
|
return ensureProxyServer(
|
|
9384
9849
|
this.config,
|
|
9385
9850
|
tools,
|
|
9386
9851
|
sessionKeyForCalls,
|
|
9387
|
-
interceptCompress
|
|
9852
|
+
interceptCompress,
|
|
9853
|
+
callerSessionId
|
|
9388
9854
|
);
|
|
9389
9855
|
}
|
|
9390
9856
|
/**
|
|
@@ -10167,10 +10633,12 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10167
10633
|
const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
|
|
10168
10634
|
resolved: false,
|
|
10169
10635
|
taskDescription: void 0,
|
|
10636
|
+
taskParameters: void 0,
|
|
10170
10637
|
questionDescription: void 0,
|
|
10171
10638
|
hasQuestion: false
|
|
10172
10639
|
};
|
|
10173
10640
|
let enrichedProxy = resolvedProxy;
|
|
10641
|
+
let backgroundSubagentsSupported = false;
|
|
10174
10642
|
if (enrichedProxy && taskProxyEnabled) {
|
|
10175
10643
|
enrichedProxy = overlayTaskProxyDescription(
|
|
10176
10644
|
enrichedProxy,
|
|
@@ -10185,6 +10653,27 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10185
10653
|
)
|
|
10186
10654
|
)
|
|
10187
10655
|
});
|
|
10656
|
+
const hostDialect = self.config.hostApi === "v2" ? "v2" : "v1";
|
|
10657
|
+
backgroundSubagentsSupported = liveTaskSupportsBackground(
|
|
10658
|
+
liveToolInfo.taskParameters,
|
|
10659
|
+
hostDialect
|
|
10660
|
+
);
|
|
10661
|
+
enrichedProxy = applyBackgroundSubagentSupport(
|
|
10662
|
+
enrichedProxy,
|
|
10663
|
+
backgroundSubagentsSupported,
|
|
10664
|
+
hostDialect
|
|
10665
|
+
);
|
|
10666
|
+
recordBackgroundSubagentGate({
|
|
10667
|
+
supported: backgroundSubagentsSupported,
|
|
10668
|
+
hostApi: hostDialect,
|
|
10669
|
+
registryResolved: liveToolInfo.resolved
|
|
10670
|
+
});
|
|
10671
|
+
log.info("background subagent gate", {
|
|
10672
|
+
supported: backgroundSubagentsSupported,
|
|
10673
|
+
registryResolved: liveToolInfo.resolved,
|
|
10674
|
+
hostApi: hostDialect,
|
|
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"
|
|
10676
|
+
});
|
|
10188
10677
|
}
|
|
10189
10678
|
if (enrichedProxy && questionProxyEnabled) {
|
|
10190
10679
|
enrichedProxy = overlayQuestionProxyDescription(
|
|
@@ -10225,7 +10714,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10225
10714
|
state.proxyServer = await self.ensureProxyServer(
|
|
10226
10715
|
combinedProxyTools,
|
|
10227
10716
|
sk,
|
|
10228
|
-
pluginCompressEnabled
|
|
10717
|
+
pluginCompressEnabled,
|
|
10718
|
+
affinity
|
|
10229
10719
|
);
|
|
10230
10720
|
}
|
|
10231
10721
|
const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
|
|
@@ -10246,6 +10736,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10246
10736
|
[
|
|
10247
10737
|
...extractSystemMessages(options.prompt),
|
|
10248
10738
|
...taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
|
|
10739
|
+
...backgroundSubagentsSupported ? [BACKGROUND_SUBAGENT_HINT] : [],
|
|
10249
10740
|
...questionProxyActive ? [QUESTION_PROXY_HINT] : []
|
|
10250
10741
|
],
|
|
10251
10742
|
{
|
|
@@ -10825,7 +11316,7 @@ import {
|
|
|
10825
11316
|
existsSync as existsSync3,
|
|
10826
11317
|
mkdirSync as mkdirSync4,
|
|
10827
11318
|
readFileSync as readFileSync6,
|
|
10828
|
-
realpathSync as
|
|
11319
|
+
realpathSync as realpathSync3,
|
|
10829
11320
|
rmSync as rmSync3,
|
|
10830
11321
|
writeFileSync as writeFileSync5
|
|
10831
11322
|
} from "fs";
|
|
@@ -10864,7 +11355,7 @@ function userIntendsToUseUnscoped() {
|
|
|
10864
11355
|
function ourLoadedDir() {
|
|
10865
11356
|
try {
|
|
10866
11357
|
const filePath = fileURLToPath3(import.meta.url);
|
|
10867
|
-
return
|
|
11358
|
+
return realpathSync3(resolve5(filePath, "..", ".."));
|
|
10868
11359
|
} catch {
|
|
10869
11360
|
return null;
|
|
10870
11361
|
}
|
|
@@ -10933,7 +11424,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
10933
11424
|
if (!existsSync3(stalePath)) return;
|
|
10934
11425
|
let realStalePath = stalePath;
|
|
10935
11426
|
try {
|
|
10936
|
-
realStalePath =
|
|
11427
|
+
realStalePath = realpathSync3(stalePath);
|
|
10937
11428
|
} catch {
|
|
10938
11429
|
}
|
|
10939
11430
|
if (ourDir && realStalePath === ourDir) return;
|
|
@@ -11006,6 +11497,30 @@ function toV1Session(info) {
|
|
|
11006
11497
|
const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path9.resolve(root, session.subpath) : root : void 0;
|
|
11007
11498
|
return { ...info, directory, parentID: session.parentID };
|
|
11008
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
|
+
}
|
|
11009
11524
|
function createV1ClientShim(ctx) {
|
|
11010
11525
|
const client = {};
|
|
11011
11526
|
const mcpList = ctx.mcp?.list;
|
|
@@ -11058,13 +11573,27 @@ ${agentList}` : subagent.description;
|
|
|
11058
11573
|
};
|
|
11059
11574
|
}
|
|
11060
11575
|
const sessionGet = ctx.session?.get;
|
|
11576
|
+
const sessionContext = ctx.session?.context;
|
|
11577
|
+
const sessionInterrupt = ctx.session?.interrupt;
|
|
11578
|
+
const session = {};
|
|
11061
11579
|
if (typeof sessionGet === "function") {
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
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 };
|
|
11066
11594
|
};
|
|
11067
11595
|
}
|
|
11596
|
+
if (Object.keys(session).length > 0) client.session = session;
|
|
11068
11597
|
return client;
|
|
11069
11598
|
}
|
|
11070
11599
|
|