@livx.cc/agentx 0.99.47 → 0.99.49

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,399 @@ 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
+ * NOTE the asymmetry that default hides: for a HOST job silence really is uninformative (the caller holds
1582
+ * the pending dispatch promise — independent proof it is alive), but a NATIVE job has no such proof AND it
1583
+ * suppresses the stall watchdog, i.e. it turns the provider's own silence into permission to keep ignoring
1584
+ * that provider's silence. So Agent passes an explicit per-job `idleMs` (nativeHoldMs) for `native`: the
1585
+ * provider must keep re-proving liveness with `running` events or forfeit the hold. See Agent.nativeHoldMs. */
1586
+ idleMs = { shell: 10 * 6e4, mcp: 10 * 6e4 };
1587
+ /** Absolute ceiling per kind (0/absent = none). */
1588
+ hardCapMs = { mcp: 30 * 6e4 };
1589
+ /** Soft foreground timeout per kind: past it the caller stops waiting and gets a job id instead. */
1590
+ foregroundMs = { mcp: 3e4 };
1591
+ /** How often the reaper runs while reapable jobs exist. */
1592
+ sweepMs = 15e3;
1593
+ /** Grace between the kill signal and a forced kill (producers that escalate read this). */
1594
+ killGraceMs = 5e3;
1595
+ /** Finished jobs kept for inspection; older finished ones are pruned (every delegated tool call is a job). */
1596
+ keepFinished = 200;
1597
+ /** Clock (injectable for tests). Late-bound so a patched Date.now is honoured. */
1598
+ now = () => Date.now();
1599
+ /** Terminal-state notification for jobs with `notify` (the Agent wires this to `inject`). */
1600
+ onExit;
1601
+ };
1602
+ secs = (ms) => `${Math.round(ms / 1e3)}s`;
1603
+ JobRegistry = class {
1604
+ options;
1605
+ jobs = /* @__PURE__ */ new Map();
1606
+ seq = 0;
1607
+ timer;
1608
+ constructor(options) {
1609
+ this.options = { ...new JobRegistryOptions(), ...options };
1610
+ }
1611
+ /** Register externally-driven work (a process, a request, a native tool) and get a handle to feed it. */
1612
+ track(opts = {}) {
1613
+ const o = this.options;
1614
+ const kind = opts.kind ?? "task";
1615
+ const now4 = o.now();
1616
+ let settle;
1617
+ const done = new Promise((r) => {
1618
+ settle = r;
1619
+ });
1620
+ const job = {
1621
+ id: `job-${++this.seq}`,
1622
+ kind,
1623
+ label: opts.label ?? "",
1624
+ status: "running",
1625
+ startedAt: now4,
1626
+ lastActivityAt: now4,
1627
+ bytes: 0,
1628
+ holds: !!opts.holds,
1629
+ buf: "",
1630
+ dropped: 0,
1631
+ idleMs: opts.idleMs ?? o.idleMs[kind] ?? 0,
1632
+ hardCapMs: opts.hardCapMs ?? o.hardCapMs[kind] ?? 0,
1633
+ drain: !!opts.drain,
1634
+ notify: !!opts.notify,
1635
+ idleAfterActivity: !!opts.idleAfterActivity,
1636
+ active: false,
1637
+ cpuTime: opts.cpuTime,
1638
+ onKill: opts.onKill,
1639
+ onExit: opts.onExit,
1640
+ done,
1641
+ settle
1642
+ };
1643
+ this.jobs.set(job.id, job);
1644
+ if (opts.seed) this.append(job, opts.seed);
1645
+ this.arm();
1646
+ return {
1647
+ id: job.id,
1648
+ chunk: (s) => {
1649
+ if (s) {
1650
+ this.append(job, s);
1651
+ job.lastActivityAt = o.now();
1652
+ job.active = true;
1653
+ }
1654
+ },
1655
+ touch: () => {
1656
+ job.lastActivityAt = o.now();
1657
+ job.active = true;
1658
+ },
1659
+ finish: (status, result, reason) => this.end(job, status, result, reason),
1660
+ detach: () => {
1661
+ job.holds = false;
1662
+ job.notify = true;
1663
+ job.drain = true;
1664
+ },
1665
+ get status() {
1666
+ return job.status;
1667
+ }
1668
+ };
1669
+ }
1670
+ /** Start `fn` in the background and return a job id immediately (sandbox/subagent semantics: drained at turn end). */
1671
+ start(fn, opts = {}) {
1672
+ return this.run(fn, { drain: true, ...opts }).handle.id;
1673
+ }
1674
+ /**
1675
+ * Run `fn` in the FOREGROUND with a soft timeout: resolves `{done:true,value}` if it settles in time, else
1676
+ * `{done:false,id}` — the work keeps going as a detached job whose completion is reported via `onExit`.
1677
+ * A rejection before the timeout is rethrown (the caller's normal error path). `softTimeoutMs` ≤ 0 waits forever.
1678
+ */
1679
+ async runForeground(fn, opts = {}) {
1680
+ const { handle, promise } = this.run(fn, opts);
1681
+ const soft = opts.softTimeoutMs ?? this.options.foregroundMs[opts.kind ?? "task"] ?? 0;
1682
+ let t;
1683
+ const late = soft > 0 ? new Promise((r) => {
1684
+ t = setTimeout(() => r("late"), soft);
1685
+ }) : new Promise(() => {
1686
+ });
1687
+ try {
1688
+ const r = await Promise.race([promise.then((v) => ({ v }), (e) => ({ e })), late]);
1689
+ if (r !== "late" && handle.status !== "running") this.jobs.delete(handle.id);
1690
+ if (r === "late") {
1691
+ if (handle.status === "running") {
1692
+ handle.detach();
1693
+ return { done: false, id: handle.id };
1694
+ }
1695
+ const settled = await promise.then((v) => ({ v }), (e) => ({ e }));
1696
+ if ("e" in settled) throw settled.e;
1697
+ return { done: true, value: settled.v };
1698
+ }
1699
+ if ("e" in r) throw r.e;
1700
+ return { done: true, value: r.v };
1701
+ } finally {
1702
+ if (t) clearTimeout(t);
1703
+ }
1704
+ }
1705
+ run(fn, opts) {
1706
+ const ctl = new AbortController();
1707
+ const handle = this.track({ ...opts, onKill: (reason) => {
1708
+ ctl.abort(Object.assign(new Error(reason), { name: "AbortError", code: "job_killed" }));
1709
+ opts.onKill?.(reason);
1710
+ } });
1711
+ let promise;
1712
+ try {
1713
+ promise = fn({ signal: ctl.signal, onChunk: handle.chunk, touch: handle.touch });
1714
+ } catch (e) {
1715
+ promise = Promise.reject(e);
1716
+ }
1717
+ promise.then(
1718
+ (res) => {
1719
+ if (handle.status === "running" && typeof res === "string" && res) handle.chunk(res);
1720
+ handle.finish("done", res);
1721
+ },
1722
+ (e) => {
1723
+ if (handle.status === "running") handle.chunk(`
1724
+ [error] ${e?.message ?? e}`);
1725
+ handle.finish("error", void 0, e?.message ?? String(e));
1726
+ }
1727
+ );
1728
+ return { handle, promise };
1729
+ }
1730
+ append(job, s) {
1731
+ job.buf += s;
1732
+ const over = job.buf.length - this.options.maxBuffer;
1733
+ if (over > 0) {
1734
+ job.buf = job.buf.slice(over);
1735
+ job.dropped += over;
1736
+ }
1737
+ job.bytes = job.buf.length;
1738
+ }
1739
+ end(job, status, result, reason) {
1740
+ if (job.status !== "running") return;
1741
+ job.status = status;
1742
+ job.endedAt = this.options.now();
1743
+ job.result = result;
1744
+ if (reason && status !== "done") job.reason = reason;
1745
+ job.holds = false;
1746
+ job.settle();
1747
+ this.prune();
1748
+ const onExit = job.onExit ?? this.options.onExit;
1749
+ if (job.notify && onExit) {
1750
+ try {
1751
+ onExit({ id: job.id, kind: job.kind, label: job.label, status, reason: job.reason, tail: job.buf.slice(-4e3), result });
1752
+ } catch (e) {
1753
+ log5.warn(`job onExit handler threw for ${job.id}: ${e}`);
1754
+ }
1755
+ }
1756
+ }
1757
+ prune() {
1758
+ const finished = [...this.jobs.values()].filter((j) => j.status !== "running");
1759
+ for (const j of finished.slice(0, Math.max(0, finished.length - this.options.keepFinished))) this.jobs.delete(j.id);
1760
+ }
1761
+ /** Current tail output (null = no such job). */
1762
+ output(id) {
1763
+ return this.jobs.get(id)?.buf ?? null;
1764
+ }
1765
+ /** Incremental read: output from absolute char `offset` (clamped to what the ring still holds). */
1766
+ read(id, offset = 0) {
1767
+ const j = this.jobs.get(id);
1768
+ if (!j) return null;
1769
+ const start = Math.max(0, offset - j.dropped);
1770
+ return { text: j.buf.slice(start), next: j.dropped + j.buf.length, truncated: offset < j.dropped };
1771
+ }
1772
+ get(id) {
1773
+ const j = this.jobs.get(id);
1774
+ return j ? this.info(j) : null;
1775
+ }
1776
+ status(id) {
1777
+ const j = this.jobs.get(id);
1778
+ return j ? { status: j.status, bytes: j.bytes, ...j.reason ? { reason: j.reason } : {} } : null;
1779
+ }
1780
+ list() {
1781
+ return [...this.jobs.values()].map((j) => this.info(j));
1782
+ }
1783
+ info(j) {
1784
+ const { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result } = j;
1785
+ return { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result };
1786
+ }
1787
+ /** Wait (bounded) for a job to settle. Resolves the job's info either way; null for an unknown id. */
1788
+ async wait(id, timeoutMs) {
1789
+ const j = this.jobs.get(id);
1790
+ if (!j) return null;
1791
+ let t;
1792
+ await Promise.race([j.done, new Promise((r) => {
1793
+ t = setTimeout(r, Math.max(0, timeoutMs));
1794
+ })]);
1795
+ if (t) clearTimeout(t);
1796
+ return this.info(j);
1797
+ }
1798
+ /** Stop a job: runs its `onKill` (abort/SIGTERM) and marks it killed with `reason`. */
1799
+ kill(id, reason = "killed by request") {
1800
+ const j = this.jobs.get(id);
1801
+ if (!j) return false;
1802
+ if (j.status === "running") {
1803
+ this.end(j, "killed", void 0, reason);
1804
+ try {
1805
+ j.onKill?.(reason);
1806
+ } catch (e) {
1807
+ log5.warn(`onKill for ${id} threw: ${e}`);
1808
+ }
1809
+ }
1810
+ return true;
1811
+ }
1812
+ /** Kill every running job — or, with `onlyDrainable`, only turn-scoped ones (sandbox/sub-agent/handed-off MCP),
1813
+ * leaving jobs meant to outlive a run (a background dev server) alone. */
1814
+ killAll(reason = "run ended", onlyDrainable = false) {
1815
+ for (const j of this.jobs.values()) if (!onlyDrainable || j.drain) this.kill(j.id, reason);
1816
+ }
1817
+ /** Jobs something is synchronously waiting on (see `JobTrackOptions.holds`) that are still running. */
1818
+ holding() {
1819
+ return [...this.jobs.values()].filter((j) => j.status === "running" && j.holds).map((j) => this.info(j));
1820
+ }
1821
+ /** Await every drainable running job (turn end). Returns the ids that were still running when drain began. */
1822
+ async drain(signal) {
1823
+ const pending = [...this.jobs.values()].filter((j) => j.status === "running" && j.drain);
1824
+ const all = Promise.all(pending.map((j) => j.done));
1825
+ await (signal ? Promise.race([all, new Promise((r) => signal.aborted ? r() : signal.addEventListener("abort", () => r(), { once: true }))]) : all);
1826
+ return pending.map((j) => j.id);
1827
+ }
1828
+ /**
1829
+ * The stuck-job decision, synchronous part: kill every running job past its hard cap, or past its idle
1830
+ * window when it has no CPU probe to argue otherwise. Cheap — the stall watchdog calls it on every tick.
1831
+ * Returns the killed ids. Jobs WITH a probe are left to `sweep()`.
1832
+ */
1833
+ reap() {
1834
+ const now4 = this.options.now();
1835
+ const killed = [];
1836
+ for (const j of this.jobs.values()) {
1837
+ if (j.status !== "running") continue;
1838
+ const verdict = this.verdict(j, now4);
1839
+ if (!verdict || verdict.idle && j.cpuTime) continue;
1840
+ this.stuck(j, verdict.reason, now4);
1841
+ killed.push(j.id);
1842
+ }
1843
+ return killed;
1844
+ }
1845
+ /** Full sweep: sample CPU probes (advancing CPU = activity), then `reap()` including probed jobs. */
1846
+ async sweep() {
1847
+ const now4 = this.options.now();
1848
+ for (const j of [...this.jobs.values()]) {
1849
+ if (j.status !== "running" || !j.cpuTime || !j.idleMs) continue;
1850
+ let cpu;
1851
+ try {
1852
+ cpu = await j.cpuTime();
1853
+ } catch (e) {
1854
+ log5.debug(`cpu probe for ${j.id} failed: ${e}`);
1855
+ }
1856
+ if (cpu != null && j.lastCpu != null && cpu > j.lastCpu) j.lastActivityAt = this.options.now();
1857
+ const prev = j.lastCpu;
1858
+ if (cpu != null) j.lastCpu = cpu;
1859
+ const verdict = this.verdict(j, now4);
1860
+ if (!verdict?.idle || j.status !== "running") continue;
1861
+ if (cpu == null) {
1862
+ log5.debug(`not reaping ${j.id}: ${verdict.reason} but CPU unknown`);
1863
+ continue;
1864
+ }
1865
+ if (prev == null) continue;
1866
+ this.stuck(j, `${verdict.reason}, no CPU progress (${prev}s \u2192 ${cpu}s)`, now4);
1867
+ }
1868
+ return this.reap();
1869
+ }
1870
+ verdict(j, now4) {
1871
+ if (j.hardCapMs && now4 - j.startedAt >= j.hardCapMs) return { reason: `stuck: exceeded hard cap ${secs(j.hardCapMs)}`, idle: false };
1872
+ if (j.idleMs && (!j.idleAfterActivity || j.active) && now4 - j.lastActivityAt >= j.idleMs) return { reason: `stuck: idle ${secs(now4 - j.lastActivityAt)}`, idle: true };
1873
+ return null;
1874
+ }
1875
+ stuck(j, reason, now4) {
1876
+ 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`);
1877
+ if (j.onKill) j.notify = true;
1878
+ this.kill(j.id, reason);
1879
+ }
1880
+ /** Keep a reaper interval alive only while a reapable job is running (unref'd: never holds the process open). */
1881
+ arm() {
1882
+ if (this.timer || !this.options.sweepMs) return;
1883
+ this.timer = setInterval(() => {
1884
+ const live = [...this.jobs.values()].some((j) => j.status === "running" && (j.idleMs || j.hardCapMs));
1885
+ if (!live) {
1886
+ clearInterval(this.timer);
1887
+ this.timer = void 0;
1888
+ return;
1889
+ }
1890
+ void this.sweep().catch((e) => log5.warn(`job sweep failed: ${e}`));
1891
+ }, this.options.sweepMs);
1892
+ this.timer.unref?.();
1893
+ }
1894
+ /** Stop the reaper timer (tests / teardown). */
1895
+ dispose() {
1896
+ if (this.timer) clearInterval(this.timer);
1897
+ this.timer = void 0;
1898
+ }
1899
+ };
1900
+ NO_JOB = (id) => `Error: no background job '${id}'. Use JobStatus with no id to list jobs.`;
1901
+ 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)`;
1902
+ }
1903
+ });
1904
+
1512
1905
  // src/worktree.ts
1513
1906
  var worktree_exports = {};
1514
1907
  __export(worktree_exports, {
@@ -2118,7 +2511,10 @@ __export(tools_shell_exports, {
2118
2511
  ShellJobRegistry: () => ShellJobRegistry,
2119
2512
  formatJobExit: () => formatJobExit,
2120
2513
  makeRealShellTool: () => makeRealShellTool,
2121
- makeShellJobTools: () => makeShellJobTools
2514
+ makeShellJobTools: () => makeShellJobTools,
2515
+ parsePsTime: () => parsePsTime,
2516
+ psGroupCpuSeconds: () => psGroupCpuSeconds,
2517
+ sumTreeCpu: () => sumTreeCpu
2122
2518
  });
2123
2519
  function killGroup(proc, signal) {
2124
2520
  if (!proc?.pid) return false;
@@ -2148,10 +2544,39 @@ async function nodeSpawn() {
2148
2544
  return _spawn;
2149
2545
  }
2150
2546
  function formatJobExit(n) {
2151
- return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}] \`${n.command}\`
2547
+ return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}${n.reason ? `: ${n.reason}` : ""}] \`${n.command}\`
2152
2548
  ` + (n.tail ? `${n.tail}
2153
2549
  ` : "(no output)\n") + `Read the full output with ShellOutput({id:"${n.id}"}).`;
2154
2550
  }
2551
+ function parsePsTime(t) {
2552
+ const m = t.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/);
2553
+ if (!m) return void 0;
2554
+ return Number(m[1] ?? 0) * 86400 + Number(m[2] ?? 0) * 3600 + Number(m[3]) * 60 + Number(m[4]);
2555
+ }
2556
+ async function psGroupCpuSeconds(pgid) {
2557
+ try {
2558
+ const { execFile: execFile4 } = await import("child_process");
2559
+ 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))));
2560
+ return sumTreeCpu(out, pgid);
2561
+ } catch (e) {
2562
+ log17.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
2563
+ return void 0;
2564
+ }
2565
+ }
2566
+ function sumTreeCpu(psOut, root) {
2567
+ 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) }));
2568
+ const inJob = new Set(rows.filter((r) => r.pgid === root || r.pid === root).map((r) => r.pid));
2569
+ for (let grew = true; grew; ) {
2570
+ grew = false;
2571
+ for (const r of rows) if (!inJob.has(r.pid) && inJob.has(r.ppid)) {
2572
+ inJob.add(r.pid);
2573
+ grew = true;
2574
+ }
2575
+ }
2576
+ let total;
2577
+ for (const r of rows) if (inJob.has(r.pid) && r.sec != null) total = (total ?? 0) + r.sec;
2578
+ return total;
2579
+ }
2155
2580
  function makeRealShellTool(options) {
2156
2581
  const defaultTimeoutMs = options.timeoutMs ?? 12e4;
2157
2582
  const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 6e5, defaultTimeoutMs);
@@ -2282,7 +2707,7 @@ function makeRealShellTool(options) {
2282
2707
  proc.stderr?.on("data", collect);
2283
2708
  proc.on("error", (err2) => {
2284
2709
  if (err2?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
2285
- log16.debug("shell spawn error", err2);
2710
+ log17.debug("shell spawn error", err2);
2286
2711
  finish(`[exit 1] ${err2?.message ?? err2}${out ? "\n" + clean(out) : ""}`);
2287
2712
  });
2288
2713
  proc.on("close", (code) => {
@@ -2302,7 +2727,7 @@ function makeRealShellTool(options) {
2302
2727
  };
2303
2728
  }
2304
2729
  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.");
2730
+ 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
2731
  return body ? `${head}
2307
2732
  Output so far:
2308
2733
  ${body}` : head;
@@ -2326,7 +2751,7 @@ function makeShellJobTools(registry) {
2326
2751
  const out = registry.output(String(id));
2327
2752
  if (out == null) return NO_JOB2(String(id));
2328
2753
  const st = registry.status(String(id));
2329
- return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ""}]
2754
+ return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ""}${st.reason ? `: ${st.reason}` : ""}]
2330
2755
  ${clean(out) || "(no output yet)"}`;
2331
2756
  }
2332
2757
  },
@@ -2341,13 +2766,13 @@ ${clean(out) || "(no output yet)"}`;
2341
2766
  return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.command}`).join("\n") : "(no background jobs)";
2342
2767
  }
2343
2768
  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));
2769
+ 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
2770
  }
2346
2771
  },
2347
2772
  {
2348
2773
  ...dropOnRebind,
2349
2774
  name: "ShellKill",
2350
- description: "Stop a running background Shell job by id (SIGTERM).",
2775
+ description: "Stop a running background Shell job by id (SIGTERM to its process group, SIGKILL if it ignores that).",
2351
2776
  parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
2352
2777
  async run({ id }) {
2353
2778
  return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB2(String(id));
@@ -2355,7 +2780,7 @@ ${clean(out) || "(no output yet)"}`;
2355
2780
  }
2356
2781
  ];
2357
2782
  }
2358
- var log16, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
2783
+ var log17, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
2359
2784
  var init_tools_shell = __esm({
2360
2785
  "src/tools.shell.ts"() {
2361
2786
  "use strict";
@@ -2363,62 +2788,94 @@ var init_tools_shell = __esm({
2363
2788
  init_redact();
2364
2789
  init_logging();
2365
2790
  init_shell_sandbox();
2366
- log16 = forComponent("shell");
2791
+ init_tools_jobs();
2792
+ log17 = forComponent("shell");
2367
2793
  clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
2368
2794
  DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
2369
2795
  SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
2370
2796
  ShellJobRegistry = class {
2371
2797
  constructor(cfg) {
2372
2798
  this.cfg = cfg;
2799
+ this.jobs = cfg.jobs ?? new JobRegistry(cfg.maxBuffer ? { maxBuffer: cfg.maxBuffer } : {});
2373
2800
  if (cfg.killOnExit && typeof process !== "undefined") process.once("exit", () => this.killAll());
2374
2801
  }
2375
2802
  cfg;
2376
- jobs = /* @__PURE__ */ new Map();
2377
- seq = 0;
2803
+ jobs;
2804
+ meta = /* @__PURE__ */ new Map();
2378
2805
  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
- };
2806
+ const h = this.track(command, { idleMs: 0 });
2386
2807
  try {
2387
2808
  const spawn4 = this.cfg.spawn ?? await nodeSpawn();
2388
2809
  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
- });
2810
+ this.attach(h, spawn4(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED }));
2408
2811
  } catch (e) {
2409
- job.status = "error";
2410
- job.buf = `failed to spawn: ${e?.message ?? e}`;
2812
+ h.chunk(`failed to spawn: ${e?.message ?? e}`);
2813
+ h.finish("error", void 0, e?.message ?? String(e));
2814
+ }
2815
+ return h.id;
2816
+ }
2817
+ track(command, extra) {
2818
+ const meta = { command };
2819
+ let id = "";
2820
+ const h = this.jobs.track({
2821
+ kind: "shell",
2822
+ label: command,
2823
+ seed: extra.seed,
2824
+ idleMs: extra.idleMs,
2825
+ cpuTime: extra.cpu ? async () => meta.proc?.pid ? (this.cfg.cpuTime ?? psGroupCpuSeconds)(meta.proc.pid) : void 0 : void 0,
2826
+ onKill: () => this.signal(meta),
2827
+ onExit: (n) => this.notifyExit(id, meta, n.status, n.reason),
2828
+ notify: true
2829
+ });
2830
+ id = h.id;
2831
+ this.meta.set(id, meta);
2832
+ return h;
2833
+ }
2834
+ attach(h, proc) {
2835
+ const meta = this.meta.get(h.id);
2836
+ meta.proc = proc;
2837
+ const append = (chunk) => h.chunk(typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "");
2838
+ proc.stdout?.on("data", append);
2839
+ proc.stderr?.on("data", append);
2840
+ proc.on("error", (err2) => {
2841
+ if (h.status === "running") {
2842
+ append(`
2843
+ [error] ${err2?.message ?? err2}`);
2844
+ h.finish("error", void 0, err2?.message ?? String(err2));
2845
+ }
2846
+ });
2847
+ proc.on("close", (code) => {
2848
+ if (h.status === "running") {
2849
+ meta.exitCode = code ?? void 0;
2850
+ h.finish("done");
2851
+ }
2852
+ });
2853
+ }
2854
+ /** SIGTERM the whole group (bg children are detached group leaders — /bin/sh alone would orphan a forked
2855
+ * server), then SIGKILL after the grace period if it is still there. Falls back to the pid for fakes. */
2856
+ signal(meta) {
2857
+ const proc = meta.proc;
2858
+ if (!killGroup(proc, "SIGTERM")) {
2859
+ try {
2860
+ proc?.kill("SIGTERM");
2861
+ } catch {
2862
+ }
2411
2863
  }
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);
2864
+ if (!proc?.pid) return;
2865
+ let closed = false;
2866
+ proc.on("close", () => {
2867
+ closed = true;
2868
+ });
2869
+ const t = setTimeout(() => {
2870
+ if (!closed) killGroup(proc, "SIGKILL");
2871
+ }, this.jobs.options.killGraceMs);
2872
+ t.unref?.();
2873
+ }
2874
+ /** 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. */
2875
+ notifyExit(id, meta, status, reason) {
2876
+ if (!this.cfg.onExit || status === "killed" && !reason?.startsWith("stuck")) return;
2420
2877
  try {
2421
- this.cfg.onExit({ id, command: job.command, status: job.status, exitCode: job.exitCode, tail: clean(job.buf).slice(-4e3) });
2878
+ 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
2879
  } catch {
2423
2880
  }
2424
2881
  }
@@ -2437,14 +2894,20 @@ var init_tools_shell = __esm({
2437
2894
  }
2438
2895
  /** Current tail output for a job (null = no such job). */
2439
2896
  output(id) {
2440
- return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? "" : null);
2897
+ return this.meta.has(id) ? this.jobs.output(id) : null;
2441
2898
  }
2899
+ // null too once the shared registry pruned it
2442
2900
  status(id) {
2443
- const j = this.jobs.get(id);
2444
- return j ? { status: j.status, exitCode: j.exitCode, bytes: j.buf.length } : null;
2901
+ const m = this.meta.get(id);
2902
+ const j = m && this.jobs.get(id);
2903
+ if (!m || !j) {
2904
+ this.meta.delete(id);
2905
+ return null;
2906
+ }
2907
+ return { status: shellStatus(j.status), exitCode: m.exitCode, bytes: j.bytes, ...j.reason ? { reason: j.reason } : {} };
2445
2908
  }
2446
2909
  list() {
2447
- return [...this.jobs].map(([id, j]) => ({ id, command: j.command, status: j.status }));
2910
+ return [...this.meta].filter(([id]) => this.jobs.get(id)).map(([id, m]) => ({ id, command: m.command, status: shellStatus(this.jobs.get(id).status) }));
2448
2911
  }
2449
2912
  /**
2450
2913
  * Take over an ALREADY-RUNNING child as a background job. A foreground command that outruns its
@@ -2452,53 +2915,22 @@ var init_tools_shell = __esm({
2452
2915
  * worse, a launcher that spawned its own detached worker leaves that worker running with nothing
2453
2916
  * tracking it. Adopting hands the model a handle instead: the command keeps going, its completion
2454
2917
  * is reported like any other job, and `seed` carries the output produced before the handoff.
2918
+ * Unlike an explicit background job it IS liveness-checked (idle window + CPU probe): nobody asked for
2919
+ * a long-lived process here, so one that goes silent and CPU-idle is stuck, not serving.
2455
2920
  */
2456
2921
  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;
2922
+ const h = this.track(command, { seed, cpu: true });
2923
+ this.attach(h, proc);
2924
+ return h.id;
2483
2925
  }
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;
2926
+ kill(id, reason = "killed by ShellKill") {
2927
+ return this.meta.has(id) && this.jobs.kill(id, reason);
2497
2928
  }
2498
2929
  killAll() {
2499
- for (const id of this.jobs.keys()) this.kill(id);
2930
+ for (const id of this.meta.keys()) this.jobs.kill(id, "agent exiting");
2500
2931
  }
2501
2932
  };
2933
+ shellStatus = (s) => s === "done" ? "exited" : s === "running" ? "running" : s === "killed" ? "killed" : "error";
2502
2934
  NO_JOB2 = (id) => `Error: no background job '${id}'. Use ShellStatus with no id to list jobs, or start one with Shell({background:true}).`;
2503
2935
  }
2504
2936
  });
@@ -2580,111 +3012,7 @@ import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo
2580
3012
  // src/Agent.ts
2581
3013
  init_llm();
2582
3014
  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
- }
3015
+ init_tools_jobs();
2688
3016
 
2689
3017
  // src/host.ts
2690
3018
  var askUserQuestionTool = {
@@ -3243,7 +3571,7 @@ init_OverlayFilesystem();
3243
3571
  init_worktree();
3244
3572
  init_NodeDiskFilesystem();
3245
3573
  init_logging();
3246
- var log5 = forComponent("subagent");
3574
+ var log6 = forComponent("subagent");
3247
3575
  async function boundedPool(items, limit, fn) {
3248
3576
  const out = new Array(items.length);
3249
3577
  let next = 0;
@@ -3317,7 +3645,7 @@ function traceFileFor(dir, label, depth) {
3317
3645
  const id = `${Date.now().toString(36)}-${(traceSeq++).toString(36)}`;
3318
3646
  return join4(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
3319
3647
  } catch (e) {
3320
- log5.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
3648
+ log6.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
3321
3649
  return void 0;
3322
3650
  }
3323
3651
  }
@@ -3325,7 +3653,7 @@ function flushTrace(file, meta, messages, status) {
3325
3653
  try {
3326
3654
  writeFileSync2(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
3327
3655
  } catch (e) {
3328
- log5.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
3656
+ log6.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
3329
3657
  }
3330
3658
  }
3331
3659
  function installTrace(child, file, meta) {
@@ -3366,7 +3694,7 @@ async function runChildTraced(args) {
3366
3694
  if (file) flushTrace(file, meta, child.transcript, wc.timedOut() ? "deadline" : res.finishReason);
3367
3695
  let result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
3368
3696
  if (wc.timedOut()) {
3369
- log5.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
3697
+ log6.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
3370
3698
  result += deadlineNote(deadlineMs);
3371
3699
  }
3372
3700
  await opts.hooks?.onSubagentStop?.(result, { label, agentType });
@@ -3820,7 +4148,7 @@ function reasoningToChatFragment(model, effort) {
3820
4148
  }
3821
4149
 
3822
4150
  // src/Agent.ts
3823
- var log6 = forComponent("Agent");
4151
+ var log7 = forComponent("Agent");
3824
4152
  function envInt(key, fallback) {
3825
4153
  const n = typeof process !== "undefined" ? Number(process.env?.[key]) : NaN;
3826
4154
  return Number.isFinite(n) && n >= 0 ? n : fallback;
@@ -3857,6 +4185,15 @@ var AgentOptions = class {
3857
4185
  * arrives for this long. The between-steps `timeoutMs` can't preempt an in-flight `chat()`, so a
3858
4186
  * provider that goes silent mid-stream would otherwise park the loop forever. 0 = off. */
3859
4187
  stallMs = 6e4;
4188
+ /** IDLE bound on a NATIVE delegated tool's hold over the stall watchdog (cursor's own shell/mcp/task:
4189
+ * `running` → silence → `completed`). Such a hold is the provider's own unverified CLAIM that it is
4190
+ * busy, and it SUSPENDS the very stall detection that would catch that provider going silent — so its
4191
+ * only backstop was `hardCapMs = toolHoldMs`, the TRANSPORT deadline (≥30m, sized for host-tool
4192
+ * round-trips a native call never makes). A wedged native call therefore bought 30m of total silence,
4193
+ * ×2 attempts = the 61-minute zero-output run (blank 2026-09-18). Idle, not absolute: every further
4194
+ * `running` event `touch()`es the job, so a native tool that keeps reporting progress holds
4195
+ * indefinitely — what expires is silence, not work. 0 = off (hard cap only). */
4196
+ nativeHoldMs = 3e5;
3860
4197
  /** Stop if the identical tool-call batch (name+args) repeats this many times in a row. 0 = off. */
3861
4198
  maxRepeats = 3;
3862
4199
  /** Cumulative cap on tool calls dispatched across the run. 0 = unbounded. */
@@ -3972,6 +4309,12 @@ var AgentOptions = class {
3972
4309
  /** Enable `bash({background:true})` + JobOutput/JobStatus/JobKill — sandbox background jobs (overlay-isolated,
3973
4310
  * committed on completion, drained at turn end). Useful when the VFS backend is slow (remote) or for sub-agents. */
3974
4311
  backgroundJobs = false;
4312
+ /** The job registry every long-lived tool call lives in (MCP calls past their soft timeout, delegated host/native
4313
+ * tools, and — when the host shares it with its `ShellJobRegistry` — shell jobs). Its liveness is what the stall
4314
+ * watchdog and the stuck-job reaper decide from. Default: a fresh registry per Agent. Knobs (idle windows, hard
4315
+ * caps, MCP soft timeout) live on `JobRegistryOptions`. Completion notices reach the model via `inject` unless
4316
+ * the host set its own `onExit`. */
4317
+ jobs;
3975
4318
  /** Plan mode: block mutating tools until the agent calls `ExitPlanMode` (host-approved). */
3976
4319
  planMode = false;
3977
4320
  /** Permission policy gating each tool call (allow / ask / deny). */
@@ -4038,17 +4381,18 @@ var Agent = class _Agent {
4038
4381
  /** Per-run ground truth: every tool name actually INVOKED this run (native dispatch + delegated
4039
4382
  * runtime activity). The ledger that makes a fabricated tool claim provable. Reset per run. */
4040
4383
  invokedTools = /* @__PURE__ */ new Set();
4041
- /** Delegated-runtime HOST tools currently executing (cursor toolExecutor). While > 0 the provider is
4042
- * not silent — it is waiting on US — so the idle-stall watchdog must not fire (see armStallWatchdog). */
4043
- hostToolsInFlight = 0;
4044
- /** The hold is bounded by the transport tool deadline: a host tool that never settles (the helper already gave
4045
- * up on it) must not disable stall protection for the rest of the request — or this instance. */
4046
- hostToolHoldUntil = 0;
4047
- /** Delegated-runtime NATIVE tools (cursor's own shell/mcp/task) seen `running` but not yet `completed`/`error`,
4048
- * id → hold-until. The runtime streams nothing while one runs, so it holds the watchdog like a host tool —
4049
- * bounded by the same tool deadline (`toolHoldMs`) so a lost `completed` event can't disable it. */
4050
- nativeToolsInFlight = /* @__PURE__ */ new Map();
4384
+ /** Every long-lived tool call is a job here (see AgentOptions.jobs). Delegated-runtime HOST tools (cursor
4385
+ * toolExecutor) and NATIVE tools (cursor's own shell/mcp/task, `running` → `completed`) are tracked as
4386
+ * `holds` jobs: while one runs the provider is not silent — it is waiting on a tool — so the idle-stall
4387
+ * watchdog must not fire. Each is bounded by the transport tool deadline (`hardCapMs = toolHoldMs`), so a
4388
+ * tool that never settles (or a lost `completed` event) is reaped and cannot disable stall protection.
4389
+ * A NATIVE job carries a second, much tighter bound (`idleMs = nativeHoldMs`): the transport deadline is
4390
+ * sized for a host-tool round-trip it never makes, and silence is the only thing it can actually prove. */
4391
+ jobs;
4392
+ /** Native delegated tool activity id → its job (per step attempt). */
4393
+ nativeJobs = /* @__PURE__ */ new Map();
4051
4394
  toolHoldMs = 0;
4395
+ nativeHoldMs = 0;
4052
4396
  /** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
4053
4397
  * from the pre-step transcript, which would re-run it — so such a stall is not retried. */
4054
4398
  toolRanThisAttempt = false;
@@ -4096,6 +4440,7 @@ var Agent = class _Agent {
4096
4440
  addTools(tools) {
4097
4441
  this.injectedTools.push(...tools);
4098
4442
  this.activeTools.push(...tools);
4443
+ if (this.prepared && tools.some(isMcpTool) && !this.activeTools.some((t) => t.name === "JobWait")) this.activeTools.push(...makeJobTools(this.jobs));
4099
4444
  }
4100
4445
  /** Remove tools by name from a running agent. Returns the count removed. Also filters
4101
4446
  * `options.tools` — otherwise a constructor-passed tool resurrects on the next prepare()
@@ -4147,6 +4492,11 @@ var Agent = class _Agent {
4147
4492
  }
4148
4493
  constructor(options) {
4149
4494
  this.options = { ...new AgentOptions(), ...options };
4495
+ this.jobs = this.options.jobs ?? new JobRegistry();
4496
+ this.jobs.options.onExit ??= (n) => {
4497
+ const imgs = n.result?.images;
4498
+ this.inject(imgs?.length ? [{ type: "text", text: formatJobNotice(n) }, ...imgs.map((i) => imagePart(`data:${i.mimeType};base64,${i.data}`))] : formatJobNotice(n));
4499
+ };
4150
4500
  if (this.options.fs) this.buildCtx();
4151
4501
  }
4152
4502
  /** Build the tool context from the resolved fs + options. Idempotent-safe: called once (ctor or ensureFs). */
@@ -4158,7 +4508,8 @@ var Agent = class _Agent {
4158
4508
  this.ctx.ai = this.options.ai;
4159
4509
  this.ctx.model = this.options.model;
4160
4510
  this.ctx.parkHuman = (p) => this.park(p);
4161
- if (this.options.backgroundJobs) this.ctx.jobs = new SandboxJobRegistry();
4511
+ this.ctx.toolJobs = this.jobs;
4512
+ if (this.options.backgroundJobs) this.ctx.jobs = this.jobs;
4162
4513
  }
4163
4514
  /**
4164
4515
  * Resolve the filesystem + build the tool context if the ctor couldn't (no `fs` was passed). The disk
@@ -4174,7 +4525,7 @@ var Agent = class _Agent {
4174
4525
  const disk = new NodeDiskFilesystem2(process.cwd());
4175
4526
  await disk.init();
4176
4527
  this.options.fs = new JailedFilesystem2(disk);
4177
- log6.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
4528
+ log7.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
4178
4529
  }
4179
4530
  this.buildCtx();
4180
4531
  }
@@ -4244,7 +4595,7 @@ var Agent = class _Agent {
4244
4595
  tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
4245
4596
  }
4246
4597
  if (o.checkpoints) tools = [...tools, ...checkpointTools()];
4247
- if (this.ctx.jobs) tools = [...tools, ...makeJobTools(this.ctx.jobs)];
4598
+ if (this.ctx.jobs || [...tools, ...this.injectedTools].some(isMcpTool)) tools = [...tools, ...makeJobTools(this.jobs)];
4248
4599
  const plan = o.planMode ? planMode({ host: o.host }) : void 0;
4249
4600
  if (plan) tools = [...tools, plan.tool];
4250
4601
  this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
@@ -4360,8 +4711,8 @@ var Agent = class _Agent {
4360
4711
  let closureNudged = false;
4361
4712
  let lastProgressAt = start;
4362
4713
  const kill = (finishReason) => {
4363
- log6.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${finishReason === "timeout" ? `, idle=${Date.now() - lastProgressAt - this.parkedMs}ms` : ""}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
4364
- this.ctx.jobs?.killAll();
4714
+ 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` : ""})`);
4715
+ this.jobs.killAll(`run ended: ${finishReason}`, true);
4365
4716
  const claims = this.survivingClaims();
4366
4717
  return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
4367
4718
  };
@@ -4383,22 +4734,26 @@ var Agent = class _Agent {
4383
4734
  const derivedToolTimeoutMs = Math.max(TRANSPORT_TOOL_TIMEOUT_FLOOR_MS, ...this.activeTools.map(transportHeadroomMs));
4384
4735
  const effectiveToolTimeoutMs = typeof hostToolTimeoutMs === "number" ? Math.max(hostToolTimeoutMs, derivedToolTimeoutMs) : derivedToolTimeoutMs;
4385
4736
  this.toolHoldMs = effectiveToolTimeoutMs;
4737
+ this.nativeHoldMs = o.nativeHoldMs;
4386
4738
  if (isCursorWithTools) {
4387
4739
  if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
4388
- log6.warn(`providerOptions.toolTimeoutMs=${hostToolTimeoutMs}ms is below what the registered tools need \u2014 RAISED to ${effectiveToolTimeoutMs}ms. ${toolTimeoutViolations(this.activeTools, hostToolTimeoutMs).length} tool(s) could outlive it, and the transport would have destroyed their results. To lower it, lower the tools' own maxDurationMs (e.g. the Shell tool's maxTimeoutMs) instead.`);
4740
+ log7.warn(`providerOptions.toolTimeoutMs=${hostToolTimeoutMs}ms is below what the registered tools need \u2014 RAISED to ${effectiveToolTimeoutMs}ms. ${toolTimeoutViolations(this.activeTools, hostToolTimeoutMs).length} tool(s) could outlive it, and the transport would have destroyed their results. To lower it, lower the tools' own maxDurationMs (e.g. the Shell tool's maxTimeoutMs) instead.`);
4389
4741
  }
4390
- for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log6.warn(v);
4742
+ for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log7.warn(v);
4391
4743
  }
4392
4744
  const cursorPo = isCursorWithTools ? {
4393
4745
  toolTimeoutMs: effectiveToolTimeoutMs,
4394
4746
  toolExecutor: async (name, args) => {
4395
4747
  const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
4396
- this.hostToolsInFlight++;
4397
- this.hostToolHoldUntil = Math.max(this.hostToolHoldUntil, Date.now() + effectiveToolTimeoutMs);
4748
+ const job = this.jobs.track({ kind: "host", label: name, holds: true, hardCapMs: effectiveToolTimeoutMs });
4398
4749
  try {
4399
- return await this.dispatch(tc);
4750
+ const r = await this.dispatch(tc);
4751
+ job.finish("done");
4752
+ return r;
4753
+ } catch (e) {
4754
+ job.finish("error", void 0, String(e));
4755
+ throw e;
4400
4756
  } finally {
4401
- this.hostToolsInFlight--;
4402
4757
  this.toolRanThisAttempt = true;
4403
4758
  this.stallPoke?.();
4404
4759
  }
@@ -4412,7 +4767,8 @@ var Agent = class _Agent {
4412
4767
  };
4413
4768
  try {
4414
4769
  for (let attempt = 0; ; attempt++) {
4415
- this.nativeToolsInFlight.clear();
4770
+ for (const [, h] of this.nativeJobs) if (h.status === "running") this.jobs.kill(h.id, "step attempt ended");
4771
+ this.nativeJobs.clear();
4416
4772
  this.toolRanThisAttempt = false;
4417
4773
  try {
4418
4774
  if (useStream) {
@@ -4444,11 +4800,11 @@ var Agent = class _Agent {
4444
4800
  const empty = err2?.code === "empty" || err2?.code === "cursor_silent";
4445
4801
  const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
4446
4802
  const replaysTool = stalled && this.toolRanThisAttempt;
4447
- if (replaysTool) log6.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
4803
+ if (replaysTool) log7.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
4448
4804
  const transient = !o.signal?.aborted && !isAbortError(err2) && !replaysTool && attempt < maxAttempts && (network || serverSide || stalled || empty);
4449
4805
  if (!transient) throw err2;
4450
4806
  const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
4451
- log6.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
4807
+ log7.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
4452
4808
  o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
4453
4809
  await new Promise((r) => setTimeout(r, waitMs));
4454
4810
  }
@@ -4456,12 +4812,12 @@ var Agent = class _Agent {
4456
4812
  } catch (err2) {
4457
4813
  if (err2?.code === "budget") return kill("budget");
4458
4814
  if (err2?.code === "stall") {
4459
- log6.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
4815
+ log7.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
4460
4816
  return { ...kill("timeout"), error: err2 };
4461
4817
  }
4462
4818
  if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
4463
4819
  if (err2?.code === "empty" || err2?.code === "cursor_silent") {
4464
- log6.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
4820
+ log7.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
4465
4821
  return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err2 };
4466
4822
  }
4467
4823
  const body = err2?.body ?? err2?.response?.data ?? err2?.error;
@@ -4473,7 +4829,7 @@ var Agent = class _Agent {
4473
4829
  }
4474
4830
  if (bodyStr && err2 instanceof Error && !err2.message.includes(bodyStr)) err2.detail = bodyStr;
4475
4831
  const errLog = err2 instanceof Error ? { message: err2.message, ...err2.detail ? { detail: err2.detail } : {}, stack: err2.stack } : err2;
4476
- log6.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
4832
+ log7.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
4477
4833
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err2 };
4478
4834
  }
4479
4835
  if (o.signal?.aborted) return kill("aborted");
@@ -4529,21 +4885,21 @@ var Agent = class _Agent {
4529
4885
  if (emptyTurn && o.failEmptyTurns) {
4530
4886
  if (!emptyNudged && canNudge()) {
4531
4887
  emptyNudged = true;
4532
- log6.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
4888
+ log7.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
4533
4889
  this.transcript.push({ role: "user", content: "Continue \u2014 you returned nothing. Answer the user now using the tool results above, or keep working if you are not done." });
4534
4890
  continue;
4535
4891
  }
4536
- log6.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
4892
+ log7.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
4537
4893
  const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
4538
4894
  e.code = "empty";
4539
4895
  return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
4540
4896
  }
4541
4897
  const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
4542
4898
  for (const n of fabricated) this.fabricatedClaims.add(n);
4543
- if (fabricated.length && !canNudge()) log6.warn(`fabricated tool use: text names ${fabricated.join(", ")} but no step/budget left to demand the call \u2014 finishing with the existing answer`);
4899
+ if (fabricated.length && !canNudge()) log7.warn(`fabricated tool use: text names ${fabricated.join(", ")} but no step/budget left to demand the call \u2014 finishing with the existing answer`);
4544
4900
  if (fabricated.length && canNudge()) {
4545
4901
  closureNudged = true;
4546
- log6.warn(`fabricated tool use: text names ${fabricated.join(", ")} but the run invoked ${this.invokedTools.size ? [...this.invokedTools].join(", ") : "nothing"} \u2014 demanding the real call`);
4902
+ log7.warn(`fabricated tool use: text names ${fabricated.join(", ")} but the run invoked ${this.invokedTools.size ? [...this.invokedTools].join(", ") : "nothing"} \u2014 demanding the real call`);
4547
4903
  this.transcript.push({
4548
4904
  role: "user",
4549
4905
  content: `You referred to ${fabricated.map((n) => `\`${n}\``).join(", ")} in your reply, but you never actually called ${fabricated.length > 1 ? "those tools" : "that tool"} \u2014 no tool call was made, so any result you reported for ${fabricated.length > 1 ? "them" : "it"} was invented. Either call ${fabricated.length > 1 ? "them" : "it"} for real now, or answer without ${fabricated.length > 1 ? "them" : "it"} and do NOT claim any outcome \u2014 including a failure or a timeout \u2014 for a call you did not make.`
@@ -4552,15 +4908,18 @@ var Agent = class _Agent {
4552
4908
  }
4553
4909
  if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
4554
4910
  closureNudged = true;
4555
- log6.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
4911
+ log7.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
4556
4912
  this.transcript.push({
4557
4913
  role: "user",
4558
4914
  content: "You ran tools but did not reply. Answer the user now using the tool results above; if a tool failed, say so plainly. Do NOT re-run those tools, do NOT narrate what you did, and do NOT ask for permission to continue."
4559
4915
  });
4560
4916
  continue;
4561
4917
  }
4562
- log6.verbose(`completed in ${steps} step(s)`);
4563
- await this.ctx.jobs?.drain();
4918
+ log7.verbose(`completed in ${steps} step(s)`);
4919
+ const drained = await this.jobs.drain(o.signal);
4920
+ if (o.signal?.aborted) return kill("aborted");
4921
+ if (drained.length) lastProgressAt = Date.now();
4922
+ if (this.drainInjections()) continue;
4564
4923
  this.releaseStaleImages();
4565
4924
  await this.activeHooks?.onStop?.(res.content ?? "");
4566
4925
  const claims = this.survivingClaims();
@@ -4586,11 +4945,12 @@ var Agent = class _Agent {
4586
4945
  }
4587
4946
  this.transcript.push({ role: "tool", tool_call_id: tc.id, name: tc.function.name, content });
4588
4947
  }
4948
+ lastProgressAt = Date.now();
4589
4949
  if (this.ctx.needsInput) {
4590
4950
  const question = this.ctx.needsInput;
4591
4951
  this.ctx.needsInput = void 0;
4592
- log6.warn(`needs_input: ${question.question} (steps=${steps})`);
4593
- await this.ctx.jobs?.drain();
4952
+ log7.warn(`needs_input: ${question.question} (steps=${steps})`);
4953
+ await this.jobs.drain();
4594
4954
  const text = [
4595
4955
  `OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
4596
4956
  ...question.options.map((o2) => ` \u2022 ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)
@@ -4628,7 +4988,15 @@ var Agent = class _Agent {
4628
4988
  if (!stallMs) return;
4629
4989
  if (timer) clearTimeout(timer);
4630
4990
  timer = setTimeout(() => {
4631
- if (this.toolHoldActive()) return poke();
4991
+ if (this.toolHoldActive()) {
4992
+ const now4 = Date.now();
4993
+ const who = this.jobs.holding().map((j) => `${j.kind} "${j.label}" (${Math.round((now4 - j.startedAt) / 1e3)}s, quiet ${Math.round((now4 - j.lastActivityAt) / 1e3)}s)`).join(", ");
4994
+ log7.warn(`stall watchdog: no stream output for ${stallMs}ms \u2014 held by ${who}`);
4995
+ this.options.host?.notify?.({ kind: "tool_hold", message: `waiting on ${who}` });
4996
+ return poke();
4997
+ }
4998
+ const inFlight = this.jobs.list().filter((j) => j.status === "running");
4999
+ log7.warn(`stall watchdog: no stream output for ${stallMs}ms and no tool holding the stream \u2014 aborting the request` + (inFlight.length ? ` (${inFlight.length} detached job(s) running, not awaited by the stream: ${inFlight.map((j) => `${j.id}/${j.kind}`).join(", ")})` : ""));
4632
5000
  fired = true;
4633
5001
  ac.abort(stallError());
4634
5002
  }, stallMs);
@@ -4648,12 +5016,11 @@ var Agent = class _Agent {
4648
5016
  reclassify: (err2) => stalled() ? stallError() : err2
4649
5017
  };
4650
5018
  }
4651
- /** A host or native delegated tool is running within its deadline — the provider is quiet, not stalled. */
5019
+ /** A host or native delegated tool is running and not stuck — the provider is quiet, not stalled. The reap runs
5020
+ * FIRST, so a holder past its deadline is released (logged with evidence) instead of holding forever. */
4652
5021
  toolHoldActive() {
4653
- const now4 = Date.now();
4654
- if (this.hostToolsInFlight > 0 && now4 < this.hostToolHoldUntil) return true;
4655
- for (const until of this.nativeToolsInFlight.values()) if (now4 < until) return true;
4656
- return false;
5022
+ this.jobs.reap();
5023
+ return this.jobs.holding().length > 0;
4657
5024
  }
4658
5025
  /** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
4659
5026
  * non-result the model produced by going silent. Callers retry it rather than report "done". */
@@ -4693,10 +5060,16 @@ var Agent = class _Agent {
4693
5060
  if (a.output !== void 0) prev.output = a.output;
4694
5061
  prev.status = a.status;
4695
5062
  }
5063
+ const nj = a.id ? this.nativeJobs.get(a.id) : void 0;
4696
5064
  if (a.status === "running") {
4697
- if (a.id) this.nativeToolsInFlight.set(a.id, Date.now() + this.toolHoldMs);
5065
+ if (nj) nj.touch();
5066
+ else if (a.id) this.nativeJobs.set(a.id, this.jobs.track({ kind: "native", label: a.name, holds: true, idleMs: this.nativeHoldMs, hardCapMs: this.toolHoldMs }));
4698
5067
  } else {
4699
- if (a.id) this.nativeToolsInFlight.delete(a.id);
5068
+ if (nj) {
5069
+ if (typeof a.output === "string") nj.chunk(a.output.slice(-4e3));
5070
+ nj.finish(a.status === "error" ? "error" : "done");
5071
+ this.nativeJobs.delete(a.id);
5072
+ }
4700
5073
  this.toolRanThisAttempt = true;
4701
5074
  }
4702
5075
  sawTool = true;
@@ -4787,13 +5160,13 @@ var Agent = class _Agent {
4787
5160
  const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
4788
5161
  if (decision?.block) {
4789
5162
  const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
4790
- log6.debug(`${tc.function.name} -> ${blocked}`);
5163
+ log7.debug(`${tc.function.name} -> ${blocked}`);
4791
5164
  await hooks?.postToolUse?.(call, blocked, meta);
4792
5165
  return blocked;
4793
5166
  }
4794
5167
  this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
4795
5168
  if (earlyError) {
4796
- log6.debug(`${tc.function.name} -> ${earlyError}`);
5169
+ log7.debug(`${tc.function.name} -> ${earlyError}`);
4797
5170
  await hooks?.postToolUse?.(call, earlyError, meta);
4798
5171
  this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
4799
5172
  return earlyError;
@@ -4802,12 +5175,12 @@ var Agent = class _Agent {
4802
5175
  let images;
4803
5176
  let threw = false;
4804
5177
  try {
4805
- log6.debug(`${tc.function.name}(${tc.function.arguments})`);
5178
+ log7.debug(`${tc.function.name}(${tc.function.arguments})`);
4806
5179
  this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
4807
5180
  try {
4808
5181
  hooks.onToolOutput(call, chunk, meta);
4809
5182
  } catch (e) {
4810
- log6.debug(`onToolOutput hook error: ${e}`);
5183
+ log7.debug(`onToolOutput hook error: ${e}`);
4811
5184
  }
4812
5185
  } : void 0;
4813
5186
  const raw = await tool.run(args, this.ctx);
@@ -4819,7 +5192,7 @@ var Agent = class _Agent {
4819
5192
  }
4820
5193
  } catch (e) {
4821
5194
  const msg = e instanceof Error ? e.message : String(e);
4822
- log6.debug(`${tc.function.name} -> error: ${msg}`);
5195
+ log7.debug(`${tc.function.name} -> error: ${msg}`);
4823
5196
  result = `Error: ${msg}`;
4824
5197
  threw = true;
4825
5198
  } finally {
@@ -4911,6 +5284,7 @@ function estimateTokens(m, keepRecentImages = 1) {
4911
5284
  return Math.ceil(sendBytes(m, keepRecentImages) / 4);
4912
5285
  }
4913
5286
  var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
5287
+ var isMcpTool = (t) => t.name.startsWith("mcp__") || t.name === "McpCall";
4914
5288
  function transportHeadroomMs(t) {
4915
5289
  const d = Number(t.maxDurationMs) || 0;
4916
5290
  if (!d) return 0;
@@ -5086,7 +5460,7 @@ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
5086
5460
  const ids = callIdSet(messages.slice(from));
5087
5461
  while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
5088
5462
  if (total > maxTokens)
5089
- log6.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
5463
+ log7.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
5090
5464
  return [...head, ...messages.slice(from)];
5091
5465
  }
5092
5466
  function compact(m, max, focus) {
@@ -5391,6 +5765,7 @@ init_OverlayFilesystem();
5391
5765
 
5392
5766
  // src/index.ts
5393
5767
  init_tools();
5768
+ init_tools_jobs();
5394
5769
 
5395
5770
  // src/scheduler.ts
5396
5771
  function parseCronField(field, min, max) {
@@ -5663,7 +6038,7 @@ init_tools_web();
5663
6038
  init_tools();
5664
6039
  init_tools_structured();
5665
6040
  init_logging();
5666
- var log7 = forComponent("scratch");
6041
+ var log8 = forComponent("scratch");
5667
6042
  var SCRATCH_DIR = "/scratch";
5668
6043
  function shortArgs(args) {
5669
6044
  try {
@@ -5703,7 +6078,7 @@ var Scratch = class {
5703
6078
  await (this.dirReady ??= mkdirp(this.fs, dir));
5704
6079
  await this.fs.writeFile(path, header + raw);
5705
6080
  } catch (e) {
5706
- log7.debug("scratch write failed; returning raw", e);
6081
+ log8.debug("scratch write failed; returning raw", e);
5707
6082
  return raw;
5708
6083
  }
5709
6084
  const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
@@ -5733,7 +6108,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
5733
6108
  await (this.dirReady ??= mkdirp(this.fs, dir));
5734
6109
  await this.fs.writeFile(path, header + full);
5735
6110
  } catch (e) {
5736
- log7.debug("scratch spill failed; cropping lossy", e);
6111
+ log8.debug("scratch spill failed; cropping lossy", e);
5737
6112
  return full.slice(0, pageBytes) + `
5738
6113
 
5739
6114
  [output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
@@ -5779,7 +6154,7 @@ Question: ${q2}`);
5779
6154
  const answer = (res.text ?? "").trim();
5780
6155
  return answer || "(no answer found in scratch)";
5781
6156
  } catch (e) {
5782
- log7.debug("Ask peek failed", e);
6157
+ log8.debug("Ask peek failed", e);
5783
6158
  return `Error querying scratch: ${e?.message ?? e}`;
5784
6159
  }
5785
6160
  }
@@ -5788,7 +6163,7 @@ Question: ${q2}`);
5788
6163
 
5789
6164
  // src/lessons.ts
5790
6165
  init_logging();
5791
- var log8 = forComponent("Lessons");
6166
+ var log9 = forComponent("Lessons");
5792
6167
  var LessonOptionsDefaults = class {
5793
6168
  minRepeats = 2;
5794
6169
  };
@@ -5813,8 +6188,8 @@ function lessonCapture(options) {
5813
6188
  counts.set(lesson.slug, n);
5814
6189
  if (n < o.minRepeats) return;
5815
6190
  written.add(lesson.slug);
5816
- await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log8.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
5817
- log8.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
6191
+ await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log9.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
6192
+ log9.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
5818
6193
  }
5819
6194
  };
5820
6195
  }
@@ -5822,7 +6197,7 @@ function lessonCapture(options) {
5822
6197
  // src/reflect.ts
5823
6198
  init_llm();
5824
6199
  init_logging();
5825
- var log9 = forComponent("Reflect");
6200
+ var log10 = forComponent("Reflect");
5826
6201
  async function reflectOnRun(o) {
5827
6202
  const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
5828
6203
  if (!digest.trim()) return null;
@@ -5838,7 +6213,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
5838
6213
  const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
5839
6214
  text = r?.content ?? "";
5840
6215
  } catch (e) {
5841
- log9.warn(`reflection call failed: ${e?.message ?? e}`);
6216
+ log10.warn(`reflection call failed: ${e?.message ?? e}`);
5842
6217
  return null;
5843
6218
  }
5844
6219
  const m = text.match(/LESSON:\s*(.+)/i);
@@ -5848,10 +6223,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
5848
6223
  try {
5849
6224
  await writeFact(o.fs, o.dir, slug2, lesson);
5850
6225
  } catch (e) {
5851
- log9.warn(`could not persist lesson: ${e?.message ?? e}`);
6226
+ log10.warn(`could not persist lesson: ${e?.message ?? e}`);
5852
6227
  return null;
5853
6228
  }
5854
- log9.debug(`reflection persisted ${slug2}`);
6229
+ log10.debug(`reflection persisted ${slug2}`);
5855
6230
  return slug2;
5856
6231
  }
5857
6232
  function digestRun(messages, maxChars) {
@@ -5900,7 +6275,7 @@ function forComponent2(name) {
5900
6275
  }
5901
6276
 
5902
6277
  // node_modules/@bod.ee/voice/src/core/emotion.ts
5903
- var log10 = forComponent2("Emotion");
6278
+ var log11 = forComponent2("Emotion");
5904
6279
  var EMOTIONS = [
5905
6280
  // primary (best results)
5906
6281
  "neutral",
@@ -6032,7 +6407,7 @@ function renderEmotions(text, opts = { show: true }) {
6032
6407
  speech += cartesiaTag(tag);
6033
6408
  if (opts.show) display += m[0];
6034
6409
  } else {
6035
- log10.debug(`dropping unknown emotion tag ${m[0]}`);
6410
+ log11.debug(`dropping unknown emotion tag ${m[0]}`);
6036
6411
  }
6037
6412
  last = m.index + m[0].length;
6038
6413
  }
@@ -6074,7 +6449,7 @@ var EmotionStream = class {
6074
6449
  if (tag) {
6075
6450
  this.pending = tag;
6076
6451
  if (this.show) display += m[0];
6077
- } else log10.debug(`dropping unknown emotion tag ${m[0]}`);
6452
+ } else log11.debug(`dropping unknown emotion tag ${m[0]}`);
6078
6453
  last = m.index + m[0].length;
6079
6454
  }
6080
6455
  this.emit(body.slice(last), (s, d, p) => {
@@ -6097,7 +6472,7 @@ var EmotionStream = class {
6097
6472
  };
6098
6473
 
6099
6474
  // node_modules/@bod.ee/voice/src/core/engine.ts
6100
- var log11 = forComponent2("VoiceEngine");
6475
+ var log12 = forComponent2("VoiceEngine");
6101
6476
  var realClock = {
6102
6477
  now: () => performance.now(),
6103
6478
  setTimeout: (fn, ms) => setTimeout(fn, ms),
@@ -6372,7 +6747,7 @@ var VoiceEngine = class _VoiceEngine {
6372
6747
  await Promise.all([this.tts.connect(), this.stt.start()]);
6373
6748
  this.tts.warmup?.();
6374
6749
  this.setState("listening");
6375
- log11.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
6750
+ log12.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
6376
6751
  }
6377
6752
  get usingAec() {
6378
6753
  return this.stt.usingAec;
@@ -6394,7 +6769,7 @@ var VoiceEngine = class _VoiceEngine {
6394
6769
  this.options.onDiag({ t: this.clock.now(), kind, ...fields });
6395
6770
  } catch (e) {
6396
6771
  this.diagOn = false;
6397
- log11.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6772
+ log12.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6398
6773
  }
6399
6774
  }
6400
6775
  idleWaiters = [];
@@ -6503,7 +6878,7 @@ var VoiceEngine = class _VoiceEngine {
6503
6878
  if (prose.trim()) this.repliedSinceDispatch = true;
6504
6879
  for (const w of this.words(this.reply)) this.echoWords.add(w);
6505
6880
  this.tts.speak(forSpeech(speech), true);
6506
- if (!this.spokeDeltas && this.turnStartAt) log11.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
6881
+ if (!this.spokeDeltas && this.turnStartAt) log12.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
6507
6882
  this.spokeDeltas = true;
6508
6883
  this.setState("speaking");
6509
6884
  if (this.options.revealMode !== "off" && display) {
@@ -6580,7 +6955,7 @@ var VoiceEngine = class _VoiceEngine {
6580
6955
  this.drainTimer = null;
6581
6956
  this.speaking = false;
6582
6957
  this.stopWordReveal(true);
6583
- if (this.turnStartAt) log11.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
6958
+ if (this.turnStartAt) log12.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
6584
6959
  this.echoUntil = this.clock.now() + 2500;
6585
6960
  if (!this.usingAec) this.stt.reset();
6586
6961
  this.setState("listening");
@@ -6668,7 +7043,7 @@ var VoiceEngine = class _VoiceEngine {
6668
7043
  this.clearAckTimer("interrupt");
6669
7044
  this.adaptiveAckFired = false;
6670
7045
  this.ackProbe = null;
6671
- if (this.uttQueue.length) log11.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
7046
+ if (this.uttQueue.length) log12.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
6672
7047
  const droppedQueued = this.uttQueue.length;
6673
7048
  this.uttQueue = [];
6674
7049
  if (!this.speaking && !this.drainTimer) return;
@@ -6814,7 +7189,7 @@ var VoiceEngine = class _VoiceEngine {
6814
7189
  if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
6815
7190
  this.specSpent = true;
6816
7191
  this.specText = this.specPartial;
6817
- log11.debug(`speculate: "${this.specText.slice(0, 60)}"`);
7192
+ log12.debug(`speculate: "${this.specText.slice(0, 60)}"`);
6818
7193
  this.diag("speculate", { text: this.specText });
6819
7194
  this.options.onSpeculate(this.specText);
6820
7195
  }, this.options.speculativeMs);
@@ -6883,7 +7258,7 @@ var VoiceEngine = class _VoiceEngine {
6883
7258
  };
6884
7259
  this.tts.newContext();
6885
7260
  this.tts.speak(phrase, false);
6886
- log11.debug(`backchannel: "${phrase}"`);
7261
+ log12.debug(`backchannel: "${phrase}"`);
6887
7262
  this.diag("backchannel", { phrase });
6888
7263
  this.options.onBackchannel(phrase);
6889
7264
  }
@@ -6916,7 +7291,7 @@ var VoiceEngine = class _VoiceEngine {
6916
7291
  });
6917
7292
  if (idx < 0) return text;
6918
7293
  toks.splice(idx, 1);
6919
- log11.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
7294
+ log12.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
6920
7295
  return toks.join(" ");
6921
7296
  }
6922
7297
  /** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
@@ -7007,7 +7382,7 @@ var VoiceEngine = class _VoiceEngine {
7007
7382
  this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
7008
7383
  if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
7009
7384
  if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
7010
- log11.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
7385
+ log12.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
7011
7386
  this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
7012
7387
  this.options.onHold();
7013
7388
  if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
@@ -7042,13 +7417,13 @@ var VoiceEngine = class _VoiceEngine {
7042
7417
  if (text) {
7043
7418
  const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
7044
7419
  if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
7045
- log11.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
7420
+ log12.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
7046
7421
  this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
7047
7422
  return;
7048
7423
  }
7049
7424
  const refinal = this.refinalizes(text);
7050
7425
  if (refinal && this.repliedSinceDispatch) {
7051
- log11.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
7426
+ log12.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
7052
7427
  this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
7053
7428
  this.lastDispatchFlat = flat;
7054
7429
  this.lastDispatchWords = text.trim().split(/\s+/);
@@ -7208,7 +7583,7 @@ var SpokenSplitter = class {
7208
7583
  };
7209
7584
 
7210
7585
  // node_modules/@bod.ee/voice/src/adapters/soniox.ts
7211
- var log12 = forComponent2("SonioxSTT");
7586
+ var log13 = forComponent2("SonioxSTT");
7212
7587
  var now = () => performance.now();
7213
7588
  var SonioxSTTOptions = class {
7214
7589
  auth = "";
@@ -7249,7 +7624,7 @@ var SonioxSTT = class {
7249
7624
  this.onDiag({ t: now(), kind, ...fields });
7250
7625
  } catch (e) {
7251
7626
  this.diagOn = false;
7252
- log12.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7627
+ log13.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7253
7628
  }
7254
7629
  }
7255
7630
  lastChunkAt = 0;
@@ -7291,11 +7666,11 @@ var SonioxSTT = class {
7291
7666
  this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
7292
7667
  this.ws.onclose = (ev) => {
7293
7668
  if (this.stopped) return;
7294
- log12.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
7669
+ log13.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
7295
7670
  this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
7296
7671
  this.reset();
7297
7672
  this.connectWs().catch((e) => {
7298
- log12.error(`soniox reconnect failed: ${e.message}`);
7673
+ log13.error(`soniox reconnect failed: ${e.message}`);
7299
7674
  this.diag("stt_reconnect_failed", { message: e.message });
7300
7675
  });
7301
7676
  };
@@ -7307,7 +7682,7 @@ var SonioxSTT = class {
7307
7682
  this.endpointTimer = setInterval(() => {
7308
7683
  const combined = (this.finalText + this.partialText).trim();
7309
7684
  if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
7310
- if (this.firstTokenAt) log12.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
7685
+ if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
7311
7686
  this.reset();
7312
7687
  this.onUtterance(combined, now());
7313
7688
  }, 120);
@@ -7319,7 +7694,7 @@ var SonioxSTT = class {
7319
7694
  if (this.stopped) return;
7320
7695
  const ref = this.lastChunkAt || this.startedChunksAt;
7321
7696
  if (now() - ref > noAudioMs) {
7322
- log12.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
7697
+ log13.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
7323
7698
  this.diag("stt_watchdog_fatal", { noAudioMs });
7324
7699
  this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
7325
7700
  this.stop();
@@ -7342,7 +7717,7 @@ var SonioxSTT = class {
7342
7717
  handle(m) {
7343
7718
  if (m.error_message) {
7344
7719
  this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
7345
- return log12.error(`soniox: ${m.error_message}`);
7720
+ return log13.error(`soniox: ${m.error_message}`);
7346
7721
  }
7347
7722
  let endpoint = false;
7348
7723
  for (const t of m.tokens ?? []) {
@@ -7359,7 +7734,7 @@ var SonioxSTT = class {
7359
7734
  this.onPartial(combined);
7360
7735
  if (endpoint && this.finalText.trim()) {
7361
7736
  const utterance = this.finalText.trim();
7362
- if (this.firstTokenAt) log12.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
7737
+ if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
7363
7738
  this.reset();
7364
7739
  this.onUtterance(utterance, now());
7365
7740
  }
@@ -7381,7 +7756,7 @@ var SonioxSTT = class {
7381
7756
  };
7382
7757
 
7383
7758
  // node_modules/@bod.ee/voice/src/adapters/cartesia.ts
7384
- var log13 = forComponent2("CartesiaTTS");
7759
+ var log14 = forComponent2("CartesiaTTS");
7385
7760
  var now2 = () => performance.now();
7386
7761
  var CartesiaTTSOptions = class {
7387
7762
  auth = "";
@@ -7417,7 +7792,7 @@ var CartesiaTTS = class _CartesiaTTS {
7417
7792
  this.onDiag({ t: now2(), kind, ...fields });
7418
7793
  } catch (e) {
7419
7794
  this.diagOn = false;
7420
- log13.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7795
+ log14.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
7421
7796
  }
7422
7797
  }
7423
7798
  firstAudioAt = 0;
@@ -7452,11 +7827,11 @@ var CartesiaTTS = class _CartesiaTTS {
7452
7827
  this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
7453
7828
  });
7454
7829
  this.ws.onclose = (ev) => {
7455
- log13.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
7830
+ log14.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
7456
7831
  this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
7457
7832
  if (!this.closed) {
7458
7833
  this.connecting = this.doConnect().catch((e) => {
7459
- log13.error(`cartesia reconnect failed: ${e.message}`);
7834
+ log14.error(`cartesia reconnect failed: ${e.message}`);
7460
7835
  this.diag("tts_reconnect_failed", { message: e.message });
7461
7836
  });
7462
7837
  }
@@ -7484,12 +7859,12 @@ var CartesiaTTS = class _CartesiaTTS {
7484
7859
  this.down = true;
7485
7860
  this.downAt = now2();
7486
7861
  this.consecutiveOk = 0;
7487
- log13.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
7862
+ log14.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
7488
7863
  this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
7489
7864
  this.onDone();
7490
7865
  this.startProbe();
7491
7866
  } else if (!this.down) {
7492
- (/No valid transcripts/i.test(m.message || "") ? log13.debug : log13.warn)(`cartesia: ${JSON.stringify(m)}`);
7867
+ (/No valid transcripts/i.test(m.message || "") ? log14.debug : log14.warn)(`cartesia: ${JSON.stringify(m)}`);
7493
7868
  }
7494
7869
  }
7495
7870
  };
@@ -7504,7 +7879,7 @@ var CartesiaTTS = class _CartesiaTTS {
7504
7879
  this.stopProbe();
7505
7880
  const downMs = this.downAt ? now2() - this.downAt : 0;
7506
7881
  this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
7507
- (downMs < 2e3 ? log13.debug : log13.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
7882
+ (downMs < 2e3 ? log14.debug : log14.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
7508
7883
  }
7509
7884
  /** Ensure the WS is open before sending — reconnects if idle-closed. */
7510
7885
  async ensureConnected() {
@@ -7595,7 +7970,7 @@ configureLogging((name) => {
7595
7970
  });
7596
7971
 
7597
7972
  // src/duplex.ts
7598
- var log14 = forComponent("DuplexAgent");
7973
+ var log15 = forComponent("DuplexAgent");
7599
7974
  function describeCall(call) {
7600
7975
  const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
7601
7976
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
@@ -7806,7 +8181,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7806
8181
  const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
7807
8182
  if (m) {
7808
8183
  this.fabricationCut = true;
7809
- log14.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
8184
+ log15.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
7810
8185
  const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
7811
8186
  if (!safe) return;
7812
8187
  if (safe.trim()) this.spokeThisTurn = true;
@@ -7901,7 +8276,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7901
8276
  return {
7902
8277
  preToolUse: (call) => {
7903
8278
  if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
7904
- log14.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
8279
+ log15.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
7905
8280
  this.abortSpeculation();
7906
8281
  return { block: true, reason: "Speculative turn aborted." };
7907
8282
  }
@@ -7939,7 +8314,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7939
8314
  try {
7940
8315
  await this.voice.send(fallback ? "[reminder] You said nothing to the user this turn. Tell them, in ONE short spoken sentence, what just happened \u2014 no tools." : dispatched ? "[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools." : "[reminder] You said nothing to the user this turn. Give your ONE short spoken reply now \u2014 no tools.");
7941
8316
  } catch (e) {
7942
- log14.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
8317
+ log15.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
7943
8318
  } finally {
7944
8319
  this.nudging = false;
7945
8320
  }
@@ -8032,7 +8407,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
8032
8407
  try {
8033
8408
  res = await this.voice.send(spec.text);
8034
8409
  } catch (e) {
8035
- log14.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
8410
+ log15.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
8036
8411
  } finally {
8037
8412
  this.voice.options.signal = prevSignal;
8038
8413
  }
@@ -8046,7 +8421,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
8046
8421
  if (d === "abort") {
8047
8422
  if (this.voice.transcript.length > base) this.voice.transcript.length = base;
8048
8423
  this.speculativeAbortedCalls++;
8049
- log14.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
8424
+ log15.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
8050
8425
  if (this.spec === spec) this.spec = void 0;
8051
8426
  return res ?? empty;
8052
8427
  }
@@ -8311,7 +8686,7 @@ Another agent just implemented the above. Independently check the CURRENT state
8311
8686
  this.notify("task_verify", `task ${id}: verifying`, { id });
8312
8687
  const cres = await new Agent(checkerOpts).run(checkBrief);
8313
8688
  if (cres.finishReason !== "stop") {
8314
- log14.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
8689
+ log15.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
8315
8690
  this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
8316
8691
  }
8317
8692
  const sum = (a = 0, b = 0) => a + b;
@@ -8447,7 +8822,7 @@ Another agent just implemented the above. Independently check the CURRENT state
8447
8822
  rec.status = "done";
8448
8823
  rec.result = res.text;
8449
8824
  const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
8450
- log14.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
8825
+ log15.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
8451
8826
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
8452
8827
  id,
8453
8828
  text: res.text,
@@ -8489,14 +8864,14 @@ Another agent just implemented the above. Independently check the CURRENT state
8489
8864
  foldIfSuperseded(rec, note) {
8490
8865
  if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
8491
8866
  if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
8492
- log14.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
8867
+ log15.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
8493
8868
  return true;
8494
8869
  }
8495
8870
  failTask(rec, msg) {
8496
8871
  this.dropAsk(rec.id);
8497
8872
  rec.status = "error";
8498
8873
  rec.result = msg;
8499
- log14.warn(`task ${rec.id} failed: ${msg}`);
8874
+ log15.warn(`task ${rec.id} failed: ${msg}`);
8500
8875
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
8501
8876
  if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
8502
8877
  this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
@@ -8745,17 +9120,42 @@ async function withDeadline(p, ms) {
8745
9120
  }
8746
9121
  }
8747
9122
  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.`;
9123
+ async function runMcpCall(name, call, ctx, timeoutMs) {
9124
+ const jobs = ctx?.toolJobs;
9125
+ const shape = (raw) => {
9126
+ const r2 = toResult(raw);
9127
+ return r2.images?.length ? r2 : r2.text;
9128
+ };
9129
+ if (!jobs) {
9130
+ const raw = await withDeadline(call({}), timeoutMs);
9131
+ return raw === TIMED_OUT ? mcpTimedOut(name, timeoutMs) : shape(raw);
9132
+ }
9133
+ const r = await jobs.runForeground(async ({ signal, touch, onChunk }) => {
9134
+ const raw = await call({
9135
+ signal,
9136
+ onProgress: (p) => {
9137
+ touch();
9138
+ if (p?.message) onChunk(`[progress${p.progress != null ? ` ${p.progress}${p.total != null ? `/${p.total}` : ""}` : ""}] ${p.message}
9139
+ `);
9140
+ }
9141
+ });
9142
+ const res = toResult(raw);
9143
+ onChunk(res.text + (res.images?.length ? `
9144
+ [${res.images.length} image(s) in the result]` : ""));
9145
+ return res;
9146
+ }, { kind: "mcp", label: name, idleAfterActivity: true });
9147
+ if (r.done) return r.value.images?.length ? r.value : r.value.text;
9148
+ const soft = jobs.options.foregroundMs.mcp ?? 0;
9149
+ 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.`;
9150
+ }
8748
9151
  function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
8749
9152
  return {
8750
9153
  name: `${prefix}${spec.name}`,
8751
9154
  description: spec.description ?? `MCP tool ${spec.name}`,
8752
9155
  parameters: spec.inputSchema ?? { type: "object", properties: {} },
8753
9156
  maxDurationMs: timeoutMs,
8754
- async run(args, _ctx) {
8755
- const raw = await withDeadline(callTool(spec.name, args ?? {}), timeoutMs);
8756
- if (raw === TIMED_OUT) return mcpTimedOut(spec.name, timeoutMs);
8757
- const r = toResult(raw);
8758
- return r.images?.length ? r : r.text;
9157
+ run(args, ctx) {
9158
+ return runMcpCall(spec.name, (o) => callTool(spec.name, args ?? {}, o), ctx, timeoutMs);
8759
9159
  }
8760
9160
  };
8761
9161
  }
@@ -8796,13 +9196,10 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
8796
9196
  }
8797
9197
  },
8798
9198
  maxDurationMs: callTimeoutMs,
8799
- async run({ name, args }) {
9199
+ async run({ name, args }, ctx) {
8800
9200
  const n = String(name ?? "");
8801
9201
  if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
8802
- const raw = await withDeadline(callTool(n, args ?? {}), callTimeoutMs);
8803
- if (raw === TIMED_OUT) return mcpTimedOut(n, callTimeoutMs);
8804
- const r = toResult(raw);
8805
- return r.images?.length ? r : r.text;
9202
+ return runMcpCall(n, (o) => callTool(n, args ?? {}, o), ctx, callTimeoutMs);
8806
9203
  }
8807
9204
  };
8808
9205
  return [searchTool, callMcpTool];
@@ -8822,17 +9219,17 @@ function buildMcpCatalog(servers) {
8822
9219
  return { specs, routes };
8823
9220
  }
8824
9221
  function searchOverCatalog(servers, specs, routes, resolve5, options) {
8825
- const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
9222
+ const tools = specs.length ? makeMcpToolSearch(specs, (name, args, o) => {
8826
9223
  const r = routes.get(name);
8827
9224
  if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
8828
- return resolve5(r.server, r.rawName, args ?? {});
9225
+ return resolve5(r.server, r.rawName, args ?? {}, o);
8829
9226
  }, options) : [];
8830
9227
  return { tools, serverNames: servers, toolCount: specs.length };
8831
9228
  }
8832
9229
  function makeMcpToolSearchFromMounted(mounted, options) {
8833
9230
  const { specs, routes } = buildMcpCatalog(mounted);
8834
9231
  const byName = new Map(mounted.map((m) => [m.name, m]));
8835
- return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args) => byName.get(server).client.callTool(rawName, args), options);
9232
+ return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args, o) => byName.get(server).client.callTool(rawName, args, o), options);
8836
9233
  }
8837
9234
 
8838
9235
  // src/index.ts
@@ -8845,9 +9242,20 @@ import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor a
8845
9242
  init_logging();
8846
9243
  import { spawn } from "child_process";
8847
9244
  import { createHash } from "crypto";
8848
- var log15 = forComponent("mcp");
9245
+ var log16 = forComponent("mcp");
8849
9246
  var PROTOCOL_VERSION = "2025-06-18";
8850
9247
  var DEFAULT_TIMEOUT_MS = 3e4;
9248
+ var DEFAULT_CALL_TIMEOUT_MS = 30 * 6e4;
9249
+ function deadlineFor(method, spec, opts) {
9250
+ if (opts?.timeoutMs != null) return opts.timeoutMs;
9251
+ if (method === "tools/call") return spec.callTimeoutMs ?? Math.max(DEFAULT_CALL_TIMEOUT_MS, spec.timeoutMs ?? 0);
9252
+ return spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
9253
+ }
9254
+ function withProgressToken(params, token) {
9255
+ const p = params && typeof params === "object" ? params : {};
9256
+ return { ...p, _meta: { ...p._meta ?? {}, progressToken: token } };
9257
+ }
9258
+ var abortError = (signal) => signal.reason instanceof Error ? signal.reason : Object.assign(new Error("MCP request cancelled"), { name: "AbortError" });
8851
9259
  var StdioTransport = class {
8852
9260
  constructor(spec) {
8853
9261
  this.spec = spec;
@@ -8864,7 +9272,7 @@ var StdioTransport = class {
8864
9272
  proc.stdout.setEncoding("utf8");
8865
9273
  proc.stdout.on("data", (chunk) => this.onData(chunk));
8866
9274
  proc.stderr.setEncoding("utf8");
8867
- proc.stderr.on("data", (chunk) => log15.debug(`[${command}] stderr:`, chunk.trimEnd()));
9275
+ proc.stderr.on("data", (chunk) => log16.debug(`[${command}] stderr:`, chunk.trimEnd()));
8868
9276
  proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
8869
9277
  proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
8870
9278
  }
@@ -8878,21 +9286,32 @@ var StdioTransport = class {
8878
9286
  try {
8879
9287
  this.dispatch(JSON.parse(line));
8880
9288
  } catch (e) {
8881
- log15.debug("dropping non-JSON line from MCP server:", line, e);
9289
+ log16.debug("dropping non-JSON line from MCP server:", line, e);
8882
9290
  }
8883
9291
  }
8884
9292
  }
8885
9293
  dispatch(msg) {
9294
+ if (msg?.method === "notifications/progress") {
9295
+ const p2 = this.pending.get(msg.params?.progressToken);
9296
+ try {
9297
+ p2?.onProgress?.(msg.params ?? {});
9298
+ } catch (e) {
9299
+ log16.debug("onProgress handler threw", e);
9300
+ }
9301
+ return;
9302
+ }
8886
9303
  if (msg?.id == null || !this.pending.has(msg.id)) return;
8887
9304
  const p = this.pending.get(msg.id);
8888
9305
  this.pending.delete(msg.id);
8889
9306
  clearTimeout(p.timer);
9307
+ p.done?.();
8890
9308
  if (msg.error) p.reject(new Error(msg.error?.message ?? JSON.stringify(msg.error)));
8891
9309
  else p.resolve(msg.result);
8892
9310
  }
8893
9311
  failAll(e) {
8894
9312
  for (const p of this.pending.values()) {
8895
9313
  clearTimeout(p.timer);
9314
+ p.done?.();
8896
9315
  p.reject(e);
8897
9316
  }
8898
9317
  this.pending.clear();
@@ -8901,19 +9320,31 @@ var StdioTransport = class {
8901
9320
  if (!this.proc?.stdin) throw new Error("MCP stdio transport not started");
8902
9321
  this.proc.stdin.write(JSON.stringify(msg) + "\n");
8903
9322
  }
8904
- request(method, params) {
9323
+ request(method, params, opts = {}) {
8905
9324
  const id = this.nextId++;
8906
- const timeoutMs = this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
9325
+ const timeoutMs = deadlineFor(method, this.spec, opts);
9326
+ const { signal, onProgress } = opts;
8907
9327
  return new Promise((resolve5, reject) => {
8908
- const timer = setTimeout(() => {
9328
+ if (signal?.aborted) return reject(abortError(signal));
9329
+ const timer = timeoutMs > 0 ? setTimeout(() => {
8909
9330
  this.pending.delete(id);
9331
+ done();
8910
9332
  reject(new Error(`MCP request "${method}" timed out after ${timeoutMs}ms`));
8911
- }, timeoutMs);
8912
- this.pending.set(id, { resolve: resolve5, reject, timer });
9333
+ }, timeoutMs) : void 0;
9334
+ const onAbort = () => {
9335
+ if (!this.pending.delete(id)) return;
9336
+ clearTimeout(timer);
9337
+ this.notify("notifications/cancelled", { requestId: id, reason: String(signal?.reason?.message ?? "cancelled") }).catch((e) => log16.debug("cancel notify failed", e));
9338
+ reject(abortError(signal));
9339
+ };
9340
+ const done = () => signal?.removeEventListener("abort", onAbort);
9341
+ signal?.addEventListener("abort", onAbort, { once: true });
9342
+ this.pending.set(id, { resolve: resolve5, reject, timer, onProgress, done });
8913
9343
  try {
8914
- this.write({ jsonrpc: "2.0", id, method, params });
9344
+ this.write({ jsonrpc: "2.0", id, method, params: onProgress ? withProgressToken(params, id) : params });
8915
9345
  } catch (e) {
8916
9346
  clearTimeout(timer);
9347
+ done();
8917
9348
  this.pending.delete(id);
8918
9349
  reject(e);
8919
9350
  }
@@ -8927,7 +9358,7 @@ var StdioTransport = class {
8927
9358
  try {
8928
9359
  this.proc?.stdin?.end();
8929
9360
  } catch (e) {
8930
- log15.debug("stdin end failed", e);
9361
+ log16.debug("stdin end failed", e);
8931
9362
  }
8932
9363
  this.proc?.kill();
8933
9364
  }
@@ -8954,13 +9385,24 @@ var HttpTransport = class {
8954
9385
  fetchImpl;
8955
9386
  async start() {
8956
9387
  }
8957
- request(method, params) {
8958
- return this.post({ jsonrpc: "2.0", id: this.nextId++, method, params }, false);
9388
+ async request(method, params, opts = {}) {
9389
+ const id = this.nextId++;
9390
+ const deadline = AbortSignal.timeout(deadlineFor(method, this.spec, opts));
9391
+ const signal = opts.signal ? AbortSignal.any([opts.signal, deadline]) : deadline;
9392
+ try {
9393
+ return await this.post({ jsonrpc: "2.0", id, method, params: opts.onProgress ? withProgressToken(params, id) : params }, false, signal, opts.onProgress);
9394
+ } catch (e) {
9395
+ if (opts.signal?.aborted) {
9396
+ void this.notify("notifications/cancelled", { requestId: id, reason: String(opts.signal.reason?.message ?? "cancelled") }).catch((err2) => log16.debug("cancel notify failed", err2));
9397
+ throw abortError(opts.signal);
9398
+ }
9399
+ throw e;
9400
+ }
8959
9401
  }
8960
9402
  async notify(method, params) {
8961
- await this.post({ jsonrpc: "2.0", method, params }, true);
9403
+ await this.post({ jsonrpc: "2.0", method, params }, true, AbortSignal.timeout(this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS));
8962
9404
  }
8963
- async post(msg, isNotify) {
9405
+ async post(msg, isNotify, signal, onProgress) {
8964
9406
  const headers = {
8965
9407
  "content-type": "application/json",
8966
9408
  accept: "application/json, text/event-stream",
@@ -8973,7 +9415,7 @@ var HttpTransport = class {
8973
9415
  method: "POST",
8974
9416
  headers,
8975
9417
  body: JSON.stringify(msg),
8976
- signal: AbortSignal.timeout(this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS)
9418
+ signal
8977
9419
  });
8978
9420
  const sid = resp.headers.get("mcp-session-id");
8979
9421
  if (sid) this.sessionId = sid;
@@ -8981,25 +9423,63 @@ var HttpTransport = class {
8981
9423
  if (!resp.ok) throw new Error(`MCP HTTP ${resp.status} ${resp.statusText}`);
8982
9424
  if (isNotify) return;
8983
9425
  const ct = resp.headers.get("content-type") ?? "";
8984
- const data = ct.includes("text/event-stream") ? parseSseResponse(await resp.text()) : await resp.json();
9426
+ const data = ct.includes("text/event-stream") ? await readSseResponse(resp, msg.id, onProgress) : await resp.json();
8985
9427
  if (data?.error) throw new Error(data.error?.message ?? JSON.stringify(data.error));
8986
9428
  return data?.result;
8987
9429
  }
8988
9430
  async close() {
8989
9431
  }
8990
9432
  };
8991
- function parseSseResponse(body) {
8992
- for (const line of body.split("\n")) {
8993
- const trimmed = line.trimStart();
8994
- if (!trimmed.startsWith("data:")) continue;
8995
- try {
8996
- const obj = JSON.parse(trimmed.slice(5).trim());
8997
- if (obj && (obj.result !== void 0 || obj.error !== void 0)) return obj;
8998
- } catch (e) {
8999
- log15.debug("skipping unparseable SSE data line", e);
9433
+ function sseMessage(line) {
9434
+ const trimmed = line.trimStart();
9435
+ if (!trimmed.startsWith("data:")) return void 0;
9436
+ try {
9437
+ return JSON.parse(trimmed.slice(5).trim());
9438
+ } catch (e) {
9439
+ log16.debug("skipping unparseable SSE data line", e);
9440
+ return void 0;
9441
+ }
9442
+ }
9443
+ async function readSseResponse(resp, id, onProgress) {
9444
+ const handle = (obj) => {
9445
+ if (obj?.method === "notifications/progress") {
9446
+ try {
9447
+ onProgress?.(obj.params ?? {});
9448
+ } catch (e) {
9449
+ log16.debug("onProgress handler threw", e);
9450
+ }
9451
+ return void 0;
9000
9452
  }
9453
+ if (obj && (obj.result !== void 0 || obj.error !== void 0) && (obj.id == null || id == null || obj.id === id)) return obj;
9454
+ return void 0;
9455
+ };
9456
+ const reader = resp.body?.getReader?.();
9457
+ if (!reader) {
9458
+ for (const l of (await resp.text()).split("\n")) {
9459
+ const r = handle(sseMessage(l));
9460
+ if (r) return r;
9461
+ }
9462
+ return {};
9463
+ }
9464
+ const dec = new TextDecoder();
9465
+ let buf = "";
9466
+ try {
9467
+ for (; ; ) {
9468
+ const { value, done } = await reader.read();
9469
+ buf += done ? dec.decode() : dec.decode(value, { stream: true });
9470
+ let nl;
9471
+ while ((nl = buf.indexOf("\n")) >= 0 || done && buf) {
9472
+ const line = nl >= 0 ? buf.slice(0, nl) : buf;
9473
+ buf = nl >= 0 ? buf.slice(nl + 1) : "";
9474
+ const r = handle(sseMessage(line));
9475
+ if (r) return r;
9476
+ }
9477
+ if (done) return {};
9478
+ }
9479
+ } finally {
9480
+ reader.cancel().catch(() => {
9481
+ });
9001
9482
  }
9002
- return {};
9003
9483
  }
9004
9484
  var McpClient = class {
9005
9485
  constructor(transport, clientInfo = { name: "agentx", version: "0" }) {
@@ -9023,8 +9503,9 @@ var McpClient = class {
9023
9503
  const r = await this.transport.request("tools/list");
9024
9504
  return Array.isArray(r?.tools) ? r.tools : [];
9025
9505
  }
9026
- async callTool(name, args) {
9027
- return this.transport.request("tools/call", { name, arguments: args ?? {} });
9506
+ /** `tools/call`. Pass `onProgress` to request progress notifications (liveness) and `signal` to cancel. */
9507
+ async callTool(name, args, opts) {
9508
+ return this.transport.request("tools/call", { name, arguments: args ?? {} }, opts);
9028
9509
  }
9029
9510
  async listResources() {
9030
9511
  const r = await this.transport.request("resources/list");
@@ -9038,7 +9519,7 @@ var McpClient = class {
9038
9519
  }
9039
9520
  };
9040
9521
  function buildTransport(cfg) {
9041
- return cfg.url ? new HttpTransport({ url: cfg.url, headers: cfg.headers, bearerToken: cfg.bearerToken, timeoutMs: cfg.timeoutMs }) : new StdioTransport({ command: cfg.command, args: cfg.args, env: cfg.env, cwd: cfg.cwd, timeoutMs: cfg.timeoutMs });
9522
+ return cfg.url ? new HttpTransport({ url: cfg.url, headers: cfg.headers, bearerToken: cfg.bearerToken, timeoutMs: cfg.timeoutMs, callTimeoutMs: cfg.callTimeoutMs }) : new StdioTransport({ command: cfg.command, args: cfg.args, env: cfg.env, cwd: cfg.cwd, timeoutMs: cfg.timeoutMs, callTimeoutMs: cfg.callTimeoutMs });
9042
9523
  }
9043
9524
  function withTimeout(p, ms, label) {
9044
9525
  if (!ms || ms <= 0) return p;
@@ -9060,11 +9541,11 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
9060
9541
  return await withTimeout((async () => {
9061
9542
  const init = await client.connect();
9062
9543
  const specs = await client.listTools();
9063
- const tools = mcpToolsToAgentTools(specs, (tool, a) => client.callTool(tool, a), `mcp__${name}__`);
9544
+ const tools = mcpToolsToAgentTools(specs, (tool, a, o) => client.callTool(tool, a, o), `mcp__${name}__`);
9064
9545
  return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
9065
9546
  })(), mountTimeoutMs, name);
9066
9547
  } catch (e) {
9067
- await client.close().catch((err2) => log15.debug(`close after failed mount of "${name}": ${err2}`));
9548
+ await client.close().catch((err2) => log16.debug(`close after failed mount of "${name}": ${err2}`));
9068
9549
  throw e;
9069
9550
  }
9070
9551
  }
@@ -9075,15 +9556,15 @@ function validEntries(servers) {
9075
9556
  return Object.entries(servers).filter(([name, cfg]) => {
9076
9557
  if (!cfg || cfg.disabled) return false;
9077
9558
  if (!cfg.command && !cfg.url) {
9078
- log15.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
9559
+ log16.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
9079
9560
  return false;
9080
9561
  }
9081
9562
  return true;
9082
9563
  });
9083
9564
  }
9084
9565
  function logMountFailure(name, e) {
9085
- if (e instanceof McpAuthError) log15.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
9086
- else log15.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
9566
+ if (e instanceof McpAuthError) log16.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
9567
+ else log16.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
9087
9568
  }
9088
9569
  async function mountMcpServers(servers = {}, opts = {}) {
9089
9570
  const entries = validEntries(servers);
@@ -9093,7 +9574,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
9093
9574
  const name = entries[i][0];
9094
9575
  if (r.status === "fulfilled") {
9095
9576
  out.push(r.value);
9096
- log15.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
9577
+ log16.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
9097
9578
  } else logMountFailure(name, r.reason);
9098
9579
  });
9099
9580
  return out;
@@ -9133,7 +9614,7 @@ var McpPool = class {
9133
9614
  const prev = this.warm.get(key);
9134
9615
  if (prev) {
9135
9616
  clearTimeout(prev.timer);
9136
- if (prev.client !== client) void prev.client.close().catch((err2) => log15.debug(`warm-pool replace close failed: ${err2}`));
9617
+ if (prev.client !== client) void prev.client.close().catch((err2) => log16.debug(`warm-pool replace close failed: ${err2}`));
9137
9618
  }
9138
9619
  const e = { client, timer: void 0 };
9139
9620
  this.warm.set(key, e);
@@ -9150,7 +9631,7 @@ var McpPool = class {
9150
9631
  const e = this.warm.get(key);
9151
9632
  if (!e) return;
9152
9633
  this.warm.delete(key);
9153
- await e.client.close().catch((err2) => log15.debug(`warm-pool evict close failed: ${err2}`));
9634
+ await e.client.close().catch((err2) => log16.debug(`warm-pool evict close failed: ${err2}`));
9154
9635
  }
9155
9636
  async closeAll() {
9156
9637
  for (const e of this.warm.values()) {
@@ -9368,7 +9849,7 @@ init_tools_shell();
9368
9849
  // src/tools.notify.ts
9369
9850
  init_logging();
9370
9851
  import { execFile } from "child_process";
9371
- var log17 = forComponent("notify");
9852
+ var log18 = forComponent("notify");
9372
9853
  function makeNotifyTool(opts = {}) {
9373
9854
  const platform2 = opts.platform ?? process.platform;
9374
9855
  const run = opts.exec ?? execFile;
@@ -9392,7 +9873,7 @@ function makeNotifyTool(opts = {}) {
9392
9873
  return new Promise((resolve5) => {
9393
9874
  run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
9394
9875
  if (e) {
9395
- log17.debug("notification failed", e);
9876
+ log18.debug("notification failed", e);
9396
9877
  resolve5(`Notification failed: ${e.message}`);
9397
9878
  } else resolve5("Notification shown.");
9398
9879
  });
@@ -9408,7 +9889,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
9408
9889
  init_logging();
9409
9890
  import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
9410
9891
  import { homedir } from "os";
9411
- var log18 = forComponent("cli-util");
9892
+ var log19 = forComponent("cli-util");
9412
9893
  function dotDirs(base, sub, opts = {}) {
9413
9894
  const home = opts.home ?? homedir();
9414
9895
  const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
@@ -9425,7 +9906,7 @@ function parseJson(text, fallback, what = "json") {
9425
9906
  try {
9426
9907
  return JSON.parse(text);
9427
9908
  } catch (e) {
9428
- log18.debug(`parseJson(${what}) failed: ${e.message}`);
9909
+ log19.debug(`parseJson(${what}) failed: ${e.message}`);
9429
9910
  return fallback;
9430
9911
  }
9431
9912
  }
@@ -9435,7 +9916,7 @@ function readJsonFile(path, fallback) {
9435
9916
  try {
9436
9917
  text = readFileSync4(path, "utf8");
9437
9918
  } catch (e) {
9438
- log18.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
9919
+ log19.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
9439
9920
  return fallback;
9440
9921
  }
9441
9922
  return parseJson(text, fallback, path);
@@ -9490,9 +9971,9 @@ function toCursorMcp(servers) {
9490
9971
  }
9491
9972
  return Object.keys(out).length ? { mcpServers: out } : void 0;
9492
9973
  }
9493
- function cursorProviderOptions(model, cwd, mcpServers) {
9974
+ function cursorProviderOptions(model, cwd, mcpServers, hostMcpServers) {
9494
9975
  if (!(model ?? "").startsWith("cursor/")) return void 0;
9495
- return { cwd, ...toCursorMcp(mcpServers) ?? {}, ...process.env.CURSOR_WARM === "0" ? {} : { cursorSession: randomUUID() } };
9976
+ return { cwd, ...toCursorMcp(mcpServers) ?? {}, ...hostMcpServers?.length ? { hostMcpServers } : {}, ...process.env.CURSOR_WARM === "0" ? {} : { cursorSession: randomUUID() } };
9496
9977
  }
9497
9978
  function toClaudeCodeMcp(servers) {
9498
9979
  if (!servers) return void 0;
@@ -9519,8 +10000,8 @@ function claudeCodeProviderOptions(model, cwd, mcpServers) {
9519
10000
  const warm = process.env.AGENTX_CC_WARM === "0" ? {} : { claudeCodeSession: randomUUID() };
9520
10001
  return { cwd, ...env ? { env } : {}, ...warm, ...toClaudeCodeMcp(mcpServers) ?? {} };
9521
10002
  }
9522
- function providerOptionsFor(model, cwd, mcpServers) {
9523
- return cursorProviderOptions(model, cwd, mcpServers) ?? claudeCodeProviderOptions(model, cwd, mcpServers);
10003
+ function providerOptionsFor(model, cwd, mcpServers, hostMcpServers) {
10004
+ return cursorProviderOptions(model, cwd, mcpServers, hostMcpServers) ?? claudeCodeProviderOptions(model, cwd, mcpServers);
9524
10005
  }
9525
10006
  async function buildAgent(o) {
9526
10007
  if (typeof o.sandbox !== "boolean")
@@ -9605,12 +10086,14 @@ Reference files in them by their mount path (the left side).`;
9605
10086
  const hooks = o.learnFromMistakes && memoryWriteDir ? composeHooks(o.hooks, lessonCapture({ fs, dir: memoryWriteDir, minRepeats: 2 })) : o.hooks;
9606
10087
  let realShell = [];
9607
10088
  const agentRef = {};
10089
+ const jobRegistry = new JobRegistry();
9608
10090
  const useRealShell = o.realShell ?? !virtual;
9609
10091
  if (useRealShell && !virtual && !isClaudeCode) {
9610
10092
  const jobs = new ShellJobRegistry({
9611
10093
  cwd,
9612
10094
  killOnExit: true,
9613
10095
  osSandbox: o.osSandbox,
10096
+ jobs: jobRegistry,
9614
10097
  onExit: (n) => agentRef.agent?.inject(formatJobExit(n))
9615
10098
  });
9616
10099
  realShell = [makeRealShellTool({ cwd, registry: jobs, osSandbox: o.osSandbox, jobsSurviveRun: o.singleRun !== true }), ...makeShellJobTools(jobs)];
@@ -9619,6 +10102,7 @@ Reference files in them by their mount path (the left side).`;
9619
10102
  const scratch = scratchDir ? new Scratch(fs, { dir: scratchDir }) : void 0;
9620
10103
  return agentRef.agent = new Agent({
9621
10104
  ai: o.ai,
10105
+ jobs: jobRegistry,
9622
10106
  fs,
9623
10107
  model: o.model ?? "anthropic/claude-sonnet-4-6",
9624
10108
  // PDF reads (disk mode only — VFS paths aren't real files): poppler's pdftotext when installed.
@@ -9630,7 +10114,7 @@ Reference files in them by their mount path (the left side).`;
9630
10114
  // Warm cursor session (default on; CURSOR_WARM=0 to disable): one long-lived agent reused across
9631
10115
  // turns, amortising the ~2s Agent.create + h2 handshake. Keyed per agentx launch.
9632
10116
  ...(() => {
9633
- const po = providerOptionsFor(o.model, cwd, o.mcpServers);
10117
+ const po = providerOptionsFor(o.model, cwd, o.mcpServers, o.hostMcpServers);
9634
10118
  return po ? { providerOptions: po } : {};
9635
10119
  })(),
9636
10120
  ...(() => {
@@ -9731,7 +10215,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
9731
10215
  import { homedir as homedir3 } from "os";
9732
10216
  import { dirname as dirname3, join as join6 } from "path";
9733
10217
  import { fileURLToPath } from "url";
9734
- var log19 = forComponent("VoiceIO");
10218
+ var log20 = forComponent("VoiceIO");
9735
10219
  var now3 = () => performance.now();
9736
10220
  var Player = class {
9737
10221
  proc = null;
@@ -9745,7 +10229,7 @@ var Player = class {
9745
10229
  ["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
9746
10230
  { stdio: ["pipe", "ignore", "ignore"] }
9747
10231
  );
9748
- this.proc.on("error", (e) => log19.warn(`ffplay error: ${e.message}`));
10232
+ this.proc.on("error", (e) => log20.warn(`ffplay error: ${e.message}`));
9749
10233
  this.proc.stdin.on("error", () => {
9750
10234
  });
9751
10235
  this.bytesWritten = 0;
@@ -9783,7 +10267,7 @@ function detectFfmpegMic() {
9783
10267
  const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
9784
10268
  const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
9785
10269
  if (!mic) throw new Error("no audio input device found");
9786
- log19.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
10270
+ log20.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
9787
10271
  return `:${mic.idx}`;
9788
10272
  }
9789
10273
  function detectedInputDevice() {
@@ -9819,15 +10303,15 @@ function resolveAecBinary() {
9819
10303
  if (existsSync4(bin) && statSync4(bin).mtimeMs >= statSync4(src).mtimeMs) return bin;
9820
10304
  if (spawnSync2("which", ["swiftc"]).status !== 0) return null;
9821
10305
  mkdirSync5(cacheDir, { recursive: true });
9822
- log19.info("compiling AEC mic helper (first run)\u2026");
10306
+ log20.info("compiling AEC mic helper (first run)\u2026");
9823
10307
  const build = spawnSync2("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
9824
10308
  if (build.status !== 0) {
9825
- log19.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
10309
+ log20.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
9826
10310
  return null;
9827
10311
  }
9828
10312
  const sign = spawnSync2("codesign", ["-fs", "-", bin], { encoding: "utf8" });
9829
10313
  if (sign.status !== 0) {
9830
- log19.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
10314
+ log20.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
9831
10315
  return null;
9832
10316
  }
9833
10317
  return bin;
@@ -9869,16 +10353,16 @@ var NodeMicSource = class {
9869
10353
  this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
9870
10354
  } else {
9871
10355
  if (spawnSync2("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
9872
- log19.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
10356
+ log20.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
9873
10357
  this.proc = spawn2(
9874
10358
  "ffmpeg",
9875
10359
  ["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
9876
10360
  { stdio: ["ignore", "pipe", "pipe"] }
9877
10361
  );
9878
- this.proc.stderr.on("data", (d) => log19.warn(`ffmpeg: ${String(d).trim()}`));
10362
+ this.proc.stderr.on("data", (d) => log20.warn(`ffmpeg: ${String(d).trim()}`));
9879
10363
  }
9880
10364
  this.proc.on("exit", (c) => {
9881
- if (c && !this.stopped) log19.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
10365
+ if (c && !this.stopped) log20.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
9882
10366
  });
9883
10367
  this.proc.stdout.on("data", (chunk) => onChunk(chunk));
9884
10368
  }
@@ -9944,7 +10428,7 @@ var AecDuplexAudio = class {
9944
10428
  this.proc.stdin.on("error", () => {
9945
10429
  });
9946
10430
  this.proc.on("exit", (c) => {
9947
- if (c && !this.stopped) log19.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
10431
+ if (c && !this.stopped) log20.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
9948
10432
  });
9949
10433
  this.proc.stdout.on("data", (chunk) => {
9950
10434
  this.gotChunk = true;
@@ -9960,7 +10444,7 @@ var AecDuplexAudio = class {
9960
10444
  openMicSettings();
9961
10445
  this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
9962
10446
  }
9963
- } else log19.debug(`mic-aec: ${s}`);
10447
+ } else log20.debug(`mic-aec: ${s}`);
9964
10448
  }
9965
10449
  });
9966
10450
  if (!this.noVpio && !this.triedFallback) {
@@ -9969,7 +10453,7 @@ var AecDuplexAudio = class {
9969
10453
  this.triedFallback = true;
9970
10454
  this.noVpio = true;
9971
10455
  this._aec = false;
9972
- log19.warn("mic-aec: VPIO delivered no audio in 2.5s \u2014 falling back to non-VPIO capture (no AEC \u2192 half-duplex, no barge-in)");
10456
+ log20.warn("mic-aec: VPIO delivered no audio in 2.5s \u2014 falling back to non-VPIO capture (no AEC \u2192 half-duplex, no barge-in)");
9973
10457
  this.onDegrade?.();
9974
10458
  this.killProc();
9975
10459
  this.spawnHelper();
@@ -10189,7 +10673,7 @@ function fakeVoiceParts(uttFile) {
10189
10673
  init_logging();
10190
10674
  import { appendFile, mkdirSync as mkdirSync6 } from "fs";
10191
10675
  import { dirname as dirname4 } from "path";
10192
- var log20 = forComponent("VoiceDiag");
10676
+ var log21 = forComponent("VoiceDiag");
10193
10677
  var JsonlDiagSink = class {
10194
10678
  constructor(path) {
10195
10679
  this.path = path;
@@ -10230,7 +10714,7 @@ var JsonlDiagSink = class {
10230
10714
  disable(e) {
10231
10715
  this.disabled = true;
10232
10716
  this.queue.length = 0;
10233
- log20.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
10717
+ log21.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
10234
10718
  }
10235
10719
  /** True once a failure permanently disabled the sink (tests/inspection). */
10236
10720
  get isDisabled() {
@@ -10300,7 +10784,7 @@ async function loadConfig(cwd) {
10300
10784
 
10301
10785
  // cli/hooks-config.ts
10302
10786
  import { spawnSync as spawnSync3 } from "child_process";
10303
- var log21 = forComponent("hooks");
10787
+ var log22 = forComponent("hooks");
10304
10788
  var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
10305
10789
  function ruleMatches(rule, toolName) {
10306
10790
  if (!rule.tool || rule.tool === "*") return true;
@@ -10317,7 +10801,7 @@ function runCmd(rule, env) {
10317
10801
  });
10318
10802
  return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
10319
10803
  } catch (e) {
10320
- log21.debug(`hook command failed: ${rule.command}`, e);
10804
+ log22.debug(`hook command failed: ${rule.command}`, e);
10321
10805
  return { code: 1, out: String(e?.message ?? e) };
10322
10806
  }
10323
10807
  }
@@ -10424,7 +10908,7 @@ function formatDiff(ops, opts = {}) {
10424
10908
  import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync } from "fs";
10425
10909
  import { homedir as homedir5 } from "os";
10426
10910
  import { join as join8 } from "path";
10427
- var log22 = forComponent("session");
10911
+ var log23 = forComponent("session");
10428
10912
  var globalDir = () => join8(homedir5(), ".agent", "sessions");
10429
10913
  var SessionStore = class {
10430
10914
  dir;
@@ -10469,7 +10953,7 @@ var SessionStore = class {
10469
10953
  }
10470
10954
  load(id) {
10471
10955
  if (!this.safeId(id)) {
10472
- log22.debug(`rejecting unsafe session id: ${id}`);
10956
+ log23.debug(`rejecting unsafe session id: ${id}`);
10473
10957
  return void 0;
10474
10958
  }
10475
10959
  const path = join8(this.dir, `${id}.json`);
@@ -10477,7 +10961,7 @@ var SessionStore = class {
10477
10961
  try {
10478
10962
  return JSON.parse(readFileSync7(path, "utf8"));
10479
10963
  } catch (e) {
10480
- log22.debug(`unreadable session ${id} \u2014 ignoring`, e);
10964
+ log23.debug(`unreadable session ${id} \u2014 ignoring`, e);
10481
10965
  return void 0;
10482
10966
  }
10483
10967
  }
@@ -10490,7 +10974,7 @@ var SessionStore = class {
10490
10974
  try {
10491
10975
  metas.push(JSON.parse(readFileSync7(join8(this.dir, f), "utf8")).meta);
10492
10976
  } catch (e) {
10493
- log22.debug(`skipping unreadable session file ${f}`, e);
10977
+ log23.debug(`skipping unreadable session file ${f}`, e);
10494
10978
  }
10495
10979
  }
10496
10980
  return metas.sort((a, b) => b.updated - a.updated);
@@ -10659,7 +11143,7 @@ import { promisify } from "util";
10659
11143
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync8 } from "fs";
10660
11144
  import { homedir as homedir6 } from "os";
10661
11145
  import { join as join9, resolve as resolve3, sep as sep3, parse as parsePath } from "path";
10662
- var log23 = forComponent("checkpoints");
11146
+ var log24 = forComponent("checkpoints");
10663
11147
  var exec = promisify(execFile3);
10664
11148
  var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
10665
11149
  function isBroadRoot(p) {
@@ -10705,7 +11189,7 @@ var ShadowRepo = class {
10705
11189
  writeFileSync5(join9(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
10706
11190
  this.ready = true;
10707
11191
  } catch (e) {
10708
- log23.debug(`git checkpoints unavailable for ${this.workTree}`, e);
11192
+ log24.debug(`git checkpoints unavailable for ${this.workTree}`, e);
10709
11193
  this.ready = false;
10710
11194
  }
10711
11195
  return this.ready;
@@ -10716,7 +11200,7 @@ var ShadowRepo = class {
10716
11200
  }
10717
11201
  async commit(label, forced = []) {
10718
11202
  await this.run("add", "-A");
10719
- for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log23.debug(`force-add failed: ${p}`, e));
11203
+ for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log24.debug(`force-add failed: ${p}`, e));
10720
11204
  await this.run("commit", "--allow-empty", "-q", "-m", label);
10721
11205
  }
10722
11206
  /** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
@@ -10724,8 +11208,8 @@ var ShadowRepo = class {
10724
11208
  * turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
10725
11209
  * point per turn, so the REPL's turn↔frame mapping stays intact. */
10726
11210
  async amendForced(paths) {
10727
- for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log23.debug(`force-capture failed: ${p}`, e));
10728
- await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log23.debug("amend failed", e));
11211
+ for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log24.debug(`force-capture failed: ${p}`, e));
11212
+ await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log24.debug("amend failed", e));
10729
11213
  }
10730
11214
  /** Commits on `ref`, oldest-first (canonical index space). */
10731
11215
  async log(ref) {
@@ -10785,7 +11269,7 @@ var ShadowRepo = class {
10785
11269
  await this.run("gc", "--auto", "-q").catch(() => {
10786
11270
  });
10787
11271
  } catch (e) {
10788
- log23.debug("checkpoint prune failed", e);
11272
+ log24.debug("checkpoint prune failed", e);
10789
11273
  }
10790
11274
  }
10791
11275
  };
@@ -10853,7 +11337,7 @@ var GitCheckpoints = class {
10853
11337
  use(sessionId) {
10854
11338
  if (sessionId === this.session) return;
10855
11339
  this.session = sessionId;
10856
- if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log23.debug("re-point failed", e));
11340
+ if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log24.debug("re-point failed", e));
10857
11341
  }
10858
11342
  async begin(label) {
10859
11343
  if (!await this.start()) return;
@@ -10873,7 +11357,7 @@ var GitCheckpoints = class {
10873
11357
  process.stderr.write(`\x1B[2m checkpoints: '${this.repos[i].root}' too large to snapshot in ${Math.round(this.options.opTimeoutMs / 1e3)}s \u2014 file rewind disabled here\x1B[0m
10874
11358
  `);
10875
11359
  } else {
10876
- log23.debug("checkpoint commit failed", e);
11360
+ log24.debug("checkpoint commit failed", e);
10877
11361
  survivors.push(this.repos[i]);
10878
11362
  survivorCaches.push(this.caches[i] ?? []);
10879
11363
  }
@@ -10908,7 +11392,7 @@ var GitCheckpoints = class {
10908
11392
  if (this.forced.has(abs)) continue;
10909
11393
  this.forced.add(abs);
10910
11394
  const i = this.repoIndexFor(abs);
10911
- if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log23.debug("amendForced failed", e));
11395
+ if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log24.debug("amendForced failed", e));
10912
11396
  }
10913
11397
  }
10914
11398
  };
@@ -12731,7 +13215,7 @@ import { spawnSync as spawnSync5 } from "child_process";
12731
13215
  import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
12732
13216
  import { homedir as homedir8 } from "os";
12733
13217
  import { join as join12 } from "path";
12734
- var log24 = forComponent("os-sched");
13218
+ var log25 = forComponent("os-sched");
12735
13219
  var OsScheduler = class {
12736
13220
  options;
12737
13221
  constructor(options) {
@@ -12793,7 +13277,7 @@ var OsScheduler = class {
12793
13277
  }
12794
13278
  }
12795
13279
  } catch (e) {
12796
- log24.debug(`cancel ${id}`, e);
13280
+ log25.debug(`cancel ${id}`, e);
12797
13281
  }
12798
13282
  for (const f of [`${id}.json`, `${id}.sh`]) {
12799
13283
  try {
@@ -12910,7 +13394,7 @@ import { spawn as spawn3 } from "child_process";
12910
13394
  import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
12911
13395
  import { homedir as homedir9 } from "os";
12912
13396
  import { join as join13 } from "path";
12913
- var log25 = forComponent("remote-trigger");
13397
+ var log26 = forComponent("remote-trigger");
12914
13398
  var TRIGGER_DIR = () => join13(homedir9(), ".agent", "triggers");
12915
13399
  var sockPath = (sessionId, dir = TRIGGER_DIR()) => join13(dir, `${sessionId}.sock`);
12916
13400
  var TriggerServer = class {
@@ -12948,13 +13432,13 @@ var TriggerServer = class {
12948
13432
  conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
12949
13433
  }
12950
13434
  });
12951
- conn.on("error", (e) => log25.debug("trigger conn error", e));
13435
+ conn.on("error", (e) => log26.debug("trigger conn error", e));
12952
13436
  });
12953
- this.server.on("error", (e) => log25.debug("trigger server error", e));
13437
+ this.server.on("error", (e) => log26.debug("trigger server error", e));
12954
13438
  this.server.listen(p);
12955
13439
  this.path = p;
12956
13440
  } catch (e) {
12957
- log25.debug("trigger server unavailable", e);
13441
+ log26.debug("trigger server unavailable", e);
12958
13442
  }
12959
13443
  }
12960
13444
  /** Re-bind on /resume (the session id changed). */
@@ -13076,7 +13560,7 @@ var strike = C("9");
13076
13560
  var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
13077
13561
  var mdPalette = { bold, dim, cyan, italic, strike, green, link };
13078
13562
  var err = (s) => process.stderr.write(s);
13079
- var log26 = forComponent("cli");
13563
+ var log27 = forComponent("cli");
13080
13564
  var VERSION = (() => {
13081
13565
  try {
13082
13566
  return JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
@@ -13126,9 +13610,9 @@ var spinner = /* @__PURE__ */ (() => {
13126
13610
  if (!tty || timer) return;
13127
13611
  t0 = this.turnStart || Date.now();
13128
13612
  timer = setInterval(() => {
13129
- const secs = Math.round((Date.now() - t0) / 1e3);
13613
+ const secs2 = Math.round((Date.now() - t0) / 1e3);
13130
13614
  const tail = this.tail?.();
13131
- err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label} ${secs ? `${secs}s \xB7 ` : ""}esc to interrupt`) + (tail ? " " + tail : ""));
13615
+ err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label} ${secs2 ? `${secs2}s \xB7 ` : ""}esc to interrupt`) + (tail ? " " + tail : ""));
13132
13616
  }, 90);
13133
13617
  },
13134
13618
  stop() {
@@ -13821,7 +14305,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
13821
14305
  function turnCost(model, usage) {
13822
14306
  return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
13823
14307
  }
13824
- async function evaluateGoal(ai, condition, transcript, log27) {
14308
+ async function evaluateGoal(ai, condition, transcript, log28) {
13825
14309
  const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
13826
14310
  const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
13827
14311
  return text.slice(0, 600);
@@ -13841,7 +14325,7 @@ ${recent}` }
13841
14325
  const match = r.content.match(/\{[\s\S]*\}/);
13842
14326
  if (match) return JSON.parse(match[0]);
13843
14327
  } catch (e) {
13844
- log27(dim(` (goal evaluator error: ${e?.message ?? e})
14328
+ log28(dim(` (goal evaluator error: ${e?.message ?? e})
13845
14329
  `));
13846
14330
  }
13847
14331
  return { met: false, reason: "evaluation unclear" };
@@ -14009,7 +14493,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
14009
14493
  anthropicWebSearch: cfg.anthropicWebSearch
14010
14494
  };
14011
14495
  }
14012
- async function makeAgent(args, ai, cfg, extraTools = []) {
14496
+ async function makeAgent(args, ai, cfg, extraTools = [], hostMcpServers) {
14013
14497
  const virtual = args.vfs || !!args.boddb;
14014
14498
  if (virtual && args.shell) err(yellow(` \u26A0 --shell ignored in ${args.boddb ? "--boddb" : "--vfs sandbox"} (a real shell would escape to disk)
14015
14499
  `));
@@ -14023,7 +14507,7 @@ async function makeAgent(args, ai, cfg, extraTools = []) {
14023
14507
  else if (args.shell === false) err(dim(" \u2296 --no-shell: VFS bash only (no real /bin/sh)\n"));
14024
14508
  if (args.harden && !virtual) err(dim(` \u26E8 hardened shell: writes confined to cwd+tmp${args.hardenNet ? "" : ", network blocked"} (sandbox-exec/bwrap)
14025
14509
  `));
14026
- const opts = optsFor(args, ai, cfg, extraTools);
14510
+ const opts = { ...optsFor(args, ai, cfg, extraTools), hostMcpServers };
14027
14511
  const agent = await buildAgent(opts);
14028
14512
  const display = displayHooks(agent.options.fs, { flush: agent.options.host?.flushText });
14029
14513
  agent.options.hooks = cfg.hooks ? composeHooks(display, hooksFromConfig(cfg.hooks)) : display;
@@ -14062,7 +14546,7 @@ function mcpAgentTools(mounted, opts) {
14062
14546
  return tools;
14063
14547
  }
14064
14548
  async function closeMcp(mounted) {
14065
- await Promise.all(mounted.map((m) => m.client.close().catch((e) => log26.debug("mcp close failed", e))));
14549
+ await Promise.all(mounted.map((m) => m.client.close().catch((e) => log27.debug("mcp close failed", e))));
14066
14550
  }
14067
14551
  var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
14068
14552
  function mentionRefs(line) {
@@ -14114,7 +14598,7 @@ async function expandMentions(fs, line) {
14114
14598
  if (loaded.includes(ref) || missing.includes(ref)) continue;
14115
14599
  if (ref.includes(":") && mcpMentionResolver) {
14116
14600
  const body = await mcpMentionResolver(ref).catch((e) => {
14117
- log26.debug("mcp mention resolve failed", e);
14601
+ log27.debug("mcp mention resolve failed", e);
14118
14602
  return null;
14119
14603
  });
14120
14604
  if (body != null) {
@@ -14201,7 +14685,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
14201
14685
  try {
14202
14686
  store.save(session);
14203
14687
  } catch (ex) {
14204
- log26.debug("mid-turn session flush failed", ex);
14688
+ log27.debug("mid-turn session flush failed", ex);
14205
14689
  }
14206
14690
  }
14207
14691
  return origNotify(e);
@@ -14228,7 +14712,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
14228
14712
  }
14229
14713
  agent.options.host?.flushText?.();
14230
14714
  flushLookupGroup();
14231
- const secs = ((Date.now() - t0) / 1e3).toFixed(1);
14715
+ const secs2 = ((Date.now() - t0) / 1e3).toFixed(1);
14232
14716
  const cost = turnCost(agent.options.model, res.usage);
14233
14717
  const m = res.usageEstimated ? "~" : "";
14234
14718
  const tok = res.usage?.totalTokens ? `${m}${(res.usage.totalTokens / 1e3).toFixed(1)}k tok \xB7 ${cost > 0 ? m + fmtUsd(cost) + " \xB7 " : ""}` : "";
@@ -14239,7 +14723,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
14239
14723
  const didWork = agent.transcript.slice(msgsBefore).some((m2) => m2.role === "assistant" || m2.role === "tool");
14240
14724
  const silentAbort = res.finishReason === "aborted" && !res.usage?.totalTokens && !didWork;
14241
14725
  if (!silentAbort)
14242
- err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : res.finishReason === "needs_input" ? yellow(" ? needs input") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs}s \xB7 ${shortId}
14726
+ err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : res.finishReason === "needs_input" ? yellow(" ? needs input") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs2}s \xB7 ${shortId}
14243
14727
  `));
14244
14728
  if (!silentAbort && process.stderr.isTTY && Date.now() - t0 > 1e4) err("\x07");
14245
14729
  if (res.finishReason === "error" && res.error) {
@@ -14350,14 +14834,14 @@ var isCancelTeardown = (e) => {
14350
14834
  function installCancelGuards(mounted) {
14351
14835
  process.on("unhandledRejection", (e) => {
14352
14836
  if (isCancelTeardown(e)) {
14353
- log26.debug("suppressed unhandledRejection (cursor stream cancel)", e);
14837
+ log27.debug("suppressed unhandledRejection (cursor stream cancel)", e);
14354
14838
  return;
14355
14839
  }
14356
- log26.error("unhandledRejection", e);
14840
+ log27.error("unhandledRejection", e);
14357
14841
  });
14358
14842
  process.on("uncaughtException", (e) => {
14359
14843
  if (isCancelTeardown(e)) {
14360
- log26.debug("suppressed uncaughtException (cursor stream cancel)", e);
14844
+ log27.debug("suppressed uncaughtException (cursor stream cancel)", e);
14361
14845
  return;
14362
14846
  }
14363
14847
  console.error(e);
@@ -14371,7 +14855,7 @@ async function repl(args, ai, cfg, cwd) {
14371
14855
  let mcpToolNames = [];
14372
14856
  const initialMcpTools = mcpAgentTools(mounted);
14373
14857
  mcpToolNames = initialMcpTools.map((t) => t.name);
14374
- const agent = await makeAgent(args, ai, cfg, initialMcpTools);
14858
+ const agent = await makeAgent(args, ai, cfg, initialMcpTools, mounted.map((m) => m.name));
14375
14859
  if (args.voice && !args.duplex) agent.options.tools = [...agent.options.tools ?? [], exitSessionTool(() => {
14376
14860
  exitRequested = true;
14377
14861
  })];
@@ -14613,7 +15097,7 @@ async function repl(args, ai, cfg, cwd) {
14613
15097
  ...args.voiceModel ?? cfg.reflexModel ? { reflexModel: resolveModelOrNewest(args.voiceModel ?? cfg.reflexModel) } : {},
14614
15098
  actModel: agent.options.model,
14615
15099
  actOptions: workerOptions,
14616
- providerOptionsFor: (m) => providerOptionsFor(m, cwd, cfg.mcpServers),
15100
+ providerOptionsFor: (m) => providerOptionsFor(m, cwd, cfg.mcpServers, mounted.map((x) => x.name)),
14617
15101
  ...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
14618
15102
  host,
14619
15103
  ...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true, emotionTags: emotionsOn } : {},
@@ -14695,7 +15179,7 @@ async function repl(args, ai, cfg, cwd) {
14695
15179
  const setModel = (m) => {
14696
15180
  work.model = m;
14697
15181
  work.contextWindow = getModelInfo(m)?.contextWindow ?? 0;
14698
- work.providerOptions = providerOptionsFor(m, cwd, cfg.mcpServers);
15182
+ work.providerOptions = providerOptionsFor(m, cwd, cfg.mcpServers, mounted.map((x) => x.name));
14699
15183
  if (dx) dx.options.actModel = m;
14700
15184
  persistModel(cwd, m);
14701
15185
  err(dim(" model \u2192 " + m + "\n"));
@@ -14762,7 +15246,7 @@ async function repl(args, ai, cfg, cwd) {
14762
15246
  try {
14763
15247
  mkdirSync12(dir, { recursive: true });
14764
15248
  } catch (e) {
14765
- log26.debug("paste image dir create failed", e);
15249
+ log27.debug("paste image dir create failed", e);
14766
15250
  }
14767
15251
  const img = grabClipboardImage(dir, String(Date.now()));
14768
15252
  return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
@@ -14875,7 +15359,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
14875
15359
  mkdirSync12(join14(cwd, ".agent"), { recursive: true });
14876
15360
  appendFileSync(histPath, line + "\n");
14877
15361
  } catch (e) {
14878
- log26.debug("history write failed", e);
15362
+ log27.debug("history write failed", e);
14879
15363
  }
14880
15364
  };
14881
15365
  const ago = (t) => {
@@ -14947,7 +15431,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
14947
15431
  try {
14948
15432
  store.save(session);
14949
15433
  } catch (e) {
14950
- log26.debug("session save after rewind failed", e);
15434
+ log27.debug("session save after rewind failed", e);
14951
15435
  }
14952
15436
  err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
14953
15437
  `));
@@ -14981,7 +15465,7 @@ ${task}`;
14981
15465
  bangContext.length = 0;
14982
15466
  }
14983
15467
  const delta = await refreshCatalogs().catch((e) => {
14984
- log26.debug("catalog refresh failed", e);
15468
+ log27.debug("catalog refresh failed", e);
14985
15469
  return "";
14986
15470
  });
14987
15471
  if (delta) {
@@ -15245,7 +15729,7 @@ ${task}`;
15245
15729
  desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
15246
15730
  run: async () => {
15247
15731
  await refreshCatalogs().catch((e) => {
15248
- log26.debug("catalog refresh failed", e);
15732
+ log27.debug("catalog refresh failed", e);
15249
15733
  });
15250
15734
  face.reprepare();
15251
15735
  err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
@@ -15747,7 +16231,7 @@ ${task}`;
15747
16231
  try {
15748
16232
  for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
15749
16233
  } catch (e) {
15750
- log26.debug(`loadAgents(${d}) failed`, e);
16234
+ log27.debug(`loadAgents(${d}) failed`, e);
15751
16235
  }
15752
16236
  }
15753
16237
  if (!seen.size) {
@@ -15835,7 +16319,7 @@ ${task}`;
15835
16319
  }
15836
16320
  if (idx >= 0) {
15837
16321
  const old = mounted.splice(idx, 1)[0];
15838
- await old.client.close().catch((e) => log26.debug("mcp close failed", e));
16322
+ await old.client.close().catch((e) => log27.debug("mcp close failed", e));
15839
16323
  }
15840
16324
  try {
15841
16325
  const m = await mountMcpServer(name, conf);
@@ -15863,7 +16347,7 @@ ${task}`;
15863
16347
  }
15864
16348
  const m = mounted.splice(idx, 1)[0];
15865
16349
  remountMcpTools();
15866
- await m.client.close().catch((e) => log26.debug("mcp close failed", e));
16350
+ await m.client.close().catch((e) => log27.debug("mcp close failed", e));
15867
16351
  err(dim(` removed "${name}"
15868
16352
  `));
15869
16353
  return;
@@ -16066,16 +16550,16 @@ ${task}`;
16066
16550
  try {
16067
16551
  return readdirSync4(join14(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
16068
16552
  } catch (e) {
16069
- log26.debug("completion readdir failed", absDir, e);
16553
+ log27.debug("completion readdir failed", absDir, e);
16070
16554
  return null;
16071
16555
  }
16072
16556
  };
16073
16557
  const agentCommandNames = () => [...cmds.map((c) => c.name), ...skills.map((s) => s.name)];
16074
16558
  const commandNames = () => [...Object.keys(builtins).filter((k) => k !== "quit"), ...agentCommandNames()];
16075
- const describe = (hit) => hit.startsWith("/") ? builtins[hit.slice(1)]?.desc ?? cmds.find((c) => c.name === hit.slice(1))?.description ?? skills.find((s) => s.name === hit.slice(1))?.description : void 0;
16559
+ const describe2 = (hit) => hit.startsWith("/") ? builtins[hit.slice(1)]?.desc ?? cmds.find((c) => c.name === hit.slice(1))?.description ?? skills.find((s) => s.name === hit.slice(1))?.description : void 0;
16076
16560
  const suggest = (lineToCursor) => {
16077
16561
  const [hits, token] = completeLine(lineToCursor, { commands: commandNames(), agentCommands: agentCommandNames(), listDir, bangHistory: history });
16078
- return { hits, token, describe };
16562
+ return { hits, token, describe: describe2 };
16079
16563
  };
16080
16564
  const editor = createLineEditor(process.stderr);
16081
16565
  editorRef = editor;
@@ -16202,7 +16686,7 @@ ${out}
16202
16686
  return;
16203
16687
  }
16204
16688
  await refreshCatalogs().catch((e) => {
16205
- log26.debug("catalog refresh failed", e);
16689
+ log27.debug("catalog refresh failed", e);
16206
16690
  });
16207
16691
  const sk = skills.find((x) => x.name === name);
16208
16692
  if (sk) {
@@ -16690,7 +17174,7 @@ async function main() {
16690
17174
  });
16691
17175
  if (args.task) {
16692
17176
  const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join14(cwd, ".agent", "mcp-auth.json") }));
16693
- const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted));
17177
+ const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted), mounted.map((m) => m.name));
16694
17178
  const store = new SessionStore(cwd);
16695
17179
  const session = startSession(args, store, agent, cwd);
16696
17180
  process.once("SIGINT", () => activeTurn?.abort());