Skip to content

Wire Antigravity session logs via a runtime conversation→worktree map (CROW-1107) - #1109

Merged
dhilgaertner merged 2 commits into
mainfrom
feature/crow-1107-wire-antigravity-collector
Aug 24, 2026
Merged

Wire Antigravity session logs via a runtime conversation→worktree map (CROW-1107)#1109
dhilgaertner merged 2 commits into
mainfrom
feature/crow-1107-wire-antigravity-collector

Conversation

@dhilgaertner

Copy link
Copy Markdown
Contributor

Closes #1107

Wiring follow-up to the CROW-1097 research spike (PR #1101). Antigravity's durable transcript (~/.gemini/antigravity-cli/brain/<conv-id>/.system_generated/logs/transcript_full.jsonl) records no cwd on any line, so the shared cwdFilter path can't attribute it — #1097 kept [] on those grounds. This PR wires it via the attribution path #1097 identified: a runtime conversation→worktree map Crow builds from its own hooks.

How it works

  • Capture (EngineRouter hook-event handler): when an Antigravity hook fires, record conversationId → worktree into AntigravityConversationMap. The worktree is taken from Crow's own session ownership (primaryWorktree(for:)), never the payload — so there is no possibility of misattribution. Best-effort, deduped, never blocks or fails the hook.
  • Read (AntigravityAgent.logSources): load the map, return exactly this worktree's transcript_full.jsonl files as .file/.jsonl sources (already NDJSON — no normalizer). A transcript with no map entry is dropped, never guessed (same invariant as Codex's cwd filter).
  • Harness typing: first-class internal LogSyncHarness.antigravity, wire-collapsed to unknown (mirrors .grok; server DB CHECK — corveil#2426 — doesn't accept it yet).
  • Backfill reuses the same map: reconstructAntigravity looks each transcript's conversationId up for its worktree (map hit → attributed like Codex/Grok; map miss → low/orphan, never guessed). No SQLite-protobuf reader.
  • Docs: session-log-collector.md + harness-transcript-locations.md moved from Deferred to Wired (adds a "runtime-map" attribution mode).

⚠️ Hard-gate deviation — please read

The ticket's Step-1 hard gate is "install + live-verify the hook payload fields before wiring — do not wire against docs alone." agy cannot be installed or run on this machine (crow agents listavailable: false; not on PATH; brain dir absent; running it needs Google-Sign-In/GCP auth). Per the ticket owner's decision ("Wire defensively + caveat"), the design mitigates the un-verifiable payload rather than trusting it:

  • The worktree comes from Crow's session ownership, never workspacePaths — so the ticket's "workspacePaths reliably equals the launch worktree" verification is moot.
  • Only conversationId is read from the payload (across candidate spellings conversationId/conversation_id); the transcript location is otherwise derived from the documented brain-dir template. A wrong payload field name yields "no map entry" (⇒ nothing uploaded), never a misattribution.
  • Every code path + doc is caveated "docs-derived; payload field names pending live-verify against a running agy before Tier-2 promotion."

Remaining pre-Tier-2 step: confirm the hook payload field names + brain-dir template against a live agy.

Tests

  • CrowCore (786): AntigravityConversationMapTests, AntigravityHomeTests, BackfillScannerTests (map hit → attributed, miss → low), updated harness-typing tests.
  • CrowAntigravity (44): AntigravityAgentLogSourcesTests (map hit → one source; empty/other-worktree → []).
  • CrowEngine: AntigravityHookMapCaptureTests (records the owned worktree, not the payload's workspacePaths; non-antigravity records nothing).
  • CrowDaemon: LogSyncCollectorTests / BackfillServiceTests green.
  • make build passes.

🤖 Generated with Claude Code

@dgershman dgershman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code & Security Review

Critical Issues (if any)

None.

Architecture / Existing Patterns

  • Existing pathway: CodingAgent.logSourcesLogSyncCollector / BackfillScanner.assemble. Codex attributes a global store via cwdFilter; Grok partitions by encoded cwd. CROW-1097 already identified that Antigravity is neither, and named the follow-up: a runtime conversationId → worktree map filled from Crow's own hooks. This PR extends that pathway rather than inventing a second collector.
  • The map lives in CrowCore (AntigravityConversationMap) so CrowEngine can write it and CrowAntigravity can read it without a new package edge. Worktree is taken from primaryWorktree(for:), not workspacePaths — that matches the hook-event ownership model and is covered by AntigravityHookMapCaptureTests.
  • No architecture finding. The new "runtime-map" attribution mode is the right shape for a global store with no cwd.

Security Review

Strengths:

  • Worktree attribution is Crow-owned, not payload-owned; a map miss drops the file instead of guessing (same invariant as Codex's cwd filter).
  • .antigravity is first-class internally and collapsed to unknown on the wire, so the server CHECK (corveil#2426) still accepts the upload.
  • Official hook docs confirm conversationId is a top-level common field on every event, so the extractor in EngineRouter.swift is the right field (camelCase), not a guess at a nested object.
  • hook-event remains local-only; logsync stays opt-in / default-off.

Concerns:

  • Yellow — payload transcriptPath is treated as an authoritative filesystem locator, not a hint. The PR's own invariant is that only conversationId is trusted from the unverified hook payload, and that the durable file is "derived from the conversation id regardless" (AntigravityConversationMap.swift:100-102). The implementation does the opposite whenever a hint is present:

    1. Every Antigravity hook carries transcriptPath (Google's common-input table). The "optional" hint is therefore set on every record(...) call (EngineRouter.swift:88-95, 1447-1453).
    2. preferredTranscript (AntigravityConversationMap.swift:174-181) uses dirname(hint)/transcript_full.jsonl and ignores brainDir. Confirmed against the existing test preferredTranscriptRedirectsToFullBesideARecordedTruncatedFile, which asserts /brain/C/.../transcript_full.jsonl while brainDir is /other/brain.
    3. transcripts() existence-checks that candidate and continues with no fallback to AntigravityHome.transcriptPath(conversationID:brainDir:) (AntigravityConversationMap.swift:158-161).
    4. Official docs illustrate transcriptPath as ~/.gemini/antigravity/brain/<uuid>/.../transcript.jsonl. FileManager.fileExists(atPath:) does not expand ~ (reproduced: returns false). A tilde-prefixed hint silently drops a conversation whose derived path would have hit.
    5. The hint is not confined to brainDir, so a payload path of /tmp/whatever/transcript.jsonl selects /tmp/whatever/transcript_full.jsonl for collection. Local-only RPC + opt-in logsync keep this from being a remote file-read, but it still contradicts "only conversationId is trusted."

    Should-fix: derive the collectable file from conversationId + brainDir (reject /, .., and empty ids). If the truncated→full rewrite is worth keeping, expand tildes, require the resolved path to be under brainDir, and fall back to the derived path when the hint misses.

Code Quality

  • Green — recordedCache is updated before a successful write (AntigravityConversationMap.swift:219-241). A failed first persist (permissions, disk) poisons the cache for that conversation until daemon restart, so later hooks never retry. Move the cache assignment into the do after write.
  • Green — stale scanner copy. BackfillScanner.swift:7-15 still says "Three harnesses are wired" and that all of them recover cwd from the file/path; Antigravity is a fourth and recovers cwd only from the map.
  • Green — BackfillServiceTests.harnessMappingHelpers still omits .antigravity even though agentKindRawValue gained a case. A missing case would silently stamp Claude's kind on the sidecar; a one-line expect would lock it.
  • Tests for the intended happy path are solid (map hit / miss / other-worktree / owned worktree vs workspacePaths / backfill high vs orphan). swift build passed; CrowCore Antigravity tests, AntigravityAgentLogSourcesTests, and AntigravityHookMapCaptureTests all passed. The docs-derived transcript_full.jsonl vs official transcript.jsonl filename split is an accepted risk and is not graded as a blocker.

Summary Table

Color Meaning Verdict effect
Red Must fix Request changes
Yellow Should fix Request changes
Green Consider Approve allowed

Recommendation: Request Changes — driven by 0 Red, 1 Yellow, 3 Green findings.


🐦‍⬛ Reviewed by Crow via Cursor

@dhilgaertner
dhilgaertner force-pushed the feature/crow-1107-wire-antigravity-collector branch from 1471866 to 7d5eb86 Compare August 24, 2026 17:50
… (CROW-1107)

Antigravity's durable transcript records no cwd, so the shared cwdFilter path
can't attribute it — CROW-1097 deferred it on those grounds. Wire it instead via
a runtime conversation→worktree map: the hook-event handler records
`conversationId → worktree` as Crow's Antigravity hooks fire (with the worktree
taken from Crow's OWN session ownership, never the payload), and
`AntigravityAgent.logSources` reads it back to return exactly that worktree's
`transcript_full.jsonl` files. A transcript with no map entry is dropped, never
guessed — the same invariant as Codex's cwd filter.

- CrowCore: new `AntigravityConversationMap` + `AntigravityHome` (map store + path
  resolution), and a first-class `LogSyncHarness.antigravity` case wire-collapsed
  to `unknown`, mirroring `.grok` (corveil#2426).
- CrowEngine: hook-event handler captures the pairing (best-effort, deduped, never
  blocks the hook); `conversationId` extracted defensively across candidate key
  spellings.
- CrowAntigravity: `AntigravityAgent.logSources` override (`.file`/`.jsonl` — no
  normalizer, already NDJSON).
- Backfill reuses the same map: `reconstructAntigravity` looks each transcript's
  `conversationId` up for its worktree; a map miss → low/orphan.
- Docs: session-log-collector.md + harness-transcript-locations.md, Deferred → Wired.

Hard-gate deviation (user-approved): `agy` is not installable on the dev machines
(closed-source, Google-Sign-In/GCP-authed), so the ticket's Step-1 "live-verify the
hook payload fields" gate could not be met. The design mitigates the un-verifiable
payload rather than trusting it — the worktree comes from session ownership (not the
payload's workspacePaths), and only `conversationId` is read from the payload, so a
wrong field name yields "no map entry" (nothing uploaded), never a misattribution.
Payload field names remain docs-derived and must be confirmed against a live `agy`
before Tier-2 promotion; flagged throughout code + docs.

Tests: swift test green (CrowCore 786, CrowAntigravity 44, CrowEngine hook suites,
CrowDaemon LogSync/Backfill); `make build` passes.

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: 7BD2FBF5-AC06-4E44-80DA-E9D17EB5C904
@dhilgaertner
dhilgaertner force-pushed the feature/crow-1107-wire-antigravity-collector branch from 7d5eb86 to 493a534 Compare August 24, 2026 18:06
@dhilgaertner dhilgaertner added the crow:merge Crow auto-merge on green label Aug 24, 2026
…sationId (CROW-1107)

Review by @dgershman (1 Yellow + 3 Green). Fixes:

- **Yellow — payload `transcriptPath` was an authoritative locator, not a hint.**
  The collectable file is now ALWAYS derived from `conversationId` + `brainDir`
  (`AntigravityHome.transcriptPath`), never from the recorded `transcriptPath`. This
  confines every collected path to `brainDir`, sidesteps the payload's `~` (which
  `fileExists` won't expand) and any out-of-tree location, and matches the stated
  invariant "only conversationId is trusted." Added `isPathSafeConversationID` —
  empty / `.` / `..` / path-separator / NUL ids are rejected at write time and never
  interpolated into a path at read time. `transcriptPath` is retained purely as
  untrusted provenance (documented as such) so the pending live-verify can compare
  agy's reported path against the derived template; `preferredTranscript` (which
  ignored `brainDir` and had no fallback) is removed.
- **Green — dedupe cache poisoned on a failed write.** `recordedCache` is now set
  only after a durable write (and on the no-change-needed path), so a failed first
  persist no longer short-circuits later hooks for that conversation into a cache hit
  that never retries.
- **Green — stale scanner doc.** `BackfillScanner` header now says "Six harnesses".
- **Green — harness-mapping test.** `BackfillServiceTests.harnessMappingHelpers`
  now asserts `.antigravity` for both `uploadFormat` (`.jsonl`) and `agentKindRawValue`.

New tests: hint-is-ignored derivation, unsafe-conversation-id rejection (write + read
+ the predicate), and a failed-write-does-not-poison-cache retry. `swift test` green
across CrowCore / CrowDaemon / CrowAntigravity / CrowEngine; `make build` passes.

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: 7BD2FBF5-AC06-4E44-80DA-E9D17EB5C904
@dhilgaertner

Copy link
Copy Markdown
Contributor Author

Thanks @dgershman — all four findings addressed in 69eb1eb.

Yellow — transcriptPath as an authoritative locator. Went with your primary suggestion: the collectable file is now always derived from conversationId + brainDir (AntigravityHome.transcriptPath), never from the recorded hint. That confines every path to brainDir, sidesteps the ~ non-expansion and the /tmp/whatever escape, and makes the code match the "only conversationId is trusted" invariant. Added isPathSafeConversationID (rejects empty / . / .. / / / \ / NUL) at both write and read time. I kept transcriptPath stored but purely as untrusted provenance — explicitly documented as never-a-locator — so whoever live-verifies against a real agy can diff its reported path against the derived template; preferredTranscript is gone.

Green — cache poisoning. recordedCache is now set only after a durable write (and on the no-op path), so a failed first persist no longer traps later hooks in a cache hit that never retries.

Green — stale scanner doc. Header now reads "Six harnesses" (the body already described Antigravity's map-based attribution).

Green — harness-mapping test. harnessMappingHelpers now asserts .antigravity for uploadFormat and agentKindRawValue.

New tests cover hint-is-ignored derivation, unsafe-id rejection, and the failed-write-doesn't-poison-cache retry. (Note: the review ran against the pre-rebase commit; the branch has since been rebased over the Cursor #1105 and OpenCode #1104 merges.)

@dhilgaertner
dhilgaertner requested a review from dgershman August 24, 2026 18:16

@dgershman dgershman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code & Security Review

Follow-up to the prior request-changes review. The Yellow (payload transcriptPath used as an authoritative locator) and the three Greens are addressed in 69eb1eb.

Critical Issues (if any)

None.

Architecture / Existing Patterns

  • Existing pathway: CodingAgent.logSourcesLogSyncCollector / BackfillScanner.assemble. Codex attributes a global store via cwdFilter; Grok partitions by encoded cwd. CROW-1097 already named this follow-up: a runtime conversationId → worktree map filled from Crow's own hooks. This PR extends that pathway rather than inventing a second collector.
  • The map lives in CrowCore (AntigravityConversationMap) so CrowEngine can write it and CrowAntigravity can read it without a new package edge. Worktree is taken from primaryWorktree(for:), not workspacePaths (EngineRouter.swift, AntigravityHookMapCaptureTests).
  • No architecture finding. The "runtime-map" attribution mode is the right shape for a global store with no cwd.

Security Review

Strengths:

  • Worktree attribution is Crow-owned, not payload-owned; a map miss drops the file instead of guessing (same invariant as Codex's cwd filter).
  • Previous Yellow, verified fixed. transcripts(forWorktreePath:brainDir:) always derives the collectable file from conversationId + brainDir via AntigravityHome.transcriptPath (AntigravityConversationMap.swift). Recorded transcriptPath is stored only as untrusted provenance and is never interpolated. Confirmed against the implementation and by transcriptsIgnoreRecordedHintAndDeriveFromBrainDir (a ~ / out-of-tree hint still returns the brain-dir path).
  • isPathSafeConversationID rejects empty / . / .. / / / \ / NUL at write and read time (AntigravityConversationMap.swift), so an untrusted hook id cannot escape brainDir. Covered by unsafeConversationIDIsNeverInterpolatedIntoAPath.
  • .antigravity is first-class internally and collapsed to unknown on the wire, so the server CHECK (corveil#2426) still accepts the upload.
  • hook-event remains local-only; logsync stays opt-in / default-off.

Concerns:

  • None remaining. The docs-derived conversationId field name and brain-dir template are an accepted, clearly caveated risk (wrong field ⇒ no map entry ⇒ nothing uploaded, never a misattribution) and are not re-graded as a blocker.

Code Quality

  • Previous Green, verified fixed. recordedCache is assigned only after a durable write (and on the no-op path). A failed first persist no longer traps later hooks. Covered by aFailedWriteDoesNotPoisonTheDedupeCache.
  • Previous Green, verified fixed. BackfillScanner header now reads "Six harnesses" and describes Antigravity's map-based attribution.
  • Previous Green, verified fixed. BackfillServiceTests.harnessMappingHelpers asserts .antigravity for uploadFormat (.jsonl) and agentKindRawValue.
  • Tests for map hit / miss / other-worktree / owned worktree vs workspacePaths / backfill high vs orphan all passed. swift build succeeded; CrowCore Antigravity suites, AntigravityAgentLogSourcesTests, AntigravityHookMapCaptureTests, and harnessMappingHelpers are green.

Summary Table

Color Meaning Verdict effect
Red Must fix Request changes
Yellow Should fix Request changes
Green Consider Approve allowed

Recommendation: Approve — driven by 0 Red, 0 Yellow, 0 Green findings.


🐦‍⬛ Reviewed by Crow via Cursor

@dhilgaertner
dhilgaertner merged commit 0b89799 into main Aug 24, 2026
4 checks passed
@dhilgaertner
dhilgaertner deleted the feature/crow-1107-wire-antigravity-collector branch August 24, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crow:merge Crow auto-merge on green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire Antigravity session logs via a runtime conversation→worktree map (CROW-1097 follow-up)

2 participants