@estebanforge/pi-antigravity-bridge 1.7.6 → 1.7.7
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 +10 -0
- package/package.json +1 -1
- package/src/provider.ts +22 -5
- package/src/sessions.ts +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.7.7] - 2026-09-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **A recreated agy conversation no longer starves the context digest.** When `session/load` failed, the ACP engine silently fell back to a fresh conversation (`session-load-failed-creating-fresh`), but the end-of-turn write-back kept advancing the stored watermark, so the new conversation never received the backlog: the digest delivered only the current prompt for the rest of the session, and the G10 system-prompt gate stayed closed too, because the surviving record always carried a conversation id. The write-back now compares the outcome's conversation id against the one stored before the turn and drops the record on a mismatch: the next turn re-delivers the full digest backlog and re-arms the system prompt, on both engines, with no protocol change. `SessionStore` gained a per-key `delete()` whose removal persists exactly once (the removal set prunes after a durable write, so a later persist can never destroy a record another process wrote in the meantime), preserving the multi-process merge invariant.
|
|
10
|
+
- **Failed and aborted turns no longer advance the digest watermark.** The write-back recorded `lastMessageCount` on any turn that reported a conversation id, ERROR and aborts included. Whether the prompt actually landed in the conversation's history is not observable from the provider, and advancing on that ambiguity starves the digest of the missed turn (the same failure class as the recreation bug above). The watermark now advances only on a clean, non-aborted turn; failed, aborted, and unknown outcomes leave the pre-turn record untouched, so the next digest re-delivers the missed turn bounded by the existing `maxChars` cap (default 8000) instead of losing it. With the digest off (the default) neither fix changes observable behavior.
|
|
11
|
+
- **Regression coverage pins both behaviors at the provider layer.** Eight new tests: record drop, backlog re-delivery, and system-prompt re-arm across the id swap; unchanged watermark across failed, aborted, and unknown outcomes on the same conversation; and the session-store delete and multi-process semantics, including a reclaimed key surviving an unrelated persist from another process. 1329 tests, tsc clean.
|
|
12
|
+
|
|
13
|
+
Thanks @fishfuuu for the report (#1).
|
|
14
|
+
|
|
5
15
|
## [1.7.6] - 2026-09-28
|
|
6
16
|
|
|
7
17
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.7",
|
|
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",
|
package/src/provider.ts
CHANGED
|
@@ -1283,11 +1283,28 @@ async function runTurnDriver(
|
|
|
1283
1283
|
|
|
1284
1284
|
const outcome = await handle.outcome;
|
|
1285
1285
|
if (outcome.conversationId) {
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1286
|
+
// A different id than the stored one means the driver recreated the
|
|
1287
|
+
// conversation under us (ACP session-load fallback, stream-json CLI
|
|
1288
|
+
// dropping a stale --conversation). The recreated conversation has seen
|
|
1289
|
+
// NOTHING, so the record's watermark would starve the digest forever and
|
|
1290
|
+
// its bound id would keep the sysPrompt gate closed. Drop the record:
|
|
1291
|
+
// the next turn resumes from watermark 0 with the gate re-armed (issue
|
|
1292
|
+
// #1). The recreation turn itself is past the gate, so it is the
|
|
1293
|
+
// documented residual; recovery starts on the next turn.
|
|
1294
|
+
if (existing?.conversationId && outcome.conversationId !== existing.conversationId) {
|
|
1295
|
+
store.delete(key);
|
|
1296
|
+
} else if (outcome.status === "OK" && !outcome.aborted) {
|
|
1297
|
+
store.set(key, {
|
|
1298
|
+
conversationId: outcome.conversationId,
|
|
1299
|
+
lastStepIdx: -1,
|
|
1300
|
+
lastMessageCount: messageCount,
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
// A failed or aborted turn on the SAME conversation keeps the pre-turn
|
|
1304
|
+
// record: whether the prompt actually landed in agy's DB is not
|
|
1305
|
+
// observable from here. Keeping the watermark re-delivers the missed
|
|
1306
|
+
// turn in the next digest (bounded by the digest cap); advancing it
|
|
1307
|
+
// would starve the conversation of context it may never have received.
|
|
1291
1308
|
}
|
|
1292
1309
|
if (outcome.aborted) {
|
|
1293
1310
|
finalize(stream, blocks, "aborted", "Operation aborted");
|
package/src/sessions.ts
CHANGED
|
@@ -68,6 +68,7 @@ export class SessionStore {
|
|
|
68
68
|
private readonly path: string;
|
|
69
69
|
private cache: StoreMap = {};
|
|
70
70
|
private readonly dirty = new Set<string>();
|
|
71
|
+
private readonly removed = new Set<string>();
|
|
71
72
|
private writeChain: Promise<void> = Promise.resolve();
|
|
72
73
|
|
|
73
74
|
constructor(storePath: string = STORE_PATH) {
|
|
@@ -100,6 +101,7 @@ export class SessionStore {
|
|
|
100
101
|
set(key: string, session: AgySession): void {
|
|
101
102
|
this.cache[key] = session;
|
|
102
103
|
this.dirty.add(key);
|
|
104
|
+
this.removed.delete(key);
|
|
103
105
|
// Serialize through a chain so concurrent sets don't interleave renames.
|
|
104
106
|
this.writeChain = this.writeChain
|
|
105
107
|
.then(() => this.persist())
|
|
@@ -108,6 +110,22 @@ export class SessionStore {
|
|
|
108
110
|
});
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
/** Drop one session binding and persist the deletion. Used when the driver
|
|
114
|
+
* recreated the conversation under us: the record's watermark claims
|
|
115
|
+
* history the new conversation never saw, and its bound id keeps the
|
|
116
|
+
* sysPrompt gate closed. Dropping resets the watermark to 0 AND re-arms
|
|
117
|
+
* the gate on the next turn (issue #1). The write is queued. */
|
|
118
|
+
delete(key: string): void {
|
|
119
|
+
delete this.cache[key];
|
|
120
|
+
this.dirty.delete(key);
|
|
121
|
+
this.removed.add(key);
|
|
122
|
+
this.writeChain = this.writeChain
|
|
123
|
+
.then(() => this.persist())
|
|
124
|
+
.catch(() => {
|
|
125
|
+
/* swallow; next write retries */
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
111
129
|
private async persist(): Promise<void> {
|
|
112
130
|
const dir = path.dirname(this.path);
|
|
113
131
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
@@ -126,6 +144,9 @@ export class SessionStore {
|
|
|
126
144
|
for (const key of this.dirty) {
|
|
127
145
|
merged[key] = this.cache[key];
|
|
128
146
|
}
|
|
147
|
+
for (const key of this.removed) {
|
|
148
|
+
delete merged[key];
|
|
149
|
+
}
|
|
129
150
|
|
|
130
151
|
const tmp = `${this.path}.${process.pid}.tmp`;
|
|
131
152
|
await fs.promises.writeFile(tmp, JSON.stringify(merged, null, 2) + "\n", {
|
|
@@ -133,6 +154,10 @@ export class SessionStore {
|
|
|
133
154
|
});
|
|
134
155
|
await fs.promises.rename(tmp, this.path);
|
|
135
156
|
this.dirty.clear();
|
|
157
|
+
// Mirror dirty: once the deletion is durable, stop re-asserting it. A
|
|
158
|
+
// later unrelated persist must not destroy a record another process
|
|
159
|
+
// wrote for that key after our delete landed.
|
|
160
|
+
this.removed.clear();
|
|
136
161
|
}
|
|
137
162
|
|
|
138
163
|
/** Wipe all session bindings (forces fresh agy conversations on every
|
|
@@ -140,6 +165,7 @@ export class SessionStore {
|
|
|
140
165
|
clear(): void {
|
|
141
166
|
this.cache = {};
|
|
142
167
|
this.dirty.clear();
|
|
168
|
+
this.removed.clear();
|
|
143
169
|
this.writeChain = this.writeChain
|
|
144
170
|
.then(async () => {
|
|
145
171
|
const tmp = `${this.path}.${process.pid}.tmp`;
|