Skip to content

[AI-1218] Antigravity live subagent nesting — CLI (INVOKE_SUBAGENT detection)#285

Merged
alexeyzimarev merged 8 commits into
mainfrom
tonyyoung/ai-1218-antigravity-subagent-link
Jul 7, 2026
Merged

[AI-1218] Antigravity live subagent nesting — CLI (INVOKE_SUBAGENT detection)#285
alexeyzimarev merged 8 commits into
mainfrom
tonyyoung/ai-1218-antigravity-subagent-link

Conversation

@realtonyyoung

Copy link
Copy Markdown
Contributor

Live subagent nesting for Google Antigravity — CLI half. Server half: a paired PR in kurrent-io/kcap-server (must merge first here, then that PR bumps the src/cli submodule).

Detects parent→child subagent links from the authoritative INVOKE_SUBAGENT step in the parent's transcript (spawn-time, complete, direction-unambiguous), replacing the old post-hoc messages/*.json scan (bidirectional / incomplete / late).

Changes

  • ParserAntigravitySubagents.ChildConversationIdsFromLine / IsInvokeSubagentLine: extract child conversationIds from an INVOKE_SUBAGENT step's embedded JSON.
  • Live watcher (WatchCommand) — parse INVOKE_SUBAGENT from drained transcript lines and POST the subagent-link at spawn time; the old messages/ scan (ChildrenOf) is deleted.
  • Import (AntigravitySubagents.BuildParentMap, AntigravityImportSource) — build the parent map from INVOKE_SUBAGENT + drift counters.
  • Idle-end — exclude invoke_subagent/define_subagent async tool calls from idle-end suppression so a subagent-spawning parent still times out.
  • Import integration test switched to INVOKE_SUBAGENT linkage.

Tests

AI-1218 CLI unit tests 71/71; NativeAOT publish clean (0 IL2026/IL3050).

Linear: AI-1218

🤖 Generated with Claude Code

realtonyyoung and others added 6 commits July 6, 2026 16:45
Antigravity fires no subagent hooks, so a live subagent is only
discoverable once the child reports back by writing a message file
into the parent's brain/<parent>/.system_generated/messages/ dir. Add
AntigravitySubagents.ChildrenOf, a parent-scoped variant of the
existing BuildParentMap (import-time full scan) that reads only one
parent's messages dir and returns the distinct child ids that have
recipient == parent.

Wire it into the antigravity watcher: scanned once per drain tick
(mirrors the Gemini/OpenCode live subagent discovery) and once more on
final drain before exit, so a late-reporting child isn't missed. Newly
seen children are POSTed to /hooks/antigravity/subagent-link (the
server normalizes dashed/dashless ids via CanonicalSessionId.Normalize,
so no id-form translation is needed here); a child is only recorded in
the in-memory WatchState.PostedSubagentLinks set once its POST
succeeds, so a failed POST retries fail-open on the next scan.
…d suppression

A parent that invokes subagents never idle-ended (only ended on Antigravity quit), so its
adopted children never consolidated onto the parent card until the app was quit. Root cause:
UpdateAntigravityPendingToolCalls counted invoke_subagent/define_subagent as tool calls in
flight, but those resolve ASYNCHRONOUSLY via a separate conversation (child reports back through
brain/<parent>/messages) and never produce a RUN_COMMAND/VIEW_FILE/LIST_DIRECTORY/CODE_ACTION
result step to decrement the counter — so PendingAntigravityToolCalls stayed > 0 forever and
ShouldEndOnIdle was permanently suppressed. Exclude the async subagent-orchestration tools from
the in-flight count; real command tools still suppress idle-end while genuinely running.

Test: PendingToolCalls_excludes_async_subagent_orchestration_so_a_parent_can_idle_end (9/9).
Shared static parser (ChildConversationIdsFromLine / IsInvokeSubagentLine)
that reads the authoritative spawn-time parent-to-child signal from a
parent's INVOKE_SUBAGENT transcript step. Foundation for the live watcher
and import parent-map redesign (Tasks 2/3).
BuildParentMap now derives child->parent linkage from each conversation's
transcript_full.jsonl INVOKE_SUBAGENT steps (the spawn-time signal) instead
of scanning brain/<parent>/.system_generated/messages/*.json, which missed
children that never reported back and had an inverted sender/recipient
direction in some captures. DiscoverAsync logs invoke-edge/dangling-child/
messages-without-invoke drift counters now that messages/ is no longer the
detection source.
…ENT linkage

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 6, 2026

Copy link
Copy Markdown

AI-1218

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Antigravity: link subagents via INVOKE_SUBAGENT transcript steps (live + import)

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Derive Antigravity parent→child subagent links from transcript INVOKE_SUBAGENT steps.
• Post subagent-link hooks at spawn time during live watch; remove messages/ directory scanning.
• Fix idle-end suppression by excluding async subagent tool calls from pending counts.
Diagram

graph TD
  T[("transcript_full.jsonl")] --> W["WatchCommand (live)"] --> P[["AntigravitySubagents (invoke parser)"]] --> H["kcap-server: /hooks/.../subagent-link"]
  T[("transcript_full.jsonl")] --> I["AntigravityImportSource (import)"] --> P[["AntigravitySubagents (invoke parser)"]]

  subgraph Legend
    direction LR
    _file[("Transcript file")] ~~~ _svc["CLI component"] ~~~ _mod[["Parser module"]]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep messages/ scan as a fallback signal
  • ➕ Provides resilience if INVOKE_SUBAGENT transcript format drifts
  • ➕ Can recover links when transcripts are missing/corrupt but messages exist
  • ➖ Reintroduces late/bidirectional/ambiguous linkage behavior
  • ➖ Extra IO (messages dir scans) during watch/import and more edge-case logic
2. Move INVOKE_SUBAGENT extraction + linking to the server
  • ➕ Single implementation for all clients; easier to update on format drift
  • ➕ Server can correlate with other ingestion signals centrally
  • ➖ Requires server access to raw transcript lines (or new hook payloads)
  • ➖ Couples linkage correctness to ingestion timing/order on server side

Recommendation: The PR’s approach (derive edges from INVOKE_SUBAGENT) is the right primary signal because it is spawn-time and direction-unambiguous. Consider optionally retaining a guarded messages/ fallback (or emitting stronger drift telemetry) if real-world captures show missing/changed INVOKE payloads, but defaulting to INVOKE-only keeps behavior deterministic and timely.

Files changed (9) +518 / -140

Enhancement (3) +161 / -40
AntigravitySubagents.csRebuild parent map from INVOKE_SUBAGENT transcript steps +141/-37

Rebuild parent map from INVOKE_SUBAGENT transcript steps

• Switches BuildParentMap from scanning messages/*.json to scanning transcript_full.jsonl for INVOKE_SUBAGENT lines. Adds strict extraction helpers (ChildConversationIdsFromLine / IsInvokeSubagentLine) that parse embedded JSON blocks, dedupe GUID-shaped ids, and tolerate truncated/unreadable transcripts.

src/Capacitor.Cli.Core/Antigravity/AntigravitySubagents.cs

Models.csTrack posted Antigravity subagent links per watch session +5/-0

Track posted Antigravity subagent links per watch session

• Adds WatchState.PostedSubagentLinks to dedupe successful subagent-link POSTs and allow retries on failures.

src/Capacitor.Cli.Core/Models.cs

AntigravityImportSource.csImport discovery uses invoke-derived parent map with drift counters +15/-3

Import discovery uses invoke-derived parent map with drift counters

• Updates docs to reflect INVOKE_SUBAGENT linkage and logs drift observability metrics (invoke edge count, dangling children, messages-without-invoke). Keeps root/descendant computation driven by AntigravitySubagents.BuildParentMap.

src/Capacitor.Cli/Commands/AntigravityImportSource.cs

Bug fix (1) +103 / -12
WatchCommand.csPost Antigravity subagent-link hooks from drained INVOKE_SUBAGENT lines +103/-12

Post Antigravity subagent-link hooks from drained INVOKE_SUBAGENT lines

• Changes DrainNewLines to return the drained lines so Antigravity can scan only the new content each tick and on final drain. Adds ExtractAndPostSubagentLinks + hook POST helper and wires ScanAntigravitySubagentLinks into the parent watcher. Fixes idle-end suppression by excluding invoke_subagent/define_subagent from pending tool-call counting.

src/Capacitor.Cli/Commands/WatchCommand.cs

Tests (5) +254 / -88
AntigravityImportTests.csIntegration test links child via INVOKE_SUBAGENT transcript step +16/-9

Integration test links child via INVOKE_SUBAGENT transcript step

• Replaces messages/*.json linkage fixture with an INVOKE_SUBAGENT line appended to the root transcript. Tightens transcript POST assertions to match on agent_id to avoid false positives from embedded child ids in the parent transcript batch.

test/Capacitor.Cli.Tests.Integration/AntigravityImportTests.cs

AntigravityImportSourceTests.csUnit tests updated to invoke-based nesting fixtures +38/-27

Unit tests updated to invoke-based nesting fixtures

• Moves discovery tests from messages-based fixtures to transcript INVOKE_SUBAGENT fixtures using GUID-shaped ids. Ensures roots/descendants/session filtering behave with the new linkage source.

test/Capacitor.Cli.Tests.Unit/AntigravityImportSourceTests.cs

AntigravitySubagentLinkScanTests.csNew unit tests for live INVOKE_SUBAGENT link posting behavior +36/-0

New unit tests for live INVOKE_SUBAGENT link posting behavior

• Adds coverage for ExtractAndPostSubagentLinks: deduping across rescans and retrying failed POSTs until success.

test/Capacitor.Cli.Tests.Unit/AntigravitySubagentLinkScanTests.cs

AntigravitySubagentsTests.csRewrite subagent mapping tests for INVOKE_SUBAGENT parsing + map semantics +146/-52

Rewrite subagent mapping tests for INVOKE_SUBAGENT parsing + map semantics

• Replaces messages-scan tests with transcript-based fixtures, covering directionality, missing child dirs, determinism on conflicts, and self-invocation guards. Adds focused tests for ChildConversationIdsFromLine and IsInvokeSubagentLine parsing behavior and strictness.

test/Capacitor.Cli.Tests.Unit/AntigravitySubagentsTests.cs

AntigravityWatchExtractorTests.csTest idle-end pending-call logic excludes async subagent tool calls +18/-0

Test idle-end pending-call logic excludes async subagent tool calls

• Adds a regression test ensuring define_subagent/invoke_subagent do not pin PendingAntigravityToolCalls and therefore do not suppress idle-end for subagent-spawning parents.

test/Capacitor.Cli.Tests.Unit/AntigravityWatchExtractorTests.cs

@qodo-code-review

qodo-code-review Bot commented Jul 6, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. README outdated for Antigravity nesting ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
This PR changes Antigravity live behavior to link/nest subagents from INVOKE_SUBAGENT steps at
spawn time, but the README still states live nesting is a future follow-up. Users relying on the
README will have incorrect expectations about Antigravity subagent capture/nesting behavior.
Code

src/Capacitor.Cli/Commands/WatchCommand.cs[R328-329]

+                } else if (agentId is null && vendor == "antigravity") {
+                    await ScanAntigravitySubagentLinks(baseUrl, sessionId, drained, state.PostedSubagentLinks, cts.Token);
Evidence
PR Compliance ID 3 requires README updates for user-facing CLI behavior changes. The watcher now
scans drained Antigravity transcript lines for INVOKE_SUBAGENT and posts subagent-link events,
while the README still states that live Antigravity subagent nesting is only a future follow-up.

CLAUDE.md: User-facing CLI surface changes must update README.md in the same PR
src/Capacitor.Cli/Commands/WatchCommand.cs[328-329]
README.md[591-591]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR changes Antigravity live subagent nesting/linking behavior (now derived from `INVOKE_SUBAGENT` lines and POSTed at spawn time), but `README.md` still documents live nesting as a follow-up.

## Issue Context
Compliance requires updating `README.md` in the same PR when the user-facing CLI behavior changes.

## Fix Focus Areas
- README.md[582-592]
- src/Capacitor.Cli/Commands/WatchCommand.cs[328-329]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Quadratic drift diagnostics ✓ Resolved 🐞 Bug ➹ Performance
Description
AntigravityImportSource.DiscoverAsync’s new drift counters use repeated linear List.Contains and
parentMap.Values.Contains calls inside full scans, making the diagnostic block O(n²) over
conversation history. This can noticeably slow discovery/import on large brain trees even though
it’s “just logging”.
Code

src/Capacitor.Cli/Commands/AntigravityImportSource.cs[R66-73]

+        // AI-1218 drift observability: surface format drift without a messages fallback.
+        var allConversationIds = convIds;
+        var invokeEdges   = parentMap.Count;
+        var danglingChild = parentMap.Keys.Count(c => !allConversationIds.Contains(c));
+        var msgButNoInvoke = allConversationIds.Count(id =>
+            Directory.Exists(AntigravityPaths.MessagesDir(id, _home, _geminiCliHome))
+            && !parentMap.ContainsKey(id) && !parentMap.Values.Contains(id));
+        Log($"Antigravity import: {invokeEdges} invoke edge(s); {danglingChild} invoked child id(s) with no conversation dir; {msgButNoInvoke} conversation(s) with messages/ but no invoke edge");
Evidence
The PR-added diagnostics compute danglingChild via allConversationIds.Contains(...) where
allConversationIds is a List, and compute msgButNoInvoke via parentMap.Values.Contains(...)
inside a loop over all conversation IDs; both are linear per call, making the overall block
quadratic.

src/Capacitor.Cli/Commands/AntigravityImportSource.cs[62-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`AntigravityImportSource.DiscoverAsync` added drift diagnostics that do membership checks against `List<string>` and `parentMap.Values` inside `Count(...)` loops. This makes the diagnostics O(n²) with the number of conversations/edges.

## Issue Context
These counters run during discovery, so they affect every import run, and large Antigravity histories can make this path slow.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/AntigravityImportSource.cs[66-73]

## Suggested fix
- Convert `convIds` to a `HashSet<string>` (Ordinal) for `Contains` checks.
- Materialize `parentMap.Values` into a `HashSet<string>` once (Ordinal) if you need membership checks.
- Keep the existing log message shape; just change how the counts are computed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Dashed sessionId assumption ✗ Dismissed 🐞 Bug ≡ Correctness
Description
PR-updated Antigravity import tests assert that discovered SessionId is the dashed brain-dir
conversationId, but the live Antigravity hook and kcap watch canonicalize session IDs by stripping
dashes. This inconsistency can lead to confusing --session filtering behavior and can prevent
dedupe/resume alignment between live-captured sessions (dashless) and imported sessions (dashed).
Code

test/Capacitor.Cli.Tests.Unit/AntigravityImportSourceTests.cs[R103-112]

+            WriteTranscript(home, SessA, UserLine("2026-07-02T19:00:00Z"));
+            WriteTranscript(home, SessB, UserLine("2026-07-02T19:00:00Z"));

            var source = new AntigravityImportSource(home: home, geminiCliHome: "");
            var discovered = await source.DiscoverAsync(
-                new DiscoveryFilters(FilterCwd: null, FilterSession: "B", Since: null, MinLines: 0),
+                new DiscoveryFilters(FilterCwd: null, FilterSession: SessB, Since: null, MinLines: 0),
                CancellationToken.None);

            await Assert.That(discovered.Count).IsEqualTo(1);
-            await Assert.That(discovered[0].SessionId).IsEqualTo("B");
+            await Assert.That(discovered[0].SessionId).IsEqualTo(SessB);
Evidence
The PR’s unit tests now expect dashed GUIDs as session IDs, but the live Antigravity hook and watch
CLI canonicalize session IDs by removing dashes, and the Antigravity import source uses the
brain-dir name as SessionId. These code paths disagree on the canonical session id format.

test/Capacitor.Cli.Tests.Unit/AntigravityImportSourceTests.cs[14-18]
test/Capacitor.Cli.Tests.Unit/AntigravityImportSourceTests.cs[64-68]
test/Capacitor.Cli.Tests.Unit/AntigravityImportSourceTests.cs[103-113]
src/Capacitor.Cli/Commands/AntigravityHookCommand.cs[64-68]
src/Capacitor.Cli/Program.cs[592-603]
src/Capacitor.Cli/Commands/AntigravityImportSource.cs[79-101]
src/Capacitor.Cli/Commands/GeminiImportSource.cs[76-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Antigravity import discovery currently treats the brain directory name (dashed GUID) as `SessionId`, and the updated unit tests in this PR codify that expectation. However, the live hook path for Antigravity strips dashes to produce a canonical dashless session id, and `kcap watch` also strips dashes.

This makes import-vs-live behavior inconsistent and can break expected dedupe/filtering semantics.

## Issue Context
- Live Antigravity hook computes `sessionId = conversationId.Replace("-", "")`.
- `kcap watch` similarly canonicalizes session id to dashless.
- Antigravity import discovery returns `SessionId: convId` where `convId` is the brain dir name (dashed).
- This PR’s tests assert dashed IDs, which may mask the inconsistency.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/AntigravityImportSource.cs[79-105]
- test/Capacitor.Cli.Tests.Unit/AntigravityImportSourceTests.cs[14-18]
- test/Capacitor.Cli.Tests.Unit/AntigravityImportSourceTests.cs[64-113]

## Suggested fix
- In `DiscoverAsync`, derive `sessionIdDashless = convId.Replace("-", "")` and use that for `DiscoveredSession.SessionId` and for `FilterSession` comparisons (optionally normalize `FilterSession` the same way).
- Keep using the dashed `convId` for filesystem paths (`TranscriptPath`) and for child `agent_id` routing.
- Update the unit/integration tests to assert the dashless `SessionId` (while still writing transcripts under dashed conversation ids on disk).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli/Commands/WatchCommand.cs
Comment thread src/Capacitor.Cli/Commands/AntigravityImportSource.cs
Comment thread test/Capacitor.Cli.Tests.Unit/AntigravityImportSourceTests.cs Outdated
…counters

- README: Antigravity subagents now nest in BOTH live capture and historical import
  (derived from INVOKE_SUBAGENT), correcting the stale "live nesting is a follow-up".
- AntigravityImportSource drift counters: HashSet membership (Ordinal) so they stay O(n),
  not O(n²) List.Contains / parentMap.Values.Contains on large brain trees.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve conflicts between AI-1218 (live INVOKE_SUBAGENT subagent nesting)
and main's #287 (dashless canonical Antigravity session/agent ids + AI-1238
format-insensitive --session filter):

- AntigravityImportSource: keep dashless session id + sessionFilter
  normalization (main) alongside INVOKE_SUBAGENT parent map + AI-1218 drift
  counters (PR); doc comment describes both.
- WatchCommand final drain: capture DrainNewLines result for the exit-time
  subagent-link scan (PR) while passing isFinalDrain: true (main).
- Integration test: assert subagent-start carries dashless agent_id (main) and
  match the child transcript by the precise "agent_id":"<dashless>" field (PR
  concern — the root batch now embeds the child id in its INVOKE_SUBAGENT step).
- Unit tests: session id assertions now expect the dashless canonical form;
  Children stay dashed (filesystem paths).
- README: live-nesting-via-INVOKE_SUBAGENT wording (PR) plus main's
  --session dashed/dashless note.

Unit 2588/2588, Antigravity integration 5/5; NativeAOT publish clean (0 IL2026/IL3050).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alexeyzimarev alexeyzimarev merged commit d475aaf into main Jul 7, 2026
5 checks passed
@alexeyzimarev alexeyzimarev deleted the tonyyoung/ai-1218-antigravity-subagent-link branch July 7, 2026 16:43
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.

2 participants