openmausbot 0.1.91 → 0.1.92

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 (150) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-DsivfZEh.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-DKNEM8Q6.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-yC_29R6e.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.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/container-mcp.js +195 -15
  64. package/dist-server/drivers/agents-proxy.js +5 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +9300 -4155
  67. package/dist-server/local-computer-proxy.js +128 -7
  68. package/dist-server/mcp-gate.js +11 -5
  69. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  70. package/dist-server/openmausbot.js +3910 -1182
  71. package/dist-server/pair-cli.js +3910 -1182
  72. package/dist-server/server/boat.js +2 -2
  73. package/dist-server/server/browser-engine.js +10 -1
  74. package/dist-server/server/browser-live.js +46 -2
  75. package/dist-server/server/browser-runtime.js +74 -8
  76. package/dist-server/server/browser-tool-shape.js +6 -4
  77. package/dist-server/server/chief-of-staff.js +1 -1
  78. package/dist-server/server/cloud-home-start.js +118 -26
  79. package/dist-server/server/cloud-home.js +39 -3
  80. package/dist-server/server/cloud-lending.js +74 -10
  81. package/dist-server/server/cloud-owner.js +196 -0
  82. package/dist-server/server/cloud-secrets-boot.js +6 -0
  83. package/dist-server/server/cloud-secrets.js +65 -0
  84. package/dist-server/server/config.js +174 -14
  85. package/dist-server/server/container-computer.js +3 -2
  86. package/dist-server/server/desktop-viewer-targets.js +26 -0
  87. package/dist-server/server/drivers/acp/core.js +167 -21
  88. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  89. package/dist-server/server/drivers/agents-catalog.js +6 -6
  90. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  91. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  92. package/dist-server/server/drivers/antigravity.js +21 -0
  93. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  94. package/dist-server/server/drivers/claude.js +312 -34
  95. package/dist-server/server/drivers/codex.js +228 -18
  96. package/dist-server/server/drivers/grok.js +2 -2
  97. package/dist-server/server/drivers/mistral.js +4 -3
  98. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  99. package/dist-server/server/drivers/openai-chat.js +41 -3
  100. package/dist-server/server/drivers/openai-compat.js +73 -23
  101. package/dist-server/server/engine-install.js +46 -12
  102. package/dist-server/server/env-path.js +15 -3
  103. package/dist-server/server/harness/registry.js +2 -2
  104. package/dist-server/server/index.js +1338 -184
  105. package/dist-server/server/lending-memory.js +479 -0
  106. package/dist-server/server/local-vm-idle.js +24 -6
  107. package/dist-server/server/managed-desktop.js +3 -0
  108. package/dist-server/server/mcp-bridge.js +5 -3
  109. package/dist-server/server/mcp-http.js +6 -3
  110. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  111. package/dist-server/server/mcp-oauth-store.js +70 -0
  112. package/dist-server/server/mcp-oauth.js +550 -0
  113. package/dist-server/server/mcp-probe.js +14 -1
  114. package/dist-server/server/mcp-registry.js +43 -0
  115. package/dist-server/server/memory-upkeep.js +94 -84
  116. package/dist-server/server/model-catalog/catalog.js +374 -0
  117. package/dist-server/server/model-catalog/presets.js +176 -0
  118. package/dist-server/server/model-catalog/trim.js +219 -0
  119. package/dist-server/server/provider-key-check.js +4 -2
  120. package/dist-server/server/recent-work.js +1 -1
  121. package/dist-server/server/request-auth.js +23 -0
  122. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  123. package/dist-server/server/routes/bot-memory.js +31 -6
  124. package/dist-server/server/routes/desktop-viewer.js +194 -0
  125. package/dist-server/server/routine-requests.js +1 -1
  126. package/dist-server/server/routines.js +1 -1
  127. package/dist-server/server/sessions.js +56 -9
  128. package/dist-server/server/skills.js +25 -10
  129. package/dist-server/server/store.js +62 -122
  130. package/dist-server/server/thread-events.js +2 -0
  131. package/dist-server/server/thread-starters.js +33 -0
  132. package/dist-server/server/thread-title.js +46 -0
  133. package/dist-server/server/tts/fish.js +4 -5
  134. package/dist-server/server/tts/index.js +5 -3
  135. package/dist-server/server/vps-computer.js +79 -12
  136. package/dist-server/server/webhooks.js +18 -1
  137. package/dist-server/server/workspace-backup-policy.js +3 -0
  138. package/dist-server/server/workspace-backup.js +3 -0
  139. package/dist-server/server/workspace.js +65 -15
  140. package/dist-server/shared/workspace-backup-client.js +1 -0
  141. package/dist-server/src/lib/citations.js +177 -0
  142. package/dist-server/src/lib/composer-attachments.js +7 -1
  143. package/dist-server/vps-container-mcp.js +196 -15
  144. package/dist-server/workspace-backup.worker.js +73 -9
  145. package/enterprise/server/index.js +5 -1
  146. package/package.json +1 -1
  147. package/dist/assets/channel-DrfGMu7X.js +0 -1
  148. package/dist/assets/index-BlnCEcU6.js +0 -588
  149. package/dist/assets/index-DVxjOvID.css +0 -1
  150. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
@@ -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;
@@ -0,0 +1,360 @@
1
+ // Where Google's Windows Antigravity runtime unpacks itself, and how the
2
+ // space it leaves behind is reclaimed.
3
+ //
4
+ // Adapted from pingdotgg/t3code@c18e5ea6ed, MIT License, Copyright (c) 2026
5
+ // T3 Tools Inc. (full text in third_party/t3-code/LICENSE); modified:
6
+ // - apps/server/src/provider/antigravityAuthSupport.ts:187-212, 238-253
7
+ // (a short temp folder per instance, beside rather than inside the
8
+ // profile, and TEMP/TMP pointed at it for the runtime);
9
+ // - apps/server/src/provider/acp/AntigravitySessionFiles.ts:45-61 and
10
+ // apps/server/src/provider/Drivers/AntigravityDriver.ts:121-132
11
+ // (reclaim what earlier runs left behind when the driver starts).
12
+ // What OMB does differently: the folder is stable per instance, never per
13
+ // process, because the ACP pool's contractKey hashes the launch environment
14
+ // and a new TEMP per process would respawn on every turn. t3code's per-process
15
+ // run-* folders (AntigravityDriver.ts:202-227) are deliberately not copied.
16
+ // Because the folder is shared, the sweep removes only unpack folders whose
17
+ // process is gone instead of the whole folder. The user-started cleanup of the
18
+ // system temp folder is OMB's own; t3code never touches system temp.
19
+ //
20
+ // Background: agy_acp_server.exe is a PyInstaller one-file build. Every launch
21
+ // unpacks thousands of files (0.34-1.26 GB) into a new %TEMP%\_MEI... folder,
22
+ // and a forced stop (taskkill /T /F) skips PyInstaller's own cleanup, so the
23
+ // folder stays behind. PyInstaller names it `_MEI` + the unpacking process ID
24
+ // + a counter from _wtempnam: the ID is `%08x` since 6.22.1 and `%d` before
25
+ // (bootloader/src/pyi_utils_win32.c, pyi_create_temporary_application_directory).
26
+ import { createHash, randomUUID } from "node:crypto";
27
+ import { lstat, readdir, realpath, rename, rm } from "node:fs/promises";
28
+ import { tmpdir } from "node:os";
29
+ import { basename, dirname, join, resolve } from "node:path";
30
+ import { DATA_DIR } from "../config.js";
31
+ const UNPACK_PREFIX = "_MEI";
32
+ /** An unpack folder OMB has claimed by renaming it and is deleting. If the
33
+ * delete stops part way, the next sweep finishes it. */
34
+ const CLAIM_PREFIX = ".omb-removing-";
35
+ /** Files at the root of every Antigravity unpack folder. Other programs use
36
+ * PyInstaller too; without both of these a folder is not Antigravity's. */
37
+ const ANTIGRAVITY_MARKERS = ["agy_acp_licenses.txt", "python310.dll"];
38
+ /** A folder this recent may still be unpacking, even if its ID looks free. */
39
+ export const SYSTEM_LEFTOVER_MIN_AGE_MS = 10 * 60_000;
40
+ const INSTANCE_FOLDER = /^[0-9a-f]{12}$/u;
41
+ /** The folder every instance's temp folder lives in. `tmp/` is already left
42
+ * out of workspace backups (workspace-backup.ts EXCLUDED). */
43
+ export function antigravityTempRoot(dataDir = DATA_DIR) {
44
+ return join(dataDir, "tmp", "agy");
45
+ }
46
+ /** Used in place of an instance ID for the one temp folder every runtime
47
+ * verification shares. It has a space, which no instance ID has, so no
48
+ * instance shares the folder. Being one fixed folder, what a verification
49
+ * leaves behind (a runtime that would not stop) is swept like an instance's
50
+ * leftovers: after the next verification and when the driver starts. */
51
+ export const VERIFICATION_TEMP_KEY = "runtime verification";
52
+ /** One stable, short temp folder per instance. Short, because the runtime
53
+ * unpacks files up to 120 characters deep and Windows paths stop at 260.
54
+ * Hashed, so instance IDs that differ only in case stay apart on
55
+ * case-insensitive disks. */
56
+ export function antigravityTempDir(dataDir, instanceId) {
57
+ const key = createHash("sha256").update(instanceId).digest("hex").slice(0, 12);
58
+ return join(antigravityTempRoot(dataDir), key);
59
+ }
60
+ /** Point the runtime's temp folder at `directory`. Windows only: that is
61
+ * where the runtime unpacks itself. Windows environment names ignore case,
62
+ * so an inherited `Temp` or `tmp` would otherwise sit beside ours and either
63
+ * could win. */
64
+ export function setAntigravityTempEnvironment(environment, directory, platform = process.platform) {
65
+ if (platform !== "win32")
66
+ return;
67
+ for (const key of Object.keys(environment)) {
68
+ const upper = key.toUpperCase();
69
+ if (upper === "TEMP" || upper === "TMP")
70
+ delete environment[key];
71
+ }
72
+ environment.TEMP = directory;
73
+ environment.TMP = directory;
74
+ }
75
+ /** Process IDs that could own an unpack folder with this name, or null when
76
+ * the name is not a PyInstaller unpack folder at all.
77
+ *
78
+ * Since 6.22.1 the ID is 8 hex digits. Before that it is plain decimal and
79
+ * runs straight into _wtempnam's decimal counter, so the split is ambiguous:
80
+ * every split that leaves a 1-3 digit counter is a candidate. A folder counts
81
+ * as abandoned only when every candidate is gone, so the guess can keep a
82
+ * leftover a while longer but can never delete a live one. */
83
+ export function unpackFolderOwnerPids(name) {
84
+ if (!name.startsWith(UNPACK_PREFIX))
85
+ return null;
86
+ const rest = name.slice(UNPACK_PREFIX.length);
87
+ const pids = new Set();
88
+ if (/^[0-9a-f]{8}\d+$/iu.test(rest))
89
+ pids.add(Number.parseInt(rest.slice(0, 8), 16));
90
+ if (/^[1-9]\d+$/u.test(rest)) {
91
+ for (let counter = 1; counter <= 3 && counter < rest.length; counter++) {
92
+ pids.add(Number.parseInt(rest.slice(0, rest.length - counter), 10));
93
+ }
94
+ }
95
+ const valid = [...pids].filter((pid) => Number.isSafeInteger(pid) && pid > 0);
96
+ return valid.length ? valid : null;
97
+ }
98
+ /** Whether a process with this ID exists. Only "no such process" (ESRCH)
99
+ * proves it is gone: EPERM means it exists but belongs to someone else. */
100
+ export function processIsAlive(pid, kill = (target, signal) => process.kill(target, signal)) {
101
+ try {
102
+ kill(pid, 0);
103
+ return true;
104
+ }
105
+ catch (error) {
106
+ return error?.code !== "ESRCH";
107
+ }
108
+ }
109
+ function ownedByDeadProcess(name, isAlive) {
110
+ const pids = unpackFolderOwnerPids(name);
111
+ return pids !== null && !pids.some((pid) => isAlive(pid));
112
+ }
113
+ /** A real directory (not a link or junction to one), nothing there at all,
114
+ * or anything else. */
115
+ async function entryKind(path) {
116
+ try {
117
+ const info = await lstat(path);
118
+ return info.isDirectory() && !info.isSymbolicLink() ? "directory" : "other";
119
+ }
120
+ catch (error) {
121
+ return error?.code === "ENOENT" ? "gone" : "other";
122
+ }
123
+ }
124
+ async function realDirectory(path) {
125
+ return (await entryKind(path)) === "directory";
126
+ }
127
+ async function listDirectory(path) {
128
+ try {
129
+ return await readdir(path, { withFileTypes: true });
130
+ }
131
+ catch {
132
+ return [];
133
+ }
134
+ }
135
+ /** The instance folders under OMB's temp root, each resolved through the
136
+ * disk. Returns nothing when any step of dataDir/tmp/agy/<id> is a link or
137
+ * junction, so a sweep can never be steered outside OMB's own folder. */
138
+ async function ownedTempDirectories(dataDir, only) {
139
+ const root = resolve(antigravityTempRoot(dataDir));
140
+ let realRoot;
141
+ try {
142
+ const realData = await realpath(resolve(dataDir));
143
+ realRoot = await realpath(root);
144
+ if (realRoot !== join(realData, "tmp", "agy"))
145
+ return [];
146
+ }
147
+ catch {
148
+ return [];
149
+ }
150
+ const names = only === undefined
151
+ ? (await listDirectory(root)).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
152
+ : [only];
153
+ const owned = [];
154
+ for (const name of names) {
155
+ if (!INSTANCE_FOLDER.test(name))
156
+ continue;
157
+ const directory = join(root, name);
158
+ try {
159
+ if (await realpath(directory) !== join(realRoot, name))
160
+ continue;
161
+ }
162
+ catch {
163
+ continue;
164
+ }
165
+ owned.push(directory);
166
+ }
167
+ return owned;
168
+ }
169
+ /** Unpack folders in one OMB-owned temp folder whose process is gone, and
170
+ * folders a previous sweep claimed but did not finish deleting. */
171
+ async function abandonedIn(directory, isAlive) {
172
+ const found = [];
173
+ for (const entry of await listDirectory(directory)) {
174
+ // Dirent follows lstat: a link or junction is never a directory here.
175
+ if (!entry.isDirectory())
176
+ continue;
177
+ if (entry.name.startsWith(CLAIM_PREFIX) || ownedByDeadProcess(entry.name, isAlive)) {
178
+ found.push(join(directory, entry.name));
179
+ }
180
+ }
181
+ return found;
182
+ }
183
+ async function isAntigravityUnpack(directory) {
184
+ for (const marker of ANTIGRAVITY_MARKERS) {
185
+ try {
186
+ if (!(await lstat(join(directory, marker))).isFile())
187
+ return false;
188
+ }
189
+ catch {
190
+ return false;
191
+ }
192
+ }
193
+ return true;
194
+ }
195
+ /** Antigravity unpack folders in the system temp folder: both marker files,
196
+ * no live owner, and untouched for ten minutes. Other apps (Zed, t3code)
197
+ * start the same runtime, so this runs only when the person asks. */
198
+ async function systemLeftovers(systemTemp, isAlive, now) {
199
+ const found = [];
200
+ for (const entry of await listDirectory(systemTemp)) {
201
+ if (!entry.isDirectory())
202
+ continue;
203
+ const directory = join(systemTemp, entry.name);
204
+ const claimed = entry.name.startsWith(CLAIM_PREFIX + UNPACK_PREFIX);
205
+ if (!claimed && !ownedByDeadProcess(entry.name, isAlive))
206
+ continue;
207
+ if (!claimed && !(await isAntigravityUnpack(directory)))
208
+ continue;
209
+ try {
210
+ if (now - (await lstat(directory)).mtimeMs < SYSTEM_LEFTOVER_MIN_AGE_MS)
211
+ continue;
212
+ }
213
+ catch {
214
+ continue;
215
+ }
216
+ found.push(directory);
217
+ }
218
+ return found;
219
+ }
220
+ /** Bytes under a folder, without following links. Stops at the deadline. */
221
+ async function folderBytes(directory, deadline = Infinity) {
222
+ let bytes = 0;
223
+ const pending = [directory];
224
+ while (pending.length) {
225
+ if (Date.now() > deadline)
226
+ return { bytes, complete: false };
227
+ const current = pending.pop();
228
+ for (const entry of await listDirectory(current)) {
229
+ const path = join(current, entry.name);
230
+ if (entry.isDirectory())
231
+ pending.push(path);
232
+ else if (entry.isFile()) {
233
+ try {
234
+ bytes += (await lstat(path)).size;
235
+ }
236
+ catch { /* already gone */ }
237
+ }
238
+ }
239
+ }
240
+ return { bytes, complete: true };
241
+ }
242
+ /** Claim a folder by renaming it, then delete it. On Windows the rename
243
+ * fails while any process still has a file inside open (every live runtime
244
+ * has its DLLs loaded), which is a second check after the process ID.
245
+ * Returns the bytes freed; "gone" when someone else (the automatic sweep and
246
+ * "Free up space" can pick the same folder) removed it first; or null when
247
+ * the folder was left alone. */
248
+ async function claimAndRemove(directory, countBytes) {
249
+ const name = basename(directory);
250
+ // Checked again at the last moment: the listing may be stale.
251
+ const kind = await entryKind(directory);
252
+ if (kind !== "directory")
253
+ return kind === "gone" ? "gone" : null;
254
+ let claimed = directory;
255
+ if (!name.startsWith(CLAIM_PREFIX)) {
256
+ claimed = join(dirname(directory), `${CLAIM_PREFIX}${name}-${randomUUID().slice(0, 8)}`);
257
+ try {
258
+ await rename(directory, claimed);
259
+ }
260
+ catch (error) {
261
+ // Lost the race to another remover: the folder is gone, not in use.
262
+ if (error?.code === "ENOENT" && (await entryKind(directory)) === "gone")
263
+ return "gone";
264
+ return null;
265
+ }
266
+ }
267
+ const { bytes } = countBytes ? await folderBytes(claimed) : { bytes: 0 };
268
+ try {
269
+ await rm(claimed, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
270
+ return bytes;
271
+ }
272
+ catch {
273
+ // The claimed name stays, and the next sweep finishes the job.
274
+ return null;
275
+ }
276
+ }
277
+ const sweeps = new Map();
278
+ /** Delete this instance's abandoned unpack folders. Runs when the driver
279
+ * starts and before each launch (and, for VERIFICATION_TEMP_KEY, after each
280
+ * runtime verification). Touches nothing outside dataDir/tmp/agy/<instance>.
281
+ * Returns how many folders it removed. */
282
+ export function sweepAntigravityTemp(instanceId, options = {}) {
283
+ const dataDir = options.dataDir ?? DATA_DIR;
284
+ const key = antigravityTempDir(dataDir, instanceId);
285
+ const running = sweeps.get(key);
286
+ if (running)
287
+ return running;
288
+ const sweep = (async () => {
289
+ const isAlive = options.isAlive ?? processIsAlive;
290
+ let removed = 0;
291
+ for (const directory of await ownedTempDirectories(dataDir, basename(key))) {
292
+ for (const folder of await abandonedIn(directory, isAlive)) {
293
+ if (typeof await claimAndRemove(folder, false) === "number")
294
+ removed++;
295
+ }
296
+ }
297
+ return removed;
298
+ })().finally(() => sweeps.delete(key));
299
+ sweeps.set(key, sweep);
300
+ return sweep;
301
+ }
302
+ /** Fire-and-forget form for launch paths: never delays or fails a turn. */
303
+ export function scheduleAntigravityTempSweep(instanceId, options = {}) {
304
+ void sweepAntigravityTemp(instanceId, options).catch((error) => {
305
+ console.warn(`antigravity: could not clear leftover runtime files: ${error instanceof Error ? error.message : String(error)}`);
306
+ });
307
+ }
308
+ async function leftoverCandidates(options) {
309
+ const isAlive = options.isAlive ?? processIsAlive;
310
+ const candidates = [];
311
+ for (const directory of await ownedTempDirectories(options.dataDir ?? DATA_DIR)) {
312
+ for (const path of await abandonedIn(directory, isAlive))
313
+ candidates.push({ path, where: "app" });
314
+ }
315
+ if ((options.platform ?? process.platform) === "win32") {
316
+ // TEMP itself may legitimately be a junction; what is inside it may not.
317
+ const systemTemp = await realpath(resolve(options.systemTemp ?? tmpdir())).catch(() => null);
318
+ if (systemTemp && await realDirectory(systemTemp)) {
319
+ for (const path of await systemLeftovers(systemTemp, isAlive, options.now ?? Date.now())) {
320
+ candidates.push({ path, where: "system" });
321
+ }
322
+ }
323
+ }
324
+ return candidates;
325
+ }
326
+ /** What "Free up space" would delete, and roughly how much it is. Deletes
327
+ * nothing. */
328
+ export async function findAntigravityLeftovers(options = {}) {
329
+ const deadline = Date.now() + (options.budgetMs ?? 15_000);
330
+ const folders = [];
331
+ let complete = true;
332
+ for (const candidate of await leftoverCandidates(options)) {
333
+ const counted = await folderBytes(candidate.path, deadline);
334
+ complete &&= counted.complete;
335
+ folders.push({ ...candidate, bytes: counted.bytes });
336
+ }
337
+ return { folders, bytes: folders.reduce((total, folder) => total + folder.bytes, 0), complete };
338
+ }
339
+ /** "Free up space": looks again rather than trusting an earlier scan, so a
340
+ * folder that came back to life since is left alone. */
341
+ export async function removeAntigravityLeftovers(options = {}) {
342
+ const deadline = Date.now() + (options.budgetMs ?? 120_000);
343
+ const candidates = await leftoverCandidates(options);
344
+ let removed = 0;
345
+ let alreadyGone = 0;
346
+ let freedBytes = 0;
347
+ for (const candidate of candidates) {
348
+ if (Date.now() > deadline)
349
+ break;
350
+ const bytes = await claimAndRemove(candidate.path, true);
351
+ if (bytes === "gone")
352
+ alreadyGone++;
353
+ if (typeof bytes !== "number")
354
+ continue;
355
+ removed++;
356
+ freedBytes += bytes;
357
+ }
358
+ // A folder another remover got to first is not "still in use".
359
+ return { removed, freedBytes, remaining: candidates.length - removed - alreadyGone };
360
+ }
@@ -2,6 +2,7 @@ import { createAcpDriver } from "./acp/core.js";
2
2
  import { AntigravityAuthController, antigravityProfileAuthenticated, catalogFromAntigravityConfigOptions, prepareAntigravityProfile, probeAntigravityModels, validateAntigravityRuntime, } from "./antigravity-acp.js";
3
3
  import { antigravityManagedInstallAvailable, installAntigravityRuntime, resolveAntigravityRuntime, } from "./antigravity-runtime.js";
4
4
  import { resolveAntigravityReleaseAsset } from "./antigravity-release.js";
5
+ import { VERIFICATION_TEMP_KEY, scheduleAntigravityTempSweep } from "./antigravity-temp.js";
5
6
  import { augmentedPath } from "../env-path.js";
6
7
  export const STATIC_ANTIGRAVITY_MODELS = {
7
8
  default: "gemini-3.8-flash-high",
@@ -39,6 +40,14 @@ const support = {
39
40
  images: true,
40
41
  spawnArgs: () => [],
41
42
  selectModel: { configId: "model" },
43
+ // Google's server reads its new-session default from this variable
44
+ // (model_selection.get_default_model_id in agy_acp_server 1.1.1). Starting
45
+ // on OMB's model skips a switch that re-fetches the account's model list
46
+ // from Google: 1.3-1.8 s on every new or cold-resumed conversation.
47
+ sessionModelEnv: "AGY_ACP_DEFAULT_MODEL",
48
+ // configureSession sets the permission mode on every turn and nothing in
49
+ // the launch depends on it, so an approval change keeps the process.
50
+ sessionScopedApproval: true,
42
51
  resumeMethod: "resume",
43
52
  clientFileSystem: true,
44
53
  redactStderr: true,
@@ -47,6 +56,11 @@ const support = {
47
56
  resolveCommand: async (env, config, instanceId) => {
48
57
  const runtime = await resolveAntigravityRuntime(config.cli, env);
49
58
  const profile = await prepareAntigravityProfile({ instanceId, runtime, baseEnv: env });
59
+ // A process stopped by force since the last launch (an idle close that
60
+ // outlived its grace, a crash) left its unpacked files behind. Reclaim
61
+ // them in the background; live folders are kept.
62
+ if (profile.tempDirectory)
63
+ scheduleAntigravityTempSweep(instanceId);
50
64
  return {
51
65
  command: runtime.executablePath,
52
66
  args: process.platform === "linux" ? ["--uid="] : [],
@@ -112,6 +126,13 @@ export const AntigravityDriver = {
112
126
  ...AcpAntigravityDriver,
113
127
  async create(input) {
114
128
  const base = await AcpAntigravityDriver.create(input);
129
+ // Reclaim what a previous run of this instance left behind when it was
130
+ // stopped by force (Windows keeps 0.34-1.26 GB per forced stop). Only
131
+ // this instance's folder under DATA_DIR/tmp/agy, only folders whose
132
+ // process is gone; the system temp folder is cleaned only on request.
133
+ scheduleAntigravityTempSweep(input.instanceId);
134
+ // The same for a runtime verification whose runtime would not stop.
135
+ scheduleAntigravityTempSweep(VERIFICATION_TEMP_KEY);
115
136
  const auth = new AntigravityAuthController();
116
137
  let installFailure;
117
138
  const runtimeAndProfile = async () => {