Skip to content

[AI-1160] Antigravity ingest (Phase 4, CLI): historical import#262

Merged
alexeyzimarev merged 24 commits into
mainfrom
tonyyoung/ai-1160-antigravity-import
Jul 5, 2026
Merged

[AI-1160] Antigravity ingest (Phase 4, CLI): historical import#262
alexeyzimarev merged 24 commits into
mainfrom
tonyyoung/ai-1160-antigravity-import

Conversation

@realtonyyoung

Copy link
Copy Markdown
Contributor

Draft — stacked on #256 (Phase 2 CLI). Base is tonyyoung/ai-1158-antigravity-cli for a clean diff; retarget to main once #256 merges. Depends on Phase 1 (kcap-server#933) + Phase 2 (#256).

Phase 4 (AI-1160) of Google Antigravity ingest — historical import kcap import --antigravity, the last phase of the epic (parent AI-1150).

What's here (3 commits)

  • AntigravitySubagents.BuildParentMap (Core) — child→parent conversation map from each brain dir's .system_generated/messages/*.json ({sender:child, recipient:parent}). This linkage is unreliable at live child-start (why live nesting is deferred) but complete on disk at import time.
  • AntigravityImportSource — mirrors GeminiImportSource (routed phase, JSONL transcript, watermark-idempotent — no ledger since the transcript is append-only). Discovers ~/.gemini/antigravity/brain/* dirs, treats roots (non-children) as top-level sessions, and imports their children as subagents (subagent-start → child transcript routed by agent_idsubagent-stop, fail-closed). Wired into Program.cs allSources + VendorSelection (turns on the --antigravity import flag deferred from Phase 2).
  • AntigravityImportTests (WireMock) + README/help-import.txt.

Notes for review

  • Session id: the import uses the raw (dashed) conversation id, which the server normalizes to the canonical dashless form in session-start/end, the transcript pipeline, AND the /last-line watermark probe (CanonicalSessionId.Normalize in all four). So a re-import and a live-captured session converge on the same dashless stream; no client-side dedup issue applies to import (unlike the live disable marker, which [AI-1158] Antigravity ingest (Phase 2, CLI): plugin, hook dispatch, watcher, cost backfill #256 fixed).
  • cwd is left null (Antigravity records no machine-readable workspace path in the transcript — same v1 limitation as Gemini; live capture gets it from the hook payload).
  • Cost on import is deferred (a documented follow-up): Antigravity's cost is off-transcript in gen_metadata, so importing it needs completeness tracking so a failed USAGE send after a complete transcript still retries. Imported sessions currently carry content + subagent nesting but not cost; live capture has cost.

Testing

41 antigravity CLI unit tests + 2 import WireMock integration tests green (full import with subagent nesting + AlreadyLoaded idempotency). AOT publish clean on the Phase 2 base.

Linear: AI-1160. Closes # (GitHub issue to be linked).

🤖 Generated with Claude Code

realtonyyoung and others added 10 commits July 3, 2026 08:57
Foundational path helpers for the Google Antigravity vendor (AI-1158, kcap-server
Phase 2). Antigravity data lives under the shared ~/.gemini home in an
`antigravity` subdir: per-conversation JSONL transcript
(brain/<id>/.system_generated/logs/transcript_full.jsonl), inter-agent messages
dir, and a SQLite conversations/<id>.db (tokens/model in gen_metadata). The kcap
capture plugin's hooks.json installs global under ~/.gemini/antigravity-cli/ or
per-workspace under <root>/.agents/. Reuses GeminiPaths.Root (honors
GEMINI_CLI_HOME). 6 unit tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lled

~/.gemini is shared with Google Antigravity (which stores state under
antigravity/ + antigravity-cli/). GeminiPaths.IsInstalled previously returned
true on bare root-dir presence, so an Antigravity-only home falsely read as a
Gemini install. Now require a Gemini-CLI-specific marker (settings.json /
projects.json / tmp/) that Antigravity doesn't create; the sole caller
(SetupCommand) already ORs the PATH probe, so a fresh Gemini install is still
caught. Tests both directions (antigravity-only → false; each marker → true).

AI-1158. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kcap's capture "plugin" for Antigravity is a hooks.json block. Antigravity's
config is a map of user-named blocks → { event → entries }, so kcap owns one
named block and never disturbs user blocks. Two entry shapes (AI-1150 spike):
tool events (PreToolUse/PostToolUse) = [{ matcher, hooks:[…] }]; lifecycle events
(PreInvocation/PostInvocation/Stop) = direct handler list. Payload has no
event-name field, so each event gets a distinct `kcap hook --antigravity <Event>`
command. Installer merges/removes only the kcap block, preserves user blocks,
backs up malformed JSON, and writes a version marker.

AI-1158. 4 unit tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Teach the transcript watcher to tail Antigravity's transcript_full.jsonl:
title-text extractors for USER_INPUT (strip the <USER_REQUEST> envelope +
trailing metadata blocks) and PLANNER_RESPONSE, the IsEvent title-threshold
gate (only conversational steps with text), and antigravity in KnownVendors.
Generalize the idle-timeout session-end path — Antigravity is a GUI whose IDE
process outlives any one conversation, so like the Codex desktop it can't rely
on a per-conversation parent-exit watchdog; ShouldEndOnIdle now fires for both,
with a KCAP_ANTIGRAVITY_IDLE_MINUTES knob.

AI-1158. 6 unit tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tart + watcher)

The kcap Antigravity plugin invokes `kcap hook --antigravity <Event>` with the
payload on stdin. AntigravityHookCommand acts only on PreInvocation: POST
/hooks/session-start/antigravity then ensure a watcher tails the conversation's
transcript_full.jsonl (vendor=antigravity). PreInvocation re-fires per turn — the
server's deterministic lifecycle id collapses the repeats and EnsureWatcherRunning
is a no-op once live. Session-end stays watcher-owned (idle), matching the GUI
lifecycle. Stop/PostInvocation/tool events are no-ops. Dashed conversation ids are
used verbatim for both the session-start payload and the watcher so they resolve to
one stream. Fail-open throughout. Wired into Program.cs dispatch + KCAP_SKIP list.

AI-1158. 9 unit tests green (arg parsing + no-network fail-open routing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire Antigravity into every vendor-registration touchpoint so it installs,
reports, and cleans up like the other eight:
- `kcap plugin install/remove --antigravity` (InstallAntigravity/RemoveAntigravity
  over AntigravityHooksInstaller; --if-installed refresh + kcap-on-PATH precheck +
  marker version), added to ExclusiveTargetFlags.
- `kcap setup` detects Antigravity (~/.gemini/antigravity or PATH), prompts, and
  installs its hooks.json block (CodingAgentsStep.HandleAntigravityHooks +
  --skip-antigravity-hooks).
- `kcap status` reports an Antigravity ✓/✗ column.
- PluginEnvironment.AntigravityHooksJson resolves the global hooks.json path.

`kcap import --antigravity` (VendorSelection) is intentionally left out — historical
import is Phase 4 (AI-1160), so Phase 2 doesn't advertise an import path that
doesn't exist yet.

AI-1158. Full CLI unit suite 2177/2177 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…GE lines

Antigravity keeps per-generation tokens/model in each conversation's SQLite db
(gen_metadata.data protobuf), not the JSONL. Decode it and stream one synthetic
USAGE line per row, which the server maps to AntigravityUsageBackfilledEvent
(priced via models.dev on read; cost never stored — AI-728).

- AntigravityGenMetadata (Core): pure protobuf field-walk, field numbers pinned
  empirically against real Antigravity 2.2.1 blobs (top.1 -> 4 -> {2 input,
  3 output, 5 cache-read}; 19 model id / 21 label). Fail-open: malformed -> null.
  Verified against real on-disk blobs (6 rows, exact match vs an independent decode).
- AntigravityGenMetadataDb (CLI): read-only WAL-tolerant reader over conversations/
  <id>.db, mirrors OpenCodeDb (native-sqlite resolver, kept out of Core/AOT daemon);
  yields USAGE lines for rows past a highwater mark.
- WatchCommand: polls the sibling db each drain (agentId ?? sessionId = conv id),
  streams new rows through the existing SignalR batch. USAGE line numbers live in a
  high band so they never collide with transcript lines; the server derives the event
  id from line content (gen_row), so re-sends are idempotent.

cache-write and reasoning tokens are intentionally omitted (no write count in
Antigravity's implicit caching; reasoning field not yet confidently identified —
follow-up); the server folds reasoning into output, so omission under-counts, never
mis-bills.

AI-1158. 4 decoder unit tests + real-blob verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README: dedicated "Google Antigravity plugin" section (hooks.json block, live
  capture, idle session-end + KCAP_ANTIGRAVITY_IDLE_MINUTES, .db cost, subagents
  captured standalone, live-only for now); add --antigravity to the plugin flag
  lists, setup detection, --skip-*-hooks list, and uninstall description.
- help-plugin/setup/status/hook.txt: --antigravity flag, --skip-antigravity-hooks,
  status column, and the `kcap hook --antigravity <Event>` contract.
- uninstall: remove the kcap block from ~/.gemini/antigravity-cli/hooks.json
  (plan line + `plugin remove --antigravity`).

Import surfaces (VendorSelection, `kcap import` docs) intentionally left out —
historical import is Phase 4 (AI-1160).

AI-1158. Full CLI unit suite green (one unrelated import-timing flake passes in
isolation); AOT publish clean (no IL2026/IL3050).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…atcher

CLI→server integration (AI-1159 test layer for the AI-1158 hook command): a
PreInvocation drives a real POST to /hooks/session-start/antigravity with the
enriched payload (session_id, hook_event_name, antigravity_version, profile
default_visibility), and an excluded workspace path short-circuits before any POST.
The watcher spawn that follows the POST is neutralized by pre-seeding a live watcher
pid file so EnsureWatcherRunning no-ops (mirrors CodexSessionStartVisibilityTests).

2 integration tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, varint guards

- [blocking] Canonical dashless id end-to-end (C2): `kcap watch` and `kcap disable`
  normalize ids to dashless, but the hook forwarded the dashed conversationId, so
  transcript/USAGE and disable ran under a different id than session-start. The hook
  now uses the dashless id for session_id, the watcher key, and disable; the watcher
  derives the sibling gen_metadata db from the transcript path (which keeps the real
  conversation id) via AntigravityPaths.ConversationDbFromTranscript.
- [blocking] Fail-open payload reads (Q1): JsonNode.GetValue<string>() threw on a
  non-string shape; use a safe accessor so a malformed control-hook payload no-ops.
- Control hook exits 0 on missing event (Q2), not 1.
- USAGE gen-watermark advances only after the batch send succeeds (C1) — a failed
  send used to skip those cost rows forever; now they re-read next drain. USAGE is
  only injected outside the below-threshold buffering phase.
- Antigravity streams past-threshold from the start (C5) — session-start is posted
  before the watcher, so a short (<10-line) conversation must still idle-end + post
  session-end instead of lingering Active.
- Varint/length overflow guards in the gen_metadata decoder (C4): reject a token
  varint above long.MaxValue (no negative counts) and a length past the buffer.

Deferred (documented follow-ups): antigravity-specific in-flight signal for the idle
policy (C3 — no such signal in the transcript; 60m window).

34 unit + 2 integration antigravity tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 3, 2026

Copy link
Copy Markdown

AI-1160

…e in-flight guard

- ConversationDbFromTranscript now validates every path segment (filename
  transcript_full.jsonl, logs, .system_generated, brain) so an unexpected transcriptPath
  fails open (null) instead of being mapped to a guessed conversations/<x>.db.
- S4: the watcher stamps each synthetic USAGE line with the conversation's most-recent
  transcript created_at, so the backfill event's recency reflects the turn, not the
  event-store write time. (Pairs with the server deriving USAGE event ids from gen_row, so
  re-emits dedupe rather than re-stamp.)
- C3: track Antigravity tool calls in flight (PLANNER_RESPONSE tool_calls vs result steps)
  and suppress the idle-timeout session-end while one is running — a long command produces
  no transcript line between its call and result, so the Codex-only in-flight signal (always
  false for Antigravity) would otherwise end the session mid-turn.

42 unit + 2 integration antigravity tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@realtonyyoung realtonyyoung force-pushed the tonyyoung/ai-1160-antigravity-import branch from f1806ab to 08de2eb Compare July 3, 2026 17:48
realtonyyoung and others added 4 commits July 3, 2026 14:14
…tion

ConversationDbFromTranscript walks up the transcript path with Path.GetDirectoryName
(which canonicalizes separators on Windows: /fake/parent -> \fake\parent) while
ConversationDb builds via Path.Combine (which preserves the input's forward slashes),
so the two produced the same file with different separator styles — a brittle string
compare that only failed on windows-latest. Compare via Path.GetFullPath so the
assertion normalizes separators on any OS. Production behavior was already correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AntigravitySubagents.BuildParentMap scans each brain dir's
.system_generated/messages/*.json ({sender:child, recipient:parent}) into a
child->parent map. This linkage is written when a child reports back, so it's
unreliable at live child-start (nesting deferred there) but complete at IMPORT
time — the basis for nesting subagents under their parent in Phase 4. Roots are
conversations that are never a sender. Best-effort: malformed/self messages skipped.

AI-1160. 3 unit tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tigravity`

Historical import of Antigravity conversations, mirroring GeminiImportSource
(routed phase, JSONL transcript, watermark-based idempotency — no ledger since the
transcript is append-only). Discovers brain dirs, uses AntigravitySubagents to pick
roots (non-children) and attach each root's children, and imports them as subagents
under the parent (subagent-start -> transcript@agentId -> subagent-stop, fail-closed).
Conversation id is used verbatim as the session id so a live-captured session and a
re-import dedupe to one stream. cwd is left null (no workspace path in the transcript,
same v1 limitation as Gemini). Wired into Program.cs allSources + VendorSelection
(the deferred `--antigravity` import flag now turns on).

Cost injection on import (gen_metadata -> USAGE) is a deliberate follow-up: it needs
completeness tracking so a failed USAGE send after a complete transcript still retries.

AI-1160. 4 discovery unit tests green; CLI builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AntigravityImportTests (WireMock): drives discover -> classify -> import over an
  on-disk brain tree with a root + a linked subagent conversation, asserting the full
  lifecycle (session-start/antigravity -> parent transcript -> subagent-start ->
  child transcript routed by agent_id -> subagent-stop -> session-end/antigravity,
  all vendor=antigravity) and that a second run is AlreadyLoaded (watermark idempotency).
- README + help-import.txt: `kcap import --antigravity`; updated the Antigravity
  section (import now nests subagents via the on-disk messages linkage; cwd empty;
  cost-on-import is a follow-up).

AI-1160. 2 import integration tests green; full CLI unit suite green apart from the
known ImportChainsAsync timing flake (passes in isolation).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@realtonyyoung realtonyyoung force-pushed the tonyyoung/ai-1160-antigravity-import branch from 08de2eb to 108195d Compare July 3, 2026 18:14

@realtonyyoung realtonyyoung left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed only the three commits unique to PR #262 on top of #256. I found issues in Antigravity subagent retry/idempotency, child watermark resume, non-tree parent-map handling, and the import help text. Per request, I did not run tests or builds.


public async Task<ImportOutcome> ImportSessionAsync(
ImportCommand.SessionClassification c, ImportContext ctx, CancellationToken ct) {
if (c.Status == ImportCommand.ClassificationStatus.AlreadyLoaded) return ImportOutcome.Skipped;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Returning here means an AlreadyLoaded root never reaches ImportChildrenAsync. If the first import posts the parent transcript and session-end but a child was skipped (watermark probe failure, subagent-start failure, missing transcript, or transcript POST failure), the next run classifies the root from the parent watermark as AlreadyLoaded and permanently skips the child. That breaks the intended child-retry behavior; child completeness needs to participate in classification, or AlreadyLoaded roots with children need a child repair pass before skipping.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3280754. ImportSessionAsync no longer returns early for AlreadyLoaded: it now runs ImportChildrenAsync as a repair pass (each child classifies independently by its own (rootId, childId) HWM) before reporting Skipped, so a subagent skipped on a prior run is retried instead of lost. New test: ImportSession_AlreadyLoaded_root_repairs_a_missing_child.

try {
await SessionImporter.SendTranscriptBatches(
httpClient: client, baseUrl: baseUrl, sessionId: rootId,
filePath: childTranscript, agentId: childId, startLine: 0, vendor: Vendor,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here the child HWM is applied as a line-number offset while still reading from line 0. For a child whose server HWM is 1, the next repair sends local lines 0 and 1 again as line_numbers 2 and 3, duplicating already imported content instead of resuming at raw line 2. Antigravity is append-only JSONL like the parent/Gemini path, so this should use the HWM as startLine (hwm + 1) rather than re-numbering the whole child transcript.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3280754. Children now resume via startLine: hwm + 1 with lineNumberOffset: 0 (removed), so surviving lines keep their true file position — matching the parent/Gemini resume. Also skips a child whose HWM already covers its last import-relevant line before touching any hook. New test: ImportChildren_resumes_by_line_position_without_shifting_numbers asserts a resumed child posts line_numbers:[1], not the shifted [1,2].

if (!string.IsNullOrEmpty(sender)
&& !string.IsNullOrEmpty(recipient)
&& !string.Equals(sender, recipient, StringComparison.Ordinal)
&& !map.ContainsKey(sender!)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping the first parent for any sender makes invalid/non-tree linkages look valid. Because discovery later drops every key from top-level roots and only imports direct Children for each root, a chain like P<-C<-G imports C under P but never imports G, and a cycle A<->B produces no roots at all; conflicting parents are also nondeterministic because enumeration order is not guaranteed. We should either reject/ignore non-tree edges (multiple parents, cycles, child that is also a parent) or traverse/import recursively so these conversations are not silently lost or mis-nested.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3280754. BuildParentMap is now deterministic on conflicts (smallest-ordinal parent wins). Added ResolveTopLevelAncestor (cycle-safe walk-up — a cycle member resolves to itself) and BuildRootDescendants (root → all transitive descendants). Discovery enumerates every brain dir and nests grandchildren+; so P<-C<-G imports both C and G under P, and A<->B import standalone rather than vanishing. New tests cover deterministic conflict, deep-chain, cycle, and transitive discovery.

Pi — ~/.pi/agent/sessions/.../<timestamp>_<sid>.jsonl (badlogic/pi-mono)
kcap walks all seven the same way.
Antigravity — ~/.gemini/antigravity/brain/<id>/.system_generated/logs/transcript_full.jsonl
kcap walks all eight the same way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This now says all eight, but Program registers nine import sources and the vendor list below includes both OpenCode and Antigravity. The source table above also still omits the OpenCode DB path, so kcap import --help understates what the command actually walks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3280754. Help text now says "all nine", adds OpenCode/Antigravity to the intro sentence, and adds the OpenCode SQLite db source line (~/.local/share/opencode/opencode.db) to the path table.

…pair, resume-by-position

Fixes the four findings from the Codex review of the Phase 4 draft:

1. AlreadyLoaded roots now still run the child-repair pass, so a subagent
   skipped on a prior run (watermark probe / subagent-start / transcript POST
   failure) is retried instead of lost forever.
2. Child subagents resume by FILE POSITION (startLine) numbered by their true
   position (offset 0), matching the parent's resume + live capture. The prior
   code fed the child HWM as lineNumberOffset and re-sent the whole file shifted,
   corrupting $lineNumber and the watermark for genuinely-new lines. Fully
   ingested children are now skipped before any hook POST.
3. Subagent linkage handles non-tree data: BuildParentMap is deterministic on
   multi-parent conflicts (smallest-ordinal parent wins); ResolveTopLevelAncestor
   walks deep chains cycle-safely; BuildRootDescendants groups all transitive
   descendants (children, grandchildren, …) under their top-level root, so a
   deep chain isn't lost and a cycle imports standalone rather than vanishing.
   Discovery now enumerates every brain dir and nests transitive descendants.
4. help-import.txt: corrected the vendor count (nine) and added the missing
   OpenCode SQLite db source line.

Tests: AntigravitySubagentsTests (deterministic conflict, ResolveTopLevelAncestor
chain/cycle, BuildRootDescendants transitive/cycle); AntigravityImportSourceTests
(transitive-descendant discovery); AntigravityImportTests (AlreadyLoaded root
repairs a missing child; child resume numbers lines by true position, not shifted).

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

@realtonyyoung realtonyyoung left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-reviewed commit 3280754 and its interaction with the Antigravity import path. The four prior fixes look correct for parent skip/child repair, child resume-by-position, transitive subagent grouping, and help text/vendor count, but I found one remaining lifecycle retry issue in the new fully-loaded-child skip path. Per request, I did not run tests or builds.

continue; // unreadable child transcript — retry on re-import
}
if (childLastImportable is null) continue; // empty child
if (chwm is { } done && done >= childLastImportable) continue; // already fully ingested

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This HWM-only skip makes fully ingested child content imply a complete child lifecycle, but subagent-stop is still best-effort below: if the child transcript POST succeeds and then the stop hook returns non-2xx or throws, the next import sees done >= childLastImportable here and skips before retrying any hook. That leaves the subagent without its completion event permanently, even though the failure happened after content import and should be repairable on re-import. Consider either treating stop failure as an incomplete child or still posting an idempotent stop for fully-loaded children when lifecycle completeness is not otherwise known.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a125072. A fully-ingested child no longer returns before the stop hook: it now re-posts subagent-stop (idempotent server-side — deterministic SubagentCompleted id via AgentLifecycleDeterministicId) so a prior run's failed-after-content stop is repaired, while an already-recorded stop dedupes. subagent-start is not re-posted (content is fail-closed behind it, so its success is implied by the ingested content) and the child content is not re-sent. New test: ImportChildren_fullyIngestedChild_reposts_idempotent_stop_without_resending_content asserts subagent-stop is posted but neither transcript nor subagent-start.

Addresses the re-review finding at AntigravityImportSource.cs:240. The
fully-loaded-child skip added in the prior commit returned before subagent-stop,
so a child whose content POST succeeded but whose stop hook then failed
(non-2xx / throw) on a prior run was left without a completion event permanently
— the content HWM only tracks transcript lines, not lifecycle.

Now a fully-ingested child still re-posts subagent-stop before continuing. It's
idempotent server-side (deterministic SubagentCompleted id via
AgentLifecycleDeterministicId), so a prior failure is repaired and an
already-recorded stop dedupes. subagent-start is not re-posted — content is
fail-closed behind it, so its success is implied by the ingested content. Child
content is not re-sent.

Test: ImportChildren_fullyIngestedChild_reposts_idempotent_stop_without_resending_content
asserts a fully-ingested child posts subagent-stop but neither transcript nor subagent-start.

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

@realtonyyoung realtonyyoung left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-reviewed commit a125072. The focused change prevents content and subagent-start from being resent and keeps AlreadyLoaded-root outcome as Skipped, but I found one remaining repair gap around the server's transient active-agent guard for stop-only retries. I did not run tests or builds.

// id), so a prior failure is repaired and an already-recorded stop dedupes.
// subagent-start is not re-posted — content is fail-closed behind it, so its
// presence is implied by the ingested content (AI-1160 review).
await PostHookAsync(client, baseUrl, "subagent-stop",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This stop-only repair still depends on the server remembering the child as active. Server-side RecordAgentStopAsync returns before appending SubagentCompleted when SessionStateTracker.TryClearActiveAgent(sessionId, agentId) is false, and that tracker is transient in-memory state. So if the first import successfully posted subagent-start and child content, then subagent-stop failed and the server restarts before the re-import, this branch posts only stop, the server treats it as inactive/duplicate, and no completion event is written; the child HWM remains content-complete, so future imports repeat the no-op. To make the repair durable across process boundaries, the fully-ingested branch needs to re-post the idempotent start before stop (or the server stop endpoint needs to allow deterministic stop repair without an active mark).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 57065cd. The fully-ingested branch now re-posts subagent-start THEN subagent-stop. Start re-marks the agent active (idempotent — deterministic SubagentStarted id via AgentLifecycleDeterministicId; already-active is a no-op), so a stop that follows clears the mark and appends SubagentCompleted even after a server restart wiped the transient active mark. This makes the completion repair durable across process boundaries. Child content is not re-sent. Test updated: ImportChildren_fullyIngestedChild_reposts_idempotent_start_and_stop_without_resending_content (asserts start-before-stop, no transcript).

…or fully-ingested children

Addresses the re-review finding at AntigravityImportSource.cs:249. The prior
stop-only repair depended on the server still holding the child's active-agent
mark, which is transient in-memory (SessionStateTracker): RecordAgentStopAsync
returns before appending SubagentCompleted when TryClearActiveAgent is false. So
if a prior import posted subagent-start + content, then subagent-stop failed AND
the server restarted before the re-import, a stop-only re-post is a no-op and the
child stays uncompleted forever (HWM is content-complete, so it repeats).

The fully-ingested branch now re-posts subagent-start THEN subagent-stop. Start
re-marks the agent active (idempotent: deterministic SubagentStarted id;
already-active is a no-op) so the following stop clears it and appends the
(idempotent) SubagentCompleted, making the repair durable across a server
restart. Child content is still not re-sent.

Test updated: ImportChildren_fullyIngestedChild_reposts_idempotent_start_and_stop_without_resending_content
asserts start-before-stop ordering and no transcript re-send.

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

@realtonyyoung realtonyyoung left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Found one remaining issue: the new start-before-stop repair still needs strict lifecycle payloads so server-side start failures are visible to the importer. I did not run tests or builds.

// already-active is a no-op) so the following stop clears it and appends the
// (idempotent, deterministic) SubagentCompleted. Child content is not re-sent
// (AI-1160 review).
if (await PostHookAsync(client, baseUrl, "subagent-start",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P2] Make the repair start hook strict. This branch treats a successful subagent-start POST as proof that the server re-marked the child active before posting stop, but BuildSubagentPayload does not send strict=true. On the server, SubagentStartHook.Strict defaults false, and HandleSubagentStart returns 200 even when RecordAgentStartAsync throws after rolling back the active mark. In that case this code still posts subagent-stop, which no-ops without an active agent, so the durable stop repair can silently fail. Please include strict=true on the synthetic subagent lifecycle payloads so lifecycle write failures surface as non-2xx and are retried.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 93be269. The repair now sends strict=true on both synthetic subagent lifecycle payloads (BuildSubagentPayload gained a strict param). HandleSubagentStart returns 500 under strict when RecordAgentStartAsync throws+rolls back the active mark, so a start that actually failed no longer reads as 200 — stop is posted only when start truly re-marked the agent active, and a re-import retries otherwise. Test asserts the repair subagent-start body carries "strict":true.

… repair

Addresses the re-review finding at AntigravityImportSource.cs:252. The
start-before-stop repair gated on a 200 from subagent-start as proof the server
re-marked the child active. But HandleSubagentStart returns a fail-open 200 even
when RecordAgentStartAsync throws and rolls back the active mark (only strict=true
makes it return 500). So a start that actually failed was treated as success and
the following stop no-oped without an active agent — the durable repair could
silently fail.

The repair now sends strict=true on both synthetic subagent lifecycle payloads
(BuildSubagentPayload gains a strict param), so a server-side lifecycle write
failure surfaces as non-2xx: stop is only posted when start truly re-marked the
agent, and a re-import retries otherwise.

Test updated to assert the repair subagent-start carries "strict":true.

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

@realtonyyoung realtonyyoung left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Found one remaining issue: the strict repair payloads are now gated correctly for start, but the stop payload still needs the full SubagentStopHook shape before the server can honor strict on the stop route. I did not run tests or builds.

return p;
}

static JsonObject BuildSubagentPayload(string eventName, string parentSid, string agentId, string transcriptPath, bool strict = false) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P1] Build the full stop-hook payload before relying on strict. This builder is now used for the strict repair subagent-stop POST, but it still only emits the start-shaped fields. The server stop route binds SubagentStopHook, whose required fields include stop_hook_active, agent_transcript_path, and last_assistant_message; Cursor/Gemini/OpenCode send those because an incomplete body can be rejected before HandleSubagentStop runs. If this Antigravity stop payload is rejected at binding, strict is never honored by the stop handler, PostHookAsync returns false, and this repair repeats start->failed-stop forever while the child remains without SubagentCompleted. Please split start/stop payload builders or add the stop-only fields for subagent_stop, and cover the stop body in the repair test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a0ee6d2. Split the payload builder into BuildSubagentStartPayload / BuildSubagentStopPayload; the stop builder now emits the full SubagentStopHook shape (stop_hook_active, agent_transcript_path, last_assistant_message) mirroring GeminiSubagentDiscovery.BuildStopPayload, so the strict stop body binds and the strict handler actually runs (no more start→failed-stop loop). Test asserts the repair stop body carries those fields alongside strict:true.

Addresses the re-review finding at AntigravityImportSource.cs:310. The strict
repair now posts subagent-stop, but the shared payload builder only emitted the
start-shaped fields. The server stop route binds SubagentStopHook, whose required
fields include stop_hook_active, agent_transcript_path, and last_assistant_message;
an incomplete body is rejected at binding BEFORE HandleSubagentStop runs, so strict
is never honored, PostHookAsync returns false, and the repair loops
start->failed-stop forever while the child stays without SubagentCompleted.

Split the builder into BuildSubagentStartPayload / BuildSubagentStopPayload; the
stop builder emits the full shape (mirroring GeminiSubagentDiscovery.BuildStopPayload).
Test asserts the repair stop body carries stop_hook_active / agent_transcript_path /
last_assistant_message alongside strict.

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

Copy link
Copy Markdown
Contributor Author

NO FINDINGS

Base automatically changed from tonyyoung/ai-1158-antigravity-cli to main July 4, 2026 13:27
realtonyyoung and others added 3 commits July 4, 2026 10:02
…gin.json marker

The live-capture install shipped in #256 wrote hooks.json to ~/.gemini/antigravity-cli/
(the `agy` CLI's config root). The Antigravity GUI never reads that dir, so its hooks
never fired — proven by a GUI re-test where nothing was captured until the plugin was
moved to the GUI's plugin location.

Fix: install to ~/.gemini/config/plugins/kcap/ as a proper plugin — a required
plugin.json manifest (without which the GUI ignores the directory) plus the same
hooks.json kcap block. Verified end-to-end: the GUI now fires PreInvocation, kcap
spawns a watcher, and the session captures + ends on idle.

- AntigravityPaths: GuiConfigRoot/PluginDir/GlobalPluginManifest; GlobalHooksJson +
  WorkspaceHooksJson now resolve under the plugin dir (dropped unused CliRoot).
- AntigravityHooks.BuildPluginManifest() (+ stable PluginVersion "1.0.0" — the GUI was
  only verified to accept a plain x.y.z, so no prerelease/build-metadata suffix).
- AntigravityHooksInstaller writes/removes plugin.json; Remove drops the emptied
  hooks.json instead of leaving an orphan {}.
- help-plugin.txt / README / uninstall message updated to the new location.
- Tests updated for the plugin-dir paths + manifest write/remove.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lifecycle to hooks.json

Two findings from the flow review of #262:

- Install swallowed plugin.json write failures then still stamped the version marker
  and reported success — recreating the AI-1158 false-success mode (a hooks.json the
  GUI ignores because the required manifest was never written). plugin.json is now
  written FIRST and its failure propagates so the install aborts (InstallAntigravityHooks
  catches → reports failure). The version marker stays best-effort (kcap's own bookkeeping).

- Remove deleted plugin.json unconditionally, even when a user-authored hooks block was
  preserved — silently disabling those remaining hooks. Remove now keeps plugin.json while
  a hooks.json remains for the GUI to load, deleting it only once the plugin is fully gone.

Tests: Install throws when the manifest can't be written; Remove keeps the manifest when
user blocks remain. 58 Antigravity unit tests pass; AOT clean; E2E install/remove verified
(clean for kcap-only, preserved for co-located user blocks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@realtonyyoung realtonyyoung marked this pull request as ready for review July 4, 2026 20:50
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add Antigravity historical import (kcap import --antigravity) with subagent nesting

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add kcap import --antigravity to backfill Antigravity brain transcripts via watermark
 idempotency.
• Infer parent/child conversation nesting from on-disk messages and import children as subagents.
• Fix Antigravity live plugin install path by writing a required GUI plugin.json manifest.
Diagram

graph TD
  CLI["kcap import --antigravity"] --> SRC[["AntigravityImportSource"]] --> FS[("Antigravity brain dirs")]
  SRC --> SUB[["AntigravitySubagents"]] --> FS
  SRC --> HWM{{"GET /api/sessions/:id/last-line"}} --> SRV["kcap-server"]
  SRC --> HK{{"POST /hooks/*"}} --> SRV

  subgraph Legend
    direction LR
    _cli["CLI cmd"] ~~~ _mod[["CLI/Core module"]] ~~~ _fs[("Filesystem") ] ~~~ _api{{"HTTP endpoint"}} ~~~ _srv["Server"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Factor a shared routed-import base class (Gemini/Antigravity)
  • ➕ Reduces duplicated watermark/statistics/payload boilerplate across vendors
  • ➕ Centralizes edge-case handling (last-relevant-line logic, resume semantics, retry behavior)
  • ➖ Adds abstraction overhead while only 2 vendors currently need it
  • ➖ Harder to keep vendor-specific lifecycle quirks readable
2. Add an import ledger instead of watermark-only idempotency
  • ➕ More robust recovery if server watermark endpoint changes or is unavailable
  • ➕ Could track lifecycle completion separately from content watermark
  • ➖ Extra local state to manage and migrate
  • ➖ Less aligned with append-only transcript model and existing Gemini approach
3. Inject cost from gen_metadata during import (now)
  • ➕ Imported sessions would match live capture completeness (usage/cost)
  • ➖ Requires completeness tracking to safely retry cost sends independently of transcript
  • ➖ Increases failure modes and review surface for this phase

Recommendation: Current approach (Gemini-style watermark idempotency + routed transcript ingestion) is the right fit for Antigravity’s append-only JSONL transcripts and matches server normalizer expectations. The most valuable follow-up is cost injection once an idempotent completeness model exists; factoring a shared routed-import base can wait until another vendor shares the same shape.

Files changed (16) +1314 / -60

Enhancement (4) +550 / -3
AntigravitySubagents.csResolve Antigravity child→parent conversation map for import-time nesting +101/-0

Resolve Antigravity child→parent conversation map for import-time nesting

• Adds import-time subagent linkage resolution by scanning each brain dir’s '.system_generated/messages/*.json'. Builds a deterministic child→parent map, provides cycle-safe ancestor resolution, and groups conversations into root→descendants for session/subagent import.

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

AntigravityImportSource.csImplement Antigravity historical import with watermark resume and subagent routing +444/-0

Implement Antigravity historical import with watermark resume and subagent routing

• Adds a full 'IImportSource' implementation that discovers Antigravity brain transcripts, classifies sessions via '/last-line' watermarks using the last import-relevant line, and imports lifecycle + transcript batches. Imports children as subagents (routed by 'agent_id') with fail-closed start/stop ordering, strict lifecycle repair for fully-ingested children, and correct resume-by-file-position semantics.

src/Capacitor.Cli/Commands/AntigravityImportSource.cs

VendorSelection.csAdd '--antigravity' vendor filter parsing and validation +4/-3

Add '--antigravity' vendor filter parsing and validation

• Extends vendor flag recognition to include '--antigravity', including unknown-option validation and hinting logic consistency with other vendor-specific flags.

src/Capacitor.Cli/Commands/VendorSelection.cs

Program.csWire AntigravityImportSource into import source registry +1/-0

Wire AntigravityImportSource into import source registry

• Registers 'AntigravityImportSource' in the CLI’s 'allSources' list so 'kcap import --antigravity' is selectable via vendor filtering and participates in discovery/classification/import flows.

src/Capacitor.Cli/Program.cs

Bug fix (3) +103 / -19
AntigravityHooks.csAdd Antigravity GUI plugin manifest builder and version +20/-0

Add Antigravity GUI plugin manifest builder and version

• Introduces a 'plugin.json' manifest payload ('BuildPluginManifest') and a conservative dotted 'PluginVersion'. This supports installing Antigravity capture as a GUI-recognized plugin directory rather than a CLI-only config location.

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

AntigravityHooksInstaller.csInstall/remove Antigravity capture as a GUI plugin dir (plugin.json + hooks.json) +56/-10

Install/remove Antigravity capture as a GUI plugin dir (plugin.json + hooks.json)

• Changes install to write a required 'plugin.json' first (failing the install if it cannot be written), then merges/writes 'hooks.json' and the version marker. Updates removal behavior to delete 'hooks.json' when it becomes empty, and to keep 'plugin.json' as long as any hooks remain (to avoid disabling user blocks).

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

AntigravityPaths.csMove Antigravity hook/plugin paths under GUI config root +27/-9

Move Antigravity hook/plugin paths under GUI config root

• Replaces the previous CLI-root hooks path with GUI plugin paths under '~/.gemini/config/plugins/kcap/'. Adds helpers for GUI config root, plugin dir, plugin manifest, and workspace plugin dir; updates global/workspace hooks.json locations accordingly.

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

Tests (5) +623 / -6
AntigravityImportTests.csWireMock integration coverage for Antigravity import + subagent lifecycle repair +282/-0

WireMock integration coverage for Antigravity import + subagent lifecycle repair

• Adds end-to-end WireMock tests asserting hook call ordering for root + subagent import, AlreadyLoaded root child repair behavior, strict lifecycle reposting without resending content for fully ingested children, and correct resume line numbering for partial child imports.

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

AntigravityHooksInstallerTests.csUnit tests for plugin.json manifest lifecycle and failure modes +58/-0

Unit tests for plugin.json manifest lifecycle and failure modes

• Adds tests ensuring 'Install' writes 'plugin.json', 'Remove' deletes it only when safe, install fails hard if manifest cannot be written, and removal preserves the manifest when user-authored hook blocks remain.

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

AntigravityImportSourceTests.csUnit tests for Antigravity import discovery and relevance filtering +131/-0

Unit tests for Antigravity import discovery and relevance filtering

• Validates that only roots are discovered as top-level sessions, descendants are attached transitively, session filtering is honored, and 'IsImportRelevantLine' matches only event-producing step types used for watermark comparisons.

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

AntigravityPathsTests.csUpdate path tests for GUI plugin directory layout +16/-6

Update path tests for GUI plugin directory layout

• Replaces CLI-root expectations with assertions that plugin dir, hooks.json, and plugin.json live under the GUI config root. Updates workspace hooks location tests to match '.agents/plugins/kcap/hooks.json'.

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

AntigravitySubagentsTests.csUnit tests for child→parent mapping, determinism, and cycle safety +136/-0

Unit tests for child→parent mapping, determinism, and cycle safety

• Adds coverage for building the parent map from messages files, ignoring malformed/self-linkage, resolving deep ancestors, handling cycles safely, and grouping root descendants for import nesting.

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

Documentation (4) +38 / -32
README.mdDocument Antigravity import flag and historical import behavior +8/-7

Document Antigravity import flag and historical import behavior

• Adds Antigravity to the supported import vendor list and documents 'kcap import --antigravity'. Updates Antigravity plugin notes to reflect GUI plugin installation location and that historical import nests subagents and is watermark-idempotent.

README.md

help-import.txtExpose '--antigravity' vendor filter and Antigravity transcript location +15/-11

Expose '--antigravity' vendor filter and Antigravity transcript location

• Updates import help text to include Antigravity and its transcript path under '~/.gemini/antigravity/brain/.../transcript_full.jsonl'. Adds '--antigravity' to the vendor filters list alongside existing sources.

src/Capacitor.Cli.Core/Resources/help-import.txt

help-plugin.txtUpdate Antigravity plugin help to reflect GUI plugin installation +13/-12

Update Antigravity plugin help to reflect GUI plugin installation

• Changes help text to describe installing/removing the Antigravity capture plugin under '~/.gemini/config/plugins/kcap/' (manifest + hooks file). Removes references to the deprecated '~/.gemini/antigravity-cli/hooks.json' location.

src/Capacitor.Cli.Core/Resources/help-plugin.txt

UninstallCommand.csUpdate uninstall messaging for Antigravity plugin directory removal +2/-2

Update uninstall messaging for Antigravity plugin directory removal

• Adjusts uninstall output and plugin removal invocation comments to reflect removing the Antigravity GUI plugin directory rather than deleting a hooks block in the old CLI location.

src/Capacitor.Cli/Commands/UninstallCommand.cs

@realtonyyoung realtonyyoung requested review from alexeyzimarev and removed request for alexeyzimarev July 4, 2026 20:54
@qodo-code-review

qodo-code-review Bot commented Jul 4, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. AlreadyLoaded skips lifecycle ✓ Resolved 🐞 Bug ≡ Correctness
Description
AntigravityImportSource.ImportSessionAsync returns early for AlreadyLoaded, so it never re-posts
session-start/antigravity and session-end/antigravity. If a prior run advanced the transcript
watermark but lifecycle posting failed (especially session-end), the import pipeline will keep
routing AlreadyLoaded sessions for repair but Antigravity will never repair the missing lifecycle
events.
Code

src/Capacitor.Cli/Commands/AntigravityImportSource.cs[R164-172]

+        // An AlreadyLoaded root's parent transcript is fully imported, but a subagent may have
+        // been skipped on a prior run (watermark probe / subagent-start / transcript POST
+        // failure). Children classify independently by their own HWM, so still run a child-repair
+        // pass — complete children are no-ops, incomplete ones resume — then report Skipped.
+        // Without this, a once-skipped child would be lost forever (AI-1160 review).
+        if (c.Status == ImportCommand.ClassificationStatus.AlreadyLoaded) {
+            await ImportChildrenAsync(ctx.HttpClient, ctx.BaseUrl, c.SessionId, c.SourceMeta!, ct);
+            return ImportOutcome.Skipped;
+        }
Evidence
Antigravity currently returns before any parent lifecycle POSTs on AlreadyLoaded, while the import
orchestrator explicitly routes AlreadyLoaded routed-phase sessions so sources can re-assert
lifecycle after watermark-only progress. Cursor demonstrates the expected pattern by always posting
lifecycle and letting the transcript leg become a no-op for AlreadyLoaded.

src/Capacitor.Cli/Commands/AntigravityImportSource.cs[159-172]
src/Capacitor.Cli/Commands/ImportCommand.cs[880-894]
src/Capacitor.Cli/Commands/CursorImportSource.cs[353-413]

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.ImportSessionAsync` short-circuits on `ClassificationStatus.AlreadyLoaded` and only repairs children. This prevents lifecycle repair for the parent session (session-start/session-end), despite `ImportCommand` routing `AlreadyLoaded` routed-phase sessions specifically to enable lifecycle re-assertion.

## Issue Context
- `ImportCommand` includes `AlreadyLoaded` in the routed set so sources can re-emit lifecycle hooks when a previous run advanced the transcript watermark but lifecycle failed.
- Cursor (and Gemini) keep posting lifecycle even when there is no transcript content to send.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/AntigravityImportSource.cs[159-204]
- src/Capacitor.Cli/Commands/ImportCommand.cs[880-894]

## Suggested fix
- In the `AlreadyLoaded` branch, post parent lifecycle hooks (at least `session-end/antigravity`, ideally both start+end) using the same payload builders, then run `ImportChildrenAsync`, and only then return `ImportOutcome.Skipped`.
- Consider failing the import (return `ImportOutcome.Failed`) if lifecycle repair POSTs fail, so reruns can retry repair (mirrors Cursor’s hard-fail posture for lifecycle).

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



Remediation recommended

2. Unescaped agentId query ✓ Resolved 🐞 Bug ☼ Reliability
Description
FetchServerLastLineAsync interpolates agentId directly into the ?agentId= query string without
URL escaping. If agentId ever contains reserved characters, watermark probing for subagents can
hit the wrong endpoint or fail, breaking resume/repair logic.
Code

src/Capacitor.Cli/Commands/AntigravityImportSource.cs[R419-422]

+    static async Task<int?> FetchServerLastLineAsync(
+            HttpClient http, string baseUrl, string sessionId, string? agentId, CancellationToken ct) {
+        var url = $"{baseUrl}/api/sessions/{sessionId}/last-line" + (agentId is not null ? $"?agentId={agentId}" : "");
+        using var resp = await http.GetWithRetryAsync(url, ct: ct);
Evidence
Antigravity uses raw ?agentId={agentId} while Cursor escapes agentId for the same endpoint,
indicating the safe/expected contract is to encode the query parameter.

src/Capacitor.Cli/Commands/AntigravityImportSource.cs[419-422]
src/Capacitor.Cli/Commands/CursorImportSource.cs[634-639]

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

## Issue description
`FetchServerLastLineAsync` constructs `/last-line?agentId=...` using raw string interpolation. This is fragile for IDs containing reserved URI characters.

## Issue Context
Cursor’s implementation of the same probe escapes `agentId` with `Uri.EscapeDataString`, which avoids malformed URLs and unintended query parsing.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/AntigravityImportSource.cs[419-429]

## Suggested fix
- Build the URL using `Uri.EscapeDataString(agentId)` (and consider using `UriBuilder`/`QueryHelpers` style composition if available).
- Keep behavior identical when `agentId` is null (no query string).

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


3. Full scan despite filter 🐞 Bug ➹ Performance
Description
DiscoverAsync always builds a global parent map by scanning every
brain/*/.system_generated/messages/*.json before applying FilterSession, so `kcap import
--session <id> --antigravity` still performs O(total history) filesystem IO. This scan also cannot
be cancelled because BuildParentMap has no CancellationToken integration.
Code

src/Capacitor.Cli/Commands/AntigravityImportSource.cs[R55-71]

+        // Resolve every conversation to its top-level ancestor: roots (no parent) import as
+        // sessions; ALL transitive descendants (children, grandchildren, …) import as their
+        // root's subagents. Cycle-/non-tree-safe (BuildRootDescendants) so a deep chain isn't
+        // lost and a cycle imports standalone rather than vanishing. Linkage is complete on disk.
+        var parentMap = AntigravitySubagents.BuildParentMap(_home, _geminiCliHome);
+        var convIds   = Directory.EnumerateDirectories(BrainRoot).Select(Path.GetFileName).OfType<string>().ToList();
+        var byRoot    = AntigravitySubagents.BuildRootDescendants(convIds, parentMap);
+
+        var sinceUtc = filters.Since is { } since
+            ? new DateTimeOffset(since.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc), TimeSpan.Zero)
+            : (DateTimeOffset?)null;
+
+        foreach (var (convId, descendants) in byRoot) {
+            ct.ThrowIfCancellationRequested();
+
+            if (filters.FilterSession is { } fs && !string.Equals(convId, fs, StringComparison.Ordinal)) continue;
+
Evidence
Discovery constructs parentMap and byRoot before checking FilterSession, and the parent map
builder enumerates all brain directories and message JSON files without any cancellation checks.

src/Capacitor.Cli/Commands/AntigravityImportSource.cs[51-71]
src/Capacitor.Cli.Core/Antigravity/AntigravitySubagents.cs[22-54]

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 discovery eagerly scans the entire brain tree’s messages to build a complete child→parent map even when the caller requests a single session via `FilterSession`. This makes targeted imports unnecessarily slow and ignores cancellation during that scan.

## Issue Context
- `AntigravitySubagents.BuildParentMap` enumerates all brain dirs and parses all message JSON files.
- `DiscoverAsync` only checks `filters.FilterSession` after building the full map.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/AntigravityImportSource.cs[51-71]
- src/Capacitor.Cli.Core/Antigravity/AntigravitySubagents.cs[22-54]

## Suggested fix
- Add a `CancellationToken` parameter to `AntigravitySubagents.BuildParentMap` and check it inside both directory and file loops.
- In `DiscoverAsync`, if `filters.FilterSession` is set, consider a fast path:
 - validate that `brain/<id>` exists and the transcript exists;
 - either (a) skip nesting for that fast path, or (b) compute descendants via a targeted scan rather than scanning every brain directory.
- At minimum, pass the cancellation token through so Ctrl+C (or pipeline cancellation) can interrupt the scan.

ⓘ 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/AntigravityImportSource.cs Outdated
Comment thread src/Capacitor.Cli/Commands/AntigravityImportSource.cs
Comment thread src/Capacitor.Cli/Commands/AntigravityImportSource.cs
…d agentId, cancellable discovery scan

Three findings from qodo on #262:

- AlreadyLoaded skipped parent lifecycle: ImportSessionAsync returned early for
  AlreadyLoaded, repairing only children. If a prior run advanced the transcript
  watermark then failed session-end, the session was stuck "running" with no repair
  path. Now the AlreadyLoaded branch re-posts session-start → repairs children →
  re-posts session-end (all idempotent via deterministic lifecycle ids), and returns
  Failed if either lifecycle POST fails so a re-run retries (mirrors Cursor).

- Unescaped agentId query: FetchServerLastLineAsync now Uri.EscapeDataString(agentId)
  in the /last-line?agentId= probe (matches CursorImportSource).

- Un-cancellable discovery scan: BuildParentMap now takes a CancellationToken and
  checks it between brain dirs and message files; DiscoverAsync threads its token
  through. (The full scan is required for correct nesting — a child records its
  parent, so finding a root's descendants means reading every link — but is now
  interruptible.)

Tests: updated both AlreadyLoaded integration tests to expect lifecycle re-assertion;
added BuildParentMap cancellation unit test. 59 unit + 5 import integration green, AOT clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alexeyzimarev alexeyzimarev merged commit 15cedf4 into main Jul 5, 2026
5 checks passed
@alexeyzimarev alexeyzimarev deleted the tonyyoung/ai-1160-antigravity-import branch July 5, 2026 11: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