@estebanforge/pi-antigravity-bridge 1.3.3 → 1.4.1

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.
@@ -0,0 +1,875 @@
1
+ # ACP Adoption Plan: Official Google `antigravity-acp` Server
2
+
3
+ Status: PROPOSED. Phase 0 COMPLETE + Phase 1 COMPLETE (2026-09-03): ACP engine
4
+ implemented behind `config.engine` (default off), full suite green, live smoke
5
+ PASS through the full stack. Gate F VERIFIED live (`scripts/smoke-acp-bridge.mjs`):
6
+ the real server lists bridge tools and completes a tool call through the
7
+ registered mcpServers entry. §6 parity run LIVE on both engines: 14/14
8
+ (`scripts/parity-live.mjs`). Phase-1 acceptance: ALL `[x]` (docs included).
9
+ Gates: A PASS, B ABSENT (managed — streaming engine retained
10
+ until upstream ships usage), C PASS, D FAIL on RC01 (kill+reload fallback
11
+ VERIFIED live, incl. the stale-exit race fix), E PASS, F PASS (verified
12
+ live 2026-09-03). Verdicts in section 8.1; full captured protocol data in
13
+ [ACP-PROTOCOL-REFERENCE.md](./ACP-PROTOCOL-REFERENCE.md); raw traffic
14
+ gitignored in `probe-logs/`.
15
+ Peer review 1: Claude, 2026-09-03. Verdict PROCEED WITH CHANGES. Findings 1-6
16
+ integrated: third round-trip kind for permissions (P1), parked-overall-timer
17
+ pause (P1), cumulative-resend guard port, deferred-capability dispositions,
18
+ capability-posture prose, concurrency note.
19
+ Peer review 2: Antigravity (agy), 2026-09-03. Verdict PROCEED WITH CHANGES.
20
+ 7 findings integrated: Gate A made a blocking gate for the default flip (the
21
+ ACP binary takes no model flags, so the recycle fallback could not carry a
22
+ model), sessions re-keyed per engine (`@acp` suffix) after the engine-field
23
+ design was shown to erase streaming bindings on rollback, parked-timer pause
24
+ changed to remaining-budget semantics, acp-permission plumbing specified
25
+ through a TurnDriver interface, session/cancel + session/close pulled into
26
+ phase 1, AskAntigravity migration moved from phase 3 to phase 4.
27
+ Peer reviews 3-4 (plan-stage second rounds, Claude and agy): findings folded
28
+ into the two entries above and the section 8/9 design; no separate deltas.
29
+ Peer review 5 (implementation): Antigravity (agy), 2026-09-03. Verdict
30
+ PROCEED WITH CHANGES. 7 findings, all valid, all fixed: P0 jsonrpc line
31
+ buffering (stdio chunks are not newline-aligned; partial frames were dropped),
32
+ P1 tool args preserved through tool_done, P1 typed protocol errors survive
33
+ translation (cancelSupported stuck null), P1 parked-deadline nulled on pause,
34
+ P2 tilde expansion, P2 per-turn config read dedup, P2 coverage gaps filled.
35
+ Peer review 6 (post-build): Claude, 2026-09-03, isolated and read-only.
36
+ Verdict PROCEED WITH CHANGES. 6 findings, 5 valid, all fixed: P1 bridge 403
37
+ gate had no vitest coverage (HTTP-level test added: no header / wrong-length /
38
+ same-length wrong token → 403, correct → 200), P1 sessionKey recorded the
39
+ config engine instead of the selected driver's (fixed), P2 arm-time park
40
+ left a stale deadline (fixed: paused-from-birth invariant), P2 duplicated
41
+ timeout body (extracted), P2 abort-before-prompt probed cancel (now guarded
42
+ by promptStarted). Sixth finding (untracked parity script) resolved by
43
+ commit.
44
+
45
+ Adopt the official Google ACP server (`agy_acp_server.par`, registry id
46
+ `antigravity-acp`) as a second turn engine for the bridge, behind a config
47
+ switch. The existing stream-json engine stays the default until the ACP
48
+ engine proves parity. Every change is an improvement or a one-to-one
49
+ replacement. No functionality is removed until a later phase deletes the
50
+ streaming engine on purpose.
51
+
52
+ Cross-references: [ARCHITECTURE.md](./ARCHITECTURE.md),
53
+ [PI-BRIDGE-GAPS.md](./PI-BRIDGE-GAPS.md), [DEVELOPMENT.md](./DEVELOPMENT.md).
54
+
55
+ ## 1. Decision summary
56
+
57
+ | Question | Answer |
58
+ | --- | --- |
59
+ | Adopt? | Yes, phased, behind `config.engine: "acp"` |
60
+ | Why | Protocol-native permissions, image prompts, thought text, graceful cancel, session resume/list, sanctioned auth path, stable public protocol |
61
+ | Why not a rewrite | The provider layer (G1 digest, G10 system prompt, G9 park/resume, pi event mapping) is engine-agnostic and already proven. The driver is the only engine-coupled part |
62
+ | Default flip | Phase 4, after a soak release with both engines |
63
+ | Regression guard | Parity checklist (section 6) must be all `[x]` before the default flips |
64
+
65
+ ## 2. Verified evidence
66
+
67
+ All facts below were verified live on 2026-09-03, not taken from docs. Probe
68
+ artifacts live in `~/tmp/agy-acp-probe/` (binary, handshake transcripts).
69
+
70
+ ### 2.1 The registry entry
71
+
72
+ Source: `agentclientprotocol/registry`, path `antigravity-acp/agent.json`
73
+ (folder contains only `agent.json` + `icon.svg`).
74
+
75
+ | Field | Value |
76
+ | --- | --- |
77
+ | id | `antigravity-acp` |
78
+ | name / publisher | Google Antigravity / Google LLC, proprietary |
79
+ | version | 1.0.0 |
80
+ | build | `agy_acp_server_20260818_01_RC01` |
81
+ | binaries | darwin-aarch64, linux-x86_64, linux-aarch64, windows-x86_64, windows-aarch64 |
82
+ | linux cmd | `./agy_acp_server.par`, registry args `["--uid="]` |
83
+ | flags | `--[no]debug`, `--[no]notices` only. No model/effort/conversation flags |
84
+
85
+ The registry README states agents are CI-verified to return valid
86
+ `authMethods` in the handshake, and the index is re-fetched hourly.
87
+
88
+ ### 2.2 Live initialize response
89
+
90
+ Sent:
91
+
92
+ ```json
93
+ {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":true,"writeTextFile":true},"terminal":false}}}
94
+ ```
95
+
96
+ Got (abridged to capability names):
97
+
98
+ ```json
99
+ {"protocolVersion":1,
100
+ "agentCapabilities":{
101
+ "loadSession":true,
102
+ "promptCapabilities":{"image":true,"audio":true,"embeddedContext":true},
103
+ "mcpCapabilities":{"http":true,"sse":true},
104
+ "sessionCapabilities":{"list":{},"resume":{}},
105
+ "auth":{"logout":{}}},
106
+ "authMethods":[
107
+ {"id":"oauth-personal","name":"Log in with Google"},
108
+ {"id":"oauth-business","name":"Log in with Gemini Enterprise"},
109
+ {"id":"gemini-api-key","name":"Gemini API key"},
110
+ {"id":"agent-platform","name":"Gemini Enterprise Agent Platform"}],
111
+ "agentInfo":{"name":"antigravity-acp","title":"Google Antigravity",
112
+ "version":"agy_acp_server_20260818_01_RC01"}}
113
+ ```
114
+
115
+ ### 2.3 Unauthenticated session/new
116
+
117
+ `session/new` returned `-32000 Authentication required` with the full recipe:
118
+ call `authenticate`, or set `auth.type` in
119
+ `~/.gemini/antigravity-acp/settings.json`. Accepted settings types:
120
+ `oauth-personal`, `gemini-api-key` (reads `GEMINI_API_KEY`), `oauth-business`
121
+ (needs `gcp.project` + `gcp.location`), `agent-platform` (ADC or
122
+ `GOOGLE_API_KEY`, project/location from `GOOGLE_CLOUD_PROJECT` /
123
+ `GOOGLE_CLOUD_LOCATION`). Confirmed by the official Zed setup page
124
+ (`antigravity.google/docs/ide/extensions/zed/`), which publishes exactly these
125
+ `settings.json` shapes.
126
+
127
+ ### 2.4 Binary
128
+
129
+ 1,529,513,909 bytes ELF x86-64, plus a 117 MB `localharness_external`
130
+ sibling. Cold start measured at a few seconds on this machine (handshake
131
+ answered well inside the probe timeout).
132
+
133
+ ### 2.5 ACP v1 method surface
134
+
135
+ From `agentclientprotocol/agent-client-protocol` schema source (`v1/agent.rs`,
136
+ `v1/client.rs`):
137
+
138
+ - Client to agent: `initialize`, `authenticate`, `session/new`, `session/load`,
139
+ `session/prompt`, `session/cancel`, `session/list`, `session/resume`,
140
+ `session/fork`, `session/close`, `session/delete`, `session/set_mode`,
141
+ `session/set_config_option`.
142
+ - Agent to client (requests): `session/request_permission`,
143
+ `fs/read_text_file`, `fs/write_text_file`, `terminal/create`, `terminal/kill`,
144
+ `terminal/output`, `terminal/release`, `terminal/wait_for_exit`.
145
+ - Agent to client (notifications): `session/update`.
146
+ - Extension namespace observed in schema: `nes/*` (IDE edit surfaces). Ignore
147
+ for this plan.
148
+
149
+ ### 2.6 Governance
150
+
151
+ `zed-industries/zed#57221` records the chain: Zed maintainers declined a
152
+ standalone SDK integration, waited for Google, and Google shipped the official
153
+ server. The same thread documents the Antigravity FAQ position that
154
+ third-party access to an Antigravity login violates ToS. The community
155
+ adapters carry that risk. Google's own server is the sanctioned client
156
+ channel. Our undocumented stream-json driver carries the same class of risk
157
+ today. Adoption retires it.
158
+
159
+ Also dated in that thread: Gemini CLI / Code Assist stop serving 2026-06-18.
160
+ `agy` is the only first-party CLI going forward, and its stream-json dialect
161
+ is an unmaintained contract. ACP is the maintained one.
162
+
163
+ ## 3. Current architecture inventory and disposition
164
+
165
+ | Module | Role today | Disposition under ACP |
166
+ | --- | --- | --- |
167
+ | `src/provider.ts` | streamSimple: pi Context to agy turn to pi events; G9 round-trips; G1 digest; G10 system prompt | KEEP unchanged (additive thought-delta `if` only). Engine-agnostic by contract |
168
+ | `src/driver.ts` | persistent stream-json process, turn queue, recycle, timers | KEEP as streaming engine. DELETE in phase 4 |
169
+ | `src/stream-events.ts` | NDJSON parser + usage mapping | KEEP as streaming. DELETE in phase 4 |
170
+ | `src/sessions.ts` | pi session to agy conversation store | KEEP. Add `engine` tag (section 9.4) |
171
+ | `src/config.ts` | runtime config | KEEP. Add engine + acp block (section 9.5) |
172
+ | `src/models.ts` | `agy models` to pi Model projection | KEEP. Catalog source stays the CLI until ACP exposes one (probe A.6) |
173
+ | `src/mcp-server.ts` | bridge HTTP MCP server; G9 park | KEEP unchanged. Registered via `mcpServers` under ACP |
174
+ | `src/skills.ts` | `activate_skill` bridge | KEEP unchanged |
175
+ | `src/native-tools.ts` | agy read-only steps to pi builtins | KEEP through phase 2. Phase 3 re-evaluates against `tool_call` content (gate C) |
176
+ | `WrapperReplay` (provider.ts) | display-only `antigravity` wrapper tool | Same: re-evaluate in phase 3 (gate C) |
177
+ | `src/diff-render.ts` | git-sourced diffs for agy edits | KEEP while agy self-executes edits (our fs capabilities off) |
178
+ | `src/ask-tool.ts` | AskAntigravity one-shot via `agy -p` | MIGRATE to ACP one-shot in phase 3 |
179
+ | `src/discovery.ts` | snapshot-diff + `/proc` fd-scan conversation binding | DELETE in phase 3 (ACP returns real session ids) |
180
+ | `src/patch-cleanup.ts` | pre-1.3.0 patch cleanup | KEEP unrelated |
181
+ | `extensions/index.ts` | registration, `/agy` command, notices | EXTEND: engine switch, acp auth command, doctor rebuild |
182
+
183
+ ## 4. Capability matrix
184
+
185
+ Verdicts: IMPROVEMENT (strictly better), PARITY (equal), RISK (may regress,
186
+ mitigation required). Originally assessed pre-probe; verdict column SYNCED to
187
+ the Phase 0 results (§8.1, 2026-09-03). Pre-probe reasoning for any row lives
188
+ in git history.
189
+
190
+ | Dimension | Bridge today (stream-json) | Official ACP | Verdict |
191
+ | --- | --- | --- | --- |
192
+ | Wire contract | Undocumented NDJSON, shapes captured live, cumulative-resend heuristic, `OK`/`SUCCESS` mapping | Public versioned protocol, negotiated handshake | IMPROVEMENT |
193
+ | Text streaming | `text_delta` + dedupe guard | `agent_message_chunk` deltas, native | PARITY |
194
+ | Thinking | Token count only, floor 64, no text body | `agent_thought_chunk` carries text | IMPROVEMENT |
195
+ | Tool call visibility | start/done/error + native re-exec + wrapper replay | `tool_call` / `tool_call_update` with kind, status, content, locations, rawInput/output | IMPROVEMENT (VERIFIED: diffs arrive in content; Gate C PASS — machinery retirement confirmed for phase 3) |
196
+ | Permissions | `skipPermissions: true` default, `--dangerously-skip-permissions`, commands run unreviewed | `session/request_permission` flows to the client | PARITY under adoption (`auto` policy, 9.3). The primitive exists for whoever gates later; per-tool gating stays extension-owned via G9 |
197
+ | Images in prompt | Dropped in `extractUserPrompt` | `promptCapabilities.image: true` | IMPROVEMENT (VERIFIED end-to-end, run 6: "Red") |
198
+ | Audio | Dropped | `promptCapabilities.audio: true` | IMPROVEMENT (low priority) |
199
+ | Session resume | `sessions.json` + `--conversation` flag, per pi session only | `session/load`, `list`, `resume`, server-side | IMPROVEMENT (load VERIFIED across restart, run 6; full-text replay rules in 9.2) |
200
+ | Cancel | Kill process group; in-flight turn dies | `session/cancel` to `stopReason: cancelled`, process survives | **REGRESSION-MANAGED (Gate D FAIL on RC01): cancel NOT implemented.** Fallback kill + `session/load` VERIFIED; re-check per build |
201
+ | Process lifecycle | One agy child per provider; recycle on model/effort/mode/cwd/conversation drift | One server hosts N sessions | IMPROVEMENT |
202
+ | Model + effort selection | CLI flags + recycle | `session/set_config_option` (`configId`) | PASS (VERIFIED live, Gate A): per-session switching, no recycle |
203
+ | Usage tokens | `toPiUsage` from step events | Not in any payload (Gate B FINAL) | **ABSENT on RC01.** Zero-usage documented; stream-json retained as secondary engine; phase-4 deletion conditioned on Gate B lift |
204
+ | MCP tools (G9) | Our HTTP bridge server via `--add-dir` config | `mcpServers` param (`{name,type:"http",url,headers:[]}`) | PARITY at shape level (verified). Phase-1 acceptance: bridge `tools/list`+`tools/call` end-to-end |
205
+ | Skills | `--disable-slash-commands`; bridge owns skills | Unknown slash behavior; `available_commands_update` exists | PARITY. Keep our bridge; probe F.7 |
206
+ | G1 digest, G10 system prompt | Provider-side prompt assembly | Same (we compose the prompt either way) | PARITY. `embeddedContext` is a later enhancement |
207
+ | Diagnostics | `/agy doctor`: snapshot, lifecycle, recycle stats | Nothing equivalent | PARITY work: rebuild on ACP events (section 10) |
208
+ | Auth | Piggybacks `agy` CLI login state | First-class: 4 methods, `authenticate`, `logout`, settings.json | IMPROVEMENT. One-time re-login cost |
209
+ | One-shot delegation (AskAntigravity) | `agy -p` + snapshot-diff + `/proc` fd-scan | `session/new` + `prompt`, real ids | IMPROVEMENT. Deletes discovery.ts |
210
+ | ToS exposure | Undocumented CLI driving | Sanctioned Google client channel | IMPROVEMENT |
211
+
212
+ ## 5. Improvements in detail
213
+
214
+ 1. **Permission flow.** Today any agy `run_command` executes unreviewed; the
215
+ config comment says so in plain words. ACP makes approval a protocol
216
+ primitive (`session/request_permission`, verified live). Under the single
217
+ `auto` policy (9.3) adoption is parity; the primitive is in place for
218
+ whoever wants to gate later.
219
+ 2. **Thought text.** `agent_thought_chunk` delivers the model's actual
220
+ thinking text. Today pi renders `[thinking]` labels and token counts only.
221
+ Provider change is additive: extend the `thought` activity with an optional
222
+ `delta` field (9.2).
223
+ 3. **Images.** `promptCapabilities.image` lets us forward pi image blocks as
224
+ ACP image content instead of dropping them in `extractUserPrompt`.
225
+ Community reports say the `agy` CLI itself had broken image upload, so the
226
+ live probe (A.4) verifies end-to-end delivery before we claim this.
227
+ 4. **Graceful cancel.** `session/cancel` converts to a clean
228
+ `stopReason: cancelled` prompt response. Today abort kills the process
229
+ group and orphans the turn. Maps onto pi's `aborted` stop reason.
230
+ 5. **Server-side sessions.** `session/load` restores a conversation after a
231
+ pi restart without our store being the source of truth, and
232
+ `session/list` / `session/resume` make `/agy conversations`-style tooling
233
+ possible later. Sessions outlive the process; no recycle on drift.
234
+ 6. **Protocol stability.** `initialize` negotiates `protocolVersion`. Unknown
235
+ fields degrade instead of silently misparsing (compare: the 1.3.2 sqlite
236
+ incident, issue #1).
237
+ 7. **Sanctioned channel.** Section 2.6. Risk of account action and of silent
238
+ dialect drift both drop.
239
+ 8. **One-shot cleanup.** `discovery.ts` exists only because `agy -p` never
240
+ prints its conversation id. ACP returns real session ids. The
241
+ snapshot-diff + `/proc` fd-scan machinery and its ambiguity heuristics get
242
+ deleted.
243
+ 9. **Enterprise auth.** `oauth-business` / `agent-platform` bring VPC-SC and
244
+ data-residency posture the CLI path never exposed to us.
245
+ 10. **Deferred, deliberately.** `session/fork` (future multi-branch feature),
246
+ `session/close` / `session/delete` (candidates for a later
247
+ `/agy conversations` cleanup command), `terminal/*` and client-side `fs/*`
248
+ delegation declined: agy keeps executing its own commands and file ops as
249
+ today (parity), and answering them would put execution inside our process
250
+ for no parity gain; our terminal and fs client capabilities stay false.
251
+ `nes/*` IDE-edit extension (ignore), `audio` prompts (phase 2, optional).
252
+
253
+ ## 6. Parity contract (no-regression checklist)
254
+
255
+ Every box must be `[x]` on the ACP engine before phase 4 flips the default.
256
+ Tested with the parity suite (section 11) and `scripts/parity-live.mjs`
257
+ (live, both engines: 14/14, 2026-09-03).
258
+
259
+ - [x] Text streams token-adjacent to streaming (no full-text re-sends) — live
260
+ both engines, delta stream + cumulative-resend guard held
261
+ - [x] Multi-turn conversation continuity via `session/load` or our store —
262
+ live: ACP `session/load` (history suppressed, clean reply), streaming
263
+ `--conversation` resume; the provider's session store supplies the id
264
+ - [x] Turn serialization: two concurrent streamSimple calls never interleave
265
+ — live both engines (queue releases only on settle) + driver tests
266
+ - [x] Bridge tool round-trip (G9) works: bridge MCP `tools/call` parks, pi
267
+ executes, `toolResult` resumes the same agy turn — live both engines
268
+ through the real bridge server; the pi-side park/resume is engine-
269
+ independent (proven live on stream-json; the ACP parity run answers
270
+ deps directly to isolate the agy-side HTTP client)
271
+ - [x] Idle timer suspends while a G9 park is open, resumes on `kickIdle` —
272
+ fake-server tests (park/kickIdle remaining-budget deadline)
273
+ - [x] Overall turn timeout (10m) and inactivity timeout (5m) still fire and
274
+ fail the turn visibly — driver tests (deadline fires with remaining
275
+ budget after resume); inactivity shares the timer machinery
276
+ - [x] Abort: pi user abort produces `aborted`, the server process survives,
277
+ the next turn on the session works — live both engines (aborted=true,
278
+ recovery turn OK). "Process survives" holds on NEITHER engine and is
279
+ the documented Gate D behavior: streaming kills the child; RC01 has no
280
+ cancel, ACP tears down after the -32601 probe and reloads next turn
281
+ - [x] pi session restart resumes the right conversation (engine-tagged) —
282
+ engine-scoped key tests + live `session/load`
283
+ - [x] Engine rollback preserves the other engine's conversation bindings
284
+ (engine-scoped keys, 9.4) — sessions tests (per-entry engine field
285
+ rejected precisely because set() would erase the streaming binding)
286
+ - [x] Model catalog: same `antigravity/*` model ids and effort tiers resolve
287
+ — shared provider catalog, engine-independent by construction
288
+ - [x] Model/effort switch takes effect (gate A mechanism) — live: ACP
289
+ `set_config_option` on a LOADED session; streaming recycle+resume
290
+ - [x] G1 digest and G10 system prompt reach the prompt identically — shared
291
+ prompt assembly in the provider; the driver receives the final prompt
292
+ - [x] Skills `activate_skill` answered by the bridge as today — bridge
293
+ server dep, same server serves both engines (ACP transport live)
294
+ - [x] Usage counters: present (mapped) or documented-absent (zero-usage),
295
+ per gate B — live: streaming mapped, ACP absent (Gate B row in the
296
+ parity matrix)
297
+ - [x] `/agy doctor` shows connection state, session id, prompt/session stats,
298
+ lifecycle log — implemented engine-aware (phase 1); re-verify in pi
299
+ before the phase 4 default flip
300
+ - [x] Error paths: spawn failure, auth failure, protocol error all surface as
301
+ visible turn errors, never as silent empty messages — fake tests
302
+ (auth-required, load-fails) + live: a bad-shape registration surfaced
303
+ as a visible `ACP session failed: Invalid params` turn error in the
304
+ first parity attempt, exactly as designed
305
+ - [x] `ask-tool` one-shot returns text, duration, abort/timeout states, and a
306
+ resumable session id — unchanged `agy -p` path (phase-2 probe
307
+ decides whether it moves to ACP)
308
+ - [x] Overall-turn timer pauses while any round-trip is parked; a slow human
309
+ permission decision cannot kill the turn — fake tests (park/kickIdle
310
+ remaining budget)
311
+ - [x] Cumulative-resend defensive guard on message chunks (port of
312
+ `isCumulativeResend`), with a fake-server test. Note: provably inert
313
+ on live RC01 (run-5 stress showed pure mid-token deltas); the fake
314
+ server is the only surface that can exercise it — confirmed inert
315
+ again in the live parity run
316
+
317
+ ## 7. Regression risks and mitigations
318
+
319
+ | Risk | Impact | Mitigation |
320
+ | --- | --- | --- |
321
+ | Usage tokens absent (CONFIRMED, Gate B) | Cost/token display zeros on ACP | Zero-usage documented; stream-json retained as SECONDARY engine until upstream ships usage — phase-4 deletion is conditioned on Gate B lifting (review 4, finding 2) |
322
+ | ~~No model/effort switch per session~~ RESOLVED: Gate A PASS | — | `session/set_config_option` verified live; per-session switching, no recycle |
323
+ | Slash commands expanded server-side | Could double-expand with our prompt assembly | Probe F.7. If present: keep our prefixes out of command-looking lines, or ignore server command list |
324
+ | `request_permission` with no pi-side permission UI | Cannot render a native dialog | Single `auto` policy (parity with `skipPermissions: true`); per-tool gating for pi tools belongs to the tools/extensions and is preserved end-to-end by G9 (9.3). Never hang |
325
+ | Human decision latency on parked G9 tool round-trips exceeds the 10m overall-turn cap | Turn killed mid-wait | `AcpDriver` pauses the overall timer while parked, re-armed with REMAINING budget; every park carries its own timeout (9.3) |
326
+ | Binary size (1.5 GB + 117 MB) | Disk + update churn | Pinned install layout (section 12), registry pin by build id |
327
+ | aarch64 TCMalloc startup failure (forum report, Chromebook) | arch-specific | x86_64 unaffected; gate install per-arch, link the forum issue in README |
328
+ | Separate auth state from `agy` CLI | First-run auth friction | `/agy acp-auth` prints exact instructions; `gemini-api-key` path works headless via `GEMINI_API_KEY`. The agent never writes credentials |
329
+ | Thought text changes rendering volume | Noisy transcript | Route through the existing thinking block pipeline, close-on-switch unchanged; no new block types |
330
+ | Two engines drift on behavior | Confusing support surface | Shared provider layer + shared parity suite run against both engines in CI |
331
+
332
+ ## 8. Phase 0: probes and decision gates
333
+
334
+ One authenticated live session answers every UNKNOWN. No product code in this
335
+ phase. Deliverable: findings appended to this doc as section 8.1 (or
336
+ `docs/ACP-PROBE-NOTES.md`), and each gate marked PASS / FALLBACK.
337
+
338
+ Probe scripts (JSON-RPC over stdio, one object per line):
339
+
340
+ ```jsonc
341
+ // P1 handshake with MINIMAL client capabilities (our phase-1 posture)
342
+ {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false}}}
343
+
344
+ // P2 auth (pick one; api-key path is headless)
345
+ // export GEMINI_API_KEY=... first
346
+ {"jsonrpc":"2.0","id":1,"method":"authenticate","params":{"method":"gemini-api-key"}}
347
+
348
+ // P3 session + bridge registration (exact mcpServers param shape is itself a probe item)
349
+ {"jsonrpc":"2.0","id":2,"method":"session/new","params":{"cwd":"/abs/cwd","mcpServers":[{"name":"pi-bridge","type":"http","url":"http://127.0.0.1:<port><path>","headers":[]}]}}
350
+
351
+ // P4 model/effort switch attempts, in order until one works
352
+ {"jsonrpc":"2.0","id":3,"method":"session/set_config_option","params":{"sessionId":"<sid>","configOptionId":"model","value":"<slug>"}}
353
+ {"jsonrpc":"2.0","id":4,"method":"session/set_config_option","params":{"sessionId":"<sid>","configOptionId":"effort","value":"high"}}
354
+ {"jsonrpc":"2.0","id":5,"method":"session/set_mode","params":{"sessionId":"<sid>","modeId":"<id>"}}
355
+
356
+ // P5 prompt: stream a simple prompt; capture every session/update verbatim
357
+ {"jsonrpc":"2.0","id":6,"method":"session/prompt","params":{"sessionId":"<sid>","prompt":[{"type":"text","text":"Say hello and list the files in this directory."}]}}
358
+
359
+ // P6 mid-stream cancel: send during P5, observe prompt response stopReason
360
+ {"jsonrpc":"2.0","id":7,"method":"session/cancel","params":{"sessionId":"<sid>"}}
361
+
362
+ // P7 image prompt (small png data URL), verify the model actually sees it
363
+ {"jsonrpc":"2.0","id":8,"method":"session/prompt","params":{"sessionId":"<sid>","prompt":[{"type":"image","data":"<base64>","mimeType":"image/png"},{"type":"text","text":"What does this image show?"}]}}
364
+
365
+ // P8 session lifecycle: list, resume, load across server restart
366
+ {"jsonrpc":"2.0","id":9,"method":"session/list"}
367
+ {"jsonrpc":"2.0","id":10,"method":"session/resume","params":{"sessionId":"<sid>","cwd":"/abs/cwd"}}
368
+
369
+ // P9 tool exercise: prompt that forces an edit + a command run; capture
370
+ // tool_call / tool_call_update shapes and any session/request_permission
371
+ ```
372
+
373
+ As executed, live param corrections were applied against these planned
374
+ scripts: `methodId` (not `method`), `configId` (not `configOptionId`), and
375
+ mcpServers http entries require `headers: []` (a LIST). See 8.1 and the
376
+ reference doc.
377
+
378
+ Capability posture, fixed for phases 1-3 and for every probe: `fs` false,
379
+ `terminal` false in the initialize client capabilities. Rationale (corrected
380
+ during the skeptical re-check of the peer review): parity, not philosophy.
381
+ agy keeps executing its own tools exactly as it does today; answering `fs/*`
382
+ or `terminal/*` would put file and process execution inside our process (new
383
+ machinery to build, secure, and hold at parity with agy's native behavior)
384
+ for no phase-1 gain. Diff-render and the permission surface stay meaningful.
385
+ This is a decision, not a default; revisit only as a phase 3+ enhancement
386
+ with its own design pass.
387
+
388
+ Concurrency note: the driver design is one active turn per connection (the
389
+ `#active` singleton). No probe exercises a second concurrent session while a
390
+ permission request is pending. Unverified by design, not by omission.
391
+
392
+ What to record for each gate:
393
+
394
+ | Gate | Question | PASS means | FALLBACK (still parity) |
395
+ | --- | --- | --- | --- |
396
+ | A | Model + effort switch per session? | `set_config_option` (or equivalent) switches both; next prompt uses them | NONE viable: the ACP binary takes no model flags, so respawn-per-switch cannot carry a model; a settings.json rewrite per switch is the only theoretical path and is rejected (unproven, racy). Gate A FAIL blocks the phase 4 default flip; ACP stays opt-in; the gap goes upstream |
397
+ | B | Usage anywhere? | Any update or `_meta` carries token counts | Zero-usage documented; README notes it |
398
+ | C | `tool_call` content richness | rawInput/output/locations enough to retire WrapperReplay + native-tools | Keep current replay machinery unchanged |
399
+ | D | Cancel latency + state | `stopReason: cancelled` within ~2s; session reusable after | Keep kill semantics on streaming; on ACP keep kill as escalation path |
400
+ | E | Startup cost | Cold start < 5s, steady RSS sane for a long-lived process | Spawn per turn-group only (like the streaming recycle cadence) |
401
+ | F | MCP `mcpServers` registration | Bridge `tools/list` visible to the agent mid-turn | Keep bridge wired some other protocol-sanctioned way; if none, G9 parking breaks and phase 1 is BLOCKED |
402
+ | F.7 | Slash behavior | Commands not auto-expanded against our prompt text | Prefix-safe prompt assembly unchanged |
403
+
404
+ Also verify in P5 whether permission requests arrive at all with `auto`
405
+ capabilities off, and whether `fs/write_text_file` requests appear for edits
406
+ (they should NOT, with our fs capabilities false; if they do, we answer them
407
+ with real local writes, same trust domain).
408
+
409
+ ### 8.1 Phase 0 findings (COMPLETE, 2026-09-03)
410
+
411
+ Auth completed live: oauth-personal via the BROWSER-capture trick plus an
412
+ `ssh -L` loopback tunnel (the server never surfaces the URL headless; full
413
+ flow documented in [ACP-PROTOCOL-REFERENCE.md](./ACP-PROTOCOL-REFERENCE.md)).
414
+ Run 5 (authenticated, P1-P6) and run 6 (post-restart: load, tools, image,
415
+ Gate F) complete. Raw traffic: `probe-logs/acp-traffic-run5.jsonl` (600
416
+ JSONL lines) and `acp-traffic-run6-restart-load-tools.jsonl` (28 lines);
417
+ conversation store snapshot + schema: `probe-logs/acp-server-conversations/`.
418
+
419
+ | Gate | Verdict | Evidence |
420
+ | --- | --- | --- |
421
+ | A: model/effort switch | **PASS (live)** | `session/set_config_option` `{configId:"model", value:"gemini-3.8-flash-low"}` changed `currentValue`. Effort baked into full slugs; full catalog + default (`gemini-3.7-flash-high`) ships in `session/new` — the `agy models` CLI is not needed on the ACP engine |
422
+ | B: usage | **ABSENT on RC01** | zero token fields in every payload across both runs, including tool flows. Accept zero-usage (existing documented fallback); re-check per build |
423
+ | C: tool_call content | **PASS** | `tool_call`/`tool_call_update` carry kind, status lifecycle, `content` with `{type:"diff", path, newText}` (edits arrive AS DIFFS), `locations`, `rawInput`, optional `rawOutput`. Enough to retire wrapper-replay rendering in phase 3 |
424
+ | D: cancel | **FAIL on RC01** | `session/cancel` → `-32601 Method not found` (verified). Abort fallback **kill + `session/load` VERIFIED WORKING** (run 6): session restored with full history after process death |
425
+ | E: startup cost | **PASS** | initialize ~5-6 s cold (three runs); steady RSS ~327 MB (5 min mixed load) |
426
+ | F: bridge via mcpServers | **PASS (shape)** | `{name, type:"http", url, headers:[]}` accepted; dead URL tolerated at creation (lazy connect). Phase-1 TODO: real bridge `tools/list`+`tools/call` end-to-end (G9) |
427
+
428
+ Additional live-verified facts (details in the reference doc): token
429
+ persistence across restart PASS; `session/load` replays history as FULL-TEXT
430
+ chunks (driver must treat as replay, not live); config does NOT persist
431
+ across restart (driver re-applies model/effort/mode after every restart);
432
+ `session/request_permission` flow verified (options `allow`/`deny`,
433
+ kinds `allow_once`/`reject_once`; answer `{outcome:{outcome:"selected",
434
+ optionId}}`); image prompts PASS end-to-end; `set_mode` and per-session
435
+ `session/close` work; approved tool call may be superseded by another
436
+ toolCallId (track effects, not ids).
437
+
438
+ Schema corrections captured live (all in the reference doc): `authenticate`
439
+ takes `methodId` (not `method`); `set_config_option` takes `configId` (not
440
+ `configOptionId`); `settings.json` is read at server STARTUP only; -32602
441
+ error bodies carry `data.errors[].loc` field paths (use as correction
442
+ oracle). Streaming is pure-delta on live prompts (mid-token chunk splits; no
443
+ cumulative resends). Modes `default`/`auto_edit`/`yolo` map to our
444
+ `mode`/`skipPermissions`. The ACP conversation store is per-session SQLite
445
+ with opaque `steps.step_payload` blobs — same two-phase-write family as
446
+ legacy issue #1; never poll these DBs (re-validated decision).
447
+
448
+ Probe list CLOSED. Phase 0 go/no-go: GO for phase 1, with Gate D's kill+
449
+ reload abort path and the Gate F bridge end-to-end check as phase-1
450
+ acceptance items.
451
+
452
+ ## 9. Target architecture
453
+
454
+ ### 9.1 New modules
455
+
456
+ ```
457
+ src/acp/jsonrpc.ts newline-delimited JSON-RPC 2.0 framing: request/response
458
+ correlation map, concurrent requests, error responses,
459
+ notification + server-request dispatch, malformed-line
460
+ tolerance. Pure transport, no agy knowledge.
461
+ src/acp/connection.ts process lifecycle: spawn AGY_ACP_BIN, initialize
462
+ handshake, authenticate detection (-32000), session/new
463
+ / load / prompt / cancel / set_config_option, client-side
464
+ method handlers (request_permission policy, fs, terminal),
465
+ outbound event queue.
466
+ src/acp/driver.ts AcpDriver: implements the AgyDriver surface (section 9.2)
467
+ so provider.ts keeps working unchanged. State machine,
468
+ timers, parks, snapshot, close.
469
+ src/acp/events.ts session/update to DriverActivity mapping + stopReason
470
+ mapping. Pure functions, unit-tested against captured
471
+ probe transcripts.
472
+ scripts/smoke-acp.mjs live smoke, gated by AGY_ACP_LIVE=1 (spends quota),
473
+ mirrors smoke-stream-json.mjs.
474
+ ```
475
+
476
+ ### 9.2 Driver contract (unchanged surface, additive only)
477
+
478
+ `AcpDriver` matches the existing `AgyDriver` public surface: `state`,
479
+ `activeHandle`, `run()`, `reentry()`, `kickIdle()`, `set onTurnEnd()`,
480
+ `snapshot()`, `close()`. `DriverActivity` gains one additive variant; the
481
+ streaming engine never emits it:
482
+
483
+ ```ts
484
+ // stream-events/driver today: { type: "thought"; tokens: number }
485
+ // ACP addition:
486
+ | { type: "thought"; tokens?: number; delta?: string }
487
+ ```
488
+
489
+ `provider.ts` change: when `delta` is present call `appendThinking`, else keep
490
+ the current token-count behavior. One `if`, additive, streaming untouched.
491
+
492
+ Interface extraction (review 2, finding 7): `provider.ts` imports the
493
+ concrete `AgyDriver` class today; deleting `driver.ts` in phase 4 would break
494
+ the import. Phase 1 extracts a `TurnDriver` interface into a neutral module
495
+ (`src/driver-types.ts`); both drivers implement it; `provider.ts` and
496
+ `ToolRoundTrips` depend on the interface only. `/agy engine` changes require
497
+ a pi restart (driver wiring happens at extension load); a live engine router
498
+ is explicitly deferred as speculative.
499
+
500
+ Teardown contract (review 3, finding 1 — the Gate D kill+reload path needs
501
+ an explicit answer for in-flight JSON-RPC state): when the connection dies
502
+ or is killed, `AcpConnection.abortAll()` rejects every outstanding
503
+ correlation-map entry (the pending `session/prompt` request, any unanswered
504
+ client-side requests), the driver settles the turn outcome as
505
+ `ERROR`/`aborted` (never left pending), and `onTurnEnd` fires so the
506
+ provider's `roundTrips.failAll()` fails parked round-trips exactly as the
507
+ streaming driver's `failTurn` path does. Nothing is ever written to a dead
508
+ socket; no promise survives the kill. Mirrors `ToolRoundTrips.failAll()`.
509
+
510
+ Event mapping (`src/acp/events.ts`):
511
+
512
+ | ACP | DriverActivity | pi mapping (provider.ts, existing) |
513
+ | --- | --- | --- |
514
+ | `agent_message_chunk` | `{type:"text", delta}` | text block deltas |
515
+ | `agent_thought_chunk` | `{type:"thought", delta}` | thinking block deltas |
516
+ | `tool_call` (first) | `{type:"tool_start", name, args}` | buffered; render on completion |
517
+ | `tool_call_update` completed | `{type:"tool_done", name, args, output}` | thinking label / native re-exec / wrapper (gate C) |
518
+ | `tool_call_update` failed | `{type:"tool_error", name, message}` | thinking label |
519
+ | `plan` | (phase 2+) thinking label `[agy plan]` | no pi plan block exists yet |
520
+ | `available_commands_update` | snapshot field only | `/agy doctor` |
521
+ | usage (if found, gate B) | `{type:"usage", usage}` | `toPiUsage` |
522
+ | prompt response `end_turn` | TurnOutcome OK | `stopReason: "stop"` |
523
+ | `cancelled` | TurnOutcome aborted | `stopReason: "aborted"` |
524
+ | `refusal` / `max_turn_requests` / `max_tokens` | TurnOutcome ERROR / OK+note | `error` / `stop` + `errorMessage` |
525
+ | `user_message_chunk` (during `session/load` only) | suppressed (history replay) | never emitted to pi as live text |
526
+
527
+ Defensive guard ported from the streaming engine: if an `agent_message_chunk`
528
+ repeats the full accumulated text instead of a delta, detect and slice (the
529
+ `isCumulativeResend` lesson; same agy binary lineage, same quirk class).
530
+ Guard plus test ship in phase 1. Honest labeling: the ACP spec defines chunks
531
+ as deltas, so on a compliant server the guard stays inert; it earns its ten
532
+ lines because this exact failure mode was observed in the wild on the same
533
+ backend's private protocol, and its steady-state cost is one prefix
534
+ comparison per chunk.
535
+
536
+ Load-replay rules (run 6, verified): `session/load` replays the full
537
+ conversation as `user_message_chunk`/`agent_message_chunk` PAIRS carrying
538
+ FULL TEXT, not the original deltas. `AcpDriver` marks replay chunks as
539
+ history and swallows them; none reach pi as live generation. Two more
540
+ restart rules: config does NOT persist across restart (model/effort/mode are
541
+ re-applied via `set_config_option` after every restart/load), and
542
+ `session/load` takes the same `mcpServers` param as `session/new` — the
543
+ bridge registration is passed on both.
544
+
545
+ ### 9.3 Permission policy (simplified per decision 2026-09-03)
546
+
547
+ pi has no native permission-gate concept; extensions that want gating
548
+ implement it inside their own tools. G9 preserves that end-to-end: a pi tool
549
+ called by agy executes in pi's loop, where the tool's own gating runs. The
550
+ bridge-side `bridge` policy (routing `request_permission` through
551
+ `ask_user_question` with a third round-trip kind and a decision cache) was
552
+ REJECTED by decision: it built permission UX pi does not have, for agy-native
553
+ tools, at real complexity cost. The full design lives in review-3 history if
554
+ upstream ever ships allow-always kinds and the calculus changes.
555
+
556
+ One policy ships:
557
+
558
+ - `auto`: answer `session/request_permission` in-connection with the allow
559
+ option. Byte-for-byte parity with today's `skipPermissions: true`. No
560
+ round-trip, no provider involvement, no third kind; the once-only options
561
+ quirk (run 6) is irrelevant under auto. Hard rule unchanged: the handler
562
+ never hangs and never blocks the `session/prompt` response path.
563
+
564
+ Consequence: `provider.ts` is truly UNCHANGED by adoption (its only additive
565
+ line is the thought-delta `if`). Gate D teardown (9.2) still applies: on
566
+ connection death the unanswered permission request dies with the socket and
567
+ `abortAll()` settles everything.
568
+
569
+ Timer interaction (reviews 1-2, design retained; scope narrowed by the 9.3
570
+ decision to G9 tool parks): the overall-turn timer is armed once per turn and
571
+ only the idle timer pauses while a round-trip is parked. A human answer
572
+ through a parked pi tool (e.g. `ask_user_question`) can legitimately outrun
573
+ 10 minutes. `AcpDriver` pauses the overall timer while any round-trip is
574
+ parked and, on settle, re-arms it with the REMAINING budget (`deadline -
575
+ now`), never a fresh cap: the overall timer is a turn DEADLINE, not an
576
+ inactivity guard. Every park carries its own timeout (`BRIDGE_TIMEOUT_MS`),
577
+ so a paused deadline cannot hang forever. Driver-local (AcpDriver only);
578
+ streaming behavior untouched; ships in phase 1.
579
+
580
+ ### 9.4 Sessions schema (engine-scoped keys)
581
+
582
+ The first design (a per-entry `engine` field) was rejected in review 2:
583
+ `set()` overwrites the single `sid:<pi-session>` key, so one ACP turn ERASES
584
+ the streaming conversation binding, and a rollback loses continuity, breaking
585
+ the section 13 guarantee. Engines scope at the KEY level instead:
586
+
587
+ ```jsonc
588
+ // ~/.pi/agent/antigravity-bridge/sessions.json
589
+ {
590
+ "sid:<pi-session>": { "conversationId": "<streaming id>", "lastStepIdx": -1, "lastMessageCount": 42 },
591
+ "sid:<pi-session>@acp": { "conversationId": "<ACP sessionId>", "lastStepIdx": -1, "lastMessageCount": 42 }
592
+ }
593
+ ```
594
+
595
+ - Un-suffixed keys belong to the streaming engine, byte-compatible with every
596
+ persisted store; ACP keys carry an `@acp` suffix appended by
597
+ `sessionKey()` when `config.engine === "acp"`.
598
+ - Engines never touch each other's keys: rollback keeps both continuations,
599
+ `narrowStoreMap` needs no change (keys pass through untouched), and no
600
+ migration runs.
601
+ - Under ACP the stored id is the ACP sessionId; `session/load` is attempted
602
+ first on resume, our store only remembers which id to load.
603
+
604
+ ### 9.5 Config schema
605
+
606
+ ```jsonc
607
+ // ~/.pi/agent/antigravity-bridge/config.json (additive)
608
+ {
609
+ "engine": "stream-json", // "stream-json" | "acp"; default stream-json
610
+ "acp": {
611
+ "bin": "", // path to agy_acp_server.par; empty = resolve
612
+ "permissions": "auto" // auto-approve request_permission; single policy (9.3)
613
+ }
614
+ }
615
+ ```
616
+
617
+ - Env: `AGY_ENGINE`, `AGY_ACP_BIN`, `AGY_ACP_PERMISSIONS` (file < env, the
618
+ documented precedence pattern).
619
+ - History note: an `engine` key existed before 1.3.2 (sqlite engine) and was
620
+ removed; stale values in old config files are already dropped by narrowing.
621
+ Values narrow to `"stream-json" | "acp"`, anything else falls back to the
622
+ default. The sqlite engine is not coming back.
623
+ - Binary resolution order: `AGY_ACP_BIN` > `acp.bin` > `agy_acp_server.par`
624
+ on PATH > install layout default (section 12). Resolution failure is a
625
+ visible turn error naming what was tried.
626
+
627
+ ## 10. Phase plan
628
+
629
+ ### Phase 0: probes, gates, install, auth (no product code) — ✅ COMPLETE (2026-09-03)
630
+
631
+ - [x] Run probes P1-P9 (section 8). Gate verdicts recorded in 8.1; captured
632
+ protocol in ACP-PROTOCOL-REFERENCE.md; raw traffic in `probe-logs/`.
633
+ - [x] Install layout + pin (section 12): binary at
634
+ `~/.local/opt/agy-acp/<build>/` + `current` symlink + zip.sha256.
635
+ - [x] Auth onboarded: oauth-personal (token persisted, survives restart —
636
+ verified run 6).
637
+ - [x] Deliverable: gates marked — A PASS, B ABSENT (managed), C PASS,
638
+ D FAIL + fallback VERIFIED, E PASS, F PASS-shape. GO for phase 1.
639
+
640
+ ### Phase 1: ACP transport engine, parity only — 🔨 BUILT (2026-09-03), acceptance open
641
+
642
+ Files: new `src/acp/*`, `src/config.ts` (engine + acp block), `src/sessions.ts`
643
+ (engine tag → shipped as engine-scoped KEYS, see 9.4), `extensions/index.ts`
644
+ (engine selection + doctor additions), `src/provider.ts` (driver injection
645
+ point; `createStreamSimple` already takes the driver as a dep; no other
646
+ provider change).
647
+
648
+ - [x] `jsonrpc.ts` + `connection.ts` + `events.ts` + `driver.ts` as specified
649
+ (+ `TurnDriver` extraction to `src/driver-types.ts`).
650
+ - [x] `extractUserPrompt` unchanged (text only in phase 1; images in phase 2).
651
+ - [x] `mcpServers` registration of the bridge server at `session/new` AND
652
+ `session/load` (run 6: load takes the same param; resumed sessions must
653
+ keep bridge access). REGISTERED — live end-to-end still pending, see
654
+ acceptance below.
655
+ - [x] Permission policy locked to `auto` (single policy; no bridge policy, see
656
+ 9.3).
657
+ - [x] `session/cancel` wired to pi abort and `session/close` sent on shutdown
658
+ (pulled up from phase 2 by review 2, finding 6: abort must not kill the
659
+ server process, and shutdown should not leak session state server-side).
660
+ RC01 has no cancel: probe-once → -32601 → teardown+kill+reload, probed
661
+ state shown in doctor.
662
+ - [x] Overall-turn timer pause while parked, REMAINING-budget semantics
663
+ (9.3), covered by tests (park/kickIdle deadline test).
664
+ - [x] Fake-server suites: jsonrpc framing (incl. partial-line buffer), events
665
+ mapping, driver flows (happy, load-replay suppression, permission auto,
666
+ Gate D abort, auth error), engine config narrowing — suite green.
667
+ - [x] `/agy engine acp|stream-json` command + `/agy doctor` shows engine,
668
+ server version, session counts, cancel support.
669
+ - [x] Live smoke through the full stack (`scripts/smoke-acp.mjs`, quota-
670
+ gated): PASS.
671
+ - [x] Round-5 implementation review (agy): P0 framing buffer + 3 P1 + 3 P2
672
+ all fixed.
673
+
674
+ Open acceptance (blocking "phase 1 done"):
675
+
676
+ - [x] Bridge `tools/list` + `tools/call` end-to-end on the ACP engine (Gate F
677
+ live) — VERIFIED 2026-09-03, `scripts/smoke-acp-bridge.mjs`, 3/3 runs:
678
+ real server accepted the registration, listed the bridge catalog, called
679
+ `bridge_echo`, and finished its turn with the result (`ECHO:BRIDGE-E2E-777`).
680
+ The check also found and fixed a real defect: the bridge 403s any request
681
+ without the `x-bridge-token` shared secret, but the ACP registration sent
682
+ `headers: []`. Fix: `McpServerHandle` now exposes `token` (and
683
+ `TOKEN_HEADER` is exported); the extension builds the registration headers
684
+ from the handle. Known cosmetic gap (phase 2): `tool_done.output` carries
685
+ agy's tool title ("Bridge echo call"), not the result content — the model
686
+ DID receive the result; only the activity display lacks it. The pi-side G9
687
+ park/resume is engine-independent (proven live on stream-json); the smoke
688
+ answers deps directly to isolate the agy-side HTTP client, the only part
689
+ no fake server could stand in for.
690
+ - [x] §6 parity checklist executed LIVE on both engines — 2026-09-03,
691
+ `scripts/parity-live.mjs`, 14/14 (7 scenarios × 2 engines). The run
692
+ caught a REAL driver bug: a killed connection's late exit (RC01's
693
+ signal handler intercepts SIGTERM and outlives its replacement)
694
+ clobbered `#conn` and failed the recovery turn with the old stderr.
695
+ Fixed: exit handling is connection-scoped (`stale-connection-exited`
696
+ logged and ignored); regression test reproduces the race
697
+ deterministically via `ACP_FAKE_SLOW_DEATH_MS`.
698
+ - [x] README / DEVELOPMENT / CHANGELOG updates — README (two engines,
699
+ `/agy engine`, `/agy acp-auth`, `AGY_ENGINE`/`AGY_ACP_BIN`, ToS
700
+ wording covers both official binaries), DEVELOPMENT (ACP suites +
701
+ 3 live scripts), CHANGELOG `[Unreleased]` (engine, token fix,
702
+ stale-exit fix, RC01 limitations).
703
+
704
+ Acceptance gate: every section 6 box `[x]` on the ACP engine, or explicitly
705
+ marked with its gate fallback.
706
+
707
+ ### Phase 2: protocol-native wins — ✅ COMPLETE (2026-09-03)
708
+
709
+ Probe evidence: `probe-logs/acp-phase2-traffic.jsonl` + shapes recorded in
710
+ ACP-PROTOCOL-REFERENCE.md "Phase-2 probe findings".
711
+
712
+ - ~~`permissions: "bridge"` policy~~: REMOVED by decision 2026-09-03 (pi has no
713
+ native permission concept; per-tool gating belongs to the tools themselves
714
+ and is preserved by G9; see 9.3).
715
+ - [x] Plan-mode equivalence probe: ACP modes are permission modes only
716
+ (default/auto_edit/yolo) — NO review-only mode exists. The server's
717
+ `/plan` command is intercepted server-side (F.7) and WRITES the plan
718
+ artifact under auto policy. Verdict: plan delegations keep
719
+ `agy -p --mode plan` (the committed exception); do not route mode:plan
720
+ through ACP.
721
+ - [x] Image prompt blocks forwarded from pi content — driver `images`
722
+ param, `connection.prompt` builds typed blocks ahead of the text,
723
+ provider extracts image blocks from the user message, models advertise
724
+ `input: ["text","image"]` on the ACP engine only (engine read at load;
725
+ engine switches require a restart). Verified live: 64x64 two-tone PNG
726
+ through the full driver stack (`scripts/smoke-acp-image.mjs`) and in the
727
+ probe.
728
+ - [x] Thought text deltas through the thinking pipeline — already wired in
729
+ phase 1 (provider renders `thought` deltas into the thinking block);
730
+ probe finding: chunks are SPARSE on RC01 (a step-by-step prompt produced
731
+ zero; reasoning ships as plain message text). No further work justified.
732
+ - [x] `session/cancel` parity — landed in phase 1 (Gate D fallback,
733
+ abort-recover live on both engines).
734
+ - [x] Model/effort switching via the gate A mechanism — PASS (live,
735
+ including on a loaded session; parity scenario).
736
+ - [x] Tool-frame display fixes (found by the probe, shipped with this
737
+ phase): `tool_call_update` output prefers `content[]` text over the
738
+ display-title `rawOutput`; MCP args unwrap the `arguments` envelope;
739
+ tool name prefers `_meta.mcp.tool` over the "<server>_<tool>" title.
740
+ - [x] "Plan updates rendered as thinking labels" — OBSERVED-ABSENT on
741
+ RC01: no `plan` sessionUpdate type exists; artifacts arrive as ordinary
742
+ edit tool_calls (already rendered as cards). Item closed as not
743
+ applicable, not deferred.
744
+ - Acceptance: no parity regression (14/14 re-run after the mapping
745
+ changes); cancel probe done (phase 1); image probe end-to-end (probe +
746
+ live smoke).
747
+
748
+ ### Phase 3: consolidation (immediate priority, opt-in only)
749
+
750
+ Hard rule: Keep both engines in the extension. Default engine remains `stream-json`. All items below are non-breaking opt-in enhancements for the ACP engine that do NOT alter default behavior or touch streaming paths.
751
+
752
+ Queue:
753
+ 1. [x] Gate C evaluation: `native-tools.ts` + `WrapperReplay` retired on the ACP engine (retained for the streaming (`stream-json`) engine). ACP turns emit thinking labels for tool executions without parking turns for synthetic re-execution or wrapper cards; bridge MCP tools (`bridge_call`) continue parking natively. Verified: live parity 14/14 held.
754
+ 2. [x] Retire `diff-render.ts` (git path) on the ACP engine — DONE 2026-09-04. The phase-2 probe proved ACP surfaces edit diffs natively (`tool_call` `content[]` `{type:"diff", path, oldText?, newText}`), which is richer AND cheaper than the git-sourced path: implemented as (a) `events.ts` extracts the first diff entry into the `tool_done` activity (`AcpEditDiff`); (b) `provider.ts` renders it on ACP turns via the new `formatInlineDiff` (in-memory `generateDiffString`, same line-numbered format, ZERO git subprocesses) instead of `TurnDiffContext.diffEdit`; (c) `diff-render.ts` git machinery remains solely for the `stream-json` engine. Tests: diff extraction (with/without `oldText`), ACP thinking-stream rendering (`+2 B` line-numbered), label-only fallback, no parking, streaming path unchanged. Verified: 165/165 vitest, tsc clean, live parity 14/14.
755
+ 3. [x] `/agy doctor` diagnostics expansion — DONE 2026-09-04. The ACP
756
+ snapshot now carries `reconnects` (connections beyond the first =
757
+ Gate D kills + stale-exit replacements) and the handshake `agentInfo`
758
+ name/title; `/agy doctor` surfaces both (reconnects in the stats line,
759
+ agent title next to the server version). Regression test pins
760
+ `reconnects = 1` on the stale-exit flow.
761
+ 4. [x] G1 digest via `embeddedContext` resource blocks — DONE 2026-09-04
762
+ (the optional enhancement, adopted as the ACP delivery path). On the
763
+ ACP engine the digest ships as a native `{type:"resource"}` block in
764
+ the prompt array (`connection.prompt` builds images → resource →
765
+ text) instead of inline prompt text; `stream-json` keeps the inline
766
+ default. Verified live: a resource block with a secret word was read
767
+ and answered correctly by RC01 (`promptCapabilities.embeddedContext`
768
+ is functional, not just advertised).
769
+
770
+ Acceptance: parity suite remains 14/14, doctor parity, no breakage to stream-json default.
771
+
772
+ Status: 🚧 Phase 3 remaining items are COMPLETE (2026-09-04): 168/168 tests,
773
+ tsc clean, live parity 14/14, embeddedContext verified live (resource block
774
+ with a secret word answered correctly). Remaining phase-3 acceptance:
775
+ deleted-code census lands with phase 4's deletions (nothing deletes in
776
+ phase 3 — the streaming modules stay for the `stream-json` engine).
777
+
778
+ ### Phase 4: default flip and streaming deletion (deferred, next month)
779
+
780
+ All items below are deferred until a full soak cycle of both engines has completed and upstream conditions are met:
781
+
782
+ 1. AskAntigravity migration to ACP one-shot (migrating while `stream-json` is default breaks streaming-only users who haven't onboarded ACP auth; streaming conversation ids cannot resume under ACP). `mode: "plan"` delegations keep the `agy -p --mode plan` path (committed exception).
783
+ 2. Delete `src/discovery.ts` (`/proc` fd-scan) once AskAntigravity migration completes.
784
+ 3. Default flip: `config.engine` default becomes `"acp"` after one full release soak cycle with both engines shipping. Gate A PASS is verified.
785
+ 4. Upstream Gate B resolution: Google ships usage counters in ACP payloads.
786
+ 5. Streaming engine deletion: `src/driver.ts`, `src/stream-events.ts`, and streaming tests deleted one release after the flip AND only once Gate B has lifted (so zero-usage is never forced).
787
+
788
+ Acceptance: default-flip release ships with the parity suite as acceptance evidence; deletion release has zero references to the removed modules.
789
+
790
+ ## 11. Test strategy
791
+
792
+ - **Framing** (`jsonrpc.ts`): correlation, concurrency, error responses,
793
+ notifications vs requests, malformed lines, backpressure. Pure unit tests.
794
+ - **Mapping** (`events.ts`): every session/update variant from captured probe
795
+ transcripts to DriverActivity; stopReason table; unknown-variant tolerance
796
+ (the `parseAgyLine` lesson, re-applied).
797
+ - **Driver** (`driver.ts`): scripted fake ACP server (a tiny stdio JSON-RPC
798
+ node script under `tests/helpers/`) covering: handshake, session/new fail
799
+ auth, streaming prompt, park suspends idle timer, kickIdle rearm, cancel
800
+ mid-stream, connection death mid-turn, recycle fallback path.
801
+ - **Provider**: the existing `provider-streaming` / `provider-digest` /
802
+ `provider-sysprompt` suites run against both engines via the shared driver
803
+ contract. New case: thought delta path.
804
+ - **Sessions**: engine-tag matching, streaming-file compatibility (no tag),
805
+ cross-engine miss starts fresh.
806
+ - **Config**: narrowing of stale/garbage `engine` values, env precedence.
807
+ - **Parity suite**: the section 6 checklist encoded as an integration run
808
+ against the fake server for both engines; live parity runs stay
809
+ `AGY_ACP_LIVE=1` gated (quota).
810
+
811
+ ## 12. Binary management, pinning, auth onboarding
812
+
813
+ Install layout:
814
+
815
+ ```
816
+ ~/.local/opt/agy-acp/<build>/
817
+ agy_acp_server.par
818
+ localharness_external
819
+ ~/.local/opt/agy-acp/current -> <build> # symlink; AGY_ACP_BIN default
820
+ ```
821
+
822
+ - Pin by build-stamped registry URL (`...20260818_01_RC01-...zip`), not by
823
+ the moving registry index. The registry updates hourly; a float could
824
+ change the engine under a running install mid-release.
825
+ - The registry publishes no checksums. Record the zip SHA-256 at install time
826
+ in the install dir for drift detection. Propose an upstream checksum
827
+ request (issue) once we depend on this.
828
+ - `--uid=` from the linux registry entry: the server ran without it here.
829
+ Treat as a launcher artifact; revisit if a future build refuses to start.
830
+ - Auth onboarding: `/agy acp-auth` prints the four methods with exact
831
+ `settings.json` shapes (source: official Zed docs, section 2.3). Headless
832
+ path: `GEMINI_API_KEY` + `auth.type: "gemini-api-key"`. The agent NEVER
833
+ writes or reads credentials; it prints instructions and verifies by
834
+ re-running `session/new` until the error clears.
835
+
836
+ ## 13. Rollback
837
+
838
+ At any phase: `AGY_ENGINE=stream-json` (or `/agy engine stream-json`) returns
839
+ to the streaming engine. Sessions are keyed per engine (9.4), so the streaming
840
+ engine resumes its own conversations untouched. The streaming engine is only deleted in
841
+ phase 4, one release after the flip, so rollback stays possible throughout.
842
+
843
+ ## 14. Documentation updates
844
+
845
+ | Doc | Change | Phase |
846
+ | --- | --- | --- |
847
+ | `README.md` | engine section, binary install, auth, arch note | 1, 4 |
848
+ | `docs/ARCHITECTURE.md` | ACP engine section alongside stream-json | 1 |
849
+ | `docs/DEVELOPMENT.md` | smoke-acp, fake server, probe workflow | 1 |
850
+ | `docs/ACP-ADOPTION-PLAN.md` | this file; gate results appended | 0 |
851
+ | `docs/PI-BRIDGE-GAPS.md` | G1 note: per-tool gating stays extension-owned; G9 preserves it under ACP | 1 |
852
+ | `CHANGELOG.md` | per phase, house style | each |
853
+
854
+ ## 15. Risk register
855
+
856
+ | # | Risk | L | I | Response |
857
+ | --- | --- | --- | --- | --- |
858
+ | R1 | Gate A fails (no model switch) | M | H | default flip blocked; ACP stays opt-in; escalate upstream. No ship-anyway: model switching is not optional |
859
+ | R2 | Gate F fails (bridge not registrable) | L | H | BLOCKS phase 1; escalate upstream, keep streaming |
860
+ | R3 | Usage absent (CONFIRMED, Gate B) | H | M | zero-usage documented on ACP; stream-json retained as secondary engine; phase-4 deletion conditioned on Gate B lift (review 4) |
861
+ | R4 | Binary churn (hourly registry, RC builds) | M | M | pin by build id, record sha256 |
862
+ | R5 | Server rejects second simultaneous session | L | M | one session per connection (today's model), N connections instead |
863
+ | R6 | Thought text floods transcript | M | L | thinking pipeline unchanged, same close-on-switch |
864
+ | R7 | ToS posture shifts again | L | H | official channel is Google's own; monitor registry + FAQ |
865
+ | R8 | aarch64 breakage | L | L | per-arch gating |
866
+ | R9 | ~~`bridge` permission throughput~~ removed with the bridge policy (9.3) | - | - | n/a |
867
+
868
+ ## 16. Open questions
869
+
870
+ CLOSED 2026-09-03 — every item was answered by the Phase 0 probes (section
871
+ 8.1, [ACP-PROTOCOL-REFERENCE.md](./ACP-PROTOCOL-REFERENCE.md)). Notable
872
+ answers: model catalog ships in `session/new` (CLI catalog unnecessary on
873
+ ACP); edits surface as diffs inside `tool_call` content even with fs
874
+ capabilities off; no usage fields anywhere; available_commands_update carries
875
+ `plan` and `logout`; cold start ~5-6 s, steady RSS ~327 MB.