@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 +28 -0
- package/README.md +3 -2
- package/extensions/index.ts +7 -3
- package/package.json +2 -2
- package/src/acp/driver.ts +7 -0
- package/src/acp/events.ts +40 -14
- package/src/acp/setup.ts +7 -1
- package/src/ask-tool.ts +573 -235
- package/src/driver-types.ts +2 -1
- package/src/driver.ts +45 -13
- package/src/mcp-registration.ts +6 -1
- package/src/models.ts +4 -6
- package/src/prompt-images.ts +92 -0
- package/src/provider.ts +104 -12
- package/src/queued-messages.ts +171 -0
- package/src/stream-sanitizer.ts +180 -0
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
|
|
230
|
+
## Delegation tool: background runs
|
|
230
231
|
|
|
231
|
-
|
|
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
|
|
package/extensions/index.ts
CHANGED
|
@@ -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
|
|
148
|
-
//
|
|
149
|
-
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
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.#
|
|
230
|
-
delta.length > this.#
|
|
231
|
-
delta.startsWith(this.#
|
|
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.#
|
|
243
|
+
if (!delta.startsWith(this.#rawAcc)) {
|
|
237
244
|
// Misflip evidence: back to deltas.
|
|
238
245
|
this.#cumulative = false;
|
|
239
|
-
this.#
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
return delta;
|
|
274
|
+
return null;
|
|
249
275
|
}
|
|
250
276
|
|
|
251
277
|
get text(): string {
|
|
252
|
-
return this.#
|
|
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
|
-
|
|
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 {
|