Skip to content

Commit 2e11aa7

Browse files
mjacobswesm
andauthored
Support Antigravity CLI SQLite sessions (#580)
## Summary *Resolves a breaking change in antigravity-cli release 1.0.4 (2026-06-01) release.* Adds support for newer Antigravity CLI sessions that are stored as per-conversation SQLite `.db` files under `~/.gemini/antigravity-cli/conversations/`. This keeps the existing encrypted `.pb` + `agy-reader` sidecar flow intact, while allowing Agentsview to index the newer DB-backed sessions directly. ## Changes - Discover Antigravity CLI `conversations/<uuid>.db` files and prefer them over same-ID `.pb` files. - Classify `.db`, `.db-wal`, and `.db-shm` filesystem events back to the canonical DB source. - Parse CLI SQLite DB steps through the existing Antigravity SQLite/protobuf path. - Include SQLite WAL/SHM mtimes in effective file info so live session updates resync reliably. - Clean noisy internal strings from DB-decoded transcripts, including step headers, UUIDs, opaque request IDs, model placeholders, AGY config paths, and tool-action JSON blobs. - Tighten prompt replacement from `history.jsonl` so sparse or blank history rows do not misassign prompts. - Update README guidance for the two Antigravity CLI storage formats. ## Root Cause Recent Antigravity CLI releases now write `conversations/<uuid>.db` instead of the older encrypted `conversations/<uuid>.pb` format. Agentsview only discovered `.pb` files for Antigravity CLI, so newer sessions were invisible. Once DB discovery was added, the old protobuf string-extraction heuristic surfaced internal protocol strings in the visible transcript; this PR filters those strings and prefers human-facing prompt text. ## Follow-up Tracked in #579: newer `history.jsonl` rows may omit `conversationId`, so DB sessions can parse cleanly but still lack project inference from history. That should be handled conservatively by matching prompt/timestamp proximity rather than assuming every nearby history row belongs to a DB. ## Validation - `go test ./internal/parser ./internal/sync` - Tested against local Antigravity CLI `.db` sessions before and after the code changes; transcript parsing produced a clean first user message without step headers or internal IDs. --------- Co-authored-by: Wes McKinney <wesmckinn+git@gmail.com>
1 parent 234d7a2 commit 2e11aa7

8 files changed

Lines changed: 1038 additions & 88 deletions

File tree

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,14 @@ Each directory can be overridden with an environment variable. See the
255255

256256
### Antigravity CLI: high-resolution transcripts
257257

258-
By default, agentsview indexes Antigravity CLI sessions in **summary mode**:
259-
your prompts from `history.jsonl` plus any plain-text artifacts under `brain/`
260-
(plans, walkthroughs, checkpoints). Assistant turns and tool calls live in
261-
AES-GCM-encrypted `.pb` files and are not visible in this mode.
262-
263-
To unlock full transcripts, run
258+
Antigravity CLI sessions now appear in two on-disk formats. Newer releases
259+
store conversation trajectories as SQLite `.db` files, which agentsview indexes
260+
directly. Older releases stored assistant turns and tool calls in
261+
AES-GCM-encrypted `.pb` files; for those sessions, agentsview falls back to
262+
**summary mode** using your prompts from `history.jsonl` plus any plain-text
263+
artifacts under `brain/` (plans, walkthroughs, checkpoints).
264+
265+
To unlock full transcripts for older `.pb` sessions, run
264266
[agy-reader](https://github.com/mjacobs/agy-reader) alongside agentsview.
265267
agy-reader talks to the local Antigravity daemon, decrypts each conversation,
266268
and writes a `<uuid>.trajectory.json` sidecar next to the encrypted `.pb` file.

internal/db/db.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,22 @@ import (
2828
// trigger a non-destructive re-sync (mtime reset + skip cache
2929
// clear) so existing session data is preserved.
3030
//
31-
// Bumped to 30: Hermes parser no longer treats cost_status
31+
// Bumped to 32: Antigravity DB parsers now filter internal
32+
// protocol strings from visible message content, remove raw step
33+
// headers, prefer prompt-like user text, and merge matching
34+
// Antigravity CLI history prompts when DB decoding drops short user
35+
// turns. Existing Antigravity DB rows need re-parsing so previously
36+
// indexed noisy or assistant-only transcripts are rewritten.
37+
//
38+
// (31: Copilot shutdown usage events use positional DedupKey to
39+
// handle multi-segment sessions correctly.)
40+
//
41+
// (30: Hermes parser no longer treats cost_status
3242
// "included" as a confident $0 when cost_source is "none"/empty (its
3343
// default for models it does not price, e.g. gpt-5.5). Such rows now
3444
// leave cost_usd nil so they are catalog-priced. Existing Hermes rows
3545
// need re-parsing so their usage cost reflects the catalog instead of a
36-
// baked-in $0.
46+
// baked-in $0.)
3747
//
3848
// (29: secret findings now record tool_result_event
3949
// coordinates by the persisted slice position (matching
@@ -102,9 +112,7 @@ import (
102112
//
103113
// (17: Codex <skill> template filtering.)
104114
// (16: <turn_aborted> system messages.)
105-
// (31: Copilot shutdown usage events use positional DedupKey to
106-
// handle multi-segment sessions correctly.)
107-
const dataVersion = 31
115+
const dataVersion = 32
108116

109117
const tokenCoverageRepairStatsKey = "token_coverage_repair_v1"
110118

internal/parser/antigravity.go

Lines changed: 164 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8+
"regexp"
89
"sort"
910
"strings"
1011
"time"
@@ -24,6 +25,10 @@ import (
2425

2526
const antigravityIDPrefix = "antigravity:"
2627

28+
var antigravityUUIDLikeRE = regexp.MustCompile(
29+
`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`,
30+
)
31+
2732
// DiscoverAntigravitySessions returns one DiscoveredFile per
2833
// conversations/<uuid>.db under the IDE root.
2934
func DiscoverAntigravitySessions(root string) []DiscoveredFile {
@@ -172,43 +177,61 @@ func ParseAntigravitySession(
172177
}
173178

174179
func loadAntigravitySteps(db *sql.DB) ([]ParsedMessage, error) {
180+
result, err := loadAntigravityStepsWithRawCount(db)
181+
if err != nil {
182+
return nil, err
183+
}
184+
return result.messages, nil
185+
}
186+
187+
type antigravityStepLoadResult struct {
188+
messages []ParsedMessage
189+
rawStepCount int
190+
}
191+
192+
func loadAntigravityStepsWithRawCount(
193+
db *sql.DB,
194+
) (antigravityStepLoadResult, error) {
175195
rows, err := db.Query(
176196
`SELECT idx, step_type, step_payload FROM steps ` +
177197
`ORDER BY idx`,
178198
)
179199
if err != nil {
180-
return nil, fmt.Errorf("query steps: %w", err)
200+
return antigravityStepLoadResult{}, fmt.Errorf("query steps: %w", err)
181201
}
182202
defer rows.Close()
183-
var out []ParsedMessage
203+
var result antigravityStepLoadResult
184204
for rows.Next() {
185205
var (
186206
idx int
187207
stepType int
188208
payload []byte
189209
)
190210
if err := rows.Scan(&idx, &stepType, &payload); err != nil {
191-
return nil, fmt.Errorf("scan step: %w", err)
211+
return antigravityStepLoadResult{}, fmt.Errorf("scan step: %w", err)
192212
}
213+
result.rawStepCount++
193214
msg, ok := decodeAntigravityStep(idx, stepType, payload)
194215
if !ok {
195216
continue
196217
}
197-
out = append(out, msg)
218+
result.messages = append(result.messages, msg)
198219
}
199220
if err := rows.Err(); err != nil {
200-
return nil, fmt.Errorf("iterate steps: %w", err)
221+
return antigravityStepLoadResult{}, fmt.Errorf("iterate steps: %w", err)
201222
}
202-
return out, nil
223+
return result, nil
203224
}
204225

205226
// decodeAntigravityStep extracts a ParsedMessage from one step's
206227
// protobuf payload. Without an upstream .proto we use heuristics:
207228
// - role: step_type 14 has been observed to carry user prompts.
208229
// Every other type is rendered as assistant. (TODO: refine
209230
// when more sample data is available.)
210-
// - content: concatenation of every UTF-8 string >= 20 chars
211-
// found anywhere in the payload tree, deduped.
231+
// - content: best-effort human-facing strings found in the
232+
// payload tree. Internal ids, local Antigravity config paths,
233+
// model placeholders, and duplicate payload echoes are filtered
234+
// out. User-input steps prefer a single prompt-like string.
212235
// - timestamp: earliest google.protobuf.Timestamp-shaped field.
213236
func decodeAntigravityStep(
214237
idx, stepType int, payload []byte,
@@ -220,7 +243,9 @@ func decodeAntigravityStep(
220243
if err != nil || len(fields) == 0 {
221244
return ParsedMessage{}, false
222245
}
223-
strs := dedupeStrings(agProtoCollectStrings(fields, 20))
246+
strs := cleanAntigravityStepStrings(
247+
dedupeStrings(agProtoCollectStrings(fields, 20)), stepType,
248+
)
224249
ts := earliestAntigravityTimestamp(fields)
225250
if len(strs) == 0 {
226251
return ParsedMessage{}, false
@@ -229,10 +254,7 @@ func decodeAntigravityStep(
229254
if stepType == 14 {
230255
role = RoleUser
231256
}
232-
header := fmt.Sprintf(
233-
"[step %d · type %d]", idx, stepType,
234-
)
235-
content := header + "\n" + strings.Join(strs, "\n\n")
257+
content := strings.Join(strs, "\n\n")
236258
return ParsedMessage{
237259
Role: role,
238260
Content: content,
@@ -254,6 +276,135 @@ func dedupeStrings(in []string) []string {
254276
return out
255277
}
256278

279+
func cleanAntigravityStepStrings(
280+
strs []string, stepType int,
281+
) []string {
282+
var cleaned []string
283+
for _, s := range strs {
284+
s = strings.TrimSpace(s)
285+
if isNoisyAntigravityStepString(s) {
286+
continue
287+
}
288+
cleaned = append(cleaned, s)
289+
}
290+
cleaned = dedupeStrings(cleaned)
291+
if stepType == 14 {
292+
if prompt := bestAntigravityUserPrompt(cleaned); prompt != "" {
293+
return []string{prompt}
294+
}
295+
}
296+
return cleaned
297+
}
298+
299+
func isNoisyAntigravityStepString(s string) bool {
300+
if s == "" {
301+
return true
302+
}
303+
if antigravityUUIDLikeRE.MatchString(s) {
304+
return true
305+
}
306+
if strings.HasPrefix(s, "MODEL_PLACEHOLDER_") {
307+
return true
308+
}
309+
if strings.HasPrefix(s, "{") &&
310+
(strings.Contains(s, `"toolAction"`) ||
311+
strings.Contains(s, `"toolSummary"`) ||
312+
strings.Contains(s, `"DirectoryPath"`)) {
313+
return true
314+
}
315+
if looksLikeAntigravityOpaqueID(s) {
316+
return true
317+
}
318+
if strings.HasPrefix(s, "file:///home/") {
319+
return true
320+
}
321+
if strings.HasPrefix(s, "/home/") &&
322+
strings.Contains(s, "/.gemini/") {
323+
return true
324+
}
325+
if strings.HasPrefix(s, "/Users/") &&
326+
strings.Contains(s, "/.gemini/") {
327+
return true
328+
}
329+
if strings.HasPrefix(s, `C:\Users\`) &&
330+
strings.Contains(s, `\.gemini\`) {
331+
return true
332+
}
333+
if strings.HasPrefix(s, "command(") ||
334+
strings.HasPrefix(s, "execute_url(") ||
335+
strings.HasPrefix(s, "read_url(") ||
336+
strings.HasPrefix(s, "mcp(") {
337+
return true
338+
}
339+
return false
340+
}
341+
342+
func looksLikeAntigravityOpaqueID(s string) bool {
343+
if strings.ContainsAny(s, " \n\t") {
344+
return false
345+
}
346+
if len(s) < 16 || len(s) > 128 {
347+
return false
348+
}
349+
var alpha, digit, symbol int
350+
for _, r := range s {
351+
switch {
352+
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z':
353+
alpha++
354+
case r >= '0' && r <= '9':
355+
digit++
356+
case r == '_' || r == '-' || r == '.':
357+
symbol++
358+
default:
359+
return false
360+
}
361+
}
362+
if alpha+digit+symbol != len(s) {
363+
return false
364+
}
365+
if digit == len(s) || digit+symbol == len(s) {
366+
return true
367+
}
368+
return alpha > 0 && digit > 0
369+
}
370+
371+
func bestAntigravityUserPrompt(strs []string) string {
372+
var best string
373+
bestScore := -1
374+
for _, s := range strs {
375+
score := antigravityPromptScore(s)
376+
if score > bestScore {
377+
best = s
378+
bestScore = score
379+
}
380+
}
381+
if bestScore <= 0 {
382+
return ""
383+
}
384+
return best
385+
}
386+
387+
func antigravityPromptScore(s string) int {
388+
trimmed := strings.TrimSpace(s)
389+
if trimmed == "" || isNoisyAntigravityStepString(trimmed) {
390+
return -1
391+
}
392+
score := len(trimmed)
393+
if strings.ContainsAny(trimmed, " \n\t") {
394+
score += 50
395+
}
396+
if strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[") {
397+
score -= 100
398+
}
399+
if strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "file://") {
400+
score -= 100
401+
}
402+
if !strings.ContainsAny(trimmed, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") {
403+
score -= 100
404+
}
405+
return score
406+
}
407+
257408
// earliestAntigravityTimestamp walks the field tree and returns
258409
// the earliest plausible google.protobuf.Timestamp value.
259410
// Plausible = seconds field in the year 2000..2100 range.

0 commit comments

Comments
 (0)