openmausbot 0.1.91 → 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.
Files changed (164) hide show
  1. package/dist/assets/DesktopViewer-CKNR3SEW.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
  3. package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
  7. package/dist/assets/channel-BU94VpxE.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
  34. package/dist/assets/index-Biji-bfL.css +1 -0
  35. package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
  36. package/dist/assets/index-DZlhoES0.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
  41. package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
  42. package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist/novnc-NOTICE.txt +118 -0
  62. package/dist-server/cloud-home-start.js +112 -14
  63. package/dist-server/companion/src/routes.js +24 -1
  64. package/dist-server/container-mcp.js +201 -16
  65. package/dist-server/drivers/agents-proxy.js +7 -4
  66. package/dist-server/enterprise/server/index.js +5 -1
  67. package/dist-server/index.js +10698 -4737
  68. package/dist-server/local-computer-proxy.js +133 -8
  69. package/dist-server/mcp-gate.js +16 -6
  70. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  71. package/dist-server/openmausbot.js +4656 -1258
  72. package/dist-server/pair-cli.js +4656 -1258
  73. package/dist-server/server/boat.js +5 -5
  74. package/dist-server/server/browser-engine.js +10 -1
  75. package/dist-server/server/browser-live.js +46 -2
  76. package/dist-server/server/browser-runtime.js +74 -8
  77. package/dist-server/server/browser-tool-shape.js +6 -4
  78. package/dist-server/server/chief-of-staff.js +1 -1
  79. package/dist-server/server/cloud-home-start.js +118 -26
  80. package/dist-server/server/cloud-home.js +39 -3
  81. package/dist-server/server/cloud-lending.js +74 -10
  82. package/dist-server/server/cloud-owner.js +196 -0
  83. package/dist-server/server/cloud-secrets-boot.js +6 -0
  84. package/dist-server/server/cloud-secrets.js +65 -0
  85. package/dist-server/server/computer-control.js +6 -0
  86. package/dist-server/server/config.js +187 -14
  87. package/dist-server/server/container-computer.js +46 -34
  88. package/dist-server/server/desktop-viewer-targets.js +26 -0
  89. package/dist-server/server/digest.js +12 -2
  90. package/dist-server/server/drivers/acp/core.js +339 -33
  91. package/dist-server/server/drivers/acp/cursor.js +13 -1
  92. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  93. package/dist-server/server/drivers/acp/quiet-status.js +257 -0
  94. package/dist-server/server/drivers/acp/qwen.js +18 -0
  95. package/dist-server/server/drivers/agents-catalog.js +6 -6
  96. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  97. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  98. package/dist-server/server/drivers/antigravity.js +21 -0
  99. package/dist-server/server/drivers/builtIn.js +2 -0
  100. package/dist-server/server/drivers/cerebras.js +108 -0
  101. package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
  102. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  103. package/dist-server/server/drivers/claude.js +329 -37
  104. package/dist-server/server/drivers/codex.js +228 -18
  105. package/dist-server/server/drivers/grok.js +2 -2
  106. package/dist-server/server/drivers/mistral.js +4 -3
  107. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  108. package/dist-server/server/drivers/openai-chat.js +66 -4
  109. package/dist-server/server/drivers/openai-compat.js +73 -23
  110. package/dist-server/server/drivers/prompt-split.js +2 -0
  111. package/dist-server/server/engine-install.js +46 -12
  112. package/dist-server/server/env-path.js +21 -5
  113. package/dist-server/server/harness/registry.js +2 -2
  114. package/dist-server/server/index.js +1545 -238
  115. package/dist-server/server/lending-memory.js +479 -0
  116. package/dist-server/server/local-vm-idle.js +24 -6
  117. package/dist-server/server/local-vm-stop-reason.js +26 -0
  118. package/dist-server/server/managed-desktop.js +3 -0
  119. package/dist-server/server/mcp-bridge.js +5 -3
  120. package/dist-server/server/mcp-http.js +6 -3
  121. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  122. package/dist-server/server/mcp-oauth-store.js +70 -0
  123. package/dist-server/server/mcp-oauth.js +550 -0
  124. package/dist-server/server/mcp-probe.js +14 -1
  125. package/dist-server/server/mcp-registry.js +43 -0
  126. package/dist-server/server/memory-upkeep.js +94 -84
  127. package/dist-server/server/model-catalog/catalog.js +374 -0
  128. package/dist-server/server/model-catalog/presets.js +176 -0
  129. package/dist-server/server/model-catalog/trim.js +219 -0
  130. package/dist-server/server/provider-key-check.js +5 -2
  131. package/dist-server/server/recent-work.js +1 -1
  132. package/dist-server/server/request-auth.js +23 -0
  133. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  134. package/dist-server/server/routes/bot-memory.js +31 -6
  135. package/dist-server/server/routes/desktop-viewer.js +238 -0
  136. package/dist-server/server/routine-requests.js +1 -1
  137. package/dist-server/server/routines.js +6 -3
  138. package/dist-server/server/sessions.js +56 -9
  139. package/dist-server/server/skills.js +25 -10
  140. package/dist-server/server/store.js +116 -122
  141. package/dist-server/server/thread-events.js +2 -0
  142. package/dist-server/server/thread-starters.js +33 -0
  143. package/dist-server/server/thread-title.js +46 -0
  144. package/dist-server/server/tts/fish.js +4 -5
  145. package/dist-server/server/tts/index.js +5 -3
  146. package/dist-server/server/vps-computer.js +80 -13
  147. package/dist-server/server/webhooks.js +18 -1
  148. package/dist-server/server/workspace-backup-policy.js +3 -0
  149. package/dist-server/server/workspace-backup.js +25 -0
  150. package/dist-server/server/workspace.js +66 -16
  151. package/dist-server/shared/approval-mode.js +1 -1
  152. package/dist-server/shared/redact.js +1 -0
  153. package/dist-server/shared/tool-name.js +22 -0
  154. package/dist-server/shared/workspace-backup-client.js +1 -0
  155. package/dist-server/src/lib/citations.js +177 -0
  156. package/dist-server/src/lib/composer-attachments.js +7 -1
  157. package/dist-server/vps-container-mcp.js +202 -16
  158. package/dist-server/workspace-backup.worker.js +87 -9
  159. package/enterprise/server/index.js +5 -1
  160. package/package.json +1 -1
  161. package/dist/assets/channel-DrfGMu7X.js +0 -1
  162. package/dist/assets/index-BlnCEcU6.js +0 -588
  163. package/dist/assets/index-DVxjOvID.css +0 -1
  164. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.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);
@@ -25,6 +25,7 @@ export function catalogProfileFromEnv(env) {
25
25
  sharedComputers: env.OMB_SHARED_COMPUTERS_ENABLED === "1",
26
26
  voiceNotes: env.OMB_VOICE_NOTES === "1",
27
27
  cloudHome: env.OMB_CLOUD_HOME === "1",
28
+ memoryEnabled: env.OMB_MEMORY_ENABLED !== "0",
28
29
  botId: env.OMB_BOT_ID ?? "",
29
30
  };
30
31
  }
@@ -210,14 +211,14 @@ const toolDefinitions = (externalRuntime) => [
210
211
  },
211
212
  {
212
213
  name: "coordinate_bots",
213
- description: "Ask existing OpenMausBot teammates for advice or assign concrete work. From normal chat every assignment you send a teammate continues your one standing conversation with that teammate, so they keep the context of what you asked before; from a room it defaults to this room. Use group_id from list_room_targets for a specific room. Give 1-4 bot_ids — teammate ids as list_bots or your roster prints them; a unique teammate name also resolves: they receive only your brief and use their own model, tools and permissions. Busy bots queue. They can consult their specialists; all results return here and resume you automatically. Include exact file paths, constraints and what must be verified. After sending all assignments, END your turn; do not poll or wait. On return, resolve tradeoffs, verify the requested outcome and request concrete corrections if necessary before giving one final answer. Do not send acknowledgements as new work.",
214
+ description: "Ask existing OpenMausBot teammates for advice or assign concrete work. From normal chat each distinct assignment starts a fresh thread for that teammate; from a room it defaults to this room. Give a self-contained brief. Use group_id from list_room_targets for a specific room. Give 1-4 bot_ids — teammate ids as list_bots or your roster prints them; a unique teammate name also resolves: they receive only your brief and use their own model, tools and permissions. Busy bots queue. They can consult their specialists; all results return here and resume you automatically. Include exact file paths, constraints and what must be verified. After sending all assignments, END your turn; do not poll or wait. On return, resolve tradeoffs, verify the requested outcome and request concrete corrections if necessary before giving one final answer. Do not send acknowledgements as new work.",
214
215
  inputSchema: { type: "object", additionalProperties: false, properties: {
215
- group_id: { type: "string", description: "Optional destination room. Omit for this room, or your standing conversation with each teammate when chatting directly." },
216
+ group_id: { type: "string", description: "Optional destination room. Omit for this room, or a fresh thread for each teammate when chatting directly." },
216
217
  bot_ids: { type: "array", items: { type: "string", description: "A teammate's id exactly as list_bots or your roster prints it ([id: …]). A teammate's unique display name also resolves; a name shared by two reachable teammates is refused." }, minItems: 1, maxItems: 4, uniqueItems: true },
217
218
  message: { type: "string", minLength: 1, maxLength: 4000, description: "Self-contained question or task for these teammates. Send separate requests when responsibilities differ." },
218
219
  request_key: { type: "string", description: "A short unique assignment key. Reuse for an identical retry." },
219
220
  rework: { type: "boolean", description: "True only for concrete additional work from someone who already completed a request." },
220
- label: { type: "string", description: "Optional short name (one line, at most 60 characters) for this job. Used only when the teammate is still working on your previous assignment and this one therefore runs in its own thread beside your standing conversation." },
221
+ label: { type: "string", description: "Optional short name (one line, at most 60 characters) for this job. Names the new direct assignment thread." },
221
222
  }, required: ["bot_ids", "message", "request_key"] },
222
223
  },
223
224
  {
@@ -778,9 +779,8 @@ export function availableTools(profile) {
778
779
  }
779
780
  function catalogTools(profile) {
780
781
  const TOOLS = toolDefinitions(profile.externalRuntime);
781
- const BOT_SCOPED_TOOLS = profile.botId === WATCHER_OPTIONS_CARD_BOT_ID
782
- ? TOOLS
783
- : TOOLS.filter((tool) => !WATCHER_TOOL_NAMES.has(tool.name));
782
+ const BOT_SCOPED_TOOLS = TOOLS.filter((tool) => (profile.botId === WATCHER_OPTIONS_CARD_BOT_ID || !WATCHER_TOOL_NAMES.has(tool.name)) &&
783
+ (profile.memoryEnabled !== false || (tool.name !== "memory_update" && tool.name !== "memory_log")));
784
784
  const AUTHORING_TOOLS = profile.skillAuthoring
785
785
  ? BOT_SCOPED_TOOLS
786
786
  : BOT_SCOPED_TOOLS.filter((tool) => !SKILL_TOOL_NAMES.has(tool.name));
@@ -6,6 +6,7 @@ import { delimiter, join, resolve } from "node:path";
6
6
  import { DATA_DIR, stripWorkspaceCredentialEnv } from "../config.js";
7
7
  import { writeFileAtomic } from "../atomic.js";
8
8
  import { killCliTree, spawnCli } from "../procs.js";
9
+ import { VERIFICATION_TEMP_KEY, antigravityTempDir, setAntigravityTempEnvironment, sweepAntigravityTemp, } from "./antigravity-temp.js";
9
10
  // Printed on stderr by Google's server, not stdout.
10
11
  export const ANTIGRAVITY_AUTH_PREFIX = "Open the following link to authenticate the ACP server: ";
11
12
  const AUTH_TIMEOUT_MS = 5 * 60_000;
@@ -16,6 +17,10 @@ const MAX_PROTOCOL_LINE_BYTES = 16 * 1024 * 1024;
16
17
  // Past the 16 KiB ceiling parseAntigravityAuthorizationUrl accepts, so a
17
18
  // partial stderr line is never dropped while it could still become a link.
18
19
  const MAX_DIAGNOSTIC_LINE_CHARS = 64 * 1024;
20
+ // How long a closed runtime gets to exit on its own after its input ends.
21
+ // A forced stop on Windows skips the runtime's own cleanup of the files it
22
+ // unpacked; exiting by itself removes them. Same grace as the ACP pool.
23
+ const GRACEFUL_EXIT_MS = 5_000;
19
24
  const REMOVED_ENVIRONMENT_KEYS = new Set([
20
25
  "GEMINI_API_KEY",
21
26
  "GOOGLE_API_KEY",
@@ -28,6 +33,8 @@ const REMOVED_ENVIRONMENT_KEYS = new Set([
28
33
  "CLOUDSDK_CORE_PROJECT",
29
34
  "AGY_ACP_CCPA_PROJECT",
30
35
  "AGY_ACP_ENABLE_OAUTH",
36
+ // OMB sets the new-session model itself (antigravity.ts sessionModelEnv).
37
+ "AGY_ACP_DEFAULT_MODEL",
31
38
  "GEMINI_HOME",
32
39
  "AGY_ACP_FORCE_FILE_STORAGE",
33
40
  "ANTIGRAVITY_HARNESS_PATH",
@@ -121,7 +128,22 @@ export async function prepareAntigravityProfile(input) {
121
128
  PYTHONUNBUFFERED: "1",
122
129
  ELECTRON_RUN_AS_NODE: "1",
123
130
  });
124
- return { directory, tokenPath: join(acpDirectory, "acp_token.json"), environment };
131
+ // On Windows the runtime unpacks 0.34-1.26 GB into TEMP on every launch.
132
+ // Keep that in a folder OMB owns and can clean. It is the same folder for
133
+ // every launch of this instance, so the pooled process's spawn contract
134
+ // (which hashes this environment) does not change from turn to turn.
135
+ let tempDirectory;
136
+ if ((input.platform ?? process.platform) === "win32") {
137
+ tempDirectory = resolve(input.tempDirectory ?? antigravityTempDir(input.baseDir ?? DATA_DIR, input.instanceId));
138
+ await mkdir(tempDirectory, { recursive: true });
139
+ setAntigravityTempEnvironment(environment, tempDirectory, "win32");
140
+ }
141
+ return {
142
+ directory,
143
+ tokenPath: join(acpDirectory, "acp_token.json"),
144
+ environment,
145
+ ...(tempDirectory ? { tempDirectory } : {}),
146
+ };
125
147
  }
126
148
  export async function antigravityProfileAuthenticated(profile) {
127
149
  try {
@@ -177,6 +199,10 @@ export class AntigravityAcpClient {
177
199
  });
178
200
  }
179
201
  consume(chunk) {
202
+ // Output after close has no reader; do not buffer it while the process
203
+ // is given time to exit.
204
+ if (this.closed)
205
+ return;
180
206
  if (!this.initializationComplete)
181
207
  this.startupOutputBytes += Buffer.byteLength(chunk);
182
208
  this.buffer += chunk;
@@ -287,15 +313,40 @@ export class AntigravityAcpClient {
287
313
  this.nativeStartupHint = undefined;
288
314
  return initialized;
289
315
  }
316
+ /** End the runtime's input and let it exit by itself, so it removes what
317
+ * it unpacked; stop it by force only if it is still running after
318
+ * GRACEFUL_EXIT_MS. killCliTree also runs after a clean exit. On macOS and
319
+ * Linux that reaps any helper still in the process group. On Windows it
320
+ * does nothing once the runtime has exited (procs.ts stopCliTree), so a
321
+ * helper that outlives a clean exit is left running, as in the ACP pool. */
290
322
  close() {
291
323
  if (this.closed)
292
324
  return;
293
325
  this.closed = true;
294
326
  this.failAll(new Error("Antigravity ACP was closed."));
295
- this.stopping = killCliTree(this.child);
327
+ this.stopping = this.stopGracefully();
296
328
  }
297
- /** Allow the shared 5s TERM grace and 1s force-stop verification to finish. */
298
- async closeAndWait(timeoutMs = 7_000) {
329
+ async stopGracefully() {
330
+ try {
331
+ this.child.stdin?.end();
332
+ }
333
+ catch {
334
+ /* already closed; the kill below still applies */
335
+ }
336
+ let timer;
337
+ await Promise.race([
338
+ this.exited,
339
+ new Promise((resolve) => {
340
+ timer = setTimeout(resolve, GRACEFUL_EXIT_MS);
341
+ timer.unref?.();
342
+ }),
343
+ ]);
344
+ clearTimeout(timer);
345
+ return killCliTree(this.child);
346
+ }
347
+ /** Allow the graceful exit, the shared 5s TERM grace, and the 1s
348
+ * force-stop verification to finish. */
349
+ async closeAndWait(timeoutMs = 12_000) {
299
350
  this.close();
300
351
  let timer;
301
352
  const timedOut = new Promise((resolve) => {
@@ -478,20 +529,19 @@ export function isValidAntigravityInitializeResult(initialized, expectedVersion)
478
529
  export async function validateAntigravityRuntime(runtime, expectedVersion) {
479
530
  const profileDirectory = await mkdtemp(join(tmpdir(), "openmaus-antigravity-verify-"));
480
531
  let client;
532
+ let tempDirectory;
481
533
  let failed = false;
482
534
  let failure;
483
535
  try {
536
+ // On Windows every verification unpacks into one shared folder under
537
+ // DATA_DIR/tmp/agy, swept below once the runtime is gone.
484
538
  const profile = await prepareAntigravityProfile({
485
539
  instanceId: `verify-${randomUUID()}`,
486
540
  runtime,
487
541
  profileDirectory,
488
- // Google's Windows one-file executable expands a large Python runtime
489
- // into TEMP. Forced shutdown skips its own cleanup. Keep verification's
490
- // extraction inside the profile we already remove after confirmed close.
491
- baseEnv: process.platform === "win32"
492
- ? { ...process.env, TEMP: profileDirectory, TMP: profileDirectory }
493
- : undefined,
542
+ tempDirectory: antigravityTempDir(DATA_DIR, VERIFICATION_TEMP_KEY),
494
543
  });
544
+ tempDirectory = profile.tempDirectory;
495
545
  client = new AntigravityAcpClient(runtime, profile, profileDirectory);
496
546
  const initialized = await client.initialize();
497
547
  if (!isValidAntigravityInitializeResult(initialized, expectedVersion)) {
@@ -521,8 +571,16 @@ export async function validateAntigravityRuntime(runtime, expectedVersion) {
521
571
  if (stopped) {
522
572
  await rm(profileDirectory, { recursive: true, force: true, maxRetries: 4, retryDelay: 250 })
523
573
  .catch((error) => {
524
- console.warn(`antigravity: could not remove verification profile ${profileDirectory}: ${error instanceof Error ? error.message : String(error)}`);
574
+ console.warn(`antigravity: could not remove verification files ${profileDirectory}: ${error instanceof Error ? error.message : String(error)}`);
525
575
  });
576
+ // The temp folder is shared, so only unpack folders whose process is
577
+ // gone go: this run's, and any an earlier run left when its runtime
578
+ // would not stop.
579
+ if (tempDirectory) {
580
+ await sweepAntigravityTemp(VERIFICATION_TEMP_KEY).catch((error) => {
581
+ console.warn(`antigravity: could not clear verification files: ${error instanceof Error ? error.message : String(error)}`);
582
+ });
583
+ }
526
584
  }
527
585
  if (failed)
528
586
  throw failure;