agentbox-sdk 0.1.514 → 0.1.516
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 +43 -1
- package/dist/agents/index.d.ts +2 -2
- package/dist/agents/index.js +2 -2
- package/dist/{chunk-D33LRRN7.js → chunk-SERVEWIE.js} +835 -184
- package/dist/{chunk-775FIGGL.js → chunk-V4M4LPPT.js} +12 -0
- package/dist/events/index.d.ts +1 -1
- package/dist/events/index.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/{types-NhKw_6mW.d.ts → types-BKvKgRMY.d.ts} +46 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
createNormalizedEvent,
|
|
3
3
|
normalizeRawAgentEvent,
|
|
4
4
|
toAISDKStream
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-V4M4LPPT.js";
|
|
6
6
|
import {
|
|
7
7
|
AgentBoxError,
|
|
8
8
|
AsyncQueue,
|
|
@@ -1857,6 +1857,9 @@ 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;
|
|
1862
1865
|
hasTaskSnapshot = false;
|
|
@@ -1878,6 +1881,20 @@ var BackgroundTaskTracker = class {
|
|
|
1878
1881
|
hasSeenBackgroundWork() {
|
|
1879
1882
|
return this.seenBackgroundWork;
|
|
1880
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
|
+
}
|
|
1881
1898
|
/** Feed one SDKMessage. Returns true when it started a follow-up turn. */
|
|
1882
1899
|
ingest(message) {
|
|
1883
1900
|
const m = asRecord2(message);
|
|
@@ -1916,6 +1933,7 @@ var BackgroundTaskTracker = class {
|
|
|
1916
1933
|
switch (m.subtype) {
|
|
1917
1934
|
case "background_tasks_changed":
|
|
1918
1935
|
this.hasTaskSnapshot = true;
|
|
1936
|
+
this.foreground.clear();
|
|
1919
1937
|
this.tasks.clear();
|
|
1920
1938
|
for (const entry of asArray(m.tasks)) {
|
|
1921
1939
|
const task = asRecord2(entry);
|
|
@@ -1923,15 +1941,31 @@ var BackgroundTaskTracker = class {
|
|
|
1923
1941
|
}
|
|
1924
1942
|
return false;
|
|
1925
1943
|
case "task_started":
|
|
1926
|
-
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);
|
|
1927
1947
|
return false;
|
|
1928
1948
|
case "task_notification":
|
|
1929
|
-
if (!this.hasTaskSnapshot)
|
|
1949
|
+
if (!this.hasTaskSnapshot) {
|
|
1950
|
+
this.tasks.delete(id);
|
|
1951
|
+
this.foreground.delete(id);
|
|
1952
|
+
}
|
|
1930
1953
|
return false;
|
|
1931
|
-
case "task_updated":
|
|
1932
|
-
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))) {
|
|
1933
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
|
+
}
|
|
1934
1967
|
return false;
|
|
1968
|
+
}
|
|
1935
1969
|
case "init":
|
|
1936
1970
|
return this.startTurn();
|
|
1937
1971
|
default:
|
|
@@ -2020,6 +2054,10 @@ var BackgroundWait = class {
|
|
|
2020
2054
|
}
|
|
2021
2055
|
this.grace ??= setTimeout(() => this.expire("grace"), this.graceMs);
|
|
2022
2056
|
}
|
|
2057
|
+
/** End the wait now on the host's request ({@link BackgroundWaitFinish}). */
|
|
2058
|
+
finish() {
|
|
2059
|
+
this.expire("finished");
|
|
2060
|
+
}
|
|
2023
2061
|
elapsedMs() {
|
|
2024
2062
|
return Date.now() - this.startedAt;
|
|
2025
2063
|
}
|
|
@@ -2028,9 +2066,23 @@ var BackgroundWait = class {
|
|
|
2028
2066
|
clearTimeout(this.ceiling);
|
|
2029
2067
|
}
|
|
2030
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
|
+
};
|
|
2031
2083
|
|
|
2032
2084
|
// src/agents/providers/claude-code.ts
|
|
2033
|
-
var DAEMON_PROTOCOL_VERSION = "
|
|
2085
|
+
var DAEMON_PROTOCOL_VERSION = "10";
|
|
2034
2086
|
var DAEMON_PORT = 43180;
|
|
2035
2087
|
var DAEMON_PATH = "/tmp/agentbox/claude-code/daemon.mjs";
|
|
2036
2088
|
var DAEMON_LOG_PATH = "/tmp/agentbox/claude-code/daemon.log";
|
|
@@ -2040,7 +2092,11 @@ var DAEMON_READY_TIMEOUT_MS = 3e4;
|
|
|
2040
2092
|
var DAEMON_READY_POLL_INTERVAL_MS = 250;
|
|
2041
2093
|
function claudeConfigDir(options) {
|
|
2042
2094
|
return path8.join(
|
|
2043
|
-
agentboxRoot(
|
|
2095
|
+
agentboxRoot(
|
|
2096
|
+
AgentProvider.ClaudeCode,
|
|
2097
|
+
Boolean(options.sandbox),
|
|
2098
|
+
options.stateDirectory
|
|
2099
|
+
),
|
|
2044
2100
|
".claude"
|
|
2045
2101
|
);
|
|
2046
2102
|
}
|
|
@@ -2061,7 +2117,9 @@ function buildClaudeQueryOptions(params) {
|
|
|
2061
2117
|
}
|
|
2062
2118
|
const includeHookEvents = provider?.includeHookEvents ?? false;
|
|
2063
2119
|
if (run.reasoning === "max" || run.reasoning === "ultra") {
|
|
2064
|
-
throw new Error(
|
|
2120
|
+
throw new Error(
|
|
2121
|
+
`Reasoning effort "${run.reasoning}" is only supported by Codex.`
|
|
2122
|
+
);
|
|
2065
2123
|
}
|
|
2066
2124
|
const effort = provider?.ultracode ? "xhigh" : run.reasoning;
|
|
2067
2125
|
return {
|
|
@@ -2072,7 +2130,10 @@ function buildClaudeQueryOptions(params) {
|
|
|
2072
2130
|
...params.request.options.configuration === "native" && provider?.fastMode !== void 0 ? { settings: { fastMode: provider.fastMode } } : {},
|
|
2073
2131
|
...params.request.options.configuration === "native" ? {
|
|
2074
2132
|
settingSources: ["user", "project", "local"],
|
|
2075
|
-
systemPrompt: {
|
|
2133
|
+
systemPrompt: {
|
|
2134
|
+
type: "preset",
|
|
2135
|
+
preset: "claude_code"
|
|
2136
|
+
}
|
|
2076
2137
|
} : {},
|
|
2077
2138
|
extraArgs,
|
|
2078
2139
|
includePartialMessages: true,
|
|
@@ -2164,6 +2225,199 @@ function isAuthorized(req) {
|
|
|
2164
2225
|
|
|
2165
2226
|
const liveRuns = new Map();
|
|
2166
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
|
+
|
|
2167
2421
|
// The SDK's default spawn does \`existsSync(pathToClaudeCodeExecutable)\`
|
|
2168
2422
|
// before invoking child_process.spawn \u2014 that check fails on bare names
|
|
2169
2423
|
// like "claude" because existsSync doesn't do PATH lookup. Resolve to an
|
|
@@ -2247,7 +2501,36 @@ async function handleStart(req, res, runId) {
|
|
|
2247
2501
|
res.end(JSON.stringify({ error: String(e?.message ?? e) }));
|
|
2248
2502
|
return;
|
|
2249
2503
|
}
|
|
2250
|
-
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
|
+
}
|
|
2251
2534
|
if (!prompt) {
|
|
2252
2535
|
res.writeHead(400);
|
|
2253
2536
|
res.end("missing prompt");
|
|
@@ -2257,70 +2540,63 @@ async function handleStart(req, res, runId) {
|
|
|
2257
2540
|
const promptStream = createPromptStream();
|
|
2258
2541
|
promptStream.push(prompt);
|
|
2259
2542
|
|
|
2260
|
-
res.writeHead(200,
|
|
2261
|
-
"content-type": "application/x-ndjson",
|
|
2262
|
-
"transfer-encoding": "chunked",
|
|
2263
|
-
"x-daemon-version": VERSION,
|
|
2264
|
-
});
|
|
2265
|
-
|
|
2266
|
-
// Heartbeat: write a blank NDJSON line every 15s so HTTPS proxies
|
|
2267
|
-
// (Modal/Daytona tunnels, intermediate LBs) don't idle-kill the
|
|
2268
|
-
// stream during long blocking tool calls (e.g. TaskOutput block:true).
|
|
2269
|
-
// The host's parseNdjsonStream skips empty lines, so this is a no-op
|
|
2270
|
-
// for consumers but keeps the underlying TCP stream warm.
|
|
2271
|
-
const heartbeat = setInterval(() => {
|
|
2272
|
-
if (!res.writableEnded) res.write("\\n");
|
|
2273
|
-
}, 15000);
|
|
2274
|
-
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
2543
|
+
res.writeHead(200, NDJSON_HEADERS);
|
|
2275
2544
|
|
|
2276
|
-
const opts = { ...(options || {}) };
|
|
2277
|
-
const autoApprove = !!opts.autoApproveTools;
|
|
2278
|
-
delete opts.autoApproveTools;
|
|
2279
|
-
const interactiveQuestions = !!opts.interactiveQuestions;
|
|
2280
|
-
delete opts.interactiveQuestions;
|
|
2281
|
-
let planning = opts.permissionMode === "plan";
|
|
2282
2545
|
const permissions = new Map();
|
|
2283
|
-
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." });
|
|
2284
2553
|
const canUseTool = async (toolName, input, context) => {
|
|
2285
2554
|
const isQuestion = toolName === "AskUserQuestion";
|
|
2286
2555
|
const isPlan = toolName === "ExitPlanMode";
|
|
2556
|
+
const mode = run.settings;
|
|
2287
2557
|
if (context.signal.aborted) return { behavior: "deny", message: "Run cancelled", interrupt: true };
|
|
2288
|
-
if ((isQuestion || isPlan) && !interactiveQuestions) return { behavior: "deny", message: "No interactive user is available." };
|
|
2289
|
-
if (!isQuestion && !isPlan && planning && toolName !== "EnterPlanMode") return { behavior: "deny", message: "Finish planning before requesting write access." };
|
|
2290
|
-
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." };
|
|
2291
2564
|
return new Promise((resolve) => {
|
|
2292
2565
|
const abort = () => { permissions.delete(context.toolUseID); resolve({ behavior: "deny", message: "Run cancelled", interrupt: true }); };
|
|
2293
|
-
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); });
|
|
2294
2567
|
context.signal.addEventListener("abort", abort, { once: true });
|
|
2295
|
-
|
|
2568
|
+
// Buffered while parked: the run that attaches answers it.
|
|
2569
|
+
writeFrame(run, { _permission: { requestId: context.toolUseID, toolName, input, title: context.title } });
|
|
2296
2570
|
});
|
|
2297
2571
|
};
|
|
2298
2572
|
opts.pathToClaudeCodeExecutable = resolveClaudeBinary(
|
|
2299
2573
|
opts.pathToClaudeCodeExecutable,
|
|
2300
2574
|
);
|
|
2301
2575
|
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
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,
|
|
2312
2598
|
};
|
|
2313
|
-
|
|
2314
|
-
// winds down instead of living on with its background work. Detected on
|
|
2315
|
-
// the response: \`req\` emits "close" as soon as its body is consumed (Node
|
|
2316
|
-
// >= 16), long before any disconnect, while the response only closes
|
|
2317
|
-
// early when the socket dies before the stream finished.
|
|
2318
|
-
res.on("close", () => {
|
|
2319
|
-
if (res.writableFinished) return;
|
|
2320
|
-
clientGone = true;
|
|
2321
|
-
releaseRun();
|
|
2322
|
-
queryHandle?.interrupt().catch(() => {});
|
|
2323
|
-
});
|
|
2599
|
+
attachSink(run, res);
|
|
2324
2600
|
|
|
2325
2601
|
// Resume-if-exists gate. \`claude --resume <id>\` errors hard with "No
|
|
2326
2602
|
// conversation found with session ID" when the local session jsonl is
|
|
@@ -2353,44 +2629,94 @@ async function handleStart(req, res, runId) {
|
|
|
2353
2629
|
}
|
|
2354
2630
|
|
|
2355
2631
|
// Nobody left to stream to: do not start a CLI for it.
|
|
2356
|
-
if (clientGone) { res.end(); return; }
|
|
2632
|
+
if (run.clientGone) { res.end(); return; }
|
|
2357
2633
|
try {
|
|
2358
|
-
|
|
2634
|
+
run.query = query({
|
|
2359
2635
|
prompt: promptStream,
|
|
2360
2636
|
options: {
|
|
2361
2637
|
...opts,
|
|
2362
2638
|
canUseTool,
|
|
2363
2639
|
hooks: { PreToolUse: [{ hooks: [async (input) => {
|
|
2364
|
-
planning = input.permission_mode === "plan";
|
|
2365
|
-
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)
|
|
2366
2642
|
? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask" } } : {};
|
|
2367
2643
|
}] }] },
|
|
2368
2644
|
},
|
|
2369
2645
|
});
|
|
2370
2646
|
} catch (e) {
|
|
2371
|
-
clearInterval(heartbeat);
|
|
2647
|
+
clearInterval(run.heartbeat);
|
|
2372
2648
|
res.write(JSON.stringify({ _error: String(e?.message ?? e) }) + "\\n");
|
|
2373
2649
|
res.end();
|
|
2374
2650
|
return;
|
|
2375
2651
|
}
|
|
2376
2652
|
|
|
2377
|
-
liveRuns.set(runId,
|
|
2653
|
+
liveRuns.set(runId, run);
|
|
2378
2654
|
|
|
2379
2655
|
// Forward every SDKMessage, not just up to the first result: in
|
|
2380
2656
|
// streaming-input mode the CLI keeps running after a turn ends and
|
|
2381
2657
|
// re-prompts the model when background work finishes. The host decides
|
|
2382
|
-
// when the run is over and disconnects (
|
|
2383
|
-
// 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.
|
|
2384
2661
|
try {
|
|
2385
|
-
for await (const message of
|
|
2386
|
-
|
|
2662
|
+
for await (const message of run.query) {
|
|
2663
|
+
observeFrame(run, message);
|
|
2664
|
+
writeFrame(run, message);
|
|
2387
2665
|
}
|
|
2388
2666
|
} catch (e) {
|
|
2389
|
-
|
|
2667
|
+
writeFrame(run, { _error: String(e?.message ?? e) });
|
|
2390
2668
|
} finally {
|
|
2391
|
-
|
|
2392
|
-
|
|
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;
|
|
2393
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();
|
|
2716
|
+
}
|
|
2717
|
+
run.parked = parked;
|
|
2718
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
2719
|
+
res.end(JSON.stringify({ parked: true }));
|
|
2394
2720
|
}
|
|
2395
2721
|
|
|
2396
2722
|
async function handleSendMessage(req, res, runId) {
|
|
@@ -2423,6 +2749,14 @@ async function handleAbort(_req, res, runId) {
|
|
|
2423
2749
|
res.end();
|
|
2424
2750
|
return;
|
|
2425
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
|
+
}
|
|
2426
2760
|
await run.query.interrupt().catch(() => {});
|
|
2427
2761
|
res.writeHead(204);
|
|
2428
2762
|
res.end();
|
|
@@ -2431,9 +2765,9 @@ async function handleAbort(_req, res, runId) {
|
|
|
2431
2765
|
async function handleDelete(_req, res, runId) {
|
|
2432
2766
|
const run = liveRuns.get(runId);
|
|
2433
2767
|
if (run) {
|
|
2434
|
-
|
|
2435
|
-
run
|
|
2436
|
-
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(() => {});
|
|
2437
2771
|
}
|
|
2438
2772
|
res.writeHead(204);
|
|
2439
2773
|
res.end();
|
|
@@ -2472,6 +2806,10 @@ const server = http.createServer((req, res) => {
|
|
|
2472
2806
|
handleAbort(req, res, decodeURIComponent(m[1]));
|
|
2473
2807
|
return;
|
|
2474
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
|
+
}
|
|
2475
2813
|
if (req.method === "DELETE" && (m = url.match(/^\\/runs\\/([^/]+)$/))) {
|
|
2476
2814
|
handleDelete(req, res, decodeURIComponent(m[1]));
|
|
2477
2815
|
return;
|
|
@@ -2724,7 +3062,16 @@ var ClaudeCodeAgentAdapter = class {
|
|
|
2724
3062
|
};
|
|
2725
3063
|
const mcpConfigJson = buildClaudeMcpConfig(options.mcps) ?? JSON.stringify({ mcpServers: {} }, null, 2);
|
|
2726
3064
|
const artifacts = [
|
|
2727
|
-
...!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
|
+
] : [],
|
|
2728
3075
|
...skillArtifacts,
|
|
2729
3076
|
...buildClaudeCommandArtifacts(options.commands, target.layout),
|
|
2730
3077
|
...buildClaudeSubagentArtifacts(options.subAgents, target.layout),
|
|
@@ -2790,7 +3137,8 @@ var ClaudeCodeAgentAdapter = class {
|
|
|
2790
3137
|
env.ANTHROPIC_CUSTOM_HEADERS = env.ANTHROPIC_CUSTOM_HEADERS ? `${env.ANTHROPIC_CUSTOM_HEADERS}
|
|
2791
3138
|
${serialized}` : serialized;
|
|
2792
3139
|
}
|
|
2793
|
-
const
|
|
3140
|
+
const attachOnly = request.run.resumeParked === true;
|
|
3141
|
+
const inputParts = attachOnly ? [] : await time(
|
|
2794
3142
|
debugClaude,
|
|
2795
3143
|
"validateProviderUserInput",
|
|
2796
3144
|
() => validateProviderUserInput(request.provider, request.run.input)
|
|
@@ -2814,13 +3162,15 @@ ${serialized}` : serialized;
|
|
|
2814
3162
|
});
|
|
2815
3163
|
const autoApproveTools = shouldAutoApproveClaudeTools(request.options);
|
|
2816
3164
|
const requestBody = {
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
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
|
+
}
|
|
2824
3174
|
},
|
|
2825
3175
|
options: {
|
|
2826
3176
|
...sdkOptions,
|
|
@@ -2836,11 +3186,18 @@ ${serialized}` : serialized;
|
|
|
2836
3186
|
const runUrl = `${baseUrl}/runs/${encodeURIComponent(request.runId)}`;
|
|
2837
3187
|
const cleanup = async () => {
|
|
2838
3188
|
try {
|
|
2839
|
-
await fetch(`${runUrl}/abort`, {
|
|
3189
|
+
await fetch(`${runUrl}/abort`, {
|
|
3190
|
+
method: "POST",
|
|
3191
|
+
headers: authHeaders
|
|
3192
|
+
});
|
|
2840
3193
|
} catch {
|
|
2841
3194
|
}
|
|
2842
3195
|
try {
|
|
2843
|
-
await fetch(runUrl, {
|
|
3196
|
+
await fetch(runUrl, {
|
|
3197
|
+
method: "DELETE",
|
|
3198
|
+
headers: authHeaders,
|
|
3199
|
+
signal: AbortSignal.timeout(3e3)
|
|
3200
|
+
});
|
|
2844
3201
|
} catch {
|
|
2845
3202
|
}
|
|
2846
3203
|
fetchAbort.abort();
|
|
@@ -2911,12 +3268,84 @@ ${serialized}` : serialized;
|
|
|
2911
3268
|
const ask = control._permission;
|
|
2912
3269
|
const isQuestion = ask.toolName === "AskUserQuestion";
|
|
2913
3270
|
const isPlan = ask.toolName === "ExitPlanMode";
|
|
2914
|
-
const answer = await sink.requestPermission({
|
|
2915
|
-
|
|
2916
|
-
|
|
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}`);
|
|
2917
3310
|
}
|
|
2918
3311
|
};
|
|
2919
|
-
|
|
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
|
+
);
|
|
2920
3349
|
return async () => void 0;
|
|
2921
3350
|
}
|
|
2922
3351
|
/**
|
|
@@ -2987,28 +3416,63 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
2987
3416
|
let lastTerminalReason;
|
|
2988
3417
|
let lastIsError = false;
|
|
2989
3418
|
const rawPayloads = [];
|
|
2990
|
-
const timeoutMs = resolveBackgroundTaskTimeoutMs(
|
|
3419
|
+
const timeoutMs = resolveBackgroundTaskTimeoutMs(
|
|
3420
|
+
request.options.backgroundTaskTimeoutMs
|
|
3421
|
+
);
|
|
2991
3422
|
const graceMs = wait.graceMs ?? BACKGROUND_TASK_GRACE_MS;
|
|
2992
3423
|
let pendingWait;
|
|
2993
3424
|
let waitedMs = 0;
|
|
2994
3425
|
let expiry;
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
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 });
|
|
2998
3439
|
if (key === lastTasksKey) return;
|
|
2999
3440
|
lastTasksKey = key;
|
|
3000
|
-
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;
|
|
3001
3462
|
};
|
|
3002
3463
|
const isAborted = () => wasCancelled() || lastTerminalReason === "aborted_streaming" || lastTerminalReason === "aborted_tools";
|
|
3003
3464
|
const endWait = () => {
|
|
3004
3465
|
if (!pendingWait) return;
|
|
3005
3466
|
waitedMs += pendingWait.elapsedMs();
|
|
3006
3467
|
pendingWait.clear();
|
|
3007
|
-
pendingWait = void 0;
|
|
3468
|
+
pendingWait = finishWait.watch(void 0);
|
|
3008
3469
|
};
|
|
3009
3470
|
const settleOnFailure = (error) => {
|
|
3010
3471
|
if (!pendingWait || !sawResult || lastIsError) return false;
|
|
3011
|
-
debugClaude(
|
|
3472
|
+
debugClaude(
|
|
3473
|
+
"\u2605 transport failed during background wait; settling on the last result: %o",
|
|
3474
|
+
error
|
|
3475
|
+
);
|
|
3012
3476
|
expiry = "transport";
|
|
3013
3477
|
return true;
|
|
3014
3478
|
};
|
|
@@ -3026,7 +3490,8 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3026
3490
|
break;
|
|
3027
3491
|
}
|
|
3028
3492
|
if ("reason" in step) {
|
|
3029
|
-
|
|
3493
|
+
if (!(step.reason === "finished" && await tryPark()))
|
|
3494
|
+
expiry = step.reason;
|
|
3030
3495
|
break;
|
|
3031
3496
|
}
|
|
3032
3497
|
if (step.result.done) break;
|
|
@@ -3038,7 +3503,28 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3038
3503
|
if (!settleOnFailure(error)) throw error;
|
|
3039
3504
|
break;
|
|
3040
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
|
+
}
|
|
3041
3525
|
if ("_notice" in ctrl) {
|
|
3526
|
+
if (ctrl._notice === "no_parked_run" || ctrl._notice === "no_parked_turn")
|
|
3527
|
+
nothingParked = true;
|
|
3042
3528
|
debugClaude("daemon notice: %o", ctrl);
|
|
3043
3529
|
sink.emitRaw(
|
|
3044
3530
|
toRawEvent(
|
|
@@ -3054,21 +3540,31 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3054
3540
|
rawPayloads.push(message);
|
|
3055
3541
|
sink.emitRaw(toRawEvent(request.runId, message, message.type));
|
|
3056
3542
|
if (tracker.ingest(message) && pendingWait) {
|
|
3057
|
-
debugClaude(
|
|
3543
|
+
debugClaude(
|
|
3544
|
+
"\u2605 follow-up turn started; background wait over (%dms since execute start)",
|
|
3545
|
+
Date.now() - executeStartedAt
|
|
3546
|
+
);
|
|
3058
3547
|
endWait();
|
|
3059
3548
|
}
|
|
3060
3549
|
emitTasks(tracker.liveTasks(), pendingWait !== void 0);
|
|
3061
3550
|
const sessionState = message.type === "system" && message.subtype === "session_state_changed" ? message.state : void 0;
|
|
3062
3551
|
if (sessionState) sawSessionState = true;
|
|
3063
|
-
pendingWait?.setIdle(
|
|
3064
|
-
|
|
3552
|
+
pendingWait?.setIdle(
|
|
3553
|
+
!sawSessionState && tracker.liveTasks().length === 0
|
|
3554
|
+
);
|
|
3555
|
+
if (sessionState === "idle" && pendingWait && (tracker.liveTasks().length === 0 || await tryPark()))
|
|
3556
|
+
break;
|
|
3065
3557
|
if (message.type === "system") {
|
|
3066
3558
|
const sub = message.subtype;
|
|
3067
3559
|
if (sub === "init") {
|
|
3068
3560
|
const sys = message;
|
|
3069
|
-
if (request.run.goal && !sys.slash_commands.some(
|
|
3561
|
+
if (request.run.goal && !sys.slash_commands.some(
|
|
3562
|
+
(command) => command.replace(/^\//, "") === "goal"
|
|
3563
|
+
)) {
|
|
3070
3564
|
await cleanup();
|
|
3071
|
-
throw new Error(
|
|
3565
|
+
throw new Error(
|
|
3566
|
+
"This Claude Code installation does not expose the native /goal command."
|
|
3567
|
+
);
|
|
3072
3568
|
}
|
|
3073
3569
|
if (sys.session_id) {
|
|
3074
3570
|
debugClaude(
|
|
@@ -3170,6 +3666,12 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3170
3666
|
continue;
|
|
3171
3667
|
}
|
|
3172
3668
|
if (message.type === "result") {
|
|
3669
|
+
if (foreignResults > 0) {
|
|
3670
|
+
foreignResults--;
|
|
3671
|
+
accumulatedText = "";
|
|
3672
|
+
streamedThinkingChars = 0;
|
|
3673
|
+
continue;
|
|
3674
|
+
}
|
|
3173
3675
|
sawResult = true;
|
|
3174
3676
|
const result = message;
|
|
3175
3677
|
lastTerminalReason = result.terminal_reason;
|
|
@@ -3179,30 +3681,55 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3179
3681
|
accumulatedText = resultText;
|
|
3180
3682
|
}
|
|
3181
3683
|
const live = tracker.liveTasks();
|
|
3182
|
-
if (timeoutMs === 0 || !tracker.hasSeenBackgroundWork() || isAborted())
|
|
3183
|
-
|
|
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
|
+
);
|
|
3184
3690
|
endWait();
|
|
3185
|
-
pendingWait =
|
|
3691
|
+
pendingWait = finishWait.watch(
|
|
3692
|
+
new BackgroundWait(graceMs, Math.max(0, timeoutMs - waitedMs))
|
|
3693
|
+
);
|
|
3186
3694
|
pendingWait.setIdle(!sawSessionState && live.length === 0);
|
|
3187
3695
|
emitTasks(live, true);
|
|
3188
3696
|
continue;
|
|
3189
3697
|
}
|
|
3190
3698
|
}
|
|
3191
|
-
if (expiry === "ceiling") {
|
|
3699
|
+
if (expiry === "ceiling" || expiry === "finished") {
|
|
3192
3700
|
const ids = tracker.liveTasks().filter((task) => task.type !== "scheduled_wakeup").map((task) => task.id);
|
|
3193
|
-
|
|
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
|
+
);
|
|
3194
3712
|
if (wait.stopTasks) {
|
|
3195
|
-
await withTimeout(
|
|
3713
|
+
await withTimeout(
|
|
3714
|
+
wait.stopTasks(ids),
|
|
3715
|
+
wait.stopTimeoutMs ?? STOP_TASKS_TIMEOUT_MS
|
|
3716
|
+
).catch(() => void 0);
|
|
3196
3717
|
}
|
|
3197
3718
|
} else if (expiry === "grace") {
|
|
3198
3719
|
debugClaude("\u2605 background set emptied with no follow-up turn; settling");
|
|
3199
3720
|
}
|
|
3200
3721
|
if (pendingWait) {
|
|
3201
3722
|
endWait();
|
|
3202
|
-
emitTasks(
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
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");
|
|
3206
3733
|
const finalText = accumulatedText;
|
|
3207
3734
|
const isCancelled = isAborted();
|
|
3208
3735
|
const isError = !isCancelled && lastIsError;
|
|
@@ -3242,18 +3769,25 @@ async function consumeClaudeMessages(request, sink, messages, executeStartedAt,
|
|
|
3242
3769
|
);
|
|
3243
3770
|
sink.complete({
|
|
3244
3771
|
text: finalText,
|
|
3245
|
-
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 } : {}
|
|
3246
3777
|
});
|
|
3247
3778
|
}
|
|
3248
3779
|
} finally {
|
|
3249
3780
|
pendingWait?.clear();
|
|
3250
|
-
await
|
|
3781
|
+
await leave();
|
|
3251
3782
|
}
|
|
3252
3783
|
}
|
|
3253
3784
|
async function executeNativeClaude(request, sink, wait = {}) {
|
|
3254
3785
|
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
3255
3786
|
const claudeDir = claudeConfigDir(request.options);
|
|
3256
|
-
const input = await validateProviderUserInput(
|
|
3787
|
+
const input = await validateProviderUserInput(
|
|
3788
|
+
request.provider,
|
|
3789
|
+
request.run.input
|
|
3790
|
+
);
|
|
3257
3791
|
const prompt = new AsyncQueue();
|
|
3258
3792
|
const sessionId = request.run.resumeSessionId ?? randomUUID();
|
|
3259
3793
|
const controller = new AbortController();
|
|
@@ -3274,13 +3808,30 @@ async function executeNativeClaude(request, sink, wait = {}) {
|
|
|
3274
3808
|
});
|
|
3275
3809
|
sink.setSessionId(sessionId);
|
|
3276
3810
|
const messageId = randomUUID();
|
|
3277
|
-
prompt.push({
|
|
3278
|
-
|
|
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
|
+
);
|
|
3279
3827
|
applyCliBackgroundWaitCeiling(hostEnv);
|
|
3280
3828
|
hostEnv.CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS ??= "1";
|
|
3281
3829
|
if (request.options.customHeaders) {
|
|
3282
3830
|
const headers = Object.entries(request.options.customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
|
|
3283
|
-
hostEnv.ANTHROPIC_CUSTOM_HEADERS = [
|
|
3831
|
+
hostEnv.ANTHROPIC_CUSTOM_HEADERS = [
|
|
3832
|
+
hostEnv.ANTHROPIC_CUSTOM_HEADERS,
|
|
3833
|
+
headers
|
|
3834
|
+
].filter(Boolean).join("\n");
|
|
3284
3835
|
}
|
|
3285
3836
|
const options = buildClaudeQueryOptions({
|
|
3286
3837
|
request,
|
|
@@ -3296,71 +3847,143 @@ async function executeNativeClaude(request, sink, wait = {}) {
|
|
|
3296
3847
|
const interactiveQuestions = hasInteractiveQuestions(request.options);
|
|
3297
3848
|
let planning = options.permissionMode === "plan";
|
|
3298
3849
|
try {
|
|
3299
|
-
handle = query({
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
return interactiveQuestions && ["AskUserQuestion", "ExitPlanMode"].includes(input2.tool_name) ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask" } } : {};
|
|
3319
|
-
}] }] },
|
|
3320
|
-
async canUseTool(toolName, input2, context) {
|
|
3321
|
-
if (cancelled || context.signal.aborted) return { behavior: "deny", message: "Run cancelled", interrupt: true };
|
|
3322
|
-
const isQuestion = toolName === "AskUserQuestion";
|
|
3323
|
-
const isPlan = toolName === "ExitPlanMode";
|
|
3324
|
-
if ((isQuestion || isPlan) && !interactiveQuestions) return { behavior: "deny", message: "No interactive user is available." };
|
|
3325
|
-
if (!isQuestion && !isPlan && planning && toolName !== "EnterPlanMode") return { behavior: "deny", message: "Finish planning before requesting write access." };
|
|
3326
|
-
if (!isQuestion && !isPlan && autoApprove) return { behavior: "allow", updatedInput: input2 };
|
|
3327
|
-
try {
|
|
3328
|
-
const response = await sink.requestPermission({
|
|
3329
|
-
type: "permission.requested",
|
|
3330
|
-
provider: request.provider,
|
|
3331
|
-
runId: request.runId,
|
|
3332
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3333
|
-
requestId: context.toolUseID,
|
|
3334
|
-
kind: isQuestion ? "question" : isPlan ? "plan" : "tool",
|
|
3335
|
-
toolName,
|
|
3336
|
-
...isQuestion ? { questions: normalizeUserQuestions("claude-code", input2) } : {},
|
|
3337
|
-
title: isQuestion ? "Your input is needed" : isPlan ? "Review the plan" : context.title ?? `Allow ${toolName}?`,
|
|
3338
|
-
message: context.description ?? context.decisionReason,
|
|
3339
|
-
input: input2,
|
|
3340
|
-
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"
|
|
3341
3869
|
});
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
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
|
+
}
|
|
3350
3951
|
}
|
|
3351
3952
|
}
|
|
3352
|
-
}
|
|
3953
|
+
});
|
|
3353
3954
|
const live = handle;
|
|
3354
3955
|
sink.setRaw({ query: live, claudeDir, runId: request.runId });
|
|
3355
|
-
sink.emitEvent(
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
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
|
+
);
|
|
3364
3987
|
} catch (error) {
|
|
3365
3988
|
await stop();
|
|
3366
3989
|
if (cancelled) sink.cancel();
|
|
@@ -4623,6 +5246,8 @@ var CodexAgentAdapter = class {
|
|
|
4623
5246
|
const isRootThread = (params) => !params?.threadId || params.threadId === rootThreadId;
|
|
4624
5247
|
let goalStatus = request.run.goal ? "active" : void 0;
|
|
4625
5248
|
let pendingWait;
|
|
5249
|
+
const finishWait = new BackgroundWaitFinish();
|
|
5250
|
+
sink.setFinishBackgroundWait?.(() => finishWait.request());
|
|
4626
5251
|
let waitedMs = 0;
|
|
4627
5252
|
let turnMessageText = "";
|
|
4628
5253
|
let lastTurn;
|
|
@@ -4636,7 +5261,7 @@ var CodexAgentAdapter = class {
|
|
|
4636
5261
|
if (!pendingWait) return;
|
|
4637
5262
|
waitedMs += pendingWait.elapsedMs();
|
|
4638
5263
|
pendingWait.clear();
|
|
4639
|
-
pendingWait = void 0;
|
|
5264
|
+
pendingWait = finishWait.watch(void 0);
|
|
4640
5265
|
emitGoalWaiting(false);
|
|
4641
5266
|
};
|
|
4642
5267
|
const completion = new Promise((resolve, reject) => {
|
|
@@ -4807,7 +5432,7 @@ var CodexAgentAdapter = class {
|
|
|
4807
5432
|
}
|
|
4808
5433
|
debugCodex("\u2605 turn ended with an active goal; waiting for native continuation");
|
|
4809
5434
|
endWait();
|
|
4810
|
-
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)));
|
|
4811
5436
|
pendingWait.setIdle(true);
|
|
4812
5437
|
emitGoalWaiting(true);
|
|
4813
5438
|
}
|
|
@@ -5665,6 +6290,8 @@ var OpenCodeAgentAdapter = class {
|
|
|
5665
6290
|
let pendingWait;
|
|
5666
6291
|
let waitedMs = 0;
|
|
5667
6292
|
let expiry;
|
|
6293
|
+
const finishWait = new BackgroundWaitFinish();
|
|
6294
|
+
sink.setFinishBackgroundWait?.(() => finishWait.request());
|
|
5668
6295
|
let sawParentIdle = false;
|
|
5669
6296
|
let lastTasksKey = JSON.stringify({ tasks: [], waiting: false });
|
|
5670
6297
|
const emitTasks = (tasks, waiting) => {
|
|
@@ -5683,7 +6310,7 @@ var OpenCodeAgentAdapter = class {
|
|
|
5683
6310
|
if (!pendingWait) return;
|
|
5684
6311
|
waitedMs += pendingWait.elapsedMs();
|
|
5685
6312
|
pendingWait.clear();
|
|
5686
|
-
pendingWait = void 0;
|
|
6313
|
+
pendingWait = finishWait.watch(void 0);
|
|
5687
6314
|
};
|
|
5688
6315
|
const onChildrenChanged = () => {
|
|
5689
6316
|
if (!pendingWait) return;
|
|
@@ -5751,6 +6378,7 @@ var OpenCodeAgentAdapter = class {
|
|
|
5751
6378
|
expiry = reason;
|
|
5752
6379
|
resolveSessionTerminal();
|
|
5753
6380
|
});
|
|
6381
|
+
finishWait.watch(wait);
|
|
5754
6382
|
emitTasks(live, true);
|
|
5755
6383
|
};
|
|
5756
6384
|
const onParentBusy = () => {
|
|
@@ -6224,7 +6852,7 @@ var OpenCodeAgentAdapter = class {
|
|
|
6224
6852
|
}
|
|
6225
6853
|
sseAbort.abort();
|
|
6226
6854
|
await sseTask;
|
|
6227
|
-
if (expiry === "ceiling" || (sessionErrorFromSse || dispatchError) && liveChildren().length > 0) {
|
|
6855
|
+
if (expiry === "ceiling" || expiry === "finished" || (sessionErrorFromSse || dispatchError) && liveChildren().length > 0) {
|
|
6228
6856
|
debugOpencode(
|
|
6229
6857
|
"\u2605 run over (%s) with %d subagent(s) live; aborting them",
|
|
6230
6858
|
expiry ?? "failure",
|
|
@@ -6457,6 +7085,8 @@ var AgentRunController = class {
|
|
|
6457
7085
|
sessionIdReady;
|
|
6458
7086
|
abortHandler = async () => void 0;
|
|
6459
7087
|
abortRequested = false;
|
|
7088
|
+
finishBackgroundWaitHandler;
|
|
7089
|
+
finishBackgroundWaitRequested = false;
|
|
6460
7090
|
eventQueue = new AsyncQueue();
|
|
6461
7091
|
rawQueue = new AsyncQueue();
|
|
6462
7092
|
events = [];
|
|
@@ -6498,6 +7128,12 @@ var AgentRunController = class {
|
|
|
6498
7128
|
void abort();
|
|
6499
7129
|
}
|
|
6500
7130
|
}
|
|
7131
|
+
setFinishBackgroundWait(finish) {
|
|
7132
|
+
this.finishBackgroundWaitHandler = finish;
|
|
7133
|
+
if (this.finishBackgroundWaitRequested) {
|
|
7134
|
+
finish();
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
6501
7137
|
setSessionId(sessionId) {
|
|
6502
7138
|
if (this.sessionId) {
|
|
6503
7139
|
return;
|
|
@@ -6614,6 +7250,7 @@ var AgentRunController = class {
|
|
|
6614
7250
|
return;
|
|
6615
7251
|
}
|
|
6616
7252
|
this.settled = true;
|
|
7253
|
+
const nothingParked = result?.nothingParked === true;
|
|
6617
7254
|
this.clearPendingPermissions(
|
|
6618
7255
|
new Error(
|
|
6619
7256
|
"Agent run completed before pending permission requests resolved."
|
|
@@ -6639,7 +7276,8 @@ var AgentRunController = class {
|
|
|
6639
7276
|
events: [...this.events],
|
|
6640
7277
|
costData: this.costData,
|
|
6641
7278
|
isCancelled: false,
|
|
6642
|
-
error: errorMsg
|
|
7279
|
+
error: errorMsg,
|
|
7280
|
+
...nothingParked ? { nothingParked: true } : {}
|
|
6643
7281
|
});
|
|
6644
7282
|
return;
|
|
6645
7283
|
}
|
|
@@ -6651,7 +7289,8 @@ var AgentRunController = class {
|
|
|
6651
7289
|
rawEvents: [...this.rawEventsList],
|
|
6652
7290
|
events: [...this.events],
|
|
6653
7291
|
costData: this.costData,
|
|
6654
|
-
isCancelled: false
|
|
7292
|
+
isCancelled: false,
|
|
7293
|
+
...nothingParked ? { nothingParked: true } : {}
|
|
6655
7294
|
});
|
|
6656
7295
|
}
|
|
6657
7296
|
cancel(result) {
|
|
@@ -6735,6 +7374,10 @@ var AgentRunController = class {
|
|
|
6735
7374
|
this.clearPendingPermissions(new Error("Agent run cancelled"));
|
|
6736
7375
|
await this.abortHandler();
|
|
6737
7376
|
}
|
|
7377
|
+
async finishBackgroundWait() {
|
|
7378
|
+
this.finishBackgroundWaitRequested = true;
|
|
7379
|
+
this.finishBackgroundWaitHandler?.();
|
|
7380
|
+
}
|
|
6738
7381
|
rawEvents() {
|
|
6739
7382
|
return this.rawQueue;
|
|
6740
7383
|
}
|
|
@@ -6848,6 +7491,14 @@ var Agent = class {
|
|
|
6848
7491
|
if (runConfig.forkAtMessageId && !runConfig.forkSessionId) {
|
|
6849
7492
|
throw new Error("AgentRunConfig.forkAtMessageId requires forkSessionId.");
|
|
6850
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
|
+
}
|
|
6851
7502
|
const runId = runConfig.runId ?? randomUUID3();
|
|
6852
7503
|
const streamCalledAt = Date.now();
|
|
6853
7504
|
debugAgent("stream() provider=%s runId=%s", this.provider, runId);
|