agentbox-sdk 0.1.508 → 0.1.512

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.
@@ -1825,8 +1825,203 @@ function extractOpenCodeCostData(events) {
1825
1825
  }) : null;
1826
1826
  }
1827
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
+
1828
2023
  // src/agents/providers/claude-code.ts
1829
- var DAEMON_PROTOCOL_VERSION = "4";
2024
+ var DAEMON_PROTOCOL_VERSION = "5";
1830
2025
  var DAEMON_PORT = 43180;
1831
2026
  var DAEMON_PATH = "/tmp/agentbox/claude-code/daemon.mjs";
1832
2027
  var DAEMON_LOG_PATH = "/tmp/agentbox/claude-code/daemon.log";
@@ -1856,12 +2051,16 @@ function buildClaudeQueryOptions(params) {
1856
2051
  extraArgs["append-system-prompt"] = run.systemPrompt;
1857
2052
  }
1858
2053
  const includeHookEvents = provider?.includeHookEvents ?? false;
2054
+ if (run.reasoning === "max" || run.reasoning === "ultra") {
2055
+ throw new Error(`Reasoning effort "${run.reasoning}" is only supported by Codex.`);
2056
+ }
1859
2057
  const effort = provider?.ultracode ? "xhigh" : run.reasoning;
1860
2058
  return {
1861
2059
  cwd: params.cwd ?? params.request.options.cwd,
1862
2060
  env: params.env,
1863
2061
  pathToClaudeCodeExecutable: provider?.binary ?? "claude",
1864
2062
  ...params.settingsPath ? { settings: params.settingsPath } : {},
2063
+ ...params.request.options.configuration === "native" && provider?.fastMode !== void 0 ? { settings: { fastMode: provider.fastMode } } : {},
1865
2064
  ...params.request.options.configuration === "native" ? {
1866
2065
  settingSources: ["user", "project", "local"],
1867
2066
  systemPrompt: { type: "preset", preset: "claude_code" }
@@ -2091,6 +2290,29 @@ async function handleStart(req, res, runId) {
2091
2290
  opts.pathToClaudeCodeExecutable,
2092
2291
  );
2093
2292
 
2293
+ let queryHandle;
2294
+ let clientGone = false;
2295
+ // This run's own teardown. The liveRuns entry is only removed when it is
2296
+ // still ours: hosts reuse a runId across retry attempts, and a successor
2297
+ // registered while our CLI winds down must not be evicted by our exit.
2298
+ const releaseRun = () => {
2299
+ clearInterval(heartbeat);
2300
+ clearPermissions();
2301
+ if (liveRuns.get(runId)?.query === queryHandle) liveRuns.delete(runId);
2302
+ promptStream.end();
2303
+ };
2304
+ // Host gone (settled, cancelled or crashed) \u2192 end the prompt so the CLI
2305
+ // winds down instead of living on with its background work. Detected on
2306
+ // the response: \`req\` emits "close" as soon as its body is consumed (Node
2307
+ // >= 16), long before any disconnect, while the response only closes
2308
+ // early when the socket dies before the stream finished.
2309
+ res.on("close", () => {
2310
+ if (res.writableFinished) return;
2311
+ clientGone = true;
2312
+ releaseRun();
2313
+ queryHandle?.interrupt().catch(() => {});
2314
+ });
2315
+
2094
2316
  // Resume-if-exists gate. \`claude --resume <id>\` errors hard with "No
2095
2317
  // conversation found with session ID" when the local session jsonl is
2096
2318
  // missing \u2014 most often because a prior post-task snapshot failed and the
@@ -2121,7 +2343,8 @@ async function handleStart(req, res, runId) {
2121
2343
  }
2122
2344
  }
2123
2345
 
2124
- let queryHandle;
2346
+ // Nobody left to stream to: do not start a CLI for it.
2347
+ if (clientGone) { res.end(); return; }
2125
2348
  try {
2126
2349
  queryHandle = query({
2127
2350
  prompt: promptStream,
@@ -2144,28 +2367,19 @@ async function handleStart(req, res, runId) {
2144
2367
 
2145
2368
  liveRuns.set(runId, { query: queryHandle, prompt: promptStream, permissions });
2146
2369
 
2147
- // Client disconnected (e.g. host process killed) \u2192 tear down.
2148
- req.on("close", () => {
2149
- clearInterval(heartbeat);
2150
- clearPermissions();
2151
- if (!liveRuns.has(runId)) return;
2152
- liveRuns.delete(runId);
2153
- promptStream.end();
2154
- queryHandle.interrupt().catch(() => {});
2155
- });
2156
-
2370
+ // Forward every SDKMessage, not just up to the first result: in
2371
+ // streaming-input mode the CLI keeps running after a turn ends and
2372
+ // re-prompts the model when background work finishes. The host decides
2373
+ // when the run is over and disconnects (res "close" above), which ends
2374
+ // the prompt and lets the CLI wind down.
2157
2375
  try {
2158
2376
  for await (const message of queryHandle) {
2159
2377
  res.write(JSON.stringify(message) + "\\n");
2160
- if (message.type === "result") break;
2161
2378
  }
2162
2379
  } catch (e) {
2163
2380
  res.write(JSON.stringify({ _error: String(e?.message ?? e) }) + "\\n");
2164
2381
  } finally {
2165
- clearInterval(heartbeat);
2166
- clearPermissions();
2167
- liveRuns.delete(runId);
2168
- promptStream.end();
2382
+ releaseRun();
2169
2383
  res.end();
2170
2384
  }
2171
2385
  }
@@ -2494,7 +2708,11 @@ var ClaudeCodeAgentAdapter = class {
2494
2708
  const workflowSettings = buildClaudeWorkflowSettings(
2495
2709
  options.provider?.ultracode
2496
2710
  );
2497
- const claudeSettings = { ...hookSettings, ...workflowSettings };
2711
+ const claudeSettings = {
2712
+ ...hookSettings,
2713
+ ...workflowSettings,
2714
+ ...options.provider?.fastMode !== void 0 ? { fastMode: options.provider.fastMode } : {}
2715
+ };
2498
2716
  const mcpConfigJson = buildClaudeMcpConfig(options.mcps) ?? JSON.stringify({ mcpServers: {} }, null, 2);
2499
2717
  const artifacts = [
2500
2718
  ...!sandbox ? [{ path: path8.join(target.layout.claudeDir, ".claude-plugin", "plugin.json"), content: JSON.stringify({ name: "agentbox", version: "1.0.0" }) }] : [],
@@ -2555,6 +2773,7 @@ var ClaudeCodeAgentAdapter = class {
2555
2773
  // user inside our images.
2556
2774
  IS_SANDBOX: "1"
2557
2775
  };
2776
+ applyCliBackgroundWaitCeiling(env);
2558
2777
  const customHeaders = request.options.customHeaders;
2559
2778
  if (customHeaders && Object.keys(customHeaders).length > 0) {
2560
2779
  const serialized = Object.entries(customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
@@ -2604,17 +2823,23 @@ ${serialized}` : serialized;
2604
2823
  }
2605
2824
  };
2606
2825
  const fetchAbort = new AbortController();
2826
+ const runUrl = `${baseUrl}/runs/${encodeURIComponent(request.runId)}`;
2607
2827
  const cleanup = async () => {
2608
2828
  try {
2609
- await fetch(
2610
- `${baseUrl}/runs/${encodeURIComponent(request.runId)}/abort`,
2611
- { method: "POST", headers: authHeaders }
2612
- );
2829
+ await fetch(`${runUrl}/abort`, { method: "POST", headers: authHeaders });
2830
+ } catch {
2831
+ }
2832
+ try {
2833
+ await fetch(runUrl, { method: "DELETE", headers: authHeaders, signal: AbortSignal.timeout(3e3) });
2613
2834
  } catch {
2614
2835
  }
2615
2836
  fetchAbort.abort();
2616
2837
  };
2617
- sink.setAbort(cleanup);
2838
+ let cancelled = false;
2839
+ sink.setAbort(async () => {
2840
+ cancelled = true;
2841
+ await cleanup();
2842
+ });
2618
2843
  sink.onMessage(async (content) => {
2619
2844
  const parts = await validateProviderUserInput(request.provider, content);
2620
2845
  const mapped = mapToClaudeUserContent(parts);
@@ -2680,7 +2905,7 @@ ${serialized}` : serialized;
2680
2905
  if (!reply.ok) throw new Error(`Claude permission response failed: ${reply.status}`);
2681
2906
  }
2682
2907
  };
2683
- await consumeClaudeMessages(request, sink, permissionMessages(), executeStartedAt, cleanup);
2908
+ await consumeClaudeMessages(request, sink, permissionMessages(), executeStartedAt, cleanup, () => cancelled);
2684
2909
  return async () => void 0;
2685
2910
  }
2686
2911
  /**
@@ -2741,24 +2966,66 @@ ${serialized}` : serialized;
2741
2966
  }
2742
2967
  }
2743
2968
  };
2744
- async function consumeClaudeMessages(request, sink, messages, executeStartedAt, cleanup, wasCancelled = () => false) {
2969
+ async function consumeClaudeMessages(request, sink, messages, executeStartedAt, cleanup, wasCancelled = () => false, wait = {}) {
2745
2970
  let accumulatedText = "";
2746
2971
  let streamedThinkingChars = 0;
2747
- let pendingMessages = 1;
2748
2972
  let sawResult = false;
2749
2973
  let firstStreamEventLogged = false;
2750
2974
  let firstTextDeltaLogged = false;
2751
2975
  let lastTerminalReason;
2752
2976
  let lastIsError = false;
2753
2977
  const rawPayloads = [];
2978
+ const tracker = new BackgroundTaskTracker();
2979
+ const timeoutMs = resolveBackgroundTaskTimeoutMs(request.options.backgroundTaskTimeoutMs);
2980
+ const graceMs = wait.graceMs ?? BACKGROUND_TASK_GRACE_MS;
2981
+ let pendingWait;
2982
+ let waitedMs = 0;
2983
+ let expiry;
2984
+ let lastTasksKey = JSON.stringify({ tasks: [], waiting: false });
2985
+ const emitTasks = (tasks, waiting) => {
2986
+ const key = JSON.stringify({ tasks, waiting });
2987
+ if (key === lastTasksKey) return;
2988
+ lastTasksKey = key;
2989
+ sink.emitEvent(createNormalizedEvent("background.tasks", { provider: request.provider, runId: request.runId }, { tasks, waiting }));
2990
+ };
2991
+ const isAborted = () => wasCancelled() || lastTerminalReason === "aborted_streaming" || lastTerminalReason === "aborted_tools";
2992
+ const endWait = () => {
2993
+ if (!pendingWait) return;
2994
+ waitedMs += pendingWait.elapsedMs();
2995
+ pendingWait.clear();
2996
+ pendingWait = void 0;
2997
+ };
2998
+ const settleOnFailure = (error) => {
2999
+ if (!pendingWait || !sawResult || lastIsError) return false;
3000
+ debugClaude("\u2605 transport failed during background wait; settling on the last result: %o", error);
3001
+ expiry = "transport";
3002
+ return true;
3003
+ };
3004
+ const iterator = messages[Symbol.asyncIterator]();
2754
3005
  try {
2755
- for await (const item of messages) {
3006
+ for (let next = iterator.next(); ; next = iterator.next()) {
3007
+ let step;
3008
+ try {
3009
+ step = pendingWait ? await Promise.race([
3010
+ next.then((result) => ({ result })),
3011
+ pendingWait.expired.then((reason) => ({ reason }))
3012
+ ]) : { result: await next };
3013
+ } catch (error) {
3014
+ if (!settleOnFailure(error)) throw error;
3015
+ break;
3016
+ }
3017
+ if ("reason" in step) {
3018
+ expiry = step.reason;
3019
+ break;
3020
+ }
3021
+ if (step.result.done) break;
3022
+ const item = step.result.value;
2756
3023
  if (item && typeof item === "object") {
2757
3024
  const ctrl = item;
2758
3025
  if ("_error" in ctrl) {
2759
- throw new Error(
2760
- String(item._error ?? "daemon error")
2761
- );
3026
+ const error = new Error(String(ctrl._error ?? "daemon error"));
3027
+ if (!settleOnFailure(error)) throw error;
3028
+ break;
2762
3029
  }
2763
3030
  if ("_notice" in ctrl) {
2764
3031
  debugClaude("daemon notice: %o", ctrl);
@@ -2775,6 +3042,12 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
2775
3042
  const message = item;
2776
3043
  rawPayloads.push(message);
2777
3044
  sink.emitRaw(toRawEvent(request.runId, message, message.type));
3045
+ if (tracker.ingest(message) && pendingWait) {
3046
+ debugClaude("\u2605 follow-up turn started; background wait over (%dms since execute start)", Date.now() - executeStartedAt);
3047
+ endWait();
3048
+ }
3049
+ emitTasks(tracker.liveTasks(), pendingWait !== void 0);
3050
+ pendingWait?.setIdle(tracker.liveTasks().length === 0);
2778
3051
  if (message.type === "system") {
2779
3052
  const sub = message.subtype;
2780
3053
  if (sub === "init") {
@@ -2891,15 +3164,33 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
2891
3164
  if (resultText && resultText !== accumulatedText) {
2892
3165
  accumulatedText = resultText;
2893
3166
  }
2894
- pendingMessages--;
2895
- if (pendingMessages <= 0) break;
3167
+ const live = tracker.liveTasks();
3168
+ if (timeoutMs === 0 || !tracker.hasSeenBackgroundWork() || isAborted()) break;
3169
+ debugClaude("\u2605 turn ended with %d background task(s); waiting", live.length);
3170
+ endWait();
3171
+ pendingWait = new BackgroundWait(graceMs, Math.max(0, timeoutMs - waitedMs));
3172
+ pendingWait.setIdle(live.length === 0);
3173
+ emitTasks(live, true);
2896
3174
  continue;
2897
3175
  }
2898
3176
  }
3177
+ if (expiry === "ceiling") {
3178
+ const ids = tracker.liveTasks().filter((task) => task.type !== "scheduled_wakeup").map((task) => task.id);
3179
+ debugClaude("\u2605 background wait ceiling (%dms) hit; stopping %d task(s)", timeoutMs, ids.length);
3180
+ if (wait.stopTasks) {
3181
+ await withTimeout(wait.stopTasks(ids), wait.stopTimeoutMs ?? STOP_TASKS_TIMEOUT_MS).catch(() => void 0);
3182
+ }
3183
+ } else if (expiry === "grace") {
3184
+ debugClaude("\u2605 background set emptied with no follow-up turn; settling");
3185
+ }
3186
+ if (pendingWait) {
3187
+ endWait();
3188
+ emitTasks([], false);
3189
+ }
2899
3190
  await cleanup();
2900
3191
  if (!sawResult && !wasCancelled()) throw new Error("Claude Code closed before reporting a result");
2901
3192
  const finalText = accumulatedText;
2902
- const isCancelled = wasCancelled() || lastTerminalReason === "aborted_streaming" || lastTerminalReason === "aborted_tools";
3193
+ const isCancelled = isAborted();
2903
3194
  const isError = !isCancelled && lastIsError;
2904
3195
  if (isCancelled) {
2905
3196
  debugClaude(
@@ -2941,10 +3232,11 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
2941
3232
  });
2942
3233
  }
2943
3234
  } finally {
3235
+ pendingWait?.clear();
2944
3236
  await cleanup();
2945
3237
  }
2946
3238
  }
2947
- async function executeNativeClaude(request, sink) {
3239
+ async function executeNativeClaude(request, sink, wait = {}) {
2948
3240
  const { query } = await import("@anthropic-ai/claude-agent-sdk");
2949
3241
  const claudeDir = claudeConfigDir(request.options);
2950
3242
  const input = await validateProviderUserInput(request.provider, request.run.input);
@@ -2969,6 +3261,7 @@ async function executeNativeClaude(request, sink) {
2969
3261
  const messageId = randomUUID();
2970
3262
  prompt.push({ type: "user", uuid: messageId, message: { role: "user", content: mapToClaudeUserContent(input) }, parent_tool_use_id: null });
2971
3263
  const hostEnv = Object.fromEntries(Object.entries({ ...process.env, ...request.options.env }).filter((entry) => entry[1] !== void 0));
3264
+ applyCliBackgroundWaitCeiling(hostEnv);
2972
3265
  if (request.options.customHeaders) {
2973
3266
  const headers = Object.entries(request.options.customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
2974
3267
  hostEnv.ANTHROPIC_CUSTOM_HEADERS = [hostEnv.ANTHROPIC_CUSTOM_HEADERS, headers].filter(Boolean).join("\n");
@@ -3041,10 +3334,17 @@ async function executeNativeClaude(request, sink) {
3041
3334
  }
3042
3335
  }
3043
3336
  } });
3044
- sink.setRaw({ query: handle, claudeDir, runId: request.runId });
3337
+ const live = handle;
3338
+ sink.setRaw({ query: live, claudeDir, runId: request.runId });
3045
3339
  sink.emitEvent(createNormalizedEvent("run.started", { provider: request.provider, runId: request.runId }));
3046
3340
  sink.emitEvent(createNormalizedEvent("message.started", { provider: request.provider, runId: request.runId }, { messageId }));
3047
- await consumeClaudeMessages(request, sink, handle, Date.now(), stop, () => cancelled);
3341
+ await consumeClaudeMessages(request, sink, live, Date.now(), stop, () => cancelled, {
3342
+ ...wait,
3343
+ // Native owns the CLI: ask it to stop leftover tasks before closing it.
3344
+ stopTasks: async (ids) => {
3345
+ await Promise.all(ids.map((id) => live.stopTask(id).catch(() => void 0)));
3346
+ }
3347
+ });
3048
3348
  } catch (error) {
3049
3349
  await stop();
3050
3350
  if (cancelled) sink.cancel();
@@ -3350,7 +3650,8 @@ function buildThreadParams(cwd, options, request) {
3350
3650
  return {
3351
3651
  cwd,
3352
3652
  model: request.run.model ?? null,
3353
- ...options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3653
+ ...request.options.provider?.serviceTier !== void 0 ? { serviceTier: request.options.provider.serviceTier } : {},
3654
+ ...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3354
3655
  sandbox: buildCodexSandboxMode(options),
3355
3656
  serviceName: "agentbox",
3356
3657
  // Persist the rollout on disk so follow-up runs can call `thread/resume`.
@@ -3365,7 +3666,8 @@ function buildResumeParams(cwd, options, request) {
3365
3666
  threadId: request.run.resumeSessionId,
3366
3667
  cwd,
3367
3668
  model: request.run.model ?? null,
3368
- ...options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3669
+ ...request.options.provider?.serviceTier !== void 0 ? { serviceTier: request.options.provider.serviceTier } : {},
3670
+ ...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3369
3671
  sandbox: buildCodexSandboxMode(options),
3370
3672
  ...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null },
3371
3673
  // We only need the thread id back; we never read `thread.turns`.
@@ -3381,7 +3683,8 @@ function buildForkParams(cwd, options, request) {
3381
3683
  lastTurnId: request.run.forkAtMessageId ?? null,
3382
3684
  cwd,
3383
3685
  model: request.run.model ?? null,
3384
- ...options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3686
+ ...request.options.provider?.serviceTier !== void 0 ? { serviceTier: request.options.provider.serviceTier } : {},
3687
+ ...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3385
3688
  sandbox: buildCodexSandboxMode(options),
3386
3689
  ...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null },
3387
3690
  excludeTurns: true
@@ -3415,11 +3718,12 @@ function buildCodexTurnStartParams(params) {
3415
3718
  return {
3416
3719
  threadId,
3417
3720
  input: inputItems,
3418
- ...request.options.configuration === "native" && !request.options.fullAccess ? {} : {
3721
+ ...request.options.provider?.approvalPolicy ? { approvalPolicy: request.options.provider.approvalPolicy } : request.options.configuration === "native" && !request.options.fullAccess ? {} : {
3419
3722
  approvalPolicy: !request.options.fullAccess && isInteractiveApproval(request.options) ? "untrusted" : "never"
3420
3723
  },
3421
3724
  ...sandboxPolicy ? { sandboxPolicy } : {},
3422
3725
  model: request.run.model ?? null,
3726
+ ...request.options.provider?.serviceTier !== void 0 ? { serviceTier: request.options.provider.serviceTier } : {},
3423
3727
  effort: request.run.reasoning ?? null,
3424
3728
  ...request.run.mode ? { collaborationMode: {
3425
3729
  mode: request.run.mode,
@@ -3428,6 +3732,23 @@ function buildCodexTurnStartParams(params) {
3428
3732
  outputSchema: null
3429
3733
  };
3430
3734
  }
3735
+ var CODEX_CANCEL_TURN_TEXT = "Run cancelled by the host.";
3736
+ async function terminateBackgroundTerminals(client, threadId) {
3737
+ await withTimeout((async () => {
3738
+ try {
3739
+ const listed = await client.request(
3740
+ "thread/backgroundTerminals/list",
3741
+ { threadId }
3742
+ );
3743
+ for (const terminal of listed?.data ?? []) {
3744
+ if (typeof terminal?.processId !== "string") continue;
3745
+ await client.request("thread/backgroundTerminals/terminate", { threadId, processId: terminal.processId });
3746
+ }
3747
+ } catch (error) {
3748
+ debugCodex("background terminal termination stopped early: %o", error);
3749
+ }
3750
+ })(), STOP_TASKS_TIMEOUT_MS);
3751
+ }
3431
3752
  function toRawEvent2(runId, payload, type) {
3432
3753
  return {
3433
3754
  provider: AgentProvider.Codex,
@@ -3584,6 +3905,42 @@ function createCodexPermissionEvent(request, notification, fileChanges) {
3584
3905
  }
3585
3906
  return null;
3586
3907
  }
3908
+ var CODEX_ELICITATION_METHOD = "mcpServer/elicitation/request";
3909
+ function createCodexElicitationPermissionEvent(request, notification) {
3910
+ if (notification.method !== CODEX_ELICITATION_METHOD || notification.id === void 0) {
3911
+ return null;
3912
+ }
3913
+ const params = notification.params ?? {};
3914
+ const meta = params._meta ?? {};
3915
+ if (meta.codex_approval_kind !== "mcp_tool_call") {
3916
+ return null;
3917
+ }
3918
+ const raw = toRawEvent2(request.runId, notification, notification.method);
3919
+ const toolName = typeof meta.tool_name === "string" ? meta.tool_name : void 0;
3920
+ const server = typeof params.serverName === "string" ? params.serverName : void 0;
3921
+ const persist = Array.isArray(meta.persist) ? meta.persist : [];
3922
+ return createNormalizedEvent(
3923
+ "permission.requested",
3924
+ { provider: request.provider, runId: request.runId, raw },
3925
+ {
3926
+ requestId: String(notification.id),
3927
+ kind: "tool",
3928
+ toolName: toolName ?? server,
3929
+ title: "Approve tool call",
3930
+ message: typeof params.message === "string" && params.message.trim() ? params.message : `Codex wants to call ${toolName ?? "an MCP tool"}${server ? ` on ${server}` : ""}.`,
3931
+ input: { server, tool: toolName, arguments: meta.tool_params, ...params },
3932
+ canRemember: persist.includes("session")
3933
+ }
3934
+ );
3935
+ }
3936
+ function toCodexElicitationResult(notification, response) {
3937
+ if (response.decision === "deny") {
3938
+ return { action: "decline", content: null };
3939
+ }
3940
+ const meta = notification.params?._meta ?? {};
3941
+ const persist = Array.isArray(meta.persist) ? meta.persist : [];
3942
+ return response.remember && persist.includes("session") ? { action: "accept", content: null, _meta: { persist: "session" } } : { action: "accept", content: null };
3943
+ }
3587
3944
  function toCodexApprovalDecision(notification, response) {
3588
3945
  const params = notification.params ?? {};
3589
3946
  const availableDecisions = Array.isArray(params.availableDecisions) ? params.availableDecisions : [];
@@ -4149,7 +4506,6 @@ var CodexAgentAdapter = class {
4149
4506
  const interactiveApproval = isInteractiveApproval(request.options);
4150
4507
  let rootThreadId;
4151
4508
  let turnId;
4152
- let pendingTurns = 1;
4153
4509
  let abortInvoked = false;
4154
4510
  sink.setAbort(async () => {
4155
4511
  abortInvoked = true;
@@ -4201,10 +4557,63 @@ var CodexAgentAdapter = class {
4201
4557
  const pendingFileChanges = /* @__PURE__ */ new Map();
4202
4558
  const fileItemKey = (params, itemId) => typeof params?.threadId === "string" && typeof params.turnId === "string" && typeof itemId === "string" ? `${params.threadId}:${params.turnId}:${itemId}` : void 0;
4203
4559
  let streamedText = "";
4560
+ const timeoutMs = resolveBackgroundTaskTimeoutMs(request.options.backgroundTaskTimeoutMs);
4561
+ const isRootThread = (params) => !params?.threadId || params.threadId === rootThreadId;
4562
+ let goalStatus = request.run.goal ? "active" : void 0;
4563
+ let pendingWait;
4564
+ let waitedMs = 0;
4565
+ let turnMessageText = "";
4566
+ let lastTurn;
4567
+ let goalWaiting = false;
4568
+ const emitGoalWaiting = (waiting) => {
4569
+ if (waiting === goalWaiting) return;
4570
+ goalWaiting = waiting;
4571
+ sink.emitEvent(createNormalizedEvent("background.tasks", { provider: request.provider, runId: request.runId }, { tasks: [], waiting }));
4572
+ };
4573
+ const endWait = () => {
4574
+ if (!pendingWait) return;
4575
+ waitedMs += pendingWait.elapsedMs();
4576
+ pendingWait.clear();
4577
+ pendingWait = void 0;
4578
+ emitGoalWaiting(false);
4579
+ };
4204
4580
  const completion = new Promise((resolve, reject) => {
4581
+ const settle = () => {
4582
+ endWait();
4583
+ sink.emitEvent(createNormalizedEvent("run.completed", { provider: request.provider, runId: request.runId }, { text: lastTurn?.messageText || void 0 }));
4584
+ resolve({ text: lastTurn?.text ?? streamedText, turnId, threadId: rootThreadId, interrupted: false });
4585
+ };
4586
+ const settleOnFailure = (error) => {
4587
+ if (!pendingWait || !lastTurn || abortInvoked) return false;
4588
+ debugCodex("\u2605 transport failed during goal wait; settling on the last turn: %o", error);
4589
+ settle();
4590
+ return true;
4591
+ };
4205
4592
  void (async () => {
4206
4593
  let firstClientMessageLogged = false;
4207
- for await (const message of client.messages()) {
4594
+ const iterator = client.messages()[Symbol.asyncIterator]();
4595
+ for (let next = iterator.next(); ; next = iterator.next()) {
4596
+ let step;
4597
+ try {
4598
+ step = pendingWait ? await Promise.race([
4599
+ next.then((result) => ({ result })),
4600
+ pendingWait.expired.then((reason) => ({ reason }))
4601
+ ]) : { result: await next };
4602
+ } catch (error) {
4603
+ if (settleOnFailure(error)) return;
4604
+ throw error;
4605
+ }
4606
+ if ("reason" in step) {
4607
+ if (!abortInvoked) {
4608
+ debugCodex("\u2605 native goal wait over (%s)", step.reason);
4609
+ settle();
4610
+ return;
4611
+ }
4612
+ endWait();
4613
+ step = { result: await next };
4614
+ }
4615
+ if (step.result.done) break;
4616
+ const message = step.result.value;
4208
4617
  if (!firstClientMessageLogged) {
4209
4618
  firstClientMessageLogged = true;
4210
4619
  debugCodex(
@@ -4260,43 +4669,92 @@ var CodexAgentAdapter = class {
4260
4669
  if (approvalKey) pendingFileChanges.delete(approvalKey);
4261
4670
  continue;
4262
4671
  }
4672
+ const elicitation = createCodexElicitationPermissionEvent(request, message);
4673
+ if (elicitation && message.id !== void 0) {
4674
+ const response = interactiveApproval ? await sink.requestPermission(elicitation) : { requestId: elicitation.requestId, decision: "allow" };
4675
+ await client.respond(message.id, toCodexElicitationResult(message, response));
4676
+ continue;
4677
+ }
4678
+ if (message.id !== void 0) {
4679
+ debugCodex("unsupported server request %s; declining", message.method);
4680
+ await (message.method === CODEX_ELICITATION_METHOD ? client.respond(message.id, { action: "cancel", content: null }) : client.respondError(message.id, {
4681
+ code: -32601,
4682
+ message: `Unsupported request ${message.method}`
4683
+ }));
4684
+ continue;
4685
+ }
4263
4686
  if (message.method === "item/completed") {
4264
4687
  const item2 = message.params?.item;
4265
4688
  if (item2?.type === "plan" && typeof item2.text === "string") {
4266
4689
  sink.emitEvent(createNormalizedEvent("plan.completed", { provider: request.provider, runId: request.runId }, { text: item2.text }));
4267
4690
  }
4268
4691
  }
4692
+ if (rootThreadId && message.params && message.params.threadId === rootThreadId) {
4693
+ if (message.method === "thread/goal/updated") {
4694
+ const goal = message.params.goal;
4695
+ if (!message.params.turnId || message.params.turnId === turnId) {
4696
+ goalStatus = typeof goal?.status === "string" ? goal.status : void 0;
4697
+ if (goalStatus !== "active" && pendingWait && !abortInvoked) {
4698
+ settle();
4699
+ return;
4700
+ }
4701
+ }
4702
+ } else if (message.method === "thread/goal/cleared") {
4703
+ goalStatus = void 0;
4704
+ if (pendingWait && !abortInvoked) {
4705
+ settle();
4706
+ return;
4707
+ }
4708
+ }
4709
+ }
4710
+ const turn = message.params?.turn;
4711
+ const rootTurnCompleted = message.method === "turn/completed" && isRootThread(message.params);
4712
+ const waitAfterTurn = rootTurnCompleted && timeoutMs !== 0 && !abortInvoked && turn?.status === "completed" && goalStatus === "active";
4269
4713
  for (const event of toNormalizedCodexEvents(request.runId, message)) {
4714
+ if (event.type === "run.completed" && (!rootTurnCompleted || waitAfterTurn || turn?.status === "interrupted")) continue;
4270
4715
  sink.emitEvent(event);
4271
4716
  if (event.type === "text.delta") {
4272
4717
  streamedText += event.delta;
4718
+ } else if (event.type === "message.completed" && event.text) {
4719
+ turnMessageText = event.text;
4273
4720
  }
4274
4721
  }
4275
4722
  if (message.method === "thread/started" && !rootThreadId) {
4276
4723
  rootThreadId = message.params?.thread?.id ?? rootThreadId;
4277
4724
  }
4278
4725
  if (message.method === "turn/started") {
4279
- turnId = message.params?.turn?.id ?? turnId;
4726
+ if (isRootThread(message.params)) {
4727
+ turnId = turn?.id ?? turnId;
4728
+ streamedText = "";
4729
+ turnMessageText = "";
4730
+ if (pendingWait) debugCodex("\u2605 follow-up turn started; native goal wait over");
4731
+ endWait();
4732
+ }
4280
4733
  }
4281
- if (message.method === "turn/completed" && (!message.params?.threadId || message.params.threadId === rootThreadId)) {
4282
- pendingTurns--;
4283
- if (pendingTurns <= 0) {
4284
- const turn = message.params?.turn;
4285
- const interrupted = turn?.status === "interrupted";
4734
+ if (rootTurnCompleted) {
4735
+ const previousText = lastTurn?.text;
4736
+ lastTurn = { text: streamedText, messageText: turnMessageText };
4737
+ if (!waitAfterTurn) {
4286
4738
  resolve({
4287
- text: streamedText,
4739
+ text: streamedText || previousText,
4288
4740
  turnId,
4289
4741
  threadId: rootThreadId,
4290
- interrupted
4742
+ interrupted: turn?.status === "interrupted"
4291
4743
  });
4292
4744
  return;
4293
4745
  }
4746
+ debugCodex("\u2605 turn ended with an active goal; waiting for native continuation");
4747
+ endWait();
4748
+ pendingWait = new BackgroundWait(BACKGROUND_TASK_GRACE_MS, Math.max(0, timeoutMs - waitedMs));
4749
+ pendingWait.setIdle(true);
4750
+ emitGoalWaiting(true);
4294
4751
  }
4295
4752
  if (message.method === "error" && !shouldIgnoreCodexError(message)) {
4296
4753
  reject(message);
4297
4754
  return;
4298
4755
  }
4299
4756
  }
4757
+ if (settleOnFailure(new Error("Codex transport closed."))) return;
4300
4758
  reject(new Error("Codex transport closed before run completed."));
4301
4759
  })().catch(reject);
4302
4760
  });
@@ -4367,6 +4825,7 @@ var CodexAgentAdapter = class {
4367
4825
  } catch (err) {
4368
4826
  completionError = err;
4369
4827
  }
4828
+ endWait();
4370
4829
  if (completionError !== void 0) {
4371
4830
  if (abortInvoked) {
4372
4831
  debugCodex(
@@ -4374,7 +4833,7 @@ var CodexAgentAdapter = class {
4374
4833
  Date.now() - executeStartedAt
4375
4834
  );
4376
4835
  sink.cancel({
4377
- text: streamedText || void 0,
4836
+ text: streamedText || lastTurn?.text || void 0,
4378
4837
  costData: extractCodexCostData(rawPayloads)
4379
4838
  });
4380
4839
  } else {
@@ -4399,6 +4858,7 @@ var CodexAgentAdapter = class {
4399
4858
  }
4400
4859
  }
4401
4860
  } finally {
4861
+ pendingWait?.clear();
4402
4862
  await runtime.cleanup().catch(() => void 0);
4403
4863
  }
4404
4864
  return async () => void 0;
@@ -4411,36 +4871,51 @@ var CodexAgentAdapter = class {
4411
4871
  * driven by the normalized `message.started` event whose
4412
4872
  * `messageId` IS the codex turnId).
4413
4873
  *
4414
- * If `sessionId` or `turnId` is missing the call is a no-op.
4874
+ * When that interrupt is rejected the thread is between native goal
4875
+ * turns, or the turn id is stale or missing — a turn is started only to be
4876
+ * interrupted, so the originating run still observes an interrupted turn
4877
+ * and cancels; the model's leftover processes are then terminated.
4878
+ * Without `sessionId` the call is a no-op.
4415
4879
  */
4416
4880
  async attachAbort(request) {
4417
4881
  const threadId = request.sessionId;
4418
- const turnId = request.turnId;
4419
- if (!threadId || !turnId) {
4420
- debugCodex(
4421
- "attachAbort runId=%s skipped: threadId=%s turnId=%s",
4422
- request.runId,
4423
- threadId,
4424
- turnId
4425
- );
4882
+ if (!threadId) {
4883
+ debugCodex("attachAbort runId=%s skipped: no threadId", request.runId);
4426
4884
  return;
4427
4885
  }
4428
4886
  await withCodexAppServer(request, async (client) => {
4429
- await Promise.race([
4430
- client.request("turn/interrupt", { threadId, turnId }),
4887
+ const bounded = (what, promise) => Promise.race([
4888
+ promise,
4431
4889
  new Promise(
4432
- (_, reject) => setTimeout(
4433
- () => reject(new Error("codex turn/interrupt timed out")),
4434
- 3e3
4435
- )
4890
+ (_, reject) => setTimeout(() => reject(new Error(`codex ${what} timed out`)), 3e3)
4436
4891
  )
4437
- ]).catch((error) => {
4438
- debugCodex(
4439
- "attachAbort runId=%s turn/interrupt failed: %o",
4440
- request.runId,
4441
- error
4892
+ ]);
4893
+ const interrupt = (turnId) => bounded("turn/interrupt", client.request("turn/interrupt", { threadId, turnId }));
4894
+ if (request.turnId) {
4895
+ try {
4896
+ await interrupt(request.turnId);
4897
+ return;
4898
+ } catch (error) {
4899
+ debugCodex("attachAbort runId=%s turn/interrupt failed: %o", request.runId, error);
4900
+ }
4901
+ }
4902
+ try {
4903
+ const response = await bounded(
4904
+ "turn/start",
4905
+ client.request("turn/start", {
4906
+ threadId,
4907
+ input: [{ type: "text", text: CODEX_CANCEL_TURN_TEXT, text_elements: [] }],
4908
+ approvalPolicy: "never",
4909
+ model: null,
4910
+ effort: null,
4911
+ outputSchema: null
4912
+ })
4442
4913
  );
4443
- });
4914
+ if (typeof response?.turn?.id === "string") await interrupt(response.turn.id);
4915
+ } catch (error) {
4916
+ debugCodex("attachAbort runId=%s cancel turn failed: %o", request.runId, error);
4917
+ }
4918
+ await terminateBackgroundTerminals(client, threadId);
4444
4919
  });
4445
4920
  }
4446
4921
  /**
@@ -4569,6 +5044,9 @@ function toRawEvent3(runId, payload, type) {
4569
5044
  payload
4570
5045
  };
4571
5046
  }
5047
+ function injectedTaskResultChild(text2) {
5048
+ return /<task id="?([^"\s>]+)"? state="?(?:completed|error)"?>/.exec(text2)?.[1];
5049
+ }
4572
5050
  function toOpenCodeModel(model) {
4573
5051
  if (!model) {
4574
5052
  return void 0;
@@ -5094,35 +5572,149 @@ var OpenCodeAgentAdapter = class {
5094
5572
  resolveSessionTerminal = resolve;
5095
5573
  });
5096
5574
  let lastSseActivityAt = Date.now();
5575
+ const postAbort = async (id) => {
5576
+ try {
5577
+ await Promise.race([
5578
+ fetchJson(`${runtime.baseUrl}/session/${id}/abort`, {
5579
+ method: "POST",
5580
+ headers: {
5581
+ "content-type": "application/json",
5582
+ ...runtime.previewHeaders
5583
+ }
5584
+ }),
5585
+ new Promise(
5586
+ (_, reject) => setTimeout(
5587
+ () => reject(new Error("opencode POST /session/abort timed out")),
5588
+ 3e3
5589
+ )
5590
+ )
5591
+ ]);
5592
+ } catch {
5593
+ }
5594
+ };
5097
5595
  let userAbortRequested = false;
5098
5596
  sink.setAbort(async () => {
5099
5597
  userAbortRequested = true;
5100
5598
  const sessionIdAtAbort = capturedSessionId;
5101
- if (sessionIdAtAbort) {
5102
- try {
5103
- await Promise.race([
5104
- fetchJson(
5105
- `${runtime.baseUrl}/session/${sessionIdAtAbort}/abort`,
5106
- {
5107
- method: "POST",
5108
- headers: {
5109
- "content-type": "application/json",
5110
- ...runtime.previewHeaders
5111
- }
5112
- }
5113
- ),
5114
- new Promise(
5115
- (_, reject) => setTimeout(
5116
- () => reject(new Error("opencode POST /session/abort timed out")),
5117
- 3e3
5118
- )
5119
- )
5120
- ]);
5121
- } catch {
5122
- }
5123
- }
5599
+ if (sessionIdAtAbort) await postAbort(sessionIdAtAbort);
5124
5600
  resolveSessionTerminal();
5125
5601
  });
5602
+ const backgroundTimeoutMs = resolveBackgroundTaskTimeoutMs(
5603
+ request.options.backgroundTaskTimeoutMs
5604
+ );
5605
+ const trackChildren = backgroundTimeoutMs !== 0;
5606
+ const children = /* @__PURE__ */ new Map();
5607
+ const liveChildren = () => [...children.values()].filter((child) => child.live).map((child) => ({
5608
+ id: child.id,
5609
+ type: "subagent",
5610
+ description: child.title
5611
+ }));
5612
+ const shouldWait = () => [...children.values()].some((child) => child.background) && liveChildren().length > 0;
5613
+ let pendingWait;
5614
+ let waitedMs = 0;
5615
+ let expiry;
5616
+ let sawParentIdle = false;
5617
+ let lastTasksKey = JSON.stringify({ tasks: [], waiting: false });
5618
+ const emitTasks = (tasks, waiting) => {
5619
+ const key = JSON.stringify({ tasks, waiting });
5620
+ if (key === lastTasksKey) return;
5621
+ lastTasksKey = key;
5622
+ sink.emitEvent(
5623
+ createNormalizedEvent(
5624
+ "background.tasks",
5625
+ { provider: request.provider, runId: request.runId },
5626
+ { tasks, waiting }
5627
+ )
5628
+ );
5629
+ };
5630
+ const endWait = () => {
5631
+ if (!pendingWait) return;
5632
+ waitedMs += pendingWait.elapsedMs();
5633
+ pendingWait.clear();
5634
+ pendingWait = void 0;
5635
+ };
5636
+ const onChildrenChanged = () => {
5637
+ if (!pendingWait) return;
5638
+ const live = liveChildren();
5639
+ emitTasks(live, true);
5640
+ pendingWait.setIdle(live.length === 0);
5641
+ };
5642
+ const registerChild = (id, title, background = false) => {
5643
+ const known = children.get(id);
5644
+ if (known) {
5645
+ known.background ||= background;
5646
+ if (title && title !== known.title) {
5647
+ known.title = title;
5648
+ onChildrenChanged();
5649
+ }
5650
+ return;
5651
+ }
5652
+ children.set(id, { id, title: title ?? "", live: true, background });
5653
+ onChildrenChanged();
5654
+ };
5655
+ const setChildLive = (id, live) => {
5656
+ const child = children.get(id);
5657
+ if (!child || child.live === live) return;
5658
+ child.live = live;
5659
+ onChildrenChanged();
5660
+ };
5661
+ const reconcileChildren = async () => {
5662
+ const live = [...children.values()].filter((child) => child.live);
5663
+ if (live.length === 0) return;
5664
+ const statuses = await withTimeout(
5665
+ fetchJson(
5666
+ `${runtime.baseUrl}/session/status`,
5667
+ { headers: runtime.previewHeaders }
5668
+ ).catch(() => void 0),
5669
+ 3e3
5670
+ );
5671
+ if (!statuses || typeof statuses !== "object") return;
5672
+ for (const child of live) {
5673
+ const status = statuses[child.id];
5674
+ if (!status || status.type === "idle") setChildLive(child.id, false);
5675
+ }
5676
+ };
5677
+ const onParentIdle = async () => {
5678
+ sawParentIdle = true;
5679
+ if (pendingWait || sessionIdleFromSse) return;
5680
+ const tracking = trackChildren && !userAbortRequested;
5681
+ if (tracking && shouldWait()) await reconcileChildren();
5682
+ if (!tracking || !shouldWait()) {
5683
+ sessionIdleFromSse = true;
5684
+ resolveSessionTerminal();
5685
+ return;
5686
+ }
5687
+ const live = liveChildren();
5688
+ debugOpencode(
5689
+ "\u2605 parent idle with %d live subagent(s); waiting",
5690
+ live.length
5691
+ );
5692
+ const wait = new BackgroundWait(
5693
+ BACKGROUND_TASK_GRACE_MS,
5694
+ Math.max(0, backgroundTimeoutMs - waitedMs)
5695
+ );
5696
+ pendingWait = wait;
5697
+ void wait.expired.then((reason) => {
5698
+ if (pendingWait !== wait) return;
5699
+ expiry = reason;
5700
+ resolveSessionTerminal();
5701
+ });
5702
+ emitTasks(live, true);
5703
+ };
5704
+ const onParentBusy = () => {
5705
+ if (!pendingWait) return;
5706
+ debugOpencode(
5707
+ "\u2605 parent resumed after %dms of background wait",
5708
+ pendingWait.elapsedMs()
5709
+ );
5710
+ endWait();
5711
+ emitTasks(liveChildren(), false);
5712
+ };
5713
+ const onInjectedResult = (childId) => {
5714
+ if (!children.has(childId)) return;
5715
+ setChildLive(childId, false);
5716
+ onParentBusy();
5717
+ };
5126
5718
  try {
5127
5719
  const interactiveApproval = !request.options.fullAccess && isInteractiveApproval(request.options);
5128
5720
  let forkedSession = null;
@@ -5225,6 +5817,12 @@ var OpenCodeAgentAdapter = class {
5225
5817
  const info = properties?.info;
5226
5818
  if (info && typeof info.id === "string" && typeof info.parentID === "string" && runSessionIds.has(info.parentID)) {
5227
5819
  runSessionIds.add(info.id);
5820
+ if (trackChildren && info.parentID === sessionId) {
5821
+ registerChild(
5822
+ info.id,
5823
+ typeof info.title === "string" ? info.title : void 0
5824
+ );
5825
+ }
5228
5826
  }
5229
5827
  }
5230
5828
  if (eventType === "message.updated") {
@@ -5333,28 +5931,35 @@ var OpenCodeAgentAdapter = class {
5333
5931
  const errMsg = typeof errData?.data?.message === "string" ? errData.data.message : typeof errData?.message === "string" ? errData.message : "OpenCode session error";
5334
5932
  sessionErrorFromSse = new Error(errMsg);
5335
5933
  }
5934
+ resolveSessionTerminal();
5336
5935
  } else {
5337
- sessionIdleFromSse = true;
5936
+ await onParentIdle();
5338
5937
  }
5339
5938
  debugOpencode(
5340
5939
  "\u2605 %s for session=%s",
5341
5940
  payloadRecord.type,
5342
5941
  sessionId
5343
5942
  );
5344
- resolveSessionTerminal();
5943
+ } else if (trackChildren && payloadRecord.type === "session.idle") {
5944
+ setChildLive(eventSessionId, false);
5345
5945
  }
5346
5946
  }
5347
5947
  if (payloadRecord?.type === "session.status") {
5348
5948
  const properties = payloadRecord.properties;
5349
5949
  const status = properties?.status;
5350
5950
  const eventSessionId = typeof properties?.sessionID === "string" ? properties.sessionID : void 0;
5351
- if ((!eventSessionId || eventSessionId === sessionId) && status?.type === "idle") {
5352
- sessionIdleFromSse = true;
5353
- debugOpencode(
5354
- "\u2605 session.status{idle} for session=%s",
5355
- sessionId
5356
- );
5357
- resolveSessionTerminal();
5951
+ if (!eventSessionId || eventSessionId === sessionId) {
5952
+ if (status?.type === "idle") {
5953
+ debugOpencode(
5954
+ "\u2605 session.status{idle} for session=%s",
5955
+ sessionId
5956
+ );
5957
+ await onParentIdle();
5958
+ } else if (status?.type === "busy" || status?.type === "retry") {
5959
+ onParentBusy();
5960
+ }
5961
+ } else if (trackChildren) {
5962
+ setChildLive(eventSessionId, status?.type !== "idle");
5358
5963
  }
5359
5964
  }
5360
5965
  if (payloadRecord?.type === "message.part.updated") {
@@ -5363,6 +5968,22 @@ var OpenCodeAgentAdapter = class {
5363
5968
  if (part && typeof part.id === "string" && typeof part.type === "string") {
5364
5969
  partTypeById.set(part.id, part.type);
5365
5970
  }
5971
+ if (trackChildren && part?.type === "tool" && part.tool === "task" && part.sessionID === sessionId) {
5972
+ const state = part.state;
5973
+ const metadata = state?.metadata;
5974
+ const childId = metadata?.sessionId ?? metadata?.jobId;
5975
+ if (metadata?.background === true && typeof childId === "string") {
5976
+ registerChild(
5977
+ childId,
5978
+ typeof state?.title === "string" ? state.title : void 0,
5979
+ true
5980
+ );
5981
+ }
5982
+ }
5983
+ if (trackChildren && part?.type === "text" && part.synthetic === true && part.sessionID === sessionId && typeof part.text === "string") {
5984
+ const childId = injectedTaskResultChild(part.text);
5985
+ if (childId) onInjectedResult(childId);
5986
+ }
5366
5987
  }
5367
5988
  if (payloadRecord?.type === "message.part.delta") {
5368
5989
  const properties = payloadRecord.properties;
@@ -5373,6 +5994,9 @@ var OpenCodeAgentAdapter = class {
5373
5994
  if (isForeignSession) {
5374
5995
  continue;
5375
5996
  }
5997
+ if (eventMessageId !== void 0 && announcedUserMessageIds.has(eventMessageId)) {
5998
+ continue;
5999
+ }
5376
6000
  const delta = typeof properties?.delta === "string" ? properties.delta : "";
5377
6001
  const field = typeof properties?.field === "string" ? properties.field : void 0;
5378
6002
  const partType = eventPartId ? partTypeById.get(eventPartId) : void 0;
@@ -5523,13 +6147,22 @@ var OpenCodeAgentAdapter = class {
5523
6147
  const SSE_POLL_INTERVAL_MS = 5e3;
5524
6148
  lastSseActivityAt = Date.now();
5525
6149
  let sseSilent = false;
5526
- while (!sessionIdleFromSse && !sessionErrorFromSse && !sessionAbortedFromSse && !userAbortRequested && !dispatchError) {
6150
+ while (!sessionIdleFromSse && !sessionErrorFromSse && !sessionAbortedFromSse && !userAbortRequested && !dispatchError && !expiry) {
5527
6151
  const silence = Date.now() - lastSseActivityAt;
5528
6152
  if (silence > SSE_SILENCE_THRESHOLD_MS) {
6153
+ if (pendingWait && sawParentIdle) {
6154
+ debugOpencode(
6155
+ "SSE went silent (%dms) during background wait; settling",
6156
+ silence
6157
+ );
6158
+ expiry = "transport";
6159
+ break;
6160
+ }
5529
6161
  sseSilent = true;
5530
6162
  debugOpencode("SSE went silent (%dms) \u2014 giving up", silence);
5531
6163
  break;
5532
6164
  }
6165
+ if (pendingWait && liveChildren().length > 0) await reconcileChildren();
5533
6166
  await Promise.race([
5534
6167
  sessionTerminal,
5535
6168
  new Promise(
@@ -5539,6 +6172,16 @@ var OpenCodeAgentAdapter = class {
5539
6172
  }
5540
6173
  sseAbort.abort();
5541
6174
  await sseTask;
6175
+ if (expiry === "ceiling" || (sessionErrorFromSse || dispatchError) && liveChildren().length > 0) {
6176
+ debugOpencode(
6177
+ "\u2605 run over (%s) with %d subagent(s) live; aborting them",
6178
+ expiry ?? "failure",
6179
+ liveChildren().length
6180
+ );
6181
+ await postAbort(sessionId);
6182
+ }
6183
+ endWait();
6184
+ emitTasks([], false);
5542
6185
  if (userAbortRequested || sessionAbortedFromSse) {
5543
6186
  debugOpencode(
5544
6187
  "\u2605 run.cancelled (%dms since execute start)",
@@ -5552,7 +6195,7 @@ var OpenCodeAgentAdapter = class {
5552
6195
  sink.fail(sessionErrorFromSse);
5553
6196
  } else if (dispatchError) {
5554
6197
  sink.fail(dispatchError);
5555
- } else if (sessionIdleFromSse) {
6198
+ } else if (sessionIdleFromSse || expiry) {
5556
6199
  debugOpencode(
5557
6200
  "\u2605 run.completed (%dms since execute start) chars=%d",
5558
6201
  Date.now() - executeStartedAt,
@@ -5597,6 +6240,7 @@ var OpenCodeAgentAdapter = class {
5597
6240
  sink.fail(new Error("opencode run ended without a terminal signal"));
5598
6241
  }
5599
6242
  } finally {
6243
+ endWait();
5600
6244
  sseAbort.abort();
5601
6245
  if (sseTask) {
5602
6246
  await sseTask.catch(() => void 0);
@@ -5744,6 +6388,7 @@ function prepareAgentOptions(_provider, options) {
5744
6388
  if (!path11.isAbsolute(options.stateDirectory)) throw new Error("stateDirectory must be an absolute path.");
5745
6389
  }
5746
6390
  if (options.sandbox && options.processGroup !== void 0) throw new Error("processGroup is only supported for host execution.");
6391
+ resolveBackgroundTaskTimeoutMs(options.backgroundTaskTimeoutMs);
5747
6392
  if (options.configuration === "native") {
5748
6393
  if (options.sandbox) throw new Error("Native configuration is only supported for host execution.");
5749
6394
  if (options.mcps?.length || options.skills?.length || options.subAgents?.length || options.commands?.length || options.enableRtk) {
@@ -6137,6 +6782,9 @@ var Agent = class {
6137
6782
  this.setupPromise = void 0;
6138
6783
  }
6139
6784
  stream(runConfig) {
6785
+ if (this.provider !== AgentProvider.Codex && (runConfig.reasoning === "max" || runConfig.reasoning === "ultra")) {
6786
+ throw new Error(`Reasoning effort "${runConfig.reasoning}" is only supported by Codex.`);
6787
+ }
6140
6788
  if (runConfig.resumeSessionId && runConfig.forkSessionId) {
6141
6789
  throw new Error(
6142
6790
  "AgentRunConfig.resumeSessionId and forkSessionId are mutually exclusive."