agentbox-sdk 0.1.503 → 0.1.511

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.
@@ -15,11 +15,12 @@ import {
15
15
  debugRelay,
16
16
  debugRuntime,
17
17
  debugSetup,
18
+ getAvailablePort,
18
19
  linesFromTextChunks,
19
20
  sleep,
20
21
  time,
21
22
  waitFor
22
- } from "./chunk-AVXJMCBC.js";
23
+ } from "./chunk-MTQ2S46C.js";
23
24
  import {
24
25
  shellQuote
25
26
  } from "./chunk-NSJM57Z4.js";
@@ -29,16 +30,86 @@ import {
29
30
  } from "./chunk-GOFJNFAD.js";
30
31
 
31
32
  // src/agents/Agent.ts
32
- import { randomUUID as randomUUID2 } from "crypto";
33
+ import { randomUUID as randomUUID3 } from "crypto";
34
+ import path11 from "path";
33
35
 
34
36
  // src/agents/providers/claude-code.ts
35
37
  import { randomUUID } from "crypto";
38
+
39
+ // src/agents/transports/spawn.ts
40
+ import { spawn } from "child_process";
41
+ import { createInterface } from "readline";
42
+ function spawnCommand(options) {
43
+ const child = spawn(options.command, options.args ?? [], {
44
+ cwd: options.cwd,
45
+ env: options.env,
46
+ stdio: "pipe",
47
+ shell: process.platform === "win32",
48
+ windowsHide: true,
49
+ detached: options.processGroup === true && process.platform !== "win32"
50
+ });
51
+ const exitPromise = new Promise((resolve, reject) => {
52
+ child.once("error", reject);
53
+ child.once("close", (code) => resolve(code ?? 0));
54
+ });
55
+ void exitPromise.catch(() => void 0);
56
+ let killPromise;
57
+ const signalProcess = (signal) => {
58
+ try {
59
+ if (options.processGroup && process.platform !== "win32" && child.pid) process.kill(-child.pid, signal);
60
+ else child.kill(signal);
61
+ } catch (error) {
62
+ if (error.code !== "ESRCH") throw error;
63
+ }
64
+ };
65
+ const waitForExit = async (timeoutMs) => {
66
+ let timer;
67
+ try {
68
+ return await Promise.race([
69
+ exitPromise.then(() => true, () => true),
70
+ new Promise((resolve) => {
71
+ timer = setTimeout(() => resolve(false), timeoutMs);
72
+ })
73
+ ]);
74
+ } finally {
75
+ if (timer) clearTimeout(timer);
76
+ }
77
+ };
78
+ return {
79
+ child,
80
+ wait: () => exitPromise,
81
+ kill: (signal = "SIGTERM") => killPromise ??= (async () => {
82
+ signalProcess(signal);
83
+ if (await waitForExit(options.terminationTimeoutMs ?? 3e3)) {
84
+ if (options.processGroup && process.platform !== "win32") signalProcess("SIGKILL");
85
+ return;
86
+ }
87
+ signalProcess("SIGKILL");
88
+ if (!await waitForExit(3e3)) throw new Error("The owned agent process did not stop");
89
+ })()
90
+ };
91
+ }
92
+ async function* linesFromNodeStream(stream) {
93
+ const lineReader = createInterface({ input: stream });
94
+ try {
95
+ for await (const line of lineReader) {
96
+ yield line;
97
+ }
98
+ } finally {
99
+ lineReader.close();
100
+ }
101
+ }
102
+
103
+ // src/agents/providers/claude-code.ts
36
104
  import path8 from "path";
37
105
 
38
106
  // src/agents/approval.ts
39
107
  function getApprovalMode(options) {
40
108
  return options.approvalMode ?? "auto";
41
109
  }
110
+ function hasInteractiveQuestions(options) {
111
+ return options.interactiveQuestions ?? isInteractiveApproval(options);
112
+ }
42
113
  function isInteractiveApproval(options) {
43
114
  return getApprovalMode(options) === "interactive";
44
115
  }
@@ -49,6 +120,58 @@ function shouldAutoApproveClaudeTools(options) {
49
120
  return !isInteractiveApproval(options);
50
121
  }
51
122
 
123
+ // src/agents/questions.ts
124
+ function record(value) {
125
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid agent question");
126
+ return value;
127
+ }
128
+ function text(value, max) {
129
+ if (typeof value !== "string" || !value.trim() || value.length > max) throw new Error("Invalid agent question text");
130
+ return value;
131
+ }
132
+ function normalizeUserQuestions(provider, input) {
133
+ const questions = record(input).questions;
134
+ if (!Array.isArray(questions) || !questions.length || questions.length > 10) throw new Error("Unsupported agent question count");
135
+ return questions.map((value, index) => {
136
+ const item = record(value);
137
+ if (item.isSecret === true) throw new Error("Secret questions cannot be answered through task history");
138
+ const options = item.options ?? [];
139
+ if (!Array.isArray(options) || options.length > 30) throw new Error("Unsupported agent question options");
140
+ const result = {
141
+ id: String(index),
142
+ question: text(item.question, 1e4),
143
+ ...item.header ? { header: text(item.header, 200) } : {},
144
+ options: options.map((value2) => {
145
+ const option = record(value2);
146
+ return { label: text(option.label, 1e3), ...option.description ? { description: text(option.description, 1e4) } : {} };
147
+ }),
148
+ multiple: provider === "claude-code" ? item.multiSelect === true : provider === "open-code" && item.multiple === true,
149
+ allowCustom: provider === "open-code" ? item.custom !== false : true
150
+ };
151
+ if (new Set(result.options.map((option) => option.label)).size !== result.options.length) throw new Error("Duplicate agent question options");
152
+ if (!result.options.length && !result.allowCustom) throw new Error("Agent question has no available answers");
153
+ return result;
154
+ });
155
+ }
156
+ function validateUserAnswers(questions, answers) {
157
+ if (!questions?.length || !answers || answers.length !== questions.length || new Set(answers.map((answer) => answer.questionId)).size !== answers.length) throw new Error("Answer each agent question once");
158
+ return questions.map((question) => {
159
+ const answer = answers.find((answer2) => answer2.questionId === question.id);
160
+ if (!answer || !Array.isArray(answer.values) || !answer.values.length || answer.values.length > (question.multiple ? 30 : 1)) throw new Error("Choose an answer for each question");
161
+ if (new Set(answer.values).size !== answer.values.length || answer.values.some((value) => typeof value !== "string" || !value.trim() || value.length > 1e4 || !question.allowCustom && !question.options.some((option) => option.label === value))) throw new Error("Invalid answer to agent question");
162
+ return { questionId: question.id, values: [...answer.values] };
163
+ });
164
+ }
165
+ function questionReply(provider, input, answers) {
166
+ const normalized = normalizeUserQuestions(provider, input);
167
+ const ordered = validateUserAnswers(normalized, answers);
168
+ if (provider === "open-code") return ordered.map((answer) => answer.values);
169
+ const original = record(input).questions;
170
+ const keys = original.map((item) => text(record(item)[provider === "codex" ? "id" : "question"], 1e4));
171
+ if (new Set(keys).size !== keys.length) throw new Error("Duplicate agent question IDs");
172
+ return Object.fromEntries(keys.map((key, index) => [key, provider === "codex" ? { answers: ordered[index].values } : ordered[index].values.join(", ")]));
173
+ }
174
+
52
175
  // src/agents/input.ts
53
176
  import { readFile } from "fs/promises";
54
177
  import path from "path";
@@ -894,64 +1017,16 @@ function buildCodexConfigToml(opts = {}) {
894
1017
  import { mkdir, chmod, rm, writeFile } from "fs/promises";
895
1018
  import os from "os";
896
1019
  import path4 from "path";
897
-
898
- // src/agents/transports/spawn.ts
899
- import { spawn } from "child_process";
900
- import { createInterface } from "readline";
901
- function spawnCommand(options) {
902
- const child = spawn(options.command, options.args ?? [], {
903
- cwd: options.cwd,
904
- env: options.env,
905
- stdio: "pipe",
906
- shell: process.platform === "win32",
907
- windowsHide: true
908
- });
909
- const exitPromise = new Promise((resolve, reject) => {
910
- child.once("error", reject);
911
- child.once("close", (code) => resolve(code ?? 0));
912
- });
913
- return {
914
- child,
915
- wait: () => exitPromise,
916
- kill: async (signal = "SIGTERM") => {
917
- child.kill(signal);
918
- await exitPromise.catch(() => void 0);
919
- }
920
- };
921
- }
922
- async function waitForHttpReady(url, options) {
923
- await waitFor(
924
- async () => {
925
- try {
926
- const response = await fetch(url, options?.init);
927
- return response.ok;
928
- } catch {
929
- return false;
930
- }
931
- },
932
- {
933
- timeoutMs: options?.timeoutMs,
934
- intervalMs: options?.intervalMs
935
- }
936
- );
937
- }
938
- async function* linesFromNodeStream(stream) {
939
- const lineReader = createInterface({ input: stream });
940
- try {
941
- for await (const line of lineReader) {
942
- yield line;
943
- }
944
- } finally {
945
- lineReader.close();
946
- }
947
- }
948
-
949
- // src/agents/config/setup.ts
950
1020
  function shortLabel(command) {
951
1021
  const oneLine = command.replace(/\s+/g, " ").trim();
952
1022
  return oneLine.length > 60 ? `${oneLine.slice(0, 60)}\u2026` : oneLine;
953
1023
  }
954
- function agentboxRoot(provider, hasSandbox = true) {
1024
+ function agentboxRoot(provider, hasSandbox = true, stateDirectory) {
1025
+ if (stateDirectory !== void 0) {
1026
+ if (hasSandbox) throw new Error("stateDirectory is only supported for host execution.");
1027
+ if (!path4.isAbsolute(stateDirectory)) throw new Error("stateDirectory must be an absolute path.");
1028
+ return path4.join(stateDirectory, provider);
1029
+ }
955
1030
  return hasSandbox ? `/tmp/agentbox/${provider}` : path4.join(os.tmpdir(), `agentbox-${provider}`);
956
1031
  }
957
1032
  function getAgentLayout(rootDir) {
@@ -1004,9 +1079,9 @@ var HostSetupTarget = class {
1004
1079
  async () => {
1005
1080
  await Promise.all(
1006
1081
  files.map(async (entry) => {
1007
- await mkdir(path4.dirname(entry.path), { recursive: true });
1082
+ await mkdir(path4.dirname(entry.path), { recursive: true, mode: 448 });
1008
1083
  const content = typeof entry.content === "string" ? entry.content : entry.content;
1009
- await writeFile(entry.path, content);
1084
+ await writeFile(entry.path, content, { mode: entry.mode ?? 384 });
1010
1085
  if (entry.mode && (entry.mode & 73) !== 0) {
1011
1086
  await chmod(entry.path, entry.mode);
1012
1087
  }
@@ -1167,12 +1242,13 @@ async function createSetupTarget(provider, setupId, options) {
1167
1242
  return time(debugRuntime, `createSetupTarget ${provider}`, async () => {
1168
1243
  void setupId;
1169
1244
  const layout = getAgentLayout(
1170
- agentboxRoot(provider, Boolean(options.sandbox))
1245
+ agentboxRoot(provider, Boolean(options.sandbox), options.stateDirectory)
1171
1246
  );
1172
1247
  if (options.sandbox) {
1173
1248
  return new SandboxSetupTarget(provider, layout, options);
1174
1249
  }
1175
- await mkdir(layout.homeDir, { recursive: true });
1250
+ await mkdir(layout.homeDir, { recursive: true, mode: 448 });
1251
+ await chmod(layout.homeDir, 448);
1176
1252
  await mkdir(layout.xdgConfigHome, { recursive: true });
1177
1253
  await mkdir(layout.agentsDir, { recursive: true });
1178
1254
  await mkdir(layout.claudeDir, { recursive: true });
@@ -1749,8 +1825,203 @@ function extractOpenCodeCostData(events) {
1749
1825
  }) : null;
1750
1826
  }
1751
1827
 
1828
+ // src/agents/background-tasks.ts
1829
+ var DEFAULT_BACKGROUND_TASK_TIMEOUT_MS = 30 * 6e4;
1830
+ var BACKGROUND_TASK_GRACE_MS = 15e3;
1831
+ var CLI_BACKGROUND_WAIT_CEILING_ENV = "CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS";
1832
+ var CLI_BACKGROUND_WAIT_CEILING_MS = 3e4;
1833
+ function resolveBackgroundTaskTimeoutMs(value) {
1834
+ if (value === void 0) return DEFAULT_BACKGROUND_TASK_TIMEOUT_MS;
1835
+ if (Number.isNaN(value) || value < 0) {
1836
+ throw new Error(
1837
+ "backgroundTaskTimeoutMs must be a non-negative number (Infinity waits forever)."
1838
+ );
1839
+ }
1840
+ return value;
1841
+ }
1842
+ function applyCliBackgroundWaitCeiling(env) {
1843
+ env[CLI_BACKGROUND_WAIT_CEILING_ENV] ??= String(
1844
+ CLI_BACKGROUND_WAIT_CEILING_MS
1845
+ );
1846
+ }
1847
+ function asRecord2(value) {
1848
+ return value !== null && typeof value === "object" ? value : void 0;
1849
+ }
1850
+ function asArray(value) {
1851
+ return Array.isArray(value) ? value : [];
1852
+ }
1853
+ var SCHEDULE_TOOLS = /* @__PURE__ */ new Set(["CronCreate", "ScheduleWakeup"]);
1854
+ var DONE_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "killed"]);
1855
+ var BackgroundTaskTracker = class {
1856
+ tasks = /* @__PURE__ */ new Map();
1857
+ wakeups = /* @__PURE__ */ new Map();
1858
+ // tool_use seen, tool_result not yet: a failed schedule adds nothing.
1859
+ pendingWakeups = /* @__PURE__ */ new Map();
1860
+ afterResult = false;
1861
+ seenBackgroundWork = false;
1862
+ liveTasks() {
1863
+ return [...this.tasks.values(), ...this.wakeups.values()];
1864
+ }
1865
+ /**
1866
+ * True once any background task or scheduled wakeup was live in this run.
1867
+ * The CLI queues a wake-up for every task that finishes and delivers it as
1868
+ * a new turn once the model is idle — including tasks that finished
1869
+ * mid-turn, whose queued turn starts right after that turn's `result`
1870
+ * with nothing live and nothing observable in between. After background
1871
+ * work has been seen, a `result` is therefore never the end of the run on
1872
+ * its own; only the grace passing without a new turn is.
1873
+ */
1874
+ hasSeenBackgroundWork() {
1875
+ return this.seenBackgroundWork;
1876
+ }
1877
+ /** Feed one SDKMessage. Returns true when it started a follow-up turn. */
1878
+ ingest(message) {
1879
+ const m = asRecord2(message);
1880
+ if (!m) return false;
1881
+ if (m.type === "result") {
1882
+ this.afterResult = true;
1883
+ return false;
1884
+ }
1885
+ if (m.type === "system") return this.ingestSystem(m);
1886
+ if (m.type === "command_lifecycle") {
1887
+ if (m.state !== "started") return false;
1888
+ this.wakeups.clear();
1889
+ return this.startTurn();
1890
+ }
1891
+ if (m.parent_tool_use_id) return false;
1892
+ if (m.type === "assistant") {
1893
+ const started = this.startTurn();
1894
+ this.ingestToolUses(m);
1895
+ return started;
1896
+ }
1897
+ if (m.type === "user") {
1898
+ this.ingestToolResults(m);
1899
+ return false;
1900
+ }
1901
+ if (m.type === "stream_event")
1902
+ return asRecord2(m.event)?.type === "message_start" && this.startTurn();
1903
+ return false;
1904
+ }
1905
+ startTurn() {
1906
+ if (!this.afterResult) return false;
1907
+ this.afterResult = false;
1908
+ return true;
1909
+ }
1910
+ ingestSystem(m) {
1911
+ const id = String(m.task_id ?? "");
1912
+ switch (m.subtype) {
1913
+ case "background_tasks_changed":
1914
+ this.tasks.clear();
1915
+ for (const entry of asArray(m.tasks)) {
1916
+ const task = asRecord2(entry);
1917
+ if (task) this.addTask(task);
1918
+ }
1919
+ return false;
1920
+ case "task_started":
1921
+ if (m.is_backgrounded === true && !m.owned_by_subagent) this.addTask(m);
1922
+ return false;
1923
+ case "task_notification":
1924
+ this.tasks.delete(id);
1925
+ return false;
1926
+ case "task_updated":
1927
+ if (DONE_STATUSES.has(String(asRecord2(m.patch)?.status)))
1928
+ this.tasks.delete(id);
1929
+ return false;
1930
+ case "init":
1931
+ return this.startTurn();
1932
+ default:
1933
+ return false;
1934
+ }
1935
+ }
1936
+ addTask(task) {
1937
+ const id = String(task.task_id ?? "");
1938
+ if (!id) return;
1939
+ this.seenBackgroundWork = true;
1940
+ this.tasks.set(id, {
1941
+ id,
1942
+ type: String(task.task_type ?? "task"),
1943
+ description: String(task.description ?? "")
1944
+ });
1945
+ }
1946
+ ingestToolUses(m) {
1947
+ for (const entry of asArray(asRecord2(m.message)?.content)) {
1948
+ const block = asRecord2(entry);
1949
+ if (block?.type !== "tool_use") continue;
1950
+ const name = String(block.name ?? "");
1951
+ const input = asRecord2(block.input) ?? {};
1952
+ if (name === "CronDelete" || name === "ScheduleWakeup" && input.stop === true) {
1953
+ this.wakeups.clear();
1954
+ continue;
1955
+ }
1956
+ if (!SCHEDULE_TOOLS.has(name)) continue;
1957
+ const id = String(block.id ?? "");
1958
+ if (!id) continue;
1959
+ this.pendingWakeups.set(id, {
1960
+ id,
1961
+ type: "scheduled_wakeup",
1962
+ description: String(input.prompt ?? input.cron ?? name)
1963
+ });
1964
+ }
1965
+ }
1966
+ ingestToolResults(m) {
1967
+ for (const entry of asArray(asRecord2(m.message)?.content)) {
1968
+ const block = asRecord2(entry);
1969
+ if (block?.type !== "tool_result") continue;
1970
+ const id = String(block.tool_use_id ?? "");
1971
+ const pending = this.pendingWakeups.get(id);
1972
+ if (!pending) continue;
1973
+ this.pendingWakeups.delete(id);
1974
+ if (block.is_error) continue;
1975
+ this.seenBackgroundWork = true;
1976
+ this.wakeups.set(id, pending);
1977
+ }
1978
+ }
1979
+ };
1980
+ var MAX_TIMER_MS = 2 ** 31 - 1;
1981
+ var STOP_TASKS_TIMEOUT_MS = 5e3;
1982
+ function withTimeout(promise, ms) {
1983
+ let timer;
1984
+ const timeout = new Promise((resolve) => {
1985
+ timer = setTimeout(() => resolve(void 0), ms);
1986
+ });
1987
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
1988
+ }
1989
+ var BackgroundWait = class {
1990
+ constructor(graceMs, ceilingMs) {
1991
+ this.graceMs = graceMs;
1992
+ this.expired = new Promise((resolve) => {
1993
+ this.expire = resolve;
1994
+ });
1995
+ if (Number.isFinite(ceilingMs)) {
1996
+ this.ceiling = setTimeout(() => this.expire("ceiling"), Math.min(ceilingMs, MAX_TIMER_MS));
1997
+ }
1998
+ }
1999
+ graceMs;
2000
+ expired;
2001
+ expire;
2002
+ grace;
2003
+ ceiling;
2004
+ startedAt = Date.now();
2005
+ /** Arm the grace timer while nothing is live; disarm it once a task appears. */
2006
+ setIdle(idle) {
2007
+ if (!idle) {
2008
+ clearTimeout(this.grace);
2009
+ this.grace = void 0;
2010
+ return;
2011
+ }
2012
+ this.grace ??= setTimeout(() => this.expire("grace"), this.graceMs);
2013
+ }
2014
+ elapsedMs() {
2015
+ return Date.now() - this.startedAt;
2016
+ }
2017
+ clear() {
2018
+ clearTimeout(this.grace);
2019
+ clearTimeout(this.ceiling);
2020
+ }
2021
+ };
2022
+
1752
2023
  // src/agents/providers/claude-code.ts
1753
- var DAEMON_PROTOCOL_VERSION = "3";
2024
+ var DAEMON_PROTOCOL_VERSION = "5";
1754
2025
  var DAEMON_PORT = 43180;
1755
2026
  var DAEMON_PATH = "/tmp/agentbox/claude-code/daemon.mjs";
1756
2027
  var DAEMON_LOG_PATH = "/tmp/agentbox/claude-code/daemon.log";
@@ -1760,7 +2031,7 @@ var DAEMON_READY_TIMEOUT_MS = 3e4;
1760
2031
  var DAEMON_READY_POLL_INTERVAL_MS = 250;
1761
2032
  function claudeConfigDir(options) {
1762
2033
  return path8.join(
1763
- agentboxRoot(AgentProvider.ClaudeCode, Boolean(options.sandbox)),
2034
+ agentboxRoot(AgentProvider.ClaudeCode, Boolean(options.sandbox), options.stateDirectory),
1764
2035
  ".claude"
1765
2036
  );
1766
2037
  }
@@ -1768,7 +2039,7 @@ function buildClaudeQueryOptions(params) {
1768
2039
  const provider = params.request.options.provider;
1769
2040
  const run = params.request.run;
1770
2041
  const extraArgs = {
1771
- "mcp-config": params.mcpConfigPath
2042
+ ...params.mcpConfigPath ? { "mcp-config": params.mcpConfigPath } : {}
1772
2043
  };
1773
2044
  for (const arg of provider?.args ?? []) {
1774
2045
  if (typeof arg !== "string") continue;
@@ -1785,7 +2056,11 @@ function buildClaudeQueryOptions(params) {
1785
2056
  cwd: params.cwd ?? params.request.options.cwd,
1786
2057
  env: params.env,
1787
2058
  pathToClaudeCodeExecutable: provider?.binary ?? "claude",
1788
- settings: params.settingsPath,
2059
+ ...params.settingsPath ? { settings: params.settingsPath } : {},
2060
+ ...params.request.options.configuration === "native" ? {
2061
+ settingSources: ["user", "project", "local"],
2062
+ systemPrompt: { type: "preset", preset: "claude_code" }
2063
+ } : {},
1789
2064
  extraArgs,
1790
2065
  includePartialMessages: true,
1791
2066
  forwardSubagentText: true,
@@ -1794,8 +2069,8 @@ function buildClaudeQueryOptions(params) {
1794
2069
  ...provider?.additionalDirectories?.length ? { additionalDirectories: provider.additionalDirectories } : {},
1795
2070
  ...run.model ? { model: run.model } : {},
1796
2071
  ...effort ? { effort } : {},
1797
- ...provider?.permissionMode ? { permissionMode: provider.permissionMode } : {},
1798
- ...provider?.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
2072
+ ...run.mode === "plan" ? { permissionMode: "plan" } : params.request.options.fullAccess ? { permissionMode: "bypassPermissions" } : run.mode === "default" ? { permissionMode: "default" } : provider?.permissionMode ? { permissionMode: provider.permissionMode } : {},
2073
+ ...params.request.options.fullAccess || provider?.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
1799
2074
  ...provider?.allowedTools?.length ? { allowedTools: provider.allowedTools } : {},
1800
2075
  ...run.resumeSessionId ? { resume: run.resumeSessionId } : {},
1801
2076
  // Fork-at-message: claude-agent-sdk natively supports slicing a
@@ -1941,8 +2216,14 @@ function readJsonBody(req) {
1941
2216
  });
1942
2217
  }
1943
2218
 
1944
- function autoApproveCanUseTool(_toolName, input) {
1945
- return { behavior: "allow", updatedInput: input };
2219
+ async function handlePermission(req, res, runId) {
2220
+ const run = liveRuns.get(runId);
2221
+ const body = await readJsonBody(req);
2222
+ const resolve = run?.permissions.get(body.requestId);
2223
+ if (!resolve) { res.writeHead(409); res.end("Request is no longer pending"); return; }
2224
+ run.permissions.delete(body.requestId);
2225
+ resolve(body.response);
2226
+ res.writeHead(204); res.end();
1946
2227
  }
1947
2228
 
1948
2229
  async function handleStart(req, res, runId) {
@@ -1982,10 +2263,52 @@ async function handleStart(req, res, runId) {
1982
2263
  const opts = { ...(options || {}) };
1983
2264
  const autoApprove = !!opts.autoApproveTools;
1984
2265
  delete opts.autoApproveTools;
2266
+ const interactiveQuestions = !!opts.interactiveQuestions;
2267
+ delete opts.interactiveQuestions;
2268
+ let planning = opts.permissionMode === "plan";
2269
+ const permissions = new Map();
2270
+ const clearPermissions = () => { for (const resolve of permissions.values()) resolve({ behavior: "deny", message: "Run ended", interrupt: true }); permissions.clear(); };
2271
+ const canUseTool = async (toolName, input, context) => {
2272
+ const isQuestion = toolName === "AskUserQuestion";
2273
+ const isPlan = toolName === "ExitPlanMode";
2274
+ if (context.signal.aborted) return { behavior: "deny", message: "Run cancelled", interrupt: true };
2275
+ if ((isQuestion || isPlan) && !interactiveQuestions) return { behavior: "deny", message: "No interactive user is available." };
2276
+ if (!isQuestion && !isPlan && planning && toolName !== "EnterPlanMode") return { behavior: "deny", message: "Finish planning before requesting write access." };
2277
+ if (!isQuestion && !isPlan && autoApprove) return { behavior: "allow", updatedInput: input };
2278
+ return new Promise((resolve) => {
2279
+ const abort = () => { permissions.delete(context.toolUseID); resolve({ behavior: "deny", message: "Run cancelled", interrupt: true }); };
2280
+ permissions.set(context.toolUseID, (response) => { context.signal.removeEventListener("abort", abort); if (isPlan && response.behavior === "allow") planning = false; resolve(response); });
2281
+ context.signal.addEventListener("abort", abort, { once: true });
2282
+ res.write(JSON.stringify({ _permission: { requestId: context.toolUseID, toolName, input, title: context.title } }) + "\\n");
2283
+ });
2284
+ };
1985
2285
  opts.pathToClaudeCodeExecutable = resolveClaudeBinary(
1986
2286
  opts.pathToClaudeCodeExecutable,
1987
2287
  );
1988
2288
 
2289
+ let queryHandle;
2290
+ let clientGone = false;
2291
+ // This run's own teardown. The liveRuns entry is only removed when it is
2292
+ // still ours: hosts reuse a runId across retry attempts, and a successor
2293
+ // registered while our CLI winds down must not be evicted by our exit.
2294
+ const releaseRun = () => {
2295
+ clearInterval(heartbeat);
2296
+ clearPermissions();
2297
+ if (liveRuns.get(runId)?.query === queryHandle) liveRuns.delete(runId);
2298
+ promptStream.end();
2299
+ };
2300
+ // Host gone (settled, cancelled or crashed) \u2192 end the prompt so the CLI
2301
+ // winds down instead of living on with its background work. Detected on
2302
+ // the response: \`req\` emits "close" as soon as its body is consumed (Node
2303
+ // >= 16), long before any disconnect, while the response only closes
2304
+ // early when the socket dies before the stream finished.
2305
+ res.on("close", () => {
2306
+ if (res.writableFinished) return;
2307
+ clientGone = true;
2308
+ releaseRun();
2309
+ queryHandle?.interrupt().catch(() => {});
2310
+ });
2311
+
1989
2312
  // Resume-if-exists gate. \`claude --resume <id>\` errors hard with "No
1990
2313
  // conversation found with session ID" when the local session jsonl is
1991
2314
  // missing \u2014 most often because a prior post-task snapshot failed and the
@@ -2016,13 +2339,19 @@ async function handleStart(req, res, runId) {
2016
2339
  }
2017
2340
  }
2018
2341
 
2019
- let queryHandle;
2342
+ // Nobody left to stream to: do not start a CLI for it.
2343
+ if (clientGone) { res.end(); return; }
2020
2344
  try {
2021
2345
  queryHandle = query({
2022
2346
  prompt: promptStream,
2023
2347
  options: {
2024
2348
  ...opts,
2025
- ...(autoApprove ? { canUseTool: autoApproveCanUseTool } : {}),
2349
+ canUseTool,
2350
+ hooks: { PreToolUse: [{ hooks: [async (input) => {
2351
+ planning = input.permission_mode === "plan";
2352
+ return interactiveQuestions && ["AskUserQuestion", "ExitPlanMode"].includes(input.tool_name)
2353
+ ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask" } } : {};
2354
+ }] }] },
2026
2355
  },
2027
2356
  });
2028
2357
  } catch (e) {
@@ -2032,28 +2361,21 @@ async function handleStart(req, res, runId) {
2032
2361
  return;
2033
2362
  }
2034
2363
 
2035
- liveRuns.set(runId, { query: queryHandle, prompt: promptStream });
2036
-
2037
- // Client disconnected (e.g. host process killed) \u2192 tear down.
2038
- req.on("close", () => {
2039
- clearInterval(heartbeat);
2040
- if (!liveRuns.has(runId)) return;
2041
- liveRuns.delete(runId);
2042
- promptStream.end();
2043
- queryHandle.interrupt().catch(() => {});
2044
- });
2364
+ liveRuns.set(runId, { query: queryHandle, prompt: promptStream, permissions });
2045
2365
 
2366
+ // Forward every SDKMessage, not just up to the first result: in
2367
+ // streaming-input mode the CLI keeps running after a turn ends and
2368
+ // re-prompts the model when background work finishes. The host decides
2369
+ // when the run is over and disconnects (res "close" above), which ends
2370
+ // the prompt and lets the CLI wind down.
2046
2371
  try {
2047
2372
  for await (const message of queryHandle) {
2048
2373
  res.write(JSON.stringify(message) + "\\n");
2049
- if (message.type === "result") break;
2050
2374
  }
2051
2375
  } catch (e) {
2052
2376
  res.write(JSON.stringify({ _error: String(e?.message ?? e) }) + "\\n");
2053
2377
  } finally {
2054
- clearInterval(heartbeat);
2055
- liveRuns.delete(runId);
2056
- promptStream.end();
2378
+ releaseRun();
2057
2379
  res.end();
2058
2380
  }
2059
2381
  }
@@ -2119,6 +2441,11 @@ const server = http.createServer((req, res) => {
2119
2441
  return;
2120
2442
  }
2121
2443
  const url = req.url ?? "";
2444
+ const permissionRoute = url.match(/^\\/runs\\/([^/]+)\\/permission$/);
2445
+ if (req.method === "POST" && permissionRoute) {
2446
+ handlePermission(req, res, decodeURIComponent(permissionRoute[1])).catch(() => { if (!res.headersSent) res.writeHead(400); res.end(); });
2447
+ return;
2448
+ }
2122
2449
  let m;
2123
2450
  if (req.method === "POST" && (m = url.match(/^\\/runs\\/([^/]+)\\/start$/))) {
2124
2451
  handleStart(req, res, decodeURIComponent(m[1]));
@@ -2355,15 +2682,11 @@ var ClaudeCodeAgentAdapter = class {
2355
2682
  ).catch(() => void 0);
2356
2683
  }
2357
2684
  async setup(request) {
2685
+ if (request.options.configuration === "native") return;
2358
2686
  await time(debugClaude, "claude-code setup()", async () => {
2359
2687
  const options = request.options;
2360
2688
  const provider = request.provider;
2361
2689
  const sandbox = options.sandbox;
2362
- if (!sandbox) {
2363
- throw new Error(
2364
- "claude-code requires a sandbox (the SDK transport runs as a daemon inside the sandbox)."
2365
- );
2366
- }
2367
2690
  const target = await createSetupTarget(provider, "shared-setup", options);
2368
2691
  const settingsPath = path8.join(target.layout.claudeDir, "settings.json");
2369
2692
  const mcpConfigPath = path8.join(
@@ -2384,6 +2707,7 @@ var ClaudeCodeAgentAdapter = class {
2384
2707
  const claudeSettings = { ...hookSettings, ...workflowSettings };
2385
2708
  const mcpConfigJson = buildClaudeMcpConfig(options.mcps) ?? JSON.stringify({ mcpServers: {} }, null, 2);
2386
2709
  const artifacts = [
2710
+ ...!sandbox ? [{ path: path8.join(target.layout.claudeDir, ".claude-plugin", "plugin.json"), content: JSON.stringify({ name: "agentbox", version: "1.0.0" }) }] : [],
2387
2711
  ...skillArtifacts,
2388
2712
  ...buildClaudeCommandArtifacts(options.commands, target.layout),
2389
2713
  ...buildClaudeSubagentArtifacts(options.subAgents, target.layout),
@@ -2394,11 +2718,11 @@ var ClaudeCodeAgentAdapter = class {
2394
2718
  { path: mcpConfigPath, content: mcpConfigJson }
2395
2719
  ];
2396
2720
  const enableRtk = options.enableRtk === true;
2397
- const daemonInfo = {
2721
+ const daemonInfo = sandbox ? {
2398
2722
  port: DAEMON_PORT,
2399
2723
  healthPath: "/__version",
2400
2724
  expectedVersionMatch: DAEMON_PROTOCOL_VERSION
2401
- };
2725
+ } : void 0;
2402
2726
  const setupId = computeSetupId({
2403
2727
  artifacts,
2404
2728
  installCommands,
@@ -2416,7 +2740,7 @@ var ClaudeCodeAgentAdapter = class {
2416
2740
  "applyDifferentialSetup",
2417
2741
  () => applyDifferentialSetup(target, artifacts, installCommands)
2418
2742
  ),
2419
- ensureClaudeCodeDaemon(options, env)
2743
+ ...sandbox ? [ensureClaudeCodeDaemon(options, env)] : []
2420
2744
  ]);
2421
2745
  if (enableRtk) {
2422
2746
  await time(debugClaude, "activateRtk", () => activateRtk(target));
@@ -2428,11 +2752,7 @@ var ClaudeCodeAgentAdapter = class {
2428
2752
  const executeStartedAt = Date.now();
2429
2753
  debugClaude("execute() start runId=%s", request.runId);
2430
2754
  const sandbox = request.options.sandbox;
2431
- if (!sandbox) {
2432
- throw new Error(
2433
- "claude-code requires a sandbox (the SDK transport runs as a daemon inside the sandbox)."
2434
- );
2435
- }
2755
+ if (!sandbox) return executeNativeClaude(request, sink);
2436
2756
  const claudeDir = claudeConfigDir(request.options);
2437
2757
  const settingsPath = path8.join(claudeDir, "settings.json");
2438
2758
  const mcpConfigPath = path8.join(claudeDir, "agentbox-mcp.json");
@@ -2445,6 +2765,7 @@ var ClaudeCodeAgentAdapter = class {
2445
2765
  // user inside our images.
2446
2766
  IS_SANDBOX: "1"
2447
2767
  };
2768
+ applyCliBackgroundWaitCeiling(env);
2448
2769
  const customHeaders = request.options.customHeaders;
2449
2770
  if (customHeaders && Object.keys(customHeaders).length > 0) {
2450
2771
  const serialized = Object.entries(customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
@@ -2489,21 +2810,28 @@ ${serialized}` : serialized;
2489
2810
  // already set `resume` for the resume path, so only stamp `sessionId` for
2490
2811
  // fresh runs.
2491
2812
  ...request.run.resumeSessionId ? {} : { sessionId: presetSessionId },
2492
- autoApproveTools
2813
+ autoApproveTools,
2814
+ interactiveQuestions: hasInteractiveQuestions(request.options)
2493
2815
  }
2494
2816
  };
2495
2817
  const fetchAbort = new AbortController();
2818
+ const runUrl = `${baseUrl}/runs/${encodeURIComponent(request.runId)}`;
2496
2819
  const cleanup = async () => {
2497
2820
  try {
2498
- await fetch(
2499
- `${baseUrl}/runs/${encodeURIComponent(request.runId)}/abort`,
2500
- { method: "POST", headers: authHeaders }
2501
- );
2821
+ await fetch(`${runUrl}/abort`, { method: "POST", headers: authHeaders });
2822
+ } catch {
2823
+ }
2824
+ try {
2825
+ await fetch(runUrl, { method: "DELETE", headers: authHeaders, signal: AbortSignal.timeout(3e3) });
2502
2826
  } catch {
2503
2827
  }
2504
2828
  fetchAbort.abort();
2505
2829
  };
2506
- sink.setAbort(cleanup);
2830
+ let cancelled = false;
2831
+ sink.setAbort(async () => {
2832
+ cancelled = true;
2833
+ await cleanup();
2834
+ });
2507
2835
  sink.onMessage(async (content) => {
2508
2836
  const parts = await validateProviderUserInput(request.provider, content);
2509
2837
  const mapped = mapToClaudeUserContent(parts);
@@ -2535,9 +2863,9 @@ ${serialized}` : serialized;
2535
2863
  })
2536
2864
  );
2537
2865
  if (!response.ok || !response.body) {
2538
- const text = await response.text().catch(() => "");
2866
+ const text2 = await response.text().catch(() => "");
2539
2867
  throw new Error(
2540
- `claude-code daemon /start failed: ${response.status} ${text}`
2868
+ `claude-code daemon /start failed: ${response.status} ${text2}`
2541
2869
  );
2542
2870
  }
2543
2871
  sink.setRaw({ baseUrl, runId: request.runId, claudeDir });
@@ -2554,199 +2882,22 @@ ${serialized}` : serialized;
2554
2882
  { messageId: initialUuid }
2555
2883
  )
2556
2884
  );
2557
- let accumulatedText = "";
2558
- let streamedThinkingChars = 0;
2559
- let pendingMessages = 1;
2560
- let firstStreamEventLogged = false;
2561
- let firstTextDeltaLogged = false;
2562
- let lastTerminalReason;
2563
- let lastIsError = false;
2564
- const rawPayloads = [];
2565
- try {
2885
+ const permissionMessages = async function* () {
2566
2886
  for await (const item of parseNdjsonStream(response.body)) {
2567
- if (item && typeof item === "object") {
2568
- const ctrl = item;
2569
- if ("_error" in ctrl) {
2570
- throw new Error(
2571
- String(item._error ?? "daemon error")
2572
- );
2573
- }
2574
- if ("_notice" in ctrl) {
2575
- debugClaude("daemon notice: %o", ctrl);
2576
- sink.emitRaw(
2577
- toRawEvent(
2578
- request.runId,
2579
- ctrl,
2580
- `daemon.${String(ctrl._notice ?? "notice")}`
2581
- )
2582
- );
2583
- continue;
2584
- }
2585
- }
2586
- const message = item;
2587
- rawPayloads.push(message);
2588
- sink.emitRaw(toRawEvent(request.runId, message, message.type));
2589
- if (message.type === "system") {
2590
- const sub = message.subtype;
2591
- if (sub === "init") {
2592
- const sys = message;
2593
- if (sys.session_id) {
2594
- debugClaude(
2595
- "\u2605 session.init session_id=%s (%dms)",
2596
- sys.session_id.slice(0, 8),
2597
- Date.now() - executeStartedAt
2598
- );
2599
- }
2600
- } else if (sub === "hook_started") {
2601
- const h = message;
2602
- debugClaude(
2603
- "hook.started name=%s event=%s hook_id=%s",
2604
- h.hook_name,
2605
- h.hook_event,
2606
- h.hook_id
2607
- );
2608
- } else if (sub === "hook_response") {
2609
- const h = message;
2610
- const stderr = h.stderr && h.stderr.length > 0 ? h.stderr.replace(/\s+$/, "") : void 0;
2611
- debugClaude(
2612
- "hook.response name=%s exit=%s outcome=%s%s",
2613
- h.hook_name,
2614
- h.exit_code,
2615
- h.outcome,
2616
- stderr ? ` stderr=${JSON.stringify(stderr).slice(0, 200)}` : ""
2617
- );
2618
- }
2619
- continue;
2620
- }
2621
- if (message.type === "stream_event") {
2622
- if (!firstStreamEventLogged) {
2623
- firstStreamEventLogged = true;
2624
- debugClaude(
2625
- "\u2605 first stream_event (%dms since execute start)",
2626
- Date.now() - executeStartedAt
2627
- );
2628
- }
2629
- const partial = message;
2630
- if (partial.parent_tool_use_id) continue;
2631
- const streamType = partial.event?.type;
2632
- if (streamType === "message_start") {
2633
- accumulatedText = "";
2634
- streamedThinkingChars = 0;
2635
- }
2636
- const { text, thinking } = extractStreamDeltas(partial);
2637
- if (thinking) {
2638
- streamedThinkingChars += thinking.length;
2639
- sink.emitEvent(
2640
- createNormalizedEvent(
2641
- "reasoning.delta",
2642
- { provider: request.provider, runId: request.runId },
2643
- { delta: thinking }
2644
- )
2645
- );
2646
- }
2647
- if (text) {
2648
- if (!firstTextDeltaLogged) {
2649
- firstTextDeltaLogged = true;
2650
- debugClaude(
2651
- "\u2605 first text delta (%dms since execute start)",
2652
- Date.now() - executeStartedAt
2653
- );
2654
- }
2655
- accumulatedText += text;
2656
- sink.emitEvent(
2657
- createNormalizedEvent(
2658
- "text.delta",
2659
- { provider: request.provider, runId: request.runId },
2660
- { delta: text }
2661
- )
2662
- );
2663
- }
2664
- continue;
2665
- }
2666
- if (message.type === "assistant") {
2667
- const asst = message;
2668
- if (asst.parent_tool_use_id) continue;
2669
- const thinking = extractAssistantThinking(asst);
2670
- if (thinking && streamedThinkingChars === 0) {
2671
- sink.emitEvent(
2672
- createNormalizedEvent(
2673
- "reasoning.delta",
2674
- { provider: request.provider, runId: request.runId },
2675
- { delta: thinking }
2676
- )
2677
- );
2678
- }
2679
- const text = extractAssistantText(asst);
2680
- sink.emitEvent(
2681
- createNormalizedEvent(
2682
- "message.completed",
2683
- { provider: request.provider, runId: request.runId },
2684
- {
2685
- text,
2686
- ...asst.uuid ? { messageId: String(asst.uuid) } : {}
2687
- }
2688
- )
2689
- );
2690
- continue;
2691
- }
2692
- if (message.type === "result") {
2693
- const result = message;
2694
- lastTerminalReason = result.terminal_reason;
2695
- lastIsError = result.is_error;
2696
- const resultText = result.subtype === "success" ? result.result : accumulatedText;
2697
- if (resultText && resultText !== accumulatedText) {
2698
- accumulatedText = resultText;
2699
- }
2700
- pendingMessages--;
2701
- if (pendingMessages <= 0) break;
2887
+ const control = item;
2888
+ if (!control._permission) {
2889
+ yield item;
2702
2890
  continue;
2703
2891
  }
2892
+ const ask = control._permission;
2893
+ const isQuestion = ask.toolName === "AskUserQuestion";
2894
+ const isPlan = ask.toolName === "ExitPlanMode";
2895
+ const answer = await sink.requestPermission({ type: "permission.requested", provider: request.provider, runId: request.runId, timestamp: (/* @__PURE__ */ new Date()).toISOString(), requestId: ask.requestId, kind: isQuestion ? "question" : isPlan ? "plan" : "tool", toolName: ask.toolName, title: isQuestion ? "Your input is needed" : isPlan ? "Review the plan" : ask.title ?? `Allow ${ask.toolName}?`, input: ask.input, ...isQuestion ? { questions: normalizeUserQuestions("claude-code", ask.input) } : {} });
2896
+ const reply = await fetch(`${baseUrl}/runs/${encodeURIComponent(request.runId)}/permission`, { method: "POST", headers: { "content-type": "application/json", ...authHeaders }, signal: fetchAbort.signal, body: JSON.stringify({ requestId: ask.requestId, response: answer.decision === "allow" ? { behavior: "allow", updatedInput: isQuestion ? { ...ask.input, answers: questionReply("claude-code", ask.input, answer.answers ?? []) } : ask.input } : { behavior: "deny", message: "The user declined this request." } }) });
2897
+ if (!reply.ok) throw new Error(`Claude permission response failed: ${reply.status}`);
2704
2898
  }
2705
- const finalText = accumulatedText;
2706
- const isCancelled = lastTerminalReason === "aborted_streaming" || lastTerminalReason === "aborted_tools";
2707
- const isError = !isCancelled && lastIsError;
2708
- if (isCancelled) {
2709
- debugClaude(
2710
- "\u2605 run.cancelled (%dms since execute start) reason=%s",
2711
- Date.now() - executeStartedAt,
2712
- lastTerminalReason
2713
- );
2714
- sink.cancel({
2715
- text: finalText,
2716
- costData: extractClaudeCostData(rawPayloads)
2717
- });
2718
- } else if (isError) {
2719
- debugClaude(
2720
- "\u2605 run.error (%dms since execute start) reason=%s",
2721
- Date.now() - executeStartedAt,
2722
- lastTerminalReason
2723
- );
2724
- sink.fail(
2725
- new Error(
2726
- finalText || `claude-code run failed (terminal_reason: ${lastTerminalReason})`
2727
- )
2728
- );
2729
- } else {
2730
- debugClaude(
2731
- "\u2605 run.completed (%dms since execute start) chars=%d",
2732
- Date.now() - executeStartedAt,
2733
- finalText.length
2734
- );
2735
- sink.emitEvent(
2736
- createNormalizedEvent(
2737
- "run.completed",
2738
- { provider: request.provider, runId: request.runId },
2739
- { text: finalText }
2740
- )
2741
- );
2742
- sink.complete({
2743
- text: finalText,
2744
- costData: extractClaudeCostData(rawPayloads)
2745
- });
2746
- }
2747
- } finally {
2748
- fetchAbort.abort();
2749
- }
2899
+ };
2900
+ await consumeClaudeMessages(request, sink, permissionMessages(), executeStartedAt, cleanup, () => cancelled);
2750
2901
  return async () => void 0;
2751
2902
  }
2752
2903
  /**
@@ -2806,7 +2957,395 @@ ${serialized}` : serialized;
2806
2957
  );
2807
2958
  }
2808
2959
  }
2809
- };
2960
+ };
2961
+ async function consumeClaudeMessages(request, sink, messages, executeStartedAt, cleanup, wasCancelled = () => false, wait = {}) {
2962
+ let accumulatedText = "";
2963
+ let streamedThinkingChars = 0;
2964
+ let sawResult = false;
2965
+ let firstStreamEventLogged = false;
2966
+ let firstTextDeltaLogged = false;
2967
+ let lastTerminalReason;
2968
+ let lastIsError = false;
2969
+ const rawPayloads = [];
2970
+ const tracker = new BackgroundTaskTracker();
2971
+ const timeoutMs = resolveBackgroundTaskTimeoutMs(request.options.backgroundTaskTimeoutMs);
2972
+ const graceMs = wait.graceMs ?? BACKGROUND_TASK_GRACE_MS;
2973
+ let pendingWait;
2974
+ let waitedMs = 0;
2975
+ let expiry;
2976
+ let lastTasksKey = JSON.stringify({ tasks: [], waiting: false });
2977
+ const emitTasks = (tasks, waiting) => {
2978
+ const key = JSON.stringify({ tasks, waiting });
2979
+ if (key === lastTasksKey) return;
2980
+ lastTasksKey = key;
2981
+ sink.emitEvent(createNormalizedEvent("background.tasks", { provider: request.provider, runId: request.runId }, { tasks, waiting }));
2982
+ };
2983
+ const isAborted = () => wasCancelled() || lastTerminalReason === "aborted_streaming" || lastTerminalReason === "aborted_tools";
2984
+ const endWait = () => {
2985
+ if (!pendingWait) return;
2986
+ waitedMs += pendingWait.elapsedMs();
2987
+ pendingWait.clear();
2988
+ pendingWait = void 0;
2989
+ };
2990
+ const settleOnFailure = (error) => {
2991
+ if (!pendingWait || !sawResult || lastIsError) return false;
2992
+ debugClaude("\u2605 transport failed during background wait; settling on the last result: %o", error);
2993
+ expiry = "transport";
2994
+ return true;
2995
+ };
2996
+ const iterator = messages[Symbol.asyncIterator]();
2997
+ try {
2998
+ for (let next = iterator.next(); ; next = iterator.next()) {
2999
+ let step;
3000
+ try {
3001
+ step = pendingWait ? await Promise.race([
3002
+ next.then((result) => ({ result })),
3003
+ pendingWait.expired.then((reason) => ({ reason }))
3004
+ ]) : { result: await next };
3005
+ } catch (error) {
3006
+ if (!settleOnFailure(error)) throw error;
3007
+ break;
3008
+ }
3009
+ if ("reason" in step) {
3010
+ expiry = step.reason;
3011
+ break;
3012
+ }
3013
+ if (step.result.done) break;
3014
+ const item = step.result.value;
3015
+ if (item && typeof item === "object") {
3016
+ const ctrl = item;
3017
+ if ("_error" in ctrl) {
3018
+ const error = new Error(String(ctrl._error ?? "daemon error"));
3019
+ if (!settleOnFailure(error)) throw error;
3020
+ break;
3021
+ }
3022
+ if ("_notice" in ctrl) {
3023
+ debugClaude("daemon notice: %o", ctrl);
3024
+ sink.emitRaw(
3025
+ toRawEvent(
3026
+ request.runId,
3027
+ ctrl,
3028
+ `daemon.${String(ctrl._notice ?? "notice")}`
3029
+ )
3030
+ );
3031
+ continue;
3032
+ }
3033
+ }
3034
+ const message = item;
3035
+ rawPayloads.push(message);
3036
+ sink.emitRaw(toRawEvent(request.runId, message, message.type));
3037
+ if (tracker.ingest(message) && pendingWait) {
3038
+ debugClaude("\u2605 follow-up turn started; background wait over (%dms since execute start)", Date.now() - executeStartedAt);
3039
+ endWait();
3040
+ }
3041
+ emitTasks(tracker.liveTasks(), pendingWait !== void 0);
3042
+ pendingWait?.setIdle(tracker.liveTasks().length === 0);
3043
+ if (message.type === "system") {
3044
+ const sub = message.subtype;
3045
+ if (sub === "init") {
3046
+ const sys = message;
3047
+ if (request.run.goal && !sys.slash_commands.some((command) => command.replace(/^\//, "") === "goal")) {
3048
+ await cleanup();
3049
+ throw new Error("This Claude Code installation does not expose the native /goal command.");
3050
+ }
3051
+ if (sys.session_id) {
3052
+ debugClaude(
3053
+ "\u2605 session.init session_id=%s (%dms)",
3054
+ sys.session_id.slice(0, 8),
3055
+ Date.now() - executeStartedAt
3056
+ );
3057
+ }
3058
+ } else if (sub === "hook_started") {
3059
+ const h = message;
3060
+ debugClaude(
3061
+ "hook.started name=%s event=%s hook_id=%s",
3062
+ h.hook_name,
3063
+ h.hook_event,
3064
+ h.hook_id
3065
+ );
3066
+ } else if (sub === "hook_response") {
3067
+ const h = message;
3068
+ const stderr = h.stderr && h.stderr.length > 0 ? h.stderr.replace(/\s+$/, "") : void 0;
3069
+ debugClaude(
3070
+ "hook.response name=%s exit=%s outcome=%s%s",
3071
+ h.hook_name,
3072
+ h.exit_code,
3073
+ h.outcome,
3074
+ stderr ? ` stderr=${JSON.stringify(stderr).slice(0, 200)}` : ""
3075
+ );
3076
+ }
3077
+ continue;
3078
+ }
3079
+ if (message.type === "stream_event") {
3080
+ if (!firstStreamEventLogged) {
3081
+ firstStreamEventLogged = true;
3082
+ debugClaude(
3083
+ "\u2605 first stream_event (%dms since execute start)",
3084
+ Date.now() - executeStartedAt
3085
+ );
3086
+ }
3087
+ const partial = message;
3088
+ if (partial.parent_tool_use_id) continue;
3089
+ const streamType = partial.event?.type;
3090
+ if (streamType === "message_start") {
3091
+ accumulatedText = "";
3092
+ streamedThinkingChars = 0;
3093
+ }
3094
+ const { text: text2, thinking } = extractStreamDeltas(partial);
3095
+ if (thinking) {
3096
+ streamedThinkingChars += thinking.length;
3097
+ sink.emitEvent(
3098
+ createNormalizedEvent(
3099
+ "reasoning.delta",
3100
+ { provider: request.provider, runId: request.runId },
3101
+ { delta: thinking }
3102
+ )
3103
+ );
3104
+ }
3105
+ if (text2) {
3106
+ if (!firstTextDeltaLogged) {
3107
+ firstTextDeltaLogged = true;
3108
+ debugClaude(
3109
+ "\u2605 first text delta (%dms since execute start)",
3110
+ Date.now() - executeStartedAt
3111
+ );
3112
+ }
3113
+ accumulatedText += text2;
3114
+ sink.emitEvent(
3115
+ createNormalizedEvent(
3116
+ "text.delta",
3117
+ { provider: request.provider, runId: request.runId },
3118
+ { delta: text2 }
3119
+ )
3120
+ );
3121
+ }
3122
+ continue;
3123
+ }
3124
+ if (message.type === "assistant") {
3125
+ const asst = message;
3126
+ if (asst.parent_tool_use_id) continue;
3127
+ const thinking = extractAssistantThinking(asst);
3128
+ if (thinking && streamedThinkingChars === 0) {
3129
+ sink.emitEvent(
3130
+ createNormalizedEvent(
3131
+ "reasoning.delta",
3132
+ { provider: request.provider, runId: request.runId },
3133
+ { delta: thinking }
3134
+ )
3135
+ );
3136
+ }
3137
+ const text2 = extractAssistantText(asst);
3138
+ sink.emitEvent(
3139
+ createNormalizedEvent(
3140
+ "message.completed",
3141
+ { provider: request.provider, runId: request.runId },
3142
+ {
3143
+ text: text2,
3144
+ ...asst.uuid ? { messageId: String(asst.uuid) } : {}
3145
+ }
3146
+ )
3147
+ );
3148
+ continue;
3149
+ }
3150
+ if (message.type === "result") {
3151
+ sawResult = true;
3152
+ const result = message;
3153
+ lastTerminalReason = result.terminal_reason;
3154
+ lastIsError = result.is_error;
3155
+ const resultText = result.subtype === "success" ? result.result : accumulatedText;
3156
+ if (resultText && resultText !== accumulatedText) {
3157
+ accumulatedText = resultText;
3158
+ }
3159
+ const live = tracker.liveTasks();
3160
+ if (timeoutMs === 0 || !tracker.hasSeenBackgroundWork() || isAborted()) break;
3161
+ debugClaude("\u2605 turn ended with %d background task(s); waiting", live.length);
3162
+ endWait();
3163
+ pendingWait = new BackgroundWait(graceMs, Math.max(0, timeoutMs - waitedMs));
3164
+ pendingWait.setIdle(live.length === 0);
3165
+ emitTasks(live, true);
3166
+ continue;
3167
+ }
3168
+ }
3169
+ if (expiry === "ceiling") {
3170
+ const ids = tracker.liveTasks().filter((task) => task.type !== "scheduled_wakeup").map((task) => task.id);
3171
+ debugClaude("\u2605 background wait ceiling (%dms) hit; stopping %d task(s)", timeoutMs, ids.length);
3172
+ if (wait.stopTasks) {
3173
+ await withTimeout(wait.stopTasks(ids), wait.stopTimeoutMs ?? STOP_TASKS_TIMEOUT_MS).catch(() => void 0);
3174
+ }
3175
+ } else if (expiry === "grace") {
3176
+ debugClaude("\u2605 background set emptied with no follow-up turn; settling");
3177
+ }
3178
+ if (pendingWait) {
3179
+ endWait();
3180
+ emitTasks([], false);
3181
+ }
3182
+ await cleanup();
3183
+ if (!sawResult && !wasCancelled()) throw new Error("Claude Code closed before reporting a result");
3184
+ const finalText = accumulatedText;
3185
+ const isCancelled = isAborted();
3186
+ const isError = !isCancelled && lastIsError;
3187
+ if (isCancelled) {
3188
+ debugClaude(
3189
+ "\u2605 run.cancelled (%dms since execute start) reason=%s",
3190
+ Date.now() - executeStartedAt,
3191
+ lastTerminalReason
3192
+ );
3193
+ sink.cancel({
3194
+ text: finalText,
3195
+ costData: extractClaudeCostData(rawPayloads)
3196
+ });
3197
+ } else if (isError) {
3198
+ debugClaude(
3199
+ "\u2605 run.error (%dms since execute start) reason=%s",
3200
+ Date.now() - executeStartedAt,
3201
+ lastTerminalReason
3202
+ );
3203
+ sink.fail(
3204
+ new Error(
3205
+ finalText || `claude-code run failed (terminal_reason: ${lastTerminalReason})`
3206
+ )
3207
+ );
3208
+ } else {
3209
+ debugClaude(
3210
+ "\u2605 run.completed (%dms since execute start) chars=%d",
3211
+ Date.now() - executeStartedAt,
3212
+ finalText.length
3213
+ );
3214
+ sink.emitEvent(
3215
+ createNormalizedEvent(
3216
+ "run.completed",
3217
+ { provider: request.provider, runId: request.runId },
3218
+ { text: finalText }
3219
+ )
3220
+ );
3221
+ sink.complete({
3222
+ text: finalText,
3223
+ costData: extractClaudeCostData(rawPayloads)
3224
+ });
3225
+ }
3226
+ } finally {
3227
+ pendingWait?.clear();
3228
+ await cleanup();
3229
+ }
3230
+ }
3231
+ async function executeNativeClaude(request, sink, wait = {}) {
3232
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
3233
+ const claudeDir = claudeConfigDir(request.options);
3234
+ const input = await validateProviderUserInput(request.provider, request.run.input);
3235
+ const prompt = new AsyncQueue();
3236
+ const sessionId = request.run.resumeSessionId ?? randomUUID();
3237
+ const controller = new AbortController();
3238
+ let handle;
3239
+ let processHandle;
3240
+ let stopped;
3241
+ let cancelled = false;
3242
+ const stop = () => stopped ??= (async () => {
3243
+ prompt.finish();
3244
+ handle?.close();
3245
+ controller.abort();
3246
+ if (processHandle) await processHandle.kill();
3247
+ })();
3248
+ sink.setAbort(async () => {
3249
+ cancelled = true;
3250
+ await stop();
3251
+ });
3252
+ sink.setSessionId(sessionId);
3253
+ const messageId = randomUUID();
3254
+ prompt.push({ type: "user", uuid: messageId, message: { role: "user", content: mapToClaudeUserContent(input) }, parent_tool_use_id: null });
3255
+ const hostEnv = Object.fromEntries(Object.entries({ ...process.env, ...request.options.env }).filter((entry) => entry[1] !== void 0));
3256
+ applyCliBackgroundWaitCeiling(hostEnv);
3257
+ if (request.options.customHeaders) {
3258
+ const headers = Object.entries(request.options.customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
3259
+ hostEnv.ANTHROPIC_CUSTOM_HEADERS = [hostEnv.ANTHROPIC_CUSTOM_HEADERS, headers].filter(Boolean).join("\n");
3260
+ }
3261
+ const options = buildClaudeQueryOptions({
3262
+ request,
3263
+ ...request.options.configuration === "native" ? {} : {
3264
+ settingsPath: path8.join(claudeDir, "settings.json"),
3265
+ mcpConfigPath: path8.join(claudeDir, "agentbox-mcp.json")
3266
+ },
3267
+ // Auth is deliberately CLI-owned. Never copy the user's credential files
3268
+ // into the generated configuration directory or a task artifact.
3269
+ env: hostEnv
3270
+ });
3271
+ const autoApprove = shouldAutoApproveClaudeTools(request.options);
3272
+ const interactiveQuestions = hasInteractiveQuestions(request.options);
3273
+ let planning = options.permissionMode === "plan";
3274
+ try {
3275
+ handle = query({ prompt, options: {
3276
+ ...options,
3277
+ // Use the SDK-matched CLI by default; an installed CLI is an explicit override.
3278
+ pathToClaudeCodeExecutable: request.options.provider?.binary,
3279
+ abortController: controller,
3280
+ // `sessionId` is rejected alongside `resume` unless `forkSession` is
3281
+ // set, where it names the forked session. Stamping it on forks keeps
3282
+ // the pre-minted id reported via `sink.setSessionId` truthful, so a
3283
+ // later run can resume the fork.
3284
+ ...request.run.resumeSessionId ? {} : { sessionId },
3285
+ ...request.options.configuration === "native" ? {} : { plugins: [{ type: "local", path: claudeDir }] },
3286
+ spawnClaudeCodeProcess(spawnOptions) {
3287
+ if (cancelled || controller.signal.aborted) throw new Error("Local run was cancelled before startup");
3288
+ processHandle = spawnCommand({ ...spawnOptions, processGroup: request.options.processGroup !== "inherited" });
3289
+ return processHandle.child;
3290
+ },
3291
+ hooks: { ...options.hooks, PreToolUse: [...options.hooks?.PreToolUse ?? [], { hooks: [async (input2) => {
3292
+ if (input2.hook_event_name !== "PreToolUse") return {};
3293
+ planning = input2.permission_mode === "plan";
3294
+ return interactiveQuestions && ["AskUserQuestion", "ExitPlanMode"].includes(input2.tool_name) ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask" } } : {};
3295
+ }] }] },
3296
+ async canUseTool(toolName, input2, context) {
3297
+ if (cancelled || context.signal.aborted) return { behavior: "deny", message: "Run cancelled", interrupt: true };
3298
+ const isQuestion = toolName === "AskUserQuestion";
3299
+ const isPlan = toolName === "ExitPlanMode";
3300
+ if ((isQuestion || isPlan) && !interactiveQuestions) return { behavior: "deny", message: "No interactive user is available." };
3301
+ if (!isQuestion && !isPlan && planning && toolName !== "EnterPlanMode") return { behavior: "deny", message: "Finish planning before requesting write access." };
3302
+ if (!isQuestion && !isPlan && autoApprove) return { behavior: "allow", updatedInput: input2 };
3303
+ try {
3304
+ const response = await sink.requestPermission({
3305
+ type: "permission.requested",
3306
+ provider: request.provider,
3307
+ runId: request.runId,
3308
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3309
+ requestId: context.toolUseID,
3310
+ kind: isQuestion ? "question" : isPlan ? "plan" : "tool",
3311
+ toolName,
3312
+ ...isQuestion ? { questions: normalizeUserQuestions("claude-code", input2) } : {},
3313
+ title: isQuestion ? "Your input is needed" : isPlan ? "Review the plan" : context.title ?? `Allow ${toolName}?`,
3314
+ message: context.description ?? context.decisionReason,
3315
+ input: input2,
3316
+ canRemember: false
3317
+ });
3318
+ if (isPlan && response.decision === "allow") planning = false;
3319
+ if (!cancelled && !context.signal.aborted && response.decision === "allow") return {
3320
+ behavior: "allow",
3321
+ updatedInput: isQuestion ? { ...input2, answers: questionReply("claude-code", input2, response.answers ?? []) } : input2
3322
+ };
3323
+ return { behavior: "deny", message: "The user denied this action" };
3324
+ } catch {
3325
+ return { behavior: "deny", message: "Run cancelled", interrupt: true };
3326
+ }
3327
+ }
3328
+ } });
3329
+ const live = handle;
3330
+ sink.setRaw({ query: live, claudeDir, runId: request.runId });
3331
+ sink.emitEvent(createNormalizedEvent("run.started", { provider: request.provider, runId: request.runId }));
3332
+ sink.emitEvent(createNormalizedEvent("message.started", { provider: request.provider, runId: request.runId }, { messageId }));
3333
+ await consumeClaudeMessages(request, sink, live, Date.now(), stop, () => cancelled, {
3334
+ ...wait,
3335
+ // Native owns the CLI: ask it to stop leftover tasks before closing it.
3336
+ stopTasks: async (ids) => {
3337
+ await Promise.all(ids.map((id) => live.stopTask(id).catch(() => void 0)));
3338
+ }
3339
+ });
3340
+ } catch (error) {
3341
+ await stop();
3342
+ if (cancelled) sink.cancel();
3343
+ else throw error;
3344
+ } finally {
3345
+ await stop();
3346
+ }
3347
+ return stop;
3348
+ }
2810
3349
 
2811
3350
  // src/agents/providers/codex.ts
2812
3351
  import crypto2 from "crypto";
@@ -2820,16 +3359,16 @@ async function fetchJson(url, init) {
2820
3359
  if (!response.ok) {
2821
3360
  throw new Error(`Request to ${url} failed with ${response.status}.`);
2822
3361
  }
2823
- const text = await response.text();
2824
- if (text.length === 0) {
3362
+ const text2 = await response.text();
3363
+ if (text2.length === 0) {
2825
3364
  throw new Error(
2826
3365
  `Request to ${url} returned status ${response.status} with an empty body.`
2827
3366
  );
2828
3367
  }
2829
3368
  try {
2830
- return JSON.parse(text);
3369
+ return JSON.parse(text2);
2831
3370
  } catch (error) {
2832
- const preview = text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
3371
+ const preview = text2.length > 200 ? `${text2.slice(0, 200)}\u2026` : text2;
2833
3372
  const cause = error instanceof Error ? error.message : String(error);
2834
3373
  throw new Error(
2835
3374
  `Could not parse JSON response from ${url} (status ${response.status}): ${cause}. Body: ${preview}`
@@ -3030,7 +3569,7 @@ var JsonRpcLineClient = class {
3030
3569
  // src/agents/providers/codex.ts
3031
3570
  function codexConfigDir(options) {
3032
3571
  return path9.join(
3033
- agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox)),
3572
+ agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox), options.stateDirectory),
3034
3573
  ".codex"
3035
3574
  );
3036
3575
  }
@@ -3097,20 +3636,20 @@ function compactEnv(values) {
3097
3636
  );
3098
3637
  }
3099
3638
  function buildCodexSandboxMode(options) {
3100
- return options.sandbox ? "workspace-write" : "read-only";
3639
+ return options.fullAccess ? "danger-full-access" : options.provider?.sandboxMode ?? (options.configuration === "native" ? void 0 : options.sandbox ? "workspace-write" : "read-only");
3101
3640
  }
3102
3641
  function buildThreadParams(cwd, options, request) {
3103
3642
  return {
3104
3643
  cwd,
3105
3644
  model: request.run.model ?? null,
3106
- approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
3645
+ ...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3107
3646
  sandbox: buildCodexSandboxMode(options),
3108
3647
  serviceName: "agentbox",
3109
3648
  // Persist the rollout on disk so follow-up runs can call `thread/resume`.
3110
3649
  // `ephemeral: true` threads have no rollout file and resume fails with
3111
3650
  // "no rollout found for thread id ...".
3112
3651
  experimentalRawEvents: true,
3113
- developerInstructions: request.run.systemPrompt ?? null
3652
+ ...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null }
3114
3653
  };
3115
3654
  }
3116
3655
  function buildResumeParams(cwd, options, request) {
@@ -3118,9 +3657,9 @@ function buildResumeParams(cwd, options, request) {
3118
3657
  threadId: request.run.resumeSessionId,
3119
3658
  cwd,
3120
3659
  model: request.run.model ?? null,
3121
- approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
3660
+ ...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3122
3661
  sandbox: buildCodexSandboxMode(options),
3123
- developerInstructions: request.run.systemPrompt ?? null,
3662
+ ...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null },
3124
3663
  // We only need the thread id back; we never read `thread.turns`.
3125
3664
  // Without this Codex hydrates the full history into the response and
3126
3665
  // emits a `deprecationNotice` ("Full-history hydration is deprecated
@@ -3134,15 +3673,22 @@ function buildForkParams(cwd, options, request) {
3134
3673
  lastTurnId: request.run.forkAtMessageId ?? null,
3135
3674
  cwd,
3136
3675
  model: request.run.model ?? null,
3137
- approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
3676
+ ...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3138
3677
  sandbox: buildCodexSandboxMode(options),
3139
- developerInstructions: request.run.systemPrompt ?? null,
3678
+ ...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null },
3140
3679
  excludeTurns: true
3141
3680
  };
3142
3681
  }
3143
3682
  function buildTurnSandboxPolicy(options) {
3683
+ if (options.fullAccess || options.provider?.sandboxMode === "danger-full-access") return { type: "dangerFullAccess" };
3144
3684
  if (!options.sandbox) {
3145
- return void 0;
3685
+ if (buildCodexSandboxMode(options) === void 0) return void 0;
3686
+ if (buildCodexSandboxMode(options) === "read-only") return void 0;
3687
+ return {
3688
+ type: "workspaceWrite",
3689
+ networkAccess: options.provider?.networkAccess ?? false,
3690
+ ...options.provider?.writableRoots?.length ? { writableRoots: options.provider.writableRoots } : {}
3691
+ };
3146
3692
  }
3147
3693
  if (options.sandbox.provider === SandboxProvider.LocalDocker) {
3148
3694
  return {
@@ -3161,13 +3707,53 @@ function buildCodexTurnStartParams(params) {
3161
3707
  return {
3162
3708
  threadId,
3163
3709
  input: inputItems,
3164
- approvalPolicy: isInteractiveApproval(request.options) ? "untrusted" : "never",
3710
+ ...request.options.provider?.approvalPolicy ? { approvalPolicy: request.options.provider.approvalPolicy } : request.options.configuration === "native" && !request.options.fullAccess ? {} : {
3711
+ approvalPolicy: !request.options.fullAccess && isInteractiveApproval(request.options) ? "untrusted" : "never"
3712
+ },
3165
3713
  ...sandboxPolicy ? { sandboxPolicy } : {},
3166
3714
  model: request.run.model ?? null,
3167
3715
  effort: request.run.reasoning ?? null,
3716
+ ...request.run.mode ? { collaborationMode: {
3717
+ mode: request.run.mode,
3718
+ settings: { model: request.run.model, reasoning_effort: request.run.reasoning ?? null, developer_instructions: null }
3719
+ } } : {},
3168
3720
  outputSchema: null
3169
3721
  };
3170
3722
  }
3723
+ var BACKGROUND_OUTPUT_TAIL_CHARS = 4e3;
3724
+ function buildCodexBackgroundFollowUp(items) {
3725
+ const reports = items.map((item) => {
3726
+ const tail = (item.aggregatedOutput ?? "").slice(-BACKGROUND_OUTPUT_TAIL_CHARS).trimEnd();
3727
+ const duration = item.durationMs === void 0 ? "an unknown time" : `${Math.round(item.durationMs / 1e3)}s`;
3728
+ return `\`${item.command}\` exited with code ${item.exitCode ?? "unknown"} after ${duration}.
3729
+ Output (last ${BACKGROUND_OUTPUT_TAIL_CHARS} chars):
3730
+ \`\`\`
3731
+ ${tail || "(no output)"}
3732
+ \`\`\`
3733
+ `;
3734
+ });
3735
+ return `Background command finished while you were idle.
3736
+
3737
+ ${reports.join("")}
3738
+ Continue from here: verify the outcome and finish the task. Do not restart the command.`;
3739
+ }
3740
+ var CODEX_CANCEL_TURN_TEXT = "Run cancelled by the host.";
3741
+ async function terminateBackgroundTerminals(client, threadId) {
3742
+ await withTimeout((async () => {
3743
+ try {
3744
+ const listed = await client.request(
3745
+ "thread/backgroundTerminals/list",
3746
+ { threadId }
3747
+ );
3748
+ for (const terminal of listed?.data ?? []) {
3749
+ if (typeof terminal?.processId !== "string") continue;
3750
+ await client.request("thread/backgroundTerminals/terminate", { threadId, processId: terminal.processId });
3751
+ }
3752
+ } catch (error) {
3753
+ debugCodex("background terminal termination stopped early: %o", error);
3754
+ }
3755
+ })(), STOP_TASKS_TIMEOUT_MS);
3756
+ }
3171
3757
  function toRawEvent2(runId, payload, type) {
3172
3758
  return {
3173
3759
  provider: AgentProvider.Codex,
@@ -3189,7 +3775,7 @@ function buildCodexCommandArgs(binary, args, options) {
3189
3775
  overrides.push(["supports_websockets", "false"]);
3190
3776
  }
3191
3777
  const overrideArgs = overrides.flatMap(([k, v]) => ["-c", `${k}=${v}`]);
3192
- return ["-u", "XDG_CONFIG_HOME", binary, ...overrideArgs, ...args];
3778
+ return [...options?.configuration === "native" ? [] : ["-u", "XDG_CONFIG_HOME"], binary, ...overrideArgs, ...args];
3193
3779
  }
3194
3780
  function toNormalizedCodexEvents(runId, notification) {
3195
3781
  const base = {
@@ -3261,8 +3847,8 @@ function toNormalizedCodexEvents(runId, notification) {
3261
3847
  }
3262
3848
  if (notification.method === "turn/completed") {
3263
3849
  const turn = notification.params?.turn;
3264
- const text = typeof turn?.lastAgentMessage === "string" ? turn.lastAgentMessage : void 0;
3265
- return [createNormalizedEvent("run.completed", base, { text })];
3850
+ const text2 = typeof turn?.lastAgentMessage === "string" ? turn.lastAgentMessage : void 0;
3851
+ return [createNormalizedEvent("run.completed", base, { text: text2 })];
3266
3852
  }
3267
3853
  if (notification.method === "error") {
3268
3854
  const error = notification.params?.error;
@@ -3274,7 +3860,7 @@ function toNormalizedCodexEvents(runId, notification) {
3274
3860
  }
3275
3861
  return [];
3276
3862
  }
3277
- function createCodexPermissionEvent(request, notification) {
3863
+ function createCodexPermissionEvent(request, notification, fileChanges) {
3278
3864
  const raw = toRawEvent2(request.runId, notification, notification.method);
3279
3865
  const params = notification.params;
3280
3866
  const requestId = notification.id;
@@ -3317,13 +3903,49 @@ function createCodexPermissionEvent(request, notification) {
3317
3903
  kind: "file-change",
3318
3904
  title: "Approve file changes",
3319
3905
  message: typeof params.reason === "string" ? params.reason : "Codex wants to modify files.",
3320
- input: params,
3906
+ input: fileChanges ? { ...params, changes: fileChanges } : params,
3321
3907
  canRemember: availableDecisions.includes("acceptForSession")
3322
3908
  }
3323
3909
  );
3324
3910
  }
3325
3911
  return null;
3326
3912
  }
3913
+ var CODEX_ELICITATION_METHOD = "mcpServer/elicitation/request";
3914
+ function createCodexElicitationPermissionEvent(request, notification) {
3915
+ if (notification.method !== CODEX_ELICITATION_METHOD || notification.id === void 0) {
3916
+ return null;
3917
+ }
3918
+ const params = notification.params ?? {};
3919
+ const meta = params._meta ?? {};
3920
+ if (meta.codex_approval_kind !== "mcp_tool_call") {
3921
+ return null;
3922
+ }
3923
+ const raw = toRawEvent2(request.runId, notification, notification.method);
3924
+ const toolName = typeof meta.tool_name === "string" ? meta.tool_name : void 0;
3925
+ const server = typeof params.serverName === "string" ? params.serverName : void 0;
3926
+ const persist = Array.isArray(meta.persist) ? meta.persist : [];
3927
+ return createNormalizedEvent(
3928
+ "permission.requested",
3929
+ { provider: request.provider, runId: request.runId, raw },
3930
+ {
3931
+ requestId: String(notification.id),
3932
+ kind: "tool",
3933
+ toolName: toolName ?? server,
3934
+ title: "Approve tool call",
3935
+ message: typeof params.message === "string" && params.message.trim() ? params.message : `Codex wants to call ${toolName ?? "an MCP tool"}${server ? ` on ${server}` : ""}.`,
3936
+ input: { server, tool: toolName, arguments: meta.tool_params, ...params },
3937
+ canRemember: persist.includes("session")
3938
+ }
3939
+ );
3940
+ }
3941
+ function toCodexElicitationResult(notification, response) {
3942
+ if (response.decision === "deny") {
3943
+ return { action: "decline", content: null };
3944
+ }
3945
+ const meta = notification.params?._meta ?? {};
3946
+ const persist = Array.isArray(meta.persist) ? meta.persist : [];
3947
+ return response.remember && persist.includes("session") ? { action: "accept", content: null, _meta: { persist: "session" } } : { action: "accept", content: null };
3948
+ }
3327
3949
  function toCodexApprovalDecision(notification, response) {
3328
3950
  const params = notification.params ?? {};
3329
3951
  const availableDecisions = Array.isArray(params.availableDecisions) ? params.availableDecisions : [];
@@ -3371,7 +3993,7 @@ async function materializeCodexImage(options, part, index) {
3371
3993
  if (data.length === 0) {
3372
3994
  throw new Error("Cannot attach an empty image to Codex.");
3373
3995
  }
3374
- const root = agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox));
3996
+ const root = agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox), options.stateDirectory);
3375
3997
  const imagePath = path9.join(
3376
3998
  root,
3377
3999
  "inputs",
@@ -3518,6 +4140,7 @@ async function connectRemoteCodexAppServer(url, headers = {}) {
3518
4140
  }
3519
4141
  async function setupCodex(request) {
3520
4142
  const options = request.options;
4143
+ if (options.configuration === "native") return;
3521
4144
  const provider = request.provider;
3522
4145
  const hooks = assertHooksSupported(provider, options);
3523
4146
  assertCommandsSupported(provider, options.commands);
@@ -3715,10 +4338,10 @@ async function createRuntime(request, inputParts) {
3715
4338
  const codexDir = codexConfigDir(options);
3716
4339
  const env = compactEnv({
3717
4340
  ...options.env ?? {},
3718
- CODEX_HOME: codexDir,
4341
+ ...options.configuration === "native" ? {} : { CODEX_HOME: codexDir },
3719
4342
  ...options.provider?.env ?? {}
3720
4343
  });
3721
- const runtimeCwd = path9.dirname(codexDir);
4344
+ const runtimeCwd = options.configuration === "native" ? options.cwd : path9.dirname(codexDir);
3722
4345
  const inputItems = await buildCodexInputItems(options, inputParts);
3723
4346
  const usesRemoteWebSocket = options.sandbox && options.sandbox.provider !== SandboxProvider.LocalDocker;
3724
4347
  if (usesRemoteWebSocket && options.sandbox) {
@@ -3788,6 +4411,7 @@ async function createRuntime(request, inputParts) {
3788
4411
  };
3789
4412
  }
3790
4413
  const processHandle = spawnCommand({
4414
+ processGroup: options.processGroup !== "inherited",
3791
4415
  command: "env",
3792
4416
  args: codexArgs,
3793
4417
  cwd: runtimeCwd,
@@ -3796,6 +4420,7 @@ async function createRuntime(request, inputParts) {
3796
4420
  ...env
3797
4421
  }
3798
4422
  });
4423
+ processHandle.child.stderr.resume();
3799
4424
  return {
3800
4425
  source: linesFromNodeStream(processHandle.child.stdout),
3801
4426
  writeLine: async (line) => {
@@ -3916,10 +4541,10 @@ var CodexAgentAdapter = class {
3916
4541
  throw new Error("Cannot send message before thread is started.");
3917
4542
  }
3918
4543
  const parts = normalizeUserInput(content);
3919
- const text = parts.filter((p) => p.type === "text").map((p) => p.text).join("");
4544
+ const text2 = parts.filter((p) => p.type === "text").map((p) => p.text).join("");
3920
4545
  const inputItems = [];
3921
- if (text.trim().length > 0) {
3922
- inputItems.push({ type: "text", text, text_elements: [] });
4546
+ if (text2.trim().length > 0) {
4547
+ inputItems.push({ type: "text", text: text2, text_elements: [] });
3923
4548
  }
3924
4549
  const response = await client.request(
3925
4550
  "turn/start",
@@ -3935,11 +4560,95 @@ var CodexAgentAdapter = class {
3935
4560
  };
3936
4561
  sink.onMessage(sendTurn);
3937
4562
  const rawPayloads = [];
4563
+ const pendingFileChanges = /* @__PURE__ */ new Map();
4564
+ const fileItemKey = (params, itemId) => typeof params?.threadId === "string" && typeof params.turnId === "string" && typeof itemId === "string" ? `${params.threadId}:${params.turnId}:${itemId}` : void 0;
3938
4565
  let streamedText = "";
4566
+ const timeoutMs = resolveBackgroundTaskTimeoutMs(request.options.backgroundTaskTimeoutMs);
4567
+ const isRootThread = (params) => !params?.threadId || params.threadId === rootThreadId;
4568
+ const inFlight = /* @__PURE__ */ new Map();
4569
+ const finished = [];
4570
+ let pendingWait;
4571
+ let waitedMs = 0;
4572
+ let followUpSent = false;
4573
+ let turnMessageText = "";
4574
+ let lastTurn;
4575
+ let lastTasksKey = JSON.stringify({ tasks: [], waiting: false });
4576
+ const liveTasks = () => [...inFlight.values()].filter((item) => item.outlived).map((item) => ({ id: item.id, type: "command", description: item.command }));
4577
+ const emitTasks = (tasks, waiting) => {
4578
+ const key = JSON.stringify({ tasks, waiting });
4579
+ if (key === lastTasksKey) return;
4580
+ lastTasksKey = key;
4581
+ sink.emitEvent(createNormalizedEvent("background.tasks", { provider: request.provider, runId: request.runId }, { tasks, waiting }));
4582
+ };
4583
+ const endWait = () => {
4584
+ if (!pendingWait) return;
4585
+ waitedMs += pendingWait.elapsedMs();
4586
+ pendingWait.clear();
4587
+ pendingWait = void 0;
4588
+ followUpSent = false;
4589
+ };
4590
+ const sendBackgroundFollowUp = async () => {
4591
+ if (!rootThreadId || !pendingWait || followUpSent || finished.length === 0 || abortInvoked) return;
4592
+ if (timeoutMs - waitedMs - pendingWait.elapsedMs() <= 0) return;
4593
+ followUpSent = true;
4594
+ const text2 = buildCodexBackgroundFollowUp(finished.splice(0));
4595
+ try {
4596
+ const response = await client.request(
4597
+ "turn/start",
4598
+ buildCodexTurnStartParams({
4599
+ threadId: rootThreadId,
4600
+ inputItems: [{ type: "text", text: text2, text_elements: [] }],
4601
+ request
4602
+ })
4603
+ );
4604
+ endWait();
4605
+ sink.emitEvent(createNormalizedEvent("message.injected", { provider: request.provider, runId: request.runId }, {
4606
+ content: text2,
4607
+ ...typeof response?.turn?.id === "string" ? { messageId: response.turn.id } : {}
4608
+ }));
4609
+ } catch (error) {
4610
+ debugCodex("background follow-up turn/start failed: %o", error);
4611
+ }
4612
+ };
3939
4613
  const completion = new Promise((resolve, reject) => {
4614
+ const settle = () => {
4615
+ endWait();
4616
+ emitTasks([], false);
4617
+ sink.emitEvent(createNormalizedEvent("run.completed", { provider: request.provider, runId: request.runId }, { text: lastTurn?.messageText || void 0 }));
4618
+ resolve({ text: lastTurn?.text ?? streamedText, turnId, threadId: rootThreadId, interrupted: false });
4619
+ };
4620
+ const settleOnFailure = (error) => {
4621
+ if (!pendingWait || !lastTurn || abortInvoked) return false;
4622
+ debugCodex("\u2605 transport failed during background wait; settling on the last turn: %o", error);
4623
+ settle();
4624
+ return true;
4625
+ };
3940
4626
  void (async () => {
3941
4627
  let firstClientMessageLogged = false;
3942
- for await (const message of client.messages()) {
4628
+ const iterator = client.messages()[Symbol.asyncIterator]();
4629
+ for (let next = iterator.next(); ; next = iterator.next()) {
4630
+ let step;
4631
+ try {
4632
+ step = pendingWait ? await Promise.race([
4633
+ next.then((result) => ({ result })),
4634
+ pendingWait.expired.then((reason) => ({ reason }))
4635
+ ]) : { result: await next };
4636
+ } catch (error) {
4637
+ if (settleOnFailure(error)) return;
4638
+ throw error;
4639
+ }
4640
+ if ("reason" in step) {
4641
+ if (!abortInvoked) {
4642
+ debugCodex("\u2605 background wait over (%s) with %d command(s) in flight", step.reason, inFlight.size);
4643
+ if (step.reason === "ceiling" && rootThreadId) await terminateBackgroundTerminals(client, rootThreadId);
4644
+ settle();
4645
+ return;
4646
+ }
4647
+ endWait();
4648
+ step = { result: await next };
4649
+ }
4650
+ if (step.result.done) break;
4651
+ const message = step.result.value;
3943
4652
  if (!firstClientMessageLogged) {
3944
4653
  firstClientMessageLogged = true;
3945
4654
  debugCodex(
@@ -3951,15 +4660,39 @@ var CodexAgentAdapter = class {
3951
4660
  const raw = toRawEvent2(request.runId, message, message.method);
3952
4661
  rawPayloads.push(message);
3953
4662
  sink.emitRaw(raw);
3954
- if (message.method === "tool/requestUserInput" && message.id !== void 0) {
3955
- reject(
3956
- new Error(
3957
- "Codex tool/requestUserInput approvals are not yet supported by AgentBox."
3958
- )
3959
- );
3960
- return;
4663
+ const item = message.params?.item;
4664
+ const itemKey = fileItemKey(message.params, item?.id);
4665
+ if (itemKey && item?.type === "fileChange") {
4666
+ if (message.method === "item/completed") pendingFileChanges.delete(itemKey);
4667
+ else if (message.method === "item/started" && Array.isArray(item.changes)) {
4668
+ if (pendingFileChanges.size >= 128) pendingFileChanges.delete(pendingFileChanges.keys().next().value);
4669
+ pendingFileChanges.set(itemKey, item.changes);
4670
+ }
4671
+ }
4672
+ if ((message.method === "item/tool/requestUserInput" || message.method === "tool/requestUserInput") && message.id !== void 0) {
4673
+ const questions = normalizeUserQuestions("codex", message.params);
4674
+ const response = hasInteractiveQuestions(request.options) ? await sink.requestPermission(createNormalizedEvent("permission.requested", {
4675
+ provider: request.provider,
4676
+ runId: request.runId,
4677
+ raw
4678
+ }, {
4679
+ requestId: String(message.id),
4680
+ kind: "question",
4681
+ toolName: "request_user_input",
4682
+ title: "Your input is needed",
4683
+ input: message.params,
4684
+ questions,
4685
+ canRemember: false
4686
+ })) : void 0;
4687
+ await client.respond(message.id, { answers: response?.decision === "allow" ? questionReply("codex", message.params, response.answers ?? []) : {} });
4688
+ continue;
3961
4689
  }
3962
- const permissionEvent = createCodexPermissionEvent(request, message);
4690
+ const approvalKey = fileItemKey(message.params, message.params?.itemId);
4691
+ const permissionEvent = createCodexPermissionEvent(
4692
+ request,
4693
+ message,
4694
+ approvalKey ? pendingFileChanges.get(approvalKey) : void 0
4695
+ );
3963
4696
  if (permissionEvent && message.id !== void 0) {
3964
4697
  const response = interactiveApproval ? await sink.requestPermission(permissionEvent) : {
3965
4698
  requestId: permissionEvent.requestId,
@@ -3968,32 +4701,99 @@ var CodexAgentAdapter = class {
3968
4701
  await client.respond(message.id, {
3969
4702
  decision: toCodexApprovalDecision(message, response)
3970
4703
  });
4704
+ if (approvalKey) pendingFileChanges.delete(approvalKey);
4705
+ continue;
4706
+ }
4707
+ const elicitation = createCodexElicitationPermissionEvent(request, message);
4708
+ if (elicitation && message.id !== void 0) {
4709
+ const response = interactiveApproval ? await sink.requestPermission(elicitation) : { requestId: elicitation.requestId, decision: "allow" };
4710
+ await client.respond(message.id, toCodexElicitationResult(message, response));
4711
+ continue;
4712
+ }
4713
+ if (message.id !== void 0) {
4714
+ debugCodex("unsupported server request %s; declining", message.method);
4715
+ await (message.method === CODEX_ELICITATION_METHOD ? client.respond(message.id, { action: "cancel", content: null }) : client.respondError(message.id, {
4716
+ code: -32601,
4717
+ message: `Unsupported request ${message.method}`
4718
+ }));
3971
4719
  continue;
3972
4720
  }
4721
+ if (message.method === "item/completed") {
4722
+ const item2 = message.params?.item;
4723
+ if (item2?.type === "plan" && typeof item2.text === "string") {
4724
+ sink.emitEvent(createNormalizedEvent("plan.completed", { provider: request.provider, runId: request.runId }, { text: item2.text }));
4725
+ }
4726
+ }
4727
+ const turn = message.params?.turn;
4728
+ const rootTurnCompleted = message.method === "turn/completed" && isRootThread(message.params);
4729
+ const waitAfterTurn = rootTurnCompleted && pendingTurns <= 1 && timeoutMs !== 0 && !abortInvoked && turn?.status === "completed" && (inFlight.size > 0 || finished.length > 0);
3973
4730
  for (const event of toNormalizedCodexEvents(request.runId, message)) {
4731
+ if (event.type === "run.completed" && (waitAfterTurn || turn?.status === "interrupted")) continue;
3974
4732
  sink.emitEvent(event);
3975
4733
  if (event.type === "text.delta") {
3976
4734
  streamedText += event.delta;
4735
+ } else if (event.type === "message.completed" && event.text) {
4736
+ turnMessageText = event.text;
3977
4737
  }
3978
4738
  }
3979
4739
  if (message.method === "thread/started" && !rootThreadId) {
3980
4740
  rootThreadId = message.params?.thread?.id ?? rootThreadId;
3981
4741
  }
3982
4742
  if (message.method === "turn/started") {
3983
- turnId = message.params?.turn?.id ?? turnId;
4743
+ turnId = turn?.id ?? turnId;
4744
+ if (isRootThread(message.params)) {
4745
+ streamedText = "";
4746
+ turnMessageText = "";
4747
+ if (pendingWait) debugCodex("\u2605 follow-up turn started; background wait over");
4748
+ endWait();
4749
+ emitTasks(liveTasks(), false);
4750
+ }
3984
4751
  }
3985
- if (message.method === "turn/completed" && (!message.params?.threadId || message.params.threadId === rootThreadId)) {
4752
+ if (item?.type === "commandExecution" && typeof item.id === "string" && isRootThread(message.params)) {
4753
+ if (message.method === "item/started") {
4754
+ inFlight.set(item.id, {
4755
+ id: item.id,
4756
+ command: String(item.command ?? ""),
4757
+ processId: typeof item.processId === "string" ? item.processId : void 0,
4758
+ outlived: false
4759
+ });
4760
+ } else if (message.method === "item/completed") {
4761
+ const tracked = inFlight.get(item.id);
4762
+ inFlight.delete(item.id);
4763
+ if (tracked?.outlived && pendingWait) {
4764
+ finished.push({
4765
+ ...tracked,
4766
+ aggregatedOutput: typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : void 0,
4767
+ exitCode: typeof item.exitCode === "number" ? item.exitCode : void 0,
4768
+ durationMs: typeof item.durationMs === "number" ? item.durationMs : void 0
4769
+ });
4770
+ }
4771
+ emitTasks(liveTasks(), pendingWait !== void 0);
4772
+ pendingWait?.setIdle(inFlight.size === 0);
4773
+ if (pendingWait && inFlight.size === 0) await sendBackgroundFollowUp();
4774
+ }
4775
+ }
4776
+ if (rootTurnCompleted) {
3986
4777
  pendingTurns--;
3987
4778
  if (pendingTurns <= 0) {
3988
- const turn = message.params?.turn;
3989
- const interrupted = turn?.status === "interrupted";
3990
- resolve({
3991
- text: streamedText,
3992
- turnId,
3993
- threadId: rootThreadId,
3994
- interrupted
3995
- });
3996
- return;
4779
+ const previousText = lastTurn?.text;
4780
+ lastTurn = { text: streamedText, messageText: turnMessageText };
4781
+ if (!waitAfterTurn) {
4782
+ resolve({
4783
+ text: streamedText || previousText,
4784
+ turnId,
4785
+ threadId: rootThreadId,
4786
+ interrupted: turn?.status === "interrupted"
4787
+ });
4788
+ return;
4789
+ }
4790
+ for (const tracked of inFlight.values()) tracked.outlived = true;
4791
+ debugCodex("\u2605 turn ended with %d command(s) in flight; waiting", inFlight.size);
4792
+ endWait();
4793
+ pendingWait = new BackgroundWait(BACKGROUND_TASK_GRACE_MS, Math.max(0, timeoutMs - waitedMs));
4794
+ pendingWait.setIdle(inFlight.size === 0);
4795
+ emitTasks(liveTasks(), true);
4796
+ if (inFlight.size === 0) await sendBackgroundFollowUp();
3997
4797
  }
3998
4798
  }
3999
4799
  if (message.method === "error" && !shouldIgnoreCodexError(message)) {
@@ -4001,6 +4801,7 @@ var CodexAgentAdapter = class {
4001
4801
  return;
4002
4802
  }
4003
4803
  }
4804
+ if (settleOnFailure(new Error("Codex transport closed."))) return;
4004
4805
  reject(new Error("Codex transport closed before run completed."));
4005
4806
  })().catch(reject);
4006
4807
  });
@@ -4049,6 +4850,13 @@ var CodexAgentAdapter = class {
4049
4850
  sink.emitRaw(
4050
4851
  toRawEvent2(request.runId, threadResponse, threadResultEventName)
4051
4852
  );
4853
+ if (request.run.mode) {
4854
+ const modes = await client.request("collaborationMode/list", {});
4855
+ if (!modes.data.some((mode) => mode.mode === request.run.mode)) throw new Error("This Codex installation does not support the requested planning mode.");
4856
+ }
4857
+ if (request.run.goal) {
4858
+ await client.request("thread/goal/set", { threadId: threadResponse.thread.id, objective: request.run.goal, status: "active" });
4859
+ }
4052
4860
  await client.request(
4053
4861
  "turn/start",
4054
4862
  buildCodexTurnStartParams({
@@ -4064,6 +4872,8 @@ var CodexAgentAdapter = class {
4064
4872
  } catch (err) {
4065
4873
  completionError = err;
4066
4874
  }
4875
+ endWait();
4876
+ emitTasks([], false);
4067
4877
  if (completionError !== void 0) {
4068
4878
  if (abortInvoked) {
4069
4879
  debugCodex(
@@ -4071,31 +4881,32 @@ var CodexAgentAdapter = class {
4071
4881
  Date.now() - executeStartedAt
4072
4882
  );
4073
4883
  sink.cancel({
4074
- text: streamedText || void 0,
4884
+ text: streamedText || lastTurn?.text || void 0,
4075
4885
  costData: extractCodexCostData(rawPayloads)
4076
4886
  });
4077
4887
  } else {
4078
4888
  sink.fail(completionError);
4079
4889
  }
4080
4890
  } else {
4081
- const { text, interrupted } = completionResult;
4891
+ const { text: text2, interrupted } = completionResult;
4082
4892
  if (abortInvoked || interrupted) {
4083
4893
  debugCodex(
4084
4894
  "\u2605 run.cancelled (%dms since execute start) interrupted=%s",
4085
4895
  Date.now() - executeStartedAt,
4086
4896
  interrupted
4087
4897
  );
4088
- sink.cancel({ text, costData: extractCodexCostData(rawPayloads) });
4898
+ sink.cancel({ text: text2, costData: extractCodexCostData(rawPayloads) });
4089
4899
  } else {
4090
4900
  debugCodex(
4091
4901
  "\u2605 run.completed (%dms since execute start) chars=%d",
4092
4902
  Date.now() - executeStartedAt,
4093
- text?.length ?? 0
4903
+ text2?.length ?? 0
4094
4904
  );
4095
4905
  sink.complete({ costData: extractCodexCostData(rawPayloads) });
4096
4906
  }
4097
4907
  }
4098
4908
  } finally {
4909
+ pendingWait?.clear();
4099
4910
  await runtime.cleanup().catch(() => void 0);
4100
4911
  }
4101
4912
  return async () => void 0;
@@ -4108,36 +4919,51 @@ var CodexAgentAdapter = class {
4108
4919
  * driven by the normalized `message.started` event whose
4109
4920
  * `messageId` IS the codex turnId).
4110
4921
  *
4111
- * If `sessionId` or `turnId` is missing the call is a no-op.
4922
+ * When that interrupt is rejected the thread is idle in a background
4923
+ * wait, or the turn id is stale or missing — a turn is started only to be
4924
+ * interrupted, so the originating run still observes an interrupted turn
4925
+ * and cancels; the model's leftover processes are then terminated.
4926
+ * Without `sessionId` the call is a no-op.
4112
4927
  */
4113
4928
  async attachAbort(request) {
4114
4929
  const threadId = request.sessionId;
4115
- const turnId = request.turnId;
4116
- if (!threadId || !turnId) {
4117
- debugCodex(
4118
- "attachAbort runId=%s skipped: threadId=%s turnId=%s",
4119
- request.runId,
4120
- threadId,
4121
- turnId
4122
- );
4930
+ if (!threadId) {
4931
+ debugCodex("attachAbort runId=%s skipped: no threadId", request.runId);
4123
4932
  return;
4124
4933
  }
4125
4934
  await withCodexAppServer(request, async (client) => {
4126
- await Promise.race([
4127
- client.request("turn/interrupt", { threadId, turnId }),
4935
+ const bounded = (what, promise) => Promise.race([
4936
+ promise,
4128
4937
  new Promise(
4129
- (_, reject) => setTimeout(
4130
- () => reject(new Error("codex turn/interrupt timed out")),
4131
- 3e3
4132
- )
4938
+ (_, reject) => setTimeout(() => reject(new Error(`codex ${what} timed out`)), 3e3)
4133
4939
  )
4134
- ]).catch((error) => {
4135
- debugCodex(
4136
- "attachAbort runId=%s turn/interrupt failed: %o",
4137
- request.runId,
4138
- error
4940
+ ]);
4941
+ const interrupt = (turnId) => bounded("turn/interrupt", client.request("turn/interrupt", { threadId, turnId }));
4942
+ if (request.turnId) {
4943
+ try {
4944
+ await interrupt(request.turnId);
4945
+ return;
4946
+ } catch (error) {
4947
+ debugCodex("attachAbort runId=%s turn/interrupt failed: %o", request.runId, error);
4948
+ }
4949
+ }
4950
+ try {
4951
+ const response = await bounded(
4952
+ "turn/start",
4953
+ client.request("turn/start", {
4954
+ threadId,
4955
+ input: [{ type: "text", text: CODEX_CANCEL_TURN_TEXT, text_elements: [] }],
4956
+ approvalPolicy: "never",
4957
+ model: null,
4958
+ effort: null,
4959
+ outputSchema: null
4960
+ })
4139
4961
  );
4140
- });
4962
+ if (typeof response?.turn?.id === "string") await interrupt(response.turn.id);
4963
+ } catch (error) {
4964
+ debugCodex("attachAbort runId=%s cancel turn failed: %o", request.runId, error);
4965
+ }
4966
+ await terminateBackgroundTerminals(client, threadId);
4141
4967
  });
4142
4968
  }
4143
4969
  /**
@@ -4152,14 +4978,14 @@ var CodexAgentAdapter = class {
4152
4978
  );
4153
4979
  }
4154
4980
  const parts = normalizeUserInput(content);
4155
- const text = joinTextParts(
4981
+ const text2 = joinTextParts(
4156
4982
  parts.filter(
4157
4983
  (part) => part.type === "text"
4158
4984
  )
4159
4985
  );
4160
4986
  const inputItems = [];
4161
- if (text.trim().length > 0) {
4162
- inputItems.push({ type: "text", text, text_elements: [] });
4987
+ if (text2.trim().length > 0) {
4988
+ inputItems.push({ type: "text", text: text2, text_elements: [] });
4163
4989
  }
4164
4990
  await withCodexAppServer(request, async (client) => {
4165
4991
  await client.request("turn/start", {
@@ -4175,10 +5001,10 @@ var CodexAgentAdapter = class {
4175
5001
  };
4176
5002
 
4177
5003
  // src/agents/providers/opencode.ts
4178
- import { createHash as createHash2 } from "crypto";
5004
+ import { createHash as createHash2, randomBytes, randomUUID as randomUUID2 } from "crypto";
5005
+ import os2 from "os";
4179
5006
  import path10 from "path";
4180
5007
  var SANDBOX_OPENCODE_PORT = 4096;
4181
- var LOCAL_OPENCODE_PORT = 4096;
4182
5008
  var SANDBOX_OPENCODE_READY_TIMEOUT_MS = 9e4;
4183
5009
  var LOCAL_OPENCODE_READY_TIMEOUT_MS = 2e4;
4184
5010
  var SHARED_OPENCODE_TARGET_ID = "shared-opencode-server";
@@ -4230,30 +5056,13 @@ function hashLlmApiKeys(env) {
4230
5056
  }
4231
5057
  return hasher.digest("hex");
4232
5058
  }
4233
- async function killLocalOpenCodeServer() {
4234
- await time(debugOpencode, "kill local opencode server", async () => {
4235
- const killer = spawnCommand({
4236
- command: "sh",
4237
- args: [
4238
- "-c",
4239
- `lsof -ti tcp:${LOCAL_OPENCODE_PORT} | xargs kill 2>/dev/null || true`
4240
- ]
4241
- });
4242
- await killer.wait().catch(() => void 0);
4243
- await waitFor(
4244
- async () => {
4245
- try {
4246
- const res = await fetch(
4247
- `http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`
4248
- );
4249
- return !res.ok;
4250
- } catch {
4251
- return true;
4252
- }
4253
- },
4254
- { timeoutMs: 5e3, intervalMs: 200 }
4255
- ).catch(() => void 0);
4256
- });
5059
+ var localOpenCodeServers = /* @__PURE__ */ new WeakMap();
5060
+ async function killLocalOpenCodeServer(options) {
5061
+ const pending = localOpenCodeServers.get(options);
5062
+ if (!pending) return;
5063
+ const server = await pending;
5064
+ await server.process.kill();
5065
+ localOpenCodeServers.delete(options);
4257
5066
  }
4258
5067
  async function killSandboxOpenCodeServer(sandbox, pidFilePath, cwd, port) {
4259
5068
  await time(debugOpencode, "kill sandbox opencode server", async () => {
@@ -4283,6 +5092,9 @@ function toRawEvent3(runId, payload, type) {
4283
5092
  payload
4284
5093
  };
4285
5094
  }
5095
+ function injectedTaskResultChild(text2) {
5096
+ return /<task id="?([^"\s>]+)"? state="?(?:completed|error)"?>/.exec(text2)?.[1];
5097
+ }
4286
5098
  function toOpenCodeModel(model) {
4287
5099
  if (!model) {
4288
5100
  return void 0;
@@ -4332,6 +5144,7 @@ function createOpenCodePermissionEvent(request, raw, payload) {
4332
5144
  },
4333
5145
  {
4334
5146
  requestId: String(properties.id ?? ""),
5147
+ toolName: permission,
4335
5148
  kind: permission === "bash" ? "bash" : permission === "edit" ? "edit" : permission === "external_directory" ? "file-change" : permission === "webfetch" ? "network" : permission === "task" ? "tool" : "unknown",
4336
5149
  title: `Approve ${permission} permission`,
4337
5150
  message: typeof properties.metadata === "object" && properties.metadata !== null ? JSON.stringify(properties.metadata) : `OpenCode requested ${permission} permission.`,
@@ -4353,6 +5166,7 @@ function buildOpenCodeConfig(options, interactiveApproval) {
4353
5166
  prompt: options.systemPrompt || FALLBACK_OPEN_CODE_AGENT_PROMPT,
4354
5167
  permission: buildOpenCodePermissionConfig(interactiveApproval),
4355
5168
  tools: {
5169
+ question: hasInteractiveQuestions(options),
4356
5170
  write: true,
4357
5171
  edit: true,
4358
5172
  bash: true,
@@ -4407,7 +5221,7 @@ async function ensureSandboxOpenCodeServer(request) {
4407
5221
  const port = SANDBOX_OPENCODE_PORT;
4408
5222
  const plugins = assertHooksSupported(request.provider, options);
4409
5223
  assertCommandsSupported(request.provider, options.commands);
4410
- const interactiveApproval = isInteractiveApproval(options);
5224
+ const interactiveApproval = !options.fullAccess && isInteractiveApproval(options);
4411
5225
  const target = await createSetupTarget(
4412
5226
  request.provider,
4413
5227
  SHARED_OPENCODE_TARGET_ID,
@@ -4475,7 +5289,8 @@ async function ensureSandboxOpenCodeServer(request) {
4475
5289
  const commonEnv = {
4476
5290
  OPENCODE_CONFIG: configPath,
4477
5291
  OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
4478
- OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
5292
+ OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
5293
+ OPENCODE_ENABLE_QUESTION_TOOL: hasInteractiveQuestions(options) ? "true" : "false"
4479
5294
  };
4480
5295
  await applyDifferentialSetup(target, allArtifacts, installCommands);
4481
5296
  if (enableRtk) {
@@ -4604,85 +5419,91 @@ ${lastLog}` : "")
4604
5419
  await markSetupComplete(target, setupId);
4605
5420
  });
4606
5421
  }
4607
- async function ensureLocalOpenCodeServer(request) {
4608
- const options = request.options;
4609
- const plugins = assertHooksSupported(request.provider, options);
4610
- assertCommandsSupported(request.provider, options.commands);
4611
- const interactiveApproval = isInteractiveApproval(options);
4612
- const target = await createSetupTarget(
4613
- request.provider,
4614
- "shared-setup",
4615
- options
4616
- );
4617
- const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(
4618
- request.provider,
4619
- options.skills,
4620
- target.layout
4621
- );
4622
- const pluginArtifacts = buildOpenCodePluginArtifacts(
4623
- plugins,
4624
- target.layout.opencodeDir
4625
- );
4626
- const configPath = path10.join(target.layout.opencodeDir, "agentbox.json");
4627
- const openCodeConfig = buildOpenCodeConfig(options, interactiveApproval);
4628
- const commonEnv = {
4629
- OPENCODE_CONFIG: configPath,
4630
- OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
4631
- OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
4632
- };
4633
- const allArtifacts = [
4634
- ...skillArtifacts,
4635
- ...pluginArtifacts,
4636
- {
4637
- path: configPath,
4638
- content: JSON.stringify(openCodeConfig, null, 2)
4639
- }
4640
- ];
4641
- const daemonInfo = {
4642
- port: LOCAL_OPENCODE_PORT,
4643
- healthPath: "/global/health"
5422
+ async function startLocalOpenCodeServer(request) {
5423
+ const originalOptions = request.options;
5424
+ const options = {
5425
+ ...originalOptions,
5426
+ stateDirectory: path10.join(originalOptions.stateDirectory ?? path10.join(os2.tmpdir(), "agentbox-native"), "instances", randomUUID2())
4644
5427
  };
4645
- const setupId = computeSetupId({
4646
- artifacts: allArtifacts,
4647
- installCommands,
4648
- daemon: daemonInfo,
4649
- extras: [`apiKeys:${hashLlmApiKeys(options.env)}`]
4650
- });
4651
- if (await preflightSetup(target, setupId, daemonInfo)) {
4652
- debugOpencode("local opencode server up-to-date \u2014 reusing");
4653
- return;
4654
- }
4655
- if (await isLocalOpenCodeServerHealthy()) {
4656
- debugOpencode(
4657
- "local opencode server already running but setup drifted \u2014 reusing it without restart; call agent.killServer() to apply the new config"
4658
- );
4659
- return;
5428
+ let generatedEnv = {};
5429
+ if (options.configuration !== "native") {
5430
+ const plugins = assertHooksSupported(request.provider, options);
5431
+ assertCommandsSupported(request.provider, options.commands);
5432
+ const target = await createSetupTarget(request.provider, "shared-setup", options);
5433
+ const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(request.provider, options.skills, target.layout);
5434
+ const configPath = path10.join(target.layout.opencodeDir, "agentbox.json");
5435
+ const artifacts = [
5436
+ ...skillArtifacts,
5437
+ ...buildOpenCodePluginArtifacts(plugins, target.layout.opencodeDir),
5438
+ { path: configPath, content: JSON.stringify(buildOpenCodeConfig(options, isInteractiveApproval(options)), null, 2) }
5439
+ ];
5440
+ await applyDifferentialSetup(target, artifacts, installCommands);
5441
+ generatedEnv = {
5442
+ OPENCODE_CONFIG: configPath,
5443
+ OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
5444
+ OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
5445
+ OPENCODE_ENABLE_QUESTION_TOOL: hasInteractiveQuestions(options) ? "true" : "false"
5446
+ };
4660
5447
  }
4661
- debugOpencode("local opencode server absent \u2014 spawning");
4662
- await applyDifferentialSetup(target, allArtifacts, installCommands);
4663
- await killLocalOpenCodeServer();
4664
- spawnCommand({
5448
+ const port = await getAvailablePort();
5449
+ const password = randomBytes(32).toString("base64url");
5450
+ const baseUrl = `http://127.0.0.1:${port}`;
5451
+ const headers = { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` };
5452
+ const processHandle = spawnCommand({
4665
5453
  command: options.provider?.binary ?? "opencode",
4666
- args: [
4667
- "serve",
4668
- "--hostname",
4669
- "127.0.0.1",
4670
- "--port",
4671
- String(LOCAL_OPENCODE_PORT),
4672
- ...options.provider?.args ?? []
4673
- ],
5454
+ args: ["serve", ...options.provider?.args ?? [], "--hostname", "127.0.0.1", "--port", String(port)],
4674
5455
  cwd: options.cwd,
5456
+ processGroup: options.processGroup !== "inherited",
4675
5457
  env: {
4676
5458
  ...process.env,
4677
- ...options.env ?? {},
4678
- ...commonEnv
5459
+ ...options.env,
5460
+ ...generatedEnv,
5461
+ ...options.fullAccess ? { OPENCODE_PERMISSION: JSON.stringify({ "*": "allow", question: "ask" }) } : {},
5462
+ ...options.interactiveQuestions === true ? { OPENCODE_ENABLE_QUESTION_TOOL: "true" } : {},
5463
+ OPENCODE_SERVER_USERNAME: "opencode",
5464
+ OPENCODE_SERVER_PASSWORD: password
4679
5465
  }
4680
5466
  });
4681
- await waitForHttpReady(
4682
- `http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`,
4683
- { timeoutMs: LOCAL_OPENCODE_READY_TIMEOUT_MS }
4684
- );
4685
- await markSetupComplete(target, setupId);
5467
+ processHandle.child.stdout.resume();
5468
+ processHandle.child.stderr.resume();
5469
+ try {
5470
+ let startupError;
5471
+ void processHandle.wait().then(
5472
+ (code) => {
5473
+ startupError = new Error(`Local OpenCode server exited before startup (${code})`);
5474
+ },
5475
+ (error) => {
5476
+ startupError = error instanceof Error ? error : new Error(String(error));
5477
+ }
5478
+ );
5479
+ await waitFor(async () => {
5480
+ if (startupError) throw startupError;
5481
+ try {
5482
+ return (await fetch(`${baseUrl}/global/health`, { headers, signal: AbortSignal.timeout(1e3) })).ok;
5483
+ } catch {
5484
+ return false;
5485
+ }
5486
+ }, { timeoutMs: LOCAL_OPENCODE_READY_TIMEOUT_MS });
5487
+ const unauthenticated = await fetch(`${baseUrl}/global/health`, { signal: AbortSignal.timeout(3e3) });
5488
+ if (unauthenticated.status !== 401) throw new Error("This OpenCode version does not enforce local server authentication. Upgrade OpenCode before running it through AgentBox.");
5489
+ return { baseUrl, headers, process: processHandle };
5490
+ } catch (error) {
5491
+ await processHandle.kill();
5492
+ throw error;
5493
+ }
5494
+ }
5495
+ async function ensureLocalOpenCodeServer(request) {
5496
+ let pending = localOpenCodeServers.get(request.options);
5497
+ if (!pending) {
5498
+ pending = startLocalOpenCodeServer(request);
5499
+ localOpenCodeServers.set(request.options, pending);
5500
+ }
5501
+ try {
5502
+ await pending;
5503
+ } catch (error) {
5504
+ localOpenCodeServers.delete(request.options);
5505
+ throw error;
5506
+ }
4686
5507
  }
4687
5508
  async function setupOpenCode(request) {
4688
5509
  if (request.options.sandbox) {
@@ -4695,16 +5516,6 @@ async function isSandboxOpenCodeServerHealthy(sandbox, cwd, port) {
4695
5516
  const probe = await sandbox.run(opencodeHealthCurl(port), { cwd, timeoutMs: 5e3 }).catch(() => void 0);
4696
5517
  return probe?.exitCode === 0;
4697
5518
  }
4698
- async function isLocalOpenCodeServerHealthy() {
4699
- try {
4700
- const res = await fetch(
4701
- `http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`
4702
- );
4703
- return res.ok;
4704
- } catch {
4705
- return false;
4706
- }
4707
- }
4708
5519
  async function killOpenCodeServer(request) {
4709
5520
  const { options } = request;
4710
5521
  if (options.sandbox) {
@@ -4725,24 +5536,22 @@ async function killOpenCodeServer(request) {
4725
5536
  );
4726
5537
  return;
4727
5538
  }
4728
- await killLocalOpenCodeServer();
5539
+ await killLocalOpenCodeServer(options);
4729
5540
  }
4730
5541
  async function buildOpenCodeRuntime(options) {
4731
5542
  if (options.sandbox) {
4732
5543
  const sandbox = options.sandbox;
4733
- const baseUrl2 = (await sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
5544
+ const baseUrl = (await sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
4734
5545
  return {
4735
- baseUrl: baseUrl2,
5546
+ baseUrl,
4736
5547
  previewHeaders: await opencodeAuthHeaders(sandbox),
4737
- raw: { baseUrl: baseUrl2, port: SANDBOX_OPENCODE_PORT }
5548
+ raw: { baseUrl, port: SANDBOX_OPENCODE_PORT }
4738
5549
  };
4739
5550
  }
4740
- const baseUrl = `http://127.0.0.1:${LOCAL_OPENCODE_PORT}`;
4741
- return {
4742
- baseUrl,
4743
- previewHeaders: {},
4744
- raw: { baseUrl, port: LOCAL_OPENCODE_PORT }
4745
- };
5551
+ const pending = localOpenCodeServers.get(options);
5552
+ if (!pending) throw new Error("Local OpenCode server has not been set up by this Agent instance");
5553
+ const server = await pending;
5554
+ return { baseUrl: server.baseUrl, previewHeaders: server.headers, raw: { baseUrl: server.baseUrl } };
4746
5555
  }
4747
5556
  var OpenCodeAgentAdapter = class {
4748
5557
  async setup(request) {
@@ -4811,37 +5620,151 @@ var OpenCodeAgentAdapter = class {
4811
5620
  resolveSessionTerminal = resolve;
4812
5621
  });
4813
5622
  let lastSseActivityAt = Date.now();
5623
+ const postAbort = async (id) => {
5624
+ try {
5625
+ await Promise.race([
5626
+ fetchJson(`${runtime.baseUrl}/session/${id}/abort`, {
5627
+ method: "POST",
5628
+ headers: {
5629
+ "content-type": "application/json",
5630
+ ...runtime.previewHeaders
5631
+ }
5632
+ }),
5633
+ new Promise(
5634
+ (_, reject) => setTimeout(
5635
+ () => reject(new Error("opencode POST /session/abort timed out")),
5636
+ 3e3
5637
+ )
5638
+ )
5639
+ ]);
5640
+ } catch {
5641
+ }
5642
+ };
4814
5643
  let userAbortRequested = false;
4815
5644
  sink.setAbort(async () => {
4816
5645
  userAbortRequested = true;
4817
5646
  const sessionIdAtAbort = capturedSessionId;
4818
- if (sessionIdAtAbort) {
4819
- try {
4820
- await Promise.race([
4821
- fetchJson(
4822
- `${runtime.baseUrl}/session/${sessionIdAtAbort}/abort`,
4823
- {
4824
- method: "POST",
4825
- headers: {
4826
- "content-type": "application/json",
4827
- ...runtime.previewHeaders
4828
- }
4829
- }
4830
- ),
4831
- new Promise(
4832
- (_, reject) => setTimeout(
4833
- () => reject(new Error("opencode POST /session/abort timed out")),
4834
- 3e3
4835
- )
4836
- )
4837
- ]);
4838
- } catch {
4839
- }
4840
- }
5647
+ if (sessionIdAtAbort) await postAbort(sessionIdAtAbort);
4841
5648
  resolveSessionTerminal();
4842
5649
  });
5650
+ const backgroundTimeoutMs = resolveBackgroundTaskTimeoutMs(
5651
+ request.options.backgroundTaskTimeoutMs
5652
+ );
5653
+ const trackChildren = backgroundTimeoutMs !== 0;
5654
+ const children = /* @__PURE__ */ new Map();
5655
+ const liveChildren = () => [...children.values()].filter((child) => child.live).map((child) => ({
5656
+ id: child.id,
5657
+ type: "subagent",
5658
+ description: child.title
5659
+ }));
5660
+ const shouldWait = () => [...children.values()].some((child) => child.background) && liveChildren().length > 0;
5661
+ let pendingWait;
5662
+ let waitedMs = 0;
5663
+ let expiry;
5664
+ let sawParentIdle = false;
5665
+ let lastTasksKey = JSON.stringify({ tasks: [], waiting: false });
5666
+ const emitTasks = (tasks, waiting) => {
5667
+ const key = JSON.stringify({ tasks, waiting });
5668
+ if (key === lastTasksKey) return;
5669
+ lastTasksKey = key;
5670
+ sink.emitEvent(
5671
+ createNormalizedEvent(
5672
+ "background.tasks",
5673
+ { provider: request.provider, runId: request.runId },
5674
+ { tasks, waiting }
5675
+ )
5676
+ );
5677
+ };
5678
+ const endWait = () => {
5679
+ if (!pendingWait) return;
5680
+ waitedMs += pendingWait.elapsedMs();
5681
+ pendingWait.clear();
5682
+ pendingWait = void 0;
5683
+ };
5684
+ const onChildrenChanged = () => {
5685
+ if (!pendingWait) return;
5686
+ const live = liveChildren();
5687
+ emitTasks(live, true);
5688
+ pendingWait.setIdle(live.length === 0);
5689
+ };
5690
+ const registerChild = (id, title, background = false) => {
5691
+ const known = children.get(id);
5692
+ if (known) {
5693
+ known.background ||= background;
5694
+ if (title && title !== known.title) {
5695
+ known.title = title;
5696
+ onChildrenChanged();
5697
+ }
5698
+ return;
5699
+ }
5700
+ children.set(id, { id, title: title ?? "", live: true, background });
5701
+ onChildrenChanged();
5702
+ };
5703
+ const setChildLive = (id, live) => {
5704
+ const child = children.get(id);
5705
+ if (!child || child.live === live) return;
5706
+ child.live = live;
5707
+ onChildrenChanged();
5708
+ };
5709
+ const reconcileChildren = async () => {
5710
+ const live = [...children.values()].filter((child) => child.live);
5711
+ if (live.length === 0) return;
5712
+ const statuses = await withTimeout(
5713
+ fetchJson(
5714
+ `${runtime.baseUrl}/session/status`,
5715
+ { headers: runtime.previewHeaders }
5716
+ ).catch(() => void 0),
5717
+ 3e3
5718
+ );
5719
+ if (!statuses || typeof statuses !== "object") return;
5720
+ for (const child of live) {
5721
+ const status = statuses[child.id];
5722
+ if (!status || status.type === "idle") setChildLive(child.id, false);
5723
+ }
5724
+ };
5725
+ const onParentIdle = async () => {
5726
+ sawParentIdle = true;
5727
+ if (pendingWait || sessionIdleFromSse) return;
5728
+ const tracking = trackChildren && !userAbortRequested;
5729
+ if (tracking && shouldWait()) await reconcileChildren();
5730
+ if (!tracking || !shouldWait()) {
5731
+ sessionIdleFromSse = true;
5732
+ resolveSessionTerminal();
5733
+ return;
5734
+ }
5735
+ const live = liveChildren();
5736
+ debugOpencode(
5737
+ "\u2605 parent idle with %d live subagent(s); waiting",
5738
+ live.length
5739
+ );
5740
+ const wait = new BackgroundWait(
5741
+ BACKGROUND_TASK_GRACE_MS,
5742
+ Math.max(0, backgroundTimeoutMs - waitedMs)
5743
+ );
5744
+ pendingWait = wait;
5745
+ void wait.expired.then((reason) => {
5746
+ if (pendingWait !== wait) return;
5747
+ expiry = reason;
5748
+ resolveSessionTerminal();
5749
+ });
5750
+ emitTasks(live, true);
5751
+ };
5752
+ const onParentBusy = () => {
5753
+ if (!pendingWait) return;
5754
+ debugOpencode(
5755
+ "\u2605 parent resumed after %dms of background wait",
5756
+ pendingWait.elapsedMs()
5757
+ );
5758
+ endWait();
5759
+ emitTasks(liveChildren(), false);
5760
+ };
5761
+ const onInjectedResult = (childId) => {
5762
+ if (!children.has(childId)) return;
5763
+ setChildLive(childId, false);
5764
+ onParentBusy();
5765
+ };
4843
5766
  try {
4844
- const interactiveApproval = isInteractiveApproval(request.options);
5767
+ const interactiveApproval = !request.options.fullAccess && isInteractiveApproval(request.options);
4845
5768
  let forkedSession = null;
4846
5769
  if (request.run.forkSessionId) {
4847
5770
  forkedSession = await fetchJson(
@@ -4942,6 +5865,12 @@ var OpenCodeAgentAdapter = class {
4942
5865
  const info = properties?.info;
4943
5866
  if (info && typeof info.id === "string" && typeof info.parentID === "string" && runSessionIds.has(info.parentID)) {
4944
5867
  runSessionIds.add(info.id);
5868
+ if (trackChildren && info.parentID === sessionId) {
5869
+ registerChild(
5870
+ info.id,
5871
+ typeof info.title === "string" ? info.title : void 0
5872
+ );
5873
+ }
4945
5874
  }
4946
5875
  }
4947
5876
  if (eventType === "message.updated") {
@@ -4982,6 +5911,32 @@ var OpenCodeAgentAdapter = class {
4982
5911
  }
4983
5912
  }
4984
5913
  }
5914
+ if (eventType === "question.asked") {
5915
+ const properties = payload.properties;
5916
+ if (properties && typeof properties.id === "string" && typeof properties.sessionID === "string" && await resolveRunSession(properties.sessionID)) {
5917
+ const questions = normalizeUserQuestions("open-code", properties);
5918
+ const response = hasInteractiveQuestions(request.options) ? await sink.requestPermission(createNormalizedEvent("permission.requested", {
5919
+ provider: request.provider,
5920
+ runId: request.runId,
5921
+ raw
5922
+ }, {
5923
+ requestId: properties.id,
5924
+ kind: "question",
5925
+ toolName: "question",
5926
+ title: "Your input is needed",
5927
+ input: properties,
5928
+ questions,
5929
+ canRemember: false
5930
+ })) : void 0;
5931
+ const allowed = response?.decision === "allow";
5932
+ await fetchJson(`${runtime.baseUrl}/question/${encodeURIComponent(properties.id)}/${allowed ? "reply" : "reject"}`, {
5933
+ method: "POST",
5934
+ headers: { "content-type": "application/json", ...runtime.previewHeaders },
5935
+ body: JSON.stringify(allowed ? { answers: questionReply("open-code", properties, response.answers ?? []) } : {})
5936
+ });
5937
+ }
5938
+ continue;
5939
+ }
4985
5940
  if (eventType === "permission.asked") {
4986
5941
  const properties = payload.properties;
4987
5942
  if (properties && typeof properties.sessionID === "string" && await resolveRunSession(properties.sessionID)) {
@@ -5024,28 +5979,35 @@ var OpenCodeAgentAdapter = class {
5024
5979
  const errMsg = typeof errData?.data?.message === "string" ? errData.data.message : typeof errData?.message === "string" ? errData.message : "OpenCode session error";
5025
5980
  sessionErrorFromSse = new Error(errMsg);
5026
5981
  }
5982
+ resolveSessionTerminal();
5027
5983
  } else {
5028
- sessionIdleFromSse = true;
5984
+ await onParentIdle();
5029
5985
  }
5030
5986
  debugOpencode(
5031
5987
  "\u2605 %s for session=%s",
5032
5988
  payloadRecord.type,
5033
5989
  sessionId
5034
5990
  );
5035
- resolveSessionTerminal();
5991
+ } else if (trackChildren && payloadRecord.type === "session.idle") {
5992
+ setChildLive(eventSessionId, false);
5036
5993
  }
5037
5994
  }
5038
5995
  if (payloadRecord?.type === "session.status") {
5039
5996
  const properties = payloadRecord.properties;
5040
5997
  const status = properties?.status;
5041
5998
  const eventSessionId = typeof properties?.sessionID === "string" ? properties.sessionID : void 0;
5042
- if ((!eventSessionId || eventSessionId === sessionId) && status?.type === "idle") {
5043
- sessionIdleFromSse = true;
5044
- debugOpencode(
5045
- "\u2605 session.status{idle} for session=%s",
5046
- sessionId
5047
- );
5048
- resolveSessionTerminal();
5999
+ if (!eventSessionId || eventSessionId === sessionId) {
6000
+ if (status?.type === "idle") {
6001
+ debugOpencode(
6002
+ "\u2605 session.status{idle} for session=%s",
6003
+ sessionId
6004
+ );
6005
+ await onParentIdle();
6006
+ } else if (status?.type === "busy" || status?.type === "retry") {
6007
+ onParentBusy();
6008
+ }
6009
+ } else if (trackChildren) {
6010
+ setChildLive(eventSessionId, status?.type !== "idle");
5049
6011
  }
5050
6012
  }
5051
6013
  if (payloadRecord?.type === "message.part.updated") {
@@ -5054,6 +6016,22 @@ var OpenCodeAgentAdapter = class {
5054
6016
  if (part && typeof part.id === "string" && typeof part.type === "string") {
5055
6017
  partTypeById.set(part.id, part.type);
5056
6018
  }
6019
+ if (trackChildren && part?.type === "tool" && part.tool === "task" && part.sessionID === sessionId) {
6020
+ const state = part.state;
6021
+ const metadata = state?.metadata;
6022
+ const childId = metadata?.sessionId ?? metadata?.jobId;
6023
+ if (metadata?.background === true && typeof childId === "string") {
6024
+ registerChild(
6025
+ childId,
6026
+ typeof state?.title === "string" ? state.title : void 0,
6027
+ true
6028
+ );
6029
+ }
6030
+ }
6031
+ if (trackChildren && part?.type === "text" && part.synthetic === true && part.sessionID === sessionId && typeof part.text === "string") {
6032
+ const childId = injectedTaskResultChild(part.text);
6033
+ if (childId) onInjectedResult(childId);
6034
+ }
5057
6035
  }
5058
6036
  if (payloadRecord?.type === "message.part.delta") {
5059
6037
  const properties = payloadRecord.properties;
@@ -5064,6 +6042,9 @@ var OpenCodeAgentAdapter = class {
5064
6042
  if (isForeignSession) {
5065
6043
  continue;
5066
6044
  }
6045
+ if (eventMessageId !== void 0 && announcedUserMessageIds.has(eventMessageId)) {
6046
+ continue;
6047
+ }
5067
6048
  const delta = typeof properties?.delta === "string" ? properties.delta : "";
5068
6049
  const field = typeof properties?.field === "string" ? properties.field : void 0;
5069
6050
  const partType = eventPartId ? partTypeById.get(eventPartId) : void 0;
@@ -5129,6 +6110,12 @@ var OpenCodeAgentAdapter = class {
5129
6110
  })
5130
6111
  );
5131
6112
  const agentSlug = openCodeAgentSlug(request.run.reasoning);
6113
+ if (request.run.goal) throw new Error("Native goals are not supported by OpenCode.");
6114
+ if (request.run.mode) {
6115
+ const agents = await fetchJson(`${runtime.baseUrl}/agent`, { headers: runtime.previewHeaders });
6116
+ const name = request.run.mode === "plan" ? "plan" : "build";
6117
+ if (!agents.some((agent) => agent.name === name)) throw new Error(`This OpenCode installation does not expose the ${name} agent.`);
6118
+ }
5132
6119
  const dispatchPrompt = async (parts) => {
5133
6120
  const body = JSON.stringify({
5134
6121
  ...request.run.model ? { model: toOpenCodeModel(request.run.model) } : {},
@@ -5144,7 +6131,8 @@ var OpenCodeAgentAdapter = class {
5144
6131
  // instead. This per-message field stays as a per-run
5145
6132
  // override path that's effective for codex/GPT/Gemini.
5146
6133
  ...request.run.systemPrompt ? { system: request.run.systemPrompt } : {},
5147
- agent: agentSlug,
6134
+ ...request.options.configuration === "native" ? request.run.reasoning ? { variant: request.run.reasoning } : {} : { agent: agentSlug },
6135
+ ...request.run.mode ? { agent: request.run.mode === "plan" ? "plan" : "build" } : {},
5148
6136
  parts
5149
6137
  });
5150
6138
  const url = `${runtime.baseUrl}/session/${sessionId}/prompt_async`;
@@ -5207,13 +6195,22 @@ var OpenCodeAgentAdapter = class {
5207
6195
  const SSE_POLL_INTERVAL_MS = 5e3;
5208
6196
  lastSseActivityAt = Date.now();
5209
6197
  let sseSilent = false;
5210
- while (!sessionIdleFromSse && !sessionErrorFromSse && !sessionAbortedFromSse && !userAbortRequested && !dispatchError) {
6198
+ while (!sessionIdleFromSse && !sessionErrorFromSse && !sessionAbortedFromSse && !userAbortRequested && !dispatchError && !expiry) {
5211
6199
  const silence = Date.now() - lastSseActivityAt;
5212
6200
  if (silence > SSE_SILENCE_THRESHOLD_MS) {
6201
+ if (pendingWait && sawParentIdle) {
6202
+ debugOpencode(
6203
+ "SSE went silent (%dms) during background wait; settling",
6204
+ silence
6205
+ );
6206
+ expiry = "transport";
6207
+ break;
6208
+ }
5213
6209
  sseSilent = true;
5214
6210
  debugOpencode("SSE went silent (%dms) \u2014 giving up", silence);
5215
6211
  break;
5216
6212
  }
6213
+ if (pendingWait && liveChildren().length > 0) await reconcileChildren();
5217
6214
  await Promise.race([
5218
6215
  sessionTerminal,
5219
6216
  new Promise(
@@ -5223,6 +6220,16 @@ var OpenCodeAgentAdapter = class {
5223
6220
  }
5224
6221
  sseAbort.abort();
5225
6222
  await sseTask;
6223
+ if (expiry === "ceiling" || (sessionErrorFromSse || dispatchError) && liveChildren().length > 0) {
6224
+ debugOpencode(
6225
+ "\u2605 run over (%s) with %d subagent(s) live; aborting them",
6226
+ expiry ?? "failure",
6227
+ liveChildren().length
6228
+ );
6229
+ await postAbort(sessionId);
6230
+ }
6231
+ endWait();
6232
+ emitTasks([], false);
5226
6233
  if (userAbortRequested || sessionAbortedFromSse) {
5227
6234
  debugOpencode(
5228
6235
  "\u2605 run.cancelled (%dms since execute start)",
@@ -5236,15 +6243,15 @@ var OpenCodeAgentAdapter = class {
5236
6243
  sink.fail(sessionErrorFromSse);
5237
6244
  } else if (dispatchError) {
5238
6245
  sink.fail(dispatchError);
5239
- } else if (sessionIdleFromSse) {
6246
+ } else if (sessionIdleFromSse || expiry) {
5240
6247
  debugOpencode(
5241
6248
  "\u2605 run.completed (%dms since execute start) chars=%d",
5242
6249
  Date.now() - executeStartedAt,
5243
6250
  streamedTextFromSse.length
5244
6251
  );
5245
6252
  let lastAssistantText = "";
5246
- for (const [messageId, text] of assistantTextByMessageId) {
5247
- lastAssistantText = text;
6253
+ for (const [messageId, text2] of assistantTextByMessageId) {
6254
+ lastAssistantText = text2;
5248
6255
  if (!announcedAssistantCompletions.has(messageId)) {
5249
6256
  announcedAssistantCompletions.add(messageId);
5250
6257
  sink.emitEvent(
@@ -5254,7 +6261,7 @@ var OpenCodeAgentAdapter = class {
5254
6261
  provider: request.provider,
5255
6262
  runId: request.runId
5256
6263
  },
5257
- { text }
6264
+ { text: text2 }
5258
6265
  )
5259
6266
  );
5260
6267
  }
@@ -5281,6 +6288,7 @@ var OpenCodeAgentAdapter = class {
5281
6288
  sink.fail(new Error("opencode run ended without a terminal signal"));
5282
6289
  }
5283
6290
  } finally {
6291
+ endWait();
5284
6292
  sseAbort.abort();
5285
6293
  if (sseTask) {
5286
6294
  await sseTask.catch(() => void 0);
@@ -5423,6 +6431,18 @@ function createAdapter(provider) {
5423
6431
  }
5424
6432
  }
5425
6433
  function prepareAgentOptions(_provider, options) {
6434
+ if (options.stateDirectory !== void 0) {
6435
+ if (options.sandbox) throw new Error("stateDirectory is only supported for host execution.");
6436
+ if (!path11.isAbsolute(options.stateDirectory)) throw new Error("stateDirectory must be an absolute path.");
6437
+ }
6438
+ if (options.sandbox && options.processGroup !== void 0) throw new Error("processGroup is only supported for host execution.");
6439
+ resolveBackgroundTaskTimeoutMs(options.backgroundTaskTimeoutMs);
6440
+ if (options.configuration === "native") {
6441
+ if (options.sandbox) throw new Error("Native configuration is only supported for host execution.");
6442
+ if (options.mcps?.length || options.skills?.length || options.subAgents?.length || options.commands?.length || options.enableRtk) {
6443
+ throw new Error("Native configuration uses the harness's own skills, MCPs, commands, and hooks.");
6444
+ }
6445
+ }
5426
6446
  return options;
5427
6447
  }
5428
6448
  var AgentRunController = class {
@@ -5552,12 +6572,15 @@ var AgentRunController = class {
5552
6572
  `Permission request ${response.requestId} is not pending for this run.`
5553
6573
  );
5554
6574
  }
6575
+ const answers = pending.event.kind === "question" && response.decision === "allow" ? validateUserAnswers(pending.event.questions, response.answers) : void 0;
6576
+ if (response.answers && !answers) throw new Error("Answers are only accepted for an allowed question request");
5555
6577
  this.pendingPermissions.delete(response.requestId);
5556
6578
  const remember = pending.event.canRemember ? response.remember : void 0;
5557
6579
  const resolvedResponse = {
5558
6580
  requestId: response.requestId,
5559
6581
  decision: response.decision,
5560
- ...remember !== void 0 ? { remember } : {}
6582
+ ...remember !== void 0 ? { remember } : {},
6583
+ ...answers ? { answers } : {}
5561
6584
  };
5562
6585
  this.pushEvent(
5563
6586
  createNormalizedEvent(
@@ -5569,7 +6592,8 @@ var AgentRunController = class {
5569
6592
  {
5570
6593
  requestId: response.requestId,
5571
6594
  decision: response.decision,
5572
- ...remember !== void 0 ? { remember } : {}
6595
+ ...remember !== void 0 ? { remember } : {},
6596
+ ...answers ? { answers } : {}
5573
6597
  }
5574
6598
  )
5575
6599
  );
@@ -5666,6 +6690,10 @@ var AgentRunController = class {
5666
6690
  if (this.settled) {
5667
6691
  return;
5668
6692
  }
6693
+ if (this.abortRequested) {
6694
+ this.cancel();
6695
+ return;
6696
+ }
5669
6697
  const normalizedError = asError(error);
5670
6698
  this.clearPendingPermissions(normalizedError);
5671
6699
  this.emitEvent(
@@ -5700,6 +6728,7 @@ var AgentRunController = class {
5700
6728
  }
5701
6729
  async abort() {
5702
6730
  this.abortRequested = true;
6731
+ this.clearPendingPermissions(new Error("Agent run cancelled"));
5703
6732
  await this.abortHandler();
5704
6733
  }
5705
6734
  rawEvents() {
@@ -5812,7 +6841,7 @@ var Agent = class {
5812
6841
  if (runConfig.forkAtMessageId && !runConfig.forkSessionId) {
5813
6842
  throw new Error("AgentRunConfig.forkAtMessageId requires forkSessionId.");
5814
6843
  }
5815
- const runId = runConfig.runId ?? randomUUID2();
6844
+ const runId = runConfig.runId ?? randomUUID3();
5816
6845
  const streamCalledAt = Date.now();
5817
6846
  debugAgent("stream() provider=%s runId=%s", this.provider, runId);
5818
6847
  const run = new AgentRunController(this.provider, runId);
@@ -5880,8 +6909,33 @@ var Agent = class {
5880
6909
  }
5881
6910
  };
5882
6911
 
6912
+ // src/agents/commands.ts
6913
+ function harnessCapabilities(provider) {
6914
+ return { commands: provider === "open-code" ? ["plan", "agent"] : ["plan", "agent", "goal"], planning: provider === "codex" ? "explicit" : "agent-directed", questions: true, fullAccess: true };
6915
+ }
6916
+ function resolveHarnessCommand(provider, input) {
6917
+ const first = typeof input === "string" ? input : input.find((part) => part.type === "text")?.text;
6918
+ const match = first?.match(/^\s*\/(plan|agent|goal)(?:\s+([\s\S]*))?$/);
6919
+ if (!match) return { input };
6920
+ const command = match[1];
6921
+ if (!harnessCapabilities(provider).commands.includes(command)) throw new Error(`/${command} is not supported by ${provider}.`);
6922
+ const body = match[2]?.trim() ?? "";
6923
+ if (command === "goal" && (!body || body.length > 4e3)) throw new Error("/goal requires an objective of 1\u20134000 characters.");
6924
+ if (command === "goal" && provider === "claude-code") return { input, goal: body };
6925
+ const text2 = body || (command === "plan" ? "Plan the requested work." : "Continue with implementation.");
6926
+ let replaced = false;
6927
+ const cleaned = typeof input === "string" ? text2 : input.map((part) => {
6928
+ if (part.type !== "text" || replaced) return part;
6929
+ replaced = true;
6930
+ return { ...part, text: text2 };
6931
+ });
6932
+ return command === "goal" ? { input: cleaned, goal: body } : { input: cleaned, mode: command === "plan" ? "plan" : "default" };
6933
+ }
6934
+
5883
6935
  export {
5884
6936
  agentboxRoot,
5885
6937
  getAgentLayout,
5886
- Agent
6938
+ Agent,
6939
+ harnessCapabilities,
6940
+ resolveHarnessCommand
5887
6941
  };