@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 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.6",
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
- store.set(key, {
1287
- conversationId: outcome.conversationId,
1288
- lastStepIdx: -1,
1289
- lastMessageCount: messageCount,
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`;