synabun 2.1.0 → 2.2.0

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 (317) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +1 -1
  3. package/docs/whatsapp.md +11 -11
  4. package/lib/client-config-repair.js +13 -9
  5. package/lib/external-tools.js +6 -5
  6. package/lib/update-snapshot.js +77 -3
  7. package/lib/virustotal.mjs +12 -1
  8. package/mcp-server/dist/index.js +1 -1
  9. package/mcp-server/dist/index.js.map +1 -1
  10. package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
  11. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  12. package/mcp-server/dist/services/budget-judgments.js +5 -3
  13. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  14. package/mcp-server/dist/services/identity.d.ts +2 -2
  15. package/mcp-server/dist/services/identity.js +2 -2
  16. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  17. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  18. package/mcp-server/dist/services/memory-sessions.js +28 -2
  19. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  20. package/mcp-server/dist/services/neural-interface.d.ts +2 -1
  21. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  22. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  23. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  24. package/mcp-server/dist/services/server-instructions.js +2 -1
  25. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  26. package/mcp-server/dist/services/tool-catalog.js +1 -1
  27. package/mcp-server/dist/services/tool-catalog.js.map +1 -1
  28. package/mcp-server/dist/services/tool-list.d.ts +1 -1
  29. package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
  30. package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
  31. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
  32. package/mcp-server/dist/services/tool-runtime.js +1 -1
  33. package/mcp-server/dist/services/tool-runtime.js.map +1 -1
  34. package/mcp-server/dist/services/typesafe-config.js +1 -1
  35. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  36. package/mcp-server/dist/tools/agents.d.ts +8 -8
  37. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  38. package/mcp-server/dist/tools/agents.js +10 -9
  39. package/mcp-server/dist/tools/agents.js.map +1 -1
  40. package/mcp-server/dist/tools/choice.d.ts +6 -0
  41. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/choice.js +18 -13
  43. package/mcp-server/dist/tools/choice.js.map +1 -1
  44. package/mcp-server/dist/tools/git-tools.js +2 -2
  45. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  46. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  47. package/mcp-server/dist/tools/morelogin.js +9 -6
  48. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  49. package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
  50. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
  51. package/mcp-server/dist/tools/skill-tools.js +11 -8
  52. package/mcp-server/dist/tools/skill-tools.js.map +1 -1
  53. package/mcp-server/package-lock.json +2 -2
  54. package/mcp-server/package.json +1 -1
  55. package/mcp-server/src/index.ts +1 -1
  56. package/mcp-server/src/services/budget-judgments.ts +5 -3
  57. package/mcp-server/src/services/identity.ts +2 -2
  58. package/mcp-server/src/services/memory-sessions.ts +24 -3
  59. package/mcp-server/src/services/neural-interface.ts +2 -1
  60. package/mcp-server/src/services/server-instructions.ts +2 -1
  61. package/mcp-server/src/services/tool-catalog.ts +1 -1
  62. package/mcp-server/src/services/tool-list.ts +1 -1
  63. package/mcp-server/src/services/tool-runtime.ts +2 -2
  64. package/mcp-server/src/services/typesafe-config.ts +1 -1
  65. package/mcp-server/src/tools/agents.ts +10 -9
  66. package/mcp-server/src/tools/choice.ts +17 -12
  67. package/mcp-server/src/tools/git-tools.ts +2 -2
  68. package/mcp-server/src/tools/morelogin.ts +6 -4
  69. package/mcp-server/src/tools/skill-tools.ts +11 -8
  70. package/neural-interface/i18n/en.json +501 -39
  71. package/neural-interface/i18n/pt-BR.json +501 -39
  72. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  73. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  74. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  75. package/neural-interface/lib/antigravity/bypass.js +100 -0
  76. package/neural-interface/lib/antigravity/catalog.js +273 -0
  77. package/neural-interface/lib/antigravity/home.js +260 -0
  78. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  79. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  80. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  81. package/neural-interface/lib/antigravity/policy.js +74 -0
  82. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  83. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  84. package/neural-interface/lib/antigravity/usage.js +43 -0
  85. package/neural-interface/lib/assistant-api.js +1 -0
  86. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  87. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  88. package/neural-interface/lib/assistant-budget-split.js +42 -15
  89. package/neural-interface/lib/assistant-catalog.js +76 -9
  90. package/neural-interface/lib/assistant-config.js +6 -3
  91. package/neural-interface/lib/assistant-dispatch.js +207 -49
  92. package/neural-interface/lib/assistant-envelope.js +153 -1
  93. package/neural-interface/lib/assistant-evidence.js +80 -0
  94. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  95. package/neural-interface/lib/assistant-persona.js +16 -11
  96. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  97. package/neural-interface/lib/assistant-router.js +237 -35
  98. package/neural-interface/lib/assistant-runtime.js +158 -12
  99. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  100. package/neural-interface/lib/assistant-tools.js +2 -2
  101. package/neural-interface/lib/assistant-usage.js +29 -0
  102. package/neural-interface/lib/backup-service.js +6 -1
  103. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  104. package/neural-interface/lib/browser-network-guard.js +274 -82
  105. package/neural-interface/lib/browser-tool-policy.js +12 -7
  106. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  107. package/neural-interface/lib/claude-history-cache.js +210 -0
  108. package/neural-interface/lib/claude-model-catalog.js +5 -1
  109. package/neural-interface/lib/cli-install-api.js +1 -1
  110. package/neural-interface/lib/cli-install.js +160 -31
  111. package/neural-interface/lib/cli-resolver.js +9 -2
  112. package/neural-interface/lib/cli-update-planner.js +25 -15
  113. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  114. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  115. package/neural-interface/lib/editor-git-api.js +3 -20
  116. package/neural-interface/lib/exec-loop-command.js +9 -2
  117. package/neural-interface/lib/file-explorer-api.js +74 -40
  118. package/neural-interface/lib/file-explorer-watch.js +9 -3
  119. package/neural-interface/lib/git-bash/install.js +52 -6
  120. package/neural-interface/lib/git-bash/manager.js +59 -17
  121. package/neural-interface/lib/git-bash/pins.js +12 -0
  122. package/neural-interface/lib/git-read.js +2 -2
  123. package/neural-interface/lib/git-status-runner.js +390 -0
  124. package/neural-interface/lib/git-status-watchdog.js +40 -0
  125. package/neural-interface/lib/greeting/budgets.js +1 -0
  126. package/neural-interface/lib/greeting/engine.js +2 -2
  127. package/neural-interface/lib/greeting/sources.js +10 -12
  128. package/neural-interface/lib/greeting/writer.js +1 -1
  129. package/neural-interface/lib/mcp-client-config.js +37 -6
  130. package/neural-interface/lib/mcp-external-servers.js +13 -4
  131. package/neural-interface/lib/mcp-installer.js +2 -1
  132. package/neural-interface/lib/memory-session-capture.js +17 -3
  133. package/neural-interface/lib/morelogin.js +147 -25
  134. package/neural-interface/lib/native-loop-providers.js +420 -21
  135. package/neural-interface/lib/native-loop-runtime.js +23 -2
  136. package/neural-interface/lib/opencode-native-boundary.js +11 -2
  137. package/neural-interface/lib/owner-authority.js +1 -1
  138. package/neural-interface/lib/path-confine.js +12 -9
  139. package/neural-interface/lib/peer-identity.js +32 -2
  140. package/neural-interface/lib/pty-host/core.js +28 -1
  141. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  142. package/neural-interface/lib/pty-host/manager.js +12 -1
  143. package/neural-interface/lib/remote-policy.js +1 -1
  144. package/neural-interface/lib/request-gate.js +3 -0
  145. package/neural-interface/lib/rulesets/api.js +6 -5
  146. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  147. package/neural-interface/lib/rulesets/installer.js +89 -10
  148. package/neural-interface/lib/rulesets/render.js +1 -1
  149. package/neural-interface/lib/session-cache-builder.js +64 -1
  150. package/neural-interface/lib/skills/catalog.js +5 -1
  151. package/neural-interface/lib/skills/creation-brief.js +1 -1
  152. package/neural-interface/lib/skills/creation-sources.js +19 -1
  153. package/neural-interface/lib/skills/deploy.js +9 -4
  154. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  155. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-trace.js +16 -0
  157. package/neural-interface/lib/skills/export.js +5 -2
  158. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  159. package/neural-interface/lib/skills/ownership.js +1 -1
  160. package/neural-interface/lib/skills/roots.js +10 -3
  161. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  162. package/neural-interface/lib/skills/service.js +15 -7
  163. package/neural-interface/lib/skills/telemetry.js +6 -0
  164. package/neural-interface/lib/synabun-version.js +1 -1
  165. package/neural-interface/lib/system-activity.js +1 -1
  166. package/neural-interface/lib/whatsapp/brain.js +7 -3
  167. package/neural-interface/lib/whatsapp/cards.js +22 -9
  168. package/neural-interface/lib/whatsapp/config.js +1 -1
  169. package/neural-interface/lib/worker-confinement.js +77 -1
  170. package/neural-interface/lib/worker-question.js +74 -0
  171. package/neural-interface/lib/worker-resume.js +13 -4
  172. package/neural-interface/lib/worker-sandbox.js +46 -16
  173. package/neural-interface/package-lock.json +18 -2
  174. package/neural-interface/package.json +2 -1
  175. package/neural-interface/public/onboarding.html +1 -1
  176. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  177. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  178. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  179. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  180. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  181. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  182. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  183. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  184. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  185. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  186. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  187. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  188. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  189. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  190. package/neural-interface/public/shared/api.js +4 -4
  191. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  192. package/neural-interface/public/shared/assistant/asst-api.js +1 -1
  193. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  194. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  195. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  196. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  197. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  198. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  199. package/neural-interface/public/shared/assistant/asst-panel.js +45 -8
  200. package/neural-interface/public/shared/assistant/asst-render.js +51 -12
  201. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  202. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  203. package/neural-interface/public/shared/assistant/asst-scenes.js +54 -1
  204. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  205. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  206. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  207. package/neural-interface/public/shared/assistant/asst-styles.js +34 -0
  208. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  209. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  210. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  211. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  212. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  213. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  214. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  215. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  216. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  217. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  218. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  219. package/neural-interface/public/shared/cli-install-view.js +7 -4
  220. package/neural-interface/public/shared/cli-status.js +9 -8
  221. package/neural-interface/public/shared/constants.js +3 -2
  222. package/neural-interface/public/shared/context-menu-model.js +1 -0
  223. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  224. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  225. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  226. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  227. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  228. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  229. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  230. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  231. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  232. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  233. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  234. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  235. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  236. package/neural-interface/public/shared/html-shell.js +21 -4
  237. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +27 -7
  238. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  239. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  240. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -0
  241. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  242. package/neural-interface/public/shared/provider-icons.js +4 -2
  243. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  244. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  245. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  246. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  247. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  248. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  249. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  250. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  251. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  252. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  253. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  254. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  255. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  256. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  257. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  258. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  259. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  260. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  261. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  262. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  263. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  264. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  265. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  266. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  267. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  268. package/neural-interface/public/shared/state.js +1 -1
  269. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  270. package/neural-interface/public/shared/styles.css +354 -34
  271. package/neural-interface/public/shared/text-cache.js +29 -0
  272. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  273. package/neural-interface/public/shared/tool-list-view.js +2 -2
  274. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  275. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  276. package/neural-interface/public/shared/ui-assistant.js +2 -0
  277. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  278. package/neural-interface/public/shared/ui-claude-panel.js +299 -77
  279. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  280. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  281. package/neural-interface/public/shared/ui-file-links.js +2 -2
  282. package/neural-interface/public/shared/ui-games.js +2 -2
  283. package/neural-interface/public/shared/ui-judgments.js +1 -1
  284. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  285. package/neural-interface/public/shared/ui-link.js +3 -3
  286. package/neural-interface/public/shared/ui-menubar.js +2 -2
  287. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  288. package/neural-interface/public/shared/ui-navbar.js +45 -0
  289. package/neural-interface/public/shared/ui-notifications.js +1 -0
  290. package/neural-interface/public/shared/ui-resume.js +9 -7
  291. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  292. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  293. package/neural-interface/public/shared/ui-sessions.js +22 -12
  294. package/neural-interface/public/shared/ui-settings.js +90 -65
  295. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  296. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  297. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  298. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  299. package/neural-interface/public/shared/ui-terminal.js +146 -10
  300. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  301. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  302. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  303. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  304. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  305. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  306. package/neural-interface/public/shared/worker-questions.js +34 -0
  307. package/neural-interface/public/sidepanel-session.html +1 -1
  308. package/neural-interface/server.js +644 -426
  309. package/neural-interface/templates/rulesets/README.md +1 -1
  310. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  311. package/neural-interface/templates/rulesets/manifest.json +1 -1
  312. package/npm-shrinkwrap.json +18 -2
  313. package/package.json +2 -1
  314. package/skills/skill-forge/SKILL.md +4 -4
  315. package/skills/synabun/SKILL.md +1 -1
  316. package/skills/synabun/modules/schedule.md +3 -3
  317. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -5,7 +5,7 @@ import { readStoredToolListRecord } from './tool-lists.js';
5
5
  // ═══════════════════════════════════════════
6
6
  //
7
7
  // A "dispatch" is one worker run launched by the central assistant: a single
8
- // provider conversation (Claude / Codex / OpenCode) that executes one task,
8
+ // provider conversation (Claude / Codex / OpenCode / Antigravity) that executes one task,
9
9
  // then stays warm for follow-up turns until the assistant completes it or the
10
10
  // idle timeout fires. It rides on NativeLoopRuntime (ledger, claims, sidepanel
11
11
  // attach) through the per-launch buildPrompt/wrapAdapter overrides, so the
@@ -43,16 +43,19 @@ import { loadPlaybook } from './assistant-playbooks.js';
43
43
  import { loadStyleGuideForRun } from './style-guide/store.js';
44
44
  import { claimCheckWaived, normalizeTestDensity, testingApplies } from './assistant-test-density.js';
45
45
  import { normalizeEffort } from './effort-levels.js';
46
- import { createTurnEvidence, recordProviderEvent, evidenceSnapshot, claimEvidenceLine } from './assistant-evidence.js';
46
+ import { acpCommandOf, createTurnEvidence, recordProviderEvent, evidenceSnapshot, claimEvidenceLine } from './assistant-evidence.js';
47
47
  import { BUDGET_MAX_USD, codexUsageCostUsd, floorCents } from './assistant-budget.js';
48
- import { MIN_SPEND_USD, PROVIDER_LABELS, isFreePrice } from './assistant-budget-split.js';
48
+ import { MIN_SPEND_USD, PROVIDER_LABELS, UNPRICED_PROVIDER_REASON, isFreePrice, isUnpricedProvider } from './assistant-budget-split.js';
49
49
  import { createBudgetPlanner } from './assistant-budget-planner.js';
50
- import { CLAUDE_ESTIMATE_SOURCE, CODEX_USAGE_KEYS, TOKEN_KEYS, addTokens, codexUsageTokens, createClaudeMeter, createCodexMeter, createOpenCodeMeter, totalTokens, withTotal, zeroTokens } from './assistant-usage.js';
50
+ import { CLAUDE_ESTIMATE_SOURCE, CODEX_USAGE_KEYS, TOKEN_KEYS, addTokens, antigravityUsageTokens, codexUsageTokens, createClaudeMeter, createCodexMeter, createOpenCodeMeter, totalTokens, withTotal, zeroTokens } from './assistant-usage.js';
51
51
  import { claudeTokensCostUsd } from './assistant-pricing.js';
52
52
  import { RemotePolicyError, clampDispatchSpec, getSessionPolicy, readRegisteredProjects } from './remote-policy.js';
53
+ import { validateWorkerReply } from './worker-question.js';
53
54
  import { limitFromText } from '../public/shared/assistant/asst-limit.js';
54
55
 
55
- export const NATIVE_PROVIDERS = new Set(['claude-code', 'codex', 'opencode']);
56
+ // Antigravity (Google's `agy`, over ACP: lib/native-loop-providers.js createAntigravityNativeLoopAdapter) is a worker
57
+ // provider only: never a brain, no accounts, its usage reported in tokens and never priced.
58
+ export const NATIVE_PROVIDERS = new Set(['claude-code', 'codex', 'opencode', 'antigravity']);
56
59
  // awaiting_route: an un-routed dispatch held until the user picks a model on
57
60
  // the route card (ask modes). It is active but consumes no rails.
58
61
  // suspended: a native worker the server suspended as it went down (its CLI
@@ -85,7 +88,7 @@ export function runStatusFromState(state) {
85
88
  export const MIN_MAX_MINUTES = 20;
86
89
 
87
90
  export const DEFAULT_LIMITS = Object.freeze({
88
- perProvider: Object.freeze({ 'claude-code': 3, codex: 4, opencode: 3 }),
91
+ perProvider: Object.freeze({ 'claude-code': 3, codex: 4, opencode: 3, antigravity: 3 }),
89
92
  perCodexAccount: 2,
90
93
  perSession: 6,
91
94
  queuedPerSession: 20,
@@ -298,9 +301,55 @@ export function summarizeProviderEvent(payload = {}) {
298
301
  if (/question\.asked/i.test(eventType)) return `question asked: ${clip((event.questions || []).map((q) => q.question).join(' | '), 200)}`;
299
302
  return null;
300
303
  }
304
+ if (payload.provider === 'antigravity') {
305
+ // eventType is the ACP session/update kind; event the update as the agent sent it.
306
+ if (eventType === 'agent_message_chunk') {
307
+ const text = event.content?.type === 'text' ? event.content.text : '';
308
+ return text && String(text).trim() ? `assistant: ${clip(text, 200)}` : null;
309
+ }
310
+ if (eventType === 'tool_call') return `${acpToolNoun(event)} started: ${acpToolDetail(event)}`;
311
+ if (eventType === 'tool_call_update') {
312
+ // Only the end of a call: its start was the tool_call line, its progress says nothing new.
313
+ if (event.status !== 'completed' && event.status !== 'failed') return null;
314
+ return `${acpToolNoun(event)} ${event.status === 'failed' ? 'failed' : 'done'}: ${acpToolDetail(event)}`;
315
+ }
316
+ if (eventType === 'plan') {
317
+ const entries = Array.isArray(event.entries) ? event.entries : [];
318
+ if (!entries.length) return null;
319
+ const done = entries.filter((step) => step?.status === 'completed').length;
320
+ const current = entries.find((step) => step?.status === 'in_progress');
321
+ return `plan: ${done}/${entries.length} done${current?.content ? `, now ${clip(current.content, 120)}` : ''}`;
322
+ }
323
+ if (eventType === 'synabun.provider_exit') {
324
+ const how = event.signal ? ` (${event.signal})` : event.code !== null && event.code !== undefined ? ` (code ${event.code})` : '';
325
+ return `error: Antigravity exited${how}${event.error ? `: ${clip(event.error, 160)}` : ''}`;
326
+ }
327
+ return null;
328
+ }
301
329
  return null;
302
330
  }
303
331
 
332
+ /** What an ACP tool call is, for a transcript line: command, file_change or tool (Codex's words). */
333
+ function acpToolNoun(call = {}) {
334
+ const kind = String(call.kind || '');
335
+ // agy names an MCP call call_mcp_tool, kind `execute`: it is a tool, never a command.
336
+ if (call.name === 'call_mcp_tool' || (call.rawInput && typeof call.rawInput === 'object' && call.rawInput.ServerName && call.rawInput.ToolName)) return 'tool';
337
+ if (kind === 'execute') return 'command';
338
+ if (kind === 'edit' || kind === 'delete' || kind === 'move') return 'file_change';
339
+ return 'tool';
340
+ }
341
+ /** The command, the paths or the title of an ACP tool call; its id when an update carries none of them. */
342
+ function acpToolDetail(call = {}) {
343
+ const input = call.rawInput && typeof call.rawInput === 'object' ? call.rawInput : {};
344
+ if (input.ServerName && input.ToolName) return clip(`${input.ServerName}_${input.ToolName}`, 140);
345
+ const command = call.kind === 'execute' ? acpCommandOf(call) : '';
346
+ if (command) return clip(command, 140);
347
+ const paths = (Array.isArray(call.locations) ? call.locations : []).map((location) => location?.path).filter((path) => typeof path === 'string' && path);
348
+ if (paths.length && call.kind !== 'execute' && call.kind !== 'fetch') return clip(paths.join(', '), 160);
349
+ if (call.title) return clip(call.title, 140);
350
+ return call.toolCallId ? `call ${String(call.toolCallId).slice(0, 12)}` : (call.kind || 'tool');
351
+ }
352
+
304
353
  // The old five-cap keys a config file may still hold: never limits (the budget planner owns the money).
305
354
  const LEGACY_BUDGET_LIMITS = new Set(['defaultBudgetUsd', 'maxBudgetUsd', 'sessionWarnUsd', 'sessionSoftBudgetUsd', 'sessionHardBudgetUsd', 'brainBudgetUsd', 'budgetVersion', 'budgetUpdatedAt']);
306
355
  function mergeLimits(overrides = {}) {
@@ -479,8 +528,10 @@ export function createAssistantDispatcher({
479
528
  let closing = false;
480
529
 
481
530
  // ── persistence ────────────────────────────────────────────────────────────
482
- /** Why the original provider account of a run cannot be used now ('' when it can). */
531
+ /** Why the original provider account of a run cannot be used now ('' when it can, or the provider has no accounts). */
483
532
  function providerAccountUnusable(accountId, provider = 'claude-code', entry = null) {
533
+ // OpenCode and Antigravity sign in inside their own CLI: SynaBun keeps no account of theirs.
534
+ if (provider !== 'codex' && provider !== 'claude-code') return '';
484
535
  if (provider === 'codex') {
485
536
  const home = codexAccounts?.homeFor?.(accountId);
486
537
  if (home && entry?.codexHome && resolve(home) !== resolve(entry.codexHome)) return 'its CODEX_HOME changed';
@@ -658,9 +709,11 @@ export function createAssistantDispatcher({
658
709
  status: runtimeRun.status, turnState: runtimeRun.turnState, stoppedReason: runtimeRun.stoppedReason,
659
710
  error: runtimeRun.error, claimedBy: runtimeRun.claimedBy, providerSessionId: runtimeRun.providerSessionId,
660
711
  providerThreadId: runtimeRun.providerThreadId,
712
+ ...((runtimeRun.antigravityHome || entry.resumeInfo?.antigravityHome || entry.antigravityHome) ? { antigravityHome: runtimeRun.antigravityHome || entry.resumeInfo?.antigravityHome || entry.antigravityHome } : {}),
661
713
  } : (entry.runtime || null),
662
714
  providerSessionId: runtimeRun?.providerSessionId || entry.providerSessionId || null,
663
715
  providerThreadId: runtimeRun?.providerThreadId || entry.providerThreadId || null,
716
+ ...((runtimeRun?.antigravityHome || entry.resumeInfo?.antigravityHome || entry.antigravityHome) ? { antigravityHome: runtimeRun?.antigravityHome || entry.resumeInfo?.antigravityHome || entry.antigravityHome } : {}),
664
717
  };
665
718
  }
666
719
  function safeRuntime() { try { return getRuntime(); } catch { return null; } }
@@ -771,7 +824,7 @@ export function createAssistantDispatcher({
771
824
  }
772
825
 
773
826
  /** `taskClass`: the run's class (its route's, else the spec's); image / video creation needs a model that makes the medium. */
774
- function normalizeSpec(spec = {}, { catalogValue = null, codexAskFallback = false, taskClass = null } = {}) {
827
+ function normalizeSpec(spec = {}, { catalogValue = null, taskClass = null } = {}) {
775
828
  const provider = String(spec.provider || '').trim().toLowerCase();
776
829
  if (!NATIVE_PROVIDERS.has(provider)) throw new DispatchError('INVALID_PROVIDER', `provider must be one of ${[...NATIVE_PROVIDERS].join(', ')}`);
777
830
  const task = String(spec.task || '').trim();
@@ -782,12 +835,6 @@ export function createAssistantDispatcher({
782
835
  let permissionPolicy = normalizePermissionPolicy(spec.permissionPolicy);
783
836
  let capability = normalizeCapability(spec.capability || 'full');
784
837
  const notes = [];
785
- if (provider === 'codex' && permissionPolicy === 'ask') {
786
- // A route card may move an 'ask' dispatch onto Codex: degrade instead of failing.
787
- if (!codexAskFallback) throw new DispatchError('PERMISSION_POLICY_UNSUPPORTED', 'Codex dispatches cannot use permissionPolicy "ask" (the Codex SDK has no approval channel). Use "restricted" for a workspace-write sandbox or "auto".', { suggested: 'restricted' });
788
- permissionPolicy = 'restricted';
789
- notes.push('permission policy "ask" became "restricted" (Codex has no approval channel)');
790
- }
791
838
  // "auto" (or nothing) = let routing pick / use the provider default.
792
839
  let model = spec.model && String(spec.model).trim() && String(spec.model).trim().toLowerCase() !== 'auto' ? String(spec.model).trim() : null;
793
840
  let modelInfo = null;
@@ -1070,10 +1117,11 @@ export function createAssistantDispatcher({
1070
1117
  /**
1071
1118
  * Why a run's spend could not be held to the budget (no price for its model), else null. Enforced
1072
1119
  * only with a budget and a price source (`pricing`): Claude reports cost, Codex needs a list price,
1073
- * OpenCode a catalog price (a free model is fine).
1120
+ * OpenCode a catalog price (a free model is fine), Antigravity never has one (tokens, not prices).
1074
1121
  */
1075
1122
  function unpricedReason(entry) {
1076
1123
  if (!pricing || entry.provider === 'claude-code' || !limitedNow()) return null;
1124
+ if (isUnpricedProvider(entry.provider)) return UNPRICED_PROVIDER_REASON;
1077
1125
  const model = entry.pricingModel || entry.model || null;
1078
1126
  if (entry.provider === 'codex') {
1079
1127
  let price = null;
@@ -1085,7 +1133,10 @@ export function createAssistantDispatcher({
1085
1133
  function refuseUnpriced(entry) {
1086
1134
  const why = unpricedReason(entry);
1087
1135
  if (!why) return;
1088
- throw new DispatchError('BUDGET_UNPRICED', `Not dispatched: ${why}, so the session budget could not hold its spend. Pick a priced model or a free one (agent_catalog shows prices).`, { status: 409, provider: entry.provider, model: entry.pricingModel || entry.model || null });
1136
+ const next = isUnpricedProvider(entry.provider)
1137
+ ? `Dispatch it to another provider (agent_catalog shows prices), or ask the user to switch the session budget to No limit (Assistant → Budget) to run it on ${providerName(entry.provider)}.`
1138
+ : 'Pick a priced model or a free one (agent_catalog shows prices).';
1139
+ throw new DispatchError('BUDGET_UNPRICED', `Not dispatched: ${why}, so the session budget could not hold its spend. ${next}`, { status: 409, provider: entry.provider, model: entry.pricingModel || entry.model || null });
1089
1140
  }
1090
1141
  const providerName = (provider) => PROVIDER_LABELS[provider] || provider;
1091
1142
  /** The session budget's words for a provider whose share is used up ("codex share used up: $4.00 of $4.00 spent"). */
@@ -1522,7 +1573,7 @@ export function createAssistantDispatcher({
1522
1573
  emit(entry, 'route_pending', { routeId: routed.routeId });
1523
1574
  return {
1524
1575
  ok: true, queued: true, awaitingRoute: true, routeId: routed.routeId, run: view(entry),
1525
- next: 'This run is waiting for the user to pick its model on the route card. Do not dispatch it again. It starts automatically after the pick; the decision arrives as a [SynaBun Mailbox] route_decided event.',
1576
+ next: 'This run is waiting for the user to pick its model on the route card. Do not dispatch it again. It starts automatically after the pick; the decision arrives as a [SynaBun Mailbox] route_decided event. If the user picks "here" instead, this run never starts and that event asks you to do the task yourself.',
1526
1577
  };
1527
1578
  }
1528
1579
 
@@ -1555,7 +1606,7 @@ export function createAssistantDispatcher({
1555
1606
  let spec;
1556
1607
  // The picked provider may need another clamp (Codex under strict worker approvals), or the level fell meanwhile.
1557
1608
  const remoteNotes = [];
1558
- try { input = clampRemote(input, entry.assistantSessionId, remoteNotes); spec = normalizeSpec(input, { catalogValue, codexAskFallback: true, taskClass: entry.taskClass || null }); }
1609
+ try { input = clampRemote(input, entry.assistantSessionId, remoteNotes); spec = normalizeSpec(input, { catalogValue, taskClass: entry.taskClass || null }); }
1559
1610
  catch (error) {
1560
1611
  entry.state = 'failed'; entry.completionReason = 'route_invalid'; entry.finishedAt = iso(now()); entry.turnState = 'terminal';
1561
1612
  entry.error = error?.message || String(error);
@@ -1581,7 +1632,10 @@ export function createAssistantDispatcher({
1581
1632
  const entry = entries.get(runId);
1582
1633
  if (!entry || entry.state !== 'awaiting_route') return false;
1583
1634
  entry.state = 'stopped'; entry.completionReason = reason; entry.finishedAt = iso(now()); entry.turnState = 'terminal'; entry.pending = [];
1584
- entry.route = { ...(entry.route || {}), status: reason === 'route_expired' ? 'expired' : 'declined', decidedBy: reason === 'route_expired' ? 'timeout' : 'user', decidedAt: iso(now()) };
1635
+ // route_here: the user picked "here" on its card, so the assistant does the task itself and this worker never starts.
1636
+ const status = reason === 'route_expired' ? 'expired' : reason === 'route_here' ? 'here' : 'declined';
1637
+ entry.route = { ...(entry.route || {}), status, decidedBy: reason === 'route_expired' ? 'timeout' : 'user', decidedAt: iso(now()) };
1638
+ if (reason === 'route_here') entry.notes.push('the user chose to do this task here, in the conversation: this worker was not started');
1585
1639
  delete entry.rawSpec;
1586
1640
  emit(entry, 'stopped');
1587
1641
  return true;
@@ -1605,7 +1659,7 @@ export function createAssistantDispatcher({
1605
1659
  accountId: to.accountId || (to.provider === entry.provider ? entry.accountId : null),
1606
1660
  title: clip(`${entry.title} ${retry ? '↻' : '↑'}`, 80), tags: entry.tags, workflowId: entry.workflowId, parentRunId: runId,
1607
1661
  tools: entry.tools?.some(item => item.startsWith('mcp:')) ? retainExternalRefs(entry.toolsRequested ?? dispatchTools({}).items, entry.tools) : entry.toolsRequested ?? undefined, capability: entry.capability,
1608
- permissionPolicy: to.provider === 'codex' && entry.permissionPolicy === 'ask' ? 'restricted' : entry.permissionPolicy,
1662
+ permissionPolicy: entry.permissionPolicy,
1609
1663
  usesBrowser: entry.usesBrowser, usesComputer: entry.usesComputer, maxMinutes: entry.maxMinutes, budgetUsd: entry.budgetUsd,
1610
1664
  outputSchema: entry.outputSchema, taskClass: entry.taskClass || null,
1611
1665
  }, { assistantSessionId: entry.assistantSessionId, origin: 'ui', brief: entry.brief || null });
@@ -1808,7 +1862,7 @@ export function createAssistantDispatcher({
1808
1862
  } else entry.scratchDir = null;
1809
1863
  const desktopGrant = mintDesktopGrant(entry);
1810
1864
  const state = runState(entry, { browserSessionId, browserTabId, desktopGrant, startedAt: iso(now()) });
1811
- if (entry.permissionPolicy === 'ask') state.permissionBroker = createBroker(entry.runId);
1865
+ state.permissionBroker = createBroker(entry.runId);
1812
1866
  // A class's rules (design), read now so an edited override applies without a restart;
1813
1867
  // they go to the prompt only, not into the loop state file.
1814
1868
  const playbook = playbookFor(entry);
@@ -1892,8 +1946,13 @@ export function createAssistantDispatcher({
1892
1946
  adminSandbox: entry.resumeInfo?.adminSandbox === true,
1893
1947
  onCodexSandbox: (value) => { resumeInfoOf(entry).adminSandbox = value === true; persist({ force: true }); },
1894
1948
  onSandboxPlan: (plan) => { resumeInfoOf(entry).sandboxPlan = plan; persist({ force: true }); },
1949
+ // Antigravity counts tokens per session (ACP usage): what its session had counted, kept for a resume after a
1950
+ // restart, so the resumed run's first turn books only its own.
1951
+ sessionUsageBase: entry.provider === 'antigravity' ? entry.resumeInfo?.acpSessionUsage || null : null,
1952
+ onSessionUsage: (value) => { if (entry.provider === 'antigravity' && value && typeof value === 'object') resumeInfoOf(entry).acpSessionUsage = value; },
1895
1953
  guardMode: entry.guardMode, scratchDir: entry.scratchDir || null, serverPort: Number(serverPort) > 0 ? Number(serverPort) : null,
1896
1954
  localPortRange: localPortRange && Number(localPortRange.from) > 0 && Number(localPortRange.to) >= Number(localPortRange.from) ? { from: Number(localPortRange.from), to: Number(localPortRange.to) } : null,
1955
+ questionTimeoutMs: limits.questionTimeoutMs,
1897
1956
  idleTimeoutMs: entry.idleTimeoutMs, maxTurns: entry.maxTurns, outputSchema: entry.outputSchema, project: entry.project,
1898
1957
  title: entry.title, tags: entry.tags, workflowId: entry.workflowId, parentRunId: entry.parentRunId,
1899
1958
  // The user's brief after a clarification (assistant-task-prompt.js briefBlock).
@@ -1914,6 +1973,8 @@ export function createAssistantDispatcher({
1914
1973
  if (!Number.isInteger(pid) || pid <= 0) return;
1915
1974
  resumeInfoOf(entry).pid = pid;
1916
1975
  entry.resumeInfo.pidStartedAt = info.startedAt || iso(now());
1976
+ // An Antigravity run's own home (lib/antigravity/home.js): where its conversation is, for a resume.
1977
+ if (typeof info.home === 'string' && info.home) { entry.resumeInfo.antigravityHome = info.home; entry.antigravityHome = info.home; }
1917
1978
  persist({ force: true });
1918
1979
  },
1919
1980
  ...extra,
@@ -1990,36 +2051,38 @@ export function createAssistantDispatcher({
1990
2051
  return account?.home || CODEX_DEFAULT_HOME || null;
1991
2052
  }
1992
2053
 
1993
- // ── permission broker ('ask' policy) ──────────────────────────────────────
2054
+ // ── request broker (questions in every policy; tool approvals under 'ask') ──────────────────────────────────────
1994
2055
  function createBroker(runId) {
1995
2056
  return {
1996
2057
  request: (request = {}) => new Promise((resolvePermission) => {
1997
2058
  const entry = entries.get(runId);
1998
2059
  const liveState = live.get(runId);
1999
- if (!entry || !liveState) { resolvePermission({ behavior: 'deny', message: 'Run is no longer tracked.' }); return; }
2060
+ if (!entry || !liveState || liveState.dead || liveState.suspending || TERMINAL_STATES.has(entry.state) || request.signal?.aborted) { resolvePermission({ behavior: 'deny', message: 'Run is no longer tracked.' }); return; }
2000
2061
  const requestId = `perm-${randomBytes(6).toString('hex')}`;
2001
2062
  const kind = request.kind === 'ask' ? 'question' : 'permission';
2002
2063
  const timeoutMs = kind === 'question' ? limits.questionTimeoutMs : limits.permissionTimeoutMs;
2003
2064
  const pending = {
2004
2065
  requestId, kind, toolName: request.toolName || null, input: request.input ?? null,
2066
+ protocol: request.protocol || null,
2005
2067
  questions: Array.isArray(request.questions) ? request.questions : [], createdAt: iso(now()), expiresAt: iso(now() + timeoutMs),
2006
2068
  };
2007
2069
  entry.pending = [...(entry.pending || []), pending];
2008
2070
  entry.state = 'awaiting_permission';
2009
2071
  entry.turnState = 'awaiting_permission';
2072
+ // Install the resolver before publishing: a synchronous listener can answer at once.
2073
+ const timer = setTimeout(() => finishPending(runId, requestId, { behavior: 'deny', message: `No decision within ${Math.round(timeoutMs / 60000)} minutes.`, resolvedBy: 'timeout' }), timeoutMs);
2074
+ timer.unref?.();
2075
+ const abortSignal = request.signal;
2076
+ const onAbort = () => finishPending(runId, requestId, { behavior: 'deny', message: 'Run aborted.', resolvedBy: 'abort' });
2077
+ liveState.pendingResolvers.set(requestId, { resolve: resolvePermission, timer, cleanup: () => abortSignal?.removeEventListener?.('abort', onAbort) });
2078
+ abortSignal?.addEventListener?.('abort', onAbort, { once: true });
2010
2079
  // Waiting on the answer is not work: the request's own timeout bounds it.
2011
2080
  pauseWork(entry, liveState);
2081
+ liveState.ctx?.setPendingRequests?.(entry.pending);
2012
2082
  liveState.ctx?.setTurnState?.('awaiting_permission');
2013
2083
  liveState.ctx?.emit?.({ type: 'synabun.permission_request', requestId, kind, toolName: pending.toolName, input: pending.input, questions: pending.questions });
2014
- const timer = setTimeout(() => finishPending(runId, requestId, { behavior: 'deny', message: `No decision within ${Math.round(timeoutMs / 60000)} minutes.`, resolvedBy: 'timeout' }), timeoutMs);
2015
- timer.unref?.();
2016
- liveState.pendingResolvers.set(requestId, { resolve: resolvePermission, timer });
2017
2084
  try { broadcastSync({ type: 'assistant:permission-request', runId, assistantSessionId: entry.assistantSessionId, provider: entry.provider, request_id: requestId, request: pending }); } catch {}
2018
2085
  emit(entry, kind === 'question' ? 'needs_input' : 'permission_request', { request: pending });
2019
- const abortSignal = request.signal;
2020
- if (abortSignal && typeof abortSignal.addEventListener === 'function') {
2021
- abortSignal.addEventListener('abort', () => finishPending(runId, requestId, { behavior: 'deny', message: 'Run aborted.', resolvedBy: 'abort' }), { once: true });
2022
- }
2023
2086
  }),
2024
2087
  };
2025
2088
  }
@@ -2030,10 +2093,12 @@ export function createAssistantDispatcher({
2030
2093
  const resolver = liveState.pendingResolvers.get(requestId);
2031
2094
  if (!resolver) return false;
2032
2095
  clearTimeout(resolver.timer);
2096
+ resolver.cleanup?.();
2033
2097
  liveState.pendingResolvers.delete(requestId);
2034
2098
  const request = (entry.pending || []).find((item) => item.requestId === requestId) || null;
2035
2099
  entry.pending = (entry.pending || []).filter((item) => item.requestId !== requestId);
2036
- if (entry.state === 'awaiting_permission' && entry.pending.length === 0) {
2100
+ liveState.ctx?.setPendingRequests?.(entry.pending);
2101
+ if (entry.state === 'awaiting_permission' && entry.pending.length === 0 && !liveState.dead && !liveState.suspending && !liveState.abortReason) {
2037
2102
  entry.state = 'running';
2038
2103
  entry.turnState = 'running';
2039
2104
  startWork(entry, liveState);
@@ -2056,9 +2121,14 @@ export function createAssistantDispatcher({
2056
2121
  throw new DispatchError(known ? 'PERMISSION_ALREADY_RESOLVED' : 'NO_PENDING_REQUEST', known ? `Request ${requestId} was already resolved` : `No pending request ${requestId} on run ${runId}`, { status: 409 });
2057
2122
  }
2058
2123
  const behavior = response.behavior === 'allow' || response.decision === 'allow' || response.decision === 'answer' ? 'allow' : 'deny';
2124
+ // Antigravity saves an "always" answer for every later session of the user's, outside the run: a worker's
2125
+ // answer is for this request only.
2126
+ const always = entry.provider === 'antigravity' ? false : !!response.always;
2127
+ try { validateWorkerReply((entry.pending || []).find((p) => p.requestId === requestId), { ...response, behavior }); }
2128
+ catch (error) { throw new DispatchError('INVALID_QUESTION_ANSWER', error.message); }
2059
2129
  finishPending(runId, requestId, {
2060
- behavior, updatedInput: response.updatedInput, message: response.message || response.note, always: !!response.always,
2061
- answers: response.answers, resolvedBy: origin,
2130
+ behavior, updatedInput: response.updatedInput, message: response.message || response.note, always,
2131
+ answers: response.answers ?? response.updatedInput?.answers, result: response.result, content: response.content, action: response.action, error: response.error, resolvedBy: origin,
2062
2132
  }, { origin });
2063
2133
  return view(entry);
2064
2134
  }
@@ -2105,6 +2175,7 @@ export function createAssistantDispatcher({
2105
2175
  liveState.turnMeteredUsd = 0;
2106
2176
  let raw = await inner.runTurn(prompt, { ...meta, turn: turnNumber, outputSchema: entry.outputSchema || undefined });
2107
2177
  if (entry.provider === 'codex') settleCodexTurn(entry, liveState, raw);
2178
+ else if (entry.provider === 'antigravity') settleAntigravityTurn(entry, liveState, raw);
2108
2179
  let text = typeof raw?.text === 'string' ? raw.text : '';
2109
2180
  const evalRun = isEvalRun(entry);
2110
2181
  let parsed = evalRun ? { found: false } : parseResultContract(text);
@@ -2142,6 +2213,7 @@ export function createAssistantDispatcher({
2142
2213
  if (retryParsed.found) { parsed = { ...retryParsed, source: 'retry' }; text = `${text}\n\n${retryText}`; }
2143
2214
  retryCost = accumulateCost(entry, retry);
2144
2215
  if (entry.provider === 'codex') settleCodexTurn(entry, liveState, retry);
2216
+ else if (entry.provider === 'antigravity') settleAntigravityTurn(entry, liveState, retry);
2145
2217
  } catch (error) {
2146
2218
  // Stopped during the retry: the run ends here (handleTurnError books the retry), with no
2147
2219
  // turn_completed or follow-up after it. The first provider turn was paid for: book it first.
@@ -2149,6 +2221,7 @@ export function createAssistantDispatcher({
2149
2221
  if (liveState.abortReason || liveState.suspending) { accumulateCost(entry, raw); throw error; }
2150
2222
  retryCost = accumulateCost(entry, error);
2151
2223
  if (entry.provider === 'codex') settleCodexTurn(entry, liveState, error);
2224
+ else if (entry.provider === 'antigravity') settleAntigravityTurn(entry, liveState, error);
2152
2225
  entry.notes.push(`result retry failed: ${error?.message || error}`);
2153
2226
  }
2154
2227
  }
@@ -2174,7 +2247,8 @@ export function createAssistantDispatcher({
2174
2247
  liveState.turnOpen = false;
2175
2248
  stopUsagePolling(liveState);
2176
2249
  let turnUsage = raw?.usage || null;
2177
- if (entry.provider === 'codex') turnUsage = liveState.turnUsage || null;
2250
+ // Codex and Antigravity: what their provider turns (the retry included) reported, in provider keys.
2251
+ if (entry.provider === 'codex' || entry.provider === 'antigravity') turnUsage = liveState.turnUsage || null;
2178
2252
  else if (entry.provider === 'opencode') turnUsage = (await settleOpenCodeTurn(entry, liveState)) || turnUsage;
2179
2253
  // A reported cost below an earlier turn's estimate can make this negative: a turn never reads below $0.
2180
2254
  const turnCost = Math.max(0, accumulateCost(entry, raw) + retryCost + (Number(liveState.turnMeteredUsd) || 0));
@@ -2383,6 +2457,8 @@ export function createAssistantDispatcher({
2383
2457
  if (entry.provider === 'codex') settleCodexTurn(entry, liveState, error);
2384
2458
  // OpenCode: what streamed before the failure, as it stands (its serve may be gone: no reconcile).
2385
2459
  else if (entry.provider === 'opencode') bookOpenCodeTurn(entry, liveState, { reconciled: false });
2460
+ // Antigravity: a stopped turn's answer still carried its tokens (the adapter puts them on the abort).
2461
+ else if (entry.provider === 'antigravity') settleAntigravityTurn(entry, liveState, error);
2386
2462
  announceUnpriced(entry);
2387
2463
  checkSession(entry.assistantSessionId, { source: entry });
2388
2464
  if (budgetStopReason(entry.completionReason)) return { providerSessionId: null };
@@ -2465,7 +2541,8 @@ export function createAssistantDispatcher({
2465
2541
  if (!raw) return 0;
2466
2542
  const cost = Number(raw.costUsd);
2467
2543
  const added = bookCost(entry, cost, raw.basis || 'reported');
2468
- if (entry.provider !== 'codex') addLegacyUsage(entry, raw.usage);
2544
+ // Codex and Antigravity usage reaches entry.usage when it is settled (settleCodexTurn, settleAntigravityTurn).
2545
+ if (entry.provider !== 'codex' && entry.provider !== 'antigravity') addLegacyUsage(entry, raw.usage);
2469
2546
  return added;
2470
2547
  }
2471
2548
 
@@ -2752,6 +2829,38 @@ export function createAssistantDispatcher({
2752
2829
  persist({ force: true });
2753
2830
  }
2754
2831
 
2832
+ // Antigravity: each provider turn's answer carries that turn's own ACP usage (the adapter takes it off the
2833
+ // session's running totals; a failed or stopped turn has none, so the next answer carries it). Tokens only:
2834
+ // Antigravity is billed by its Google sign-in and reports no price, so every row is unpriced, never $0.
2835
+ /**
2836
+ * Book one Antigravity provider turn: its five token classes into the ledger (exact when the agent's own total
2837
+ * agrees with one reading of its classes, else partial: 'antigravity-total-mismatch'), its usage in provider keys
2838
+ * into the run's and the turn's legacy totals, and the run marked unpriced. Nothing without usage.
2839
+ */
2840
+ function settleAntigravityTurn(entry, liveState, raw) {
2841
+ if (entry.usageState?.closed) return;
2842
+ const usage = raw?.usage && typeof raw.usage === 'object' ? raw.usage : null;
2843
+ if (!usage) return;
2844
+ const { tokens, mismatch } = antigravityUsageTokens(usage);
2845
+ if (totalTokens(tokens) === 0) return;
2846
+ const used = { input_tokens: tokens.input, output_tokens: tokens.output, reasoning_tokens: tokens.reasoning, cache_read_input_tokens: tokens.cacheRead, cache_write_input_tokens: tokens.cacheWrite };
2847
+ addLegacyUsage(entry, used);
2848
+ if (liveState) {
2849
+ const prev = liveState.turnUsage || {};
2850
+ liveState.turnUsage = Object.fromEntries(Object.entries(used).map(([key, value]) => [key, (Number(prev[key]) || 0) + value]));
2851
+ }
2852
+ markUnpriced(entry, UNPRICED_PROVIDER_REASON);
2853
+ // No model named: the one the session runs (the adapter reads it off the session's config options).
2854
+ let model = entry.model || null;
2855
+ if (!model) { try { model = liveState?.inner?.describe?.()?.model || null; } catch { model = null; } }
2856
+ settleUsage(entry, {
2857
+ model, part: 'main', tokens, costUsd: null, costBasis: 'unpriced',
2858
+ fidelity: mismatch ? 'partial' : 'exact', reason: mismatch ? 'antigravity-total-mismatch' : null, source: 'antigravity-acp-usage',
2859
+ });
2860
+ // At once, not debounced: a kill must not leave the saved run behind the ledger.
2861
+ persist({ force: true });
2862
+ }
2863
+
2755
2864
  // OpenCode: every session of the run's own serve (the root and the `task` tool's child sessions).
2756
2865
  function openCodeMeterOf(entry) {
2757
2866
  const held = metersOf(entry);
@@ -3054,6 +3163,7 @@ export function createAssistantDispatcher({
3054
3163
  entry.suspendReason = String(reason || 'server_shutdown');
3055
3164
  delete entry.crashed;
3056
3165
  // An open permission request goes with the process: the resumed worker asks again.
3166
+ for (const requestId of [...liveState.pendingResolvers.keys()]) finishPending(entry.runId, requestId, { behavior: 'deny', message: 'Server restarting; ask again after resume.', resolvedBy: 'server_restart' });
3057
3167
  entry.pending = [];
3058
3168
  // What the cut-off turn spent so far is booked now (an estimate per call the stream showed: no result will
3059
3169
  // count them); the meter takes the resumed CLI's counters as its new baseline (rebaseClaudeUsage).
@@ -3245,14 +3355,14 @@ export function createAssistantDispatcher({
3245
3355
  const state = runState(entry, {
3246
3356
  browserSessionId, browserTabId, desktopGrant,
3247
3357
  startedAt: entry.startedAt || iso(now()),
3248
- extra: { resumeSessionId: entry.providerSessionId, resumeThreadId: entry.providerThreadId, costBaseUsd, resumingWorker: true },
3358
+ extra: { resumeSessionId: entry.providerSessionId, resumeThreadId: entry.providerThreadId, costBaseUsd, resumingWorker: true, antigravityHome: entry.resumeInfo?.antigravityHome || null },
3249
3359
  });
3250
3360
  // The CLI's spend stop (--max-budget-usd) counts from the total the resumed CLI restores (costBaseUsd): what the run
3251
3361
  // may still spend (its ceiling less what it spent, never more than the session has left) goes on top of that. A
3252
3362
  // backstop only: the dispatcher holds the run to its ceiling and its share between calls.
3253
3363
  const allowanceUsd = claudeRunCap(entry, { spentUsd: entry.costUsd });
3254
3364
  if (Number(allowanceUsd) > 0) state.maxBudgetUsd = Number((Number(allowanceUsd) + Math.max(0, Number(costBaseUsd) || 0)).toFixed(2));
3255
- if (entry.permissionPolicy === 'ask') state.permissionBroker = createBroker(entry.runId);
3365
+ state.permissionBroker = createBroker(entry.runId);
3256
3366
  const at = clockTime(entry.suspendedAt);
3257
3367
  const selfRestarts = isInside(entry.cwd, PACKAGE_ROOT);
3258
3368
  const playbook = mode === 'fresh' ? playbookFor(entry) : null;
@@ -3511,6 +3621,57 @@ export function createAssistantDispatcher({
3511
3621
  }
3512
3622
  processQueue().catch(() => {});
3513
3623
  }
3624
+ /** One transcript line into a live run's ring (bounded by lines and characters). */
3625
+ function pushRingLine(liveState, line) {
3626
+ liveState.ring.push(line);
3627
+ liveState.ringChars += line.length;
3628
+ // The message line still growing is no longer the last one.
3629
+ liveState.messageLine = null;
3630
+ while (liveState.ring.length > limits.ringSize || liveState.ringChars > limits.ringChars) {
3631
+ const removed = liveState.ring.shift();
3632
+ liveState.ringChars -= removed?.length || 0;
3633
+ }
3634
+ }
3635
+ /**
3636
+ * An Antigravity message chunk (ACP agent_message_chunk): the text joins the message's line when that line is
3637
+ * still the ring's last one (same messageId), else it starts a new "assistant:" line. Clipped as every line is.
3638
+ */
3639
+ function growMessageLine(liveState, update = {}) {
3640
+ const text = update?.content?.type === 'text' ? String(update.content.text || '') : '';
3641
+ if (!text) return;
3642
+ const id = update.messageId ?? null;
3643
+ const open = liveState.messageLine;
3644
+ if (open && open.id === id && liveState.ring.length && liveState.ring[liveState.ring.length - 1] === open.line) {
3645
+ open.text += text;
3646
+ const line = `${open.stamp} assistant: ${clip(open.text, 200)}`;
3647
+ if (line !== open.line) {
3648
+ liveState.ring[liveState.ring.length - 1] = line;
3649
+ liveState.ringChars += line.length - open.line.length;
3650
+ open.line = line;
3651
+ }
3652
+ return;
3653
+ }
3654
+ if (!text.trim()) return;
3655
+ const stamp = iso(now()).slice(11, 19);
3656
+ const line = `${stamp} assistant: ${clip(text, 200)}`;
3657
+ pushRingLine(liveState, line);
3658
+ liveState.messageLine = { id, text, stamp, line };
3659
+ }
3660
+ /**
3661
+ * An ACP tool_call_update carries only what changed: the call it updates, as the run has seen it so far, so its
3662
+ * end is said with the call's kind, command and title. Other payloads pass as they are. Bounded per run.
3663
+ */
3664
+ function withAcpCall(liveState, payload) {
3665
+ if ((payload.eventType !== 'tool_call' && payload.eventType !== 'tool_call_update') || !payload.event?.toolCallId) return payload;
3666
+ const calls = liveState.acpCalls || (liveState.acpCalls = new Map());
3667
+ const id = String(payload.event.toolCallId);
3668
+ const call = { ...(calls.get(id) || {}) };
3669
+ for (const [key, value] of Object.entries(payload.event)) if (value !== undefined && value !== null) call[key] = value;
3670
+ calls.delete(id);
3671
+ calls.set(id, call);
3672
+ if (calls.size > 200) calls.delete(calls.keys().next().value);
3673
+ return { ...payload, event: call };
3674
+ }
3514
3675
  function onRuntimeMessage(message) {
3515
3676
  const runId = message?.run?.runId;
3516
3677
  if (!runId || !entries.has(runId)) return;
@@ -3529,7 +3690,9 @@ export function createAssistantDispatcher({
3529
3690
  // trigger reading (lib/skills/eval-trace.js; read with transcript format 'tools').
3530
3691
  if (isEvalRun(entry)) { try { recordEvalCalls(liveState, message.event || {}); } catch {} }
3531
3692
  if (entry.provider === 'opencode') { try { meterOpenCode(entry, liveState, message.event || {}); } catch (error) { log(runId, 'assistant:meter-error', error?.message || String(error)); } }
3532
- const summary = summarizeProviderEvent(message.event || {});
3693
+ // Antigravity streams each message in small chunks: one transcript line per message, grown as it streams.
3694
+ if (entry.provider === 'antigravity' && message.event?.eventType === 'agent_message_chunk') { growMessageLine(liveState, message.event.event); return; }
3695
+ const summary = summarizeProviderEvent(entry.provider === 'antigravity' ? withAcpCall(liveState, message.event || {}) : message.event || {});
3533
3696
  // A guard decision (lib/worker-confinement.js) is also a note of the run: what was refused or warned about,
3534
3697
  // with its reason, so the brain and the panel see it without the transcript. The first twenty.
3535
3698
  if ((message.event?.eventType || message.event?.event?.type) === 'synabun.confinement') {
@@ -3537,15 +3700,7 @@ export function createAssistantDispatcher({
3537
3700
  if (count <= 20) entry.notes.push(`guard ${message.event.event?.action || 'refused'}: ${clip(summary, 300)}`);
3538
3701
  else if (count === 21) entry.notes.push('guard: more decisions follow in the transcript');
3539
3702
  }
3540
- if (summary) {
3541
- const line = `${iso(now()).slice(11, 19)} ${summary}`;
3542
- liveState.ring.push(line);
3543
- liveState.ringChars += line.length;
3544
- while (liveState.ring.length > limits.ringSize || liveState.ringChars > limits.ringChars) {
3545
- const removed = liveState.ring.shift();
3546
- liveState.ringChars -= removed?.length || 0;
3547
- }
3548
- }
3703
+ if (summary) pushRingLine(liveState, `${iso(now()).slice(11, 19)} ${summary}`);
3549
3704
  }
3550
3705
  return;
3551
3706
  }
@@ -3556,13 +3711,16 @@ export function createAssistantDispatcher({
3556
3711
  persist({ force: true });
3557
3712
  emit(entry, 'tools_changed');
3558
3713
  }
3559
- entry.runtime = { status: run.status, turnState: run.turnState, stoppedReason: run.stoppedReason, error: run.error, claimedBy: run.claimedBy, providerSessionId: run.providerSessionId, providerThreadId: run.providerThreadId };
3714
+ const runHome = run.antigravityHome || entry.resumeInfo?.antigravityHome || entry.antigravityHome || null;
3715
+ entry.runtime = { status: run.status, turnState: run.turnState, stoppedReason: run.stoppedReason, error: run.error, claimedBy: run.claimedBy, providerSessionId: run.providerSessionId, providerThreadId: run.providerThreadId, ...(runHome ? { antigravityHome: runHome } : {}) };
3560
3716
  // A new provider identity is saved at once: a crash before the run's next write (its first alive mark comes up to
3561
3717
  // a minute later) would otherwise leave no session to resume.
3562
3718
  const newIdentity = (run.providerSessionId && run.providerSessionId !== entry.providerSessionId)
3563
- || (run.providerThreadId && run.providerThreadId !== entry.providerThreadId);
3719
+ || (run.providerThreadId && run.providerThreadId !== entry.providerThreadId)
3720
+ || (run.antigravityHome && run.antigravityHome !== entry.antigravityHome);
3564
3721
  if (run.providerSessionId) entry.providerSessionId = run.providerSessionId;
3565
3722
  if (run.providerThreadId) entry.providerThreadId = run.providerThreadId;
3723
+ if (run.antigravityHome) entry.antigravityHome = run.antigravityHome;
3566
3724
  if (newIdentity) persist({ force: true });
3567
3725
  if (message.type === 'sidepanel:run-updated' && message.reason === 'started' && entry.state === 'starting') {
3568
3726
  entry.state = 'running';