@livx.cc/agentx 0.99.46 → 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,12 +4367,18 @@ 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;
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();
4378
+ toolHoldMs = 0;
4379
+ /** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
4380
+ * from the pre-step transcript, which would re-run it — so such a stall is not retried. */
4381
+ toolRanThisAttempt = false;
4047
4382
  /** Resets the active request's stall timer; a host tool's completion restarts the model's idle window. */
4048
4383
  stallPoke;
4049
4384
  /** Advertised tools this run's text claimed to have used while the ledger showed zero calls. */
@@ -4088,6 +4423,7 @@ var Agent = class _Agent {
4088
4423
  addTools(tools) {
4089
4424
  this.injectedTools.push(...tools);
4090
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));
4091
4427
  }
4092
4428
  /** Remove tools by name from a running agent. Returns the count removed. Also filters
4093
4429
  * `options.tools` — otherwise a constructor-passed tool resurrects on the next prepare()
@@ -4139,6 +4475,11 @@ var Agent = class _Agent {
4139
4475
  }
4140
4476
  constructor(options) {
4141
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
+ };
4142
4483
  if (this.options.fs) this.buildCtx();
4143
4484
  }
4144
4485
  /** Build the tool context from the resolved fs + options. Idempotent-safe: called once (ctor or ensureFs). */
@@ -4150,7 +4491,8 @@ var Agent = class _Agent {
4150
4491
  this.ctx.ai = this.options.ai;
4151
4492
  this.ctx.model = this.options.model;
4152
4493
  this.ctx.parkHuman = (p) => this.park(p);
4153
- 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;
4154
4496
  }
4155
4497
  /**
4156
4498
  * Resolve the filesystem + build the tool context if the ctor couldn't (no `fs` was passed). The disk
@@ -4166,7 +4508,7 @@ var Agent = class _Agent {
4166
4508
  const disk = new NodeDiskFilesystem2(process.cwd());
4167
4509
  await disk.init();
4168
4510
  this.options.fs = new JailedFilesystem2(disk);
4169
- 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()}`);
4170
4512
  }
4171
4513
  this.buildCtx();
4172
4514
  }
@@ -4236,7 +4578,7 @@ var Agent = class _Agent {
4236
4578
  tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
4237
4579
  }
4238
4580
  if (o.checkpoints) tools = [...tools, ...checkpointTools()];
4239
- 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)];
4240
4582
  const plan = o.planMode ? planMode({ host: o.host }) : void 0;
4241
4583
  if (plan) tools = [...tools, plan.tool];
4242
4584
  this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
@@ -4352,8 +4694,8 @@ var Agent = class _Agent {
4352
4694
  let closureNudged = false;
4353
4695
  let lastProgressAt = start;
4354
4696
  const kill = (finishReason) => {
4355
- 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` : ""})`);
4356
- 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);
4357
4699
  const claims = this.survivingClaims();
4358
4700
  return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
4359
4701
  };
@@ -4374,22 +4716,27 @@ var Agent = class _Agent {
4374
4716
  const hostToolTimeoutMs = o.providerOptions?.toolTimeoutMs;
4375
4717
  const derivedToolTimeoutMs = Math.max(TRANSPORT_TOOL_TIMEOUT_FLOOR_MS, ...this.activeTools.map(transportHeadroomMs));
4376
4718
  const effectiveToolTimeoutMs = typeof hostToolTimeoutMs === "number" ? Math.max(hostToolTimeoutMs, derivedToolTimeoutMs) : derivedToolTimeoutMs;
4719
+ this.toolHoldMs = effectiveToolTimeoutMs;
4377
4720
  if (isCursorWithTools) {
4378
4721
  if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
4379
- 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.`);
4380
4723
  }
4381
- for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log6.warn(v);
4724
+ for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log7.warn(v);
4382
4725
  }
4383
4726
  const cursorPo = isCursorWithTools ? {
4384
4727
  toolTimeoutMs: effectiveToolTimeoutMs,
4385
4728
  toolExecutor: async (name, args) => {
4386
4729
  const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
4387
- this.hostToolsInFlight++;
4388
- this.hostToolHoldUntil = Math.max(this.hostToolHoldUntil, Date.now() + effectiveToolTimeoutMs);
4730
+ const job = this.jobs.track({ kind: "host", label: name, holds: true, hardCapMs: effectiveToolTimeoutMs });
4389
4731
  try {
4390
- 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;
4391
4738
  } finally {
4392
- this.hostToolsInFlight--;
4739
+ this.toolRanThisAttempt = true;
4393
4740
  this.stallPoke?.();
4394
4741
  }
4395
4742
  }
@@ -4402,6 +4749,9 @@ var Agent = class _Agent {
4402
4749
  };
4403
4750
  try {
4404
4751
  for (let attempt = 0; ; attempt++) {
4752
+ for (const [, h] of this.nativeJobs) if (h.status === "running") this.jobs.kill(h.id, "step attempt ended");
4753
+ this.nativeJobs.clear();
4754
+ this.toolRanThisAttempt = false;
4405
4755
  try {
4406
4756
  if (useStream) {
4407
4757
  const stall = this.armStallWatchdog(o.signal, o.stallMs);
@@ -4431,10 +4781,12 @@ var Agent = class _Agent {
4431
4781
  const stalled = err2?.code === "stall";
4432
4782
  const empty = err2?.code === "empty" || err2?.code === "cursor_silent";
4433
4783
  const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
4434
- const transient = !o.signal?.aborted && !isAbortError(err2) && attempt < maxAttempts && (network || serverSide || stalled || empty);
4784
+ const replaysTool = stalled && this.toolRanThisAttempt;
4785
+ if (replaysTool) log7.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
4786
+ const transient = !o.signal?.aborted && !isAbortError(err2) && !replaysTool && attempt < maxAttempts && (network || serverSide || stalled || empty);
4435
4787
  if (!transient) throw err2;
4436
4788
  const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
4437
- 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`);
4438
4790
  o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
4439
4791
  await new Promise((r) => setTimeout(r, waitMs));
4440
4792
  }
@@ -4442,12 +4794,12 @@ var Agent = class _Agent {
4442
4794
  } catch (err2) {
4443
4795
  if (err2?.code === "budget") return kill("budget");
4444
4796
  if (err2?.code === "stall") {
4445
- log6.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
4797
+ log7.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
4446
4798
  return { ...kill("timeout"), error: err2 };
4447
4799
  }
4448
4800
  if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
4449
4801
  if (err2?.code === "empty" || err2?.code === "cursor_silent") {
4450
- 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`);
4451
4803
  return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err2 };
4452
4804
  }
4453
4805
  const body = err2?.body ?? err2?.response?.data ?? err2?.error;
@@ -4459,7 +4811,7 @@ var Agent = class _Agent {
4459
4811
  }
4460
4812
  if (bodyStr && err2 instanceof Error && !err2.message.includes(bodyStr)) err2.detail = bodyStr;
4461
4813
  const errLog = err2 instanceof Error ? { message: err2.message, ...err2.detail ? { detail: err2.detail } : {}, stack: err2.stack } : err2;
4462
- log6.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
4814
+ log7.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
4463
4815
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err2 };
4464
4816
  }
4465
4817
  if (o.signal?.aborted) return kill("aborted");
@@ -4515,21 +4867,21 @@ var Agent = class _Agent {
4515
4867
  if (emptyTurn && o.failEmptyTurns) {
4516
4868
  if (!emptyNudged && canNudge()) {
4517
4869
  emptyNudged = true;
4518
- 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`);
4519
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." });
4520
4872
  continue;
4521
4873
  }
4522
- 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`);
4523
4875
  const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
4524
4876
  e.code = "empty";
4525
4877
  return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
4526
4878
  }
4527
4879
  const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
4528
4880
  for (const n of fabricated) this.fabricatedClaims.add(n);
4529
- 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`);
4530
4882
  if (fabricated.length && canNudge()) {
4531
4883
  closureNudged = true;
4532
- 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`);
4533
4885
  this.transcript.push({
4534
4886
  role: "user",
4535
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.`
@@ -4538,15 +4890,18 @@ var Agent = class _Agent {
4538
4890
  }
4539
4891
  if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
4540
4892
  closureNudged = true;
4541
- 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");
4542
4894
  this.transcript.push({
4543
4895
  role: "user",
4544
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."
4545
4897
  });
4546
4898
  continue;
4547
4899
  }
4548
- log6.verbose(`completed in ${steps} step(s)`);
4549
- 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;
4550
4905
  this.releaseStaleImages();
4551
4906
  await this.activeHooks?.onStop?.(res.content ?? "");
4552
4907
  const claims = this.survivingClaims();
@@ -4572,11 +4927,12 @@ var Agent = class _Agent {
4572
4927
  }
4573
4928
  this.transcript.push({ role: "tool", tool_call_id: tc.id, name: tc.function.name, content });
4574
4929
  }
4930
+ lastProgressAt = Date.now();
4575
4931
  if (this.ctx.needsInput) {
4576
4932
  const question = this.ctx.needsInput;
4577
4933
  this.ctx.needsInput = void 0;
4578
- log6.warn(`needs_input: ${question.question} (steps=${steps})`);
4579
- await this.ctx.jobs?.drain();
4934
+ log7.warn(`needs_input: ${question.question} (steps=${steps})`);
4935
+ await this.jobs.drain();
4580
4936
  const text = [
4581
4937
  `OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
4582
4938
  ...question.options.map((o2) => ` \u2022 ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)
@@ -4614,7 +4970,9 @@ var Agent = class _Agent {
4614
4970
  if (!stallMs) return;
4615
4971
  if (timer) clearTimeout(timer);
4616
4972
  timer = setTimeout(() => {
4617
- if (this.hostToolsInFlight > 0 && Date.now() < this.hostToolHoldUntil) return poke();
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(", ")})` : ""));
4618
4976
  fired = true;
4619
4977
  ac.abort(stallError());
4620
4978
  }, stallMs);
@@ -4634,6 +4992,12 @@ var Agent = class _Agent {
4634
4992
  reclassify: (err2) => stalled() ? stallError() : err2
4635
4993
  };
4636
4994
  }
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. */
4997
+ toolHoldActive() {
4998
+ this.jobs.reap();
4999
+ return this.jobs.holding().length > 0;
5000
+ }
4637
5001
  /** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
4638
5002
  * non-result the model produced by going silent. Callers retry it rather than report "done". */
4639
5003
  isEmptyStop(res) {
@@ -4672,6 +5036,18 @@ var Agent = class _Agent {
4672
5036
  if (a.output !== void 0) prev.output = a.output;
4673
5037
  prev.status = a.status;
4674
5038
  }
5039
+ const nj = a.id ? this.nativeJobs.get(a.id) : void 0;
5040
+ if (a.status === "running") {
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 }));
5043
+ } else {
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
+ }
5049
+ this.toolRanThisAttempt = true;
5050
+ }
4675
5051
  sawTool = true;
4676
5052
  this.invokedTools.add(a.name);
4677
5053
  trailing = "";
@@ -4760,13 +5136,13 @@ var Agent = class _Agent {
4760
5136
  const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
4761
5137
  if (decision?.block) {
4762
5138
  const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
4763
- log6.debug(`${tc.function.name} -> ${blocked}`);
5139
+ log7.debug(`${tc.function.name} -> ${blocked}`);
4764
5140
  await hooks?.postToolUse?.(call, blocked, meta);
4765
5141
  return blocked;
4766
5142
  }
4767
5143
  this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
4768
5144
  if (earlyError) {
4769
- log6.debug(`${tc.function.name} -> ${earlyError}`);
5145
+ log7.debug(`${tc.function.name} -> ${earlyError}`);
4770
5146
  await hooks?.postToolUse?.(call, earlyError, meta);
4771
5147
  this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
4772
5148
  return earlyError;
@@ -4775,12 +5151,12 @@ var Agent = class _Agent {
4775
5151
  let images;
4776
5152
  let threw = false;
4777
5153
  try {
4778
- log6.debug(`${tc.function.name}(${tc.function.arguments})`);
5154
+ log7.debug(`${tc.function.name}(${tc.function.arguments})`);
4779
5155
  this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
4780
5156
  try {
4781
5157
  hooks.onToolOutput(call, chunk, meta);
4782
5158
  } catch (e) {
4783
- log6.debug(`onToolOutput hook error: ${e}`);
5159
+ log7.debug(`onToolOutput hook error: ${e}`);
4784
5160
  }
4785
5161
  } : void 0;
4786
5162
  const raw = await tool.run(args, this.ctx);
@@ -4792,7 +5168,7 @@ var Agent = class _Agent {
4792
5168
  }
4793
5169
  } catch (e) {
4794
5170
  const msg = e instanceof Error ? e.message : String(e);
4795
- log6.debug(`${tc.function.name} -> error: ${msg}`);
5171
+ log7.debug(`${tc.function.name} -> error: ${msg}`);
4796
5172
  result = `Error: ${msg}`;
4797
5173
  threw = true;
4798
5174
  } finally {
@@ -4884,6 +5260,7 @@ function estimateTokens(m, keepRecentImages = 1) {
4884
5260
  return Math.ceil(sendBytes(m, keepRecentImages) / 4);
4885
5261
  }
4886
5262
  var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
5263
+ var isMcpTool = (t) => t.name.startsWith("mcp__") || t.name === "McpCall";
4887
5264
  function transportHeadroomMs(t) {
4888
5265
  const d = Number(t.maxDurationMs) || 0;
4889
5266
  if (!d) return 0;
@@ -5059,7 +5436,7 @@ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
5059
5436
  const ids = callIdSet(messages.slice(from));
5060
5437
  while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
5061
5438
  if (total > maxTokens)
5062
- 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)`);
5063
5440
  return [...head, ...messages.slice(from)];
5064
5441
  }
5065
5442
  function compact(m, max, focus) {
@@ -5364,6 +5741,7 @@ init_OverlayFilesystem();
5364
5741
 
5365
5742
  // src/index.ts
5366
5743
  init_tools();
5744
+ init_tools_jobs();
5367
5745
 
5368
5746
  // src/scheduler.ts
5369
5747
  function parseCronField(field, min, max) {
@@ -5636,7 +6014,7 @@ init_tools_web();
5636
6014
  init_tools();
5637
6015
  init_tools_structured();
5638
6016
  init_logging();
5639
- var log7 = forComponent("scratch");
6017
+ var log8 = forComponent("scratch");
5640
6018
  var SCRATCH_DIR = "/scratch";
5641
6019
  function shortArgs(args) {
5642
6020
  try {
@@ -5676,7 +6054,7 @@ var Scratch = class {
5676
6054
  await (this.dirReady ??= mkdirp(this.fs, dir));
5677
6055
  await this.fs.writeFile(path, header + raw);
5678
6056
  } catch (e) {
5679
- log7.debug("scratch write failed; returning raw", e);
6057
+ log8.debug("scratch write failed; returning raw", e);
5680
6058
  return raw;
5681
6059
  }
5682
6060
  const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
@@ -5706,7 +6084,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
5706
6084
  await (this.dirReady ??= mkdirp(this.fs, dir));
5707
6085
  await this.fs.writeFile(path, header + full);
5708
6086
  } catch (e) {
5709
- log7.debug("scratch spill failed; cropping lossy", e);
6087
+ log8.debug("scratch spill failed; cropping lossy", e);
5710
6088
  return full.slice(0, pageBytes) + `
5711
6089
 
5712
6090
  [output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
@@ -5752,7 +6130,7 @@ Question: ${q2}`);
5752
6130
  const answer = (res.text ?? "").trim();
5753
6131
  return answer || "(no answer found in scratch)";
5754
6132
  } catch (e) {
5755
- log7.debug("Ask peek failed", e);
6133
+ log8.debug("Ask peek failed", e);
5756
6134
  return `Error querying scratch: ${e?.message ?? e}`;
5757
6135
  }
5758
6136
  }
@@ -5761,7 +6139,7 @@ Question: ${q2}`);
5761
6139
 
5762
6140
  // src/lessons.ts
5763
6141
  init_logging();
5764
- var log8 = forComponent("Lessons");
6142
+ var log9 = forComponent("Lessons");
5765
6143
  var LessonOptionsDefaults = class {
5766
6144
  minRepeats = 2;
5767
6145
  };
@@ -5786,8 +6164,8 @@ function lessonCapture(options) {
5786
6164
  counts.set(lesson.slug, n);
5787
6165
  if (n < o.minRepeats) return;
5788
6166
  written.add(lesson.slug);
5789
- await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log8.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
5790
- 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)`);
5791
6169
  }
5792
6170
  };
5793
6171
  }
@@ -5795,7 +6173,7 @@ function lessonCapture(options) {
5795
6173
  // src/reflect.ts
5796
6174
  init_llm();
5797
6175
  init_logging();
5798
- var log9 = forComponent("Reflect");
6176
+ var log10 = forComponent("Reflect");
5799
6177
  async function reflectOnRun(o) {
5800
6178
  const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
5801
6179
  if (!digest.trim()) return null;
@@ -5811,7 +6189,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
5811
6189
  const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
5812
6190
  text = r?.content ?? "";
5813
6191
  } catch (e) {
5814
- log9.warn(`reflection call failed: ${e?.message ?? e}`);
6192
+ log10.warn(`reflection call failed: ${e?.message ?? e}`);
5815
6193
  return null;
5816
6194
  }
5817
6195
  const m = text.match(/LESSON:\s*(.+)/i);
@@ -5821,10 +6199,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
5821
6199
  try {
5822
6200
  await writeFact(o.fs, o.dir, slug2, lesson);
5823
6201
  } catch (e) {
5824
- log9.warn(`could not persist lesson: ${e?.message ?? e}`);
6202
+ log10.warn(`could not persist lesson: ${e?.message ?? e}`);
5825
6203
  return null;
5826
6204
  }
5827
- log9.debug(`reflection persisted ${slug2}`);
6205
+ log10.debug(`reflection persisted ${slug2}`);
5828
6206
  return slug2;
5829
6207
  }
5830
6208
  function digestRun(messages, maxChars) {
@@ -5873,7 +6251,7 @@ function forComponent2(name) {
5873
6251
  }
5874
6252
 
5875
6253
  // node_modules/@bod.ee/voice/src/core/emotion.ts
5876
- var log10 = forComponent2("Emotion");
6254
+ var log11 = forComponent2("Emotion");
5877
6255
  var EMOTIONS = [
5878
6256
  // primary (best results)
5879
6257
  "neutral",
@@ -6005,7 +6383,7 @@ function renderEmotions(text, opts = { show: true }) {
6005
6383
  speech += cartesiaTag(tag);
6006
6384
  if (opts.show) display += m[0];
6007
6385
  } else {
6008
- log10.debug(`dropping unknown emotion tag ${m[0]}`);
6386
+ log11.debug(`dropping unknown emotion tag ${m[0]}`);
6009
6387
  }
6010
6388
  last = m.index + m[0].length;
6011
6389
  }
@@ -6047,7 +6425,7 @@ var EmotionStream = class {
6047
6425
  if (tag) {
6048
6426
  this.pending = tag;
6049
6427
  if (this.show) display += m[0];
6050
- } else log10.debug(`dropping unknown emotion tag ${m[0]}`);
6428
+ } else log11.debug(`dropping unknown emotion tag ${m[0]}`);
6051
6429
  last = m.index + m[0].length;
6052
6430
  }
6053
6431
  this.emit(body.slice(last), (s, d, p) => {
@@ -6070,7 +6448,7 @@ var EmotionStream = class {
6070
6448
  };
6071
6449
 
6072
6450
  // node_modules/@bod.ee/voice/src/core/engine.ts
6073
- var log11 = forComponent2("VoiceEngine");
6451
+ var log12 = forComponent2("VoiceEngine");
6074
6452
  var realClock = {
6075
6453
  now: () => performance.now(),
6076
6454
  setTimeout: (fn, ms) => setTimeout(fn, ms),
@@ -6345,7 +6723,7 @@ var VoiceEngine = class _VoiceEngine {
6345
6723
  await Promise.all([this.tts.connect(), this.stt.start()]);
6346
6724
  this.tts.warmup?.();
6347
6725
  this.setState("listening");
6348
- 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)`);
6349
6727
  }
6350
6728
  get usingAec() {
6351
6729
  return this.stt.usingAec;
@@ -6367,7 +6745,7 @@ var VoiceEngine = class _VoiceEngine {
6367
6745
  this.options.onDiag({ t: this.clock.now(), kind, ...fields });
6368
6746
  } catch (e) {
6369
6747
  this.diagOn = false;
6370
- 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}`);
6371
6749
  }
6372
6750
  }
6373
6751
  idleWaiters = [];
@@ -6476,7 +6854,7 @@ var VoiceEngine = class _VoiceEngine {
6476
6854
  if (prose.trim()) this.repliedSinceDispatch = true;
6477
6855
  for (const w of this.words(this.reply)) this.echoWords.add(w);
6478
6856
  this.tts.speak(forSpeech(speech), true);
6479
- 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`);
6480
6858
  this.spokeDeltas = true;
6481
6859
  this.setState("speaking");
6482
6860
  if (this.options.revealMode !== "off" && display) {
@@ -6553,7 +6931,7 @@ var VoiceEngine = class _VoiceEngine {
6553
6931
  this.drainTimer = null;
6554
6932
  this.speaking = false;
6555
6933
  this.stopWordReveal(true);
6556
- 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)`);
6557
6935
  this.echoUntil = this.clock.now() + 2500;
6558
6936
  if (!this.usingAec) this.stt.reset();
6559
6937
  this.setState("listening");
@@ -6641,7 +7019,7 @@ var VoiceEngine = class _VoiceEngine {
6641
7019
  this.clearAckTimer("interrupt");
6642
7020
  this.adaptiveAckFired = false;
6643
7021
  this.ackProbe = null;
6644
- 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)`);
6645
7023
  const droppedQueued = this.uttQueue.length;
6646
7024
  this.uttQueue = [];
6647
7025
  if (!this.speaking && !this.drainTimer) return;
@@ -6787,7 +7165,7 @@ var VoiceEngine = class _VoiceEngine {
6787
7165
  if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
6788
7166
  this.specSpent = true;
6789
7167
  this.specText = this.specPartial;
6790
- log11.debug(`speculate: "${this.specText.slice(0, 60)}"`);
7168
+ log12.debug(`speculate: "${this.specText.slice(0, 60)}"`);
6791
7169
  this.diag("speculate", { text: this.specText });
6792
7170
  this.options.onSpeculate(this.specText);
6793
7171
  }, this.options.speculativeMs);
@@ -6856,7 +7234,7 @@ var VoiceEngine = class _VoiceEngine {
6856
7234
  };
6857
7235
  this.tts.newContext();
6858
7236
  this.tts.speak(phrase, false);
6859
- log11.debug(`backchannel: "${phrase}"`);
7237
+ log12.debug(`backchannel: "${phrase}"`);
6860
7238
  this.diag("backchannel", { phrase });
6861
7239
  this.options.onBackchannel(phrase);
6862
7240
  }
@@ -6889,7 +7267,7 @@ var VoiceEngine = class _VoiceEngine {
6889
7267
  });
6890
7268
  if (idx < 0) return text;
6891
7269
  toks.splice(idx, 1);
6892
- log11.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
7270
+ log12.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
6893
7271
  return toks.join(" ");
6894
7272
  }
6895
7273
  /** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
@@ -6980,7 +7358,7 @@ var VoiceEngine = class _VoiceEngine {
6980
7358
  this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
6981
7359
  if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
6982
7360
  if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
6983
- log11.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
7361
+ log12.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
6984
7362
  this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
6985
7363
  this.options.onHold();
6986
7364
  if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
@@ -7015,13 +7393,13 @@ var VoiceEngine = class _VoiceEngine {
7015
7393
  if (text) {
7016
7394
  const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
7017
7395
  if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
7018
- 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)`);
7019
7397
  this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
7020
7398
  return;
7021
7399
  }
7022
7400
  const refinal = this.refinalizes(text);
7023
7401
  if (refinal && this.repliedSinceDispatch) {
7024
- 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)`);
7025
7403
  this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
7026
7404
  this.lastDispatchFlat = flat;
7027
7405
  this.lastDispatchWords = text.trim().split(/\s+/);
@@ -7181,7 +7559,7 @@ var SpokenSplitter = class {
7181
7559
  };
7182
7560
 
7183
7561
  // node_modules/@bod.ee/voice/src/adapters/soniox.ts
7184
- var log12 = forComponent2("SonioxSTT");
7562
+ var log13 = forComponent2("SonioxSTT");
7185
7563
  var now = () => performance.now();
7186
7564
  var SonioxSTTOptions = class {
7187
7565
  auth = "";
@@ -7222,7 +7600,7 @@ var SonioxSTT = class {
7222
7600
  this.onDiag({ t: now(), kind, ...fields });
7223
7601
  } catch (e) {
7224
7602
  this.diagOn = false;
7225
- 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}`);
7226
7604
  }
7227
7605
  }
7228
7606
  lastChunkAt = 0;
@@ -7264,11 +7642,11 @@ var SonioxSTT = class {
7264
7642
  this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
7265
7643
  this.ws.onclose = (ev) => {
7266
7644
  if (this.stopped) return;
7267
- log12.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
7645
+ log13.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
7268
7646
  this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
7269
7647
  this.reset();
7270
7648
  this.connectWs().catch((e) => {
7271
- log12.error(`soniox reconnect failed: ${e.message}`);
7649
+ log13.error(`soniox reconnect failed: ${e.message}`);
7272
7650
  this.diag("stt_reconnect_failed", { message: e.message });
7273
7651
  });
7274
7652
  };
@@ -7280,7 +7658,7 @@ var SonioxSTT = class {
7280
7658
  this.endpointTimer = setInterval(() => {
7281
7659
  const combined = (this.finalText + this.partialText).trim();
7282
7660
  if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
7283
- 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)}"`);
7284
7662
  this.reset();
7285
7663
  this.onUtterance(combined, now());
7286
7664
  }, 120);
@@ -7292,7 +7670,7 @@ var SonioxSTT = class {
7292
7670
  if (this.stopped) return;
7293
7671
  const ref = this.lastChunkAt || this.startedChunksAt;
7294
7672
  if (now() - ref > noAudioMs) {
7295
- 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`);
7296
7674
  this.diag("stt_watchdog_fatal", { noAudioMs });
7297
7675
  this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
7298
7676
  this.stop();
@@ -7315,7 +7693,7 @@ var SonioxSTT = class {
7315
7693
  handle(m) {
7316
7694
  if (m.error_message) {
7317
7695
  this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
7318
- return log12.error(`soniox: ${m.error_message}`);
7696
+ return log13.error(`soniox: ${m.error_message}`);
7319
7697
  }
7320
7698
  let endpoint = false;
7321
7699
  for (const t of m.tokens ?? []) {
@@ -7332,7 +7710,7 @@ var SonioxSTT = class {
7332
7710
  this.onPartial(combined);
7333
7711
  if (endpoint && this.finalText.trim()) {
7334
7712
  const utterance = this.finalText.trim();
7335
- 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)}"`);
7336
7714
  this.reset();
7337
7715
  this.onUtterance(utterance, now());
7338
7716
  }
@@ -7354,7 +7732,7 @@ var SonioxSTT = class {
7354
7732
  };
7355
7733
 
7356
7734
  // node_modules/@bod.ee/voice/src/adapters/cartesia.ts
7357
- var log13 = forComponent2("CartesiaTTS");
7735
+ var log14 = forComponent2("CartesiaTTS");
7358
7736
  var now2 = () => performance.now();
7359
7737
  var CartesiaTTSOptions = class {
7360
7738
  auth = "";
@@ -7390,7 +7768,7 @@ var CartesiaTTS = class _CartesiaTTS {
7390
7768
  this.onDiag({ t: now2(), kind, ...fields });
7391
7769
  } catch (e) {
7392
7770
  this.diagOn = false;
7393
- 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}`);
7394
7772
  }
7395
7773
  }
7396
7774
  firstAudioAt = 0;
@@ -7425,11 +7803,11 @@ var CartesiaTTS = class _CartesiaTTS {
7425
7803
  this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
7426
7804
  });
7427
7805
  this.ws.onclose = (ev) => {
7428
- log13.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
7806
+ log14.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
7429
7807
  this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
7430
7808
  if (!this.closed) {
7431
7809
  this.connecting = this.doConnect().catch((e) => {
7432
- log13.error(`cartesia reconnect failed: ${e.message}`);
7810
+ log14.error(`cartesia reconnect failed: ${e.message}`);
7433
7811
  this.diag("tts_reconnect_failed", { message: e.message });
7434
7812
  });
7435
7813
  }
@@ -7457,12 +7835,12 @@ var CartesiaTTS = class _CartesiaTTS {
7457
7835
  this.down = true;
7458
7836
  this.downAt = now2();
7459
7837
  this.consecutiveOk = 0;
7460
- 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`);
7461
7839
  this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
7462
7840
  this.onDone();
7463
7841
  this.startProbe();
7464
7842
  } else if (!this.down) {
7465
- (/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)}`);
7466
7844
  }
7467
7845
  }
7468
7846
  };
@@ -7477,7 +7855,7 @@ var CartesiaTTS = class _CartesiaTTS {
7477
7855
  this.stopProbe();
7478
7856
  const downMs = this.downAt ? now2() - this.downAt : 0;
7479
7857
  this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
7480
- (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)` : ""}`);
7481
7859
  }
7482
7860
  /** Ensure the WS is open before sending — reconnects if idle-closed. */
7483
7861
  async ensureConnected() {
@@ -7568,7 +7946,7 @@ configureLogging((name) => {
7568
7946
  });
7569
7947
 
7570
7948
  // src/duplex.ts
7571
- var log14 = forComponent("DuplexAgent");
7949
+ var log15 = forComponent("DuplexAgent");
7572
7950
  function describeCall(call) {
7573
7951
  const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
7574
7952
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
@@ -7779,7 +8157,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7779
8157
  const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
7780
8158
  if (m) {
7781
8159
  this.fabricationCut = true;
7782
- 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)`);
7783
8161
  const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
7784
8162
  if (!safe) return;
7785
8163
  if (safe.trim()) this.spokeThisTurn = true;
@@ -7874,7 +8252,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7874
8252
  return {
7875
8253
  preToolUse: (call) => {
7876
8254
  if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
7877
- log14.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
8255
+ log15.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
7878
8256
  this.abortSpeculation();
7879
8257
  return { block: true, reason: "Speculative turn aborted." };
7880
8258
  }
@@ -7912,7 +8290,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7912
8290
  try {
7913
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.");
7914
8292
  } catch (e) {
7915
- log14.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
8293
+ log15.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
7916
8294
  } finally {
7917
8295
  this.nudging = false;
7918
8296
  }
@@ -8005,7 +8383,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
8005
8383
  try {
8006
8384
  res = await this.voice.send(spec.text);
8007
8385
  } catch (e) {
8008
- log14.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
8386
+ log15.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
8009
8387
  } finally {
8010
8388
  this.voice.options.signal = prevSignal;
8011
8389
  }
@@ -8019,7 +8397,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
8019
8397
  if (d === "abort") {
8020
8398
  if (this.voice.transcript.length > base) this.voice.transcript.length = base;
8021
8399
  this.speculativeAbortedCalls++;
8022
- 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)}"`);
8023
8401
  if (this.spec === spec) this.spec = void 0;
8024
8402
  return res ?? empty;
8025
8403
  }
@@ -8284,7 +8662,7 @@ Another agent just implemented the above. Independently check the CURRENT state
8284
8662
  this.notify("task_verify", `task ${id}: verifying`, { id });
8285
8663
  const cres = await new Agent(checkerOpts).run(checkBrief);
8286
8664
  if (cres.finishReason !== "stop") {
8287
- log14.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
8665
+ log15.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
8288
8666
  this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
8289
8667
  }
8290
8668
  const sum = (a = 0, b = 0) => a + b;
@@ -8420,7 +8798,7 @@ Another agent just implemented the above. Independently check the CURRENT state
8420
8798
  rec.status = "done";
8421
8799
  rec.result = res.text;
8422
8800
  const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
8423
- 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}` : ""})`);
8424
8802
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
8425
8803
  id,
8426
8804
  text: res.text,
@@ -8462,14 +8840,14 @@ Another agent just implemented the above. Independently check the CURRENT state
8462
8840
  foldIfSuperseded(rec, note) {
8463
8841
  if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
8464
8842
  if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
8465
- 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`);
8466
8844
  return true;
8467
8845
  }
8468
8846
  failTask(rec, msg) {
8469
8847
  this.dropAsk(rec.id);
8470
8848
  rec.status = "error";
8471
8849
  rec.result = msg;
8472
- log14.warn(`task ${rec.id} failed: ${msg}`);
8850
+ log15.warn(`task ${rec.id} failed: ${msg}`);
8473
8851
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
8474
8852
  if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
8475
8853
  this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
@@ -8718,17 +9096,42 @@ async function withDeadline(p, ms) {
8718
9096
  }
8719
9097
  }
8720
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
+ }
8721
9127
  function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
8722
9128
  return {
8723
9129
  name: `${prefix}${spec.name}`,
8724
9130
  description: spec.description ?? `MCP tool ${spec.name}`,
8725
9131
  parameters: spec.inputSchema ?? { type: "object", properties: {} },
8726
9132
  maxDurationMs: timeoutMs,
8727
- async run(args, _ctx) {
8728
- const raw = await withDeadline(callTool(spec.name, args ?? {}), timeoutMs);
8729
- if (raw === TIMED_OUT) return mcpTimedOut(spec.name, timeoutMs);
8730
- const r = toResult(raw);
8731
- return r.images?.length ? r : r.text;
9133
+ run(args, ctx) {
9134
+ return runMcpCall(spec.name, (o) => callTool(spec.name, args ?? {}, o), ctx, timeoutMs);
8732
9135
  }
8733
9136
  };
8734
9137
  }
@@ -8769,13 +9172,10 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
8769
9172
  }
8770
9173
  },
8771
9174
  maxDurationMs: callTimeoutMs,
8772
- async run({ name, args }) {
9175
+ async run({ name, args }, ctx) {
8773
9176
  const n = String(name ?? "");
8774
9177
  if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
8775
- const raw = await withDeadline(callTool(n, args ?? {}), callTimeoutMs);
8776
- if (raw === TIMED_OUT) return mcpTimedOut(n, callTimeoutMs);
8777
- const r = toResult(raw);
8778
- return r.images?.length ? r : r.text;
9178
+ return runMcpCall(n, (o) => callTool(n, args ?? {}, o), ctx, callTimeoutMs);
8779
9179
  }
8780
9180
  };
8781
9181
  return [searchTool, callMcpTool];
@@ -8795,17 +9195,17 @@ function buildMcpCatalog(servers) {
8795
9195
  return { specs, routes };
8796
9196
  }
8797
9197
  function searchOverCatalog(servers, specs, routes, resolve5, options) {
8798
- const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
9198
+ const tools = specs.length ? makeMcpToolSearch(specs, (name, args, o) => {
8799
9199
  const r = routes.get(name);
8800
9200
  if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
8801
- return resolve5(r.server, r.rawName, args ?? {});
9201
+ return resolve5(r.server, r.rawName, args ?? {}, o);
8802
9202
  }, options) : [];
8803
9203
  return { tools, serverNames: servers, toolCount: specs.length };
8804
9204
  }
8805
9205
  function makeMcpToolSearchFromMounted(mounted, options) {
8806
9206
  const { specs, routes } = buildMcpCatalog(mounted);
8807
9207
  const byName = new Map(mounted.map((m) => [m.name, m]));
8808
- 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);
8809
9209
  }
8810
9210
 
8811
9211
  // src/index.ts
@@ -8818,9 +9218,20 @@ import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor a
8818
9218
  init_logging();
8819
9219
  import { spawn } from "child_process";
8820
9220
  import { createHash } from "crypto";
8821
- var log15 = forComponent("mcp");
9221
+ var log16 = forComponent("mcp");
8822
9222
  var PROTOCOL_VERSION = "2025-06-18";
8823
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" });
8824
9235
  var StdioTransport = class {
8825
9236
  constructor(spec) {
8826
9237
  this.spec = spec;
@@ -8837,7 +9248,7 @@ var StdioTransport = class {
8837
9248
  proc.stdout.setEncoding("utf8");
8838
9249
  proc.stdout.on("data", (chunk) => this.onData(chunk));
8839
9250
  proc.stderr.setEncoding("utf8");
8840
- proc.stderr.on("data", (chunk) => log15.debug(`[${command}] stderr:`, chunk.trimEnd()));
9251
+ proc.stderr.on("data", (chunk) => log16.debug(`[${command}] stderr:`, chunk.trimEnd()));
8841
9252
  proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
8842
9253
  proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
8843
9254
  }
@@ -8851,21 +9262,32 @@ var StdioTransport = class {
8851
9262
  try {
8852
9263
  this.dispatch(JSON.parse(line));
8853
9264
  } catch (e) {
8854
- log15.debug("dropping non-JSON line from MCP server:", line, e);
9265
+ log16.debug("dropping non-JSON line from MCP server:", line, e);
8855
9266
  }
8856
9267
  }
8857
9268
  }
8858
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
+ }
8859
9279
  if (msg?.id == null || !this.pending.has(msg.id)) return;
8860
9280
  const p = this.pending.get(msg.id);
8861
9281
  this.pending.delete(msg.id);
8862
9282
  clearTimeout(p.timer);
9283
+ p.done?.();
8863
9284
  if (msg.error) p.reject(new Error(msg.error?.message ?? JSON.stringify(msg.error)));
8864
9285
  else p.resolve(msg.result);
8865
9286
  }
8866
9287
  failAll(e) {
8867
9288
  for (const p of this.pending.values()) {
8868
9289
  clearTimeout(p.timer);
9290
+ p.done?.();
8869
9291
  p.reject(e);
8870
9292
  }
8871
9293
  this.pending.clear();
@@ -8874,19 +9296,31 @@ var StdioTransport = class {
8874
9296
  if (!this.proc?.stdin) throw new Error("MCP stdio transport not started");
8875
9297
  this.proc.stdin.write(JSON.stringify(msg) + "\n");
8876
9298
  }
8877
- request(method, params) {
9299
+ request(method, params, opts = {}) {
8878
9300
  const id = this.nextId++;
8879
- const timeoutMs = this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
9301
+ const timeoutMs = deadlineFor(method, this.spec, opts);
9302
+ const { signal, onProgress } = opts;
8880
9303
  return new Promise((resolve5, reject) => {
8881
- const timer = setTimeout(() => {
9304
+ if (signal?.aborted) return reject(abortError(signal));
9305
+ const timer = timeoutMs > 0 ? setTimeout(() => {
8882
9306
  this.pending.delete(id);
9307
+ done();
8883
9308
  reject(new Error(`MCP request "${method}" timed out after ${timeoutMs}ms`));
8884
- }, timeoutMs);
8885
- 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 });
8886
9319
  try {
8887
- this.write({ jsonrpc: "2.0", id, method, params });
9320
+ this.write({ jsonrpc: "2.0", id, method, params: onProgress ? withProgressToken(params, id) : params });
8888
9321
  } catch (e) {
8889
9322
  clearTimeout(timer);
9323
+ done();
8890
9324
  this.pending.delete(id);
8891
9325
  reject(e);
8892
9326
  }
@@ -8900,7 +9334,7 @@ var StdioTransport = class {
8900
9334
  try {
8901
9335
  this.proc?.stdin?.end();
8902
9336
  } catch (e) {
8903
- log15.debug("stdin end failed", e);
9337
+ log16.debug("stdin end failed", e);
8904
9338
  }
8905
9339
  this.proc?.kill();
8906
9340
  }
@@ -8927,13 +9361,24 @@ var HttpTransport = class {
8927
9361
  fetchImpl;
8928
9362
  async start() {
8929
9363
  }
8930
- request(method, params) {
8931
- 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
+ }
8932
9377
  }
8933
9378
  async notify(method, params) {
8934
- 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));
8935
9380
  }
8936
- async post(msg, isNotify) {
9381
+ async post(msg, isNotify, signal, onProgress) {
8937
9382
  const headers = {
8938
9383
  "content-type": "application/json",
8939
9384
  accept: "application/json, text/event-stream",
@@ -8946,7 +9391,7 @@ var HttpTransport = class {
8946
9391
  method: "POST",
8947
9392
  headers,
8948
9393
  body: JSON.stringify(msg),
8949
- signal: AbortSignal.timeout(this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS)
9394
+ signal
8950
9395
  });
8951
9396
  const sid = resp.headers.get("mcp-session-id");
8952
9397
  if (sid) this.sessionId = sid;
@@ -8954,25 +9399,63 @@ var HttpTransport = class {
8954
9399
  if (!resp.ok) throw new Error(`MCP HTTP ${resp.status} ${resp.statusText}`);
8955
9400
  if (isNotify) return;
8956
9401
  const ct = resp.headers.get("content-type") ?? "";
8957
- 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();
8958
9403
  if (data?.error) throw new Error(data.error?.message ?? JSON.stringify(data.error));
8959
9404
  return data?.result;
8960
9405
  }
8961
9406
  async close() {
8962
9407
  }
8963
9408
  };
8964
- function parseSseResponse(body) {
8965
- for (const line of body.split("\n")) {
8966
- const trimmed = line.trimStart();
8967
- if (!trimmed.startsWith("data:")) continue;
8968
- try {
8969
- const obj = JSON.parse(trimmed.slice(5).trim());
8970
- if (obj && (obj.result !== void 0 || obj.error !== void 0)) return obj;
8971
- } catch (e) {
8972
- 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;
8973
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
+ });
8974
9458
  }
8975
- return {};
8976
9459
  }
8977
9460
  var McpClient = class {
8978
9461
  constructor(transport, clientInfo = { name: "agentx", version: "0" }) {
@@ -8996,8 +9479,9 @@ var McpClient = class {
8996
9479
  const r = await this.transport.request("tools/list");
8997
9480
  return Array.isArray(r?.tools) ? r.tools : [];
8998
9481
  }
8999
- async callTool(name, args) {
9000
- 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);
9001
9485
  }
9002
9486
  async listResources() {
9003
9487
  const r = await this.transport.request("resources/list");
@@ -9011,7 +9495,7 @@ var McpClient = class {
9011
9495
  }
9012
9496
  };
9013
9497
  function buildTransport(cfg) {
9014
- 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 });
9015
9499
  }
9016
9500
  function withTimeout(p, ms, label) {
9017
9501
  if (!ms || ms <= 0) return p;
@@ -9033,11 +9517,11 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
9033
9517
  return await withTimeout((async () => {
9034
9518
  const init = await client.connect();
9035
9519
  const specs = await client.listTools();
9036
- 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}__`);
9037
9521
  return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
9038
9522
  })(), mountTimeoutMs, name);
9039
9523
  } catch (e) {
9040
- 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}`));
9041
9525
  throw e;
9042
9526
  }
9043
9527
  }
@@ -9048,15 +9532,15 @@ function validEntries(servers) {
9048
9532
  return Object.entries(servers).filter(([name, cfg]) => {
9049
9533
  if (!cfg || cfg.disabled) return false;
9050
9534
  if (!cfg.command && !cfg.url) {
9051
- 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`);
9052
9536
  return false;
9053
9537
  }
9054
9538
  return true;
9055
9539
  });
9056
9540
  }
9057
9541
  function logMountFailure(name, e) {
9058
- if (e instanceof McpAuthError) log15.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
9059
- 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}`);
9060
9544
  }
9061
9545
  async function mountMcpServers(servers = {}, opts = {}) {
9062
9546
  const entries = validEntries(servers);
@@ -9066,7 +9550,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
9066
9550
  const name = entries[i][0];
9067
9551
  if (r.status === "fulfilled") {
9068
9552
  out.push(r.value);
9069
- 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}` : ""}`);
9070
9554
  } else logMountFailure(name, r.reason);
9071
9555
  });
9072
9556
  return out;
@@ -9106,7 +9590,7 @@ var McpPool = class {
9106
9590
  const prev = this.warm.get(key);
9107
9591
  if (prev) {
9108
9592
  clearTimeout(prev.timer);
9109
- 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}`));
9110
9594
  }
9111
9595
  const e = { client, timer: void 0 };
9112
9596
  this.warm.set(key, e);
@@ -9123,7 +9607,7 @@ var McpPool = class {
9123
9607
  const e = this.warm.get(key);
9124
9608
  if (!e) return;
9125
9609
  this.warm.delete(key);
9126
- 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}`));
9127
9611
  }
9128
9612
  async closeAll() {
9129
9613
  for (const e of this.warm.values()) {
@@ -9341,7 +9825,7 @@ init_tools_shell();
9341
9825
  // src/tools.notify.ts
9342
9826
  init_logging();
9343
9827
  import { execFile } from "child_process";
9344
- var log17 = forComponent("notify");
9828
+ var log18 = forComponent("notify");
9345
9829
  function makeNotifyTool(opts = {}) {
9346
9830
  const platform2 = opts.platform ?? process.platform;
9347
9831
  const run = opts.exec ?? execFile;
@@ -9365,7 +9849,7 @@ function makeNotifyTool(opts = {}) {
9365
9849
  return new Promise((resolve5) => {
9366
9850
  run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
9367
9851
  if (e) {
9368
- log17.debug("notification failed", e);
9852
+ log18.debug("notification failed", e);
9369
9853
  resolve5(`Notification failed: ${e.message}`);
9370
9854
  } else resolve5("Notification shown.");
9371
9855
  });
@@ -9381,7 +9865,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
9381
9865
  init_logging();
9382
9866
  import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
9383
9867
  import { homedir } from "os";
9384
- var log18 = forComponent("cli-util");
9868
+ var log19 = forComponent("cli-util");
9385
9869
  function dotDirs(base, sub, opts = {}) {
9386
9870
  const home = opts.home ?? homedir();
9387
9871
  const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
@@ -9398,7 +9882,7 @@ function parseJson(text, fallback, what = "json") {
9398
9882
  try {
9399
9883
  return JSON.parse(text);
9400
9884
  } catch (e) {
9401
- log18.debug(`parseJson(${what}) failed: ${e.message}`);
9885
+ log19.debug(`parseJson(${what}) failed: ${e.message}`);
9402
9886
  return fallback;
9403
9887
  }
9404
9888
  }
@@ -9408,7 +9892,7 @@ function readJsonFile(path, fallback) {
9408
9892
  try {
9409
9893
  text = readFileSync4(path, "utf8");
9410
9894
  } catch (e) {
9411
- log18.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
9895
+ log19.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
9412
9896
  return fallback;
9413
9897
  }
9414
9898
  return parseJson(text, fallback, path);
@@ -9463,9 +9947,9 @@ function toCursorMcp(servers) {
9463
9947
  }
9464
9948
  return Object.keys(out).length ? { mcpServers: out } : void 0;
9465
9949
  }
9466
- function cursorProviderOptions(model, cwd, mcpServers) {
9950
+ function cursorProviderOptions(model, cwd, mcpServers, hostMcpServers) {
9467
9951
  if (!(model ?? "").startsWith("cursor/")) return void 0;
9468
- 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() } };
9469
9953
  }
9470
9954
  function toClaudeCodeMcp(servers) {
9471
9955
  if (!servers) return void 0;
@@ -9492,8 +9976,8 @@ function claudeCodeProviderOptions(model, cwd, mcpServers) {
9492
9976
  const warm = process.env.AGENTX_CC_WARM === "0" ? {} : { claudeCodeSession: randomUUID() };
9493
9977
  return { cwd, ...env ? { env } : {}, ...warm, ...toClaudeCodeMcp(mcpServers) ?? {} };
9494
9978
  }
9495
- function providerOptionsFor(model, cwd, mcpServers) {
9496
- 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);
9497
9981
  }
9498
9982
  async function buildAgent(o) {
9499
9983
  if (typeof o.sandbox !== "boolean")
@@ -9578,12 +10062,14 @@ Reference files in them by their mount path (the left side).`;
9578
10062
  const hooks = o.learnFromMistakes && memoryWriteDir ? composeHooks(o.hooks, lessonCapture({ fs, dir: memoryWriteDir, minRepeats: 2 })) : o.hooks;
9579
10063
  let realShell = [];
9580
10064
  const agentRef = {};
10065
+ const jobRegistry = new JobRegistry();
9581
10066
  const useRealShell = o.realShell ?? !virtual;
9582
10067
  if (useRealShell && !virtual && !isClaudeCode) {
9583
10068
  const jobs = new ShellJobRegistry({
9584
10069
  cwd,
9585
10070
  killOnExit: true,
9586
10071
  osSandbox: o.osSandbox,
10072
+ jobs: jobRegistry,
9587
10073
  onExit: (n) => agentRef.agent?.inject(formatJobExit(n))
9588
10074
  });
9589
10075
  realShell = [makeRealShellTool({ cwd, registry: jobs, osSandbox: o.osSandbox, jobsSurviveRun: o.singleRun !== true }), ...makeShellJobTools(jobs)];
@@ -9592,6 +10078,7 @@ Reference files in them by their mount path (the left side).`;
9592
10078
  const scratch = scratchDir ? new Scratch(fs, { dir: scratchDir }) : void 0;
9593
10079
  return agentRef.agent = new Agent({
9594
10080
  ai: o.ai,
10081
+ jobs: jobRegistry,
9595
10082
  fs,
9596
10083
  model: o.model ?? "anthropic/claude-sonnet-4-6",
9597
10084
  // PDF reads (disk mode only — VFS paths aren't real files): poppler's pdftotext when installed.
@@ -9603,7 +10090,7 @@ Reference files in them by their mount path (the left side).`;
9603
10090
  // Warm cursor session (default on; CURSOR_WARM=0 to disable): one long-lived agent reused across
9604
10091
  // turns, amortising the ~2s Agent.create + h2 handshake. Keyed per agentx launch.
9605
10092
  ...(() => {
9606
- const po = providerOptionsFor(o.model, cwd, o.mcpServers);
10093
+ const po = providerOptionsFor(o.model, cwd, o.mcpServers, o.hostMcpServers);
9607
10094
  return po ? { providerOptions: po } : {};
9608
10095
  })(),
9609
10096
  ...(() => {
@@ -9704,7 +10191,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
9704
10191
  import { homedir as homedir3 } from "os";
9705
10192
  import { dirname as dirname3, join as join6 } from "path";
9706
10193
  import { fileURLToPath } from "url";
9707
- var log19 = forComponent("VoiceIO");
10194
+ var log20 = forComponent("VoiceIO");
9708
10195
  var now3 = () => performance.now();
9709
10196
  var Player = class {
9710
10197
  proc = null;
@@ -9718,7 +10205,7 @@ var Player = class {
9718
10205
  ["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
9719
10206
  { stdio: ["pipe", "ignore", "ignore"] }
9720
10207
  );
9721
- this.proc.on("error", (e) => log19.warn(`ffplay error: ${e.message}`));
10208
+ this.proc.on("error", (e) => log20.warn(`ffplay error: ${e.message}`));
9722
10209
  this.proc.stdin.on("error", () => {
9723
10210
  });
9724
10211
  this.bytesWritten = 0;
@@ -9756,7 +10243,7 @@ function detectFfmpegMic() {
9756
10243
  const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
9757
10244
  const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
9758
10245
  if (!mic) throw new Error("no audio input device found");
9759
- log19.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
10246
+ log20.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
9760
10247
  return `:${mic.idx}`;
9761
10248
  }
9762
10249
  function detectedInputDevice() {
@@ -9792,15 +10279,15 @@ function resolveAecBinary() {
9792
10279
  if (existsSync4(bin) && statSync4(bin).mtimeMs >= statSync4(src).mtimeMs) return bin;
9793
10280
  if (spawnSync2("which", ["swiftc"]).status !== 0) return null;
9794
10281
  mkdirSync5(cacheDir, { recursive: true });
9795
- log19.info("compiling AEC mic helper (first run)\u2026");
10282
+ log20.info("compiling AEC mic helper (first run)\u2026");
9796
10283
  const build = spawnSync2("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
9797
10284
  if (build.status !== 0) {
9798
- log19.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
10285
+ log20.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
9799
10286
  return null;
9800
10287
  }
9801
10288
  const sign = spawnSync2("codesign", ["-fs", "-", bin], { encoding: "utf8" });
9802
10289
  if (sign.status !== 0) {
9803
- log19.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
10290
+ log20.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
9804
10291
  return null;
9805
10292
  }
9806
10293
  return bin;
@@ -9842,16 +10329,16 @@ var NodeMicSource = class {
9842
10329
  this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
9843
10330
  } else {
9844
10331
  if (spawnSync2("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
9845
- 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");
9846
10333
  this.proc = spawn2(
9847
10334
  "ffmpeg",
9848
10335
  ["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
9849
10336
  { stdio: ["ignore", "pipe", "pipe"] }
9850
10337
  );
9851
- 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()}`));
9852
10339
  }
9853
10340
  this.proc.on("exit", (c) => {
9854
- 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`);
9855
10342
  });
9856
10343
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
9857
10344
  }
@@ -9917,7 +10404,7 @@ var AecDuplexAudio = class {
9917
10404
  this.proc.stdin.on("error", () => {
9918
10405
  });
9919
10406
  this.proc.on("exit", (c) => {
9920
- 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`);
9921
10408
  });
9922
10409
  this.proc.stdout.on("data", (chunk) => {
9923
10410
  this.gotChunk = true;
@@ -9933,7 +10420,7 @@ var AecDuplexAudio = class {
9933
10420
  openMicSettings();
9934
10421
  this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
9935
10422
  }
9936
- } else log19.debug(`mic-aec: ${s}`);
10423
+ } else log20.debug(`mic-aec: ${s}`);
9937
10424
  }
9938
10425
  });
9939
10426
  if (!this.noVpio && !this.triedFallback) {
@@ -9942,7 +10429,7 @@ var AecDuplexAudio = class {
9942
10429
  this.triedFallback = true;
9943
10430
  this.noVpio = true;
9944
10431
  this._aec = false;
9945
- 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)");
9946
10433
  this.onDegrade?.();
9947
10434
  this.killProc();
9948
10435
  this.spawnHelper();
@@ -10162,7 +10649,7 @@ function fakeVoiceParts(uttFile) {
10162
10649
  init_logging();
10163
10650
  import { appendFile, mkdirSync as mkdirSync6 } from "fs";
10164
10651
  import { dirname as dirname4 } from "path";
10165
- var log20 = forComponent("VoiceDiag");
10652
+ var log21 = forComponent("VoiceDiag");
10166
10653
  var JsonlDiagSink = class {
10167
10654
  constructor(path) {
10168
10655
  this.path = path;
@@ -10203,7 +10690,7 @@ var JsonlDiagSink = class {
10203
10690
  disable(e) {
10204
10691
  this.disabled = true;
10205
10692
  this.queue.length = 0;
10206
- 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}`);
10207
10694
  }
10208
10695
  /** True once a failure permanently disabled the sink (tests/inspection). */
10209
10696
  get isDisabled() {
@@ -10273,7 +10760,7 @@ async function loadConfig(cwd) {
10273
10760
 
10274
10761
  // cli/hooks-config.ts
10275
10762
  import { spawnSync as spawnSync3 } from "child_process";
10276
- var log21 = forComponent("hooks");
10763
+ var log22 = forComponent("hooks");
10277
10764
  var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
10278
10765
  function ruleMatches(rule, toolName) {
10279
10766
  if (!rule.tool || rule.tool === "*") return true;
@@ -10290,7 +10777,7 @@ function runCmd(rule, env) {
10290
10777
  });
10291
10778
  return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
10292
10779
  } catch (e) {
10293
- log21.debug(`hook command failed: ${rule.command}`, e);
10780
+ log22.debug(`hook command failed: ${rule.command}`, e);
10294
10781
  return { code: 1, out: String(e?.message ?? e) };
10295
10782
  }
10296
10783
  }
@@ -10397,7 +10884,7 @@ function formatDiff(ops, opts = {}) {
10397
10884
  import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync } from "fs";
10398
10885
  import { homedir as homedir5 } from "os";
10399
10886
  import { join as join8 } from "path";
10400
- var log22 = forComponent("session");
10887
+ var log23 = forComponent("session");
10401
10888
  var globalDir = () => join8(homedir5(), ".agent", "sessions");
10402
10889
  var SessionStore = class {
10403
10890
  dir;
@@ -10442,7 +10929,7 @@ var SessionStore = class {
10442
10929
  }
10443
10930
  load(id) {
10444
10931
  if (!this.safeId(id)) {
10445
- log22.debug(`rejecting unsafe session id: ${id}`);
10932
+ log23.debug(`rejecting unsafe session id: ${id}`);
10446
10933
  return void 0;
10447
10934
  }
10448
10935
  const path = join8(this.dir, `${id}.json`);
@@ -10450,7 +10937,7 @@ var SessionStore = class {
10450
10937
  try {
10451
10938
  return JSON.parse(readFileSync7(path, "utf8"));
10452
10939
  } catch (e) {
10453
- log22.debug(`unreadable session ${id} \u2014 ignoring`, e);
10940
+ log23.debug(`unreadable session ${id} \u2014 ignoring`, e);
10454
10941
  return void 0;
10455
10942
  }
10456
10943
  }
@@ -10463,7 +10950,7 @@ var SessionStore = class {
10463
10950
  try {
10464
10951
  metas.push(JSON.parse(readFileSync7(join8(this.dir, f), "utf8")).meta);
10465
10952
  } catch (e) {
10466
- log22.debug(`skipping unreadable session file ${f}`, e);
10953
+ log23.debug(`skipping unreadable session file ${f}`, e);
10467
10954
  }
10468
10955
  }
10469
10956
  return metas.sort((a, b) => b.updated - a.updated);
@@ -10632,7 +11119,7 @@ import { promisify } from "util";
10632
11119
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync8 } from "fs";
10633
11120
  import { homedir as homedir6 } from "os";
10634
11121
  import { join as join9, resolve as resolve3, sep as sep3, parse as parsePath } from "path";
10635
- var log23 = forComponent("checkpoints");
11122
+ var log24 = forComponent("checkpoints");
10636
11123
  var exec = promisify(execFile3);
10637
11124
  var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
10638
11125
  function isBroadRoot(p) {
@@ -10678,7 +11165,7 @@ var ShadowRepo = class {
10678
11165
  writeFileSync5(join9(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
10679
11166
  this.ready = true;
10680
11167
  } catch (e) {
10681
- log23.debug(`git checkpoints unavailable for ${this.workTree}`, e);
11168
+ log24.debug(`git checkpoints unavailable for ${this.workTree}`, e);
10682
11169
  this.ready = false;
10683
11170
  }
10684
11171
  return this.ready;
@@ -10689,7 +11176,7 @@ var ShadowRepo = class {
10689
11176
  }
10690
11177
  async commit(label, forced = []) {
10691
11178
  await this.run("add", "-A");
10692
- 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));
10693
11180
  await this.run("commit", "--allow-empty", "-q", "-m", label);
10694
11181
  }
10695
11182
  /** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
@@ -10697,8 +11184,8 @@ var ShadowRepo = class {
10697
11184
  * turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
10698
11185
  * point per turn, so the REPL's turn↔frame mapping stays intact. */
10699
11186
  async amendForced(paths) {
10700
- for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log23.debug(`force-capture failed: ${p}`, e));
10701
- 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));
10702
11189
  }
10703
11190
  /** Commits on `ref`, oldest-first (canonical index space). */
10704
11191
  async log(ref) {
@@ -10758,7 +11245,7 @@ var ShadowRepo = class {
10758
11245
  await this.run("gc", "--auto", "-q").catch(() => {
10759
11246
  });
10760
11247
  } catch (e) {
10761
- log23.debug("checkpoint prune failed", e);
11248
+ log24.debug("checkpoint prune failed", e);
10762
11249
  }
10763
11250
  }
10764
11251
  };
@@ -10826,7 +11313,7 @@ var GitCheckpoints = class {
10826
11313
  use(sessionId) {
10827
11314
  if (sessionId === this.session) return;
10828
11315
  this.session = sessionId;
10829
- 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));
10830
11317
  }
10831
11318
  async begin(label) {
10832
11319
  if (!await this.start()) return;
@@ -10846,7 +11333,7 @@ var GitCheckpoints = class {
10846
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
10847
11334
  `);
10848
11335
  } else {
10849
- log23.debug("checkpoint commit failed", e);
11336
+ log24.debug("checkpoint commit failed", e);
10850
11337
  survivors.push(this.repos[i]);
10851
11338
  survivorCaches.push(this.caches[i] ?? []);
10852
11339
  }
@@ -10881,7 +11368,7 @@ var GitCheckpoints = class {
10881
11368
  if (this.forced.has(abs)) continue;
10882
11369
  this.forced.add(abs);
10883
11370
  const i = this.repoIndexFor(abs);
10884
- 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));
10885
11372
  }
10886
11373
  }
10887
11374
  };
@@ -12704,7 +13191,7 @@ import { spawnSync as spawnSync5 } from "child_process";
12704
13191
  import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
12705
13192
  import { homedir as homedir8 } from "os";
12706
13193
  import { join as join12 } from "path";
12707
- var log24 = forComponent("os-sched");
13194
+ var log25 = forComponent("os-sched");
12708
13195
  var OsScheduler = class {
12709
13196
  options;
12710
13197
  constructor(options) {
@@ -12766,7 +13253,7 @@ var OsScheduler = class {
12766
13253
  }
12767
13254
  }
12768
13255
  } catch (e) {
12769
- log24.debug(`cancel ${id}`, e);
13256
+ log25.debug(`cancel ${id}`, e);
12770
13257
  }
12771
13258
  for (const f of [`${id}.json`, `${id}.sh`]) {
12772
13259
  try {
@@ -12883,7 +13370,7 @@ import { spawn as spawn3 } from "child_process";
12883
13370
  import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
12884
13371
  import { homedir as homedir9 } from "os";
12885
13372
  import { join as join13 } from "path";
12886
- var log25 = forComponent("remote-trigger");
13373
+ var log26 = forComponent("remote-trigger");
12887
13374
  var TRIGGER_DIR = () => join13(homedir9(), ".agent", "triggers");
12888
13375
  var sockPath = (sessionId, dir = TRIGGER_DIR()) => join13(dir, `${sessionId}.sock`);
12889
13376
  var TriggerServer = class {
@@ -12921,13 +13408,13 @@ var TriggerServer = class {
12921
13408
  conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
12922
13409
  }
12923
13410
  });
12924
- conn.on("error", (e) => log25.debug("trigger conn error", e));
13411
+ conn.on("error", (e) => log26.debug("trigger conn error", e));
12925
13412
  });
12926
- this.server.on("error", (e) => log25.debug("trigger server error", e));
13413
+ this.server.on("error", (e) => log26.debug("trigger server error", e));
12927
13414
  this.server.listen(p);
12928
13415
  this.path = p;
12929
13416
  } catch (e) {
12930
- log25.debug("trigger server unavailable", e);
13417
+ log26.debug("trigger server unavailable", e);
12931
13418
  }
12932
13419
  }
12933
13420
  /** Re-bind on /resume (the session id changed). */
@@ -13049,7 +13536,7 @@ var strike = C("9");
13049
13536
  var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
13050
13537
  var mdPalette = { bold, dim, cyan, italic, strike, green, link };
13051
13538
  var err = (s) => process.stderr.write(s);
13052
- var log26 = forComponent("cli");
13539
+ var log27 = forComponent("cli");
13053
13540
  var VERSION = (() => {
13054
13541
  try {
13055
13542
  return JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
@@ -13099,9 +13586,9 @@ var spinner = /* @__PURE__ */ (() => {
13099
13586
  if (!tty || timer) return;
13100
13587
  t0 = this.turnStart || Date.now();
13101
13588
  timer = setInterval(() => {
13102
- const secs = Math.round((Date.now() - t0) / 1e3);
13589
+ const secs2 = Math.round((Date.now() - t0) / 1e3);
13103
13590
  const tail = this.tail?.();
13104
- 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 : ""));
13105
13592
  }, 90);
13106
13593
  },
13107
13594
  stop() {
@@ -13794,7 +14281,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
13794
14281
  function turnCost(model, usage) {
13795
14282
  return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
13796
14283
  }
13797
- async function evaluateGoal(ai, condition, transcript, log27) {
14284
+ async function evaluateGoal(ai, condition, transcript, log28) {
13798
14285
  const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
13799
14286
  const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
13800
14287
  return text.slice(0, 600);
@@ -13814,7 +14301,7 @@ ${recent}` }
13814
14301
  const match = r.content.match(/\{[\s\S]*\}/);
13815
14302
  if (match) return JSON.parse(match[0]);
13816
14303
  } catch (e) {
13817
- log27(dim(` (goal evaluator error: ${e?.message ?? e})
14304
+ log28(dim(` (goal evaluator error: ${e?.message ?? e})
13818
14305
  `));
13819
14306
  }
13820
14307
  return { met: false, reason: "evaluation unclear" };
@@ -13982,7 +14469,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
13982
14469
  anthropicWebSearch: cfg.anthropicWebSearch
13983
14470
  };
13984
14471
  }
13985
- async function makeAgent(args, ai, cfg, extraTools = []) {
14472
+ async function makeAgent(args, ai, cfg, extraTools = [], hostMcpServers) {
13986
14473
  const virtual = args.vfs || !!args.boddb;
13987
14474
  if (virtual && args.shell) err(yellow(` \u26A0 --shell ignored in ${args.boddb ? "--boddb" : "--vfs sandbox"} (a real shell would escape to disk)
13988
14475
  `));
@@ -13996,7 +14483,7 @@ async function makeAgent(args, ai, cfg, extraTools = []) {
13996
14483
  else if (args.shell === false) err(dim(" \u2296 --no-shell: VFS bash only (no real /bin/sh)\n"));
13997
14484
  if (args.harden && !virtual) err(dim(` \u26E8 hardened shell: writes confined to cwd+tmp${args.hardenNet ? "" : ", network blocked"} (sandbox-exec/bwrap)
13998
14485
  `));
13999
- const opts = optsFor(args, ai, cfg, extraTools);
14486
+ const opts = { ...optsFor(args, ai, cfg, extraTools), hostMcpServers };
14000
14487
  const agent = await buildAgent(opts);
14001
14488
  const display = displayHooks(agent.options.fs, { flush: agent.options.host?.flushText });
14002
14489
  agent.options.hooks = cfg.hooks ? composeHooks(display, hooksFromConfig(cfg.hooks)) : display;
@@ -14035,7 +14522,7 @@ function mcpAgentTools(mounted, opts) {
14035
14522
  return tools;
14036
14523
  }
14037
14524
  async function closeMcp(mounted) {
14038
- 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))));
14039
14526
  }
14040
14527
  var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
14041
14528
  function mentionRefs(line) {
@@ -14087,7 +14574,7 @@ async function expandMentions(fs, line) {
14087
14574
  if (loaded.includes(ref) || missing.includes(ref)) continue;
14088
14575
  if (ref.includes(":") && mcpMentionResolver) {
14089
14576
  const body = await mcpMentionResolver(ref).catch((e) => {
14090
- log26.debug("mcp mention resolve failed", e);
14577
+ log27.debug("mcp mention resolve failed", e);
14091
14578
  return null;
14092
14579
  });
14093
14580
  if (body != null) {
@@ -14174,7 +14661,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
14174
14661
  try {
14175
14662
  store.save(session);
14176
14663
  } catch (ex) {
14177
- log26.debug("mid-turn session flush failed", ex);
14664
+ log27.debug("mid-turn session flush failed", ex);
14178
14665
  }
14179
14666
  }
14180
14667
  return origNotify(e);
@@ -14201,7 +14688,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
14201
14688
  }
14202
14689
  agent.options.host?.flushText?.();
14203
14690
  flushLookupGroup();
14204
- const secs = ((Date.now() - t0) / 1e3).toFixed(1);
14691
+ const secs2 = ((Date.now() - t0) / 1e3).toFixed(1);
14205
14692
  const cost = turnCost(agent.options.model, res.usage);
14206
14693
  const m = res.usageEstimated ? "~" : "";
14207
14694
  const tok = res.usage?.totalTokens ? `${m}${(res.usage.totalTokens / 1e3).toFixed(1)}k tok \xB7 ${cost > 0 ? m + fmtUsd(cost) + " \xB7 " : ""}` : "";
@@ -14212,7 +14699,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
14212
14699
  const didWork = agent.transcript.slice(msgsBefore).some((m2) => m2.role === "assistant" || m2.role === "tool");
14213
14700
  const silentAbort = res.finishReason === "aborted" && !res.usage?.totalTokens && !didWork;
14214
14701
  if (!silentAbort)
14215
- 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}
14216
14703
  `));
14217
14704
  if (!silentAbort && process.stderr.isTTY && Date.now() - t0 > 1e4) err("\x07");
14218
14705
  if (res.finishReason === "error" && res.error) {
@@ -14323,14 +14810,14 @@ var isCancelTeardown = (e) => {
14323
14810
  function installCancelGuards(mounted) {
14324
14811
  process.on("unhandledRejection", (e) => {
14325
14812
  if (isCancelTeardown(e)) {
14326
- log26.debug("suppressed unhandledRejection (cursor stream cancel)", e);
14813
+ log27.debug("suppressed unhandledRejection (cursor stream cancel)", e);
14327
14814
  return;
14328
14815
  }
14329
- log26.error("unhandledRejection", e);
14816
+ log27.error("unhandledRejection", e);
14330
14817
  });
14331
14818
  process.on("uncaughtException", (e) => {
14332
14819
  if (isCancelTeardown(e)) {
14333
- log26.debug("suppressed uncaughtException (cursor stream cancel)", e);
14820
+ log27.debug("suppressed uncaughtException (cursor stream cancel)", e);
14334
14821
  return;
14335
14822
  }
14336
14823
  console.error(e);
@@ -14344,7 +14831,7 @@ async function repl(args, ai, cfg, cwd) {
14344
14831
  let mcpToolNames = [];
14345
14832
  const initialMcpTools = mcpAgentTools(mounted);
14346
14833
  mcpToolNames = initialMcpTools.map((t) => t.name);
14347
- const agent = await makeAgent(args, ai, cfg, initialMcpTools);
14834
+ const agent = await makeAgent(args, ai, cfg, initialMcpTools, mounted.map((m) => m.name));
14348
14835
  if (args.voice && !args.duplex) agent.options.tools = [...agent.options.tools ?? [], exitSessionTool(() => {
14349
14836
  exitRequested = true;
14350
14837
  })];
@@ -14586,7 +15073,7 @@ async function repl(args, ai, cfg, cwd) {
14586
15073
  ...args.voiceModel ?? cfg.reflexModel ? { reflexModel: resolveModelOrNewest(args.voiceModel ?? cfg.reflexModel) } : {},
14587
15074
  actModel: agent.options.model,
14588
15075
  actOptions: workerOptions,
14589
- providerOptionsFor: (m) => providerOptionsFor(m, cwd, cfg.mcpServers),
15076
+ providerOptionsFor: (m) => providerOptionsFor(m, cwd, cfg.mcpServers, mounted.map((x) => x.name)),
14590
15077
  ...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
14591
15078
  host,
14592
15079
  ...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true, emotionTags: emotionsOn } : {},
@@ -14668,7 +15155,7 @@ async function repl(args, ai, cfg, cwd) {
14668
15155
  const setModel = (m) => {
14669
15156
  work.model = m;
14670
15157
  work.contextWindow = getModelInfo(m)?.contextWindow ?? 0;
14671
- work.providerOptions = providerOptionsFor(m, cwd, cfg.mcpServers);
15158
+ work.providerOptions = providerOptionsFor(m, cwd, cfg.mcpServers, mounted.map((x) => x.name));
14672
15159
  if (dx) dx.options.actModel = m;
14673
15160
  persistModel(cwd, m);
14674
15161
  err(dim(" model \u2192 " + m + "\n"));
@@ -14735,7 +15222,7 @@ async function repl(args, ai, cfg, cwd) {
14735
15222
  try {
14736
15223
  mkdirSync12(dir, { recursive: true });
14737
15224
  } catch (e) {
14738
- log26.debug("paste image dir create failed", e);
15225
+ log27.debug("paste image dir create failed", e);
14739
15226
  }
14740
15227
  const img = grabClipboardImage(dir, String(Date.now()));
14741
15228
  return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
@@ -14848,7 +15335,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
14848
15335
  mkdirSync12(join14(cwd, ".agent"), { recursive: true });
14849
15336
  appendFileSync(histPath, line + "\n");
14850
15337
  } catch (e) {
14851
- log26.debug("history write failed", e);
15338
+ log27.debug("history write failed", e);
14852
15339
  }
14853
15340
  };
14854
15341
  const ago = (t) => {
@@ -14920,7 +15407,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
14920
15407
  try {
14921
15408
  store.save(session);
14922
15409
  } catch (e) {
14923
- log26.debug("session save after rewind failed", e);
15410
+ log27.debug("session save after rewind failed", e);
14924
15411
  }
14925
15412
  err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
14926
15413
  `));
@@ -14954,7 +15441,7 @@ ${task}`;
14954
15441
  bangContext.length = 0;
14955
15442
  }
14956
15443
  const delta = await refreshCatalogs().catch((e) => {
14957
- log26.debug("catalog refresh failed", e);
15444
+ log27.debug("catalog refresh failed", e);
14958
15445
  return "";
14959
15446
  });
14960
15447
  if (delta) {
@@ -15218,7 +15705,7 @@ ${task}`;
15218
15705
  desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
15219
15706
  run: async () => {
15220
15707
  await refreshCatalogs().catch((e) => {
15221
- log26.debug("catalog refresh failed", e);
15708
+ log27.debug("catalog refresh failed", e);
15222
15709
  });
15223
15710
  face.reprepare();
15224
15711
  err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
@@ -15720,7 +16207,7 @@ ${task}`;
15720
16207
  try {
15721
16208
  for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
15722
16209
  } catch (e) {
15723
- log26.debug(`loadAgents(${d}) failed`, e);
16210
+ log27.debug(`loadAgents(${d}) failed`, e);
15724
16211
  }
15725
16212
  }
15726
16213
  if (!seen.size) {
@@ -15808,7 +16295,7 @@ ${task}`;
15808
16295
  }
15809
16296
  if (idx >= 0) {
15810
16297
  const old = mounted.splice(idx, 1)[0];
15811
- 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));
15812
16299
  }
15813
16300
  try {
15814
16301
  const m = await mountMcpServer(name, conf);
@@ -15836,7 +16323,7 @@ ${task}`;
15836
16323
  }
15837
16324
  const m = mounted.splice(idx, 1)[0];
15838
16325
  remountMcpTools();
15839
- 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));
15840
16327
  err(dim(` removed "${name}"
15841
16328
  `));
15842
16329
  return;
@@ -16039,16 +16526,16 @@ ${task}`;
16039
16526
  try {
16040
16527
  return readdirSync4(join14(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
16041
16528
  } catch (e) {
16042
- log26.debug("completion readdir failed", absDir, e);
16529
+ log27.debug("completion readdir failed", absDir, e);
16043
16530
  return null;
16044
16531
  }
16045
16532
  };
16046
16533
  const agentCommandNames = () => [...cmds.map((c) => c.name), ...skills.map((s) => s.name)];
16047
16534
  const commandNames = () => [...Object.keys(builtins).filter((k) => k !== "quit"), ...agentCommandNames()];
16048
- 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;
16049
16536
  const suggest = (lineToCursor) => {
16050
16537
  const [hits, token] = completeLine(lineToCursor, { commands: commandNames(), agentCommands: agentCommandNames(), listDir, bangHistory: history });
16051
- return { hits, token, describe };
16538
+ return { hits, token, describe: describe2 };
16052
16539
  };
16053
16540
  const editor = createLineEditor(process.stderr);
16054
16541
  editorRef = editor;
@@ -16175,7 +16662,7 @@ ${out}
16175
16662
  return;
16176
16663
  }
16177
16664
  await refreshCatalogs().catch((e) => {
16178
- log26.debug("catalog refresh failed", e);
16665
+ log27.debug("catalog refresh failed", e);
16179
16666
  });
16180
16667
  const sk = skills.find((x) => x.name === name);
16181
16668
  if (sk) {
@@ -16663,7 +17150,7 @@ async function main() {
16663
17150
  });
16664
17151
  if (args.task) {
16665
17152
  const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join14(cwd, ".agent", "mcp-auth.json") }));
16666
- const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted));
17153
+ const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted), mounted.map((m) => m.name));
16667
17154
  const store = new SessionStore(cwd);
16668
17155
  const session = startSession(args, store, agent, cwd);
16669
17156
  process.once("SIGINT", () => activeTurn?.abort());