agentbox-sdk 0.1.508 → 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.
@@ -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";
@@ -2091,6 +2286,29 @@ async function handleStart(req, res, runId) {
2091
2286
  opts.pathToClaudeCodeExecutable,
2092
2287
  );
2093
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
+
2094
2312
  // Resume-if-exists gate. \`claude --resume <id>\` errors hard with "No
2095
2313
  // conversation found with session ID" when the local session jsonl is
2096
2314
  // missing \u2014 most often because a prior post-task snapshot failed and the
@@ -2121,7 +2339,8 @@ async function handleStart(req, res, runId) {
2121
2339
  }
2122
2340
  }
2123
2341
 
2124
- let queryHandle;
2342
+ // Nobody left to stream to: do not start a CLI for it.
2343
+ if (clientGone) { res.end(); return; }
2125
2344
  try {
2126
2345
  queryHandle = query({
2127
2346
  prompt: promptStream,
@@ -2144,28 +2363,19 @@ async function handleStart(req, res, runId) {
2144
2363
 
2145
2364
  liveRuns.set(runId, { query: queryHandle, prompt: promptStream, permissions });
2146
2365
 
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
-
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.
2157
2371
  try {
2158
2372
  for await (const message of queryHandle) {
2159
2373
  res.write(JSON.stringify(message) + "\\n");
2160
- if (message.type === "result") break;
2161
2374
  }
2162
2375
  } catch (e) {
2163
2376
  res.write(JSON.stringify({ _error: String(e?.message ?? e) }) + "\\n");
2164
2377
  } finally {
2165
- clearInterval(heartbeat);
2166
- clearPermissions();
2167
- liveRuns.delete(runId);
2168
- promptStream.end();
2378
+ releaseRun();
2169
2379
  res.end();
2170
2380
  }
2171
2381
  }
@@ -2555,6 +2765,7 @@ var ClaudeCodeAgentAdapter = class {
2555
2765
  // user inside our images.
2556
2766
  IS_SANDBOX: "1"
2557
2767
  };
2768
+ applyCliBackgroundWaitCeiling(env);
2558
2769
  const customHeaders = request.options.customHeaders;
2559
2770
  if (customHeaders && Object.keys(customHeaders).length > 0) {
2560
2771
  const serialized = Object.entries(customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
@@ -2604,17 +2815,23 @@ ${serialized}` : serialized;
2604
2815
  }
2605
2816
  };
2606
2817
  const fetchAbort = new AbortController();
2818
+ const runUrl = `${baseUrl}/runs/${encodeURIComponent(request.runId)}`;
2607
2819
  const cleanup = async () => {
2608
2820
  try {
2609
- await fetch(
2610
- `${baseUrl}/runs/${encodeURIComponent(request.runId)}/abort`,
2611
- { method: "POST", headers: authHeaders }
2612
- );
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) });
2613
2826
  } catch {
2614
2827
  }
2615
2828
  fetchAbort.abort();
2616
2829
  };
2617
- sink.setAbort(cleanup);
2830
+ let cancelled = false;
2831
+ sink.setAbort(async () => {
2832
+ cancelled = true;
2833
+ await cleanup();
2834
+ });
2618
2835
  sink.onMessage(async (content) => {
2619
2836
  const parts = await validateProviderUserInput(request.provider, content);
2620
2837
  const mapped = mapToClaudeUserContent(parts);
@@ -2680,7 +2897,7 @@ ${serialized}` : serialized;
2680
2897
  if (!reply.ok) throw new Error(`Claude permission response failed: ${reply.status}`);
2681
2898
  }
2682
2899
  };
2683
- await consumeClaudeMessages(request, sink, permissionMessages(), executeStartedAt, cleanup);
2900
+ await consumeClaudeMessages(request, sink, permissionMessages(), executeStartedAt, cleanup, () => cancelled);
2684
2901
  return async () => void 0;
2685
2902
  }
2686
2903
  /**
@@ -2741,24 +2958,66 @@ ${serialized}` : serialized;
2741
2958
  }
2742
2959
  }
2743
2960
  };
2744
- async function consumeClaudeMessages(request, sink, messages, executeStartedAt, cleanup, wasCancelled = () => false) {
2961
+ async function consumeClaudeMessages(request, sink, messages, executeStartedAt, cleanup, wasCancelled = () => false, wait = {}) {
2745
2962
  let accumulatedText = "";
2746
2963
  let streamedThinkingChars = 0;
2747
- let pendingMessages = 1;
2748
2964
  let sawResult = false;
2749
2965
  let firstStreamEventLogged = false;
2750
2966
  let firstTextDeltaLogged = false;
2751
2967
  let lastTerminalReason;
2752
2968
  let lastIsError = false;
2753
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]();
2754
2997
  try {
2755
- for await (const item of messages) {
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;
2756
3015
  if (item && typeof item === "object") {
2757
3016
  const ctrl = item;
2758
3017
  if ("_error" in ctrl) {
2759
- throw new Error(
2760
- String(item._error ?? "daemon error")
2761
- );
3018
+ const error = new Error(String(ctrl._error ?? "daemon error"));
3019
+ if (!settleOnFailure(error)) throw error;
3020
+ break;
2762
3021
  }
2763
3022
  if ("_notice" in ctrl) {
2764
3023
  debugClaude("daemon notice: %o", ctrl);
@@ -2775,6 +3034,12 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
2775
3034
  const message = item;
2776
3035
  rawPayloads.push(message);
2777
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);
2778
3043
  if (message.type === "system") {
2779
3044
  const sub = message.subtype;
2780
3045
  if (sub === "init") {
@@ -2891,15 +3156,33 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
2891
3156
  if (resultText && resultText !== accumulatedText) {
2892
3157
  accumulatedText = resultText;
2893
3158
  }
2894
- pendingMessages--;
2895
- if (pendingMessages <= 0) break;
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);
2896
3166
  continue;
2897
3167
  }
2898
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
+ }
2899
3182
  await cleanup();
2900
3183
  if (!sawResult && !wasCancelled()) throw new Error("Claude Code closed before reporting a result");
2901
3184
  const finalText = accumulatedText;
2902
- const isCancelled = wasCancelled() || lastTerminalReason === "aborted_streaming" || lastTerminalReason === "aborted_tools";
3185
+ const isCancelled = isAborted();
2903
3186
  const isError = !isCancelled && lastIsError;
2904
3187
  if (isCancelled) {
2905
3188
  debugClaude(
@@ -2941,10 +3224,11 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
2941
3224
  });
2942
3225
  }
2943
3226
  } finally {
3227
+ pendingWait?.clear();
2944
3228
  await cleanup();
2945
3229
  }
2946
3230
  }
2947
- async function executeNativeClaude(request, sink) {
3231
+ async function executeNativeClaude(request, sink, wait = {}) {
2948
3232
  const { query } = await import("@anthropic-ai/claude-agent-sdk");
2949
3233
  const claudeDir = claudeConfigDir(request.options);
2950
3234
  const input = await validateProviderUserInput(request.provider, request.run.input);
@@ -2969,6 +3253,7 @@ async function executeNativeClaude(request, sink) {
2969
3253
  const messageId = randomUUID();
2970
3254
  prompt.push({ type: "user", uuid: messageId, message: { role: "user", content: mapToClaudeUserContent(input) }, parent_tool_use_id: null });
2971
3255
  const hostEnv = Object.fromEntries(Object.entries({ ...process.env, ...request.options.env }).filter((entry) => entry[1] !== void 0));
3256
+ applyCliBackgroundWaitCeiling(hostEnv);
2972
3257
  if (request.options.customHeaders) {
2973
3258
  const headers = Object.entries(request.options.customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
2974
3259
  hostEnv.ANTHROPIC_CUSTOM_HEADERS = [hostEnv.ANTHROPIC_CUSTOM_HEADERS, headers].filter(Boolean).join("\n");
@@ -3041,10 +3326,17 @@ async function executeNativeClaude(request, sink) {
3041
3326
  }
3042
3327
  }
3043
3328
  } });
3044
- sink.setRaw({ query: handle, claudeDir, runId: request.runId });
3329
+ const live = handle;
3330
+ sink.setRaw({ query: live, claudeDir, runId: request.runId });
3045
3331
  sink.emitEvent(createNormalizedEvent("run.started", { provider: request.provider, runId: request.runId }));
3046
3332
  sink.emitEvent(createNormalizedEvent("message.started", { provider: request.provider, runId: request.runId }, { messageId }));
3047
- await consumeClaudeMessages(request, sink, handle, Date.now(), stop, () => cancelled);
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
+ });
3048
3340
  } catch (error) {
3049
3341
  await stop();
3050
3342
  if (cancelled) sink.cancel();
@@ -3350,7 +3642,7 @@ function buildThreadParams(cwd, options, request) {
3350
3642
  return {
3351
3643
  cwd,
3352
3644
  model: request.run.model ?? null,
3353
- ...options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3645
+ ...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3354
3646
  sandbox: buildCodexSandboxMode(options),
3355
3647
  serviceName: "agentbox",
3356
3648
  // Persist the rollout on disk so follow-up runs can call `thread/resume`.
@@ -3365,7 +3657,7 @@ function buildResumeParams(cwd, options, request) {
3365
3657
  threadId: request.run.resumeSessionId,
3366
3658
  cwd,
3367
3659
  model: request.run.model ?? null,
3368
- ...options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3660
+ ...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3369
3661
  sandbox: buildCodexSandboxMode(options),
3370
3662
  ...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null },
3371
3663
  // We only need the thread id back; we never read `thread.turns`.
@@ -3381,7 +3673,7 @@ function buildForkParams(cwd, options, request) {
3381
3673
  lastTurnId: request.run.forkAtMessageId ?? null,
3382
3674
  cwd,
3383
3675
  model: request.run.model ?? null,
3384
- ...options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3676
+ ...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3385
3677
  sandbox: buildCodexSandboxMode(options),
3386
3678
  ...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null },
3387
3679
  excludeTurns: true
@@ -3415,7 +3707,7 @@ function buildCodexTurnStartParams(params) {
3415
3707
  return {
3416
3708
  threadId,
3417
3709
  input: inputItems,
3418
- ...request.options.configuration === "native" && !request.options.fullAccess ? {} : {
3710
+ ...request.options.provider?.approvalPolicy ? { approvalPolicy: request.options.provider.approvalPolicy } : request.options.configuration === "native" && !request.options.fullAccess ? {} : {
3419
3711
  approvalPolicy: !request.options.fullAccess && isInteractiveApproval(request.options) ? "untrusted" : "never"
3420
3712
  },
3421
3713
  ...sandboxPolicy ? { sandboxPolicy } : {},
@@ -3428,6 +3720,40 @@ function buildCodexTurnStartParams(params) {
3428
3720
  outputSchema: null
3429
3721
  };
3430
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
+ }
3431
3757
  function toRawEvent2(runId, payload, type) {
3432
3758
  return {
3433
3759
  provider: AgentProvider.Codex,
@@ -3584,6 +3910,42 @@ function createCodexPermissionEvent(request, notification, fileChanges) {
3584
3910
  }
3585
3911
  return null;
3586
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
+ }
3587
3949
  function toCodexApprovalDecision(notification, response) {
3588
3950
  const params = notification.params ?? {};
3589
3951
  const availableDecisions = Array.isArray(params.availableDecisions) ? params.availableDecisions : [];
@@ -4201,10 +4563,92 @@ var CodexAgentAdapter = class {
4201
4563
  const pendingFileChanges = /* @__PURE__ */ new Map();
4202
4564
  const fileItemKey = (params, itemId) => typeof params?.threadId === "string" && typeof params.turnId === "string" && typeof itemId === "string" ? `${params.threadId}:${params.turnId}:${itemId}` : void 0;
4203
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
+ };
4204
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
+ };
4205
4626
  void (async () => {
4206
4627
  let firstClientMessageLogged = false;
4207
- 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;
4208
4652
  if (!firstClientMessageLogged) {
4209
4653
  firstClientMessageLogged = true;
4210
4654
  debugCodex(
@@ -4260,36 +4704,96 @@ var CodexAgentAdapter = class {
4260
4704
  if (approvalKey) pendingFileChanges.delete(approvalKey);
4261
4705
  continue;
4262
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
+ }));
4719
+ continue;
4720
+ }
4263
4721
  if (message.method === "item/completed") {
4264
4722
  const item2 = message.params?.item;
4265
4723
  if (item2?.type === "plan" && typeof item2.text === "string") {
4266
4724
  sink.emitEvent(createNormalizedEvent("plan.completed", { provider: request.provider, runId: request.runId }, { text: item2.text }));
4267
4725
  }
4268
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);
4269
4730
  for (const event of toNormalizedCodexEvents(request.runId, message)) {
4731
+ if (event.type === "run.completed" && (waitAfterTurn || turn?.status === "interrupted")) continue;
4270
4732
  sink.emitEvent(event);
4271
4733
  if (event.type === "text.delta") {
4272
4734
  streamedText += event.delta;
4735
+ } else if (event.type === "message.completed" && event.text) {
4736
+ turnMessageText = event.text;
4273
4737
  }
4274
4738
  }
4275
4739
  if (message.method === "thread/started" && !rootThreadId) {
4276
4740
  rootThreadId = message.params?.thread?.id ?? rootThreadId;
4277
4741
  }
4278
4742
  if (message.method === "turn/started") {
4279
- 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
+ }
4280
4751
  }
4281
- 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) {
4282
4777
  pendingTurns--;
4283
4778
  if (pendingTurns <= 0) {
4284
- const turn = message.params?.turn;
4285
- const interrupted = turn?.status === "interrupted";
4286
- resolve({
4287
- text: streamedText,
4288
- turnId,
4289
- threadId: rootThreadId,
4290
- interrupted
4291
- });
4292
- 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();
4293
4797
  }
4294
4798
  }
4295
4799
  if (message.method === "error" && !shouldIgnoreCodexError(message)) {
@@ -4297,6 +4801,7 @@ var CodexAgentAdapter = class {
4297
4801
  return;
4298
4802
  }
4299
4803
  }
4804
+ if (settleOnFailure(new Error("Codex transport closed."))) return;
4300
4805
  reject(new Error("Codex transport closed before run completed."));
4301
4806
  })().catch(reject);
4302
4807
  });
@@ -4367,6 +4872,8 @@ var CodexAgentAdapter = class {
4367
4872
  } catch (err) {
4368
4873
  completionError = err;
4369
4874
  }
4875
+ endWait();
4876
+ emitTasks([], false);
4370
4877
  if (completionError !== void 0) {
4371
4878
  if (abortInvoked) {
4372
4879
  debugCodex(
@@ -4374,7 +4881,7 @@ var CodexAgentAdapter = class {
4374
4881
  Date.now() - executeStartedAt
4375
4882
  );
4376
4883
  sink.cancel({
4377
- text: streamedText || void 0,
4884
+ text: streamedText || lastTurn?.text || void 0,
4378
4885
  costData: extractCodexCostData(rawPayloads)
4379
4886
  });
4380
4887
  } else {
@@ -4399,6 +4906,7 @@ var CodexAgentAdapter = class {
4399
4906
  }
4400
4907
  }
4401
4908
  } finally {
4909
+ pendingWait?.clear();
4402
4910
  await runtime.cleanup().catch(() => void 0);
4403
4911
  }
4404
4912
  return async () => void 0;
@@ -4411,36 +4919,51 @@ var CodexAgentAdapter = class {
4411
4919
  * driven by the normalized `message.started` event whose
4412
4920
  * `messageId` IS the codex turnId).
4413
4921
  *
4414
- * 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.
4415
4927
  */
4416
4928
  async attachAbort(request) {
4417
4929
  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
- );
4930
+ if (!threadId) {
4931
+ debugCodex("attachAbort runId=%s skipped: no threadId", request.runId);
4426
4932
  return;
4427
4933
  }
4428
4934
  await withCodexAppServer(request, async (client) => {
4429
- await Promise.race([
4430
- client.request("turn/interrupt", { threadId, turnId }),
4935
+ const bounded = (what, promise) => Promise.race([
4936
+ promise,
4431
4937
  new Promise(
4432
- (_, reject) => setTimeout(
4433
- () => reject(new Error("codex turn/interrupt timed out")),
4434
- 3e3
4435
- )
4938
+ (_, reject) => setTimeout(() => reject(new Error(`codex ${what} timed out`)), 3e3)
4436
4939
  )
4437
- ]).catch((error) => {
4438
- debugCodex(
4439
- "attachAbort runId=%s turn/interrupt failed: %o",
4440
- request.runId,
4441
- 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
+ })
4442
4961
  );
4443
- });
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);
4444
4967
  });
4445
4968
  }
4446
4969
  /**
@@ -4569,6 +5092,9 @@ function toRawEvent3(runId, payload, type) {
4569
5092
  payload
4570
5093
  };
4571
5094
  }
5095
+ function injectedTaskResultChild(text2) {
5096
+ return /<task id="?([^"\s>]+)"? state="?(?:completed|error)"?>/.exec(text2)?.[1];
5097
+ }
4572
5098
  function toOpenCodeModel(model) {
4573
5099
  if (!model) {
4574
5100
  return void 0;
@@ -5094,35 +5620,149 @@ var OpenCodeAgentAdapter = class {
5094
5620
  resolveSessionTerminal = resolve;
5095
5621
  });
5096
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
+ };
5097
5643
  let userAbortRequested = false;
5098
5644
  sink.setAbort(async () => {
5099
5645
  userAbortRequested = true;
5100
5646
  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
- }
5647
+ if (sessionIdAtAbort) await postAbort(sessionIdAtAbort);
5124
5648
  resolveSessionTerminal();
5125
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
+ };
5126
5766
  try {
5127
5767
  const interactiveApproval = !request.options.fullAccess && isInteractiveApproval(request.options);
5128
5768
  let forkedSession = null;
@@ -5225,6 +5865,12 @@ var OpenCodeAgentAdapter = class {
5225
5865
  const info = properties?.info;
5226
5866
  if (info && typeof info.id === "string" && typeof info.parentID === "string" && runSessionIds.has(info.parentID)) {
5227
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
+ }
5228
5874
  }
5229
5875
  }
5230
5876
  if (eventType === "message.updated") {
@@ -5333,28 +5979,35 @@ var OpenCodeAgentAdapter = class {
5333
5979
  const errMsg = typeof errData?.data?.message === "string" ? errData.data.message : typeof errData?.message === "string" ? errData.message : "OpenCode session error";
5334
5980
  sessionErrorFromSse = new Error(errMsg);
5335
5981
  }
5982
+ resolveSessionTerminal();
5336
5983
  } else {
5337
- sessionIdleFromSse = true;
5984
+ await onParentIdle();
5338
5985
  }
5339
5986
  debugOpencode(
5340
5987
  "\u2605 %s for session=%s",
5341
5988
  payloadRecord.type,
5342
5989
  sessionId
5343
5990
  );
5344
- resolveSessionTerminal();
5991
+ } else if (trackChildren && payloadRecord.type === "session.idle") {
5992
+ setChildLive(eventSessionId, false);
5345
5993
  }
5346
5994
  }
5347
5995
  if (payloadRecord?.type === "session.status") {
5348
5996
  const properties = payloadRecord.properties;
5349
5997
  const status = properties?.status;
5350
5998
  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();
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");
5358
6011
  }
5359
6012
  }
5360
6013
  if (payloadRecord?.type === "message.part.updated") {
@@ -5363,6 +6016,22 @@ var OpenCodeAgentAdapter = class {
5363
6016
  if (part && typeof part.id === "string" && typeof part.type === "string") {
5364
6017
  partTypeById.set(part.id, part.type);
5365
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
+ }
5366
6035
  }
5367
6036
  if (payloadRecord?.type === "message.part.delta") {
5368
6037
  const properties = payloadRecord.properties;
@@ -5373,6 +6042,9 @@ var OpenCodeAgentAdapter = class {
5373
6042
  if (isForeignSession) {
5374
6043
  continue;
5375
6044
  }
6045
+ if (eventMessageId !== void 0 && announcedUserMessageIds.has(eventMessageId)) {
6046
+ continue;
6047
+ }
5376
6048
  const delta = typeof properties?.delta === "string" ? properties.delta : "";
5377
6049
  const field = typeof properties?.field === "string" ? properties.field : void 0;
5378
6050
  const partType = eventPartId ? partTypeById.get(eventPartId) : void 0;
@@ -5523,13 +6195,22 @@ var OpenCodeAgentAdapter = class {
5523
6195
  const SSE_POLL_INTERVAL_MS = 5e3;
5524
6196
  lastSseActivityAt = Date.now();
5525
6197
  let sseSilent = false;
5526
- while (!sessionIdleFromSse && !sessionErrorFromSse && !sessionAbortedFromSse && !userAbortRequested && !dispatchError) {
6198
+ while (!sessionIdleFromSse && !sessionErrorFromSse && !sessionAbortedFromSse && !userAbortRequested && !dispatchError && !expiry) {
5527
6199
  const silence = Date.now() - lastSseActivityAt;
5528
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
+ }
5529
6209
  sseSilent = true;
5530
6210
  debugOpencode("SSE went silent (%dms) \u2014 giving up", silence);
5531
6211
  break;
5532
6212
  }
6213
+ if (pendingWait && liveChildren().length > 0) await reconcileChildren();
5533
6214
  await Promise.race([
5534
6215
  sessionTerminal,
5535
6216
  new Promise(
@@ -5539,6 +6220,16 @@ var OpenCodeAgentAdapter = class {
5539
6220
  }
5540
6221
  sseAbort.abort();
5541
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);
5542
6233
  if (userAbortRequested || sessionAbortedFromSse) {
5543
6234
  debugOpencode(
5544
6235
  "\u2605 run.cancelled (%dms since execute start)",
@@ -5552,7 +6243,7 @@ var OpenCodeAgentAdapter = class {
5552
6243
  sink.fail(sessionErrorFromSse);
5553
6244
  } else if (dispatchError) {
5554
6245
  sink.fail(dispatchError);
5555
- } else if (sessionIdleFromSse) {
6246
+ } else if (sessionIdleFromSse || expiry) {
5556
6247
  debugOpencode(
5557
6248
  "\u2605 run.completed (%dms since execute start) chars=%d",
5558
6249
  Date.now() - executeStartedAt,
@@ -5597,6 +6288,7 @@ var OpenCodeAgentAdapter = class {
5597
6288
  sink.fail(new Error("opencode run ended without a terminal signal"));
5598
6289
  }
5599
6290
  } finally {
6291
+ endWait();
5600
6292
  sseAbort.abort();
5601
6293
  if (sseTask) {
5602
6294
  await sseTask.catch(() => void 0);
@@ -5744,6 +6436,7 @@ function prepareAgentOptions(_provider, options) {
5744
6436
  if (!path11.isAbsolute(options.stateDirectory)) throw new Error("stateDirectory must be an absolute path.");
5745
6437
  }
5746
6438
  if (options.sandbox && options.processGroup !== void 0) throw new Error("processGroup is only supported for host execution.");
6439
+ resolveBackgroundTaskTimeoutMs(options.backgroundTaskTimeoutMs);
5747
6440
  if (options.configuration === "native") {
5748
6441
  if (options.sandbox) throw new Error("Native configuration is only supported for host execution.");
5749
6442
  if (options.mcps?.length || options.skills?.length || options.subAgents?.length || options.commands?.length || options.enableRtk) {