Skip to content

Add Antigravity (agy) provider via an ACP compatibility bridge#4497

Open
agentool wants to merge 9 commits into
pingdotgg:mainfrom
agentool:antigravity-provider
Open

Add Antigravity (agy) provider via an ACP compatibility bridge#4497
agentool wants to merge 9 commits into
pingdotgg:mainfrom
agentool:antigravity-provider

Conversation

@agentool

@agentool agentool commented Jul 25, 2026

Copy link
Copy Markdown

Adds antigravity as a provider driver alongside Codex, Claude, Cursor, Grok and OpenCode.

Up front, re: CONTRIBUTING: this is large (+3442) and unsolicited, and I did not open an issue first. Opening it anyway as a working reference rather than a request for your time — if the answer is no, close it and it costs you nothing. Notes on scope and on splitting it are at the bottom.

Why this needs a bridge rather than another ACP driver

Antigravity ships no native agent protocol. Verified against agy 1.1.7:

  • agy agent stdiounexpected arguments: [stdio] (Grok's entrypoint is grok agent stdio)
  • no --acp flag; the binary carries jsonrpc2 and MCP, but no ACP methods
  • Antigravity.app/Contents/Resources/bin/language_server speaks Codeium gRPC (exa.language_server_pb), not ACP

So provider/acp/antigravity/agyBridge.ts presents the ACP surface AcpSessionRuntime already expects, and runs each turn through the documented non-interactive agy --print. It ships as hidden subcommands of the server binary (t3 agy-acp, t3 agy-hook) so it versions with the server and adds no build artifact.

How the event stream is reconstructed

Two documented Antigravity sources correlate on step index — a hook's stepIdx is the transcript record's step_index:

Signal Source
Tool start, name, args, human title (toolSummary) PreToolUse hook
Tool output (real stdout / file contents) transcript record with matching step_index
Tool completed/failed PostToolUse hook
Assistant text, streamed during the turn PLANNER_RESPONSE records as they land
conversationId for resume present on every hook

Neither source alone is sufficient: hooks never carry tool output, the transcript never carries tool arguments.

Three things that cost real debugging time

--add-dir replaces the workspace, it does not extend cwd. Hooks are registered via a throwaway --add-dir workspace so nothing is written into the user's repo — but passing only that dir makes the project invisible to the agent. Both the session cwd and the hook workspace are passed.

Edit diffs must be captured inside the hook process. Both hooks for a fast edit land within one 50ms poll, so reading the file while draining hook output makes "before" read back as "after". The hook brackets the tool call and is the only place that sees true pre-edit state.

Probes must set stdin: "ignore". agy starts a language server and emits nothing while stdin stays open; ChildProcess defaults stdin to "pipe", which hangs it. Presented as installed: true, models: 0 while agy models worked by hand.

Known constraints

All inherent to print mode, documented in AGENTS.md:

  • tools auto-approve (--dangerously-skip-permissions); no approval flow is possible
  • no image attachments, no session modes
  • model binds at spawn, so sessionModelSwitch is "unsupported"
  • assistant text streams per planner step, not per token

The trajectory JSONL format is undocumented. Every parse path degrades to emitting nothing rather than failing a turn, so an agy release that changes it costs observability, not correctness.

UI surface

No new components. The provider renders through the existing rows: PROVIDER_OPTIONS, PROVIDER_ICON_BY_PROVIDER, and PROVIDER_CLIENT_DEFINITIONS, reusing the AntigravityIcon already in Icons.tsx for the editor integration, with an Experimental badge in the same slot Cursor and Grok use for Early Access. CONTRIBUTING asks for before/after images on UI changes — I don't have a clean way to attach them here; the visible delta is one additional provider row.

Verification

  • 1076 tests pass (6 skipped), including 39 new across agyEvents, agyTranscript, AntigravityAcpSupport
  • 0 type errors, 0 lint errors, format clean
  • pnpm build clean; bridge bundles into dist/bin.mjs and all three entrypoints work off the built binary
  • live probe: status: ready | version: 1.1.7 | models: 11
  • a full turn driven through the built binary against real agy: tool calls, real tool output, a file edit with diff, stopReason: end_turn, file on disk actually changed

Three ProviderRegistry fixtures assert an exact provider list or that only the subject provider probes, so they gain the new driver.

On size

Most of the diff is AntigravityAdapter.ts (~690 lines), which follows the existing convention of one standalone adapter per provider (Cursor 1.2k, Grok 1.5k, Claude 3.9k) over the shared ACP helpers. I deliberately did not refactor a shared adapter core out of GrokAdapter, since that would mean touching a working provider to land a new one.

Happy to split this into (1) contracts + bridge + tests and (2) driver + UI wiring, or to drop the text-generation backend, if either would make it reviewable. Equally happy for you to take the bridge approach and reimplement it your way — the --add-dir, hook-timing and stdin findings above are the parts that were expensive to discover.


Note

High Risk
Large new provider surface (~3.4k LOC) spanning tool approval gating, external CLI execution with --dangerously-skip-permissions, and complex cancel/steer concurrency; mistakes could allow unapproved tools or strand turns in the UI.

Overview
Introduces antigravity as a built-in provider because the Antigravity CLI has no native agent protocol. Sessions talk to a bundled ACP bridge (t3 agy-acp over stdio) that runs each turn with agy --print, while t3 agy-hook observes tool lifecycle via Antigravity hooks registered from a throwaway workspace (session cwd plus hook dir — --add-dir replaces rather than extends).

Live streaming is rebuilt by correlating hook stepIdx with transcript step_index: hooks announce tools and completion; transcript.jsonl supplies assistant text and real tool output. The adapter wires permissions through PreToolUse when requireToolApproval is on (fail-closed), sends attachments as resource_link + staged paths, supports in-session model switches on the next spawn, and rejects rollback when the trajectory cannot be truncated server-side.

Contracts and UI add AntigravitySettings, model defaults/aliases, registry/tests updates, settings/picker icons, attachmentFileUrl, and AntigravityTextGeneration for git/thread helpers. AGENTS.md documents bridge constraints (stdin ignore on probes, print-mode limits).

Reviewed by Cursor Bugbot for commit d87551f. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add Antigravity (agy) provider via an ACP compatibility bridge

  • Adds a new antigravity provider driver that integrates with the agy CLI via an ACP (Agent Communication Protocol) compatibility bridge, running agy in print mode as a subprocess
  • Introduces AntigravityAdapter.ts with full session lifecycle: start/stop, per-turn prompt sending with attachments, tool approval mediation with an 11-minute timeout, interruption, per-turn model switching, and event streaming via PubSub
  • Implements AntigravityAcpSupport.ts which spawns the ACP bridge as a server sub-process, passing configuration and per-turn parameters (model, effort, timeout, approval gating) via environment variables
  • Adds agy-acp and agy-hook hidden CLI subcommands to the server binary to handle ACP bridge and hook lifecycle events
  • Streams tool call events and assistant text by tailing agy's transcript JSONL file and processing hook callbacks, translating them into ACP-shaped updates
  • Adds Antigravity-backed text generation for commit messages, PR content, branch names, and thread titles with a 180s timeout
  • Surfaces the provider in settings UI, the session provider picker (with a 'new' badge), and the provider icon map
  • Risk: the bridge depends on the agy CLI being installed and accessible; missing binary degrades to a warning status with no models available

Macroscope summarized d87551f.

Antigravity ships no native agent protocol: `agy agent stdio` is not a
thing, and the bundled `language_server` speaks Codeium gRPC rather than
ACP. So this adds a bridge that presents the ACP surface
`AcpSessionRuntime` needs and runs each turn through the documented
non-interactive `agy --print`.

The bridge ships as hidden subcommands of the server binary (`t3 agy-acp`,
`t3 agy-hook`) so it always versions with the server and needs no separate
artifact.

A live event stream is reconstructed from two documented Antigravity
sources that correlate on step index (a hook's `stepIdx` is the transcript
record's `step_index`):

  - Hooks (PreToolUse/PostToolUse/Stop) supply tool name, arguments and
    completion status. They are registered through a throwaway `--add-dir`
    workspace, so nothing is written into the user's repository. Both the
    session cwd and that hook workspace are passed, because `--add-dir`
    replaces the workspace rather than extending cwd -- passing only the
    hook dir hides the project from the agent.
  - The trajectory transcript supplies assistant text and real tool output
    as the turn runs. Its format is undocumented, so every parse path
    degrades to emitting nothing rather than failing a turn.

File contents for an edit diff are captured inside the hook process. The
bridge cannot read them when it drains hook output: both hooks for a fast
edit land within a single poll interval, by which point "before" would
read back as "after".

Any `agy` subcommand spawned for a probe sets `stdin: "ignore"`. `agy`
starts a language server and emits nothing while stdin stays open, so the
default "pipe" hangs it and model discovery returns empty.

Known constraints, all inherent to print mode and documented in AGENTS.md:
tools auto-approve (`--dangerously-skip-permissions`) so no approval flow
is possible; no image attachments; no session modes; the model binds at
spawn, so `sessionModelSwitch` is "unsupported".

The three ProviderRegistry fixtures assert an exact provider list or that
only the subject provider probes, so they gain the new driver.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c994ac1-0a7a-44af-b987-6cf01764799f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 25, 2026
Comment thread apps/server/src/provider/acp/antigravity/agyBridge.ts
Comment thread apps/server/src/provider/acp/antigravity/agyBridge.ts
Comment thread apps/server/src/provider/acp/antigravity/agyBridge.ts
Comment thread apps/server/src/provider/acp/antigravity/agyBridge.ts
Comment thread apps/server/src/provider/Layers/AntigravityAdapter.ts
- session/load no longer treats a UUID-shaped session id as an Antigravity
  conversation id. Bridge session ids are random UUIDs themselves, so the
  fallback could not tell them apart and resumed a conversation that never
  existed whenever the session map was missing. The map is now the only
  authority; an unknown id starts fresh.

- The transcript file is pinned for the turn. transcript.jsonl appearing
  after streaming began from transcript_full.jsonl used to switch files
  while keeping the old byte offset, misaligning reads or re-emitting
  records already streamed.

- Tool call bookkeeping survives PostToolUse. Hooks are drained before the
  transcript, so for a fast tool both arrive in one poll; deleting the step
  on completion dropped the real output permanently, with no fallback since
  post hooks carry no tool body. Records are now marked completed and the
  exit sweep only fails the ones still open.

- session/cancel only records a cancellation for the turn actually running.
  Cancels bypass the request queue, so one arriving after a turn finished
  stayed set and made the next successful turn report stopReason cancelled.

- The adapter validates the prompt before emitting turn.started, so an
  attachment-only or whitespace-only prompt no longer leaves a turn that
  never completes in the UI.
Comment thread apps/server/src/provider/Layers/AntigravityAdapter.ts
Comment thread apps/server/src/provider/acp/antigravity/agyBridge.ts Outdated
Comment thread apps/server/src/provider/acp/antigravity/agyBridge.ts Outdated
Comment on lines +448 to +453
).pipe(
Effect.catch((cause) =>
Effect.logError("Failed to process Antigravity runtime notification.", { cause }),
),
Effect.forkChild,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High Layers/AntigravityAdapter.ts:448

The notification drain fiber is forked with Effect.forkChild, which attaches it to the Effect.scoped startSession workflow's scope. When startSession returns, that scope closes and immediately interrupts the notification fiber — even though sessionScope was created separately and transferred into ctx. As a result, the live session stops forwarding all subsequent ACP content, tool, and plan events. Fork the drain into sessionScope via Effect.forkIn(ctx.scope) instead of Effect.forkChild.

Suggested change
).pipe(
Effect.catch((cause) =>
Effect.logError("Failed to process Antigravity runtime notification.", { cause }),
),
Effect.forkChild,
);
).pipe(
Effect.catch((cause) =>
Effect.logError("Failed to process Antigravity runtime notification.", { cause }),
),
Effect.forkIn(ctx.scope),
);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/AntigravityAdapter.ts around lines 448-453:

The notification drain fiber is forked with `Effect.forkChild`, which attaches it to the `Effect.scoped` `startSession` workflow's scope. When `startSession` returns, that scope closes and immediately interrupts the notification fiber — even though `sessionScope` was created separately and transferred into `ctx`. As a result, the live session stops forwarding all subsequent ACP content, tool, and plan events. Fork the drain into `sessionScope` via `Effect.forkIn(ctx.scope)` instead of `Effect.forkChild`.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change — this matches GrokAdapter, and live streaming is verified working end to end.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/server/src/provider/Layers/AntigravityAdapter.ts
Comment thread apps/server/src/provider/acp/antigravity/agyBridge.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

4 blocking correctness issues found. This PR adds a substantial new provider integration (~4700 lines) with complex session management, process coordination, and tool approval workflows. Multiple high-severity bugs have been identified in review comments affecting cancellation behavior, event forwarding, and resource cleanup that require resolution before merging.

You can customize Macroscope's approvability policy. Learn more.

@kridaydave

Copy link
Copy Markdown
Contributor

Bro ... Read contributors md :/

agentool added 3 commits July 25, 2026 18:29
Round-two fixes from automated review:

- Attachments are staged into a per-turn temp directory instead of adding
  the attachment store to the workspace, which would have handed an
  auto-approving agent read access to every thread's uploads.
- Resumed conversations no longer replay history. One conversation keeps a
  single append-only transcript and reading starts at byte 0, so the first
  batch is trimmed to what follows the last USER_INPUT record — verified
  against real two-turn transcripts.
- The session map is written through a temp file and renamed. It is now the
  sole resume authority, and a torn write read back as empty JSON would
  silently start a fresh conversation.
- Non-string values in that map are discarded rather than cast, and an
  unhandled handler failure now answers the request instead of leaving the
  client blocked forever.
- Turn setup releases its claim and removes its temp directories if it
  throws, and a failed turn clears the public session's active turn id.
- File URIs go through pathToFileURL, which escapes characters that would
  otherwise truncate the path on the way back.

Two limitations removed after checking the CLI rather than assuming:

- Model switching works. `--model` composes with `--conversation`: a resumed
  conversation answers on the new model with its history intact, so the
  bridge takes `session/set_model` and the capability is now "in-session".
- Attachments work. `agy --print` has no attachment flag, so files travel as
  `resource_link` blocks and the bridge names the staged paths in the prompt.
Print mode cannot prompt, so `agy` has to run with permissions skipped.
That does not mean tools must be auto-approved: a PreToolUse hook returning
`{"decision":"deny"}` genuinely blocks the call and Antigravity reports the
reason back to the model. Verified against the CLI before building on it.

The hook process becomes the gate. It writes its event, then blocks polling
for a decision file. The bridge — which is the ACP agent — issues
`session/request_permission` to T3 Code, and writes the answer out when the
user replies. The adapter registers the matching callback and resolves it
from `respondToRequest`, the same path the other ACP providers use.

Fails closed everywhere it can: a cancelled prompt, an unknown option, a
malformed reply, a lost bridge, or a timeout all deny rather than allow.
Antigravity's own hook timeout is set above the bridge's wait so the deny
lands rather than the CLI abandoning the hook first.

Off by default via `requireToolApproval`, because every approval blocks the
turn until a human answers.

Verified end to end against the real CLI through the built bundle: asked to
overwrite a file, the bridge raised two permission requests (run_command,
then write_to_file), both were rejected, and the file was unchanged.
More findings from automated review:

- Tool output no longer arrives after the call is completed. Hooks and the
  transcript are drained in one pass, so for a fast tool the completion and
  its output were emitted back to back; AcpSessionRuntime drops tool state on
  completion, so the output resurfaced as a second, never-completed item.
  Terminal updates are now held until that step's transcript record has been
  streamed, or until the final drain if none arrives.

- Session-to-conversation mappings are one file per session. A shared map
  meant a read-modify-write, so two bridge processes finishing turns at once
  would each rebuild it from a stale snapshot and the later write would drop
  the other's entry, losing a thread's history. Atomic rename alone did not
  fix that; independent files do not contend at all.

- Terminal turn events are published from exactly one place. The old success
  path tested `promptsInFlight === 1` and then yielded before publishing, so
  a steer arriving in that window could make two prompts each believe they
  owned the turn and emit a completion. Decrement and the last-prompt test
  are now a single synchronous step.

- A model switch is applied before `turn.started`, so the announced model is
  the one the turn runs on rather than the previous one.

- Non-`file:` resource links are rendered into the prompt for Antigravity's
  fetch tool instead of being dropped, and an attachment that cannot be
  staged now fails the turn — silently dropping it would run the turn against
  a prompt the user did not write.
Comment thread apps/server/src/provider/Layers/AntigravityAdapter.ts
Comment thread apps/server/src/provider/Layers/AntigravityAdapter.ts
Comment thread apps/server/src/provider/Layers/AntigravityAdapter.ts Outdated
Comment thread apps/server/src/provider/Layers/AntigravityAdapter.ts
Comment thread apps/server/src/provider/Layers/AntigravityAdapter.ts
Comment thread apps/server/src/provider/acp/antigravity/agyBridge.ts Outdated
Comment thread apps/server/src/provider/acp/antigravity/agyBridge.ts
Comment thread apps/server/src/provider/acp/antigravity/agyBridge.ts
Comment thread apps/server/src/provider/acp/antigravity/agyBridge.ts
Comment thread apps/server/src/provider/Layers/AntigravityAdapter.ts
Comment thread packages/contracts/src/settings.ts
agentool added 4 commits July 25, 2026 19:20
Final review round, mostly against the approval gate added in the previous
commit:

- The gate no longer fails open. A hook that could not parse its payload or
  write its event file fell through to the observation-only response, which
  answers "allow" — so a full disk or an unwritable directory ran the tool
  with nobody having approved it. Every PreToolUse failure now denies; only a
  validated decision allows.

- Cancelling stops prompts that have not started yet. Turns are serialized,
  so a steer queued behind a long turn used to begin executing tools after
  the user pressed Stop. Cancels are counted per session and each prompt
  records the count when it is accepted, which invalidates queued prompts
  without reviving the stale-cancel bug a plain flag caused.

- Approval requests carry a deadline and are settled on interrupt, so a
  client that never answers cannot pin an entry for the life of the process,
  and a cancelled turn releases the hook waiting behind it.

- No approval is opened during the final drain: the child has already exited,
  so nothing is waiting on the answer.

- A tool whose transcript output arrived before its PostToolUse hook is
  completed immediately rather than at end of turn. The transcript is read
  once by byte offset, so that record never comes round again.

- A failed model switch clears the active turn instead of leaving one
  advertised to listSessions and the reaper; staging cleans up copied
  uploads if a later copy fails; the approvals map is registered only after
  startup succeeds.

Re-verified end to end through the built bundle: reject path blocks the tool
and leaves the file untouched; approve path runs it and emits
in_progress -> content -> completed.
…e bugs

`requireToolApproval` now defaults to on. `agy` runs with permissions
skipped either way, so the hook is the only thing between the model and a
tool; a gate that has to be discovered protects nobody.

Fixes from the latest review round:

- "Allow for this session" no longer denies the tool. The bridge advertised
  only allow_once/reject_once, so the client mapped that choice to no option
  id, the reply read as cancelled, and the gate denied — the button did the
  opposite of what it said. An allow_always option is offered and honoured
  for the rest of the session.

- `requireToolApproval` was missing from AntigravitySettingsPatch, so the
  setting was stripped on every settings update and could never be changed
  from its default.

- `requiresNewThreadForModelChange` was still true, which kept the UI from
  offering the in-session model switch the adapter had just gained.

- A resumed transcript whose current-turn USER_INPUT had not been written yet
  could replay the previous turn's trailing records as live output. Those
  lines are now retained and re-examined instead of emitted.

- Transcript bytes are decoded through a streaming decoder. A write landing
  mid-multibyte-character used to be replaced with U+FFFD and skipped,
  corrupting non-ASCII assistant text and tool output.

- Two concurrent sendTurns could both read promptsInFlight as zero and open
  rival turns; the id is now minted before the read so claiming is atomic.

- Turn settlement catches defects, not only typed failures, and cancelling an
  already-settled approval no longer aborts teardown.

Verified end to end through the built bundle: reject blocks the tool and
leaves the file untouched; allow and allow-for-session both run it and emit
in_progress -> content -> completed.
- A cancel now stops prompts already accepted but still queued upstream. The
  bridge's own generation check was too late: the ACP runtime serializes
  prompts behind a semaphore, so a steer could still be waiting there when
  Stop was pressed, reach the bridge after the cancel, capture the
  post-cancel state and run anyway. The adapter records a cancel epoch when
  it accepts a prompt and rechecks it immediately before submitting.

- `turn.started` is claimed from shared per-turn state rather than from
  "am I a steer". A steer assumed the prompt it folded into had already
  announced the turn, which is false if that prompt failed during preflight —
  it would then emit content and a completion for a turn nobody started.

- The adapter's approval handler races its own deadline and cleans up in an
  `ensuring`. The bridge forgetting a timed-out request only freed its side;
  this one stayed blocked on the deferred with its UI prompt open, and those
  accumulated across turns. An unanswered request resolves to cancel, which
  denies.

- `approvalsByThread` is registered after the session, so an interrupted
  startup cannot leave an entry with no context to clean it up, and
  `createHookWorkspace` removes its directory if setup throws — the caller
  never receives the path and cannot do it.

Re-verified end to end: reject blocks the tool, allow runs it with
in_progress -> content -> completed.
- The approval gate answers on every gating path. A PreToolUse payload
  carrying no `toolCall` still fell through to the observation-only response,
  which allows — so a change in Antigravity's payload shape would have run
  tools unapproved. Missing or unidentifiable tool calls now deny.

- Resumed conversations use a byte baseline taken before `agy` starts instead
  of inferring the turn boundary from the last USER_INPUT record. The
  transcript always holds earlier USER_INPUT records, so a poll landing
  before the new one was written treated a previous turn's output as live.
  The conversation's log directory is derivable from its id, so the boundary
  can be measured rather than guessed; the marker heuristic remains as a
  fallback when the file cannot be read.

- The turn id is bound in the same synchronous step as the prompt claim. A
  concurrent call could otherwise observe promptsInFlight > 0 with no active
  id yet and open a rival turn.

- Turn settlement reads shared per-turn state rather than the settling call's
  own flag, and a turn is marked started only after the event is actually
  published. Otherwise a steer that became the last outstanding prompt could
  drop the completion, or emit one for a turn nobody saw start.

Known and unfixed, both structural: a steer can still pass the adapter's
cancel check and then wait on the ACP runtime's prompt semaphore, reaching
the bridge after a cancel; and an interrupted startup between scope transfer
and the first published event leaves the session registered. Both live in
shared runtime code this provider only borrows.
try {
const stats = NodeFS.statSync(transcriptPath);
if (stats.size > input.transcriptOffset.value) {
const fd = NodeFS.openSync(transcriptPath, "r");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium antigravity/agyBridge.ts:865

drain advances transcriptOffset.value to stats.size and decodes the entire buffer regardless of how many bytes NodeFS.readSync actually returned. On filesystems where readSync returns fewer than length bytes, the unread tail of the buffer is zero-filled and decoded as transcript content, while the real unread bytes are permanently skipped — corrupting or silently losing transcript records and tool output. Advance the offset and decode only the actual bytesRead, looping until the requested range is fully consumed.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/antigravity/agyBridge.ts around line 865:

`drain` advances `transcriptOffset.value` to `stats.size` and decodes the entire buffer regardless of how many bytes `NodeFS.readSync` actually returned. On filesystems where `readSync` returns fewer than `length` bytes, the unread tail of the buffer is zero-filled and decoded as transcript content, while the real unread bytes are permanently skipped — corrupting or silently losing transcript records and tool output. Advance the offset and decode only the actual `bytesRead`, looping until the requested range is fully consumed.

}).pipe(Effect.scoped),
);

const sendTurn: AntigravityAdapterShape["sendTurn"] = (input) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/AntigravityAdapter.ts:656

sendTurn calls requireSession up front but then yields during attachment resolution and model switching without holding the per-thread lock or rechecking ctx.stopped. A concurrent stopSession can delete the session and close ctx.scope during that window, yet sendTurn continues to publish turn.started and call ctx.acp.prompt on the closed runtime, emitting turn events after session.exited. Serialize sendTurn under withThreadLock so teardown cannot run concurrently, or recheck ctx.stopped before publishing and submitting.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/AntigravityAdapter.ts around line 656:

`sendTurn` calls `requireSession` up front but then yields during attachment resolution and model switching without holding the per-thread lock or rechecking `ctx.stopped`. A concurrent `stopSession` can delete the session and close `ctx.scope` during that window, yet `sendTurn` continues to publish `turn.started` and call `ctx.acp.prompt` on the closed runtime, emitting turn events after `session.exited`. Serialize `sendTurn` under `withThreadLock` so teardown cannot run concurrently, or recheck `ctx.stopped` before publishing and submitting.

// stdin closed: no approval can still be answered, so unblock the hooks
// waiting on them (they fail closed) rather than letting them time out.
failPendingOutbound("T3 Code disconnected before approving this tool call");
await queue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High antigravity/agyBridge.ts:1333

On stdin disconnect, runAgyBridge calls await queue before killing activeChild, so the bridge blocks until the in-flight session/prompt resolves — which only happens when agy exits (default two-hour timeout). A disconnected client leaves the bridge and child process alive for hours. Kill activeChild before await queue so disconnect terminates the active turn.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/antigravity/agyBridge.ts around line 1333:

On stdin disconnect, `runAgyBridge` calls `await queue` before killing `activeChild`, so the bridge blocks until the in-flight `session/prompt` resolves — which only happens when `agy` exits (default two-hour timeout). A disconnected client leaves the bridge and child process alive for hours. Kill `activeChild` before `await queue` so disconnect terminates the active turn.

* output as live. Returns 0 when the file cannot be measured, which falls back
* to the marker heuristic rather than skipping the turn's own output.
*/
function transcriptBaseline(conversationId: string | undefined): number {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium antigravity/agyBridge.ts:110

transcriptBaseline measures whichever transcript file exists first — usually transcript_full.jsonl — but resolveTranscriptPath can later pin the turn to the condensed transcript.jsonl once it appears. The byte offset captured from one file is then used to seek into a different file. If that offset equals or exceeds the condensed file's size, drain reads zero bytes from the transcript and the resumed turn's tool-call output and assistant text are skipped entirely. Consider measuring the baseline against the same file path that resolveTranscriptPath will select, or deferring the baseline read until the pinned path is known.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/antigravity/agyBridge.ts around line 110:

`transcriptBaseline` measures whichever transcript file exists first — usually `transcript_full.jsonl` — but `resolveTranscriptPath` can later pin the turn to the condensed `transcript.jsonl` once it appears. The byte offset captured from one file is then used to seek into a different file. If that offset equals or exceeds the condensed file's size, `drain` reads zero bytes from the transcript and the resumed turn's tool-call output and assistant text are skipped entirely. Consider measuring the baseline against the same file path that `resolveTranscriptPath` will select, or deferring the baseline read until the pinned path is known.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d87551f. Configure here.

threadId: input.threadId,
turnId,
resumeCursor: ctx.session.resumeCursor,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cancel misses steers on semaphore

High Severity

cancelEpoch is rechecked only before calling acp.prompt, but prompt serialization waits on a semaphore inside that call. A steer that already passed the check and is queued there survives Stop: acp.cancel only interrupts the active fiber, and the bridge records cancel generation at enqueue time—after the cancel—so the steer still spawns agy.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d87551f. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants