@livx.cc/agentx 0.99.47 → 0.99.48

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/dist/cli.js CHANGED
@@ -1509,6 +1509,394 @@ var init_tools = __esm({
1509
1509
  }
1510
1510
  });
1511
1511
 
1512
+ // src/tools.jobs.ts
1513
+ function formatJobNotice(n) {
1514
+ return `[job ${n.id} ${n.status}${n.reason ? `: ${n.reason}` : ""}] ${n.kind}${n.label ? ` \`${n.label}\`` : ""}
1515
+ ` + (n.tail ? `${n.tail.replace(/\n+$/, "")}
1516
+ ` : "(no output)\n") + `Read the full output with JobOutput({id:"${n.id}"}).`;
1517
+ }
1518
+ function makeJobTools(registry, opts = {}) {
1519
+ const maxWaitMs = opts.maxWaitMs ?? 12e4;
1520
+ return [
1521
+ {
1522
+ name: "JobOutput",
1523
+ description: "Read a job's output by id. Without `offset`: the buffered tail. With `offset` (the `next` value from a previous read): only what arrived since.",
1524
+ parameters: { type: "object", required: ["id"], properties: { id: { type: "string" }, offset: { type: "number", description: "return output after this position (from a previous read's `next`)" } } },
1525
+ async run({ id, offset }) {
1526
+ const r = registry.read(String(id), Number(offset) || 0);
1527
+ if (!r) return NO_JOB(String(id));
1528
+ const st = registry.status(String(id));
1529
+ return `[${st.status}${st.reason ? `: ${st.reason}` : ""}] next=${r.next}${r.truncated ? " (older output dropped)" : ""}
1530
+ ${r.text.replace(/\n+$/, "") || "(no new output)"}`;
1531
+ }
1532
+ },
1533
+ {
1534
+ name: "JobStatus",
1535
+ description: "Status of a job (running/done/error/killed, age, last activity). Omit `id` to list all jobs.",
1536
+ parameters: { type: "object", properties: { id: { type: "string" } } },
1537
+ async run({ id }) {
1538
+ const now4 = registry.options.now();
1539
+ if (!id) {
1540
+ const all = registry.list();
1541
+ return all.length ? all.map((j2) => describe(j2, now4)).join("\n") : "(no background jobs)";
1542
+ }
1543
+ const j = registry.get(String(id));
1544
+ return j ? describe(j, now4) : NO_JOB(String(id));
1545
+ }
1546
+ },
1547
+ {
1548
+ name: "JobKill",
1549
+ description: "Stop a running job by id (sandbox jobs are cancelled without committing; processes get SIGTERM; MCP calls are cancelled).",
1550
+ parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
1551
+ async run({ id }) {
1552
+ return registry.kill(String(id), "killed by JobKill") ? `Killed job ${id}.` : NO_JOB(String(id));
1553
+ }
1554
+ },
1555
+ {
1556
+ name: "JobWait",
1557
+ description: `Block until a job finishes or \`timeoutMs\` passes (max ${maxWaitMs}ms), then return its status and output tail. Prefer this over sleeping.`,
1558
+ parameters: { type: "object", required: ["id"], properties: { id: { type: "string" }, timeoutMs: { type: "number" } } },
1559
+ async run({ id, timeoutMs }) {
1560
+ const j = await registry.wait(String(id), Math.min(Number(timeoutMs) || maxWaitMs, maxWaitMs));
1561
+ if (!j) return NO_JOB(String(id));
1562
+ const tail = (registry.output(j.id) ?? "").slice(-4e3).replace(/\n+$/, "");
1563
+ return `${describe(j, registry.options.now())}
1564
+ ${tail || "(no output)"}`;
1565
+ }
1566
+ }
1567
+ ];
1568
+ }
1569
+ var log5, JobRegistryOptions, secs, JobRegistry, NO_JOB, describe;
1570
+ var init_tools_jobs = __esm({
1571
+ "src/tools.jobs.ts"() {
1572
+ "use strict";
1573
+ init_logging();
1574
+ log5 = forComponent("jobs");
1575
+ JobRegistryOptions = class {
1576
+ /** Tail buffer cap per job (chars). */
1577
+ maxBuffer = 256 * 1024;
1578
+ /** Idle window per kind: no output/progress/CPU for this long ⇒ stuck. Kinds without an activity channel
1579
+ * (host, native) are absent on purpose — silence is all they can ever show, so it proves nothing; their
1580
+ * bound is the hard cap. Explicit `background:true` shell jobs opt out per job (a quiet server is healthy). */
1581
+ idleMs = { shell: 10 * 6e4, mcp: 10 * 6e4 };
1582
+ /** Absolute ceiling per kind (0/absent = none). */
1583
+ hardCapMs = { mcp: 30 * 6e4 };
1584
+ /** Soft foreground timeout per kind: past it the caller stops waiting and gets a job id instead. */
1585
+ foregroundMs = { mcp: 3e4 };
1586
+ /** How often the reaper runs while reapable jobs exist. */
1587
+ sweepMs = 15e3;
1588
+ /** Grace between the kill signal and a forced kill (producers that escalate read this). */
1589
+ killGraceMs = 5e3;
1590
+ /** Finished jobs kept for inspection; older finished ones are pruned (every delegated tool call is a job). */
1591
+ keepFinished = 200;
1592
+ /** Clock (injectable for tests). Late-bound so a patched Date.now is honoured. */
1593
+ now = () => Date.now();
1594
+ /** Terminal-state notification for jobs with `notify` (the Agent wires this to `inject`). */
1595
+ onExit;
1596
+ };
1597
+ secs = (ms) => `${Math.round(ms / 1e3)}s`;
1598
+ JobRegistry = class {
1599
+ options;
1600
+ jobs = /* @__PURE__ */ new Map();
1601
+ seq = 0;
1602
+ timer;
1603
+ constructor(options) {
1604
+ this.options = { ...new JobRegistryOptions(), ...options };
1605
+ }
1606
+ /** Register externally-driven work (a process, a request, a native tool) and get a handle to feed it. */
1607
+ track(opts = {}) {
1608
+ const o = this.options;
1609
+ const kind = opts.kind ?? "task";
1610
+ const now4 = o.now();
1611
+ let settle;
1612
+ const done = new Promise((r) => {
1613
+ settle = r;
1614
+ });
1615
+ const job = {
1616
+ id: `job-${++this.seq}`,
1617
+ kind,
1618
+ label: opts.label ?? "",
1619
+ status: "running",
1620
+ startedAt: now4,
1621
+ lastActivityAt: now4,
1622
+ bytes: 0,
1623
+ holds: !!opts.holds,
1624
+ buf: "",
1625
+ dropped: 0,
1626
+ idleMs: opts.idleMs ?? o.idleMs[kind] ?? 0,
1627
+ hardCapMs: opts.hardCapMs ?? o.hardCapMs[kind] ?? 0,
1628
+ drain: !!opts.drain,
1629
+ notify: !!opts.notify,
1630
+ idleAfterActivity: !!opts.idleAfterActivity,
1631
+ active: false,
1632
+ cpuTime: opts.cpuTime,
1633
+ onKill: opts.onKill,
1634
+ onExit: opts.onExit,
1635
+ done,
1636
+ settle
1637
+ };
1638
+ this.jobs.set(job.id, job);
1639
+ if (opts.seed) this.append(job, opts.seed);
1640
+ this.arm();
1641
+ return {
1642
+ id: job.id,
1643
+ chunk: (s) => {
1644
+ if (s) {
1645
+ this.append(job, s);
1646
+ job.lastActivityAt = o.now();
1647
+ job.active = true;
1648
+ }
1649
+ },
1650
+ touch: () => {
1651
+ job.lastActivityAt = o.now();
1652
+ job.active = true;
1653
+ },
1654
+ finish: (status, result, reason) => this.end(job, status, result, reason),
1655
+ detach: () => {
1656
+ job.holds = false;
1657
+ job.notify = true;
1658
+ job.drain = true;
1659
+ },
1660
+ get status() {
1661
+ return job.status;
1662
+ }
1663
+ };
1664
+ }
1665
+ /** Start `fn` in the background and return a job id immediately (sandbox/subagent semantics: drained at turn end). */
1666
+ start(fn, opts = {}) {
1667
+ return this.run(fn, { drain: true, ...opts }).handle.id;
1668
+ }
1669
+ /**
1670
+ * Run `fn` in the FOREGROUND with a soft timeout: resolves `{done:true,value}` if it settles in time, else
1671
+ * `{done:false,id}` — the work keeps going as a detached job whose completion is reported via `onExit`.
1672
+ * A rejection before the timeout is rethrown (the caller's normal error path). `softTimeoutMs` ≤ 0 waits forever.
1673
+ */
1674
+ async runForeground(fn, opts = {}) {
1675
+ const { handle, promise } = this.run(fn, opts);
1676
+ const soft = opts.softTimeoutMs ?? this.options.foregroundMs[opts.kind ?? "task"] ?? 0;
1677
+ let t;
1678
+ const late = soft > 0 ? new Promise((r) => {
1679
+ t = setTimeout(() => r("late"), soft);
1680
+ }) : new Promise(() => {
1681
+ });
1682
+ try {
1683
+ const r = await Promise.race([promise.then((v) => ({ v }), (e) => ({ e })), late]);
1684
+ if (r !== "late" && handle.status !== "running") this.jobs.delete(handle.id);
1685
+ if (r === "late") {
1686
+ if (handle.status === "running") {
1687
+ handle.detach();
1688
+ return { done: false, id: handle.id };
1689
+ }
1690
+ const settled = await promise.then((v) => ({ v }), (e) => ({ e }));
1691
+ if ("e" in settled) throw settled.e;
1692
+ return { done: true, value: settled.v };
1693
+ }
1694
+ if ("e" in r) throw r.e;
1695
+ return { done: true, value: r.v };
1696
+ } finally {
1697
+ if (t) clearTimeout(t);
1698
+ }
1699
+ }
1700
+ run(fn, opts) {
1701
+ const ctl = new AbortController();
1702
+ const handle = this.track({ ...opts, onKill: (reason) => {
1703
+ ctl.abort(Object.assign(new Error(reason), { name: "AbortError", code: "job_killed" }));
1704
+ opts.onKill?.(reason);
1705
+ } });
1706
+ let promise;
1707
+ try {
1708
+ promise = fn({ signal: ctl.signal, onChunk: handle.chunk, touch: handle.touch });
1709
+ } catch (e) {
1710
+ promise = Promise.reject(e);
1711
+ }
1712
+ promise.then(
1713
+ (res) => {
1714
+ if (handle.status === "running" && typeof res === "string" && res) handle.chunk(res);
1715
+ handle.finish("done", res);
1716
+ },
1717
+ (e) => {
1718
+ if (handle.status === "running") handle.chunk(`
1719
+ [error] ${e?.message ?? e}`);
1720
+ handle.finish("error", void 0, e?.message ?? String(e));
1721
+ }
1722
+ );
1723
+ return { handle, promise };
1724
+ }
1725
+ append(job, s) {
1726
+ job.buf += s;
1727
+ const over = job.buf.length - this.options.maxBuffer;
1728
+ if (over > 0) {
1729
+ job.buf = job.buf.slice(over);
1730
+ job.dropped += over;
1731
+ }
1732
+ job.bytes = job.buf.length;
1733
+ }
1734
+ end(job, status, result, reason) {
1735
+ if (job.status !== "running") return;
1736
+ job.status = status;
1737
+ job.endedAt = this.options.now();
1738
+ job.result = result;
1739
+ if (reason && status !== "done") job.reason = reason;
1740
+ job.holds = false;
1741
+ job.settle();
1742
+ this.prune();
1743
+ const onExit = job.onExit ?? this.options.onExit;
1744
+ if (job.notify && onExit) {
1745
+ try {
1746
+ onExit({ id: job.id, kind: job.kind, label: job.label, status, reason: job.reason, tail: job.buf.slice(-4e3), result });
1747
+ } catch (e) {
1748
+ log5.warn(`job onExit handler threw for ${job.id}: ${e}`);
1749
+ }
1750
+ }
1751
+ }
1752
+ prune() {
1753
+ const finished = [...this.jobs.values()].filter((j) => j.status !== "running");
1754
+ for (const j of finished.slice(0, Math.max(0, finished.length - this.options.keepFinished))) this.jobs.delete(j.id);
1755
+ }
1756
+ /** Current tail output (null = no such job). */
1757
+ output(id) {
1758
+ return this.jobs.get(id)?.buf ?? null;
1759
+ }
1760
+ /** Incremental read: output from absolute char `offset` (clamped to what the ring still holds). */
1761
+ read(id, offset = 0) {
1762
+ const j = this.jobs.get(id);
1763
+ if (!j) return null;
1764
+ const start = Math.max(0, offset - j.dropped);
1765
+ return { text: j.buf.slice(start), next: j.dropped + j.buf.length, truncated: offset < j.dropped };
1766
+ }
1767
+ get(id) {
1768
+ const j = this.jobs.get(id);
1769
+ return j ? this.info(j) : null;
1770
+ }
1771
+ status(id) {
1772
+ const j = this.jobs.get(id);
1773
+ return j ? { status: j.status, bytes: j.bytes, ...j.reason ? { reason: j.reason } : {} } : null;
1774
+ }
1775
+ list() {
1776
+ return [...this.jobs.values()].map((j) => this.info(j));
1777
+ }
1778
+ info(j) {
1779
+ const { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result } = j;
1780
+ return { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result };
1781
+ }
1782
+ /** Wait (bounded) for a job to settle. Resolves the job's info either way; null for an unknown id. */
1783
+ async wait(id, timeoutMs) {
1784
+ const j = this.jobs.get(id);
1785
+ if (!j) return null;
1786
+ let t;
1787
+ await Promise.race([j.done, new Promise((r) => {
1788
+ t = setTimeout(r, Math.max(0, timeoutMs));
1789
+ })]);
1790
+ if (t) clearTimeout(t);
1791
+ return this.info(j);
1792
+ }
1793
+ /** Stop a job: runs its `onKill` (abort/SIGTERM) and marks it killed with `reason`. */
1794
+ kill(id, reason = "killed by request") {
1795
+ const j = this.jobs.get(id);
1796
+ if (!j) return false;
1797
+ if (j.status === "running") {
1798
+ this.end(j, "killed", void 0, reason);
1799
+ try {
1800
+ j.onKill?.(reason);
1801
+ } catch (e) {
1802
+ log5.warn(`onKill for ${id} threw: ${e}`);
1803
+ }
1804
+ }
1805
+ return true;
1806
+ }
1807
+ /** Kill every running job — or, with `onlyDrainable`, only turn-scoped ones (sandbox/sub-agent/handed-off MCP),
1808
+ * leaving jobs meant to outlive a run (a background dev server) alone. */
1809
+ killAll(reason = "run ended", onlyDrainable = false) {
1810
+ for (const j of this.jobs.values()) if (!onlyDrainable || j.drain) this.kill(j.id, reason);
1811
+ }
1812
+ /** Jobs something is synchronously waiting on (see `JobTrackOptions.holds`) that are still running. */
1813
+ holding() {
1814
+ return [...this.jobs.values()].filter((j) => j.status === "running" && j.holds).map((j) => this.info(j));
1815
+ }
1816
+ /** Await every drainable running job (turn end). Returns the ids that were still running when drain began. */
1817
+ async drain(signal) {
1818
+ const pending = [...this.jobs.values()].filter((j) => j.status === "running" && j.drain);
1819
+ const all = Promise.all(pending.map((j) => j.done));
1820
+ await (signal ? Promise.race([all, new Promise((r) => signal.aborted ? r() : signal.addEventListener("abort", () => r(), { once: true }))]) : all);
1821
+ return pending.map((j) => j.id);
1822
+ }
1823
+ /**
1824
+ * The stuck-job decision, synchronous part: kill every running job past its hard cap, or past its idle
1825
+ * window when it has no CPU probe to argue otherwise. Cheap — the stall watchdog calls it on every tick.
1826
+ * Returns the killed ids. Jobs WITH a probe are left to `sweep()`.
1827
+ */
1828
+ reap() {
1829
+ const now4 = this.options.now();
1830
+ const killed = [];
1831
+ for (const j of this.jobs.values()) {
1832
+ if (j.status !== "running") continue;
1833
+ const verdict = this.verdict(j, now4);
1834
+ if (!verdict || verdict.idle && j.cpuTime) continue;
1835
+ this.stuck(j, verdict.reason, now4);
1836
+ killed.push(j.id);
1837
+ }
1838
+ return killed;
1839
+ }
1840
+ /** Full sweep: sample CPU probes (advancing CPU = activity), then `reap()` including probed jobs. */
1841
+ async sweep() {
1842
+ const now4 = this.options.now();
1843
+ for (const j of [...this.jobs.values()]) {
1844
+ if (j.status !== "running" || !j.cpuTime || !j.idleMs) continue;
1845
+ let cpu;
1846
+ try {
1847
+ cpu = await j.cpuTime();
1848
+ } catch (e) {
1849
+ log5.debug(`cpu probe for ${j.id} failed: ${e}`);
1850
+ }
1851
+ if (cpu != null && j.lastCpu != null && cpu > j.lastCpu) j.lastActivityAt = this.options.now();
1852
+ const prev = j.lastCpu;
1853
+ if (cpu != null) j.lastCpu = cpu;
1854
+ const verdict = this.verdict(j, now4);
1855
+ if (!verdict?.idle || j.status !== "running") continue;
1856
+ if (cpu == null) {
1857
+ log5.debug(`not reaping ${j.id}: ${verdict.reason} but CPU unknown`);
1858
+ continue;
1859
+ }
1860
+ if (prev == null) continue;
1861
+ this.stuck(j, `${verdict.reason}, no CPU progress (${prev}s \u2192 ${cpu}s)`, now4);
1862
+ }
1863
+ return this.reap();
1864
+ }
1865
+ verdict(j, now4) {
1866
+ if (j.hardCapMs && now4 - j.startedAt >= j.hardCapMs) return { reason: `stuck: exceeded hard cap ${secs(j.hardCapMs)}`, idle: false };
1867
+ if (j.idleMs && (!j.idleAfterActivity || j.active) && now4 - j.lastActivityAt >= j.idleMs) return { reason: `stuck: idle ${secs(now4 - j.lastActivityAt)}`, idle: true };
1868
+ return null;
1869
+ }
1870
+ stuck(j, reason, now4) {
1871
+ log5.warn(`reaping ${j.id} (${j.kind} "${j.label}"): ${reason} \u2014 age ${secs(now4 - j.startedAt)}, last activity ${secs(now4 - j.lastActivityAt)} ago, idleMs=${j.idleMs}, hardCapMs=${j.hardCapMs}, ${j.bytes} byte(s) output`);
1872
+ if (j.onKill) j.notify = true;
1873
+ this.kill(j.id, reason);
1874
+ }
1875
+ /** Keep a reaper interval alive only while a reapable job is running (unref'd: never holds the process open). */
1876
+ arm() {
1877
+ if (this.timer || !this.options.sweepMs) return;
1878
+ this.timer = setInterval(() => {
1879
+ const live = [...this.jobs.values()].some((j) => j.status === "running" && (j.idleMs || j.hardCapMs));
1880
+ if (!live) {
1881
+ clearInterval(this.timer);
1882
+ this.timer = void 0;
1883
+ return;
1884
+ }
1885
+ void this.sweep().catch((e) => log5.warn(`job sweep failed: ${e}`));
1886
+ }, this.options.sweepMs);
1887
+ this.timer.unref?.();
1888
+ }
1889
+ /** Stop the reaper timer (tests / teardown). */
1890
+ dispose() {
1891
+ if (this.timer) clearInterval(this.timer);
1892
+ this.timer = void 0;
1893
+ }
1894
+ };
1895
+ NO_JOB = (id) => `Error: no background job '${id}'. Use JobStatus with no id to list jobs.`;
1896
+ describe = (j, now4) => `${j.id} ${j.status}${j.reason ? ` (${j.reason})` : ""} ${j.kind}${j.label ? " " + j.label : ""} \xB7 ${secs((j.endedAt ?? now4) - j.startedAt)} old \xB7 last activity ${secs(now4 - j.lastActivityAt)} ago \xB7 ${j.bytes} byte(s)`;
1897
+ }
1898
+ });
1899
+
1512
1900
  // src/worktree.ts
1513
1901
  var worktree_exports = {};
1514
1902
  __export(worktree_exports, {
@@ -2118,7 +2506,10 @@ __export(tools_shell_exports, {
2118
2506
  ShellJobRegistry: () => ShellJobRegistry,
2119
2507
  formatJobExit: () => formatJobExit,
2120
2508
  makeRealShellTool: () => makeRealShellTool,
2121
- makeShellJobTools: () => makeShellJobTools
2509
+ makeShellJobTools: () => makeShellJobTools,
2510
+ parsePsTime: () => parsePsTime,
2511
+ psGroupCpuSeconds: () => psGroupCpuSeconds,
2512
+ sumTreeCpu: () => sumTreeCpu
2122
2513
  });
2123
2514
  function killGroup(proc, signal) {
2124
2515
  if (!proc?.pid) return false;
@@ -2148,10 +2539,39 @@ async function nodeSpawn() {
2148
2539
  return _spawn;
2149
2540
  }
2150
2541
  function formatJobExit(n) {
2151
- return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}] \`${n.command}\`
2542
+ return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}${n.reason ? `: ${n.reason}` : ""}] \`${n.command}\`
2152
2543
  ` + (n.tail ? `${n.tail}
2153
2544
  ` : "(no output)\n") + `Read the full output with ShellOutput({id:"${n.id}"}).`;
2154
2545
  }
2546
+ function parsePsTime(t) {
2547
+ const m = t.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/);
2548
+ if (!m) return void 0;
2549
+ return Number(m[1] ?? 0) * 86400 + Number(m[2] ?? 0) * 3600 + Number(m[3]) * 60 + Number(m[4]);
2550
+ }
2551
+ async function psGroupCpuSeconds(pgid) {
2552
+ try {
2553
+ const { execFile: execFile4 } = await import("child_process");
2554
+ const out = await new Promise((res, rej) => execFile4("ps", ["-A", "-o", "pid=,ppid=,pgid=,time="], { timeout: 5e3, env: { ...process.env, LC_ALL: "C" } }, (e, so) => e ? rej(e) : res(String(so))));
2555
+ return sumTreeCpu(out, pgid);
2556
+ } catch (e) {
2557
+ log17.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
2558
+ return void 0;
2559
+ }
2560
+ }
2561
+ function sumTreeCpu(psOut, root) {
2562
+ const rows = psOut.split("\n").map((l) => l.trim().split(/\s+/)).filter((r) => r.length >= 4).map(([pid, ppid, pgid, time]) => ({ pid: Number(pid), ppid: Number(ppid), pgid: Number(pgid), sec: parsePsTime(time) }));
2563
+ const inJob = new Set(rows.filter((r) => r.pgid === root || r.pid === root).map((r) => r.pid));
2564
+ for (let grew = true; grew; ) {
2565
+ grew = false;
2566
+ for (const r of rows) if (!inJob.has(r.pid) && inJob.has(r.ppid)) {
2567
+ inJob.add(r.pid);
2568
+ grew = true;
2569
+ }
2570
+ }
2571
+ let total;
2572
+ for (const r of rows) if (inJob.has(r.pid) && r.sec != null) total = (total ?? 0) + r.sec;
2573
+ return total;
2574
+ }
2155
2575
  function makeRealShellTool(options) {
2156
2576
  const defaultTimeoutMs = options.timeoutMs ?? 12e4;
2157
2577
  const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 6e5, defaultTimeoutMs);
@@ -2282,7 +2702,7 @@ function makeRealShellTool(options) {
2282
2702
  proc.stderr?.on("data", collect);
2283
2703
  proc.on("error", (err2) => {
2284
2704
  if (err2?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
2285
- log16.debug("shell spawn error", err2);
2705
+ log17.debug("shell spawn error", err2);
2286
2706
  finish(`[exit 1] ${err2?.message ?? err2}${out ? "\n" + clean(out) : ""}`);
2287
2707
  });
2288
2708
  proc.on("close", (code) => {
@@ -2302,7 +2722,7 @@ function makeRealShellTool(options) {
2302
2722
  };
2303
2723
  }
2304
2724
  function handoffFor(timeoutMs, id, body, notifies) {
2305
- const head = `[still running] exceeded ${timeoutMs}ms, so it was handed to background job ${id} \u2014 NOT killed, it is still going. Check on it with ShellOutput({id:"${id}"}) / ShellStatus({id:"${id}"}), stop it with ShellKill({id:"${id}"}). ` + (notifies ? "Its completion will be reported to you, so you may continue with other work meanwhile." : "Nothing will tell you when it finishes \u2014 poll it yourself before you rely on its result.");
2725
+ const head = `[still running] exceeded ${timeoutMs}ms, so it was handed to background job ${id} \u2014 NOT killed, it is still going (it is only stopped if it goes silent with no CPU activity for a long time, and then you are told why). Check on it with ShellOutput({id:"${id}"}) / ShellStatus({id:"${id}"}), stop it with ShellKill({id:"${id}"}). ` + (notifies ? "Its completion will be reported to you, so you may continue with other work meanwhile." : "Nothing will tell you when it finishes \u2014 poll it yourself before you rely on its result.");
2306
2726
  return body ? `${head}
2307
2727
  Output so far:
2308
2728
  ${body}` : head;
@@ -2326,7 +2746,7 @@ function makeShellJobTools(registry) {
2326
2746
  const out = registry.output(String(id));
2327
2747
  if (out == null) return NO_JOB2(String(id));
2328
2748
  const st = registry.status(String(id));
2329
- return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ""}]
2749
+ return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ""}${st.reason ? `: ${st.reason}` : ""}]
2330
2750
  ${clean(out) || "(no output yet)"}`;
2331
2751
  }
2332
2752
  },
@@ -2341,13 +2761,13 @@ ${clean(out) || "(no output yet)"}`;
2341
2761
  return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.command}`).join("\n") : "(no background jobs)";
2342
2762
  }
2343
2763
  const st = registry.status(String(id));
2344
- return st ? `${st.status}${st.exitCode != null ? ` (exit ${st.exitCode})` : ""} \xB7 ${st.bytes} byte(s) buffered` : NO_JOB2(String(id));
2764
+ return st ? `${st.status}${st.exitCode != null ? ` (exit ${st.exitCode})` : ""}${st.reason ? ` \u2014 ${st.reason}` : ""} \xB7 ${st.bytes} byte(s) buffered` : NO_JOB2(String(id));
2345
2765
  }
2346
2766
  },
2347
2767
  {
2348
2768
  ...dropOnRebind,
2349
2769
  name: "ShellKill",
2350
- description: "Stop a running background Shell job by id (SIGTERM).",
2770
+ description: "Stop a running background Shell job by id (SIGTERM to its process group, SIGKILL if it ignores that).",
2351
2771
  parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
2352
2772
  async run({ id }) {
2353
2773
  return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB2(String(id));
@@ -2355,7 +2775,7 @@ ${clean(out) || "(no output yet)"}`;
2355
2775
  }
2356
2776
  ];
2357
2777
  }
2358
- var log16, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
2778
+ var log17, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
2359
2779
  var init_tools_shell = __esm({
2360
2780
  "src/tools.shell.ts"() {
2361
2781
  "use strict";
@@ -2363,62 +2783,94 @@ var init_tools_shell = __esm({
2363
2783
  init_redact();
2364
2784
  init_logging();
2365
2785
  init_shell_sandbox();
2366
- log16 = forComponent("shell");
2786
+ init_tools_jobs();
2787
+ log17 = forComponent("shell");
2367
2788
  clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
2368
2789
  DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
2369
2790
  SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
2370
2791
  ShellJobRegistry = class {
2371
2792
  constructor(cfg) {
2372
2793
  this.cfg = cfg;
2794
+ this.jobs = cfg.jobs ?? new JobRegistry(cfg.maxBuffer ? { maxBuffer: cfg.maxBuffer } : {});
2373
2795
  if (cfg.killOnExit && typeof process !== "undefined") process.once("exit", () => this.killAll());
2374
2796
  }
2375
2797
  cfg;
2376
- jobs = /* @__PURE__ */ new Map();
2377
- seq = 0;
2798
+ jobs;
2799
+ meta = /* @__PURE__ */ new Map();
2378
2800
  async start(command) {
2379
- const id = `job-${++this.seq}`;
2380
- const max = this.cfg.maxBuffer ?? 256 * 1024;
2381
- const job = { command, buf: "", status: "running" };
2382
- const append = (chunk) => {
2383
- const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
2384
- job.buf = (job.buf + s).slice(-max);
2385
- };
2801
+ const h = this.track(command, { idleMs: 0 });
2386
2802
  try {
2387
2803
  const spawn4 = this.cfg.spawn ?? await nodeSpawn();
2388
2804
  const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: "/bin/sh", args: ["-c", command] };
2389
- const proc = spawn4(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED });
2390
- job.proc = proc;
2391
- proc.stdout?.on("data", append);
2392
- proc.stderr?.on("data", append);
2393
- proc.on("error", (err2) => {
2394
- if (job.status === "running") {
2395
- job.status = "error";
2396
- append(`
2397
- [error] ${err2?.message ?? err2}`);
2398
- this.notifyExit(id, job);
2399
- }
2400
- });
2401
- proc.on("close", (code) => {
2402
- if (job.status === "running") {
2403
- job.status = "exited";
2404
- job.exitCode = code ?? void 0;
2405
- this.notifyExit(id, job);
2406
- }
2407
- });
2805
+ this.attach(h, spawn4(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED }));
2408
2806
  } catch (e) {
2409
- job.status = "error";
2410
- job.buf = `failed to spawn: ${e?.message ?? e}`;
2807
+ h.chunk(`failed to spawn: ${e?.message ?? e}`);
2808
+ h.finish("error", void 0, e?.message ?? String(e));
2411
2809
  }
2412
- this.jobs.set(id, job);
2413
- return id;
2414
- }
2415
- /** Fire `onExit` at most once per job, with the tail so the model can act without a second round-trip. */
2416
- notified = /* @__PURE__ */ new Set();
2417
- notifyExit(id, job) {
2418
- if (this.notified.has(id) || !this.cfg.onExit) return;
2419
- this.notified.add(id);
2810
+ return h.id;
2811
+ }
2812
+ track(command, extra) {
2813
+ const meta = { command };
2814
+ let id = "";
2815
+ const h = this.jobs.track({
2816
+ kind: "shell",
2817
+ label: command,
2818
+ seed: extra.seed,
2819
+ idleMs: extra.idleMs,
2820
+ cpuTime: extra.cpu ? async () => meta.proc?.pid ? (this.cfg.cpuTime ?? psGroupCpuSeconds)(meta.proc.pid) : void 0 : void 0,
2821
+ onKill: () => this.signal(meta),
2822
+ onExit: (n) => this.notifyExit(id, meta, n.status, n.reason),
2823
+ notify: true
2824
+ });
2825
+ id = h.id;
2826
+ this.meta.set(id, meta);
2827
+ return h;
2828
+ }
2829
+ attach(h, proc) {
2830
+ const meta = this.meta.get(h.id);
2831
+ meta.proc = proc;
2832
+ const append = (chunk) => h.chunk(typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "");
2833
+ proc.stdout?.on("data", append);
2834
+ proc.stderr?.on("data", append);
2835
+ proc.on("error", (err2) => {
2836
+ if (h.status === "running") {
2837
+ append(`
2838
+ [error] ${err2?.message ?? err2}`);
2839
+ h.finish("error", void 0, err2?.message ?? String(err2));
2840
+ }
2841
+ });
2842
+ proc.on("close", (code) => {
2843
+ if (h.status === "running") {
2844
+ meta.exitCode = code ?? void 0;
2845
+ h.finish("done");
2846
+ }
2847
+ });
2848
+ }
2849
+ /** SIGTERM the whole group (bg children are detached group leaders — /bin/sh alone would orphan a forked
2850
+ * server), then SIGKILL after the grace period if it is still there. Falls back to the pid for fakes. */
2851
+ signal(meta) {
2852
+ const proc = meta.proc;
2853
+ if (!killGroup(proc, "SIGTERM")) {
2854
+ try {
2855
+ proc?.kill("SIGTERM");
2856
+ } catch {
2857
+ }
2858
+ }
2859
+ if (!proc?.pid) return;
2860
+ let closed = false;
2861
+ proc.on("close", () => {
2862
+ closed = true;
2863
+ });
2864
+ const t = setTimeout(() => {
2865
+ if (!closed) killGroup(proc, "SIGKILL");
2866
+ }, this.jobs.options.killGraceMs);
2867
+ t.unref?.();
2868
+ }
2869
+ /** Fire `onExit` at most once per job (the registry guarantees one terminal state), with the tail so the model can act without a second round-trip. */
2870
+ notifyExit(id, meta, status, reason) {
2871
+ if (!this.cfg.onExit || status === "killed" && !reason?.startsWith("stuck")) return;
2420
2872
  try {
2421
- this.cfg.onExit({ id, command: job.command, status: job.status, exitCode: job.exitCode, tail: clean(job.buf).slice(-4e3) });
2873
+ this.cfg.onExit({ id, command: meta.command, status: shellStatus(status), exitCode: meta.exitCode, tail: clean(this.jobs.output(id) ?? "").slice(-4e3), ...reason && status !== "done" ? { reason } : {} });
2422
2874
  } catch {
2423
2875
  }
2424
2876
  }
@@ -2437,14 +2889,20 @@ var init_tools_shell = __esm({
2437
2889
  }
2438
2890
  /** Current tail output for a job (null = no such job). */
2439
2891
  output(id) {
2440
- return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? "" : null);
2892
+ return this.meta.has(id) ? this.jobs.output(id) : null;
2441
2893
  }
2894
+ // null too once the shared registry pruned it
2442
2895
  status(id) {
2443
- const j = this.jobs.get(id);
2444
- return j ? { status: j.status, exitCode: j.exitCode, bytes: j.buf.length } : null;
2896
+ const m = this.meta.get(id);
2897
+ const j = m && this.jobs.get(id);
2898
+ if (!m || !j) {
2899
+ this.meta.delete(id);
2900
+ return null;
2901
+ }
2902
+ return { status: shellStatus(j.status), exitCode: m.exitCode, bytes: j.bytes, ...j.reason ? { reason: j.reason } : {} };
2445
2903
  }
2446
2904
  list() {
2447
- return [...this.jobs].map(([id, j]) => ({ id, command: j.command, status: j.status }));
2905
+ return [...this.meta].filter(([id]) => this.jobs.get(id)).map(([id, m]) => ({ id, command: m.command, status: shellStatus(this.jobs.get(id).status) }));
2448
2906
  }
2449
2907
  /**
2450
2908
  * Take over an ALREADY-RUNNING child as a background job. A foreground command that outruns its
@@ -2452,53 +2910,22 @@ var init_tools_shell = __esm({
2452
2910
  * worse, a launcher that spawned its own detached worker leaves that worker running with nothing
2453
2911
  * tracking it. Adopting hands the model a handle instead: the command keeps going, its completion
2454
2912
  * is reported like any other job, and `seed` carries the output produced before the handoff.
2913
+ * Unlike an explicit background job it IS liveness-checked (idle window + CPU probe): nobody asked for
2914
+ * a long-lived process here, so one that goes silent and CPU-idle is stuck, not serving.
2455
2915
  */
2456
2916
  adopt(command, proc, seed = "") {
2457
- const id = `job-${++this.seq}`;
2458
- const max = this.cfg.maxBuffer ?? 256 * 1024;
2459
- const job = { command, buf: seed.slice(-max), status: "running", proc };
2460
- const append = (chunk) => {
2461
- const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
2462
- job.buf = (job.buf + s).slice(-max);
2463
- };
2464
- proc.stdout?.on("data", append);
2465
- proc.stderr?.on("data", append);
2466
- proc.on("error", (err2) => {
2467
- if (job.status === "running") {
2468
- job.status = "error";
2469
- append(`
2470
- [error] ${err2?.message ?? err2}`);
2471
- this.notifyExit(id, job);
2472
- }
2473
- });
2474
- proc.on("close", (code) => {
2475
- if (job.status === "running") {
2476
- job.status = "exited";
2477
- job.exitCode = code ?? void 0;
2478
- this.notifyExit(id, job);
2479
- }
2480
- });
2481
- this.jobs.set(id, job);
2482
- return id;
2917
+ const h = this.track(command, { seed, cpu: true });
2918
+ this.attach(h, proc);
2919
+ return h.id;
2483
2920
  }
2484
- kill(id) {
2485
- const j = this.jobs.get(id);
2486
- if (!j) return false;
2487
- if (j.status === "running") {
2488
- if (!killGroup(j.proc, "SIGTERM")) {
2489
- try {
2490
- j.proc?.kill("SIGTERM");
2491
- } catch {
2492
- }
2493
- }
2494
- j.status = "killed";
2495
- }
2496
- return true;
2921
+ kill(id, reason = "killed by ShellKill") {
2922
+ return this.meta.has(id) && this.jobs.kill(id, reason);
2497
2923
  }
2498
2924
  killAll() {
2499
- for (const id of this.jobs.keys()) this.kill(id);
2925
+ for (const id of this.meta.keys()) this.jobs.kill(id, "agent exiting");
2500
2926
  }
2501
2927
  };
2928
+ shellStatus = (s) => s === "done" ? "exited" : s === "running" ? "running" : s === "killed" ? "killed" : "error";
2502
2929
  NO_JOB2 = (id) => `Error: no background job '${id}'. Use ShellStatus with no id to list jobs, or start one with Shell({background:true}).`;
2503
2930
  }
2504
2931
  });
@@ -2580,111 +3007,7 @@ import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo
2580
3007
  // src/Agent.ts
2581
3008
  init_llm();
2582
3009
  init_tools();
2583
-
2584
- // src/tools.jobs.ts
2585
- var SandboxJobRegistry = class {
2586
- constructor(maxBuffer = 256 * 1024) {
2587
- this.maxBuffer = maxBuffer;
2588
- }
2589
- maxBuffer;
2590
- jobs = /* @__PURE__ */ new Map();
2591
- seq = 0;
2592
- /** Start `fn` in the background and return a job id immediately (does not await completion). */
2593
- start(fn, opts = {}) {
2594
- const id = `job-${++this.seq}`;
2595
- const ctl = new AbortController();
2596
- const onChunk = (s) => {
2597
- job.buf = (job.buf + s).slice(-this.maxBuffer);
2598
- };
2599
- const job = { kind: opts.kind ?? "task", label: opts.label ?? "", status: "running", buf: "", ctl, promise: Promise.resolve() };
2600
- job.promise = fn({ signal: ctl.signal, onChunk }).then(
2601
- (res) => {
2602
- if (job.status === "running") {
2603
- job.status = "done";
2604
- if (res) onChunk(res);
2605
- }
2606
- },
2607
- (e) => {
2608
- if (job.status === "running") {
2609
- job.status = "error";
2610
- onChunk(`
2611
- [error] ${e?.message ?? e}`);
2612
- }
2613
- }
2614
- );
2615
- this.jobs.set(id, job);
2616
- return id;
2617
- }
2618
- /** Tail output for a job (null = no such job, '' = job exists but no output yet). */
2619
- output(id) {
2620
- return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? "" : null);
2621
- }
2622
- status(id) {
2623
- const j = this.jobs.get(id);
2624
- return j ? { status: j.status, bytes: j.buf.length } : null;
2625
- }
2626
- list() {
2627
- return [...this.jobs].map(([id, j]) => ({ id, kind: j.kind, label: j.label, status: j.status }));
2628
- }
2629
- /** Signal cancel (the producer decides what abort means — e.g. skip the overlay commit). */
2630
- kill(id) {
2631
- const j = this.jobs.get(id);
2632
- if (!j) return false;
2633
- if (j.status === "running") {
2634
- j.ctl.abort();
2635
- j.status = "killed";
2636
- }
2637
- return true;
2638
- }
2639
- killAll() {
2640
- for (const id of this.jobs.keys()) this.kill(id);
2641
- }
2642
- /** Await every still-running job. Called at turn end so a job's committed writes aren't lost from view
2643
- * just because the model didn't poll it. Returns the ids that were still running when drain began. */
2644
- async drain() {
2645
- const pending = [...this.jobs].filter(([, j]) => j.status === "running").map(([id]) => id);
2646
- await Promise.all([...this.jobs.values()].map((j) => j.promise));
2647
- return pending;
2648
- }
2649
- };
2650
- var NO_JOB = (id) => `Error: no background job '${id}'. Use JobStatus with no id to list jobs, or start one with bash({background:true}).`;
2651
- function makeJobTools(registry) {
2652
- return [
2653
- {
2654
- name: "JobOutput",
2655
- description: "Read the accumulated output (tail) of a background job by id (from bash({background:true})).",
2656
- parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
2657
- async run({ id }) {
2658
- const out = registry.output(String(id));
2659
- if (out == null) return NO_JOB(String(id));
2660
- const st = registry.status(String(id));
2661
- return `[${st.status}]
2662
- ${out.replace(/\n+$/, "") || "(no output yet)"}`;
2663
- }
2664
- },
2665
- {
2666
- name: "JobStatus",
2667
- description: "Status of a background job (running/done/killed/error). Omit `id` to list all jobs.",
2668
- parameters: { type: "object", properties: { id: { type: "string" } } },
2669
- async run({ id }) {
2670
- if (!id) {
2671
- const jobs = registry.list();
2672
- return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.kind}${j.label ? " " + j.label : ""}`).join("\n") : "(no background jobs)";
2673
- }
2674
- const st = registry.status(String(id));
2675
- return st ? `${st.status} \xB7 ${st.bytes} byte(s) buffered` : NO_JOB(String(id));
2676
- }
2677
- },
2678
- {
2679
- name: "JobKill",
2680
- description: "Stop a running background job by id (the job is cancelled; its overlay writes are NOT committed).",
2681
- parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
2682
- async run({ id }) {
2683
- return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB(String(id));
2684
- }
2685
- }
2686
- ];
2687
- }
3010
+ init_tools_jobs();
2688
3011
 
2689
3012
  // src/host.ts
2690
3013
  var askUserQuestionTool = {
@@ -3243,7 +3566,7 @@ init_OverlayFilesystem();
3243
3566
  init_worktree();
3244
3567
  init_NodeDiskFilesystem();
3245
3568
  init_logging();
3246
- var log5 = forComponent("subagent");
3569
+ var log6 = forComponent("subagent");
3247
3570
  async function boundedPool(items, limit, fn) {
3248
3571
  const out = new Array(items.length);
3249
3572
  let next = 0;
@@ -3317,7 +3640,7 @@ function traceFileFor(dir, label, depth) {
3317
3640
  const id = `${Date.now().toString(36)}-${(traceSeq++).toString(36)}`;
3318
3641
  return join4(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
3319
3642
  } catch (e) {
3320
- log5.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
3643
+ log6.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
3321
3644
  return void 0;
3322
3645
  }
3323
3646
  }
@@ -3325,7 +3648,7 @@ function flushTrace(file, meta, messages, status) {
3325
3648
  try {
3326
3649
  writeFileSync2(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
3327
3650
  } catch (e) {
3328
- log5.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
3651
+ log6.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
3329
3652
  }
3330
3653
  }
3331
3654
  function installTrace(child, file, meta) {
@@ -3366,7 +3689,7 @@ async function runChildTraced(args) {
3366
3689
  if (file) flushTrace(file, meta, child.transcript, wc.timedOut() ? "deadline" : res.finishReason);
3367
3690
  let result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
3368
3691
  if (wc.timedOut()) {
3369
- log5.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
3692
+ log6.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
3370
3693
  result += deadlineNote(deadlineMs);
3371
3694
  }
3372
3695
  await opts.hooks?.onSubagentStop?.(result, { label, agentType });
@@ -3820,7 +4143,7 @@ function reasoningToChatFragment(model, effort) {
3820
4143
  }
3821
4144
 
3822
4145
  // src/Agent.ts
3823
- var log6 = forComponent("Agent");
4146
+ var log7 = forComponent("Agent");
3824
4147
  function envInt(key, fallback) {
3825
4148
  const n = typeof process !== "undefined" ? Number(process.env?.[key]) : NaN;
3826
4149
  return Number.isFinite(n) && n >= 0 ? n : fallback;
@@ -3972,6 +4295,12 @@ var AgentOptions = class {
3972
4295
  /** Enable `bash({background:true})` + JobOutput/JobStatus/JobKill — sandbox background jobs (overlay-isolated,
3973
4296
  * committed on completion, drained at turn end). Useful when the VFS backend is slow (remote) or for sub-agents. */
3974
4297
  backgroundJobs = false;
4298
+ /** The job registry every long-lived tool call lives in (MCP calls past their soft timeout, delegated host/native
4299
+ * tools, and — when the host shares it with its `ShellJobRegistry` — shell jobs). Its liveness is what the stall
4300
+ * watchdog and the stuck-job reaper decide from. Default: a fresh registry per Agent. Knobs (idle windows, hard
4301
+ * caps, MCP soft timeout) live on `JobRegistryOptions`. Completion notices reach the model via `inject` unless
4302
+ * the host set its own `onExit`. */
4303
+ jobs;
3975
4304
  /** Plan mode: block mutating tools until the agent calls `ExitPlanMode` (host-approved). */
3976
4305
  planMode = false;
3977
4306
  /** Permission policy gating each tool call (allow / ask / deny). */
@@ -4038,16 +4367,14 @@ var Agent = class _Agent {
4038
4367
  /** Per-run ground truth: every tool name actually INVOKED this run (native dispatch + delegated
4039
4368
  * runtime activity). The ledger that makes a fabricated tool claim provable. Reset per run. */
4040
4369
  invokedTools = /* @__PURE__ */ new Set();
4041
- /** Delegated-runtime HOST tools currently executing (cursor toolExecutor). While > 0 the provider is
4042
- * not silent — it is waiting on US — so the idle-stall watchdog must not fire (see armStallWatchdog). */
4043
- hostToolsInFlight = 0;
4044
- /** The hold is bounded by the transport tool deadline: a host tool that never settles (the helper already gave
4045
- * up on it) must not disable stall protection for the rest of the request — or this instance. */
4046
- hostToolHoldUntil = 0;
4047
- /** Delegated-runtime NATIVE tools (cursor's own shell/mcp/task) seen `running` but not yet `completed`/`error`,
4048
- * id → hold-until. The runtime streams nothing while one runs, so it holds the watchdog like a host tool —
4049
- * bounded by the same tool deadline (`toolHoldMs`) so a lost `completed` event can't disable it. */
4050
- nativeToolsInFlight = /* @__PURE__ */ new Map();
4370
+ /** Every long-lived tool call is a job here (see AgentOptions.jobs). Delegated-runtime HOST tools (cursor
4371
+ * toolExecutor) and NATIVE tools (cursor's own shell/mcp/task, `running` → `completed`) are tracked as
4372
+ * `holds` jobs: while one runs the provider is not silent — it is waiting on a tool — so the idle-stall
4373
+ * watchdog must not fire. Each is bounded by the transport tool deadline (`hardCapMs = toolHoldMs`), so a
4374
+ * tool that never settles (or a lost `completed` event) is reaped and cannot disable stall protection. */
4375
+ jobs;
4376
+ /** Native delegated tool activity id → its job (per step attempt). */
4377
+ nativeJobs = /* @__PURE__ */ new Map();
4051
4378
  toolHoldMs = 0;
4052
4379
  /** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
4053
4380
  * from the pre-step transcript, which would re-run it — so such a stall is not retried. */
@@ -4096,6 +4423,7 @@ var Agent = class _Agent {
4096
4423
  addTools(tools) {
4097
4424
  this.injectedTools.push(...tools);
4098
4425
  this.activeTools.push(...tools);
4426
+ if (this.prepared && tools.some(isMcpTool) && !this.activeTools.some((t) => t.name === "JobWait")) this.activeTools.push(...makeJobTools(this.jobs));
4099
4427
  }
4100
4428
  /** Remove tools by name from a running agent. Returns the count removed. Also filters
4101
4429
  * `options.tools` — otherwise a constructor-passed tool resurrects on the next prepare()
@@ -4147,6 +4475,11 @@ var Agent = class _Agent {
4147
4475
  }
4148
4476
  constructor(options) {
4149
4477
  this.options = { ...new AgentOptions(), ...options };
4478
+ this.jobs = this.options.jobs ?? new JobRegistry();
4479
+ this.jobs.options.onExit ??= (n) => {
4480
+ const imgs = n.result?.images;
4481
+ this.inject(imgs?.length ? [{ type: "text", text: formatJobNotice(n) }, ...imgs.map((i) => imagePart(`data:${i.mimeType};base64,${i.data}`))] : formatJobNotice(n));
4482
+ };
4150
4483
  if (this.options.fs) this.buildCtx();
4151
4484
  }
4152
4485
  /** Build the tool context from the resolved fs + options. Idempotent-safe: called once (ctor or ensureFs). */
@@ -4158,7 +4491,8 @@ var Agent = class _Agent {
4158
4491
  this.ctx.ai = this.options.ai;
4159
4492
  this.ctx.model = this.options.model;
4160
4493
  this.ctx.parkHuman = (p) => this.park(p);
4161
- if (this.options.backgroundJobs) this.ctx.jobs = new SandboxJobRegistry();
4494
+ this.ctx.toolJobs = this.jobs;
4495
+ if (this.options.backgroundJobs) this.ctx.jobs = this.jobs;
4162
4496
  }
4163
4497
  /**
4164
4498
  * Resolve the filesystem + build the tool context if the ctor couldn't (no `fs` was passed). The disk
@@ -4174,7 +4508,7 @@ var Agent = class _Agent {
4174
4508
  const disk = new NodeDiskFilesystem2(process.cwd());
4175
4509
  await disk.init();
4176
4510
  this.options.fs = new JailedFilesystem2(disk);
4177
- log6.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
4511
+ log7.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
4178
4512
  }
4179
4513
  this.buildCtx();
4180
4514
  }
@@ -4244,7 +4578,7 @@ var Agent = class _Agent {
4244
4578
  tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
4245
4579
  }
4246
4580
  if (o.checkpoints) tools = [...tools, ...checkpointTools()];
4247
- if (this.ctx.jobs) tools = [...tools, ...makeJobTools(this.ctx.jobs)];
4581
+ if (this.ctx.jobs || [...tools, ...this.injectedTools].some(isMcpTool)) tools = [...tools, ...makeJobTools(this.jobs)];
4248
4582
  const plan = o.planMode ? planMode({ host: o.host }) : void 0;
4249
4583
  if (plan) tools = [...tools, plan.tool];
4250
4584
  this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
@@ -4360,8 +4694,8 @@ var Agent = class _Agent {
4360
4694
  let closureNudged = false;
4361
4695
  let lastProgressAt = start;
4362
4696
  const kill = (finishReason) => {
4363
- log6.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${finishReason === "timeout" ? `, idle=${Date.now() - lastProgressAt - this.parkedMs}ms` : ""}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
4364
- this.ctx.jobs?.killAll();
4697
+ log7.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${finishReason === "timeout" ? `, idle=${Date.now() - lastProgressAt - this.parkedMs}ms` : ""}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
4698
+ this.jobs.killAll(`run ended: ${finishReason}`, true);
4365
4699
  const claims = this.survivingClaims();
4366
4700
  return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
4367
4701
  };
@@ -4385,20 +4719,23 @@ var Agent = class _Agent {
4385
4719
  this.toolHoldMs = effectiveToolTimeoutMs;
4386
4720
  if (isCursorWithTools) {
4387
4721
  if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
4388
- log6.warn(`providerOptions.toolTimeoutMs=${hostToolTimeoutMs}ms is below what the registered tools need \u2014 RAISED to ${effectiveToolTimeoutMs}ms. ${toolTimeoutViolations(this.activeTools, hostToolTimeoutMs).length} tool(s) could outlive it, and the transport would have destroyed their results. To lower it, lower the tools' own maxDurationMs (e.g. the Shell tool's maxTimeoutMs) instead.`);
4722
+ log7.warn(`providerOptions.toolTimeoutMs=${hostToolTimeoutMs}ms is below what the registered tools need \u2014 RAISED to ${effectiveToolTimeoutMs}ms. ${toolTimeoutViolations(this.activeTools, hostToolTimeoutMs).length} tool(s) could outlive it, and the transport would have destroyed their results. To lower it, lower the tools' own maxDurationMs (e.g. the Shell tool's maxTimeoutMs) instead.`);
4389
4723
  }
4390
- for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log6.warn(v);
4724
+ for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log7.warn(v);
4391
4725
  }
4392
4726
  const cursorPo = isCursorWithTools ? {
4393
4727
  toolTimeoutMs: effectiveToolTimeoutMs,
4394
4728
  toolExecutor: async (name, args) => {
4395
4729
  const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
4396
- this.hostToolsInFlight++;
4397
- this.hostToolHoldUntil = Math.max(this.hostToolHoldUntil, Date.now() + effectiveToolTimeoutMs);
4730
+ const job = this.jobs.track({ kind: "host", label: name, holds: true, hardCapMs: effectiveToolTimeoutMs });
4398
4731
  try {
4399
- return await this.dispatch(tc);
4732
+ const r = await this.dispatch(tc);
4733
+ job.finish("done");
4734
+ return r;
4735
+ } catch (e) {
4736
+ job.finish("error", void 0, String(e));
4737
+ throw e;
4400
4738
  } finally {
4401
- this.hostToolsInFlight--;
4402
4739
  this.toolRanThisAttempt = true;
4403
4740
  this.stallPoke?.();
4404
4741
  }
@@ -4412,7 +4749,8 @@ var Agent = class _Agent {
4412
4749
  };
4413
4750
  try {
4414
4751
  for (let attempt = 0; ; attempt++) {
4415
- this.nativeToolsInFlight.clear();
4752
+ for (const [, h] of this.nativeJobs) if (h.status === "running") this.jobs.kill(h.id, "step attempt ended");
4753
+ this.nativeJobs.clear();
4416
4754
  this.toolRanThisAttempt = false;
4417
4755
  try {
4418
4756
  if (useStream) {
@@ -4444,11 +4782,11 @@ var Agent = class _Agent {
4444
4782
  const empty = err2?.code === "empty" || err2?.code === "cursor_silent";
4445
4783
  const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
4446
4784
  const replaysTool = stalled && this.toolRanThisAttempt;
4447
- if (replaysTool) log6.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
4785
+ if (replaysTool) log7.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
4448
4786
  const transient = !o.signal?.aborted && !isAbortError(err2) && !replaysTool && attempt < maxAttempts && (network || serverSide || stalled || empty);
4449
4787
  if (!transient) throw err2;
4450
4788
  const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
4451
- log6.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
4789
+ log7.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
4452
4790
  o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
4453
4791
  await new Promise((r) => setTimeout(r, waitMs));
4454
4792
  }
@@ -4456,12 +4794,12 @@ var Agent = class _Agent {
4456
4794
  } catch (err2) {
4457
4795
  if (err2?.code === "budget") return kill("budget");
4458
4796
  if (err2?.code === "stall") {
4459
- log6.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
4797
+ log7.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
4460
4798
  return { ...kill("timeout"), error: err2 };
4461
4799
  }
4462
4800
  if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
4463
4801
  if (err2?.code === "empty" || err2?.code === "cursor_silent") {
4464
- log6.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
4802
+ log7.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
4465
4803
  return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err2 };
4466
4804
  }
4467
4805
  const body = err2?.body ?? err2?.response?.data ?? err2?.error;
@@ -4473,7 +4811,7 @@ var Agent = class _Agent {
4473
4811
  }
4474
4812
  if (bodyStr && err2 instanceof Error && !err2.message.includes(bodyStr)) err2.detail = bodyStr;
4475
4813
  const errLog = err2 instanceof Error ? { message: err2.message, ...err2.detail ? { detail: err2.detail } : {}, stack: err2.stack } : err2;
4476
- log6.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
4814
+ log7.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
4477
4815
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err2 };
4478
4816
  }
4479
4817
  if (o.signal?.aborted) return kill("aborted");
@@ -4529,21 +4867,21 @@ var Agent = class _Agent {
4529
4867
  if (emptyTurn && o.failEmptyTurns) {
4530
4868
  if (!emptyNudged && canNudge()) {
4531
4869
  emptyNudged = true;
4532
- log6.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
4870
+ log7.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
4533
4871
  this.transcript.push({ role: "user", content: "Continue \u2014 you returned nothing. Answer the user now using the tool results above, or keep working if you are not done." });
4534
4872
  continue;
4535
4873
  }
4536
- log6.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
4874
+ log7.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
4537
4875
  const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
4538
4876
  e.code = "empty";
4539
4877
  return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
4540
4878
  }
4541
4879
  const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
4542
4880
  for (const n of fabricated) this.fabricatedClaims.add(n);
4543
- if (fabricated.length && !canNudge()) log6.warn(`fabricated tool use: text names ${fabricated.join(", ")} but no step/budget left to demand the call \u2014 finishing with the existing answer`);
4881
+ if (fabricated.length && !canNudge()) log7.warn(`fabricated tool use: text names ${fabricated.join(", ")} but no step/budget left to demand the call \u2014 finishing with the existing answer`);
4544
4882
  if (fabricated.length && canNudge()) {
4545
4883
  closureNudged = true;
4546
- log6.warn(`fabricated tool use: text names ${fabricated.join(", ")} but the run invoked ${this.invokedTools.size ? [...this.invokedTools].join(", ") : "nothing"} \u2014 demanding the real call`);
4884
+ log7.warn(`fabricated tool use: text names ${fabricated.join(", ")} but the run invoked ${this.invokedTools.size ? [...this.invokedTools].join(", ") : "nothing"} \u2014 demanding the real call`);
4547
4885
  this.transcript.push({
4548
4886
  role: "user",
4549
4887
  content: `You referred to ${fabricated.map((n) => `\`${n}\``).join(", ")} in your reply, but you never actually called ${fabricated.length > 1 ? "those tools" : "that tool"} \u2014 no tool call was made, so any result you reported for ${fabricated.length > 1 ? "them" : "it"} was invented. Either call ${fabricated.length > 1 ? "them" : "it"} for real now, or answer without ${fabricated.length > 1 ? "them" : "it"} and do NOT claim any outcome \u2014 including a failure or a timeout \u2014 for a call you did not make.`
@@ -4552,15 +4890,18 @@ var Agent = class _Agent {
4552
4890
  }
4553
4891
  if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
4554
4892
  closureNudged = true;
4555
- log6.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
4893
+ log7.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
4556
4894
  this.transcript.push({
4557
4895
  role: "user",
4558
4896
  content: "You ran tools but did not reply. Answer the user now using the tool results above; if a tool failed, say so plainly. Do NOT re-run those tools, do NOT narrate what you did, and do NOT ask for permission to continue."
4559
4897
  });
4560
4898
  continue;
4561
4899
  }
4562
- log6.verbose(`completed in ${steps} step(s)`);
4563
- await this.ctx.jobs?.drain();
4900
+ log7.verbose(`completed in ${steps} step(s)`);
4901
+ const drained = await this.jobs.drain(o.signal);
4902
+ if (o.signal?.aborted) return kill("aborted");
4903
+ if (drained.length) lastProgressAt = Date.now();
4904
+ if (this.drainInjections()) continue;
4564
4905
  this.releaseStaleImages();
4565
4906
  await this.activeHooks?.onStop?.(res.content ?? "");
4566
4907
  const claims = this.survivingClaims();
@@ -4586,11 +4927,12 @@ var Agent = class _Agent {
4586
4927
  }
4587
4928
  this.transcript.push({ role: "tool", tool_call_id: tc.id, name: tc.function.name, content });
4588
4929
  }
4930
+ lastProgressAt = Date.now();
4589
4931
  if (this.ctx.needsInput) {
4590
4932
  const question = this.ctx.needsInput;
4591
4933
  this.ctx.needsInput = void 0;
4592
- log6.warn(`needs_input: ${question.question} (steps=${steps})`);
4593
- await this.ctx.jobs?.drain();
4934
+ log7.warn(`needs_input: ${question.question} (steps=${steps})`);
4935
+ await this.jobs.drain();
4594
4936
  const text = [
4595
4937
  `OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
4596
4938
  ...question.options.map((o2) => ` \u2022 ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)
@@ -4629,6 +4971,8 @@ var Agent = class _Agent {
4629
4971
  if (timer) clearTimeout(timer);
4630
4972
  timer = setTimeout(() => {
4631
4973
  if (this.toolHoldActive()) return poke();
4974
+ const inFlight = this.jobs.list().filter((j) => j.status === "running");
4975
+ log7.warn(`stall watchdog: no stream output for ${stallMs}ms and no tool holding the stream \u2014 aborting the request` + (inFlight.length ? ` (${inFlight.length} detached job(s) running, not awaited by the stream: ${inFlight.map((j) => `${j.id}/${j.kind}`).join(", ")})` : ""));
4632
4976
  fired = true;
4633
4977
  ac.abort(stallError());
4634
4978
  }, stallMs);
@@ -4648,12 +4992,11 @@ var Agent = class _Agent {
4648
4992
  reclassify: (err2) => stalled() ? stallError() : err2
4649
4993
  };
4650
4994
  }
4651
- /** A host or native delegated tool is running within its deadline — the provider is quiet, not stalled. */
4995
+ /** A host or native delegated tool is running and not stuck — the provider is quiet, not stalled. The reap runs
4996
+ * FIRST, so a holder past its deadline is released (logged with evidence) instead of holding forever. */
4652
4997
  toolHoldActive() {
4653
- const now4 = Date.now();
4654
- if (this.hostToolsInFlight > 0 && now4 < this.hostToolHoldUntil) return true;
4655
- for (const until of this.nativeToolsInFlight.values()) if (now4 < until) return true;
4656
- return false;
4998
+ this.jobs.reap();
4999
+ return this.jobs.holding().length > 0;
4657
5000
  }
4658
5001
  /** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
4659
5002
  * non-result the model produced by going silent. Callers retry it rather than report "done". */
@@ -4693,10 +5036,16 @@ var Agent = class _Agent {
4693
5036
  if (a.output !== void 0) prev.output = a.output;
4694
5037
  prev.status = a.status;
4695
5038
  }
5039
+ const nj = a.id ? this.nativeJobs.get(a.id) : void 0;
4696
5040
  if (a.status === "running") {
4697
- if (a.id) this.nativeToolsInFlight.set(a.id, Date.now() + this.toolHoldMs);
5041
+ if (nj) nj.touch();
5042
+ else if (a.id) this.nativeJobs.set(a.id, this.jobs.track({ kind: "native", label: a.name, holds: true, hardCapMs: this.toolHoldMs }));
4698
5043
  } else {
4699
- if (a.id) this.nativeToolsInFlight.delete(a.id);
5044
+ if (nj) {
5045
+ if (typeof a.output === "string") nj.chunk(a.output.slice(-4e3));
5046
+ nj.finish(a.status === "error" ? "error" : "done");
5047
+ this.nativeJobs.delete(a.id);
5048
+ }
4700
5049
  this.toolRanThisAttempt = true;
4701
5050
  }
4702
5051
  sawTool = true;
@@ -4787,13 +5136,13 @@ var Agent = class _Agent {
4787
5136
  const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
4788
5137
  if (decision?.block) {
4789
5138
  const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
4790
- log6.debug(`${tc.function.name} -> ${blocked}`);
5139
+ log7.debug(`${tc.function.name} -> ${blocked}`);
4791
5140
  await hooks?.postToolUse?.(call, blocked, meta);
4792
5141
  return blocked;
4793
5142
  }
4794
5143
  this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
4795
5144
  if (earlyError) {
4796
- log6.debug(`${tc.function.name} -> ${earlyError}`);
5145
+ log7.debug(`${tc.function.name} -> ${earlyError}`);
4797
5146
  await hooks?.postToolUse?.(call, earlyError, meta);
4798
5147
  this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
4799
5148
  return earlyError;
@@ -4802,12 +5151,12 @@ var Agent = class _Agent {
4802
5151
  let images;
4803
5152
  let threw = false;
4804
5153
  try {
4805
- log6.debug(`${tc.function.name}(${tc.function.arguments})`);
5154
+ log7.debug(`${tc.function.name}(${tc.function.arguments})`);
4806
5155
  this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
4807
5156
  try {
4808
5157
  hooks.onToolOutput(call, chunk, meta);
4809
5158
  } catch (e) {
4810
- log6.debug(`onToolOutput hook error: ${e}`);
5159
+ log7.debug(`onToolOutput hook error: ${e}`);
4811
5160
  }
4812
5161
  } : void 0;
4813
5162
  const raw = await tool.run(args, this.ctx);
@@ -4819,7 +5168,7 @@ var Agent = class _Agent {
4819
5168
  }
4820
5169
  } catch (e) {
4821
5170
  const msg = e instanceof Error ? e.message : String(e);
4822
- log6.debug(`${tc.function.name} -> error: ${msg}`);
5171
+ log7.debug(`${tc.function.name} -> error: ${msg}`);
4823
5172
  result = `Error: ${msg}`;
4824
5173
  threw = true;
4825
5174
  } finally {
@@ -4911,6 +5260,7 @@ function estimateTokens(m, keepRecentImages = 1) {
4911
5260
  return Math.ceil(sendBytes(m, keepRecentImages) / 4);
4912
5261
  }
4913
5262
  var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
5263
+ var isMcpTool = (t) => t.name.startsWith("mcp__") || t.name === "McpCall";
4914
5264
  function transportHeadroomMs(t) {
4915
5265
  const d = Number(t.maxDurationMs) || 0;
4916
5266
  if (!d) return 0;
@@ -5086,7 +5436,7 @@ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
5086
5436
  const ids = callIdSet(messages.slice(from));
5087
5437
  while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
5088
5438
  if (total > maxTokens)
5089
- log6.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
5439
+ log7.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
5090
5440
  return [...head, ...messages.slice(from)];
5091
5441
  }
5092
5442
  function compact(m, max, focus) {
@@ -5391,6 +5741,7 @@ init_OverlayFilesystem();
5391
5741
 
5392
5742
  // src/index.ts
5393
5743
  init_tools();
5744
+ init_tools_jobs();
5394
5745
 
5395
5746
  // src/scheduler.ts
5396
5747
  function parseCronField(field, min, max) {
@@ -5663,7 +6014,7 @@ init_tools_web();
5663
6014
  init_tools();
5664
6015
  init_tools_structured();
5665
6016
  init_logging();
5666
- var log7 = forComponent("scratch");
6017
+ var log8 = forComponent("scratch");
5667
6018
  var SCRATCH_DIR = "/scratch";
5668
6019
  function shortArgs(args) {
5669
6020
  try {
@@ -5703,7 +6054,7 @@ var Scratch = class {
5703
6054
  await (this.dirReady ??= mkdirp(this.fs, dir));
5704
6055
  await this.fs.writeFile(path, header + raw);
5705
6056
  } catch (e) {
5706
- log7.debug("scratch write failed; returning raw", e);
6057
+ log8.debug("scratch write failed; returning raw", e);
5707
6058
  return raw;
5708
6059
  }
5709
6060
  const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
@@ -5733,7 +6084,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
5733
6084
  await (this.dirReady ??= mkdirp(this.fs, dir));
5734
6085
  await this.fs.writeFile(path, header + full);
5735
6086
  } catch (e) {
5736
- log7.debug("scratch spill failed; cropping lossy", e);
6087
+ log8.debug("scratch spill failed; cropping lossy", e);
5737
6088
  return full.slice(0, pageBytes) + `
5738
6089
 
5739
6090
  [output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
@@ -5779,7 +6130,7 @@ Question: ${q2}`);
5779
6130
  const answer = (res.text ?? "").trim();
5780
6131
  return answer || "(no answer found in scratch)";
5781
6132
  } catch (e) {
5782
- log7.debug("Ask peek failed", e);
6133
+ log8.debug("Ask peek failed", e);
5783
6134
  return `Error querying scratch: ${e?.message ?? e}`;
5784
6135
  }
5785
6136
  }
@@ -5788,7 +6139,7 @@ Question: ${q2}`);
5788
6139
 
5789
6140
  // src/lessons.ts
5790
6141
  init_logging();
5791
- var log8 = forComponent("Lessons");
6142
+ var log9 = forComponent("Lessons");
5792
6143
  var LessonOptionsDefaults = class {
5793
6144
  minRepeats = 2;
5794
6145
  };
@@ -5813,8 +6164,8 @@ function lessonCapture(options) {
5813
6164
  counts.set(lesson.slug, n);
5814
6165
  if (n < o.minRepeats) return;
5815
6166
  written.add(lesson.slug);
5816
- await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log8.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
5817
- log8.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
6167
+ await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log9.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
6168
+ log9.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
5818
6169
  }
5819
6170
  };
5820
6171
  }
@@ -5822,7 +6173,7 @@ function lessonCapture(options) {
5822
6173
  // src/reflect.ts
5823
6174
  init_llm();
5824
6175
  init_logging();
5825
- var log9 = forComponent("Reflect");
6176
+ var log10 = forComponent("Reflect");
5826
6177
  async function reflectOnRun(o) {
5827
6178
  const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
5828
6179
  if (!digest.trim()) return null;
@@ -5838,7 +6189,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
5838
6189
  const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
5839
6190
  text = r?.content ?? "";
5840
6191
  } catch (e) {
5841
- log9.warn(`reflection call failed: ${e?.message ?? e}`);
6192
+ log10.warn(`reflection call failed: ${e?.message ?? e}`);
5842
6193
  return null;
5843
6194
  }
5844
6195
  const m = text.match(/LESSON:\s*(.+)/i);
@@ -5848,10 +6199,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
5848
6199
  try {
5849
6200
  await writeFact(o.fs, o.dir, slug2, lesson);
5850
6201
  } catch (e) {
5851
- log9.warn(`could not persist lesson: ${e?.message ?? e}`);
6202
+ log10.warn(`could not persist lesson: ${e?.message ?? e}`);
5852
6203
  return null;
5853
6204
  }
5854
- log9.debug(`reflection persisted ${slug2}`);
6205
+ log10.debug(`reflection persisted ${slug2}`);
5855
6206
  return slug2;
5856
6207
  }
5857
6208
  function digestRun(messages, maxChars) {
@@ -5900,7 +6251,7 @@ function forComponent2(name) {
5900
6251
  }
5901
6252
 
5902
6253
  // node_modules/@bod.ee/voice/src/core/emotion.ts
5903
- var log10 = forComponent2("Emotion");
6254
+ var log11 = forComponent2("Emotion");
5904
6255
  var EMOTIONS = [
5905
6256
  // primary (best results)
5906
6257
  "neutral",
@@ -6032,7 +6383,7 @@ function renderEmotions(text, opts = { show: true }) {
6032
6383
  speech += cartesiaTag(tag);
6033
6384
  if (opts.show) display += m[0];
6034
6385
  } else {
6035
- log10.debug(`dropping unknown emotion tag ${m[0]}`);
6386
+ log11.debug(`dropping unknown emotion tag ${m[0]}`);
6036
6387
  }
6037
6388
  last = m.index + m[0].length;
6038
6389
  }
@@ -6074,7 +6425,7 @@ var EmotionStream = class {
6074
6425
  if (tag) {
6075
6426
  this.pending = tag;
6076
6427
  if (this.show) display += m[0];
6077
- } else log10.debug(`dropping unknown emotion tag ${m[0]}`);
6428
+ } else log11.debug(`dropping unknown emotion tag ${m[0]}`);
6078
6429
  last = m.index + m[0].length;
6079
6430
  }
6080
6431
  this.emit(body.slice(last), (s, d, p) => {
@@ -6097,7 +6448,7 @@ var EmotionStream = class {
6097
6448
  };
6098
6449
 
6099
6450
  // node_modules/@bod.ee/voice/src/core/engine.ts
6100
- var log11 = forComponent2("VoiceEngine");
6451
+ var log12 = forComponent2("VoiceEngine");
6101
6452
  var realClock = {
6102
6453
  now: () => performance.now(),
6103
6454
  setTimeout: (fn, ms) => setTimeout(fn, ms),
@@ -6372,7 +6723,7 @@ var VoiceEngine = class _VoiceEngine {
6372
6723
  await Promise.all([this.tts.connect(), this.stt.start()]);
6373
6724
  this.tts.warmup?.();
6374
6725
  this.setState("listening");
6375
- log11.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
6726
+ log12.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
6376
6727
  }
6377
6728
  get usingAec() {
6378
6729
  return this.stt.usingAec;
@@ -6394,7 +6745,7 @@ var VoiceEngine = class _VoiceEngine {
6394
6745
  this.options.onDiag({ t: this.clock.now(), kind, ...fields });
6395
6746
  } catch (e) {
6396
6747
  this.diagOn = false;
6397
- log11.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6748
+ log12.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6398
6749
  }
6399
6750
  }
6400
6751
  idleWaiters = [];
@@ -6503,7 +6854,7 @@ var VoiceEngine = class _VoiceEngine {
6503
6854
  if (prose.trim()) this.repliedSinceDispatch = true;
6504
6855
  for (const w of this.words(this.reply)) this.echoWords.add(w);
6505
6856
  this.tts.speak(forSpeech(speech), true);
6506
- if (!this.spokeDeltas && this.turnStartAt) log11.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
6857
+ if (!this.spokeDeltas && this.turnStartAt) log12.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
6507
6858
  this.spokeDeltas = true;
6508
6859
  this.setState("speaking");
6509
6860
  if (this.options.revealMode !== "off" && display) {
@@ -6580,7 +6931,7 @@ var VoiceEngine = class _VoiceEngine {
6580
6931
  this.drainTimer = null;
6581
6932
  this.speaking = false;
6582
6933
  this.stopWordReveal(true);
6583
- if (this.turnStartAt) log11.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
6934
+ if (this.turnStartAt) log12.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
6584
6935
  this.echoUntil = this.clock.now() + 2500;
6585
6936
  if (!this.usingAec) this.stt.reset();
6586
6937
  this.setState("listening");
@@ -6668,7 +7019,7 @@ var VoiceEngine = class _VoiceEngine {
6668
7019
  this.clearAckTimer("interrupt");
6669
7020
  this.adaptiveAckFired = false;
6670
7021
  this.ackProbe = null;
6671
- if (this.uttQueue.length) log11.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
7022
+ if (this.uttQueue.length) log12.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
6672
7023
  const droppedQueued = this.uttQueue.length;
6673
7024
  this.uttQueue = [];
6674
7025
  if (!this.speaking && !this.drainTimer) return;
@@ -6814,7 +7165,7 @@ var VoiceEngine = class _VoiceEngine {
6814
7165
  if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
6815
7166
  this.specSpent = true;
6816
7167
  this.specText = this.specPartial;
6817
- log11.debug(`speculate: "${this.specText.slice(0, 60)}"`);
7168
+ log12.debug(`speculate: "${this.specText.slice(0, 60)}"`);
6818
7169
  this.diag("speculate", { text: this.specText });
6819
7170
  this.options.onSpeculate(this.specText);
6820
7171
  }, this.options.speculativeMs);
@@ -6883,7 +7234,7 @@ var VoiceEngine = class _VoiceEngine {
6883
7234
  };
6884
7235
  this.tts.newContext();
6885
7236
  this.tts.speak(phrase, false);
6886
- log11.debug(`backchannel: "${phrase}"`);
7237
+ log12.debug(`backchannel: "${phrase}"`);
6887
7238
  this.diag("backchannel", { phrase });
6888
7239
  this.options.onBackchannel(phrase);
6889
7240
  }
@@ -6916,7 +7267,7 @@ var VoiceEngine = class _VoiceEngine {
6916
7267
  });
6917
7268
  if (idx < 0) return text;
6918
7269
  toks.splice(idx, 1);
6919
- log11.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
7270
+ log12.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
6920
7271
  return toks.join(" ");
6921
7272
  }
6922
7273
  /** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
@@ -7007,7 +7358,7 @@ var VoiceEngine = class _VoiceEngine {
7007
7358
  this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
7008
7359
  if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
7009
7360
  if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
7010
- log11.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
7361
+ log12.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
7011
7362
  this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
7012
7363
  this.options.onHold();
7013
7364
  if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
@@ -7042,13 +7393,13 @@ var VoiceEngine = class _VoiceEngine {
7042
7393
  if (text) {
7043
7394
  const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
7044
7395
  if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
7045
- log11.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
7396
+ log12.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
7046
7397
  this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
7047
7398
  return;
7048
7399
  }
7049
7400
  const refinal = this.refinalizes(text);
7050
7401
  if (refinal && this.repliedSinceDispatch) {
7051
- log11.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
7402
+ log12.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
7052
7403
  this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
7053
7404
  this.lastDispatchFlat = flat;
7054
7405
  this.lastDispatchWords = text.trim().split(/\s+/);
@@ -7208,7 +7559,7 @@ var SpokenSplitter = class {
7208
7559
  };
7209
7560
 
7210
7561
  // node_modules/@bod.ee/voice/src/adapters/soniox.ts
7211
- var log12 = forComponent2("SonioxSTT");
7562
+ var log13 = forComponent2("SonioxSTT");
7212
7563
  var now = () => performance.now();
7213
7564
  var SonioxSTTOptions = class {
7214
7565
  auth = "";
@@ -7249,7 +7600,7 @@ var SonioxSTT = class {
7249
7600
  this.onDiag({ t: now(), kind, ...fields });
7250
7601
  } catch (e) {
7251
7602
  this.diagOn = false;
7252
- log12.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7603
+ log13.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7253
7604
  }
7254
7605
  }
7255
7606
  lastChunkAt = 0;
@@ -7291,11 +7642,11 @@ var SonioxSTT = class {
7291
7642
  this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
7292
7643
  this.ws.onclose = (ev) => {
7293
7644
  if (this.stopped) return;
7294
- log12.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
7645
+ log13.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
7295
7646
  this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
7296
7647
  this.reset();
7297
7648
  this.connectWs().catch((e) => {
7298
- log12.error(`soniox reconnect failed: ${e.message}`);
7649
+ log13.error(`soniox reconnect failed: ${e.message}`);
7299
7650
  this.diag("stt_reconnect_failed", { message: e.message });
7300
7651
  });
7301
7652
  };
@@ -7307,7 +7658,7 @@ var SonioxSTT = class {
7307
7658
  this.endpointTimer = setInterval(() => {
7308
7659
  const combined = (this.finalText + this.partialText).trim();
7309
7660
  if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
7310
- if (this.firstTokenAt) log12.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
7661
+ if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
7311
7662
  this.reset();
7312
7663
  this.onUtterance(combined, now());
7313
7664
  }, 120);
@@ -7319,7 +7670,7 @@ var SonioxSTT = class {
7319
7670
  if (this.stopped) return;
7320
7671
  const ref = this.lastChunkAt || this.startedChunksAt;
7321
7672
  if (now() - ref > noAudioMs) {
7322
- log12.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
7673
+ log13.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
7323
7674
  this.diag("stt_watchdog_fatal", { noAudioMs });
7324
7675
  this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
7325
7676
  this.stop();
@@ -7342,7 +7693,7 @@ var SonioxSTT = class {
7342
7693
  handle(m) {
7343
7694
  if (m.error_message) {
7344
7695
  this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
7345
- return log12.error(`soniox: ${m.error_message}`);
7696
+ return log13.error(`soniox: ${m.error_message}`);
7346
7697
  }
7347
7698
  let endpoint = false;
7348
7699
  for (const t of m.tokens ?? []) {
@@ -7359,7 +7710,7 @@ var SonioxSTT = class {
7359
7710
  this.onPartial(combined);
7360
7711
  if (endpoint && this.finalText.trim()) {
7361
7712
  const utterance = this.finalText.trim();
7362
- if (this.firstTokenAt) log12.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
7713
+ if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
7363
7714
  this.reset();
7364
7715
  this.onUtterance(utterance, now());
7365
7716
  }
@@ -7381,7 +7732,7 @@ var SonioxSTT = class {
7381
7732
  };
7382
7733
 
7383
7734
  // node_modules/@bod.ee/voice/src/adapters/cartesia.ts
7384
- var log13 = forComponent2("CartesiaTTS");
7735
+ var log14 = forComponent2("CartesiaTTS");
7385
7736
  var now2 = () => performance.now();
7386
7737
  var CartesiaTTSOptions = class {
7387
7738
  auth = "";
@@ -7417,7 +7768,7 @@ var CartesiaTTS = class _CartesiaTTS {
7417
7768
  this.onDiag({ t: now2(), kind, ...fields });
7418
7769
  } catch (e) {
7419
7770
  this.diagOn = false;
7420
- log13.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7771
+ log14.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7421
7772
  }
7422
7773
  }
7423
7774
  firstAudioAt = 0;
@@ -7452,11 +7803,11 @@ var CartesiaTTS = class _CartesiaTTS {
7452
7803
  this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
7453
7804
  });
7454
7805
  this.ws.onclose = (ev) => {
7455
- log13.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
7806
+ log14.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
7456
7807
  this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
7457
7808
  if (!this.closed) {
7458
7809
  this.connecting = this.doConnect().catch((e) => {
7459
- log13.error(`cartesia reconnect failed: ${e.message}`);
7810
+ log14.error(`cartesia reconnect failed: ${e.message}`);
7460
7811
  this.diag("tts_reconnect_failed", { message: e.message });
7461
7812
  });
7462
7813
  }
@@ -7484,12 +7835,12 @@ var CartesiaTTS = class _CartesiaTTS {
7484
7835
  this.down = true;
7485
7836
  this.downAt = now2();
7486
7837
  this.consecutiveOk = 0;
7487
- log13.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
7838
+ log14.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
7488
7839
  this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
7489
7840
  this.onDone();
7490
7841
  this.startProbe();
7491
7842
  } else if (!this.down) {
7492
- (/No valid transcripts/i.test(m.message || "") ? log13.debug : log13.warn)(`cartesia: ${JSON.stringify(m)}`);
7843
+ (/No valid transcripts/i.test(m.message || "") ? log14.debug : log14.warn)(`cartesia: ${JSON.stringify(m)}`);
7493
7844
  }
7494
7845
  }
7495
7846
  };
@@ -7504,7 +7855,7 @@ var CartesiaTTS = class _CartesiaTTS {
7504
7855
  this.stopProbe();
7505
7856
  const downMs = this.downAt ? now2() - this.downAt : 0;
7506
7857
  this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
7507
- (downMs < 2e3 ? log13.debug : log13.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
7858
+ (downMs < 2e3 ? log14.debug : log14.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
7508
7859
  }
7509
7860
  /** Ensure the WS is open before sending — reconnects if idle-closed. */
7510
7861
  async ensureConnected() {
@@ -7595,7 +7946,7 @@ configureLogging((name) => {
7595
7946
  });
7596
7947
 
7597
7948
  // src/duplex.ts
7598
- var log14 = forComponent("DuplexAgent");
7949
+ var log15 = forComponent("DuplexAgent");
7599
7950
  function describeCall(call) {
7600
7951
  const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
7601
7952
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
@@ -7806,7 +8157,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7806
8157
  const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
7807
8158
  if (m) {
7808
8159
  this.fabricationCut = true;
7809
- log14.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
8160
+ log15.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
7810
8161
  const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
7811
8162
  if (!safe) return;
7812
8163
  if (safe.trim()) this.spokeThisTurn = true;
@@ -7901,7 +8252,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7901
8252
  return {
7902
8253
  preToolUse: (call) => {
7903
8254
  if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
7904
- log14.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
8255
+ log15.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
7905
8256
  this.abortSpeculation();
7906
8257
  return { block: true, reason: "Speculative turn aborted." };
7907
8258
  }
@@ -7939,7 +8290,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7939
8290
  try {
7940
8291
  await this.voice.send(fallback ? "[reminder] You said nothing to the user this turn. Tell them, in ONE short spoken sentence, what just happened \u2014 no tools." : dispatched ? "[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools." : "[reminder] You said nothing to the user this turn. Give your ONE short spoken reply now \u2014 no tools.");
7941
8292
  } catch (e) {
7942
- log14.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
8293
+ log15.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
7943
8294
  } finally {
7944
8295
  this.nudging = false;
7945
8296
  }
@@ -8032,7 +8383,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
8032
8383
  try {
8033
8384
  res = await this.voice.send(spec.text);
8034
8385
  } catch (e) {
8035
- log14.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
8386
+ log15.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
8036
8387
  } finally {
8037
8388
  this.voice.options.signal = prevSignal;
8038
8389
  }
@@ -8046,7 +8397,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
8046
8397
  if (d === "abort") {
8047
8398
  if (this.voice.transcript.length > base) this.voice.transcript.length = base;
8048
8399
  this.speculativeAbortedCalls++;
8049
- log14.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
8400
+ log15.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
8050
8401
  if (this.spec === spec) this.spec = void 0;
8051
8402
  return res ?? empty;
8052
8403
  }
@@ -8311,7 +8662,7 @@ Another agent just implemented the above. Independently check the CURRENT state
8311
8662
  this.notify("task_verify", `task ${id}: verifying`, { id });
8312
8663
  const cres = await new Agent(checkerOpts).run(checkBrief);
8313
8664
  if (cres.finishReason !== "stop") {
8314
- log14.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
8665
+ log15.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
8315
8666
  this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
8316
8667
  }
8317
8668
  const sum = (a = 0, b = 0) => a + b;
@@ -8447,7 +8798,7 @@ Another agent just implemented the above. Independently check the CURRENT state
8447
8798
  rec.status = "done";
8448
8799
  rec.result = res.text;
8449
8800
  const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
8450
- log14.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
8801
+ log15.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
8451
8802
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
8452
8803
  id,
8453
8804
  text: res.text,
@@ -8489,14 +8840,14 @@ Another agent just implemented the above. Independently check the CURRENT state
8489
8840
  foldIfSuperseded(rec, note) {
8490
8841
  if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
8491
8842
  if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
8492
- log14.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
8843
+ log15.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
8493
8844
  return true;
8494
8845
  }
8495
8846
  failTask(rec, msg) {
8496
8847
  this.dropAsk(rec.id);
8497
8848
  rec.status = "error";
8498
8849
  rec.result = msg;
8499
- log14.warn(`task ${rec.id} failed: ${msg}`);
8850
+ log15.warn(`task ${rec.id} failed: ${msg}`);
8500
8851
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
8501
8852
  if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
8502
8853
  this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
@@ -8745,17 +9096,42 @@ async function withDeadline(p, ms) {
8745
9096
  }
8746
9097
  }
8747
9098
  var mcpTimedOut = (name, ms = MCP_CALL_TIMEOUT_MS) => `[${name}] no response after ${ms / 1e3}s \u2014 the MCP server never answered. The call's outcome is UNKNOWN: do not assume it ran or that it failed. Verify the state (or pick a bounded/narrower call) before retrying.`;
9099
+ async function runMcpCall(name, call, ctx, timeoutMs) {
9100
+ const jobs = ctx?.toolJobs;
9101
+ const shape = (raw) => {
9102
+ const r2 = toResult(raw);
9103
+ return r2.images?.length ? r2 : r2.text;
9104
+ };
9105
+ if (!jobs) {
9106
+ const raw = await withDeadline(call({}), timeoutMs);
9107
+ return raw === TIMED_OUT ? mcpTimedOut(name, timeoutMs) : shape(raw);
9108
+ }
9109
+ const r = await jobs.runForeground(async ({ signal, touch, onChunk }) => {
9110
+ const raw = await call({
9111
+ signal,
9112
+ onProgress: (p) => {
9113
+ touch();
9114
+ if (p?.message) onChunk(`[progress${p.progress != null ? ` ${p.progress}${p.total != null ? `/${p.total}` : ""}` : ""}] ${p.message}
9115
+ `);
9116
+ }
9117
+ });
9118
+ const res = toResult(raw);
9119
+ onChunk(res.text + (res.images?.length ? `
9120
+ [${res.images.length} image(s) in the result]` : ""));
9121
+ return res;
9122
+ }, { kind: "mcp", label: name, idleAfterActivity: true });
9123
+ if (r.done) return r.value.images?.length ? r.value : r.value.text;
9124
+ const soft = jobs.options.foregroundMs.mcp ?? 0;
9125
+ return `[still running] MCP call ${name} is still working after ${soft >= 1e3 ? `${Math.round(soft / 1e3)}s` : `${soft}ms`}, so it was handed to background job ${r.id} \u2014 NOT cancelled, the server keeps going. Its result will be delivered to you when it finishes, so continue with other work; to block on it use JobWait({id:"${r.id}"}) instead of sleeping. JobStatus/JobOutput show its progress; JobKill({id:"${r.id}"}) cancels it. Do NOT re-issue the same call \u2014 that would run it twice.`;
9126
+ }
8748
9127
  function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
8749
9128
  return {
8750
9129
  name: `${prefix}${spec.name}`,
8751
9130
  description: spec.description ?? `MCP tool ${spec.name}`,
8752
9131
  parameters: spec.inputSchema ?? { type: "object", properties: {} },
8753
9132
  maxDurationMs: timeoutMs,
8754
- async run(args, _ctx) {
8755
- const raw = await withDeadline(callTool(spec.name, args ?? {}), timeoutMs);
8756
- if (raw === TIMED_OUT) return mcpTimedOut(spec.name, timeoutMs);
8757
- const r = toResult(raw);
8758
- return r.images?.length ? r : r.text;
9133
+ run(args, ctx) {
9134
+ return runMcpCall(spec.name, (o) => callTool(spec.name, args ?? {}, o), ctx, timeoutMs);
8759
9135
  }
8760
9136
  };
8761
9137
  }
@@ -8796,13 +9172,10 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
8796
9172
  }
8797
9173
  },
8798
9174
  maxDurationMs: callTimeoutMs,
8799
- async run({ name, args }) {
9175
+ async run({ name, args }, ctx) {
8800
9176
  const n = String(name ?? "");
8801
9177
  if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
8802
- const raw = await withDeadline(callTool(n, args ?? {}), callTimeoutMs);
8803
- if (raw === TIMED_OUT) return mcpTimedOut(n, callTimeoutMs);
8804
- const r = toResult(raw);
8805
- return r.images?.length ? r : r.text;
9178
+ return runMcpCall(n, (o) => callTool(n, args ?? {}, o), ctx, callTimeoutMs);
8806
9179
  }
8807
9180
  };
8808
9181
  return [searchTool, callMcpTool];
@@ -8822,17 +9195,17 @@ function buildMcpCatalog(servers) {
8822
9195
  return { specs, routes };
8823
9196
  }
8824
9197
  function searchOverCatalog(servers, specs, routes, resolve5, options) {
8825
- const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
9198
+ const tools = specs.length ? makeMcpToolSearch(specs, (name, args, o) => {
8826
9199
  const r = routes.get(name);
8827
9200
  if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
8828
- return resolve5(r.server, r.rawName, args ?? {});
9201
+ return resolve5(r.server, r.rawName, args ?? {}, o);
8829
9202
  }, options) : [];
8830
9203
  return { tools, serverNames: servers, toolCount: specs.length };
8831
9204
  }
8832
9205
  function makeMcpToolSearchFromMounted(mounted, options) {
8833
9206
  const { specs, routes } = buildMcpCatalog(mounted);
8834
9207
  const byName = new Map(mounted.map((m) => [m.name, m]));
8835
- return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args) => byName.get(server).client.callTool(rawName, args), options);
9208
+ return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args, o) => byName.get(server).client.callTool(rawName, args, o), options);
8836
9209
  }
8837
9210
 
8838
9211
  // src/index.ts
@@ -8845,9 +9218,20 @@ import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor a
8845
9218
  init_logging();
8846
9219
  import { spawn } from "child_process";
8847
9220
  import { createHash } from "crypto";
8848
- var log15 = forComponent("mcp");
9221
+ var log16 = forComponent("mcp");
8849
9222
  var PROTOCOL_VERSION = "2025-06-18";
8850
9223
  var DEFAULT_TIMEOUT_MS = 3e4;
9224
+ var DEFAULT_CALL_TIMEOUT_MS = 30 * 6e4;
9225
+ function deadlineFor(method, spec, opts) {
9226
+ if (opts?.timeoutMs != null) return opts.timeoutMs;
9227
+ if (method === "tools/call") return spec.callTimeoutMs ?? Math.max(DEFAULT_CALL_TIMEOUT_MS, spec.timeoutMs ?? 0);
9228
+ return spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
9229
+ }
9230
+ function withProgressToken(params, token) {
9231
+ const p = params && typeof params === "object" ? params : {};
9232
+ return { ...p, _meta: { ...p._meta ?? {}, progressToken: token } };
9233
+ }
9234
+ var abortError = (signal) => signal.reason instanceof Error ? signal.reason : Object.assign(new Error("MCP request cancelled"), { name: "AbortError" });
8851
9235
  var StdioTransport = class {
8852
9236
  constructor(spec) {
8853
9237
  this.spec = spec;
@@ -8864,7 +9248,7 @@ var StdioTransport = class {
8864
9248
  proc.stdout.setEncoding("utf8");
8865
9249
  proc.stdout.on("data", (chunk) => this.onData(chunk));
8866
9250
  proc.stderr.setEncoding("utf8");
8867
- proc.stderr.on("data", (chunk) => log15.debug(`[${command}] stderr:`, chunk.trimEnd()));
9251
+ proc.stderr.on("data", (chunk) => log16.debug(`[${command}] stderr:`, chunk.trimEnd()));
8868
9252
  proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
8869
9253
  proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
8870
9254
  }
@@ -8878,21 +9262,32 @@ var StdioTransport = class {
8878
9262
  try {
8879
9263
  this.dispatch(JSON.parse(line));
8880
9264
  } catch (e) {
8881
- log15.debug("dropping non-JSON line from MCP server:", line, e);
9265
+ log16.debug("dropping non-JSON line from MCP server:", line, e);
8882
9266
  }
8883
9267
  }
8884
9268
  }
8885
9269
  dispatch(msg) {
9270
+ if (msg?.method === "notifications/progress") {
9271
+ const p2 = this.pending.get(msg.params?.progressToken);
9272
+ try {
9273
+ p2?.onProgress?.(msg.params ?? {});
9274
+ } catch (e) {
9275
+ log16.debug("onProgress handler threw", e);
9276
+ }
9277
+ return;
9278
+ }
8886
9279
  if (msg?.id == null || !this.pending.has(msg.id)) return;
8887
9280
  const p = this.pending.get(msg.id);
8888
9281
  this.pending.delete(msg.id);
8889
9282
  clearTimeout(p.timer);
9283
+ p.done?.();
8890
9284
  if (msg.error) p.reject(new Error(msg.error?.message ?? JSON.stringify(msg.error)));
8891
9285
  else p.resolve(msg.result);
8892
9286
  }
8893
9287
  failAll(e) {
8894
9288
  for (const p of this.pending.values()) {
8895
9289
  clearTimeout(p.timer);
9290
+ p.done?.();
8896
9291
  p.reject(e);
8897
9292
  }
8898
9293
  this.pending.clear();
@@ -8901,19 +9296,31 @@ var StdioTransport = class {
8901
9296
  if (!this.proc?.stdin) throw new Error("MCP stdio transport not started");
8902
9297
  this.proc.stdin.write(JSON.stringify(msg) + "\n");
8903
9298
  }
8904
- request(method, params) {
9299
+ request(method, params, opts = {}) {
8905
9300
  const id = this.nextId++;
8906
- const timeoutMs = this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
9301
+ const timeoutMs = deadlineFor(method, this.spec, opts);
9302
+ const { signal, onProgress } = opts;
8907
9303
  return new Promise((resolve5, reject) => {
8908
- const timer = setTimeout(() => {
9304
+ if (signal?.aborted) return reject(abortError(signal));
9305
+ const timer = timeoutMs > 0 ? setTimeout(() => {
8909
9306
  this.pending.delete(id);
9307
+ done();
8910
9308
  reject(new Error(`MCP request "${method}" timed out after ${timeoutMs}ms`));
8911
- }, timeoutMs);
8912
- this.pending.set(id, { resolve: resolve5, reject, timer });
9309
+ }, timeoutMs) : void 0;
9310
+ const onAbort = () => {
9311
+ if (!this.pending.delete(id)) return;
9312
+ clearTimeout(timer);
9313
+ this.notify("notifications/cancelled", { requestId: id, reason: String(signal?.reason?.message ?? "cancelled") }).catch((e) => log16.debug("cancel notify failed", e));
9314
+ reject(abortError(signal));
9315
+ };
9316
+ const done = () => signal?.removeEventListener("abort", onAbort);
9317
+ signal?.addEventListener("abort", onAbort, { once: true });
9318
+ this.pending.set(id, { resolve: resolve5, reject, timer, onProgress, done });
8913
9319
  try {
8914
- this.write({ jsonrpc: "2.0", id, method, params });
9320
+ this.write({ jsonrpc: "2.0", id, method, params: onProgress ? withProgressToken(params, id) : params });
8915
9321
  } catch (e) {
8916
9322
  clearTimeout(timer);
9323
+ done();
8917
9324
  this.pending.delete(id);
8918
9325
  reject(e);
8919
9326
  }
@@ -8927,7 +9334,7 @@ var StdioTransport = class {
8927
9334
  try {
8928
9335
  this.proc?.stdin?.end();
8929
9336
  } catch (e) {
8930
- log15.debug("stdin end failed", e);
9337
+ log16.debug("stdin end failed", e);
8931
9338
  }
8932
9339
  this.proc?.kill();
8933
9340
  }
@@ -8954,13 +9361,24 @@ var HttpTransport = class {
8954
9361
  fetchImpl;
8955
9362
  async start() {
8956
9363
  }
8957
- request(method, params) {
8958
- return this.post({ jsonrpc: "2.0", id: this.nextId++, method, params }, false);
9364
+ async request(method, params, opts = {}) {
9365
+ const id = this.nextId++;
9366
+ const deadline = AbortSignal.timeout(deadlineFor(method, this.spec, opts));
9367
+ const signal = opts.signal ? AbortSignal.any([opts.signal, deadline]) : deadline;
9368
+ try {
9369
+ return await this.post({ jsonrpc: "2.0", id, method, params: opts.onProgress ? withProgressToken(params, id) : params }, false, signal, opts.onProgress);
9370
+ } catch (e) {
9371
+ if (opts.signal?.aborted) {
9372
+ void this.notify("notifications/cancelled", { requestId: id, reason: String(opts.signal.reason?.message ?? "cancelled") }).catch((err2) => log16.debug("cancel notify failed", err2));
9373
+ throw abortError(opts.signal);
9374
+ }
9375
+ throw e;
9376
+ }
8959
9377
  }
8960
9378
  async notify(method, params) {
8961
- await this.post({ jsonrpc: "2.0", method, params }, true);
9379
+ await this.post({ jsonrpc: "2.0", method, params }, true, AbortSignal.timeout(this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS));
8962
9380
  }
8963
- async post(msg, isNotify) {
9381
+ async post(msg, isNotify, signal, onProgress) {
8964
9382
  const headers = {
8965
9383
  "content-type": "application/json",
8966
9384
  accept: "application/json, text/event-stream",
@@ -8973,7 +9391,7 @@ var HttpTransport = class {
8973
9391
  method: "POST",
8974
9392
  headers,
8975
9393
  body: JSON.stringify(msg),
8976
- signal: AbortSignal.timeout(this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS)
9394
+ signal
8977
9395
  });
8978
9396
  const sid = resp.headers.get("mcp-session-id");
8979
9397
  if (sid) this.sessionId = sid;
@@ -8981,25 +9399,63 @@ var HttpTransport = class {
8981
9399
  if (!resp.ok) throw new Error(`MCP HTTP ${resp.status} ${resp.statusText}`);
8982
9400
  if (isNotify) return;
8983
9401
  const ct = resp.headers.get("content-type") ?? "";
8984
- const data = ct.includes("text/event-stream") ? parseSseResponse(await resp.text()) : await resp.json();
9402
+ const data = ct.includes("text/event-stream") ? await readSseResponse(resp, msg.id, onProgress) : await resp.json();
8985
9403
  if (data?.error) throw new Error(data.error?.message ?? JSON.stringify(data.error));
8986
9404
  return data?.result;
8987
9405
  }
8988
9406
  async close() {
8989
9407
  }
8990
9408
  };
8991
- function parseSseResponse(body) {
8992
- for (const line of body.split("\n")) {
8993
- const trimmed = line.trimStart();
8994
- if (!trimmed.startsWith("data:")) continue;
8995
- try {
8996
- const obj = JSON.parse(trimmed.slice(5).trim());
8997
- if (obj && (obj.result !== void 0 || obj.error !== void 0)) return obj;
8998
- } catch (e) {
8999
- log15.debug("skipping unparseable SSE data line", e);
9409
+ function sseMessage(line) {
9410
+ const trimmed = line.trimStart();
9411
+ if (!trimmed.startsWith("data:")) return void 0;
9412
+ try {
9413
+ return JSON.parse(trimmed.slice(5).trim());
9414
+ } catch (e) {
9415
+ log16.debug("skipping unparseable SSE data line", e);
9416
+ return void 0;
9417
+ }
9418
+ }
9419
+ async function readSseResponse(resp, id, onProgress) {
9420
+ const handle = (obj) => {
9421
+ if (obj?.method === "notifications/progress") {
9422
+ try {
9423
+ onProgress?.(obj.params ?? {});
9424
+ } catch (e) {
9425
+ log16.debug("onProgress handler threw", e);
9426
+ }
9427
+ return void 0;
9000
9428
  }
9429
+ if (obj && (obj.result !== void 0 || obj.error !== void 0) && (obj.id == null || id == null || obj.id === id)) return obj;
9430
+ return void 0;
9431
+ };
9432
+ const reader = resp.body?.getReader?.();
9433
+ if (!reader) {
9434
+ for (const l of (await resp.text()).split("\n")) {
9435
+ const r = handle(sseMessage(l));
9436
+ if (r) return r;
9437
+ }
9438
+ return {};
9439
+ }
9440
+ const dec = new TextDecoder();
9441
+ let buf = "";
9442
+ try {
9443
+ for (; ; ) {
9444
+ const { value, done } = await reader.read();
9445
+ buf += done ? dec.decode() : dec.decode(value, { stream: true });
9446
+ let nl;
9447
+ while ((nl = buf.indexOf("\n")) >= 0 || done && buf) {
9448
+ const line = nl >= 0 ? buf.slice(0, nl) : buf;
9449
+ buf = nl >= 0 ? buf.slice(nl + 1) : "";
9450
+ const r = handle(sseMessage(line));
9451
+ if (r) return r;
9452
+ }
9453
+ if (done) return {};
9454
+ }
9455
+ } finally {
9456
+ reader.cancel().catch(() => {
9457
+ });
9001
9458
  }
9002
- return {};
9003
9459
  }
9004
9460
  var McpClient = class {
9005
9461
  constructor(transport, clientInfo = { name: "agentx", version: "0" }) {
@@ -9023,8 +9479,9 @@ var McpClient = class {
9023
9479
  const r = await this.transport.request("tools/list");
9024
9480
  return Array.isArray(r?.tools) ? r.tools : [];
9025
9481
  }
9026
- async callTool(name, args) {
9027
- return this.transport.request("tools/call", { name, arguments: args ?? {} });
9482
+ /** `tools/call`. Pass `onProgress` to request progress notifications (liveness) and `signal` to cancel. */
9483
+ async callTool(name, args, opts) {
9484
+ return this.transport.request("tools/call", { name, arguments: args ?? {} }, opts);
9028
9485
  }
9029
9486
  async listResources() {
9030
9487
  const r = await this.transport.request("resources/list");
@@ -9038,7 +9495,7 @@ var McpClient = class {
9038
9495
  }
9039
9496
  };
9040
9497
  function buildTransport(cfg) {
9041
- return cfg.url ? new HttpTransport({ url: cfg.url, headers: cfg.headers, bearerToken: cfg.bearerToken, timeoutMs: cfg.timeoutMs }) : new StdioTransport({ command: cfg.command, args: cfg.args, env: cfg.env, cwd: cfg.cwd, timeoutMs: cfg.timeoutMs });
9498
+ return cfg.url ? new HttpTransport({ url: cfg.url, headers: cfg.headers, bearerToken: cfg.bearerToken, timeoutMs: cfg.timeoutMs, callTimeoutMs: cfg.callTimeoutMs }) : new StdioTransport({ command: cfg.command, args: cfg.args, env: cfg.env, cwd: cfg.cwd, timeoutMs: cfg.timeoutMs, callTimeoutMs: cfg.callTimeoutMs });
9042
9499
  }
9043
9500
  function withTimeout(p, ms, label) {
9044
9501
  if (!ms || ms <= 0) return p;
@@ -9060,11 +9517,11 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
9060
9517
  return await withTimeout((async () => {
9061
9518
  const init = await client.connect();
9062
9519
  const specs = await client.listTools();
9063
- const tools = mcpToolsToAgentTools(specs, (tool, a) => client.callTool(tool, a), `mcp__${name}__`);
9520
+ const tools = mcpToolsToAgentTools(specs, (tool, a, o) => client.callTool(tool, a, o), `mcp__${name}__`);
9064
9521
  return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
9065
9522
  })(), mountTimeoutMs, name);
9066
9523
  } catch (e) {
9067
- await client.close().catch((err2) => log15.debug(`close after failed mount of "${name}": ${err2}`));
9524
+ await client.close().catch((err2) => log16.debug(`close after failed mount of "${name}": ${err2}`));
9068
9525
  throw e;
9069
9526
  }
9070
9527
  }
@@ -9075,15 +9532,15 @@ function validEntries(servers) {
9075
9532
  return Object.entries(servers).filter(([name, cfg]) => {
9076
9533
  if (!cfg || cfg.disabled) return false;
9077
9534
  if (!cfg.command && !cfg.url) {
9078
- log15.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
9535
+ log16.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
9079
9536
  return false;
9080
9537
  }
9081
9538
  return true;
9082
9539
  });
9083
9540
  }
9084
9541
  function logMountFailure(name, e) {
9085
- if (e instanceof McpAuthError) log15.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
9086
- else log15.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
9542
+ if (e instanceof McpAuthError) log16.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
9543
+ else log16.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
9087
9544
  }
9088
9545
  async function mountMcpServers(servers = {}, opts = {}) {
9089
9546
  const entries = validEntries(servers);
@@ -9093,7 +9550,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
9093
9550
  const name = entries[i][0];
9094
9551
  if (r.status === "fulfilled") {
9095
9552
  out.push(r.value);
9096
- log15.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
9553
+ log16.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
9097
9554
  } else logMountFailure(name, r.reason);
9098
9555
  });
9099
9556
  return out;
@@ -9133,7 +9590,7 @@ var McpPool = class {
9133
9590
  const prev = this.warm.get(key);
9134
9591
  if (prev) {
9135
9592
  clearTimeout(prev.timer);
9136
- if (prev.client !== client) void prev.client.close().catch((err2) => log15.debug(`warm-pool replace close failed: ${err2}`));
9593
+ if (prev.client !== client) void prev.client.close().catch((err2) => log16.debug(`warm-pool replace close failed: ${err2}`));
9137
9594
  }
9138
9595
  const e = { client, timer: void 0 };
9139
9596
  this.warm.set(key, e);
@@ -9150,7 +9607,7 @@ var McpPool = class {
9150
9607
  const e = this.warm.get(key);
9151
9608
  if (!e) return;
9152
9609
  this.warm.delete(key);
9153
- await e.client.close().catch((err2) => log15.debug(`warm-pool evict close failed: ${err2}`));
9610
+ await e.client.close().catch((err2) => log16.debug(`warm-pool evict close failed: ${err2}`));
9154
9611
  }
9155
9612
  async closeAll() {
9156
9613
  for (const e of this.warm.values()) {
@@ -9368,7 +9825,7 @@ init_tools_shell();
9368
9825
  // src/tools.notify.ts
9369
9826
  init_logging();
9370
9827
  import { execFile } from "child_process";
9371
- var log17 = forComponent("notify");
9828
+ var log18 = forComponent("notify");
9372
9829
  function makeNotifyTool(opts = {}) {
9373
9830
  const platform2 = opts.platform ?? process.platform;
9374
9831
  const run = opts.exec ?? execFile;
@@ -9392,7 +9849,7 @@ function makeNotifyTool(opts = {}) {
9392
9849
  return new Promise((resolve5) => {
9393
9850
  run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
9394
9851
  if (e) {
9395
- log17.debug("notification failed", e);
9852
+ log18.debug("notification failed", e);
9396
9853
  resolve5(`Notification failed: ${e.message}`);
9397
9854
  } else resolve5("Notification shown.");
9398
9855
  });
@@ -9408,7 +9865,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
9408
9865
  init_logging();
9409
9866
  import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
9410
9867
  import { homedir } from "os";
9411
- var log18 = forComponent("cli-util");
9868
+ var log19 = forComponent("cli-util");
9412
9869
  function dotDirs(base, sub, opts = {}) {
9413
9870
  const home = opts.home ?? homedir();
9414
9871
  const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
@@ -9425,7 +9882,7 @@ function parseJson(text, fallback, what = "json") {
9425
9882
  try {
9426
9883
  return JSON.parse(text);
9427
9884
  } catch (e) {
9428
- log18.debug(`parseJson(${what}) failed: ${e.message}`);
9885
+ log19.debug(`parseJson(${what}) failed: ${e.message}`);
9429
9886
  return fallback;
9430
9887
  }
9431
9888
  }
@@ -9435,7 +9892,7 @@ function readJsonFile(path, fallback) {
9435
9892
  try {
9436
9893
  text = readFileSync4(path, "utf8");
9437
9894
  } catch (e) {
9438
- log18.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
9895
+ log19.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
9439
9896
  return fallback;
9440
9897
  }
9441
9898
  return parseJson(text, fallback, path);
@@ -9490,9 +9947,9 @@ function toCursorMcp(servers) {
9490
9947
  }
9491
9948
  return Object.keys(out).length ? { mcpServers: out } : void 0;
9492
9949
  }
9493
- function cursorProviderOptions(model, cwd, mcpServers) {
9950
+ function cursorProviderOptions(model, cwd, mcpServers, hostMcpServers) {
9494
9951
  if (!(model ?? "").startsWith("cursor/")) return void 0;
9495
- return { cwd, ...toCursorMcp(mcpServers) ?? {}, ...process.env.CURSOR_WARM === "0" ? {} : { cursorSession: randomUUID() } };
9952
+ return { cwd, ...toCursorMcp(mcpServers) ?? {}, ...hostMcpServers?.length ? { hostMcpServers } : {}, ...process.env.CURSOR_WARM === "0" ? {} : { cursorSession: randomUUID() } };
9496
9953
  }
9497
9954
  function toClaudeCodeMcp(servers) {
9498
9955
  if (!servers) return void 0;
@@ -9519,8 +9976,8 @@ function claudeCodeProviderOptions(model, cwd, mcpServers) {
9519
9976
  const warm = process.env.AGENTX_CC_WARM === "0" ? {} : { claudeCodeSession: randomUUID() };
9520
9977
  return { cwd, ...env ? { env } : {}, ...warm, ...toClaudeCodeMcp(mcpServers) ?? {} };
9521
9978
  }
9522
- function providerOptionsFor(model, cwd, mcpServers) {
9523
- return cursorProviderOptions(model, cwd, mcpServers) ?? claudeCodeProviderOptions(model, cwd, mcpServers);
9979
+ function providerOptionsFor(model, cwd, mcpServers, hostMcpServers) {
9980
+ return cursorProviderOptions(model, cwd, mcpServers, hostMcpServers) ?? claudeCodeProviderOptions(model, cwd, mcpServers);
9524
9981
  }
9525
9982
  async function buildAgent(o) {
9526
9983
  if (typeof o.sandbox !== "boolean")
@@ -9605,12 +10062,14 @@ Reference files in them by their mount path (the left side).`;
9605
10062
  const hooks = o.learnFromMistakes && memoryWriteDir ? composeHooks(o.hooks, lessonCapture({ fs, dir: memoryWriteDir, minRepeats: 2 })) : o.hooks;
9606
10063
  let realShell = [];
9607
10064
  const agentRef = {};
10065
+ const jobRegistry = new JobRegistry();
9608
10066
  const useRealShell = o.realShell ?? !virtual;
9609
10067
  if (useRealShell && !virtual && !isClaudeCode) {
9610
10068
  const jobs = new ShellJobRegistry({
9611
10069
  cwd,
9612
10070
  killOnExit: true,
9613
10071
  osSandbox: o.osSandbox,
10072
+ jobs: jobRegistry,
9614
10073
  onExit: (n) => agentRef.agent?.inject(formatJobExit(n))
9615
10074
  });
9616
10075
  realShell = [makeRealShellTool({ cwd, registry: jobs, osSandbox: o.osSandbox, jobsSurviveRun: o.singleRun !== true }), ...makeShellJobTools(jobs)];
@@ -9619,6 +10078,7 @@ Reference files in them by their mount path (the left side).`;
9619
10078
  const scratch = scratchDir ? new Scratch(fs, { dir: scratchDir }) : void 0;
9620
10079
  return agentRef.agent = new Agent({
9621
10080
  ai: o.ai,
10081
+ jobs: jobRegistry,
9622
10082
  fs,
9623
10083
  model: o.model ?? "anthropic/claude-sonnet-4-6",
9624
10084
  // PDF reads (disk mode only — VFS paths aren't real files): poppler's pdftotext when installed.
@@ -9630,7 +10090,7 @@ Reference files in them by their mount path (the left side).`;
9630
10090
  // Warm cursor session (default on; CURSOR_WARM=0 to disable): one long-lived agent reused across
9631
10091
  // turns, amortising the ~2s Agent.create + h2 handshake. Keyed per agentx launch.
9632
10092
  ...(() => {
9633
- const po = providerOptionsFor(o.model, cwd, o.mcpServers);
10093
+ const po = providerOptionsFor(o.model, cwd, o.mcpServers, o.hostMcpServers);
9634
10094
  return po ? { providerOptions: po } : {};
9635
10095
  })(),
9636
10096
  ...(() => {
@@ -9731,7 +10191,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
9731
10191
  import { homedir as homedir3 } from "os";
9732
10192
  import { dirname as dirname3, join as join6 } from "path";
9733
10193
  import { fileURLToPath } from "url";
9734
- var log19 = forComponent("VoiceIO");
10194
+ var log20 = forComponent("VoiceIO");
9735
10195
  var now3 = () => performance.now();
9736
10196
  var Player = class {
9737
10197
  proc = null;
@@ -9745,7 +10205,7 @@ var Player = class {
9745
10205
  ["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
9746
10206
  { stdio: ["pipe", "ignore", "ignore"] }
9747
10207
  );
9748
- this.proc.on("error", (e) => log19.warn(`ffplay error: ${e.message}`));
10208
+ this.proc.on("error", (e) => log20.warn(`ffplay error: ${e.message}`));
9749
10209
  this.proc.stdin.on("error", () => {
9750
10210
  });
9751
10211
  this.bytesWritten = 0;
@@ -9783,7 +10243,7 @@ function detectFfmpegMic() {
9783
10243
  const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
9784
10244
  const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
9785
10245
  if (!mic) throw new Error("no audio input device found");
9786
- log19.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
10246
+ log20.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
9787
10247
  return `:${mic.idx}`;
9788
10248
  }
9789
10249
  function detectedInputDevice() {
@@ -9819,15 +10279,15 @@ function resolveAecBinary() {
9819
10279
  if (existsSync4(bin) && statSync4(bin).mtimeMs >= statSync4(src).mtimeMs) return bin;
9820
10280
  if (spawnSync2("which", ["swiftc"]).status !== 0) return null;
9821
10281
  mkdirSync5(cacheDir, { recursive: true });
9822
- log19.info("compiling AEC mic helper (first run)\u2026");
10282
+ log20.info("compiling AEC mic helper (first run)\u2026");
9823
10283
  const build = spawnSync2("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
9824
10284
  if (build.status !== 0) {
9825
- log19.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
10285
+ log20.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
9826
10286
  return null;
9827
10287
  }
9828
10288
  const sign = spawnSync2("codesign", ["-fs", "-", bin], { encoding: "utf8" });
9829
10289
  if (sign.status !== 0) {
9830
- log19.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
10290
+ log20.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
9831
10291
  return null;
9832
10292
  }
9833
10293
  return bin;
@@ -9869,16 +10329,16 @@ var NodeMicSource = class {
9869
10329
  this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
9870
10330
  } else {
9871
10331
  if (spawnSync2("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
9872
- log19.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
10332
+ log20.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
9873
10333
  this.proc = spawn2(
9874
10334
  "ffmpeg",
9875
10335
  ["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
9876
10336
  { stdio: ["ignore", "pipe", "pipe"] }
9877
10337
  );
9878
- this.proc.stderr.on("data", (d) => log19.warn(`ffmpeg: ${String(d).trim()}`));
10338
+ this.proc.stderr.on("data", (d) => log20.warn(`ffmpeg: ${String(d).trim()}`));
9879
10339
  }
9880
10340
  this.proc.on("exit", (c) => {
9881
- if (c && !this.stopped) log19.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
10341
+ if (c && !this.stopped) log20.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
9882
10342
  });
9883
10343
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
9884
10344
  }
@@ -9944,7 +10404,7 @@ var AecDuplexAudio = class {
9944
10404
  this.proc.stdin.on("error", () => {
9945
10405
  });
9946
10406
  this.proc.on("exit", (c) => {
9947
- if (c && !this.stopped) log19.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
10407
+ if (c && !this.stopped) log20.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
9948
10408
  });
9949
10409
  this.proc.stdout.on("data", (chunk) => {
9950
10410
  this.gotChunk = true;
@@ -9960,7 +10420,7 @@ var AecDuplexAudio = class {
9960
10420
  openMicSettings();
9961
10421
  this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
9962
10422
  }
9963
- } else log19.debug(`mic-aec: ${s}`);
10423
+ } else log20.debug(`mic-aec: ${s}`);
9964
10424
  }
9965
10425
  });
9966
10426
  if (!this.noVpio && !this.triedFallback) {
@@ -9969,7 +10429,7 @@ var AecDuplexAudio = class {
9969
10429
  this.triedFallback = true;
9970
10430
  this.noVpio = true;
9971
10431
  this._aec = false;
9972
- log19.warn("mic-aec: VPIO delivered no audio in 2.5s \u2014 falling back to non-VPIO capture (no AEC \u2192 half-duplex, no barge-in)");
10432
+ log20.warn("mic-aec: VPIO delivered no audio in 2.5s \u2014 falling back to non-VPIO capture (no AEC \u2192 half-duplex, no barge-in)");
9973
10433
  this.onDegrade?.();
9974
10434
  this.killProc();
9975
10435
  this.spawnHelper();
@@ -10189,7 +10649,7 @@ function fakeVoiceParts(uttFile) {
10189
10649
  init_logging();
10190
10650
  import { appendFile, mkdirSync as mkdirSync6 } from "fs";
10191
10651
  import { dirname as dirname4 } from "path";
10192
- var log20 = forComponent("VoiceDiag");
10652
+ var log21 = forComponent("VoiceDiag");
10193
10653
  var JsonlDiagSink = class {
10194
10654
  constructor(path) {
10195
10655
  this.path = path;
@@ -10230,7 +10690,7 @@ var JsonlDiagSink = class {
10230
10690
  disable(e) {
10231
10691
  this.disabled = true;
10232
10692
  this.queue.length = 0;
10233
- log20.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
10693
+ log21.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
10234
10694
  }
10235
10695
  /** True once a failure permanently disabled the sink (tests/inspection). */
10236
10696
  get isDisabled() {
@@ -10300,7 +10760,7 @@ async function loadConfig(cwd) {
10300
10760
 
10301
10761
  // cli/hooks-config.ts
10302
10762
  import { spawnSync as spawnSync3 } from "child_process";
10303
- var log21 = forComponent("hooks");
10763
+ var log22 = forComponent("hooks");
10304
10764
  var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
10305
10765
  function ruleMatches(rule, toolName) {
10306
10766
  if (!rule.tool || rule.tool === "*") return true;
@@ -10317,7 +10777,7 @@ function runCmd(rule, env) {
10317
10777
  });
10318
10778
  return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
10319
10779
  } catch (e) {
10320
- log21.debug(`hook command failed: ${rule.command}`, e);
10780
+ log22.debug(`hook command failed: ${rule.command}`, e);
10321
10781
  return { code: 1, out: String(e?.message ?? e) };
10322
10782
  }
10323
10783
  }
@@ -10424,7 +10884,7 @@ function formatDiff(ops, opts = {}) {
10424
10884
  import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync } from "fs";
10425
10885
  import { homedir as homedir5 } from "os";
10426
10886
  import { join as join8 } from "path";
10427
- var log22 = forComponent("session");
10887
+ var log23 = forComponent("session");
10428
10888
  var globalDir = () => join8(homedir5(), ".agent", "sessions");
10429
10889
  var SessionStore = class {
10430
10890
  dir;
@@ -10469,7 +10929,7 @@ var SessionStore = class {
10469
10929
  }
10470
10930
  load(id) {
10471
10931
  if (!this.safeId(id)) {
10472
- log22.debug(`rejecting unsafe session id: ${id}`);
10932
+ log23.debug(`rejecting unsafe session id: ${id}`);
10473
10933
  return void 0;
10474
10934
  }
10475
10935
  const path = join8(this.dir, `${id}.json`);
@@ -10477,7 +10937,7 @@ var SessionStore = class {
10477
10937
  try {
10478
10938
  return JSON.parse(readFileSync7(path, "utf8"));
10479
10939
  } catch (e) {
10480
- log22.debug(`unreadable session ${id} \u2014 ignoring`, e);
10940
+ log23.debug(`unreadable session ${id} \u2014 ignoring`, e);
10481
10941
  return void 0;
10482
10942
  }
10483
10943
  }
@@ -10490,7 +10950,7 @@ var SessionStore = class {
10490
10950
  try {
10491
10951
  metas.push(JSON.parse(readFileSync7(join8(this.dir, f), "utf8")).meta);
10492
10952
  } catch (e) {
10493
- log22.debug(`skipping unreadable session file ${f}`, e);
10953
+ log23.debug(`skipping unreadable session file ${f}`, e);
10494
10954
  }
10495
10955
  }
10496
10956
  return metas.sort((a, b) => b.updated - a.updated);
@@ -10659,7 +11119,7 @@ import { promisify } from "util";
10659
11119
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync8 } from "fs";
10660
11120
  import { homedir as homedir6 } from "os";
10661
11121
  import { join as join9, resolve as resolve3, sep as sep3, parse as parsePath } from "path";
10662
- var log23 = forComponent("checkpoints");
11122
+ var log24 = forComponent("checkpoints");
10663
11123
  var exec = promisify(execFile3);
10664
11124
  var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
10665
11125
  function isBroadRoot(p) {
@@ -10705,7 +11165,7 @@ var ShadowRepo = class {
10705
11165
  writeFileSync5(join9(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
10706
11166
  this.ready = true;
10707
11167
  } catch (e) {
10708
- log23.debug(`git checkpoints unavailable for ${this.workTree}`, e);
11168
+ log24.debug(`git checkpoints unavailable for ${this.workTree}`, e);
10709
11169
  this.ready = false;
10710
11170
  }
10711
11171
  return this.ready;
@@ -10716,7 +11176,7 @@ var ShadowRepo = class {
10716
11176
  }
10717
11177
  async commit(label, forced = []) {
10718
11178
  await this.run("add", "-A");
10719
- for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log23.debug(`force-add failed: ${p}`, e));
11179
+ for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log24.debug(`force-add failed: ${p}`, e));
10720
11180
  await this.run("commit", "--allow-empty", "-q", "-m", label);
10721
11181
  }
10722
11182
  /** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
@@ -10724,8 +11184,8 @@ var ShadowRepo = class {
10724
11184
  * turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
10725
11185
  * point per turn, so the REPL's turn↔frame mapping stays intact. */
10726
11186
  async amendForced(paths) {
10727
- for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log23.debug(`force-capture failed: ${p}`, e));
10728
- await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log23.debug("amend failed", e));
11187
+ for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log24.debug(`force-capture failed: ${p}`, e));
11188
+ await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log24.debug("amend failed", e));
10729
11189
  }
10730
11190
  /** Commits on `ref`, oldest-first (canonical index space). */
10731
11191
  async log(ref) {
@@ -10785,7 +11245,7 @@ var ShadowRepo = class {
10785
11245
  await this.run("gc", "--auto", "-q").catch(() => {
10786
11246
  });
10787
11247
  } catch (e) {
10788
- log23.debug("checkpoint prune failed", e);
11248
+ log24.debug("checkpoint prune failed", e);
10789
11249
  }
10790
11250
  }
10791
11251
  };
@@ -10853,7 +11313,7 @@ var GitCheckpoints = class {
10853
11313
  use(sessionId) {
10854
11314
  if (sessionId === this.session) return;
10855
11315
  this.session = sessionId;
10856
- if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log23.debug("re-point failed", e));
11316
+ if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log24.debug("re-point failed", e));
10857
11317
  }
10858
11318
  async begin(label) {
10859
11319
  if (!await this.start()) return;
@@ -10873,7 +11333,7 @@ var GitCheckpoints = class {
10873
11333
  process.stderr.write(`\x1B[2m checkpoints: '${this.repos[i].root}' too large to snapshot in ${Math.round(this.options.opTimeoutMs / 1e3)}s \u2014 file rewind disabled here\x1B[0m
10874
11334
  `);
10875
11335
  } else {
10876
- log23.debug("checkpoint commit failed", e);
11336
+ log24.debug("checkpoint commit failed", e);
10877
11337
  survivors.push(this.repos[i]);
10878
11338
  survivorCaches.push(this.caches[i] ?? []);
10879
11339
  }
@@ -10908,7 +11368,7 @@ var GitCheckpoints = class {
10908
11368
  if (this.forced.has(abs)) continue;
10909
11369
  this.forced.add(abs);
10910
11370
  const i = this.repoIndexFor(abs);
10911
- if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log23.debug("amendForced failed", e));
11371
+ if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log24.debug("amendForced failed", e));
10912
11372
  }
10913
11373
  }
10914
11374
  };
@@ -12731,7 +13191,7 @@ import { spawnSync as spawnSync5 } from "child_process";
12731
13191
  import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
12732
13192
  import { homedir as homedir8 } from "os";
12733
13193
  import { join as join12 } from "path";
12734
- var log24 = forComponent("os-sched");
13194
+ var log25 = forComponent("os-sched");
12735
13195
  var OsScheduler = class {
12736
13196
  options;
12737
13197
  constructor(options) {
@@ -12793,7 +13253,7 @@ var OsScheduler = class {
12793
13253
  }
12794
13254
  }
12795
13255
  } catch (e) {
12796
- log24.debug(`cancel ${id}`, e);
13256
+ log25.debug(`cancel ${id}`, e);
12797
13257
  }
12798
13258
  for (const f of [`${id}.json`, `${id}.sh`]) {
12799
13259
  try {
@@ -12910,7 +13370,7 @@ import { spawn as spawn3 } from "child_process";
12910
13370
  import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
12911
13371
  import { homedir as homedir9 } from "os";
12912
13372
  import { join as join13 } from "path";
12913
- var log25 = forComponent("remote-trigger");
13373
+ var log26 = forComponent("remote-trigger");
12914
13374
  var TRIGGER_DIR = () => join13(homedir9(), ".agent", "triggers");
12915
13375
  var sockPath = (sessionId, dir = TRIGGER_DIR()) => join13(dir, `${sessionId}.sock`);
12916
13376
  var TriggerServer = class {
@@ -12948,13 +13408,13 @@ var TriggerServer = class {
12948
13408
  conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
12949
13409
  }
12950
13410
  });
12951
- conn.on("error", (e) => log25.debug("trigger conn error", e));
13411
+ conn.on("error", (e) => log26.debug("trigger conn error", e));
12952
13412
  });
12953
- this.server.on("error", (e) => log25.debug("trigger server error", e));
13413
+ this.server.on("error", (e) => log26.debug("trigger server error", e));
12954
13414
  this.server.listen(p);
12955
13415
  this.path = p;
12956
13416
  } catch (e) {
12957
- log25.debug("trigger server unavailable", e);
13417
+ log26.debug("trigger server unavailable", e);
12958
13418
  }
12959
13419
  }
12960
13420
  /** Re-bind on /resume (the session id changed). */
@@ -13076,7 +13536,7 @@ var strike = C("9");
13076
13536
  var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
13077
13537
  var mdPalette = { bold, dim, cyan, italic, strike, green, link };
13078
13538
  var err = (s) => process.stderr.write(s);
13079
- var log26 = forComponent("cli");
13539
+ var log27 = forComponent("cli");
13080
13540
  var VERSION = (() => {
13081
13541
  try {
13082
13542
  return JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
@@ -13126,9 +13586,9 @@ var spinner = /* @__PURE__ */ (() => {
13126
13586
  if (!tty || timer) return;
13127
13587
  t0 = this.turnStart || Date.now();
13128
13588
  timer = setInterval(() => {
13129
- const secs = Math.round((Date.now() - t0) / 1e3);
13589
+ const secs2 = Math.round((Date.now() - t0) / 1e3);
13130
13590
  const tail = this.tail?.();
13131
- err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label} ${secs ? `${secs}s \xB7 ` : ""}esc to interrupt`) + (tail ? " " + tail : ""));
13591
+ err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label} ${secs2 ? `${secs2}s \xB7 ` : ""}esc to interrupt`) + (tail ? " " + tail : ""));
13132
13592
  }, 90);
13133
13593
  },
13134
13594
  stop() {
@@ -13821,7 +14281,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
13821
14281
  function turnCost(model, usage) {
13822
14282
  return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
13823
14283
  }
13824
- async function evaluateGoal(ai, condition, transcript, log27) {
14284
+ async function evaluateGoal(ai, condition, transcript, log28) {
13825
14285
  const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
13826
14286
  const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
13827
14287
  return text.slice(0, 600);
@@ -13841,7 +14301,7 @@ ${recent}` }
13841
14301
  const match = r.content.match(/\{[\s\S]*\}/);
13842
14302
  if (match) return JSON.parse(match[0]);
13843
14303
  } catch (e) {
13844
- log27(dim(` (goal evaluator error: ${e?.message ?? e})
14304
+ log28(dim(` (goal evaluator error: ${e?.message ?? e})
13845
14305
  `));
13846
14306
  }
13847
14307
  return { met: false, reason: "evaluation unclear" };
@@ -14009,7 +14469,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
14009
14469
  anthropicWebSearch: cfg.anthropicWebSearch
14010
14470
  };
14011
14471
  }
14012
- async function makeAgent(args, ai, cfg, extraTools = []) {
14472
+ async function makeAgent(args, ai, cfg, extraTools = [], hostMcpServers) {
14013
14473
  const virtual = args.vfs || !!args.boddb;
14014
14474
  if (virtual && args.shell) err(yellow(` \u26A0 --shell ignored in ${args.boddb ? "--boddb" : "--vfs sandbox"} (a real shell would escape to disk)
14015
14475
  `));
@@ -14023,7 +14483,7 @@ async function makeAgent(args, ai, cfg, extraTools = []) {
14023
14483
  else if (args.shell === false) err(dim(" \u2296 --no-shell: VFS bash only (no real /bin/sh)\n"));
14024
14484
  if (args.harden && !virtual) err(dim(` \u26E8 hardened shell: writes confined to cwd+tmp${args.hardenNet ? "" : ", network blocked"} (sandbox-exec/bwrap)
14025
14485
  `));
14026
- const opts = optsFor(args, ai, cfg, extraTools);
14486
+ const opts = { ...optsFor(args, ai, cfg, extraTools), hostMcpServers };
14027
14487
  const agent = await buildAgent(opts);
14028
14488
  const display = displayHooks(agent.options.fs, { flush: agent.options.host?.flushText });
14029
14489
  agent.options.hooks = cfg.hooks ? composeHooks(display, hooksFromConfig(cfg.hooks)) : display;
@@ -14062,7 +14522,7 @@ function mcpAgentTools(mounted, opts) {
14062
14522
  return tools;
14063
14523
  }
14064
14524
  async function closeMcp(mounted) {
14065
- await Promise.all(mounted.map((m) => m.client.close().catch((e) => log26.debug("mcp close failed", e))));
14525
+ await Promise.all(mounted.map((m) => m.client.close().catch((e) => log27.debug("mcp close failed", e))));
14066
14526
  }
14067
14527
  var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
14068
14528
  function mentionRefs(line) {
@@ -14114,7 +14574,7 @@ async function expandMentions(fs, line) {
14114
14574
  if (loaded.includes(ref) || missing.includes(ref)) continue;
14115
14575
  if (ref.includes(":") && mcpMentionResolver) {
14116
14576
  const body = await mcpMentionResolver(ref).catch((e) => {
14117
- log26.debug("mcp mention resolve failed", e);
14577
+ log27.debug("mcp mention resolve failed", e);
14118
14578
  return null;
14119
14579
  });
14120
14580
  if (body != null) {
@@ -14201,7 +14661,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
14201
14661
  try {
14202
14662
  store.save(session);
14203
14663
  } catch (ex) {
14204
- log26.debug("mid-turn session flush failed", ex);
14664
+ log27.debug("mid-turn session flush failed", ex);
14205
14665
  }
14206
14666
  }
14207
14667
  return origNotify(e);
@@ -14228,7 +14688,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
14228
14688
  }
14229
14689
  agent.options.host?.flushText?.();
14230
14690
  flushLookupGroup();
14231
- const secs = ((Date.now() - t0) / 1e3).toFixed(1);
14691
+ const secs2 = ((Date.now() - t0) / 1e3).toFixed(1);
14232
14692
  const cost = turnCost(agent.options.model, res.usage);
14233
14693
  const m = res.usageEstimated ? "~" : "";
14234
14694
  const tok = res.usage?.totalTokens ? `${m}${(res.usage.totalTokens / 1e3).toFixed(1)}k tok \xB7 ${cost > 0 ? m + fmtUsd(cost) + " \xB7 " : ""}` : "";
@@ -14239,7 +14699,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
14239
14699
  const didWork = agent.transcript.slice(msgsBefore).some((m2) => m2.role === "assistant" || m2.role === "tool");
14240
14700
  const silentAbort = res.finishReason === "aborted" && !res.usage?.totalTokens && !didWork;
14241
14701
  if (!silentAbort)
14242
- err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : res.finishReason === "needs_input" ? yellow(" ? needs input") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs}s \xB7 ${shortId}
14702
+ err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : res.finishReason === "needs_input" ? yellow(" ? needs input") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs2}s \xB7 ${shortId}
14243
14703
  `));
14244
14704
  if (!silentAbort && process.stderr.isTTY && Date.now() - t0 > 1e4) err("\x07");
14245
14705
  if (res.finishReason === "error" && res.error) {
@@ -14350,14 +14810,14 @@ var isCancelTeardown = (e) => {
14350
14810
  function installCancelGuards(mounted) {
14351
14811
  process.on("unhandledRejection", (e) => {
14352
14812
  if (isCancelTeardown(e)) {
14353
- log26.debug("suppressed unhandledRejection (cursor stream cancel)", e);
14813
+ log27.debug("suppressed unhandledRejection (cursor stream cancel)", e);
14354
14814
  return;
14355
14815
  }
14356
- log26.error("unhandledRejection", e);
14816
+ log27.error("unhandledRejection", e);
14357
14817
  });
14358
14818
  process.on("uncaughtException", (e) => {
14359
14819
  if (isCancelTeardown(e)) {
14360
- log26.debug("suppressed uncaughtException (cursor stream cancel)", e);
14820
+ log27.debug("suppressed uncaughtException (cursor stream cancel)", e);
14361
14821
  return;
14362
14822
  }
14363
14823
  console.error(e);
@@ -14371,7 +14831,7 @@ async function repl(args, ai, cfg, cwd) {
14371
14831
  let mcpToolNames = [];
14372
14832
  const initialMcpTools = mcpAgentTools(mounted);
14373
14833
  mcpToolNames = initialMcpTools.map((t) => t.name);
14374
- const agent = await makeAgent(args, ai, cfg, initialMcpTools);
14834
+ const agent = await makeAgent(args, ai, cfg, initialMcpTools, mounted.map((m) => m.name));
14375
14835
  if (args.voice && !args.duplex) agent.options.tools = [...agent.options.tools ?? [], exitSessionTool(() => {
14376
14836
  exitRequested = true;
14377
14837
  })];
@@ -14613,7 +15073,7 @@ async function repl(args, ai, cfg, cwd) {
14613
15073
  ...args.voiceModel ?? cfg.reflexModel ? { reflexModel: resolveModelOrNewest(args.voiceModel ?? cfg.reflexModel) } : {},
14614
15074
  actModel: agent.options.model,
14615
15075
  actOptions: workerOptions,
14616
- providerOptionsFor: (m) => providerOptionsFor(m, cwd, cfg.mcpServers),
15076
+ providerOptionsFor: (m) => providerOptionsFor(m, cwd, cfg.mcpServers, mounted.map((x) => x.name)),
14617
15077
  ...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
14618
15078
  host,
14619
15079
  ...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true, emotionTags: emotionsOn } : {},
@@ -14695,7 +15155,7 @@ async function repl(args, ai, cfg, cwd) {
14695
15155
  const setModel = (m) => {
14696
15156
  work.model = m;
14697
15157
  work.contextWindow = getModelInfo(m)?.contextWindow ?? 0;
14698
- work.providerOptions = providerOptionsFor(m, cwd, cfg.mcpServers);
15158
+ work.providerOptions = providerOptionsFor(m, cwd, cfg.mcpServers, mounted.map((x) => x.name));
14699
15159
  if (dx) dx.options.actModel = m;
14700
15160
  persistModel(cwd, m);
14701
15161
  err(dim(" model \u2192 " + m + "\n"));
@@ -14762,7 +15222,7 @@ async function repl(args, ai, cfg, cwd) {
14762
15222
  try {
14763
15223
  mkdirSync12(dir, { recursive: true });
14764
15224
  } catch (e) {
14765
- log26.debug("paste image dir create failed", e);
15225
+ log27.debug("paste image dir create failed", e);
14766
15226
  }
14767
15227
  const img = grabClipboardImage(dir, String(Date.now()));
14768
15228
  return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
@@ -14875,7 +15335,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
14875
15335
  mkdirSync12(join14(cwd, ".agent"), { recursive: true });
14876
15336
  appendFileSync(histPath, line + "\n");
14877
15337
  } catch (e) {
14878
- log26.debug("history write failed", e);
15338
+ log27.debug("history write failed", e);
14879
15339
  }
14880
15340
  };
14881
15341
  const ago = (t) => {
@@ -14947,7 +15407,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
14947
15407
  try {
14948
15408
  store.save(session);
14949
15409
  } catch (e) {
14950
- log26.debug("session save after rewind failed", e);
15410
+ log27.debug("session save after rewind failed", e);
14951
15411
  }
14952
15412
  err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
14953
15413
  `));
@@ -14981,7 +15441,7 @@ ${task}`;
14981
15441
  bangContext.length = 0;
14982
15442
  }
14983
15443
  const delta = await refreshCatalogs().catch((e) => {
14984
- log26.debug("catalog refresh failed", e);
15444
+ log27.debug("catalog refresh failed", e);
14985
15445
  return "";
14986
15446
  });
14987
15447
  if (delta) {
@@ -15245,7 +15705,7 @@ ${task}`;
15245
15705
  desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
15246
15706
  run: async () => {
15247
15707
  await refreshCatalogs().catch((e) => {
15248
- log26.debug("catalog refresh failed", e);
15708
+ log27.debug("catalog refresh failed", e);
15249
15709
  });
15250
15710
  face.reprepare();
15251
15711
  err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
@@ -15747,7 +16207,7 @@ ${task}`;
15747
16207
  try {
15748
16208
  for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
15749
16209
  } catch (e) {
15750
- log26.debug(`loadAgents(${d}) failed`, e);
16210
+ log27.debug(`loadAgents(${d}) failed`, e);
15751
16211
  }
15752
16212
  }
15753
16213
  if (!seen.size) {
@@ -15835,7 +16295,7 @@ ${task}`;
15835
16295
  }
15836
16296
  if (idx >= 0) {
15837
16297
  const old = mounted.splice(idx, 1)[0];
15838
- await old.client.close().catch((e) => log26.debug("mcp close failed", e));
16298
+ await old.client.close().catch((e) => log27.debug("mcp close failed", e));
15839
16299
  }
15840
16300
  try {
15841
16301
  const m = await mountMcpServer(name, conf);
@@ -15863,7 +16323,7 @@ ${task}`;
15863
16323
  }
15864
16324
  const m = mounted.splice(idx, 1)[0];
15865
16325
  remountMcpTools();
15866
- await m.client.close().catch((e) => log26.debug("mcp close failed", e));
16326
+ await m.client.close().catch((e) => log27.debug("mcp close failed", e));
15867
16327
  err(dim(` removed "${name}"
15868
16328
  `));
15869
16329
  return;
@@ -16066,16 +16526,16 @@ ${task}`;
16066
16526
  try {
16067
16527
  return readdirSync4(join14(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
16068
16528
  } catch (e) {
16069
- log26.debug("completion readdir failed", absDir, e);
16529
+ log27.debug("completion readdir failed", absDir, e);
16070
16530
  return null;
16071
16531
  }
16072
16532
  };
16073
16533
  const agentCommandNames = () => [...cmds.map((c) => c.name), ...skills.map((s) => s.name)];
16074
16534
  const commandNames = () => [...Object.keys(builtins).filter((k) => k !== "quit"), ...agentCommandNames()];
16075
- const describe = (hit) => hit.startsWith("/") ? builtins[hit.slice(1)]?.desc ?? cmds.find((c) => c.name === hit.slice(1))?.description ?? skills.find((s) => s.name === hit.slice(1))?.description : void 0;
16535
+ const describe2 = (hit) => hit.startsWith("/") ? builtins[hit.slice(1)]?.desc ?? cmds.find((c) => c.name === hit.slice(1))?.description ?? skills.find((s) => s.name === hit.slice(1))?.description : void 0;
16076
16536
  const suggest = (lineToCursor) => {
16077
16537
  const [hits, token] = completeLine(lineToCursor, { commands: commandNames(), agentCommands: agentCommandNames(), listDir, bangHistory: history });
16078
- return { hits, token, describe };
16538
+ return { hits, token, describe: describe2 };
16079
16539
  };
16080
16540
  const editor = createLineEditor(process.stderr);
16081
16541
  editorRef = editor;
@@ -16202,7 +16662,7 @@ ${out}
16202
16662
  return;
16203
16663
  }
16204
16664
  await refreshCatalogs().catch((e) => {
16205
- log26.debug("catalog refresh failed", e);
16665
+ log27.debug("catalog refresh failed", e);
16206
16666
  });
16207
16667
  const sk = skills.find((x) => x.name === name);
16208
16668
  if (sk) {
@@ -16690,7 +17150,7 @@ async function main() {
16690
17150
  });
16691
17151
  if (args.task) {
16692
17152
  const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join14(cwd, ".agent", "mcp-auth.json") }));
16693
- const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted));
17153
+ const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted), mounted.map((m) => m.name));
16694
17154
  const store = new SessionStore(cwd);
16695
17155
  const session = startSession(args, store, agent, cwd);
16696
17156
  process.once("SIGINT", () => activeTurn?.abort());