@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/README.md +2 -1
- package/dist/{Agent-BWbQ4Sqp.d.ts → Agent-CXkPfYX2.d.ts} +29 -12
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +913 -429
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +721 -316
- package/dist/index.js.map +1 -1
- package/dist/{mcp-kgG6rtsd.d.ts → mcp-CSLRyvEp.d.ts} +13 -4
- package/dist/mcp.client.d.ts +33 -7
- package/dist/mcp.client.js +147 -40
- package/dist/mcp.client.js.map +1 -1
- package/dist/{tools-BqL8Lk4J.d.ts → tools-HsbxgqjF.d.ts} +185 -40
- package/dist/tools.shell.d.ts +40 -11
- package/dist/tools.shell.js +457 -91
- package/dist/tools.shell.js.map +1 -1
- package/package.json +4 -4
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2377
|
-
|
|
2803
|
+
jobs;
|
|
2804
|
+
meta = /* @__PURE__ */ new Map();
|
|
2378
2805
|
async start(command) {
|
|
2379
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
2410
|
-
|
|
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
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
this.
|
|
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:
|
|
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.
|
|
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
|
|
2444
|
-
|
|
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.
|
|
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
|
|
2458
|
-
|
|
2459
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
4042
|
-
*
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
*
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
4364
|
-
this.
|
|
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
|
-
|
|
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))
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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())
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4563
|
-
await this.
|
|
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
|
-
|
|
4593
|
-
await this.
|
|
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())
|
|
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
|
|
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
|
-
|
|
4654
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
5817
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6226
|
+
log10.warn(`could not persist lesson: ${e?.message ?? e}`);
|
|
5852
6227
|
return null;
|
|
5853
6228
|
}
|
|
5854
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 || "") ?
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8755
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
9325
|
+
const timeoutMs = deadlineFor(method, this.spec, opts);
|
|
9326
|
+
const { signal, onProgress } = opts;
|
|
8907
9327
|
return new Promise((resolve5, reject) => {
|
|
8908
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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") ?
|
|
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
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
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
|
-
|
|
9027
|
-
|
|
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) =>
|
|
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
|
-
|
|
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)
|
|
9086
|
-
else
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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)
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
10728
|
-
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
13435
|
+
conn.on("error", (e) => log26.debug("trigger conn error", e));
|
|
12952
13436
|
});
|
|
12953
|
-
this.server.on("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
|
-
|
|
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
|
|
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
|
|
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} ${
|
|
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,
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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}${
|
|
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
|
-
|
|
14837
|
+
log27.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
14354
14838
|
return;
|
|
14355
14839
|
}
|
|
14356
|
-
|
|
14840
|
+
log27.error("unhandledRejection", e);
|
|
14357
14841
|
});
|
|
14358
14842
|
process.on("uncaughtException", (e) => {
|
|
14359
14843
|
if (isCancelTeardown(e)) {
|
|
14360
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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());
|