openmausbot 0.1.92 → 0.1.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{DesktopViewer-Do6lBa4C.js → DesktopViewer-CKNR3SEW.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
- package/dist/assets/{arc-DIYc7Ymx.js → arc-BBdBuCgB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
- package/dist/assets/channel-BU94VpxE.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-BCAElss-.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
- package/dist/assets/index-Biji-bfL.css +1 -0
- package/dist/assets/{index-DGBaEXrM.js → index-BkYwilkG.js} +1 -1
- package/dist/assets/index-DZlhoES0.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
- package/dist/assets/{linear-CmOWzX0P.js → linear-bq1PTDTW.js} +1 -1
- package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-s4LTbQle.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +24 -1
- package/dist-server/container-mcp.js +6 -1
- package/dist-server/drivers/agents-proxy.js +2 -0
- package/dist-server/index.js +1722 -906
- package/dist-server/local-computer-proxy.js +5 -1
- package/dist-server/mcp-gate.js +5 -1
- package/dist-server/openmausbot.js +763 -93
- package/dist-server/pair-cli.js +763 -93
- package/dist-server/server/boat.js +3 -3
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/config.js +14 -1
- package/dist-server/server/container-computer.js +43 -32
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +172 -12
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
- package/dist-server/server/drivers/claude.js +17 -3
- package/dist-server/server/drivers/openai-chat.js +26 -2
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/env-path.js +6 -2
- package/dist-server/server/index.js +210 -57
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/routes/desktop-viewer.js +50 -6
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/store.js +54 -0
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup.js +22 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/vps-container-mcp.js +6 -1
- package/dist-server/workspace-backup.worker.js +14 -0
- package/package.json +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +0 -1
- package/dist/assets/index-Cdmjrl28.css +0 -1
- package/dist/assets/index-DR7YPMkW.js +0 -597
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +0 -8
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// What an ACP agent is doing while it sends nothing.
|
|
2
|
+
//
|
|
3
|
+
// ACP has no "still working" message, and agents go quiet for minutes in
|
|
4
|
+
// normal work: Qwen Code 0.24 says nothing over the wire while it waits out
|
|
5
|
+
// a rate limit, while its SDK retries a request, or while one slow model
|
|
6
|
+
// call runs. Silence alone cannot tell that from a wedged process, so a
|
|
7
|
+
// quiet turn is probed from outside: the agent's own debug log where it
|
|
8
|
+
// keeps one, and the process tree's open connections and CPU time. The
|
|
9
|
+
// result becomes a plain-words notice in the chat, so the person is never
|
|
10
|
+
// left watching a spinner without knowing why.
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
13
|
+
/** Qwen Code's debug log (QWEN_DEBUG_LOG_FILE=1), lines like
|
|
14
|
+
* `2026-10-02T02:50:41.757Z [WARN] [RETRY] Attempt 1 failed with status 429. Retrying after explicit delay of 20000ms...`.
|
|
15
|
+
* Only the facts worth telling a person are parsed; every other line still
|
|
16
|
+
* counts as proof the process is alive. */
|
|
17
|
+
export function parseQwenLogLine(line) {
|
|
18
|
+
if (line.includes("[RETRY]") && /\bAttempt \d+ failed\b/.test(line)) {
|
|
19
|
+
const attempt = /\bAttempt (\d+) failed/.exec(line);
|
|
20
|
+
const status = /\bwith status (\d{3})\b/.exec(line);
|
|
21
|
+
const explicit = /explicit delay of (\d+)\s*ms/.exec(line);
|
|
22
|
+
const seconds = /Retrying in (\d+)\s*s\b/.exec(line);
|
|
23
|
+
return {
|
|
24
|
+
kind: "retry",
|
|
25
|
+
attempt: attempt ? Number(attempt[1]) : null,
|
|
26
|
+
status: status ? Number(status[1]) : null,
|
|
27
|
+
delayMs: explicit ? Number(explicit[1]) : seconds ? Number(seconds[1]) * 1000 : null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (/\[compaction\] hard-tier rescue triggered|Reactive compression|tryCompress/.test(line)
|
|
31
|
+
&& !/succeeded|failed|skipped|stopped/.test(line)) {
|
|
32
|
+
return { kind: "compressing" };
|
|
33
|
+
}
|
|
34
|
+
const gate = /\[compaction\] cheap-gate NOOP: effectiveTokens=(\d+), auto=(\d+)/.exec(line);
|
|
35
|
+
if (gate)
|
|
36
|
+
return { kind: "context", tokens: Number(gate[1]), threshold: Number(gate[2]) };
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/** Reads the lines appended to a log file since the last read. A file that
|
|
40
|
+
* does not exist yet is simply empty; the first read skips what was already
|
|
41
|
+
* there, so an old conversation's log is not replayed as news. */
|
|
42
|
+
export class LogTail {
|
|
43
|
+
offset = null;
|
|
44
|
+
partial = "";
|
|
45
|
+
path;
|
|
46
|
+
maxBytes;
|
|
47
|
+
constructor(path, maxBytes = 256 * 1024) {
|
|
48
|
+
this.path = path;
|
|
49
|
+
this.maxBytes = maxBytes;
|
|
50
|
+
}
|
|
51
|
+
read() {
|
|
52
|
+
let fd;
|
|
53
|
+
try {
|
|
54
|
+
fd = openSync(this.path, "r");
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
if (this.offset === null)
|
|
58
|
+
this.offset = 0;
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const size = fstatSync(fd).size;
|
|
63
|
+
if (this.offset === null) {
|
|
64
|
+
this.offset = size;
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
// truncated or replaced: start over from its beginning
|
|
68
|
+
if (size < this.offset) {
|
|
69
|
+
this.offset = 0;
|
|
70
|
+
this.partial = "";
|
|
71
|
+
}
|
|
72
|
+
if (size === this.offset)
|
|
73
|
+
return [];
|
|
74
|
+
// a burst bigger than the cap keeps only its tail
|
|
75
|
+
const start = Math.max(this.offset, size - this.maxBytes);
|
|
76
|
+
if (start > this.offset)
|
|
77
|
+
this.partial = "";
|
|
78
|
+
const buf = Buffer.alloc(size - start);
|
|
79
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
80
|
+
this.offset = size;
|
|
81
|
+
const text = this.partial + buf.toString("utf8");
|
|
82
|
+
const lines = text.split(/\r?\n/);
|
|
83
|
+
this.partial = lines.pop() ?? "";
|
|
84
|
+
return lines.filter((l) => l.trim());
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
try {
|
|
91
|
+
closeSync(fd);
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const run = (file, args, timeoutMs = 4_000) => new Promise((resolve) => {
|
|
98
|
+
execFile(file, args, { timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, windowsHide: true }, (err, stdout) => {
|
|
99
|
+
// lsof exits 1 when it finds nothing to list; its stdout is still the answer
|
|
100
|
+
if (err && !(typeof stdout === "string" && err.code === 1))
|
|
101
|
+
resolve(null);
|
|
102
|
+
else
|
|
103
|
+
resolve(String(stdout));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
/** `ps` TIME: [[dd-]hh:]mm:ss[.cc] */
|
|
107
|
+
function psTimeMs(raw) {
|
|
108
|
+
const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/.exec(raw.trim());
|
|
109
|
+
if (!m)
|
|
110
|
+
return null;
|
|
111
|
+
const [, d, h, min, s] = m;
|
|
112
|
+
return ((Number(d ?? 0) * 24 + Number(h ?? 0)) * 60 + Number(min)) * 60_000 + Number(s) * 1000;
|
|
113
|
+
}
|
|
114
|
+
/** The root and every descendant, from a `pid ppid ...` table. */
|
|
115
|
+
export function treePids(rows, root) {
|
|
116
|
+
const kids = new Map();
|
|
117
|
+
for (const r of rows) {
|
|
118
|
+
const list = kids.get(r.ppid);
|
|
119
|
+
if (list)
|
|
120
|
+
list.push(r.pid);
|
|
121
|
+
else
|
|
122
|
+
kids.set(r.ppid, [r.pid]);
|
|
123
|
+
}
|
|
124
|
+
const out = [];
|
|
125
|
+
const queue = [root];
|
|
126
|
+
while (queue.length && out.length < 512) {
|
|
127
|
+
const pid = queue.shift();
|
|
128
|
+
if (out.includes(pid))
|
|
129
|
+
continue;
|
|
130
|
+
out.push(pid);
|
|
131
|
+
queue.push(...(kids.get(pid) ?? []));
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
async function samplePosix(root) {
|
|
136
|
+
const table = await run("ps", ["-A", "-o", "pid=,ppid=,time="]);
|
|
137
|
+
if (table === null)
|
|
138
|
+
return { cpuMs: null, connections: null };
|
|
139
|
+
const rows = [];
|
|
140
|
+
for (const line of table.split("\n")) {
|
|
141
|
+
const [pid, ppid, time] = line.trim().split(/\s+/);
|
|
142
|
+
if (!pid || !ppid || !time)
|
|
143
|
+
continue;
|
|
144
|
+
rows.push({ pid: Number(pid), ppid: Number(ppid), cpuMs: psTimeMs(time) });
|
|
145
|
+
}
|
|
146
|
+
const pids = treePids(rows, root);
|
|
147
|
+
if (!rows.some((r) => r.pid === root))
|
|
148
|
+
return { cpuMs: null, connections: null };
|
|
149
|
+
let cpuMs = 0;
|
|
150
|
+
for (const r of rows)
|
|
151
|
+
if (pids.includes(r.pid))
|
|
152
|
+
cpuMs += r.cpuMs ?? 0;
|
|
153
|
+
const lsof = await run("lsof", ["-nP", "-a", "-p", pids.join(","), "-iTCP", "-sTCP:ESTABLISHED", "-Fn"]);
|
|
154
|
+
// -Fn prints one `n<addr>` line per connection
|
|
155
|
+
const connections = lsof === null ? null : lsof.split("\n").filter((l) => l.startsWith("n")).length;
|
|
156
|
+
return { cpuMs, connections };
|
|
157
|
+
}
|
|
158
|
+
async function sampleWindows(root) {
|
|
159
|
+
const script = "$ErrorActionPreference='SilentlyContinue';" +
|
|
160
|
+
"$all=Get-CimInstance Win32_Process|Select-Object ProcessId,ParentProcessId,KernelModeTime,UserModeTime;" +
|
|
161
|
+
`$ids=@(${root});$q=@(${root});` +
|
|
162
|
+
"while($q.Count){$p=$q[0];$q=@($q|Select-Object -Skip 1);foreach($c in $all|Where-Object{$_.ParentProcessId -eq $p}){if($ids -notcontains $c.ProcessId){$ids+=$c.ProcessId;$q+=$c.ProcessId}}};" +
|
|
163
|
+
"$cpu=($all|Where-Object{$ids -contains $_.ProcessId}|ForEach-Object{[double]$_.KernelModeTime+[double]$_.UserModeTime}|Measure-Object -Sum).Sum/10000;" +
|
|
164
|
+
"$n=@(Get-NetTCPConnection -State Established|Where-Object{$ids -contains $_.OwningProcess}).Count;" +
|
|
165
|
+
"Write-Output \"$cpu $n\"";
|
|
166
|
+
const out = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], 8_000);
|
|
167
|
+
const [cpu, n] = (out ?? "").trim().split(/\s+/);
|
|
168
|
+
const cpuMs = Number(cpu);
|
|
169
|
+
const connections = Number(n);
|
|
170
|
+
return {
|
|
171
|
+
cpuMs: cpu !== undefined && Number.isFinite(cpuMs) ? cpuMs : null,
|
|
172
|
+
connections: n !== undefined && Number.isFinite(connections) ? connections : null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** CPU time and open connections of a process and its children. Best
|
|
176
|
+
* effort: a missing tool or a refused query reads as null (unknown), never
|
|
177
|
+
* as "idle". */
|
|
178
|
+
export function sampleProcessTree(pid) {
|
|
179
|
+
return (process.platform === "win32" ? sampleWindows(pid) : samplePosix(pid))
|
|
180
|
+
.catch(() => ({ cpuMs: null, connections: null }));
|
|
181
|
+
}
|
|
182
|
+
/** CPU a process tree can burn on bookkeeping while doing no real work. */
|
|
183
|
+
const BUSY_CPU_MS_PER_MIN = 3_000;
|
|
184
|
+
export function classifyQuiet(input) {
|
|
185
|
+
const { logged, sample, previous } = input;
|
|
186
|
+
if (logged?.kind === "retry")
|
|
187
|
+
return { kind: "retrying", status: logged.status, delayMs: logged.delayMs, attempt: logged.attempt };
|
|
188
|
+
if (logged?.kind === "compressing")
|
|
189
|
+
return { kind: "compressing" };
|
|
190
|
+
const nearFullContext = input.context !== null && input.context.threshold > 0
|
|
191
|
+
&& input.context.tokens >= input.context.threshold * 0.9;
|
|
192
|
+
if (sample.connections !== null && sample.connections > 0)
|
|
193
|
+
return { kind: "waiting-model", nearFullContext };
|
|
194
|
+
const cpuDelta = sample.cpuMs !== null && previous?.cpuMs != null ? sample.cpuMs - previous.cpuMs : null;
|
|
195
|
+
if (cpuDelta !== null && input.sinceMs > 0 && cpuDelta > BUSY_CPU_MS_PER_MIN * (input.sinceMs / 60_000))
|
|
196
|
+
return { kind: "busy" };
|
|
197
|
+
if (sample.connections === 0 && cpuDelta !== null)
|
|
198
|
+
return { kind: "no-signs" };
|
|
199
|
+
return { kind: "unknown" };
|
|
200
|
+
}
|
|
201
|
+
/** Two states read the same to a person when this matches; a notice is
|
|
202
|
+
* sent only when it changes. */
|
|
203
|
+
export function quietKey(state) {
|
|
204
|
+
if (state.kind === "retrying")
|
|
205
|
+
return `retrying:${state.attempt ?? "?"}:${state.status ?? "?"}`;
|
|
206
|
+
if (state.kind === "waiting-model")
|
|
207
|
+
return `waiting-model:${state.nearFullContext}`;
|
|
208
|
+
return state.kind;
|
|
209
|
+
}
|
|
210
|
+
const minutes = (ms) => {
|
|
211
|
+
if (ms < 90_000)
|
|
212
|
+
return `${Math.max(1, Math.round(ms / 1000))} s`;
|
|
213
|
+
return `${Math.round(ms / 60_000)} min`;
|
|
214
|
+
};
|
|
215
|
+
/** The chat notice for a quiet state. */
|
|
216
|
+
export function describeQuiet(name, state, quietMs, stopAfterMs) {
|
|
217
|
+
const quiet = minutes(quietMs);
|
|
218
|
+
switch (state.kind) {
|
|
219
|
+
case "retrying": {
|
|
220
|
+
const why = state.status === 429 ? "hit a rate limit (HTTP 429)"
|
|
221
|
+
: state.status !== null && state.status >= 500 ? `got a server error from its model provider (HTTP ${state.status})`
|
|
222
|
+
: state.status !== null ? `got an error from its model provider (HTTP ${state.status})`
|
|
223
|
+
: "had a failed model request";
|
|
224
|
+
const next = state.delayMs !== null ? ` Next try in ${Math.max(1, Math.round(state.delayMs / 1000))} s.` : "";
|
|
225
|
+
const attempt = state.attempt !== null ? ` (attempt ${state.attempt + 1})` : "";
|
|
226
|
+
return `${name} ${why} and is retrying${attempt}.${next} It is still working.`;
|
|
227
|
+
}
|
|
228
|
+
case "compressing":
|
|
229
|
+
return `${name} is compressing its conversation history to make room. This can take a few minutes; it is still working.`;
|
|
230
|
+
case "waiting-model":
|
|
231
|
+
return state.nearFullContext
|
|
232
|
+
? `${name} is waiting on its model: no reply for ${quiet}, but its request is still open. Its history is nearly full, so it may be compressing it.`
|
|
233
|
+
: `${name} is waiting on its model: no reply for ${quiet}, but its request is still open. Slow models and rate limits look like this.`;
|
|
234
|
+
case "busy":
|
|
235
|
+
return `${name} is busy on this computer but has sent nothing for ${quiet}.`;
|
|
236
|
+
case "between-requests":
|
|
237
|
+
return `${name} has sent nothing for ${quiet} and has no request open to its model right now. ` +
|
|
238
|
+
"It may be waiting before a retry. Still waiting; press Stop to end the turn.";
|
|
239
|
+
case "no-signs":
|
|
240
|
+
return `${name} has sent nothing for ${quiet}, has no open request to its model, and is not using the CPU. It may be stuck. ` +
|
|
241
|
+
`Press Stop to end the turn; otherwise it is stopped after ${minutes(stopAfterMs)} of silence.`;
|
|
242
|
+
case "unknown":
|
|
243
|
+
return `${name} has sent nothing for ${quiet}. Still waiting; press Stop to end the turn.`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** A short phrase for the final stuck error: what the agent looked like last. */
|
|
247
|
+
export function lastSeenPhrase(state) {
|
|
248
|
+
switch (state.kind) {
|
|
249
|
+
case "retrying": return "retrying a failed model request";
|
|
250
|
+
case "compressing": return "compressing its history";
|
|
251
|
+
case "waiting-model": return "waiting on its model with a request open";
|
|
252
|
+
case "busy": return "busy on this computer";
|
|
253
|
+
case "between-requests": return "no request open to its model";
|
|
254
|
+
case "no-signs": return "no open request and no CPU use";
|
|
255
|
+
case "unknown": return "no readable process state";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -6,6 +6,7 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { qualifiedModelLabel } from "../../contracts.js";
|
|
8
8
|
import { harnessHome } from "../../env-path.js";
|
|
9
|
+
import { parseQwenLogLine } from "./quiet-status.js";
|
|
9
10
|
import { decodeInjectId, hostApiKey, localHost, mergeLocalInject } from "../local-inject.js";
|
|
10
11
|
import { createAcpDriver } from "./core.js";
|
|
11
12
|
const EMPTY = { default: "", options: [] };
|
|
@@ -199,6 +200,15 @@ export function resolveQwenTurnModel(model, env) {
|
|
|
199
200
|
throw new Error("This Qwen model is no longer configured. Refresh models and select it again.");
|
|
200
201
|
return matches[0].id;
|
|
201
202
|
}
|
|
203
|
+
/** Where Qwen Code 0.24 writes a session's debug log: `<runtime dir>/debug/<session id>.txt`,
|
|
204
|
+
* the runtime dir being QWEN_RUNTIME_DIR, else QWEN_HOME, else ~/.qwen. */
|
|
205
|
+
export function qwenDebugLogPath(env, sessionId) {
|
|
206
|
+
const off = ["", "0", "false", "off", "no"].includes((env.QWEN_DEBUG_LOG_FILE ?? "").trim().toLowerCase());
|
|
207
|
+
if (off || !/^[\w-]{1,128}$/.test(sessionId))
|
|
208
|
+
return null;
|
|
209
|
+
const base = env.QWEN_RUNTIME_DIR || env.QWEN_HOME || harnessHome("qwen", env);
|
|
210
|
+
return join(base, "debug", `${sessionId}.txt`);
|
|
211
|
+
}
|
|
202
212
|
/** Qwen Code's own approval ladder, passed through (qwen --help, 0.24):
|
|
203
213
|
* `--approval-mode default` asks, `auto-edit` approves file edits, `auto`
|
|
204
214
|
* runs Qwen's LLM classifier that approves safe actions and blocks risky
|
|
@@ -243,5 +253,13 @@ const support = {
|
|
|
243
253
|
authFailure: "continue",
|
|
244
254
|
isAuthenticated: () => true,
|
|
245
255
|
buildPromptText: (turn) => (turn.system ? `${turn.system}\n\n${turn.text}` : turn.text),
|
|
256
|
+
// Qwen says nothing over ACP while it retries a rate limit or compresses
|
|
257
|
+
// its history, but its debug log does. Turn the log on (unless the person
|
|
258
|
+
// turned it off) and read it while a prompt is quiet.
|
|
259
|
+
transformEnv: (env) => {
|
|
260
|
+
if (env.QWEN_DEBUG_LOG_FILE === undefined)
|
|
261
|
+
env.QWEN_DEBUG_LOG_FILE = "1";
|
|
262
|
+
},
|
|
263
|
+
statusLog: { path: qwenDebugLogPath, parse: parseQwenLogLine },
|
|
246
264
|
};
|
|
247
265
|
export const QwenAgentDriver = createAcpDriver(support);
|
|
@@ -15,6 +15,7 @@ import { HermesAgentDriver } from "./acp/hermes.js";
|
|
|
15
15
|
import { OpenAICompatDriver } from "./openai-compat.js";
|
|
16
16
|
import { PiDriver } from "./pi.js";
|
|
17
17
|
import { MistralDriver } from "./mistral.js";
|
|
18
|
+
import { CerebrasDriver } from "./cerebras.js";
|
|
18
19
|
import { MinimaxDriver } from "./minimax.js";
|
|
19
20
|
export const BUILT_IN_DRIVERS = [
|
|
20
21
|
GrokDriver,
|
|
@@ -35,4 +36,5 @@ export const BUILT_IN_DRIVERS = [
|
|
|
35
36
|
BoatAgentDriver,
|
|
36
37
|
MinimaxDriver,
|
|
37
38
|
MistralDriver,
|
|
39
|
+
CerebrasDriver,
|
|
38
40
|
];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createOpenAIChatRuntime } from "./openai-chat.js";
|
|
3
|
+
// Cerebras serves open models on wafer-scale chips: same OpenAI chat
|
|
4
|
+
// contract, several times the tokens per second of a GPU cloud.
|
|
5
|
+
const DEFAULT_URL = "https://api.cerebras.ai/v1";
|
|
6
|
+
// The account tier is unknown: use the shared free-tier limit, not paid 128K.
|
|
7
|
+
// https://inference-docs.cerebras.ai/models/overview
|
|
8
|
+
const DEFAULT_MODELS = {
|
|
9
|
+
default: "gpt-oss-120b",
|
|
10
|
+
options: [
|
|
11
|
+
{ id: "gpt-oss-120b", label: "GPT OSS 120B", contextWindow: 65536 },
|
|
12
|
+
{ id: "qwen-3.8-27b", label: "Qwen3.8 27B", contextWindow: 65536 },
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
const configSchema = z.object({
|
|
16
|
+
url: z.string().trim().url().default(DEFAULT_URL),
|
|
17
|
+
model: z.string().trim().min(1).optional(),
|
|
18
|
+
tools: z.boolean().optional(),
|
|
19
|
+
});
|
|
20
|
+
function decodeConfig(raw) {
|
|
21
|
+
const config = configSchema.parse(raw ?? {});
|
|
22
|
+
config.url = config.url.replace(/\/+$/, "");
|
|
23
|
+
const url = new URL(config.url);
|
|
24
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" &&
|
|
25
|
+
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
|
|
26
|
+
throw new Error("Cerebras requires HTTPS, except for a local test endpoint.");
|
|
27
|
+
}
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
const modelCard = z.object({ id: z.string().min(1) });
|
|
31
|
+
/** Cerebras answers "Only 'text' content type can be used" for these. */
|
|
32
|
+
const TEXT_ONLY_MODELS = /^(?:gpt-oss|llama3\.1-8b|llama-3\.3-70b)/i;
|
|
33
|
+
export const CerebrasDriver = {
|
|
34
|
+
driverKind: "cerebras",
|
|
35
|
+
metadata: { displayName: "Cerebras (API)", supportsMultipleInstances: true, access: "api" },
|
|
36
|
+
models: DEFAULT_MODELS,
|
|
37
|
+
install: {
|
|
38
|
+
docsUrl: "https://cloud.cerebras.ai/",
|
|
39
|
+
settings: "connections",
|
|
40
|
+
signInCommand: "Save a Cerebras API key in Settings → Connections, or set CEREBRAS_API_KEY on the server.",
|
|
41
|
+
},
|
|
42
|
+
decodeConfig,
|
|
43
|
+
defaultConfig: () => decodeConfig({}),
|
|
44
|
+
async create(input) {
|
|
45
|
+
const { config } = input;
|
|
46
|
+
const apiKey = (input.environment.CEREBRAS_API_KEY ?? process.env.CEREBRAS_API_KEY ?? "").trim();
|
|
47
|
+
const known = new Map(DEFAULT_MODELS.options.map((option) => [option.id, option]));
|
|
48
|
+
const withConfiguredModel = (options) => {
|
|
49
|
+
const preferred = config.model ?? DEFAULT_MODELS.default;
|
|
50
|
+
if (config.model && !options.some((option) => option.id === config.model)) {
|
|
51
|
+
options = [{ id: config.model, label: config.model }, ...options];
|
|
52
|
+
}
|
|
53
|
+
return { default: options.some((option) => option.id === preferred) ? preferred : options[0].id, options };
|
|
54
|
+
};
|
|
55
|
+
let catalog = withConfiguredModel(DEFAULT_MODELS.options);
|
|
56
|
+
const refreshModels = async () => {
|
|
57
|
+
if (!apiKey)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(`${config.url}/models`, {
|
|
61
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
62
|
+
redirect: "error",
|
|
63
|
+
signal: AbortSignal.timeout(8_000),
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok)
|
|
66
|
+
return;
|
|
67
|
+
const json = await response.json();
|
|
68
|
+
const rows = Array.isArray(json) ? json : z.object({ data: z.array(z.unknown()) }).parse(json).data;
|
|
69
|
+
const options = [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
const parsed = modelCard.safeParse(row);
|
|
73
|
+
if (!parsed.success || !parsed.data.id.trim() || seen.has(parsed.data.id))
|
|
74
|
+
continue;
|
|
75
|
+
seen.add(parsed.data.id);
|
|
76
|
+
options.push(known.get(parsed.data.id) ?? { id: parsed.data.id, label: parsed.data.id });
|
|
77
|
+
}
|
|
78
|
+
if (options.length)
|
|
79
|
+
catalog = withConfiguredModel(options);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// A failed refresh keeps the last usable catalog, including custom models.
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
if (apiKey)
|
|
86
|
+
void refreshModels();
|
|
87
|
+
return createOpenAIChatRuntime({
|
|
88
|
+
input, driverKind: "cerebras", apiKey, apiUrl: config.url,
|
|
89
|
+
tools: config.tools, models: () => catalog, refreshModels, reasoning: true,
|
|
90
|
+
reasoningReplayField: "reasoning",
|
|
91
|
+
computerUse: true, imageInput: (model) => !TEXT_ONLY_MODELS.test(model),
|
|
92
|
+
nudgeAnnouncedAction: true,
|
|
93
|
+
requestBody: (model, messages, stream) => ({
|
|
94
|
+
model, messages, stream,
|
|
95
|
+
...(stream ? { stream_options: { include_usage: true } } : {}),
|
|
96
|
+
}),
|
|
97
|
+
httpErrorLabel: "Cerebras",
|
|
98
|
+
missingKeyError: "Save a Cerebras API key in Settings → Connections, or set CEREBRAS_API_KEY.",
|
|
99
|
+
unavailableReason: "No Cerebras API key — open Settings → Connections.",
|
|
100
|
+
timeoutMs: 180_000, billing: "metered", includeUsageInCompleted: true,
|
|
101
|
+
nativeLog: {
|
|
102
|
+
source: "cerebras.chat.completions",
|
|
103
|
+
outgoing: (_turn, messages, model) => ({ model, messageCount: messages.length }),
|
|
104
|
+
incoming: ({ text, reasoning, usage }) => ({ textLength: text.length, reasoningLength: reasoning.length, usage }),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -266,6 +266,22 @@ function boundedText(value) {
|
|
|
266
266
|
end -= 1;
|
|
267
267
|
return `${bytes.subarray(0, end).toString()}\n[MCP result truncated at 50KB; request less output.]`;
|
|
268
268
|
}
|
|
269
|
+
/** Optional fields a tool documents as "omit to use the default", where a
|
|
270
|
+
* blank string is an error instead. Smaller models fill optional fields with
|
|
271
|
+
* "" rather than leaving them out; dropping the blank restores the documented
|
|
272
|
+
* call. agent_browser_read: "Omit url to read the active tab." */
|
|
273
|
+
const BUILT_IN_BROWSER_BLANK_MEANS_OMITTED = {
|
|
274
|
+
agent_browser_read: ["url"],
|
|
275
|
+
};
|
|
276
|
+
function omitBlankDefaults(builtInBrowser, tool, args) {
|
|
277
|
+
if (!builtInBrowser || !object(args))
|
|
278
|
+
return;
|
|
279
|
+
for (const field of BUILT_IN_BROWSER_BLANK_MEANS_OMITTED[tool] ?? []) {
|
|
280
|
+
const value = args[field];
|
|
281
|
+
if (typeof value === "string" && !value.trim())
|
|
282
|
+
delete args[field];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
269
285
|
export async function mountChatTools(integrations, signal, computerUse = false) {
|
|
270
286
|
const servers = [];
|
|
271
287
|
if (computerUse && integrations?.computer)
|
|
@@ -316,12 +332,12 @@ export async function mountChatTools(integrations, signal, computerUse = false)
|
|
|
316
332
|
// image delivery, including custom servers. Text stays bounded below.
|
|
317
333
|
const client = "boxId" in descriptor ? new ChatBoatClient(descriptor) : new ChatMcpClient(descriptor, computerUse);
|
|
318
334
|
clients.push(client);
|
|
319
|
-
return { name, client, tools: await client.tools(signal) };
|
|
335
|
+
return { name, client, builtInBrowser: descriptor === integrations?.browser, tools: await client.tools(signal) };
|
|
320
336
|
}));
|
|
321
337
|
for (const mount of mounts) {
|
|
322
338
|
if (mount.status === "rejected")
|
|
323
339
|
throw mount.reason;
|
|
324
|
-
const { name: server, client, tools } = mount.value;
|
|
340
|
+
const { name: server, client, builtInBrowser, tools } = mount.value;
|
|
325
341
|
const originalNames = new Set();
|
|
326
342
|
for (const tool of tools) {
|
|
327
343
|
if (!object(tool) || typeof tool.name !== "string" || !tool.name.trim() || originalNames.has(tool.name))
|
|
@@ -352,7 +368,7 @@ export async function mountChatTools(integrations, signal, computerUse = false)
|
|
|
352
368
|
const suffix = `_${index}`;
|
|
353
369
|
name = base.slice(0, 64 - suffix.length) + suffix;
|
|
354
370
|
}
|
|
355
|
-
registered.set(name, { client, name: tool.name, schema });
|
|
371
|
+
registered.set(name, { client, builtInBrowser, name: tool.name, schema });
|
|
356
372
|
definitions.push({ type: "function", function: { name, description, parameters } });
|
|
357
373
|
if (Buffer.byteLength(JSON.stringify(definitions)) > CATALOG_BYTES)
|
|
358
374
|
throw new Error("MCP tool catalog exceeds the 1MB limit");
|
|
@@ -371,6 +387,7 @@ export async function mountChatTools(integrations, signal, computerUse = false)
|
|
|
371
387
|
const tool = registered.get(name);
|
|
372
388
|
if (!tool)
|
|
373
389
|
throw new Error("The requested tool was not advertised for this turn");
|
|
390
|
+
omitBlankDefaults(tool.builtInBrowser, tool.name, args);
|
|
374
391
|
if (!object(args) || !tool.schema(args))
|
|
375
392
|
throw new Error("Tool arguments do not match the advertised input schema; use its required fields and types");
|
|
376
393
|
};
|
|
@@ -1032,11 +1032,18 @@ export const ClaudeDriver = {
|
|
|
1032
1032
|
// harness snapshots every instance whenever it describes them — app
|
|
1033
1033
|
// load, the Engines page, and right after `claude update`, which is
|
|
1034
1034
|
// exactly when the answer changes — so a turn normally finds it filled.
|
|
1035
|
-
// Most
|
|
1036
|
-
//
|
|
1035
|
+
// Most flags before any snapshot assume a current CLI; autocompact
|
|
1036
|
+
// requires confirmed help support. A coordinated turn checks first
|
|
1037
|
+
// because the snapshot-refresh flag is newer than the
|
|
1037
1038
|
// other context controls and an unknown flag would reject that request.
|
|
1038
1039
|
let cliVersion = null;
|
|
1039
1040
|
let cliVersionChecked = false;
|
|
1041
|
+
// Whether `claude --help` lists --autocompact, read once per CLI version
|
|
1042
|
+
// by snapshot(). The flag is not in every build above its version floor
|
|
1043
|
+
// (2.1.129 rejects it), so the listing wins over the floor; null until
|
|
1044
|
+
// probed, or when the probe fails.
|
|
1045
|
+
let cliHasAutocompact = null;
|
|
1046
|
+
let cliHelpVersion = null;
|
|
1040
1047
|
const readCliVersion = (env) => new Promise((resolve) => {
|
|
1041
1048
|
execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout.trim() || null));
|
|
1042
1049
|
});
|
|
@@ -1230,7 +1237,7 @@ export const ClaudeDriver = {
|
|
|
1230
1237
|
args.push("--setting-sources", "project");
|
|
1231
1238
|
}
|
|
1232
1239
|
const compactWindow = autoCompactWindow(turnEnvironment);
|
|
1233
|
-
if (compactWindow &&
|
|
1240
|
+
if (compactWindow && cliHasAutocompact === true) {
|
|
1234
1241
|
args.push("--autocompact", compactWindow);
|
|
1235
1242
|
}
|
|
1236
1243
|
// An old pair conversation can still carry its first assignment in
|
|
@@ -2263,6 +2270,13 @@ export const ClaudeDriver = {
|
|
|
2263
2270
|
return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
|
|
2264
2271
|
cliVersion = parseClaudeCliVersion(version);
|
|
2265
2272
|
cliVersionChecked = true;
|
|
2273
|
+
if (version !== cliHelpVersion) {
|
|
2274
|
+
const help = await new Promise((resolve) => {
|
|
2275
|
+
execCli(config.cli, ["--help"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout));
|
|
2276
|
+
});
|
|
2277
|
+
cliHasAutocompact = help === null ? null : /^\s*--autocompact\b/m.test(help);
|
|
2278
|
+
cliHelpVersion = version;
|
|
2279
|
+
}
|
|
2266
2280
|
const update = claudeCliUpdate(version, config.cli);
|
|
2267
2281
|
const warning = claudeInheritWarning(env);
|
|
2268
2282
|
if (config.requireApiKey) {
|
|
@@ -30,6 +30,17 @@ const usageFrom = (usage) => usage
|
|
|
30
30
|
const asError = (value) => value instanceof Error ? value : new Error(String(value));
|
|
31
31
|
class UnsupportedChatToolsError extends Error {
|
|
32
32
|
}
|
|
33
|
+
const NUDGE_ANNOUNCED_ACTION = "You said what you would do next but called no tool. Do it now with your tools, or reply with your final answer if nothing is left to do.";
|
|
34
|
+
/** A short reply that only announces a next step, with nothing to answer. */
|
|
35
|
+
export function announcesAction(text) {
|
|
36
|
+
const reply = text.trim();
|
|
37
|
+
if (!reply || reply.length > 400 || reply.includes("?"))
|
|
38
|
+
return false;
|
|
39
|
+
return /(?:[:\u2014\u2013]|\.\.\.|\u2026)$/.test(reply) ||
|
|
40
|
+
/^(?:checking|let me|i'll|i will|i'm going to|i am going to|opening|pulling|looking|searching|navigating|reading|fetching|loading|now (?:i'll|let me|opening|checking|pulling|reading))\b/i.test(reply);
|
|
41
|
+
}
|
|
42
|
+
const TEXT_ONLY_ATTACHMENT_NOTE = "[The person attached image(s), but this model cannot see images. Say so if the request depends on them.]";
|
|
43
|
+
const TEXT_ONLY_SCREENSHOT_NOTE = "[Screenshot not shown: this model cannot see images. Read the page with a snapshot or a text tool instead.]";
|
|
33
44
|
function rejectsToolsParameter(status, body) {
|
|
34
45
|
if (status !== 400 && status !== 422)
|
|
35
46
|
return false;
|
|
@@ -282,6 +293,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
282
293
|
clearTimeout(idleTimer);
|
|
283
294
|
}
|
|
284
295
|
};
|
|
296
|
+
const seesImages = (turn) => options.imageInput?.(turn.model || options.models().default) ?? true;
|
|
285
297
|
const messagesFor = (turn) => {
|
|
286
298
|
// The system message is the head of the resent prefix, so only the
|
|
287
299
|
// stable half belongs there: a volatile edit must not re-price the
|
|
@@ -302,7 +314,9 @@ export function createOpenAIChatRuntime(options) {
|
|
|
302
314
|
role: message.role,
|
|
303
315
|
content: message.text,
|
|
304
316
|
})),
|
|
305
|
-
{ role: "user", content: options.computerUse ?
|
|
317
|
+
{ role: "user", content: !options.computerUse ? userTurn.text
|
|
318
|
+
: seesImages(turn) ? chatUserContent(userTurn)
|
|
319
|
+
: userTurn.images?.length ? `${userTurn.text}\n\n${TEXT_ONLY_ATTACHMENT_NOTE}` : userTurn.text },
|
|
306
320
|
];
|
|
307
321
|
};
|
|
308
322
|
const sendTurn = async (turn) => {
|
|
@@ -381,6 +395,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
381
395
|
let toolFailed = false;
|
|
382
396
|
const denials = [];
|
|
383
397
|
const seenCalls = new Set();
|
|
398
|
+
let nudged = false;
|
|
384
399
|
try {
|
|
385
400
|
tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse);
|
|
386
401
|
let optionalQuestionOnly = options.tools !== false && tools.definitions.length === 0;
|
|
@@ -490,6 +505,11 @@ export function createOpenAIChatRuntime(options) {
|
|
|
490
505
|
if (completion.finishReason && completion.finishReason !== "stop") {
|
|
491
506
|
throw new ChatProtocolError(`provider did not finish the response (${completion.finishReason})`);
|
|
492
507
|
}
|
|
508
|
+
if (options.nudgeAnnouncedAction && !nudged && tools.definitions.length && announcesAction(completion.text)) {
|
|
509
|
+
nudged = true;
|
|
510
|
+
messages.push({ role: "assistant", content: completion.text }, { role: "user", content: NUDGE_ANNOUNCED_ACTION });
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
493
513
|
if (toolFailed) {
|
|
494
514
|
stopReason = "tool_error";
|
|
495
515
|
throw new ChatProtocolError("One or more tool operations failed or were denied. See the tool results; the final response is not an execution receipt.");
|
|
@@ -508,7 +528,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
508
528
|
if (seenCalls.size > MAX_CHAT_TOOL_CALLS)
|
|
509
529
|
throw new ChatProtocolError("tool-call limit reached");
|
|
510
530
|
messages.push({ role: "assistant", content: completion.text || null, tool_calls: completion.toolCalls,
|
|
511
|
-
...(completion.protocolReasoning && !reasoningReplayRejected.has(model) ? { reasoning_content: completion.protocolReasoning } : {}),
|
|
531
|
+
...(completion.protocolReasoning && !reasoningReplayRejected.has(model) ? { [options.reasoningReplayField ?? "reasoning_content"]: completion.protocolReasoning } : {}),
|
|
512
532
|
...(completion.protocolReasoningDetails.length ? { reasoning_details: completion.protocolReasoningDetails } : {}),
|
|
513
533
|
});
|
|
514
534
|
const screenshotParts = [];
|
|
@@ -573,6 +593,10 @@ export function createOpenAIChatRuntime(options) {
|
|
|
573
593
|
started = true;
|
|
574
594
|
if (allowed) {
|
|
575
595
|
result = await tools.execute(call.function.name, args, abort.signal);
|
|
596
|
+
if (result.images?.length && !seesImages(turn)) {
|
|
597
|
+
const { images: _dropped, ...rest } = result;
|
|
598
|
+
result = { ...rest, text: `${rest.text}\n${TEXT_ONLY_SCREENSHOT_NOTE}` };
|
|
599
|
+
}
|
|
576
600
|
if (result.images?.length) {
|
|
577
601
|
try {
|
|
578
602
|
assertImageTransport(options.apiUrl);
|
|
@@ -57,6 +57,8 @@ export function readPromptSplitReceipt(scope, key) {
|
|
|
57
57
|
stable: record.stable,
|
|
58
58
|
volatile: record.volatile,
|
|
59
59
|
...(typeof record.turnsSinceFull === "number" ? { turnsSinceFull: record.turnsSinceFull } : {}),
|
|
60
|
+
...(typeof record.lastUsed === "number" && record.lastUsed > 0 ? { lastUsed: record.lastUsed } : {}),
|
|
61
|
+
...(typeof record.peakUsed === "number" && record.peakUsed > 0 ? { peakUsed: record.peakUsed } : {}),
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
}
|
|
@@ -64,8 +64,9 @@ function knownDirs() {
|
|
|
64
64
|
* invisible until it restarts, because Windows never pushes PATH changes
|
|
65
65
|
* into a live process. Scanning the standard install locations recovers
|
|
66
66
|
* those without a restart — `~/.grok/bin` (the x.ai installer) and
|
|
67
|
-
* `%APPDATA%\npm` (global npm shims), plus `%LOCALAPPDATA%\agy\bin
|
|
68
|
-
* every engine we ship an install
|
|
67
|
+
* `%APPDATA%\npm` (global npm shims), plus `%LOCALAPPDATA%\agy\bin` and
|
|
68
|
+
* `%LOCALAPPDATA%\cursor-agent`, cover every engine we ship an install
|
|
69
|
+
* command for. */
|
|
69
70
|
function windowsKnownDirs() {
|
|
70
71
|
const home = homedir();
|
|
71
72
|
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
@@ -74,12 +75,15 @@ function windowsKnownDirs() {
|
|
|
74
75
|
join(appData, "npm"), // npm -g shims: claude, codex
|
|
75
76
|
join(home, ".grok", "bin"), // x.ai installer
|
|
76
77
|
join(localAppData, "agy", "bin"), // Antigravity installer
|
|
78
|
+
// Cursor installer: cursor-agent.* and its `agent` copies (MOCA-272)
|
|
79
|
+
join(localAppData, "cursor-agent"),
|
|
77
80
|
join(home, ".local", "bin"), // claude native installer
|
|
78
81
|
join(home, ".claude", "local"),
|
|
79
82
|
join(home, "bin"), // Factory droid installer (%USERPROFILE%\bin)
|
|
80
83
|
join(home, ".bun", "bin"),
|
|
81
84
|
join(home, ".deno", "bin"),
|
|
82
85
|
join(home, "go", "bin"),
|
|
86
|
+
join(process.env.ProgramFiles ?? "C:\\Program Files", "Docker", "Docker", "resources", "bin"), // Docker Desktop installer
|
|
83
87
|
];
|
|
84
88
|
}
|
|
85
89
|
let cached = null;
|