@estebanforge/pi-antigravity-bridge 1.5.2 → 1.5.3
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 +7 -1
- package/extensions/index.ts +9 -1
- package/package.json +1 -1
- package/src/dialog-lock.ts +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [1.5.
|
|
5
|
+
## [1.5.3] - 2026-09-10
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Parallel agy tool approvals no longer clobber each other.** The shadow-tool `GatePolicy` asked through pi's `ui.confirm`, but pi's TUI shows ONE extension dialog at a time and an overlapping call replaces the live dialog without settling it: with several agy bash/write/edit calls in flight, approvals were silently lost (the park timeout eventually resolved them as declines). The confirm now holds the same shared cross-extension dialog lock as the pi-*-me gates (`withDialogLock`, `Symbol.for("pi-me.dialog-lock")`), so approvals queue and render in turn; the park timeout still bounds each dialog and is held inside the lock. New `src/dialog-lock.ts` module plus contract tests (FIFO order, throw-release, shared key).
|
|
10
|
+
|
|
11
|
+
## [1.5.2] - 2026-09-09
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
8
14
|
|
package/extensions/index.ts
CHANGED
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
import { mapAgyToolToNative } from "../src/native-tools.js";
|
|
89
89
|
import { Type } from "typebox";
|
|
90
90
|
import { patchStatus, restorePatch } from "../src/patch-cleanup.js";
|
|
91
|
+
import { withDialogLock } from "../src/dialog-lock.js";
|
|
91
92
|
|
|
92
93
|
// Last UI seen (session_start / /agy commands). The ACP login URL arrives
|
|
93
94
|
// via the driver log sink, which has no command context; the stash lets that
|
|
@@ -808,7 +809,14 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
808
809
|
: typeof params.path === "string"
|
|
809
810
|
? params.path
|
|
810
811
|
: JSON.stringify(stripMarkerFields(params)).slice(0, 200);
|
|
811
|
-
|
|
812
|
+
// Dialog lock: parallel agy tool approvals queue up instead of
|
|
813
|
+
// clobbering the live dialog (which silently loses the approval).
|
|
814
|
+
// Capture the narrowed method: TS drops the guard's narrowing
|
|
815
|
+
// inside the deferred closure.
|
|
816
|
+
const uiConfirm = extCtx.ui.confirm.bind(extCtx.ui);
|
|
817
|
+
const ok = await withDialogLock(() =>
|
|
818
|
+
uiConfirm(`agy ${tool}?`, what, { timeout: APPROVAL_PARK_MS }),
|
|
819
|
+
);
|
|
812
820
|
return ok ? { allow: true } : { allow: false, reason: `declined in pi (agy ${tool})` };
|
|
813
821
|
};
|
|
814
822
|
const handle = r.handle;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. antigravity/* models in Pi's /model picker, no-patch MCP bridge: agy runs Pi's tools. ToS safe to use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Process-wide FIFO lock for human-facing dialogs (confirm/editor/select).
|
|
2
|
+
//
|
|
3
|
+
// pi's interactive UI shows ONE extension dialog at a time; an overlapping
|
|
4
|
+
// ctx.ui.* dialog call REPLACES the live dialog and the replaced promise
|
|
5
|
+
// never settles, so parallel gated tool calls hang forever (or silently lose
|
|
6
|
+
// their prompt). Holding this lock around each dialog serializes them: the
|
|
7
|
+
// first renders, the rest appear in turn as each is answered.
|
|
8
|
+
//
|
|
9
|
+
// The lock is keyed via Symbol.for because ALL pi-* extensions run in the
|
|
10
|
+
// SAME pi process and must share one queue: a per-module lock would still let
|
|
11
|
+
// a dialog from one extension clobber a dialog from another in the same
|
|
12
|
+
// parallel tool-call batch.
|
|
13
|
+
const DIALOG_LOCK: unique symbol = Symbol.for("pi-me.dialog-lock");
|
|
14
|
+
|
|
15
|
+
interface DialogQueue {
|
|
16
|
+
tail: Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dialogQueue(): DialogQueue {
|
|
20
|
+
const host = globalThis as typeof globalThis & Record<symbol, unknown>;
|
|
21
|
+
const existing = host[DIALOG_LOCK] as DialogQueue | undefined;
|
|
22
|
+
if (existing) return existing;
|
|
23
|
+
const created: DialogQueue = { tail: Promise.resolve() };
|
|
24
|
+
host[DIALOG_LOCK] = created;
|
|
25
|
+
return created;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run `run` while holding the cross-extension dialog lock. FIFO: each caller
|
|
30
|
+
* chains onto the queue tail synchronously (before its first await), so call
|
|
31
|
+
* order is the order the dialogs appear. Released in a finally block, so one
|
|
32
|
+
* throwing dialog can never wedge the queue for the callers behind it.
|
|
33
|
+
* NOT reentrant: calling it inside a held `run` self-deadlocks.
|
|
34
|
+
*/
|
|
35
|
+
export async function withDialogLock<T>(run: () => Promise<T>): Promise<T> {
|
|
36
|
+
const queue = dialogQueue();
|
|
37
|
+
const prev = queue.tail;
|
|
38
|
+
let release!: () => void;
|
|
39
|
+
queue.tail = new Promise<void>((resolve) => {
|
|
40
|
+
release = resolve;
|
|
41
|
+
});
|
|
42
|
+
await prev;
|
|
43
|
+
try {
|
|
44
|
+
return await run();
|
|
45
|
+
} finally {
|
|
46
|
+
release();
|
|
47
|
+
}
|
|
48
|
+
}
|