@estebanforge/pi-antigravity-bridge 1.8.7 → 1.9.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.9.0] - 2026-10-27
6
+
7
+ ### Fixed
8
+
9
+ - **`/agy-stop` no longer discards an answer that already finished computing.** Between the agy process closing and the run settling (conversation discovery + classification, up to ~500ms), a stop settled the run failed and silently dropped the computed answer. The stop handler now consults a finishing guard (the new opt-in `guard` option on pi-ask-shared's `createStopHandler`) and refuses with "already finished; its result is being delivered" while that window is open; the wake then delivers normally.
10
+
11
+ - **`details.stderr` is redacted at the boundary.** Tool-result details are persisted in session files, and raw stderr can carry auth material; the stored field now passes through the bridge's redactor. The blocking answer text stays raw, like every other ask tool. This rides the same change as the background feature, which introduced the background-run stderr wake path that made the asymmetry visible.
12
+
13
+ - **The suppression refcount survives `/reload`.** It lived in module state, so a runtime replacement reset it to zero and the old runtime's late release (its kill path is async) could re-enable the bridge's global entries while the new runtime's delegation was still live. It now lives in a `Symbol.for`-keyed `globalThis` slot, shared by every runtime instance in the process.
14
+
15
+ ### Changed
16
+
17
+ - **Refusal results render with the error glyph.** Refusal paths returned `exitCode: 0`, so pi showed "✓ AskAntigravity" for calls that never ran. They now carry `details.refused`, and `renderResult` flips to ✗. Details only; refusal text is unchanged.
18
+
19
+ ### Added
20
+
21
+ - **`AskAntigravity` supports background runs, 1:1 with the standalone pi-ask-antigravity extension.** The old `background: true` refusal is gone: the call returns a `bg_` runId at once and the full answer arrives as an UNTRUSTED-tagged wake message (customType `ask-antigravity-result`, secret-redacted through the bridge's redactor) when the detached `agy -p` tree settles, carrying the conversationId resume handle. New `/agy-stop [runId]` command kills a run by id prefix, or the only running one with no argument. Shared `pi-ask-shared` machinery gives the same semantics as every other ask tool: concurrency cap 4 with a visible refusal, settle-once so a stop and a natural close can never double-wake, TTL sweep, and `session_shutdown` killing in-flight runs silently (pi's `/new`, `/resume`, `/fork`, `/reload` wipe module state and take the runs with them). Background is refused in print/json mode, where the process exits before the wake could arrive. Two behavior fixes ride along: the MCP-suppression window now spans background runs too (execute() returns while agy is still alive, so the release moved into the run lifecycle and fires exactly once per run), and the per-conversation lock (one run per conversationId, since two runs resuming the same conversation interleave turns inside agy's SQLite state) now also covers blocking calls. The delegation path itself is unchanged and engine-independent: always a fresh one-shot `agy -p`, never the ACP or stream-json turn engine. Review hardening riding in the same change: the detached run now settles on stream close instead of process exit (an exit-time settle could truncate the answer and disarmed the SIGKILL escalation while group members were still dying), stdout/stderr buffers carry valves (1M/64k) so a runaway child cannot grow memory over a days-long timeout, the timeout wake's partial-output slice is redacted before cutting (a sliced secret can fall under the redactor's length thresholds), and the suppression release is exactly-once at the call site, not just inside the callee's refcount guard.
22
+
23
+ ## [1.8.8] - 2026-10-27
24
+
25
+ ### Fixed
26
+
27
+ - **Short-circuit observational-memory background turns instead of letting Gemini hunt the missing observer tool.** pi-observational-memory's turn-end observer falls back to the session model, and its prompt demands a private `record_observations` tool the bridge never sends; on antigravity models that spawned autonomous `find`/`grep`/`sed` shell sweeps (~500 ms per cycle) hunting the tool before reporting "observer found nothing new in this chunk". `streamSimple` now detects the observer context structurally (its `record_observations`/`record_reflections`/`drop_observations` tool names) and finalizes with a clean empty stop, which engages the observer's empty-backoff instead of erroring every turn; a one-time per-process `observational-memory-unsupported` warning explains the gap.
28
+
29
+ - **Strip Antigravity's echoed `<SYSTEM_MESSAGE>` notifications and default Queued Messages to send-immediately.** The backend injects async background-task notifications wrapped in `<SYSTEM_MESSAGE>` blocks, and Gemini (notably 3.8 Flash) echoes them verbatim as assistant text; a stateful streaming sanitizer (`src/stream-sanitizer.ts`) now suppresses the exact preamble+block pattern in both engines' text paths, failing open at EOF so legitimate text is never lost. Separately, the engines' default "Queued Messages" behavior buffered user input until turn end while pi sends interactive input mid-turn; the bridge now writes `"queuedMessages": "send-immediately"` to each engine's settings.json when unset (atomic, existing config respected, corrupt files never touched).
30
+
31
+ - **Deliver image pixels across all the paths that used to lose them.** Verified live 2026-10-22 (couch session): gemini-3.8-flash read a screenshot, the parked bridge call escalated, the model never polled `bridge_poll_result`, and the stranded redelivery carried text only — the image never reached it and the model fell back to hunting OCR libraries. Late/stranded redeliveries now carry image blocks via `driver.run` (ACP native prompt blocks; stream-json stages them), user-attached images on the stream-json engine stage to temp files with a prompt note pointing at the bridge read tool (the CLI turn input is text-only; `src/prompt-images.ts`), and both engines now advertise `text+image` input. The tool-priority note states outright that `read` returns visible images (models refused screenshots from priors) and mandates polling `bridge_poll_result` before other work after a STILL RUNNING ack. Staging failures degrade to an explicit "image(s) could not be delivered" note instead of dropping silently.
32
+
5
33
  ## [1.8.7] - 2026-10-21
6
34
 
7
35
  ### Changed
package/README.md CHANGED
@@ -150,6 +150,7 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
150
150
  /agy engine acp|stream-json switch the turn engine (restart to apply; default acp, stream-json is the fallback; acp runs self-service setup: binary install + auth bootstrap). No arguments opens the engine picker modal (TUI)
151
151
  /agy auth-manual manual ACP credential setup (fallback; auto-setup normally covers this; default login = your Antigravity subscription, same account as the agy CLI)
152
152
  /agy clear drop all session bindings (force fresh conversations)
153
+ /agy-stop [runId] stop a background AskAntigravity run (id prefix, or the only running one with no argument)
153
154
  ```
154
155
 
155
156
  ### Permissions
@@ -226,9 +227,9 @@ pi-antigravity-bridge practical risk is low, near zero. But not zero: the "in co
226
227
 
227
228
  This is engineering analysis, not legal advice. Use against your own Antigravity account at your own risk; I am not responsible for any consequence to your account.
228
229
 
229
- ## Delegation tool: background is refused
230
+ ## Delegation tool: background runs
230
231
 
231
- The `AskAntigravity` delegation tool rejects `background: true` by design: a background wake would land in the pi session even when the caller is a nested delegation through the bridge catalog, and long delegations already have the bridge's own async flow (early-ack plus `bridge_poll_result`). Call without `background`; for unattended peer runs launch the CLI directly in a terminal.
232
+ `AskAntigravity` accepts `background: true` (1:1 with the standalone pi-ask-antigravity extension): the call returns a `bg_` runId at once and the full answer arrives later as an UNTRUSTED-tagged message (secret-redacted) carrying the resume handle. `/agy-stop [runId]` stops a run by id prefix, or the only running one with no argument; the concurrency cap is 4. A stop landing after agy already closed but before the result settles (the discovery window) is refused with "already finished; its result is being delivered" instead of dropping the answer. Background is refused in pi's print/json modes (the process exits before the message could arrive), and `/new`, `/resume`, `/fork`, `/reload` kill in-flight runs. Delegation always spawns a one-shot `agy -p` process - never the ACP or stream-json turn engine.
232
233
 
233
234
  ## License
234
235
 
@@ -63,6 +63,7 @@ import { runAcpAuth } from "../src/acp/auth.js";
63
63
  import { setAuthInvalidatedHook } from "../src/acp/connection.js";
64
64
  import { setupAuthUrlCapture } from "../src/acp/browser-capture.js";
65
65
  import { clearAcpAuthVerified, ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
66
+ import { ensureQueuedMessagesDefaults } from "../src/queued-messages.js";
66
67
  import type { TurnDriver, TurnOutcome } from "../src/driver-types.js";
67
68
  import { CONFIG_PATH, loadConfig, logsDir, MAX_TURN_CAP_MIN, parseCapMinutes, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
68
69
  import { agyMissingMessage, ENGINE_SETTINGS_ROW_ID, engineSettingsRow, isAgyInstalled, savedEngineMessage, showEnginePicker, shouldOfferEnginePicker } from "../src/engine-picker.js";
@@ -117,6 +118,8 @@ function resolveAgyBinary(): string {
117
118
  }
118
119
 
119
120
  export default async function (pi: ExtensionAPI): Promise<void> {
121
+ ensureQueuedMessagesDefaults({ onlyIfDirExists: true });
122
+
120
123
  // Claim the AskAntigravity tool for this process. pi-ask-antigravity (if also
121
124
  // installed) checks this in-process flag OR the bridge's package.json on disk
122
125
  // and defers. See that extension's isBridgeInstalled().
@@ -144,9 +147,10 @@ export default async function (pi: ExtensionAPI): Promise<void> {
144
147
  // kickIdle, and reentry pointing at the other engine (round-7 finding).
145
148
  const engine: Engine = loadConfig().engine;
146
149
  // Engine switching requires a restart, so the catalog-time engine read is
147
- // authoritative for input advertising: image attach rides only when turns
148
- // will run on the ACP engine (the stream-json CLI prompt is text-only).
149
- const modelInput: Array<"text" | "image"> = engine === "acp" ? ["text", "image"] : ["text"];
150
+ // authoritative for input advertising. Both engines accept images: ACP
151
+ // rides native content blocks; stream-json stages user attachments to
152
+ // temp files and the prompt points the model at them (prompt-images.ts).
153
+ const modelInput: Array<"text" | "image"> = ["text", "image"];
150
154
  const models = entries.map((e) => toPiModel(e, modelInput));
151
155
 
152
156
  // failAll reasons the bridge treats as routine (turn end / shutdown
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-antigravity-bridge",
3
- "version": "1.8.7",
3
+ "version": "1.9.0",
4
4
  "description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. Use antigravity/* models in Pi. Full /model picker support. Models runs Pi's tools. ToS safe to use.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -54,7 +54,7 @@
54
54
  }
55
55
  },
56
56
  "dependencies": {
57
- "@estebanforge/pi-ask-shared": "^0.1.2",
57
+ "@estebanforge/pi-ask-shared": "^0.1.3",
58
58
  "@estebanforge/pi-tui-dialogs": "^0.1.2",
59
59
  "@modelcontextprotocol/sdk": "^1.31.0"
60
60
  }
package/src/acp/driver.ts CHANGED
@@ -736,6 +736,13 @@ export class AcpDriver implements TurnDriver {
736
736
 
737
737
  #settle(turn: ActiveTurn, outcome: TurnOutcome): void {
738
738
  if (turn.closed) return;
739
+ const flushed = turn.response.flush();
740
+ if (flushed) {
741
+ turn.textDeltas += 1;
742
+ turn.textTokens += estimateTokens(flushed);
743
+ this.#emit(turn, { type: "text", delta: flushed });
744
+ }
745
+ outcome.response = turn.response.text;
739
746
  // Gate B stopgap: synthesize usage on clean turns while the server sends
740
747
  // none. Runs BEFORE close so the usage activity drains through the
741
748
  // normal #nextActivity loop (provider maps it onto partial.usage).
package/src/acp/events.ts CHANGED
@@ -14,6 +14,10 @@
14
14
  // - available_commands_update { availableCommands: [...] }
15
15
  // Usage: ABSENT on RC01 (Gate B) — mapped defensively should a future build add it.
16
16
 
17
+ import { StreamSanitizer, sanitizeSystemMessage } from "../stream-sanitizer.js";
18
+
19
+ export { StreamSanitizer, sanitizeSystemMessage };
20
+
17
21
  /** Map one `update` object to a driver-level event, or null when the update
18
22
  * carries nothing the driver consumes (plan/commands are doctor/phase-2
19
23
  * material; user_message_chunk is load replay and must never reach pi as
@@ -218,38 +222,60 @@ export function mapStopReason(stopReason: unknown): {
218
222
  const FLIP_MIN_CHARS = 32;
219
223
 
220
224
  export class TextAccumulator {
221
- #acc = "";
225
+ #rawAcc = "";
226
+ #sanitizedAcc = "";
222
227
  #cumulative: boolean | undefined;
228
+ #sanitizer = new StreamSanitizer();
223
229
 
224
230
  /** Returns the delta to emit, or null when nothing new should be emitted. */
225
231
  append(delta: string): string | null {
232
+ let rawEmit: string | null = null;
226
233
  if (this.#cumulative === undefined) this.#cumulative = false;
227
234
  else if (
228
235
  !this.#cumulative &&
229
- this.#acc.length >= FLIP_MIN_CHARS &&
230
- delta.length > this.#acc.length &&
231
- delta.startsWith(this.#acc)
236
+ this.#rawAcc.length >= FLIP_MIN_CHARS &&
237
+ delta.length > this.#rawAcc.length &&
238
+ delta.startsWith(this.#rawAcc)
232
239
  ) {
233
240
  this.#cumulative = true;
234
241
  }
235
242
  if (this.#cumulative) {
236
- if (!delta.startsWith(this.#acc)) {
243
+ if (!delta.startsWith(this.#rawAcc)) {
237
244
  // Misflip evidence: back to deltas.
238
245
  this.#cumulative = false;
239
- this.#acc += delta;
240
- return delta;
246
+ this.#rawAcc += delta;
247
+ rawEmit = delta;
248
+ } else if (delta.length <= this.#rawAcc.length) {
249
+ return null; // duplicate frame
250
+ } else {
251
+ rawEmit = delta.slice(this.#rawAcc.length);
252
+ this.#rawAcc = delta;
241
253
  }
242
- if (delta.length <= this.#acc.length) return null; // duplicate frame
243
- const emit = delta.slice(this.#acc.length);
244
- this.#acc = delta;
245
- return emit;
254
+ } else {
255
+ this.#rawAcc += delta;
256
+ rawEmit = delta;
257
+ }
258
+
259
+ if (rawEmit === null) return null;
260
+ const sanitized = this.#sanitizer.push(rawEmit);
261
+ if (sanitized.length > 0) {
262
+ this.#sanitizedAcc += sanitized;
263
+ return sanitized;
264
+ }
265
+ return null;
266
+ }
267
+
268
+ flush(): string | null {
269
+ const flushed = this.#sanitizer.flush();
270
+ if (flushed.length > 0) {
271
+ this.#sanitizedAcc += flushed;
272
+ return flushed;
246
273
  }
247
- this.#acc += delta;
248
- return delta;
274
+ return null;
249
275
  }
250
276
 
251
277
  get text(): string {
252
- return this.#acc;
278
+ return this.#sanitizedAcc;
253
279
  }
254
280
  }
255
281
 
package/src/acp/setup.ts CHANGED
@@ -16,6 +16,7 @@ import { createHash } from "node:crypto";
16
16
  import fs from "node:fs";
17
17
  import os from "node:os";
18
18
  import path from "node:path";
19
+ import { ensureQueuedMessagesInFile, QUEUED_MESSAGES_SEND_IMMEDIATELY } from "../queued-messages.js";
19
20
 
20
21
  export const REGISTRY_URL =
21
22
  "https://raw.githubusercontent.com/agentclientprotocol/registry/main/antigravity-acp/agent.json";
@@ -312,7 +313,11 @@ export function writeAuthType(type: "gemini-api-key" | "oauth-personal", dir?: s
312
313
  /* fresh file */
313
314
  }
314
315
  const prevAuth = (typeof settings.auth === "object" && settings.auth !== null ? settings.auth : {}) as Record<string, unknown>;
315
- fs.writeFileSync(file, `${JSON.stringify({ ...settings, auth: { ...prevAuth, type } }, null, 2)}\n`, { mode: 0o600 });
316
+ const nextSettings: Record<string, unknown> = { ...settings, auth: { ...prevAuth, type } };
317
+ if (typeof nextSettings.queuedMessages !== "string" || !nextSettings.queuedMessages.trim()) {
318
+ nextSettings.queuedMessages = QUEUED_MESSAGES_SEND_IMMEDIATELY;
319
+ }
320
+ fs.writeFileSync(file, `${JSON.stringify(nextSettings, null, 2)}\n`, { mode: 0o600 });
316
321
  }
317
322
 
318
323
  export interface BinaryResolution {
@@ -386,6 +391,7 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
386
391
  // plan as the agy CLI login). gemini-api-key (metered paid API) stays a
387
392
  // manual option for headless boxes (/agy auth-manual); never the default.
388
393
  const gdir = defaultGeminiDir(opts);
394
+ ensureQueuedMessagesInFile(path.join(gdir, "settings.json"));
389
395
  const auth = readAuthState(gdir);
390
396
  if (!auth.configured) {
391
397
  try {