Skip to content

feat(antigravity): reuse one stream-json driver across successful turns (#535) - #618

Draft
Tericerice wants to merge 11 commits into
BytePioneer-AI:mainfrom
Tericerice:feat/535-antigravity-persistent-session
Draft

Tericerice wants to merge 11 commits into
BytePioneer-AI:mainfrom
Tericerice:feat/535-antigravity-persistent-session

Conversation

@Tericerice

Copy link
Copy Markdown

Summary

The Antigravity adapter spawned agy for every turn.start and, on every #completeTurn including successes, closed its stdin, waited for exit, and signalled a lingering process. Continuing a conversation therefore repaid the CLI, Language Server, and customization startup cost on each Turn. The official headless protocol keeps one stream-json process and reads the next user message from the same stdin, which this PR adopts.

Fixes #535

Behaviour

  • A successful Turn keeps its driver. The process, its Question bridge, and its log move to the Session; the next Turn writes to the same stdin. --conversation is not needed on that path, and --add-dir/--log-file keep pointing at the same directory and log.
  • The Question bridge is rebound, not rebuilt. rebind moves the Turn identity, the owning-Turn check, the emit callback, and the duplicate-question guard to the new Turn, so a question from an earlier Turn can neither be attributed to the current Turn nor answered. A disposed or busy bridge refuses to rebind.
  • Replayed native output is filtered. agy re-emits the previous successful result and earlier steps when a new stdin line arrives; only a result whose num_turns advanced (or whose payload differs) and steps at or beyond the new Turn's first index reach the Host, so history records each Turn once.
  • Every other outcome releases the driver — failure, cancellation, Session close, a different model/effort/permission selection, or a process that exited or lost writable stdin. Release stops the bridge, ends stdin, and escalates SIGTERM → SIGKILL within the existing grace and cleanup-wait bounds.
  • Real errors stay errors. A native failure, including a num_turns that did not advance, still fails the Turn; nothing is reported as success merely because text arrived.

Tests

  • The adapter test stand-ins now behave like the real CLI: they answer each stdin line instead of exiting after one Turn. This is what exposed the replayed-result filtering.
  • New test: three Turns run on one PID, and model.select followed by a fourth Turn starts a new PID, with all four Turns present in history.

Limits (unchanged from the issue)

  • No statistical benchmark is claimed. The issue's measurements are a small number of observations with differing tool calls and upstream latency.
  • Long idle periods, background subagents, and every native protocol version are not covered here; the driver is released on every non-successful outcome and on Session close, which bounds the exposure.

🤖 Generated with Claude Code

…ns (BytePioneer-AI#535)

Each turn.start previously spawned agy and every #completeTurn closed its stdin and
waited for exit, so continuing a conversation repaid the full CLI, Language Server, and
customization startup cost. The official headless protocol keeps one stream-json process
and reads the next user message from the same stdin.

- A successful Turn hands its process, Question bridge, and log to the Session; the next
  Turn rebinds the bridge to its own turn id, ownership check, and duplicate guard
- Reused processes ignore the previous Turn's replayed result and steps, keyed on
  num_turns and step index, so history receives each Turn exactly once
- A different model/effort/permission selection, a dead or unwritable process, or a Turn
  that failed, was cancelled, or closed the Session disposes the driver with bounded
  SIGTERM/SIGKILL escalation
- Test fixtures now answer each stdin line like the real CLI instead of exiting after one
  Turn, and a new test asserts one PID across three Turns and a new one after model.select

Co-Authored-By: Claude Code <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 26, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Tericerice and others added 10 commits October 26, 2026 13:37
… made ambiguous (BytePioneer-AI#535)

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ePioneer-AI#535)

- Claim the Session synchronously before any await, so two concurrent turn.start calls
  cannot both pass the busy check and overwrite #active
- Stamp each Question request with a bridge generation and reject one that straddles a
  rebind, instead of attributing it to the Turn that now owns the bridge
- Release a retained driver that exits between Turns; nothing else could reach its
  bridge or log once the close listener cleared #transport
- Cancel a failed Turn's Subagents before awaiting them, so an observable-but-stuck
  Subagent cannot strand the driver and leave the Session busy
- Filter a replayed result regardless of its native status: an ERROR the Host accepted
  as success keeps the driver, so its replay would fail the next Turn early

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ve driver (BytePioneer-AI#535)

Pi's round-1 review confirmed the concurrency and attribution Blocker were already fixed
on head, and raised three remaining issues:

- Step replay filtering assumed agy keeps numbering steps across stdin messages, with no
  evidence and no test. It now recognises a replay by comparing each step line with the
  previous Turn's at the same index, and only while the Turn is still reading that
  contiguous replay prefix. This is correct whether agy continues or restarts numbering,
  and a new test replays the previous Turn then emits steps that reuse indices 1 and 2.
- A retained driver keeps writing the Conversation database between Turns, so fork and
  rollback were copying a live SQLite file. Both now release an idle driver first.
- Two doc statements described the old per-Turn process lifecycle, and the idle-unload
  proposal listed Antigravity as having no resident process; all three are corrected.

Also: escalate to SIGKILL after the bounded wait instead of cancelling it, report a
disposed/busy bridge as a non-retryable invalidState and drop the driver, and describe the
real fallback behaviour in the driver-reuse comment.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ver exited (BytePioneer-AI#535)

Codex's confirmation round found two gaps in the previous fix:

- Replay detection only ended when a step line differed, so a new Turn whose own first
  step repeated the previous Turn's line verbatim was still dropped. Each replayed line is
  now consumed as it matches, so an identical new step is delivered once the replay is
  spent. The regression test runs four Turns and fails against the previous code.
- releaseIdleDriver resolved after scheduling a SIGKILL, not after the process exited, so
  fork and rollback could still copy a database a live agy was writing. Disposal now
  reports whether exit was confirmed, and both entry points fail with sessionBusy instead
  of copying when it was not.
- A disposed or busy bridge is now evaluated inside the setup try, so it reports a
  non-retryable invalidState and releases the driver rather than escaping as a retryable
  nativeFailure.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ePioneer-AI#535)

Codex's round-4 review found that releasing an unresponsive driver cleared #transport
before exit was confirmed. A first attempt therefore returned false, but an immediate
retry saw no driver and returned true, letting fork or rollback copy a database a live agy
was still writing.

- Clear #transport only once the process exit is confirmed, in both the close listener and
  disposal, so repeated release attempts keep reporting the truth until the escalation
  actually kills it
- Test: a driver that ignores stdin ending and SIGTERM keeps reporting false on every
  attempt, and the fork entry point refuses while it does

Co-Authored-By: Claude Code <noreply@anthropic.com>
… timing (BytePioneer-AI#535)

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ts cwd (BytePioneer-AI#535)

Windows keeps a directory locked while a process still has it as its working directory,
and this fixture deliberately starts a driver that ignores stdin ending and SIGTERM. The
escalation's SIGKILL does end it, so the test now records its pid and waits for it to
disappear before cleaning up, instead of failing on EBUSY.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…succeeds (BytePioneer-AI#535)

Windows keeps a directory locked while any process still uses it as its working
directory, and this fixture starts a driver that ignores stdin ending and SIGTERM. The
driver now ends itself after both bounded release waits have run, and cleanup waits for it
with a bounded poll, so the test asserts the release behaviour without depending on
temp-directory removal timing.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ePioneer-AI#535)

The stuck-driver fixture assumed POSIX signal semantics: it ignored SIGTERM and expected a
retry to be refused. Windows has no SIGTERM — kill() terminates the process — so the
first attempt could legitimately succeed there, and the test failed on Windows and macOS.

The test now asserts the invariant that actually matters and holds everywhere: release
reports success only when the process is really gone. When it cannot confirm exit, a
retry must keep telling the truth rather than report release because the first attempt
already asked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ePioneer-AI#535)

The fixture needs a driver that survives termination so the release path cannot confirm
exit. Windows has no signal to ignore, and its .cmd wrapper runs agy as a grandchild, so
killing the wrapper leaves the grandchild holding the pipe. The adapter gap that reveals is
worth a separate look; a test that cannot model it on that platform should say so rather
than fail or assert something weaker.

Co-Authored-By: Claude Code <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.

[Performance] Antigravity 每轮关闭并重启 agy,建议复用 stream-json 会话以减少续聊启动开销

1 participant