Skip to content

feat: add agy backend, RunFirstTurn, ContentBlock/BlockSender, engine fixes#53

Merged
dmora merged 1 commit into
mainfrom
feat/agy-runfirstturn-blocksender
Jun 7, 2026
Merged

feat: add agy backend, RunFirstTurn, ContentBlock/BlockSender, engine fixes#53
dmora merged 1 commit into
mainfrom
feat/agy-runfirstturn-blocksender

Conversation

@dmora

@dmora dmora commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Summary

Bundles four related pieces of work (all green under make qa + make mcp-qa):

1. New Antigravity CLI (agy) backend — engine/cli/agy

Spawner + Parser + Resumer (spawn-per-turn). agy is plain-text, spawn-per-turn, with no JSON/streaming mode and the conversation ID only in its log file, so the backend:

  • wraps execution in an injection-safe sh -c '"$0" "$@"; ...' shell that emits a {"type":"result","stop_reason":"end_turn"} sentinel on clean exit (parsed into MessageResult),
  • captures the conversation UUID from agy --log-file <tmp> and resumes via --conversation <id>,
  • maps Model--model, OptionAddDirs--add-dir, HITL/agy.dangerously_skip_permissions--dangerously-skip-permissions, agy.sandbox--sandbox.
  • Registered in the agentrun-mcp diagnostic server (list_backends, parse_line, run_turn, probe_backend, sessions).

2. RunFirstTurn helper — fixes the spawn-per-turn first-turn double-prompt

Spawn-per-turn backends (SequentialSender) run turn 1 at Start (prompt baked into the spawn); streaming/ACP backends start turn 1 on the first Send. Callers that did Start(prompt) + RunTurn(prompt) triggered a redundant turn-2 resume before the session ID was captured, which broke run_turn / session_start / probe_backend for codex, opencode, and agy (cli: resume args: ... no ID available).

agentrun.RunFirstTurn drains for SequentialSender and sends otherwise, so callers don't branch on backend type. Applied in the MCP harness (doRunTurn, doSessionStart, doProbe) and examples/interactive.

3. CLI engine turn-boundary race fix — engine/cli/process.go

A Send arriving right after MessageResult (before the subprocess readLoop closed the channels) reused an output channel that finish() was concurrently closing → the resumed turn was silently dropped. Fix: close channels under p.mu (finishLocked/finalizeTurn); replaceSubprocess re-checks done under the same lock and falls back to fresh channels (spawnCleanResume), returning ErrTerminated instead of silently resuming a turn that finalized with an error.

4. ContentBlock / BlockSender (multi-modal input)

Root BlockSender/ContentBlock + RunTurnBlocks; cli.BlockFormatter (Claude impl); ACP SendBlocks; text-degradation fallback.

Tests

Deterministic regression tests: engine/cli/race_test.go (turn-boundary race + crashed-turn-not-resumed), engine/cli/firstturn_test.go, runfirstturn_test.go. The race test fails reliably without the fix and passes count=30 under -race.

Known pre-existing limitation (out of scope, documented)

An adversarial review surfaced a separate pre-existing concurrency issue: Stop() called concurrently with a resume Send can deadlock or bypass the grace period, because cmdDone is a single buffered channel that Stop, replaceSubprocess, and the resume drain compete for. A surgical patch proved fragile and was reverted; a proper fix needs per-generation exit-signal channels. Normal sequential usage (drain → Send; Stop after/between turns) is unaffected.

🤖 Generated with Claude Code

… fixes

New Antigravity CLI (agy) backend
- engine/cli/agy: Spawner + Parser + Resumer (spawn-per-turn). Wraps agy in an
  injection-safe `sh -c '"$0" "$@" ...'` shell that emits a JSON result sentinel
  on clean exit; captures the conversation ID from agy's --log-file and resumes
  via --conversation. Maps Model/AddDirs/HITL/sandbox/skip-permissions options.
- cmd/agentrun-mcp: register agy in newCLIBackend, cliBackendNames, validBackends.

RunFirstTurn helper (fixes spawn-per-turn first-turn double-prompt)
- Root RunFirstTurn drains the Start-initiated turn for SequentialSender
  (spawn-per-turn) backends and sends for streaming/ACP backends, so callers no
  longer branch on backend type. Previously Start(prompt)+RunTurn(prompt) ran a
  redundant turn-2 resume before the session ID was captured, breaking codex,
  opencode, and agy under run_turn / session_start / probe_backend.
- Use it in cmd/agentrun-mcp doRunTurn/doSessionStart/doProbe and
  examples/interactive.

CLI engine turn-boundary race fix
- Close output/done channels under p.mu (finishLocked/finalizeTurn) and have
  replaceSubprocess re-check done under the same lock, falling back to fresh
  channels (spawnCleanResume) — and returning ErrTerminated rather than silently
  resuming a turn that finalized with an error. Fixes a Send-right-after-result
  race that dropped the resumed turn.

ContentBlock / BlockSender (multi-modal input)
- Root BlockSender/ContentBlock + RunTurnBlocks; cli.BlockFormatter with Claude
  implementation; ACP SendBlocks; text-degradation fallback.

Tests: deterministic regression tests for the race fix and RunFirstTurn
(engine/cli/race_test.go, firstturn_test.go, runfirstturn_test.go). make qa and
make mcp-qa green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dmora dmora merged commit c1cad63 into main Jun 7, 2026
6 checks passed
@dmora dmora deleted the feat/agy-runfirstturn-blocksender branch June 7, 2026 18:47

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for structured content blocks (such as text and base64 images) via the BlockSender and BlockFormatter interfaces, allowing multi-modal inputs with graceful degradation to plain text for unsupported backends. It also adds a new Antigravity CLI backend (agy) and introduces RunFirstTurn to unify the first-turn execution logic across different backend models. The review feedback highlights two resource leaks where temporary log files created by the agy backend are not cleaned up during single-turn executions or test runs, suggesting the use of runtime finalizers and t.Cleanup to resolve them.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread engine/cli/agy/agy.go
Comment on lines +88 to +91
if f, err := os.CreateTemp("", "agy-*.log"); err == nil {
f.Close()
b.logFile = f.Name()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The temporary log file created here is only deleted if ResumeArgs is eventually called. However, for single-turn executions (such as the run_turn tool or sessions that are stopped before any resume), ResumeArgs is never invoked, leading to a persistent temporary file leak in the OS temp directory.

To prevent this resource leak, you can register a runtime finalizer on the Backend struct to automatically clean up the temporary file when the backend is garbage collected. Note that you will also need to import the "runtime" package.

	if f, err := os.CreateTemp("", "agy-*.log"); err == nil {
		f.Close()
		logFile := f.Name()
		b.logFile = logFile
		runtime.SetFinalizer(b, func(_ *Backend) {
			_ = os.Remove(logFile)
		})
	}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch — confirmed. Fixed in #54: SpawnArgs is now a pure, I/O-free builder. Rather than a finalizer, the shell wrapper allocates its own log via mktemp and rms it on exit, and the conversation ID is captured from a stdout sentinel (codex/opencode pattern) instead of a persistent temp file — so nothing leaks on Validate, single-turn runs, or otherwise.

Comment on lines +47 to +49
if b.logFile == "" {
t.Error("Backend.logFile not set by SpawnArgs")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The temporary log file created during SpawnArgs is not cleaned up by this test, leading to leaked files in the system temporary directory on every test run.

Using t.Cleanup to delete the file ensures the system remains clean after the test completes.

	if b.logFile == "" {
		t.Error("Backend.logFile not set by SpawnArgs")
	} else {
		t.Cleanup(func() {
			_ = os.Remove(b.logFile)
		})
	}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in #54 — the redesign removes the Go-side temp file entirely (the wrapper manages its own via mktemp), so there is no b.logFile for the test to leak. Added a purity test asserting SpawnArgs creates no temp files.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5e3801d5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread engine/cli/agy/agy.go
args = append(args, "-c", shellWrapper, b.binary)
args = append(args, agyArgs...)
args = append(args, "--print", prompt)
return "sh", args

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid wrapping agy in a shell that leaks the child

When an agy turn is cancelled or stopped before normal completion, the existing CLI lifecycle code signals only cmd.Process during Stop/replacement; with this wrapper that process is sh, not agy. A non-interactive sh -c 'agy ...' does not reliably forward SIGTERM to its foreground child, so timeouts, session_stop, or interrupted replacement can leave the agy agent running orphaned after agentrun reports it stopped, potentially continuing tool work or consuming resources.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed real (the engine signals cmd.Process = sh, no process group). Fixed in #54: the wrapper now runs agy in the background and traps SIGTERM/SIGINT, forwarding to the agy child. Verified live — SIGTERM to the wrapper terminates agy with no orphan.

Comment thread engine/cli/agy/agy.go
Comment on lines +88 to +90
if f, err := os.CreateTemp("", "agy-*.log"); err == nil {
f.Close()
b.logFile = f.Name()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep SpawnArgs side-effect free

Engine.Validate calls backend.SpawnArgs(agentrun.Session{}) before each MCP run/session/probe to discover the binary, so creating the log file here leaks a temp agy-*.log on every validation call: Start calls SpawnArgs again, overwrites b.logFile, and only the second path can later be removed by ResumeArgs. This also violates the Spawner contract that SpawnArgs is a pure argument builder; allocate the capture file as part of actual start state instead of validation-time argument construction.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed — SpawnArgs must be side-effect free since Engine.Validate calls it with a zero Session. Fixed in #54: log-file allocation moved out of SpawnArgs into the run-time wrapper (mktemp); SpawnArgs now only builds args.

dmora added a commit that referenced this pull request Jun 7, 2026
)

Addresses post-merge review feedback on #53 from Gemini Code Assist and
Codex.

## Issues fixed

| # | Reviewer | Issue |
|---|---|---|
| Temp-file leak | Gemini (medium), Codex P2 | `SpawnArgs` created a
temp log file → leaked on every `Engine.Validate()` (zero session),
leaked again because `Start` re-calls `SpawnArgs` and overwrites the
path, leaked on single-turn runs (no `ResumeArgs`), and violated the
"`SpawnArgs` is a pure builder" contract. |
| Test temp-file leak | Gemini (medium) | The test left `agy-*.log`
files in the temp dir. |
| Orphaned agy on Stop | Codex **P1** | The `sh -c` wrapper means
`cmd.Process` is `sh`, not `agy`. The engine signals `cmd.Process` on
Stop/replacement, and a plain `sh -c` doesn't forward signals to its
child → agy left running orphaned. |

## Approach

Rather than papering over with a finalizer, the backend now matches the
codex/opencode capture pattern and hardens the wrapper:

- **`SpawnArgs` is pure** — no file I/O. The shell wrapper allocates its
own temp log via `mktemp` and `rm`s it on exit, so nothing leaks
(Validate, single-turn, or test).
- **Conversation ID via stdout** — the wrapper extracts `Created
conversation <id>` from the log and emits a
`{"type":"agy_session","id":"..."}` sentinel; `ParseLine` captures it
write-once into an atomic pointer. `ResumeArgs` no longer reads/removes
a file side channel.
- **Signal forwarding** — the wrapper runs agy in the background and
`trap`s `SIGTERM`/`SIGINT`, forwarding to the agy child so
Stop/replacement actually terminates agy.

## Verification
- `make qa` + `make mcp-qa` green.
- Live (real agy): turn-1 ID capture + turn-2 resume passed 2/2; SIGTERM
to the wrapper killed agy with **no orphan**; no leftover temp files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant