agentbox-sdk 0.1.513 → 0.1.515
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -2
- package/dist/agents/index.d.ts +2 -2
- package/dist/agents/index.js +1 -1
- package/dist/{chunk-XJWZR2NR.js → chunk-HVXPNGSC.js} +861 -194
- package/dist/events/index.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{types-NhKw_6mW.d.ts → types-BKvKgRMY.d.ts} +46 -0
- package/package.json +2 -2
|
@@ -1857,23 +1857,44 @@ var BackgroundTaskTracker = class {
|
|
|
1857
1857
|
wakeups = /* @__PURE__ */ new Map();
|
|
1858
1858
|
// tool_use seen, tool_result not yet: a failed schedule adds nothing.
|
|
1859
1859
|
pendingWakeups = /* @__PURE__ */ new Map();
|
|
1860
|
+
// task_started seen while the command was still in the foreground, kept so
|
|
1861
|
+
// a later "moved to the background" patch can name the task it promotes.
|
|
1862
|
+
foreground = /* @__PURE__ */ new Map();
|
|
1860
1863
|
afterResult = false;
|
|
1861
1864
|
seenBackgroundWork = false;
|
|
1865
|
+
hasTaskSnapshot = false;
|
|
1862
1866
|
liveTasks() {
|
|
1863
|
-
return [
|
|
1867
|
+
return [
|
|
1868
|
+
...[...this.tasks.values()].filter(({ ambient }) => !ambient).map(({ task }) => task),
|
|
1869
|
+
...this.wakeups.values()
|
|
1870
|
+
];
|
|
1864
1871
|
}
|
|
1865
1872
|
/**
|
|
1866
1873
|
* True once any background task or scheduled wakeup was live in this run.
|
|
1867
1874
|
* The CLI queues a wake-up for every task that finishes and delivers it as
|
|
1868
1875
|
* a new turn once the model is idle — including tasks that finished
|
|
1869
1876
|
* mid-turn, whose queued turn starts right after that turn's `result`
|
|
1870
|
-
* with nothing live
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
1877
|
+
* with nothing live in between. After background work has been seen, wait
|
|
1878
|
+
* for session_state_changed/idle with an empty live set. Older CLIs that
|
|
1879
|
+
* never emit session state use an idle grace as a compatibility fallback.
|
|
1873
1880
|
*/
|
|
1874
1881
|
hasSeenBackgroundWork() {
|
|
1875
1882
|
return this.seenBackgroundWork;
|
|
1876
1883
|
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Prime a run that attaches to a parked harness with what was live when it
|
|
1886
|
+
* was parked: that run never saw the work start, and would otherwise take
|
|
1887
|
+
* its first `result` for the end of the run and tear the harness down.
|
|
1888
|
+
* Snapshots and schedule events replayed afterwards supersede it.
|
|
1889
|
+
*/
|
|
1890
|
+
restore(tasks) {
|
|
1891
|
+
for (const task of tasks) {
|
|
1892
|
+
if (!task.id) continue;
|
|
1893
|
+
this.seenBackgroundWork = true;
|
|
1894
|
+
if (task.type === "scheduled_wakeup") this.wakeups.set(task.id, task);
|
|
1895
|
+
else this.tasks.set(task.id, { task, ambient: false });
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1877
1898
|
/** Feed one SDKMessage. Returns true when it started a follow-up turn. */
|
|
1878
1899
|
ingest(message) {
|
|
1879
1900
|
const m = asRecord2(message);
|
|
@@ -1911,6 +1932,8 @@ var BackgroundTaskTracker = class {
|
|
|
1911
1932
|
const id = String(m.task_id ?? "");
|
|
1912
1933
|
switch (m.subtype) {
|
|
1913
1934
|
case "background_tasks_changed":
|
|
1935
|
+
this.hasTaskSnapshot = true;
|
|
1936
|
+
this.foreground.clear();
|
|
1914
1937
|
this.tasks.clear();
|
|
1915
1938
|
for (const entry of asArray(m.tasks)) {
|
|
1916
1939
|
const task = asRecord2(entry);
|
|
@@ -1918,15 +1941,31 @@ var BackgroundTaskTracker = class {
|
|
|
1918
1941
|
}
|
|
1919
1942
|
return false;
|
|
1920
1943
|
case "task_started":
|
|
1921
|
-
if (
|
|
1944
|
+
if (this.hasTaskSnapshot || m.owned_by_subagent) return false;
|
|
1945
|
+
if (m.is_backgrounded === true) this.addTask(m);
|
|
1946
|
+
else if (id) this.foreground.set(id, m);
|
|
1922
1947
|
return false;
|
|
1923
1948
|
case "task_notification":
|
|
1924
|
-
this.
|
|
1949
|
+
if (!this.hasTaskSnapshot) {
|
|
1950
|
+
this.tasks.delete(id);
|
|
1951
|
+
this.foreground.delete(id);
|
|
1952
|
+
}
|
|
1925
1953
|
return false;
|
|
1926
|
-
case "task_updated":
|
|
1927
|
-
if (
|
|
1954
|
+
case "task_updated": {
|
|
1955
|
+
if (this.hasTaskSnapshot) return false;
|
|
1956
|
+
const patch = asRecord2(m.patch);
|
|
1957
|
+
if (DONE_STATUSES.has(String(patch?.status))) {
|
|
1928
1958
|
this.tasks.delete(id);
|
|
1959
|
+
this.foreground.delete(id);
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
const started = this.foreground.get(id);
|
|
1963
|
+
if (patch?.is_backgrounded === true && started) {
|
|
1964
|
+
this.foreground.delete(id);
|
|
1965
|
+
this.addTask(started);
|
|
1966
|
+
}
|
|
1929
1967
|
return false;
|
|
1968
|
+
}
|
|
1930
1969
|
case "init":
|
|
1931
1970
|
return this.startTurn();
|
|
1932
1971
|
default:
|
|
@@ -1936,11 +1975,15 @@ var BackgroundTaskTracker = class {
|
|
|
1936
1975
|
addTask(task) {
|
|
1937
1976
|
const id = String(task.task_id ?? "");
|
|
1938
1977
|
if (!id) return;
|
|
1939
|
-
|
|
1978
|
+
const ambient = task.ambient === true || task.skip_transcript === true;
|
|
1979
|
+
if (!ambient) this.seenBackgroundWork = true;
|
|
1940
1980
|
this.tasks.set(id, {
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1981
|
+
ambient,
|
|
1982
|
+
task: {
|
|
1983
|
+
id,
|
|
1984
|
+
type: String(task.task_type ?? "task"),
|
|
1985
|
+
description: String(task.description ?? "")
|
|
1986
|
+
}
|
|
1944
1987
|
});
|
|
1945
1988
|
}
|
|
1946
1989
|
ingestToolUses(m) {
|
|
@@ -2011,6 +2054,10 @@ var BackgroundWait = class {
|
|
|
2011
2054
|
}
|
|
2012
2055
|
this.grace ??= setTimeout(() => this.expire("grace"), this.graceMs);
|
|
2013
2056
|
}
|
|
2057
|
+
/** End the wait now on the host's request ({@link BackgroundWaitFinish}). */
|
|
2058
|
+
finish() {
|
|
2059
|
+
this.expire("finished");
|
|
2060
|
+
}
|
|
2014
2061
|
elapsedMs() {
|
|
2015
2062
|
return Date.now() - this.startedAt;
|
|
2016
2063
|
}
|
|
@@ -2019,9 +2066,23 @@ var BackgroundWait = class {
|
|
|
2019
2066
|
clearTimeout(this.ceiling);
|
|
2020
2067
|
}
|
|
2021
2068
|
};
|
|
2069
|
+
var BackgroundWaitFinish = class {
|
|
2070
|
+
requested = false;
|
|
2071
|
+
wait;
|
|
2072
|
+
request() {
|
|
2073
|
+
this.requested = true;
|
|
2074
|
+
this.wait?.finish();
|
|
2075
|
+
}
|
|
2076
|
+
/** Follow the run's current wait; a latched request ends a new one at once. */
|
|
2077
|
+
watch(wait) {
|
|
2078
|
+
this.wait = wait;
|
|
2079
|
+
if (this.requested) wait?.finish();
|
|
2080
|
+
return wait;
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2022
2083
|
|
|
2023
2084
|
// src/agents/providers/claude-code.ts
|
|
2024
|
-
var DAEMON_PROTOCOL_VERSION = "
|
|
2085
|
+
var DAEMON_PROTOCOL_VERSION = "10";
|
|
2025
2086
|
var DAEMON_PORT = 43180;
|
|
2026
2087
|
var DAEMON_PATH = "/tmp/agentbox/claude-code/daemon.mjs";
|
|
2027
2088
|
var DAEMON_LOG_PATH = "/tmp/agentbox/claude-code/daemon.log";
|
|
@@ -2031,7 +2092,11 @@ var DAEMON_READY_TIMEOUT_MS = 3e4;
|
|
|
2031
2092
|
var DAEMON_READY_POLL_INTERVAL_MS = 250;
|
|
2032
2093
|
function claudeConfigDir(options) {
|
|
2033
2094
|
return path8.join(
|
|
2034
|
-
agentboxRoot(
|
|
2095
|
+
agentboxRoot(
|
|
2096
|
+
AgentProvider.ClaudeCode,
|
|
2097
|
+
Boolean(options.sandbox),
|
|
2098
|
+
options.stateDirectory
|
|
2099
|
+
),
|
|
2035
2100
|
".claude"
|
|
2036
2101
|
);
|
|
2037
2102
|
}
|
|
@@ -2052,7 +2117,9 @@ function buildClaudeQueryOptions(params) {
|
|
|
2052
2117
|
}
|
|
2053
2118
|
const includeHookEvents = provider?.includeHookEvents ?? false;
|
|
2054
2119
|
if (run.reasoning === "max" || run.reasoning === "ultra") {
|
|
2055
|
-
throw new Error(
|
|
2120
|
+
throw new Error(
|
|
2121
|
+
`Reasoning effort "${run.reasoning}" is only supported by Codex.`
|
|
2122
|
+
);
|
|
2056
2123
|
}
|
|
2057
2124
|
const effort = provider?.ultracode ? "xhigh" : run.reasoning;
|
|
2058
2125
|
return {
|
|
@@ -2063,7 +2130,10 @@ function buildClaudeQueryOptions(params) {
|
|
|
2063
2130
|
...params.request.options.configuration === "native" && provider?.fastMode !== void 0 ? { settings: { fastMode: provider.fastMode } } : {},
|
|
2064
2131
|
...params.request.options.configuration === "native" ? {
|
|
2065
2132
|
settingSources: ["user", "project", "local"],
|
|
2066
|
-
systemPrompt: {
|
|
2133
|
+
systemPrompt: {
|
|
2134
|
+
type: "preset",
|
|
2135
|
+
preset: "claude_code"
|
|
2136
|
+
}
|
|
2067
2137
|
} : {},
|
|
2068
2138
|
extraArgs,
|
|
2069
2139
|
includePartialMessages: true,
|
|
@@ -2130,7 +2200,7 @@ function createClaudeCodeDaemonScript() {
|
|
|
2130
2200
|
return `import http from "node:http";
|
|
2131
2201
|
import { execSync } from "node:child_process";
|
|
2132
2202
|
import { existsSync, readFileSync } from "node:fs";
|
|
2133
|
-
import { timingSafeEqual } from "node:crypto";
|
|
2203
|
+
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
2134
2204
|
import { query, getSessionInfo } from "@anthropic-ai/claude-agent-sdk";
|
|
2135
2205
|
|
|
2136
2206
|
const VERSION = ${version};
|
|
@@ -2155,6 +2225,199 @@ function isAuthorized(req) {
|
|
|
2155
2225
|
|
|
2156
2226
|
const liveRuns = new Map();
|
|
2157
2227
|
|
|
2228
|
+
// A parked run keeps its CLI, and the background work that CLI owns, alive
|
|
2229
|
+
// after the host settled the run and left. Its output is buffered until a
|
|
2230
|
+
// later run for the same session attaches through /start; a turn the CLI
|
|
2231
|
+
// starts on its own (a task finished, a schedule fired) wakes the host.
|
|
2232
|
+
const NDJSON_HEADERS = { "content-type": "application/x-ndjson", "transfer-encoding": "chunked", "x-daemon-version": VERSION };
|
|
2233
|
+
const PARK_BUFFER_LIMIT = 16 * 1024 * 1024;
|
|
2234
|
+
// A host may answer "busy" (non-2xx) for far longer than a fixed ladder: the
|
|
2235
|
+
// run that parked this work may still be finalizing, another job may hold the
|
|
2236
|
+
// task, a deploy may be rolling. Back off to WAKE_RETRY_CEILING_MS and then
|
|
2237
|
+
// keep asking for as long as the park lives \u2014 the park's own TTL is the bound,
|
|
2238
|
+
// not the ladder. A run attaching, or the park ending, stops the retries.
|
|
2239
|
+
const WAKE_RETRY_MS = [0, 1000, 2000, 4000, 8000, 16000, 30000, 60000];
|
|
2240
|
+
const WAKE_RETRY_CEILING_MS = 60000;
|
|
2241
|
+
|
|
2242
|
+
function findParkedRun(sessionId) {
|
|
2243
|
+
for (const run of liveRuns.values()) {
|
|
2244
|
+
if (run.parked && !run.sink && run.sessionId === sessionId) return run;
|
|
2245
|
+
}
|
|
2246
|
+
return undefined;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// A run's own teardown. The liveRuns entry is only removed when it is still
|
|
2250
|
+
// ours: hosts reuse a runId across retry attempts, and a successor
|
|
2251
|
+
// registered while our CLI winds down must not be evicted by our exit.
|
|
2252
|
+
function releaseRun(run) {
|
|
2253
|
+
clearInterval(run.heartbeat);
|
|
2254
|
+
clearTimeout(run.parked?.timer);
|
|
2255
|
+
run.parked = null;
|
|
2256
|
+
run.clearPermissions();
|
|
2257
|
+
if (liveRuns.get(run.id) === run) liveRuns.delete(run.id);
|
|
2258
|
+
run.prompt.end();
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// End the prompt so the CLI winds down with its background work.
|
|
2262
|
+
function endRun(run) {
|
|
2263
|
+
releaseRun(run);
|
|
2264
|
+
run.query?.interrupt().catch(() => {});
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
function writeFrame(run, message) {
|
|
2268
|
+
const line = JSON.stringify(message) + "\\n";
|
|
2269
|
+
if (run.sink && !run.sink.writableEnded) { run.sink.write(line); return; }
|
|
2270
|
+
// Nobody is attached. Partial deltas are rebuilt from the final messages.
|
|
2271
|
+
if (message.type === "stream_event") return;
|
|
2272
|
+
// byteLength, not .length: JSON.stringify keeps non-ASCII verbatim, so
|
|
2273
|
+
// UTF-16 units under-count the bytes this buffer actually holds.
|
|
2274
|
+
const size = Buffer.byteLength(line);
|
|
2275
|
+
if (run.bufferedBytes + size > PARK_BUFFER_LIMIT) {
|
|
2276
|
+
// Out of room while nobody is attached. Keep what the CLI already
|
|
2277
|
+
// produced and drop the rest rather than killing the CLI: the
|
|
2278
|
+
// background work goes on, and the run that attaches learns from the
|
|
2279
|
+
// notice that its transcript is incomplete. Turn accounting is
|
|
2280
|
+
// unaffected \u2014 observeFrame counts results before we are called.
|
|
2281
|
+
if (!run.bufferOverflowed) {
|
|
2282
|
+
run.bufferOverflowed = true;
|
|
2283
|
+
run.buffer.push(JSON.stringify({ _notice: "park_buffer_overflow" }) + "\\n");
|
|
2284
|
+
if (run.parked) wakeHost(run);
|
|
2285
|
+
}
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
run.bufferedBytes += size;
|
|
2289
|
+
run.buffer.push(line);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function attachSink(run, res) {
|
|
2293
|
+
clearInterval(run.heartbeat);
|
|
2294
|
+
run.sink = res;
|
|
2295
|
+
// Heartbeat: write a blank NDJSON line every 15s so HTTPS proxies
|
|
2296
|
+
// (Modal/Daytona tunnels, intermediate LBs) don't idle-kill the
|
|
2297
|
+
// stream during long blocking tool calls (e.g. TaskOutput block:true).
|
|
2298
|
+
// The host's parseNdjsonStream skips empty lines, so this is a no-op
|
|
2299
|
+
// for consumers but keeps the underlying TCP stream warm.
|
|
2300
|
+
run.heartbeat = setInterval(() => {
|
|
2301
|
+
if (run.sink === res && !res.writableEnded) res.write("\\n");
|
|
2302
|
+
}, 15000);
|
|
2303
|
+
if (typeof run.heartbeat.unref === "function") run.heartbeat.unref();
|
|
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, unless the
|
|
2306
|
+
// host parked the run first. Detected on the response: \`req\` emits
|
|
2307
|
+
// "close" as soon as its body is consumed (Node >= 16), long before any
|
|
2308
|
+
// disconnect, while the response only closes early when the socket dies
|
|
2309
|
+
// before the stream finished.
|
|
2310
|
+
res.on("close", () => {
|
|
2311
|
+
if (run.sink !== res) return;
|
|
2312
|
+
run.sink = null;
|
|
2313
|
+
clearInterval(run.heartbeat);
|
|
2314
|
+
if (run.parked) {
|
|
2315
|
+
// The CLI survives, but a permission left outstanding would wedge it:
|
|
2316
|
+
// nobody is attached to decide, and a blocked tool call never ends the
|
|
2317
|
+
// turn, so the host would never be woken either. Decline without
|
|
2318
|
+
// interrupting so the background work carries on.
|
|
2319
|
+
run.declinePermissions();
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
if (res.writableFinished) return;
|
|
2323
|
+
run.clientGone = true;
|
|
2324
|
+
endRun(run);
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// Track turn boundaries so an attaching run knows how many results precede
|
|
2329
|
+
// its own, and so a turn nobody is watching wakes the host.
|
|
2330
|
+
function observeFrame(run, message) {
|
|
2331
|
+
const state = message.type === "system" && message.subtype === "session_state_changed"
|
|
2332
|
+
? message.state : undefined;
|
|
2333
|
+
const started = message.type === "system" &&
|
|
2334
|
+
(message.subtype === "init" || state === "running");
|
|
2335
|
+
if (started) run.turnActive = true;
|
|
2336
|
+
// A turn ends at its result \u2014 but a turn that dies without one (a CLI
|
|
2337
|
+
// error, a killed subagent) would otherwise leave turnActive stuck true
|
|
2338
|
+
// and inflate the turn count adoptRun hands to the next run, which would
|
|
2339
|
+
// then skip its own result. Idle means no turn is running, whatever the
|
|
2340
|
+
// reason, so it clears the flag too.
|
|
2341
|
+
if (message.type === "result" || state === "idle") run.turnActive = false;
|
|
2342
|
+
if (message.type === "result" && !run.sink) run.bufferedResults++;
|
|
2343
|
+
// Anything a parked, unattended CLI produces on its own is news for the
|
|
2344
|
+
// host: the start of a turn, and equally the result of a turn that was
|
|
2345
|
+
// already in flight when the park landed (whose start edge was consumed
|
|
2346
|
+
// while the host was still attached, so it never announced itself).
|
|
2347
|
+
if (run.parked && !run.sink && (started || message.type === "result")) {
|
|
2348
|
+
wakeHost(run);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
async function wakeHost(run) {
|
|
2353
|
+
const parked = run.parked;
|
|
2354
|
+
if (!parked || !parked.wakeUrl || parked.waking) return;
|
|
2355
|
+
// A wake the host accepted stands until a run attaches. The job it started
|
|
2356
|
+
// takes a while to boot its side, and every frame the CLI emits meanwhile
|
|
2357
|
+
// calls us again \u2014 without this, one chatty turn is one POST per frame.
|
|
2358
|
+
// Retry only once it is plain that no run is coming.
|
|
2359
|
+
if (parked.wokeAt && Date.now() - parked.wokeAt < WAKE_RETRY_CEILING_MS) return;
|
|
2360
|
+
parked.waking = true;
|
|
2361
|
+
try {
|
|
2362
|
+
for (let attempt = 0; ; attempt++) {
|
|
2363
|
+
const delay = attempt < WAKE_RETRY_MS.length
|
|
2364
|
+
? WAKE_RETRY_MS[attempt]
|
|
2365
|
+
: WAKE_RETRY_CEILING_MS;
|
|
2366
|
+
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2367
|
+
// Attached or torn down meanwhile: someone is already listening, or
|
|
2368
|
+
// the park's TTL ended it. Either way, stop asking.
|
|
2369
|
+
if (run.parked !== parked || run.sink) return;
|
|
2370
|
+
try {
|
|
2371
|
+
const response = await fetch(parked.wakeUrl, {
|
|
2372
|
+
method: "POST",
|
|
2373
|
+
headers: { "content-type": "application/json", authorization: "Bearer " + parked.wakeToken },
|
|
2374
|
+
body: JSON.stringify({ runId: run.id, sessionId: run.sessionId }),
|
|
2375
|
+
signal: AbortSignal.timeout(10000),
|
|
2376
|
+
});
|
|
2377
|
+
if (response.ok) { parked.wokeAt = Date.now(); return; }
|
|
2378
|
+
} catch {}
|
|
2379
|
+
}
|
|
2380
|
+
} finally {
|
|
2381
|
+
parked.waking = false;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// A later run for a parked session takes the CLI over instead of starting a
|
|
2386
|
+
// second one on the same session: replay what was buffered, then stream.
|
|
2387
|
+
function adoptRun(run, res, runId, prompt, attachOnly, settings) {
|
|
2388
|
+
const turns = run.bufferedResults + (run.turnActive ? 1 : 0);
|
|
2389
|
+
res.writeHead(200, NDJSON_HEADERS);
|
|
2390
|
+
// Nothing happened while parked: leave it for the wake that will.
|
|
2391
|
+
if (attachOnly && turns === 0) {
|
|
2392
|
+
res.end(JSON.stringify({ _notice: "no_parked_turn", sessionId: run.sessionId }) + "\\n");
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
const parked = run.parked;
|
|
2396
|
+
clearTimeout(parked.timer);
|
|
2397
|
+
run.parked = null;
|
|
2398
|
+
// The adopting run's permission settings govern its own turn: a CLI parked
|
|
2399
|
+
// by a full-access run must not keep auto-approving once the task is back
|
|
2400
|
+
// in ask or plan mode. An attach-only run sends no input of its own, so it
|
|
2401
|
+
// leaves the settings of the turn already in flight alone.
|
|
2402
|
+
if (settings) run.settings = settings;
|
|
2403
|
+
liveRuns.delete(run.id);
|
|
2404
|
+
run.id = runId;
|
|
2405
|
+
liveRuns.set(runId, run);
|
|
2406
|
+
attachSink(run, res);
|
|
2407
|
+
// What is left of the background budget, so a run that adopts and parks
|
|
2408
|
+
// again inherits the deadline instead of restarting it.
|
|
2409
|
+
const budgetLeftMs = Number.isFinite(run.parkDeadline)
|
|
2410
|
+
? Math.max(0, run.parkDeadline - Date.now())
|
|
2411
|
+
: null;
|
|
2412
|
+
res.write(JSON.stringify({ _parked: { tasks: parked.tasks, turns, budgetLeftMs } }) + "\\n");
|
|
2413
|
+
for (const line of run.buffer) res.write(line);
|
|
2414
|
+
run.buffer = [];
|
|
2415
|
+
run.bufferedBytes = 0;
|
|
2416
|
+
run.bufferedResults = 0;
|
|
2417
|
+
run.bufferOverflowed = false;
|
|
2418
|
+
if (prompt) run.prompt.push(prompt);
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2158
2421
|
// The SDK's default spawn does \`existsSync(pathToClaudeCodeExecutable)\`
|
|
2159
2422
|
// before invoking child_process.spawn \u2014 that check fails on bare names
|
|
2160
2423
|
// like "claude" because existsSync doesn't do PATH lookup. Resolve to an
|
|
@@ -2238,7 +2501,36 @@ async function handleStart(req, res, runId) {
|
|
|
2238
2501
|
res.end(JSON.stringify({ error: String(e?.message ?? e) }));
|
|
2239
2502
|
return;
|
|
2240
2503
|
}
|
|
2241
|
-
const { prompt, options } = body || {};
|
|
2504
|
+
const { prompt, options, attach } = body || {};
|
|
2505
|
+
const opts = { ...(options || {}) };
|
|
2506
|
+
// Permission shaping belongs to the request, not to the CLI process that
|
|
2507
|
+
// happens to serve it: read it before the adopt branch so a run taking over
|
|
2508
|
+
// a parked CLI applies its own mode rather than inheriting the parking
|
|
2509
|
+
// run's. The rest of \`opts\` configures the CLI at spawn and cannot change
|
|
2510
|
+
// under a live query.
|
|
2511
|
+
const settings = {
|
|
2512
|
+
autoApprove: !!opts.autoApproveTools,
|
|
2513
|
+
interactiveQuestions: !!opts.interactiveQuestions,
|
|
2514
|
+
planning: opts.permissionMode === "plan",
|
|
2515
|
+
};
|
|
2516
|
+
delete opts.autoApproveTools;
|
|
2517
|
+
delete opts.interactiveQuestions;
|
|
2518
|
+
const parkedRun = opts.resume ? findParkedRun(opts.resume) : undefined;
|
|
2519
|
+
if (parkedRun && opts.forkSession) {
|
|
2520
|
+
// The conversation is being rewound: work started on the abandoned
|
|
2521
|
+
// branch goes with it.
|
|
2522
|
+
endRun(parkedRun);
|
|
2523
|
+
} else if (parkedRun) {
|
|
2524
|
+
adoptRun(parkedRun, res, runId, prompt, !!attach, attach ? undefined : settings);
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
// An attach-only request streams the turn a parked CLI started on its own;
|
|
2528
|
+
// without one there is nothing to run.
|
|
2529
|
+
if (attach) {
|
|
2530
|
+
res.writeHead(200, NDJSON_HEADERS);
|
|
2531
|
+
res.end(JSON.stringify({ _notice: "no_parked_run", sessionId: opts.resume ?? "" }) + "\\n");
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2242
2534
|
if (!prompt) {
|
|
2243
2535
|
res.writeHead(400);
|
|
2244
2536
|
res.end("missing prompt");
|
|
@@ -2248,70 +2540,63 @@ async function handleStart(req, res, runId) {
|
|
|
2248
2540
|
const promptStream = createPromptStream();
|
|
2249
2541
|
promptStream.push(prompt);
|
|
2250
2542
|
|
|
2251
|
-
res.writeHead(200,
|
|
2252
|
-
"content-type": "application/x-ndjson",
|
|
2253
|
-
"transfer-encoding": "chunked",
|
|
2254
|
-
"x-daemon-version": VERSION,
|
|
2255
|
-
});
|
|
2256
|
-
|
|
2257
|
-
// Heartbeat: write a blank NDJSON line every 15s so HTTPS proxies
|
|
2258
|
-
// (Modal/Daytona tunnels, intermediate LBs) don't idle-kill the
|
|
2259
|
-
// stream during long blocking tool calls (e.g. TaskOutput block:true).
|
|
2260
|
-
// The host's parseNdjsonStream skips empty lines, so this is a no-op
|
|
2261
|
-
// for consumers but keeps the underlying TCP stream warm.
|
|
2262
|
-
const heartbeat = setInterval(() => {
|
|
2263
|
-
if (!res.writableEnded) res.write("\\n");
|
|
2264
|
-
}, 15000);
|
|
2265
|
-
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
2543
|
+
res.writeHead(200, NDJSON_HEADERS);
|
|
2266
2544
|
|
|
2267
|
-
const opts = { ...(options || {}) };
|
|
2268
|
-
const autoApprove = !!opts.autoApproveTools;
|
|
2269
|
-
delete opts.autoApproveTools;
|
|
2270
|
-
const interactiveQuestions = !!opts.interactiveQuestions;
|
|
2271
|
-
delete opts.interactiveQuestions;
|
|
2272
|
-
let planning = opts.permissionMode === "plan";
|
|
2273
2545
|
const permissions = new Map();
|
|
2274
|
-
const
|
|
2546
|
+
const denyAll = (response) => { for (const resolve of permissions.values()) resolve(response); permissions.clear(); };
|
|
2547
|
+
const clearPermissions = () => denyAll({ behavior: "deny", message: "Run ended", interrupt: true });
|
|
2548
|
+
// Nobody is attached to decide. Declining without \`interrupt\` lets the CLI
|
|
2549
|
+
// report and carry on with its background work instead of blocking forever
|
|
2550
|
+
// on a tool call \u2014 a blocked call never ends the turn, so the host would
|
|
2551
|
+
// never be woken to answer it either.
|
|
2552
|
+
const declinePermissions = () => denyAll({ behavior: "deny", message: "No user is attached to approve this request." });
|
|
2275
2553
|
const canUseTool = async (toolName, input, context) => {
|
|
2276
2554
|
const isQuestion = toolName === "AskUserQuestion";
|
|
2277
2555
|
const isPlan = toolName === "ExitPlanMode";
|
|
2556
|
+
const mode = run.settings;
|
|
2278
2557
|
if (context.signal.aborted) return { behavior: "deny", message: "Run cancelled", interrupt: true };
|
|
2279
|
-
if ((isQuestion || isPlan) && !interactiveQuestions) return { behavior: "deny", message: "No interactive user is available." };
|
|
2280
|
-
if (!isQuestion && !isPlan && planning && toolName !== "EnterPlanMode") return { behavior: "deny", message: "Finish planning before requesting write access." };
|
|
2281
|
-
if (!isQuestion && !isPlan && autoApprove) return { behavior: "allow", updatedInput: input };
|
|
2558
|
+
if ((isQuestion || isPlan) && !mode.interactiveQuestions) return { behavior: "deny", message: "No interactive user is available." };
|
|
2559
|
+
if (!isQuestion && !isPlan && mode.planning && toolName !== "EnterPlanMode") return { behavior: "deny", message: "Finish planning before requesting write access." };
|
|
2560
|
+
if (!isQuestion && !isPlan && mode.autoApprove) return { behavior: "allow", updatedInput: input };
|
|
2561
|
+
// Parked with nobody attached: there is no one to ask, and waiting would
|
|
2562
|
+
// wedge the CLI out of reach of any wake.
|
|
2563
|
+
if (run.parked && !run.sink) return { behavior: "deny", message: "No user is attached to approve this request." };
|
|
2282
2564
|
return new Promise((resolve) => {
|
|
2283
2565
|
const abort = () => { permissions.delete(context.toolUseID); resolve({ behavior: "deny", message: "Run cancelled", interrupt: true }); };
|
|
2284
|
-
permissions.set(context.toolUseID, (response) => { context.signal.removeEventListener("abort", abort); if (isPlan && response.behavior === "allow") planning = false; resolve(response); });
|
|
2566
|
+
permissions.set(context.toolUseID, (response) => { context.signal.removeEventListener("abort", abort); if (isPlan && response.behavior === "allow") run.settings.planning = false; resolve(response); });
|
|
2285
2567
|
context.signal.addEventListener("abort", abort, { once: true });
|
|
2286
|
-
|
|
2568
|
+
// Buffered while parked: the run that attaches answers it.
|
|
2569
|
+
writeFrame(run, { _permission: { requestId: context.toolUseID, toolName, input, title: context.title } });
|
|
2287
2570
|
});
|
|
2288
2571
|
};
|
|
2289
2572
|
opts.pathToClaudeCodeExecutable = resolveClaudeBinary(
|
|
2290
2573
|
opts.pathToClaudeCodeExecutable,
|
|
2291
2574
|
);
|
|
2292
2575
|
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2576
|
+
const run = {
|
|
2577
|
+
id: runId,
|
|
2578
|
+
sessionId: opts.resume || opts.sessionId || "",
|
|
2579
|
+
query: undefined,
|
|
2580
|
+
prompt: promptStream,
|
|
2581
|
+
permissions,
|
|
2582
|
+
clearPermissions,
|
|
2583
|
+
declinePermissions,
|
|
2584
|
+
// Replaced by each run that adopts this CLI out of a park.
|
|
2585
|
+
settings,
|
|
2586
|
+
sink: null,
|
|
2587
|
+
heartbeat: undefined,
|
|
2588
|
+
clientGone: false,
|
|
2589
|
+
parked: null,
|
|
2590
|
+
// Absolute end of the background budget, carried across parks so a CLI
|
|
2591
|
+
// that is adopted and parked again cannot extend its own ceiling.
|
|
2592
|
+
parkDeadline: Infinity,
|
|
2593
|
+
buffer: [],
|
|
2594
|
+
bufferedBytes: 0,
|
|
2595
|
+
bufferedResults: 0,
|
|
2596
|
+
bufferOverflowed: false,
|
|
2597
|
+
turnActive: false,
|
|
2303
2598
|
};
|
|
2304
|
-
|
|
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
|
-
});
|
|
2599
|
+
attachSink(run, res);
|
|
2315
2600
|
|
|
2316
2601
|
// Resume-if-exists gate. \`claude --resume <id>\` errors hard with "No
|
|
2317
2602
|
// conversation found with session ID" when the local session jsonl is
|
|
@@ -2344,44 +2629,94 @@ async function handleStart(req, res, runId) {
|
|
|
2344
2629
|
}
|
|
2345
2630
|
|
|
2346
2631
|
// Nobody left to stream to: do not start a CLI for it.
|
|
2347
|
-
if (clientGone) { res.end(); return; }
|
|
2632
|
+
if (run.clientGone) { res.end(); return; }
|
|
2348
2633
|
try {
|
|
2349
|
-
|
|
2634
|
+
run.query = query({
|
|
2350
2635
|
prompt: promptStream,
|
|
2351
2636
|
options: {
|
|
2352
2637
|
...opts,
|
|
2353
2638
|
canUseTool,
|
|
2354
2639
|
hooks: { PreToolUse: [{ hooks: [async (input) => {
|
|
2355
|
-
planning = input.permission_mode === "plan";
|
|
2356
|
-
return interactiveQuestions && ["AskUserQuestion", "ExitPlanMode"].includes(input.tool_name)
|
|
2640
|
+
run.settings.planning = input.permission_mode === "plan";
|
|
2641
|
+
return run.settings.interactiveQuestions && ["AskUserQuestion", "ExitPlanMode"].includes(input.tool_name)
|
|
2357
2642
|
? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask" } } : {};
|
|
2358
2643
|
}] }] },
|
|
2359
2644
|
},
|
|
2360
2645
|
});
|
|
2361
2646
|
} catch (e) {
|
|
2362
|
-
clearInterval(heartbeat);
|
|
2647
|
+
clearInterval(run.heartbeat);
|
|
2363
2648
|
res.write(JSON.stringify({ _error: String(e?.message ?? e) }) + "\\n");
|
|
2364
2649
|
res.end();
|
|
2365
2650
|
return;
|
|
2366
2651
|
}
|
|
2367
2652
|
|
|
2368
|
-
liveRuns.set(runId,
|
|
2653
|
+
liveRuns.set(runId, run);
|
|
2369
2654
|
|
|
2370
2655
|
// Forward every SDKMessage, not just up to the first result: in
|
|
2371
2656
|
// streaming-input mode the CLI keeps running after a turn ends and
|
|
2372
2657
|
// re-prompts the model when background work finishes. The host decides
|
|
2373
|
-
// when the run is over and disconnects (
|
|
2374
|
-
// the prompt and lets the CLI wind down
|
|
2658
|
+
// when the run is over and disconnects (the sink's "close"), which ends
|
|
2659
|
+
// the prompt and lets the CLI wind down, or parks the run first, which
|
|
2660
|
+
// keeps it. Frames go to whichever run is attached, else to the buffer.
|
|
2375
2661
|
try {
|
|
2376
|
-
for await (const message of
|
|
2377
|
-
|
|
2662
|
+
for await (const message of run.query) {
|
|
2663
|
+
observeFrame(run, message);
|
|
2664
|
+
writeFrame(run, message);
|
|
2378
2665
|
}
|
|
2379
2666
|
} catch (e) {
|
|
2380
|
-
|
|
2667
|
+
writeFrame(run, { _error: String(e?.message ?? e) });
|
|
2381
2668
|
} finally {
|
|
2382
|
-
|
|
2383
|
-
|
|
2669
|
+
const sink = run.sink;
|
|
2670
|
+
releaseRun(run);
|
|
2671
|
+
sink?.end();
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// The host settled a run whose background work is still live and is about
|
|
2676
|
+
// to leave: keep the CLI instead of winding it down. \`tasks\` is what the
|
|
2677
|
+
// host saw live (handed back to the run that attaches), \`ttlMs\` what is
|
|
2678
|
+
// left of its background budget, so unattended work never outlives it.
|
|
2679
|
+
async function handlePark(req, res, runId) {
|
|
2680
|
+
let body;
|
|
2681
|
+
try { body = await readJsonBody(req); }
|
|
2682
|
+
catch { res.writeHead(400); res.end(); return; }
|
|
2683
|
+
const run = liveRuns.get(runId);
|
|
2684
|
+
if (!run || !run.query || run.clientGone) {
|
|
2685
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
2686
|
+
res.end(JSON.stringify({ parked: false }));
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
clearTimeout(run.parked?.timer);
|
|
2690
|
+
const parked = {
|
|
2691
|
+
wakeUrl: /^https?:\\/\\//.test(String(body?.wakeUrl ?? "")) ? String(body.wakeUrl) : "",
|
|
2692
|
+
wakeToken: String(body?.wakeToken ?? ""),
|
|
2693
|
+
tasks: Array.isArray(body?.tasks) ? body.tasks : [],
|
|
2694
|
+
waking: false,
|
|
2695
|
+
wokeAt: 0,
|
|
2696
|
+
timer: undefined,
|
|
2697
|
+
};
|
|
2698
|
+
// \`ttlMs: null\` is how the host says "no bound" (backgroundTaskTimeoutMs
|
|
2699
|
+
// of Infinity). Number(null) is 0, so the raw value has to be typed \u2014
|
|
2700
|
+
// coercing it would arm an immediate teardown for an unbounded park.
|
|
2701
|
+
const ttlMs = body?.ttlMs;
|
|
2702
|
+
const requested = typeof ttlMs === "number" && Number.isFinite(ttlMs) && ttlMs >= 0
|
|
2703
|
+
? ttlMs
|
|
2704
|
+
: Infinity;
|
|
2705
|
+
// Never past a deadline inherited from an earlier park: the ceiling bounds
|
|
2706
|
+
// the work, not each park, so a CLI that is adopted and re-parked on every
|
|
2707
|
+
// wake-up cannot hold the sandbox open forever.
|
|
2708
|
+
run.parkDeadline = Math.min(
|
|
2709
|
+
run.parkDeadline ?? Infinity,
|
|
2710
|
+
requested === Infinity ? Infinity : Date.now() + requested,
|
|
2711
|
+
);
|
|
2712
|
+
if (Number.isFinite(run.parkDeadline)) {
|
|
2713
|
+
const delay = Math.max(0, run.parkDeadline - Date.now());
|
|
2714
|
+
parked.timer = setTimeout(() => { if (run.parked === parked) endRun(run); }, Math.min(delay, 2147483647));
|
|
2715
|
+
if (typeof parked.timer.unref === "function") parked.timer.unref();
|
|
2384
2716
|
}
|
|
2717
|
+
run.parked = parked;
|
|
2718
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
2719
|
+
res.end(JSON.stringify({ parked: true }));
|
|
2385
2720
|
}
|
|
2386
2721
|
|
|
2387
2722
|
async function handleSendMessage(req, res, runId) {
|
|
@@ -2414,6 +2749,14 @@ async function handleAbort(_req, res, runId) {
|
|
|
2414
2749
|
res.end();
|
|
2415
2750
|
return;
|
|
2416
2751
|
}
|
|
2752
|
+
// Stop means stop: a parked CLI kept for background work must not survive
|
|
2753
|
+
// the abort and wake the host with a new turn after the user pressed it.
|
|
2754
|
+
if (run.parked) {
|
|
2755
|
+
endRun(run);
|
|
2756
|
+
res.writeHead(204);
|
|
2757
|
+
res.end();
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2417
2760
|
await run.query.interrupt().catch(() => {});
|
|
2418
2761
|
res.writeHead(204);
|
|
2419
2762
|
res.end();
|
|
@@ -2422,9 +2765,9 @@ async function handleAbort(_req, res, runId) {
|
|
|
2422
2765
|
async function handleDelete(_req, res, runId) {
|
|
2423
2766
|
const run = liveRuns.get(runId);
|
|
2424
2767
|
if (run) {
|
|
2425
|
-
|
|
2426
|
-
run
|
|
2427
|
-
await run.query
|
|
2768
|
+
// Also drops a park: an explicit delete ends the background work too.
|
|
2769
|
+
releaseRun(run);
|
|
2770
|
+
await run.query?.interrupt().catch(() => {});
|
|
2428
2771
|
}
|
|
2429
2772
|
res.writeHead(204);
|
|
2430
2773
|
res.end();
|
|
@@ -2463,6 +2806,10 @@ const server = http.createServer((req, res) => {
|
|
|
2463
2806
|
handleAbort(req, res, decodeURIComponent(m[1]));
|
|
2464
2807
|
return;
|
|
2465
2808
|
}
|
|
2809
|
+
if (req.method === "POST" && (m = url.match(/^\\/runs\\/([^/]+)\\/park$/))) {
|
|
2810
|
+
handlePark(req, res, decodeURIComponent(m[1])).catch(() => { if (!res.headersSent) res.writeHead(400); res.end(); });
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2466
2813
|
if (req.method === "DELETE" && (m = url.match(/^\\/runs\\/([^/]+)$/))) {
|
|
2467
2814
|
handleDelete(req, res, decodeURIComponent(m[1]));
|
|
2468
2815
|
return;
|
|
@@ -2715,7 +3062,16 @@ var ClaudeCodeAgentAdapter = class {
|
|
|
2715
3062
|
};
|
|
2716
3063
|
const mcpConfigJson = buildClaudeMcpConfig(options.mcps) ?? JSON.stringify({ mcpServers: {} }, null, 2);
|
|
2717
3064
|
const artifacts = [
|
|
2718
|
-
...!sandbox ? [
|
|
3065
|
+
...!sandbox ? [
|
|
3066
|
+
{
|
|
3067
|
+
path: path8.join(
|
|
3068
|
+
target.layout.claudeDir,
|
|
3069
|
+
".claude-plugin",
|
|
3070
|
+
"plugin.json"
|
|
3071
|
+
),
|
|
3072
|
+
content: JSON.stringify({ name: "agentbox", version: "1.0.0" })
|
|
3073
|
+
}
|
|
3074
|
+
] : [],
|
|
2719
3075
|
...skillArtifacts,
|
|
2720
3076
|
...buildClaudeCommandArtifacts(options.commands, target.layout),
|
|
2721
3077
|
...buildClaudeSubagentArtifacts(options.subAgents, target.layout),
|
|
@@ -2774,13 +3130,15 @@ var ClaudeCodeAgentAdapter = class {
|
|
|
2774
3130
|
IS_SANDBOX: "1"
|
|
2775
3131
|
};
|
|
2776
3132
|
applyCliBackgroundWaitCeiling(env);
|
|
3133
|
+
env.CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS ??= "1";
|
|
2777
3134
|
const customHeaders = request.options.customHeaders;
|
|
2778
3135
|
if (customHeaders && Object.keys(customHeaders).length > 0) {
|
|
2779
3136
|
const serialized = Object.entries(customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
|
|
2780
3137
|
env.ANTHROPIC_CUSTOM_HEADERS = env.ANTHROPIC_CUSTOM_HEADERS ? `${env.ANTHROPIC_CUSTOM_HEADERS}
|
|
2781
3138
|
${serialized}` : serialized;
|
|
2782
3139
|
}
|
|
2783
|
-
const
|
|
3140
|
+
const attachOnly = request.run.resumeParked === true;
|
|
3141
|
+
const inputParts = attachOnly ? [] : await time(
|
|
2784
3142
|
debugClaude,
|
|
2785
3143
|
"validateProviderUserInput",
|
|
2786
3144
|
() => validateProviderUserInput(request.provider, request.run.input)
|
|
@@ -2804,13 +3162,15 @@ ${serialized}` : serialized;
|
|
|
2804
3162
|
});
|
|
2805
3163
|
const autoApproveTools = shouldAutoApproveClaudeTools(request.options);
|
|
2806
3164
|
const requestBody = {
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
3165
|
+
...attachOnly ? { attach: true } : {
|
|
3166
|
+
prompt: {
|
|
3167
|
+
type: "user",
|
|
3168
|
+
message: {
|
|
3169
|
+
role: "user",
|
|
3170
|
+
content: userContent
|
|
3171
|
+
},
|
|
3172
|
+
parent_tool_use_id: null
|
|
3173
|
+
}
|
|
2814
3174
|
},
|
|
2815
3175
|
options: {
|
|
2816
3176
|
...sdkOptions,
|
|
@@ -2826,11 +3186,18 @@ ${serialized}` : serialized;
|
|
|
2826
3186
|
const runUrl = `${baseUrl}/runs/${encodeURIComponent(request.runId)}`;
|
|
2827
3187
|
const cleanup = async () => {
|
|
2828
3188
|
try {
|
|
2829
|
-
await fetch(`${runUrl}/abort`, {
|
|
3189
|
+
await fetch(`${runUrl}/abort`, {
|
|
3190
|
+
method: "POST",
|
|
3191
|
+
headers: authHeaders
|
|
3192
|
+
});
|
|
2830
3193
|
} catch {
|
|
2831
3194
|
}
|
|
2832
3195
|
try {
|
|
2833
|
-
await fetch(runUrl, {
|
|
3196
|
+
await fetch(runUrl, {
|
|
3197
|
+
method: "DELETE",
|
|
3198
|
+
headers: authHeaders,
|
|
3199
|
+
signal: AbortSignal.timeout(3e3)
|
|
3200
|
+
});
|
|
2834
3201
|
} catch {
|
|
2835
3202
|
}
|
|
2836
3203
|
fetchAbort.abort();
|
|
@@ -2890,6 +3257,7 @@ ${serialized}` : serialized;
|
|
|
2890
3257
|
{ messageId: initialUuid }
|
|
2891
3258
|
)
|
|
2892
3259
|
);
|
|
3260
|
+
const tracker = new BackgroundTaskTracker();
|
|
2893
3261
|
const permissionMessages = async function* () {
|
|
2894
3262
|
for await (const item of parseNdjsonStream(response.body)) {
|
|
2895
3263
|
const control = item;
|
|
@@ -2900,12 +3268,84 @@ ${serialized}` : serialized;
|
|
|
2900
3268
|
const ask = control._permission;
|
|
2901
3269
|
const isQuestion = ask.toolName === "AskUserQuestion";
|
|
2902
3270
|
const isPlan = ask.toolName === "ExitPlanMode";
|
|
2903
|
-
const answer = await sink.requestPermission({
|
|
2904
|
-
|
|
2905
|
-
|
|
3271
|
+
const answer = await sink.requestPermission({
|
|
3272
|
+
type: "permission.requested",
|
|
3273
|
+
provider: request.provider,
|
|
3274
|
+
runId: request.runId,
|
|
3275
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3276
|
+
requestId: ask.requestId,
|
|
3277
|
+
kind: isQuestion ? "question" : isPlan ? "plan" : "tool",
|
|
3278
|
+
toolName: ask.toolName,
|
|
3279
|
+
title: isQuestion ? "Your input is needed" : isPlan ? "Review the plan" : ask.title ?? `Allow ${ask.toolName}?`,
|
|
3280
|
+
input: ask.input,
|
|
3281
|
+
...isQuestion ? { questions: normalizeUserQuestions("claude-code", ask.input) } : {}
|
|
3282
|
+
});
|
|
3283
|
+
const reply = await fetch(
|
|
3284
|
+
`${baseUrl}/runs/${encodeURIComponent(request.runId)}/permission`,
|
|
3285
|
+
{
|
|
3286
|
+
method: "POST",
|
|
3287
|
+
headers: { "content-type": "application/json", ...authHeaders },
|
|
3288
|
+
signal: fetchAbort.signal,
|
|
3289
|
+
body: JSON.stringify({
|
|
3290
|
+
requestId: ask.requestId,
|
|
3291
|
+
response: answer.decision === "allow" ? {
|
|
3292
|
+
behavior: "allow",
|
|
3293
|
+
updatedInput: isQuestion ? {
|
|
3294
|
+
...ask.input,
|
|
3295
|
+
answers: questionReply(
|
|
3296
|
+
"claude-code",
|
|
3297
|
+
ask.input,
|
|
3298
|
+
answer.answers ?? []
|
|
3299
|
+
)
|
|
3300
|
+
} : ask.input
|
|
3301
|
+
} : {
|
|
3302
|
+
behavior: "deny",
|
|
3303
|
+
message: "The user declined this request."
|
|
3304
|
+
}
|
|
3305
|
+
})
|
|
3306
|
+
}
|
|
3307
|
+
);
|
|
3308
|
+
if (!reply.ok)
|
|
3309
|
+
throw new Error(`Claude permission response failed: ${reply.status}`);
|
|
2906
3310
|
}
|
|
2907
3311
|
};
|
|
2908
|
-
|
|
3312
|
+
const parking = request.options.provider?.parkBackgroundWork;
|
|
3313
|
+
await consumeClaudeMessages(
|
|
3314
|
+
request,
|
|
3315
|
+
sink,
|
|
3316
|
+
permissionMessages(),
|
|
3317
|
+
executeStartedAt,
|
|
3318
|
+
cleanup,
|
|
3319
|
+
() => cancelled,
|
|
3320
|
+
{
|
|
3321
|
+
attachOnly,
|
|
3322
|
+
...parking ? {
|
|
3323
|
+
// The daemon keeps the CLI once told to; only then may the stream be
|
|
3324
|
+
// dropped without the wind-down that `cleanup` asks for.
|
|
3325
|
+
park: async (tasks, ttlMs) => {
|
|
3326
|
+
const response2 = await fetch(`${runUrl}/park`, {
|
|
3327
|
+
method: "POST",
|
|
3328
|
+
headers: {
|
|
3329
|
+
"content-type": "application/json",
|
|
3330
|
+
...authHeaders
|
|
3331
|
+
},
|
|
3332
|
+
signal: AbortSignal.timeout(1e4),
|
|
3333
|
+
body: JSON.stringify({
|
|
3334
|
+
wakeUrl: parking.wakeUrl,
|
|
3335
|
+
wakeToken: parking.wakeToken,
|
|
3336
|
+
tasks,
|
|
3337
|
+
ttlMs: Number.isFinite(ttlMs) ? ttlMs : null
|
|
3338
|
+
})
|
|
3339
|
+
});
|
|
3340
|
+
return response2.ok && (await response2.json()).parked === true;
|
|
3341
|
+
},
|
|
3342
|
+
detach: async () => {
|
|
3343
|
+
fetchAbort.abort();
|
|
3344
|
+
}
|
|
3345
|
+
} : {}
|
|
3346
|
+
},
|
|
3347
|
+
tracker
|
|
3348
|
+
);
|
|
2909
3349
|
return async () => void 0;
|
|
2910
3350
|
}
|
|
2911
3351
|
/**
|
|
@@ -2966,38 +3406,73 @@ ${serialized}` : serialized;
|
|
|
2966
3406
|
}
|
|
2967
3407
|
}
|
|
2968
3408
|
};
|
|
2969
|
-
async function consumeClaudeMessages(request, sink, messages, executeStartedAt, cleanup, wasCancelled = () => false, wait = {}) {
|
|
3409
|
+
async function consumeClaudeMessages(request, sink, messages, executeStartedAt, cleanup, wasCancelled = () => false, wait = {}, tracker = new BackgroundTaskTracker()) {
|
|
2970
3410
|
let accumulatedText = "";
|
|
2971
3411
|
let streamedThinkingChars = 0;
|
|
2972
3412
|
let sawResult = false;
|
|
3413
|
+
let sawSessionState = false;
|
|
2973
3414
|
let firstStreamEventLogged = false;
|
|
2974
3415
|
let firstTextDeltaLogged = false;
|
|
2975
3416
|
let lastTerminalReason;
|
|
2976
3417
|
let lastIsError = false;
|
|
2977
3418
|
const rawPayloads = [];
|
|
2978
|
-
const
|
|
2979
|
-
|
|
3419
|
+
const timeoutMs = resolveBackgroundTaskTimeoutMs(
|
|
3420
|
+
request.options.backgroundTaskTimeoutMs
|
|
3421
|
+
);
|
|
2980
3422
|
const graceMs = wait.graceMs ?? BACKGROUND_TASK_GRACE_MS;
|
|
2981
3423
|
let pendingWait;
|
|
2982
3424
|
let waitedMs = 0;
|
|
2983
3425
|
let expiry;
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3426
|
+
const finishWait = new BackgroundWaitFinish();
|
|
3427
|
+
sink.setFinishBackgroundWait?.(() => finishWait.request());
|
|
3428
|
+
let parked = false;
|
|
3429
|
+
let leave = cleanup;
|
|
3430
|
+
let foreignResults = 0;
|
|
3431
|
+
let nothingParked = false;
|
|
3432
|
+
let lastTasksKey = JSON.stringify({
|
|
3433
|
+
tasks: [],
|
|
3434
|
+
waiting: false,
|
|
3435
|
+
isParked: false
|
|
3436
|
+
});
|
|
3437
|
+
const emitTasks = (tasks, waiting, isParked = false) => {
|
|
3438
|
+
const key = JSON.stringify({ tasks, waiting, isParked });
|
|
2987
3439
|
if (key === lastTasksKey) return;
|
|
2988
3440
|
lastTasksKey = key;
|
|
2989
|
-
sink.emitEvent(
|
|
3441
|
+
sink.emitEvent(
|
|
3442
|
+
createNormalizedEvent(
|
|
3443
|
+
"background.tasks",
|
|
3444
|
+
{ provider: request.provider, runId: request.runId },
|
|
3445
|
+
{ tasks, waiting, ...isParked ? { parked: true } : {} }
|
|
3446
|
+
)
|
|
3447
|
+
);
|
|
3448
|
+
};
|
|
3449
|
+
const budgetLeftMs = () => Math.max(0, timeoutMs - waitedMs - (pendingWait?.elapsedMs() ?? 0));
|
|
3450
|
+
const tryPark = async () => {
|
|
3451
|
+
const live = tracker.liveTasks();
|
|
3452
|
+
const ttlMs = budgetLeftMs();
|
|
3453
|
+
if (!wait.park || live.length === 0 || isAborted() || ttlMs <= 0)
|
|
3454
|
+
return false;
|
|
3455
|
+
parked = await wait.park(live, ttlMs).catch(() => false);
|
|
3456
|
+
if (parked)
|
|
3457
|
+
debugClaude(
|
|
3458
|
+
"\u2605 parked the harness with %d background task(s); settling",
|
|
3459
|
+
live.length
|
|
3460
|
+
);
|
|
3461
|
+
return parked;
|
|
2990
3462
|
};
|
|
2991
3463
|
const isAborted = () => wasCancelled() || lastTerminalReason === "aborted_streaming" || lastTerminalReason === "aborted_tools";
|
|
2992
3464
|
const endWait = () => {
|
|
2993
3465
|
if (!pendingWait) return;
|
|
2994
3466
|
waitedMs += pendingWait.elapsedMs();
|
|
2995
3467
|
pendingWait.clear();
|
|
2996
|
-
pendingWait = void 0;
|
|
3468
|
+
pendingWait = finishWait.watch(void 0);
|
|
2997
3469
|
};
|
|
2998
3470
|
const settleOnFailure = (error) => {
|
|
2999
3471
|
if (!pendingWait || !sawResult || lastIsError) return false;
|
|
3000
|
-
debugClaude(
|
|
3472
|
+
debugClaude(
|
|
3473
|
+
"\u2605 transport failed during background wait; settling on the last result: %o",
|
|
3474
|
+
error
|
|
3475
|
+
);
|
|
3001
3476
|
expiry = "transport";
|
|
3002
3477
|
return true;
|
|
3003
3478
|
};
|
|
@@ -3015,7 +3490,8 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3015
3490
|
break;
|
|
3016
3491
|
}
|
|
3017
3492
|
if ("reason" in step) {
|
|
3018
|
-
|
|
3493
|
+
if (!(step.reason === "finished" && await tryPark()))
|
|
3494
|
+
expiry = step.reason;
|
|
3019
3495
|
break;
|
|
3020
3496
|
}
|
|
3021
3497
|
if (step.result.done) break;
|
|
@@ -3027,7 +3503,28 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3027
3503
|
if (!settleOnFailure(error)) throw error;
|
|
3028
3504
|
break;
|
|
3029
3505
|
}
|
|
3506
|
+
if ("_parked" in ctrl) {
|
|
3507
|
+
const info = ctrl._parked;
|
|
3508
|
+
tracker.restore(Array.isArray(info?.tasks) ? info.tasks : []);
|
|
3509
|
+
foreignResults = Math.max(
|
|
3510
|
+
0,
|
|
3511
|
+
Number(info?.turns ?? 0) - (wait.attachOnly ? 1 : 0)
|
|
3512
|
+
);
|
|
3513
|
+
const budgetLeft = info?.budgetLeftMs;
|
|
3514
|
+
if (typeof budgetLeft === "number" && Number.isFinite(timeoutMs)) {
|
|
3515
|
+
waitedMs = Math.max(0, timeoutMs - Math.max(0, budgetLeft));
|
|
3516
|
+
}
|
|
3517
|
+
debugClaude(
|
|
3518
|
+
"\u2605 attached to a parked harness: %d task(s) live, %d earlier result(s)",
|
|
3519
|
+
tracker.liveTasks().length,
|
|
3520
|
+
foreignResults
|
|
3521
|
+
);
|
|
3522
|
+
emitTasks(tracker.liveTasks(), false);
|
|
3523
|
+
continue;
|
|
3524
|
+
}
|
|
3030
3525
|
if ("_notice" in ctrl) {
|
|
3526
|
+
if (ctrl._notice === "no_parked_run" || ctrl._notice === "no_parked_turn")
|
|
3527
|
+
nothingParked = true;
|
|
3031
3528
|
debugClaude("daemon notice: %o", ctrl);
|
|
3032
3529
|
sink.emitRaw(
|
|
3033
3530
|
toRawEvent(
|
|
@@ -3043,18 +3540,31 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3043
3540
|
rawPayloads.push(message);
|
|
3044
3541
|
sink.emitRaw(toRawEvent(request.runId, message, message.type));
|
|
3045
3542
|
if (tracker.ingest(message) && pendingWait) {
|
|
3046
|
-
debugClaude(
|
|
3543
|
+
debugClaude(
|
|
3544
|
+
"\u2605 follow-up turn started; background wait over (%dms since execute start)",
|
|
3545
|
+
Date.now() - executeStartedAt
|
|
3546
|
+
);
|
|
3047
3547
|
endWait();
|
|
3048
3548
|
}
|
|
3049
3549
|
emitTasks(tracker.liveTasks(), pendingWait !== void 0);
|
|
3050
|
-
|
|
3550
|
+
const sessionState = message.type === "system" && message.subtype === "session_state_changed" ? message.state : void 0;
|
|
3551
|
+
if (sessionState) sawSessionState = true;
|
|
3552
|
+
pendingWait?.setIdle(
|
|
3553
|
+
!sawSessionState && tracker.liveTasks().length === 0
|
|
3554
|
+
);
|
|
3555
|
+
if (sessionState === "idle" && pendingWait && (tracker.liveTasks().length === 0 || await tryPark()))
|
|
3556
|
+
break;
|
|
3051
3557
|
if (message.type === "system") {
|
|
3052
3558
|
const sub = message.subtype;
|
|
3053
3559
|
if (sub === "init") {
|
|
3054
3560
|
const sys = message;
|
|
3055
|
-
if (request.run.goal && !sys.slash_commands.some(
|
|
3561
|
+
if (request.run.goal && !sys.slash_commands.some(
|
|
3562
|
+
(command) => command.replace(/^\//, "") === "goal"
|
|
3563
|
+
)) {
|
|
3056
3564
|
await cleanup();
|
|
3057
|
-
throw new Error(
|
|
3565
|
+
throw new Error(
|
|
3566
|
+
"This Claude Code installation does not expose the native /goal command."
|
|
3567
|
+
);
|
|
3058
3568
|
}
|
|
3059
3569
|
if (sys.session_id) {
|
|
3060
3570
|
debugClaude(
|
|
@@ -3156,6 +3666,12 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3156
3666
|
continue;
|
|
3157
3667
|
}
|
|
3158
3668
|
if (message.type === "result") {
|
|
3669
|
+
if (foreignResults > 0) {
|
|
3670
|
+
foreignResults--;
|
|
3671
|
+
accumulatedText = "";
|
|
3672
|
+
streamedThinkingChars = 0;
|
|
3673
|
+
continue;
|
|
3674
|
+
}
|
|
3159
3675
|
sawResult = true;
|
|
3160
3676
|
const result = message;
|
|
3161
3677
|
lastTerminalReason = result.terminal_reason;
|
|
@@ -3165,30 +3681,55 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3165
3681
|
accumulatedText = resultText;
|
|
3166
3682
|
}
|
|
3167
3683
|
const live = tracker.liveTasks();
|
|
3168
|
-
if (timeoutMs === 0 || !tracker.hasSeenBackgroundWork() || isAborted())
|
|
3169
|
-
|
|
3684
|
+
if (timeoutMs === 0 || !tracker.hasSeenBackgroundWork() || isAborted())
|
|
3685
|
+
break;
|
|
3686
|
+
debugClaude(
|
|
3687
|
+
"\u2605 turn ended with %d background task(s); waiting",
|
|
3688
|
+
live.length
|
|
3689
|
+
);
|
|
3170
3690
|
endWait();
|
|
3171
|
-
pendingWait =
|
|
3172
|
-
|
|
3691
|
+
pendingWait = finishWait.watch(
|
|
3692
|
+
new BackgroundWait(graceMs, Math.max(0, timeoutMs - waitedMs))
|
|
3693
|
+
);
|
|
3694
|
+
pendingWait.setIdle(!sawSessionState && live.length === 0);
|
|
3173
3695
|
emitTasks(live, true);
|
|
3174
3696
|
continue;
|
|
3175
3697
|
}
|
|
3176
3698
|
}
|
|
3177
|
-
if (expiry === "ceiling") {
|
|
3699
|
+
if (expiry === "ceiling" || expiry === "finished") {
|
|
3178
3700
|
const ids = tracker.liveTasks().filter((task) => task.type !== "scheduled_wakeup").map((task) => task.id);
|
|
3179
|
-
|
|
3701
|
+
if (expiry === "ceiling")
|
|
3702
|
+
debugClaude(
|
|
3703
|
+
"\u2605 background wait ceiling (%dms) hit; stopping %d task(s)",
|
|
3704
|
+
timeoutMs,
|
|
3705
|
+
ids.length
|
|
3706
|
+
);
|
|
3707
|
+
else
|
|
3708
|
+
debugClaude(
|
|
3709
|
+
"\u2605 host finished the background wait; stopping %d task(s)",
|
|
3710
|
+
ids.length
|
|
3711
|
+
);
|
|
3180
3712
|
if (wait.stopTasks) {
|
|
3181
|
-
await withTimeout(
|
|
3713
|
+
await withTimeout(
|
|
3714
|
+
wait.stopTasks(ids),
|
|
3715
|
+
wait.stopTimeoutMs ?? STOP_TASKS_TIMEOUT_MS
|
|
3716
|
+
).catch(() => void 0);
|
|
3182
3717
|
}
|
|
3183
3718
|
} else if (expiry === "grace") {
|
|
3184
3719
|
debugClaude("\u2605 background set emptied with no follow-up turn; settling");
|
|
3185
3720
|
}
|
|
3186
3721
|
if (pendingWait) {
|
|
3187
3722
|
endWait();
|
|
3188
|
-
emitTasks(
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
if (
|
|
3723
|
+
if (parked) emitTasks(tracker.liveTasks(), false, true);
|
|
3724
|
+
else emitTasks([], false);
|
|
3725
|
+
}
|
|
3726
|
+
if (parked && wait.detach) {
|
|
3727
|
+
await wait.detach();
|
|
3728
|
+
leave = wait.detach;
|
|
3729
|
+
} else await cleanup();
|
|
3730
|
+
if (nothingParked && !sawResult) sawResult = true;
|
|
3731
|
+
if (!sawResult && !wasCancelled())
|
|
3732
|
+
throw new Error("Claude Code closed before reporting a result");
|
|
3192
3733
|
const finalText = accumulatedText;
|
|
3193
3734
|
const isCancelled = isAborted();
|
|
3194
3735
|
const isError = !isCancelled && lastIsError;
|
|
@@ -3228,21 +3769,29 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3228
3769
|
);
|
|
3229
3770
|
sink.complete({
|
|
3230
3771
|
text: finalText,
|
|
3231
|
-
costData: extractClaudeCostData(rawPayloads)
|
|
3772
|
+
costData: extractClaudeCostData(rawPayloads),
|
|
3773
|
+
// Told, not guessed: an attach that found nothing parked ran no turn
|
|
3774
|
+
// at all, which from the outside looks exactly like a real tool-only
|
|
3775
|
+
// turn that produced no answer text and no usage.
|
|
3776
|
+
...nothingParked ? { nothingParked: true } : {}
|
|
3232
3777
|
});
|
|
3233
3778
|
}
|
|
3234
3779
|
} finally {
|
|
3235
3780
|
pendingWait?.clear();
|
|
3236
|
-
await
|
|
3781
|
+
await leave();
|
|
3237
3782
|
}
|
|
3238
3783
|
}
|
|
3239
3784
|
async function executeNativeClaude(request, sink, wait = {}) {
|
|
3240
3785
|
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
3241
3786
|
const claudeDir = claudeConfigDir(request.options);
|
|
3242
|
-
const input = await validateProviderUserInput(
|
|
3787
|
+
const input = await validateProviderUserInput(
|
|
3788
|
+
request.provider,
|
|
3789
|
+
request.run.input
|
|
3790
|
+
);
|
|
3243
3791
|
const prompt = new AsyncQueue();
|
|
3244
3792
|
const sessionId = request.run.resumeSessionId ?? randomUUID();
|
|
3245
3793
|
const controller = new AbortController();
|
|
3794
|
+
const tracker = new BackgroundTaskTracker();
|
|
3246
3795
|
let handle;
|
|
3247
3796
|
let processHandle;
|
|
3248
3797
|
let stopped;
|
|
@@ -3259,12 +3808,30 @@ async function executeNativeClaude(request, sink, wait = {}) {
|
|
|
3259
3808
|
});
|
|
3260
3809
|
sink.setSessionId(sessionId);
|
|
3261
3810
|
const messageId = randomUUID();
|
|
3262
|
-
prompt.push({
|
|
3263
|
-
|
|
3811
|
+
prompt.push({
|
|
3812
|
+
type: "user",
|
|
3813
|
+
uuid: messageId,
|
|
3814
|
+
message: {
|
|
3815
|
+
role: "user",
|
|
3816
|
+
content: mapToClaudeUserContent(
|
|
3817
|
+
input
|
|
3818
|
+
)
|
|
3819
|
+
},
|
|
3820
|
+
parent_tool_use_id: null
|
|
3821
|
+
});
|
|
3822
|
+
const hostEnv = Object.fromEntries(
|
|
3823
|
+
Object.entries({ ...process.env, ...request.options.env }).filter(
|
|
3824
|
+
(entry) => entry[1] !== void 0
|
|
3825
|
+
)
|
|
3826
|
+
);
|
|
3264
3827
|
applyCliBackgroundWaitCeiling(hostEnv);
|
|
3828
|
+
hostEnv.CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS ??= "1";
|
|
3265
3829
|
if (request.options.customHeaders) {
|
|
3266
3830
|
const headers = Object.entries(request.options.customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
|
|
3267
|
-
hostEnv.ANTHROPIC_CUSTOM_HEADERS = [
|
|
3831
|
+
hostEnv.ANTHROPIC_CUSTOM_HEADERS = [
|
|
3832
|
+
hostEnv.ANTHROPIC_CUSTOM_HEADERS,
|
|
3833
|
+
headers
|
|
3834
|
+
].filter(Boolean).join("\n");
|
|
3268
3835
|
}
|
|
3269
3836
|
const options = buildClaudeQueryOptions({
|
|
3270
3837
|
request,
|
|
@@ -3280,71 +3847,143 @@ async function executeNativeClaude(request, sink, wait = {}) {
|
|
|
3280
3847
|
const interactiveQuestions = hasInteractiveQuestions(request.options);
|
|
3281
3848
|
let planning = options.permissionMode === "plan";
|
|
3282
3849
|
try {
|
|
3283
|
-
handle = query({
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
return interactiveQuestions && ["AskUserQuestion", "ExitPlanMode"].includes(input2.tool_name) ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask" } } : {};
|
|
3303
|
-
}] }] },
|
|
3304
|
-
async canUseTool(toolName, input2, context) {
|
|
3305
|
-
if (cancelled || context.signal.aborted) return { behavior: "deny", message: "Run cancelled", interrupt: true };
|
|
3306
|
-
const isQuestion = toolName === "AskUserQuestion";
|
|
3307
|
-
const isPlan = toolName === "ExitPlanMode";
|
|
3308
|
-
if ((isQuestion || isPlan) && !interactiveQuestions) return { behavior: "deny", message: "No interactive user is available." };
|
|
3309
|
-
if (!isQuestion && !isPlan && planning && toolName !== "EnterPlanMode") return { behavior: "deny", message: "Finish planning before requesting write access." };
|
|
3310
|
-
if (!isQuestion && !isPlan && autoApprove) return { behavior: "allow", updatedInput: input2 };
|
|
3311
|
-
try {
|
|
3312
|
-
const response = await sink.requestPermission({
|
|
3313
|
-
type: "permission.requested",
|
|
3314
|
-
provider: request.provider,
|
|
3315
|
-
runId: request.runId,
|
|
3316
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3317
|
-
requestId: context.toolUseID,
|
|
3318
|
-
kind: isQuestion ? "question" : isPlan ? "plan" : "tool",
|
|
3319
|
-
toolName,
|
|
3320
|
-
...isQuestion ? { questions: normalizeUserQuestions("claude-code", input2) } : {},
|
|
3321
|
-
title: isQuestion ? "Your input is needed" : isPlan ? "Review the plan" : context.title ?? `Allow ${toolName}?`,
|
|
3322
|
-
message: context.description ?? context.decisionReason,
|
|
3323
|
-
input: input2,
|
|
3324
|
-
canRemember: false
|
|
3850
|
+
handle = query({
|
|
3851
|
+
prompt,
|
|
3852
|
+
options: {
|
|
3853
|
+
...options,
|
|
3854
|
+
// Use the SDK-matched CLI by default; an installed CLI is an explicit override.
|
|
3855
|
+
pathToClaudeCodeExecutable: request.options.provider?.binary,
|
|
3856
|
+
abortController: controller,
|
|
3857
|
+
// `sessionId` is rejected alongside `resume` unless `forkSession` is
|
|
3858
|
+
// set, where it names the forked session. Stamping it on forks keeps
|
|
3859
|
+
// the pre-minted id reported via `sink.setSessionId` truthful, so a
|
|
3860
|
+
// later run can resume the fork.
|
|
3861
|
+
...request.run.resumeSessionId ? {} : { sessionId },
|
|
3862
|
+
...request.options.configuration === "native" ? {} : { plugins: [{ type: "local", path: claudeDir }] },
|
|
3863
|
+
spawnClaudeCodeProcess(spawnOptions) {
|
|
3864
|
+
if (cancelled || controller.signal.aborted)
|
|
3865
|
+
throw new Error("Local run was cancelled before startup");
|
|
3866
|
+
processHandle = spawnCommand({
|
|
3867
|
+
...spawnOptions,
|
|
3868
|
+
processGroup: request.options.processGroup !== "inherited"
|
|
3325
3869
|
});
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3870
|
+
return processHandle.child;
|
|
3871
|
+
},
|
|
3872
|
+
hooks: {
|
|
3873
|
+
...options.hooks,
|
|
3874
|
+
PreToolUse: [
|
|
3875
|
+
...options.hooks?.PreToolUse ?? [],
|
|
3876
|
+
{
|
|
3877
|
+
hooks: [
|
|
3878
|
+
async (input2) => {
|
|
3879
|
+
if (input2.hook_event_name !== "PreToolUse") return {};
|
|
3880
|
+
planning = input2.permission_mode === "plan";
|
|
3881
|
+
return interactiveQuestions && ["AskUserQuestion", "ExitPlanMode"].includes(
|
|
3882
|
+
input2.tool_name
|
|
3883
|
+
) ? {
|
|
3884
|
+
hookSpecificOutput: {
|
|
3885
|
+
hookEventName: "PreToolUse",
|
|
3886
|
+
permissionDecision: "ask"
|
|
3887
|
+
}
|
|
3888
|
+
} : {};
|
|
3889
|
+
}
|
|
3890
|
+
]
|
|
3891
|
+
}
|
|
3892
|
+
]
|
|
3893
|
+
},
|
|
3894
|
+
async canUseTool(toolName, input2, context) {
|
|
3895
|
+
if (cancelled || context.signal.aborted)
|
|
3896
|
+
return {
|
|
3897
|
+
behavior: "deny",
|
|
3898
|
+
message: "Run cancelled",
|
|
3899
|
+
interrupt: true
|
|
3900
|
+
};
|
|
3901
|
+
const isQuestion = toolName === "AskUserQuestion";
|
|
3902
|
+
const isPlan = toolName === "ExitPlanMode";
|
|
3903
|
+
if ((isQuestion || isPlan) && !interactiveQuestions)
|
|
3904
|
+
return {
|
|
3905
|
+
behavior: "deny",
|
|
3906
|
+
message: "No interactive user is available."
|
|
3907
|
+
};
|
|
3908
|
+
if (!isQuestion && !isPlan && planning && toolName !== "EnterPlanMode")
|
|
3909
|
+
return {
|
|
3910
|
+
behavior: "deny",
|
|
3911
|
+
message: "Finish planning before requesting write access."
|
|
3912
|
+
};
|
|
3913
|
+
if (!isQuestion && !isPlan && autoApprove)
|
|
3914
|
+
return { behavior: "allow", updatedInput: input2 };
|
|
3915
|
+
try {
|
|
3916
|
+
const response = await sink.requestPermission({
|
|
3917
|
+
type: "permission.requested",
|
|
3918
|
+
provider: request.provider,
|
|
3919
|
+
runId: request.runId,
|
|
3920
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3921
|
+
requestId: context.toolUseID,
|
|
3922
|
+
kind: isQuestion ? "question" : isPlan ? "plan" : "tool",
|
|
3923
|
+
toolName,
|
|
3924
|
+
...isQuestion ? { questions: normalizeUserQuestions("claude-code", input2) } : {},
|
|
3925
|
+
title: isQuestion ? "Your input is needed" : isPlan ? "Review the plan" : context.title ?? `Allow ${toolName}?`,
|
|
3926
|
+
message: context.description ?? context.decisionReason,
|
|
3927
|
+
input: input2,
|
|
3928
|
+
canRemember: false
|
|
3929
|
+
});
|
|
3930
|
+
if (isPlan && response.decision === "allow") planning = false;
|
|
3931
|
+
if (!cancelled && !context.signal.aborted && response.decision === "allow")
|
|
3932
|
+
return {
|
|
3933
|
+
behavior: "allow",
|
|
3934
|
+
updatedInput: isQuestion ? {
|
|
3935
|
+
...input2,
|
|
3936
|
+
answers: questionReply(
|
|
3937
|
+
"claude-code",
|
|
3938
|
+
input2,
|
|
3939
|
+
response.answers ?? []
|
|
3940
|
+
)
|
|
3941
|
+
} : input2
|
|
3942
|
+
};
|
|
3943
|
+
return { behavior: "deny", message: "The user denied this action" };
|
|
3944
|
+
} catch {
|
|
3945
|
+
return {
|
|
3946
|
+
behavior: "deny",
|
|
3947
|
+
message: "Run cancelled",
|
|
3948
|
+
interrupt: true
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3334
3951
|
}
|
|
3335
3952
|
}
|
|
3336
|
-
}
|
|
3953
|
+
});
|
|
3337
3954
|
const live = handle;
|
|
3338
3955
|
sink.setRaw({ query: live, claudeDir, runId: request.runId });
|
|
3339
|
-
sink.emitEvent(
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3956
|
+
sink.emitEvent(
|
|
3957
|
+
createNormalizedEvent("run.started", {
|
|
3958
|
+
provider: request.provider,
|
|
3959
|
+
runId: request.runId
|
|
3960
|
+
})
|
|
3961
|
+
);
|
|
3962
|
+
sink.emitEvent(
|
|
3963
|
+
createNormalizedEvent(
|
|
3964
|
+
"message.started",
|
|
3965
|
+
{ provider: request.provider, runId: request.runId },
|
|
3966
|
+
{ messageId }
|
|
3967
|
+
)
|
|
3968
|
+
);
|
|
3969
|
+
await consumeClaudeMessages(
|
|
3970
|
+
request,
|
|
3971
|
+
sink,
|
|
3972
|
+
live,
|
|
3973
|
+
Date.now(),
|
|
3974
|
+
stop,
|
|
3975
|
+
() => cancelled,
|
|
3976
|
+
{
|
|
3977
|
+
...wait,
|
|
3978
|
+
// Native owns the CLI: ask it to stop leftover tasks before closing it.
|
|
3979
|
+
stopTasks: async (ids) => {
|
|
3980
|
+
await Promise.all(
|
|
3981
|
+
ids.map((id) => live.stopTask(id).catch(() => void 0))
|
|
3982
|
+
);
|
|
3983
|
+
}
|
|
3984
|
+
},
|
|
3985
|
+
tracker
|
|
3986
|
+
);
|
|
3348
3987
|
} catch (error) {
|
|
3349
3988
|
await stop();
|
|
3350
3989
|
if (cancelled) sink.cancel();
|
|
@@ -4607,6 +5246,8 @@ var CodexAgentAdapter = class {
|
|
|
4607
5246
|
const isRootThread = (params) => !params?.threadId || params.threadId === rootThreadId;
|
|
4608
5247
|
let goalStatus = request.run.goal ? "active" : void 0;
|
|
4609
5248
|
let pendingWait;
|
|
5249
|
+
const finishWait = new BackgroundWaitFinish();
|
|
5250
|
+
sink.setFinishBackgroundWait?.(() => finishWait.request());
|
|
4610
5251
|
let waitedMs = 0;
|
|
4611
5252
|
let turnMessageText = "";
|
|
4612
5253
|
let lastTurn;
|
|
@@ -4620,7 +5261,7 @@ var CodexAgentAdapter = class {
|
|
|
4620
5261
|
if (!pendingWait) return;
|
|
4621
5262
|
waitedMs += pendingWait.elapsedMs();
|
|
4622
5263
|
pendingWait.clear();
|
|
4623
|
-
pendingWait = void 0;
|
|
5264
|
+
pendingWait = finishWait.watch(void 0);
|
|
4624
5265
|
emitGoalWaiting(false);
|
|
4625
5266
|
};
|
|
4626
5267
|
const completion = new Promise((resolve, reject) => {
|
|
@@ -4791,7 +5432,7 @@ var CodexAgentAdapter = class {
|
|
|
4791
5432
|
}
|
|
4792
5433
|
debugCodex("\u2605 turn ended with an active goal; waiting for native continuation");
|
|
4793
5434
|
endWait();
|
|
4794
|
-
pendingWait = new BackgroundWait(BACKGROUND_TASK_GRACE_MS, Math.max(0, timeoutMs - waitedMs));
|
|
5435
|
+
pendingWait = finishWait.watch(new BackgroundWait(BACKGROUND_TASK_GRACE_MS, Math.max(0, timeoutMs - waitedMs)));
|
|
4795
5436
|
pendingWait.setIdle(true);
|
|
4796
5437
|
emitGoalWaiting(true);
|
|
4797
5438
|
}
|
|
@@ -5649,6 +6290,8 @@ var OpenCodeAgentAdapter = class {
|
|
|
5649
6290
|
let pendingWait;
|
|
5650
6291
|
let waitedMs = 0;
|
|
5651
6292
|
let expiry;
|
|
6293
|
+
const finishWait = new BackgroundWaitFinish();
|
|
6294
|
+
sink.setFinishBackgroundWait?.(() => finishWait.request());
|
|
5652
6295
|
let sawParentIdle = false;
|
|
5653
6296
|
let lastTasksKey = JSON.stringify({ tasks: [], waiting: false });
|
|
5654
6297
|
const emitTasks = (tasks, waiting) => {
|
|
@@ -5667,7 +6310,7 @@ var OpenCodeAgentAdapter = class {
|
|
|
5667
6310
|
if (!pendingWait) return;
|
|
5668
6311
|
waitedMs += pendingWait.elapsedMs();
|
|
5669
6312
|
pendingWait.clear();
|
|
5670
|
-
pendingWait = void 0;
|
|
6313
|
+
pendingWait = finishWait.watch(void 0);
|
|
5671
6314
|
};
|
|
5672
6315
|
const onChildrenChanged = () => {
|
|
5673
6316
|
if (!pendingWait) return;
|
|
@@ -5735,6 +6378,7 @@ var OpenCodeAgentAdapter = class {
|
|
|
5735
6378
|
expiry = reason;
|
|
5736
6379
|
resolveSessionTerminal();
|
|
5737
6380
|
});
|
|
6381
|
+
finishWait.watch(wait);
|
|
5738
6382
|
emitTasks(live, true);
|
|
5739
6383
|
};
|
|
5740
6384
|
const onParentBusy = () => {
|
|
@@ -6208,7 +6852,7 @@ var OpenCodeAgentAdapter = class {
|
|
|
6208
6852
|
}
|
|
6209
6853
|
sseAbort.abort();
|
|
6210
6854
|
await sseTask;
|
|
6211
|
-
if (expiry === "ceiling" || (sessionErrorFromSse || dispatchError) && liveChildren().length > 0) {
|
|
6855
|
+
if (expiry === "ceiling" || expiry === "finished" || (sessionErrorFromSse || dispatchError) && liveChildren().length > 0) {
|
|
6212
6856
|
debugOpencode(
|
|
6213
6857
|
"\u2605 run over (%s) with %d subagent(s) live; aborting them",
|
|
6214
6858
|
expiry ?? "failure",
|
|
@@ -6441,6 +7085,8 @@ var AgentRunController = class {
|
|
|
6441
7085
|
sessionIdReady;
|
|
6442
7086
|
abortHandler = async () => void 0;
|
|
6443
7087
|
abortRequested = false;
|
|
7088
|
+
finishBackgroundWaitHandler;
|
|
7089
|
+
finishBackgroundWaitRequested = false;
|
|
6444
7090
|
eventQueue = new AsyncQueue();
|
|
6445
7091
|
rawQueue = new AsyncQueue();
|
|
6446
7092
|
events = [];
|
|
@@ -6482,6 +7128,12 @@ var AgentRunController = class {
|
|
|
6482
7128
|
void abort();
|
|
6483
7129
|
}
|
|
6484
7130
|
}
|
|
7131
|
+
setFinishBackgroundWait(finish) {
|
|
7132
|
+
this.finishBackgroundWaitHandler = finish;
|
|
7133
|
+
if (this.finishBackgroundWaitRequested) {
|
|
7134
|
+
finish();
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
6485
7137
|
setSessionId(sessionId) {
|
|
6486
7138
|
if (this.sessionId) {
|
|
6487
7139
|
return;
|
|
@@ -6598,6 +7250,7 @@ var AgentRunController = class {
|
|
|
6598
7250
|
return;
|
|
6599
7251
|
}
|
|
6600
7252
|
this.settled = true;
|
|
7253
|
+
const nothingParked = result?.nothingParked === true;
|
|
6601
7254
|
this.clearPendingPermissions(
|
|
6602
7255
|
new Error(
|
|
6603
7256
|
"Agent run completed before pending permission requests resolved."
|
|
@@ -6623,7 +7276,8 @@ var AgentRunController = class {
|
|
|
6623
7276
|
events: [...this.events],
|
|
6624
7277
|
costData: this.costData,
|
|
6625
7278
|
isCancelled: false,
|
|
6626
|
-
error: errorMsg
|
|
7279
|
+
error: errorMsg,
|
|
7280
|
+
...nothingParked ? { nothingParked: true } : {}
|
|
6627
7281
|
});
|
|
6628
7282
|
return;
|
|
6629
7283
|
}
|
|
@@ -6635,7 +7289,8 @@ var AgentRunController = class {
|
|
|
6635
7289
|
rawEvents: [...this.rawEventsList],
|
|
6636
7290
|
events: [...this.events],
|
|
6637
7291
|
costData: this.costData,
|
|
6638
|
-
isCancelled: false
|
|
7292
|
+
isCancelled: false,
|
|
7293
|
+
...nothingParked ? { nothingParked: true } : {}
|
|
6639
7294
|
});
|
|
6640
7295
|
}
|
|
6641
7296
|
cancel(result) {
|
|
@@ -6719,6 +7374,10 @@ var AgentRunController = class {
|
|
|
6719
7374
|
this.clearPendingPermissions(new Error("Agent run cancelled"));
|
|
6720
7375
|
await this.abortHandler();
|
|
6721
7376
|
}
|
|
7377
|
+
async finishBackgroundWait() {
|
|
7378
|
+
this.finishBackgroundWaitRequested = true;
|
|
7379
|
+
this.finishBackgroundWaitHandler?.();
|
|
7380
|
+
}
|
|
6722
7381
|
rawEvents() {
|
|
6723
7382
|
return this.rawQueue;
|
|
6724
7383
|
}
|
|
@@ -6832,6 +7491,14 @@ var Agent = class {
|
|
|
6832
7491
|
if (runConfig.forkAtMessageId && !runConfig.forkSessionId) {
|
|
6833
7492
|
throw new Error("AgentRunConfig.forkAtMessageId requires forkSessionId.");
|
|
6834
7493
|
}
|
|
7494
|
+
if (runConfig.resumeParked) {
|
|
7495
|
+
if (this.provider !== AgentProvider.ClaudeCode || !this.options.sandbox) {
|
|
7496
|
+
throw new Error("AgentRunConfig.resumeParked is only supported for claude-code sandbox runs.");
|
|
7497
|
+
}
|
|
7498
|
+
if (!runConfig.resumeSessionId) {
|
|
7499
|
+
throw new Error("AgentRunConfig.resumeParked requires resumeSessionId.");
|
|
7500
|
+
}
|
|
7501
|
+
}
|
|
6835
7502
|
const runId = runConfig.runId ?? randomUUID3();
|
|
6836
7503
|
const streamCalledAt = Date.now();
|
|
6837
7504
|
debugAgent("stream() provider=%s runId=%s", this.provider, runId);
|