@adhdev/daemon-core 0.9.82-rc.442 → 0.9.82-rc.444
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/high-family/types.d.ts +4 -0
- package/dist/commands/router.d.ts +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +551 -63
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +543 -63
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-ledger.d.ts +39 -1
- package/dist/mesh/mesh-node-identity.d.ts +15 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +15 -1
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +28 -2
- package/package.json +2 -2
- package/src/commands/chat-commands-read.ts +41 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +4 -2
- package/src/commands/high-family/mesh-events.ts +11 -2
- package/src/commands/high-family/mesh-status.ts +102 -9
- package/src/commands/high-family/types.ts +5 -1
- package/src/commands/router.ts +19 -2
- package/src/index.ts +1 -1
- package/src/mesh/coordinator-prompt.ts +1 -0
- package/src/mesh/mesh-ledger.ts +185 -0
- package/src/mesh/mesh-node-identity.ts +106 -18
- package/src/mesh/mesh-reconcile-loop.ts +22 -1
- package/src/providers/chat-message-normalization.ts +1 -1
- package/src/providers/cli-provider-instance.ts +130 -17
- package/src/providers/native-history/antigravity-cli-transcript.ts +348 -25
- package/src/providers/native-history/dispatcher.ts +33 -14
|
@@ -12,11 +12,35 @@
|
|
|
12
12
|
* { source: 'USER_EXPLICIT'|'MODEL', type: string, content: string, status: 'DONE'|..., created_at: number }
|
|
13
13
|
*
|
|
14
14
|
* 3. ~/.gemini/antigravity-cli/conversations/<uuid>.pb
|
|
15
|
-
*
|
|
15
|
+
* Legacy protobuf binary — schema not publicly documented. Adapter extracts
|
|
16
16
|
* printable UTF-8 text runs as best-effort content (no proto library needed).
|
|
17
17
|
*
|
|
18
|
+
* 4. ~/.gemini/antigravity-cli/conversations/<uuid>.db ← current format
|
|
19
|
+
* Per-session SQLite database. Recent antigravity migrated conversation
|
|
20
|
+
* storage from .pb (+ brain/*.jsonl) to a per-session SQLite db. The
|
|
21
|
+
* schema is a trajectory of `steps`, NOT a simple messages(role,content)
|
|
22
|
+
* table:
|
|
23
|
+
* steps(idx INTEGER PK, step_type INTEGER, status INTEGER,
|
|
24
|
+
* step_payload BLOB [protobuf], ...)
|
|
25
|
+
* Each `step_payload` is a protobuf message. Empirically (introspected
|
|
26
|
+
* from real stores):
|
|
27
|
+
* - step_type 14 → a USER turn. The prompt text is the largest
|
|
28
|
+
* contiguous UTF-8 run inside the payload (field 19 subtree).
|
|
29
|
+
* - step_type 15 → a MODEL/assistant turn. The assistant's final
|
|
30
|
+
* natural-language answer lives at payload field 20 → field 1
|
|
31
|
+
* (identical to field 8). Field 20 → field 3 is the internal
|
|
32
|
+
* reasoning summary and is intentionally NOT surfaced.
|
|
33
|
+
* - other step types are tool calls / ephemeral system context.
|
|
34
|
+
* We read the blobs with a tiny dependency-free protobuf field walker
|
|
35
|
+
* (no proto schema / codegen needed) and map the two message step types.
|
|
36
|
+
* Because the daemon does NOT read this db, native history previously
|
|
37
|
+
* returned 0 rows for these sessions and read_chat fell back to the
|
|
38
|
+
* pty parser (which only echoes the user's own input) — assistant
|
|
39
|
+
* answers appeared lost even though they were on disk.
|
|
40
|
+
*
|
|
18
41
|
* This adapter provides:
|
|
19
|
-
* - Full coverage
|
|
42
|
+
* - Full coverage from a per-session .db (current format) — preferred.
|
|
43
|
+
* - Full coverage when a brain transcript exists (legacy authoritative source).
|
|
20
44
|
* - Partial coverage (user prompts only) from history.jsonl as fallback.
|
|
21
45
|
* - Best-effort raw-string extraction from .pb files when no other source exists.
|
|
22
46
|
*
|
|
@@ -24,6 +48,7 @@
|
|
|
24
48
|
* ~/.gemini/antigravity-cli/history.jsonl
|
|
25
49
|
* ~/.gemini/antigravity-cli/brain/{uuid}/.system_generated/logs/transcript*.jsonl
|
|
26
50
|
* ~/.gemini/antigravity-cli/conversations/{uuid}.pb
|
|
51
|
+
* ~/.gemini/antigravity-cli/conversations/{uuid}.db
|
|
27
52
|
*
|
|
28
53
|
* OSS code (AGPL-3.0). Must not import from packages/ (proprietary).
|
|
29
54
|
*/
|
|
@@ -31,6 +56,7 @@
|
|
|
31
56
|
import * as fs from 'fs';
|
|
32
57
|
import * as path from 'path';
|
|
33
58
|
import * as os from 'os';
|
|
59
|
+
import { loadBetterSqlite3 } from '../../system/load-better-sqlite3.js';
|
|
34
60
|
|
|
35
61
|
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
36
62
|
|
|
@@ -382,6 +408,232 @@ function parsePbFile(
|
|
|
382
408
|
];
|
|
383
409
|
}
|
|
384
410
|
|
|
411
|
+
// ─── SQLite (.db) conversation reader ────────────────────────────────────────
|
|
412
|
+
//
|
|
413
|
+
// Recent antigravity stores each conversation in a per-session SQLite db at
|
|
414
|
+
// conversations/<uuid>.db. See the file header for the schema. We decode the
|
|
415
|
+
// protobuf `step_payload` blobs with a minimal, dependency-free field walker —
|
|
416
|
+
// we only need two leaf strings (user prompt / assistant answer), so a full
|
|
417
|
+
// proto schema is unnecessary.
|
|
418
|
+
|
|
419
|
+
/** Antigravity step_type values that map to a chat message. */
|
|
420
|
+
const AGY_STEP_TYPE_USER = 14;
|
|
421
|
+
const AGY_STEP_TYPE_MODEL = 15;
|
|
422
|
+
|
|
423
|
+
interface ProtoField {
|
|
424
|
+
field: number;
|
|
425
|
+
wireType: number;
|
|
426
|
+
/** For wireType 2 (length-delimited): the raw bytes. */
|
|
427
|
+
bytes?: Buffer;
|
|
428
|
+
/** For wireType 0 (varint): the value. */
|
|
429
|
+
varint?: number;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Read a base-128 varint starting at `offset`. Returns [value, nextOffset].
|
|
434
|
+
* Values are read as JS numbers (safe: the fields we consume are small).
|
|
435
|
+
*/
|
|
436
|
+
function readVarint(buf: Buffer, offset: number): [number, number] {
|
|
437
|
+
let result = 0;
|
|
438
|
+
let shift = 0;
|
|
439
|
+
let i = offset;
|
|
440
|
+
while (i < buf.length) {
|
|
441
|
+
const byte = buf[i];
|
|
442
|
+
i += 1;
|
|
443
|
+
result += (byte & 0x7f) * Math.pow(2, shift);
|
|
444
|
+
if ((byte & 0x80) === 0) return [result, i];
|
|
445
|
+
shift += 7;
|
|
446
|
+
if (shift > 63) break; // malformed / oversized
|
|
447
|
+
}
|
|
448
|
+
return [result, i];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Decode the top-level fields of a protobuf message. Best-effort: stops on the
|
|
453
|
+
* first malformed byte rather than throwing, so a partially-corrupt blob still
|
|
454
|
+
* yields the fields decoded so far.
|
|
455
|
+
*/
|
|
456
|
+
function decodeProtoFields(buf: Buffer): ProtoField[] {
|
|
457
|
+
const fields: ProtoField[] = [];
|
|
458
|
+
let i = 0;
|
|
459
|
+
while (i < buf.length) {
|
|
460
|
+
const [key, afterKey] = readVarint(buf, i);
|
|
461
|
+
if (afterKey === i) break;
|
|
462
|
+
i = afterKey;
|
|
463
|
+
const field = Math.floor(key / 8);
|
|
464
|
+
const wireType = key & 7;
|
|
465
|
+
if (field <= 0) break;
|
|
466
|
+
if (wireType === 0) {
|
|
467
|
+
const [value, next] = readVarint(buf, i);
|
|
468
|
+
if (next === i) break;
|
|
469
|
+
i = next;
|
|
470
|
+
fields.push({ field, wireType, varint: value });
|
|
471
|
+
} else if (wireType === 2) {
|
|
472
|
+
const [len, afterLen] = readVarint(buf, i);
|
|
473
|
+
i = afterLen;
|
|
474
|
+
if (len < 0 || i + len > buf.length) break;
|
|
475
|
+
fields.push({ field, wireType, bytes: buf.subarray(i, i + len) });
|
|
476
|
+
i += len;
|
|
477
|
+
} else if (wireType === 5) {
|
|
478
|
+
i += 4;
|
|
479
|
+
} else if (wireType === 1) {
|
|
480
|
+
i += 8;
|
|
481
|
+
} else {
|
|
482
|
+
break; // wireType 3/4 (groups) — unused by antigravity payloads
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return fields;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Return the bytes of the first length-delimited field with number `field`. */
|
|
489
|
+
function firstLenField(buf: Buffer, field: number): Buffer | null {
|
|
490
|
+
for (const f of decodeProtoFields(buf)) {
|
|
491
|
+
if (f.field === field && f.wireType === 2 && f.bytes) return f.bytes;
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Heuristic: is this buffer (mostly) printable UTF-8 text? */
|
|
497
|
+
function looksLikeText(buf: Buffer): boolean {
|
|
498
|
+
if (buf.length === 0) return false;
|
|
499
|
+
let printable = 0;
|
|
500
|
+
for (let i = 0; i < buf.length; i++) {
|
|
501
|
+
const b = buf[i];
|
|
502
|
+
// ASCII printable + common whitespace, or any high byte (UTF-8 lead/cont).
|
|
503
|
+
if ((b >= 32 && b <= 126) || b === 9 || b === 10 || b === 13 || b >= 0x80) printable += 1;
|
|
504
|
+
}
|
|
505
|
+
return printable / buf.length >= 0.9;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Antigravity prefixes some assistant answers with a literal `MARKER_V1`
|
|
510
|
+
* sentinel followed by a blank line. Strip it so the user-visible bubble starts
|
|
511
|
+
* at the real text.
|
|
512
|
+
*/
|
|
513
|
+
function stripAnswerMarker(text: string): string {
|
|
514
|
+
return text.replace(/^\s*MARKER_V1\s*/, '');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Extract the assistant's final natural-language answer from a step_type 15
|
|
519
|
+
* payload: field 20 → field 1 (identical to field 8). Field 20 → field 3 is the
|
|
520
|
+
* private reasoning summary and is deliberately skipped. Returns '' if absent
|
|
521
|
+
* (e.g. a pure reasoning / tool-only model step, which carries no user-visible
|
|
522
|
+
* answer text).
|
|
523
|
+
*/
|
|
524
|
+
function extractModelAnswer(payload: Buffer): string {
|
|
525
|
+
const inner = firstLenField(payload, 20);
|
|
526
|
+
if (!inner) return '';
|
|
527
|
+
const answer = firstLenField(inner, 1) ?? firstLenField(inner, 8);
|
|
528
|
+
if (!answer || !looksLikeText(answer)) return '';
|
|
529
|
+
return stripAnswerMarker(answer.toString('utf-8')).trim();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Extract the user prompt from a step_type 14 payload: field 19 → field 2 (the
|
|
534
|
+
* clean prompt text; field 19 → field 3 wraps the same string with a leading
|
|
535
|
+
* newline and is used only as a fallback). The USER_REQUEST XML wrapper, when
|
|
536
|
+
* present, is unwrapped to match the brain-transcript reader's output.
|
|
537
|
+
*/
|
|
538
|
+
function extractUserPrompt(payload: Buffer): string {
|
|
539
|
+
const inner = firstLenField(payload, 19);
|
|
540
|
+
if (!inner) return '';
|
|
541
|
+
const raw = firstLenField(inner, 2) ?? firstLenField(inner, 3);
|
|
542
|
+
if (!raw || !looksLikeText(raw)) return '';
|
|
543
|
+
const text = raw.toString('utf-8').trim();
|
|
544
|
+
if (!text) return '';
|
|
545
|
+
return extractUserRequestContent(text);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
interface AgyDbStepRow {
|
|
549
|
+
idx: number;
|
|
550
|
+
step_type: number;
|
|
551
|
+
step_payload: Buffer | null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Parse a per-session conversations/<uuid>.db (SQLite) into NativeHistoryMessages.
|
|
556
|
+
* Returns null when the db is unreadable, empty, or yields no chat messages.
|
|
557
|
+
*/
|
|
558
|
+
function parseConversationDb(
|
|
559
|
+
filePath: string,
|
|
560
|
+
sessionId: string,
|
|
561
|
+
workspace?: string,
|
|
562
|
+
): NativeHistoryMessage[] | null {
|
|
563
|
+
let db: any;
|
|
564
|
+
try {
|
|
565
|
+
const Database = loadBetterSqlite3();
|
|
566
|
+
db = new Database(filePath, { readonly: true, fileMustExist: true });
|
|
567
|
+
} catch {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
let rows: AgyDbStepRow[];
|
|
572
|
+
try {
|
|
573
|
+
rows = db
|
|
574
|
+
.prepare(
|
|
575
|
+
`SELECT idx, step_type, step_payload
|
|
576
|
+
FROM steps
|
|
577
|
+
WHERE step_type IN (${AGY_STEP_TYPE_USER}, ${AGY_STEP_TYPE_MODEL})
|
|
578
|
+
ORDER BY idx ASC`,
|
|
579
|
+
)
|
|
580
|
+
.all() as AgyDbStepRow[];
|
|
581
|
+
} catch {
|
|
582
|
+
// `steps` table absent / unexpected schema.
|
|
583
|
+
return null;
|
|
584
|
+
} finally {
|
|
585
|
+
try { db.close(); } catch { /* ignore */ }
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (!Array.isArray(rows) || rows.length === 0) return null;
|
|
589
|
+
|
|
590
|
+
const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
|
|
591
|
+
const baseTs = statMtimeMs(filePath) || Date.now();
|
|
592
|
+
const messages: NativeHistoryMessage[] = [];
|
|
593
|
+
|
|
594
|
+
for (const row of rows) {
|
|
595
|
+
const payload = row.step_payload;
|
|
596
|
+
if (!payload || !Buffer.isBuffer(payload) || payload.length === 0) continue;
|
|
597
|
+
|
|
598
|
+
// Steps are ordered by idx; the db carries no per-step timestamp we can
|
|
599
|
+
// trust as ms, so synthesize a monotonically increasing receivedAt that
|
|
600
|
+
// preserves order (idx-derived) around the file mtime.
|
|
601
|
+
const receivedAt = baseTs + messages.length;
|
|
602
|
+
|
|
603
|
+
if (row.step_type === AGY_STEP_TYPE_USER) {
|
|
604
|
+
const content = extractUserPrompt(payload);
|
|
605
|
+
if (!content) continue;
|
|
606
|
+
const msg: NativeHistoryMessage = {
|
|
607
|
+
ts: new Date(receivedAt).toISOString(),
|
|
608
|
+
receivedAt,
|
|
609
|
+
role: 'user',
|
|
610
|
+
content,
|
|
611
|
+
kind: 'standard',
|
|
612
|
+
agent: 'antigravity-cli',
|
|
613
|
+
historySessionId: sessionId,
|
|
614
|
+
};
|
|
615
|
+
if (normalizedWorkspace) msg.workspace = normalizedWorkspace;
|
|
616
|
+
messages.push(msg);
|
|
617
|
+
} else if (row.step_type === AGY_STEP_TYPE_MODEL) {
|
|
618
|
+
const content = extractModelAnswer(payload);
|
|
619
|
+
if (!content) continue; // reasoning-only / tool-only model step — no answer text
|
|
620
|
+
const msg: NativeHistoryMessage = {
|
|
621
|
+
ts: new Date(receivedAt).toISOString(),
|
|
622
|
+
receivedAt,
|
|
623
|
+
role: 'assistant',
|
|
624
|
+
content,
|
|
625
|
+
kind: 'standard',
|
|
626
|
+
agent: 'antigravity-cli',
|
|
627
|
+
historySessionId: sessionId,
|
|
628
|
+
};
|
|
629
|
+
if (normalizedWorkspace) msg.workspace = normalizedWorkspace;
|
|
630
|
+
messages.push(msg);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return messages.length > 0 ? messages : null;
|
|
635
|
+
}
|
|
636
|
+
|
|
385
637
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
386
638
|
|
|
387
639
|
/**
|
|
@@ -390,6 +642,7 @@ function parsePbFile(
|
|
|
390
642
|
* `sessionPath` is the absolute path to one of:
|
|
391
643
|
* - A brain transcript JSONL: ~/.gemini/antigravity-cli/brain/<uuid>/.system_generated/logs/transcript*.jsonl
|
|
392
644
|
* - The shared history.jsonl: ~/.gemini/antigravity-cli/history.jsonl
|
|
645
|
+
* - A conversation SQLite db: ~/.gemini/antigravity-cli/conversations/<uuid>.db
|
|
393
646
|
* - A conversation protobuf: ~/.gemini/antigravity-cli/conversations/<uuid>.pb
|
|
394
647
|
*
|
|
395
648
|
* The session UUID is inferred from the directory name (brain), filename (pb), or
|
|
@@ -430,7 +683,26 @@ export function readSession(
|
|
|
430
683
|
};
|
|
431
684
|
}
|
|
432
685
|
|
|
433
|
-
// ── Case
|
|
686
|
+
// ── Case 2a: .db conversation file (current per-session SQLite format) ───
|
|
687
|
+
if (sessionPath.endsWith('.db')) {
|
|
688
|
+
const dbSessionId = sessionId || path.basename(sessionPath, '.db');
|
|
689
|
+
if (!isUuidLike(dbSessionId)) return null;
|
|
690
|
+
|
|
691
|
+
const messages = parseConversationDb(sessionPath, dbSessionId, workspace);
|
|
692
|
+
if (!messages || messages.length === 0) return null;
|
|
693
|
+
|
|
694
|
+
return {
|
|
695
|
+
messages,
|
|
696
|
+
providerSessionId: dbSessionId,
|
|
697
|
+
source: 'provider-native',
|
|
698
|
+
sourcePath: sessionPath,
|
|
699
|
+
sourceMtimeMs,
|
|
700
|
+
nativeHistoryCoverage: 'full',
|
|
701
|
+
workspace,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ── Case 2b: .pb conversation file (legacy protobuf) ─────────────────────
|
|
434
706
|
if (sessionPath.endsWith('.pb')) {
|
|
435
707
|
const pbSessionId = sessionId || path.basename(sessionPath, '.pb');
|
|
436
708
|
if (!isUuidLike(pbSessionId)) return null;
|
|
@@ -607,34 +879,85 @@ export async function listSessions(_watchPath: string): Promise<NativeHistorySes
|
|
|
607
879
|
seen.add(sessionId);
|
|
608
880
|
}
|
|
609
881
|
|
|
610
|
-
// ── Step 3: .pb
|
|
882
|
+
// ── Step 3: conversations/<uuid>.db and .pb without any other source ────
|
|
883
|
+
//
|
|
884
|
+
// Current antigravity writes per-session SQLite dbs; older sessions kept a
|
|
885
|
+
// .pb protobuf. Both can coexist in conversations/. Discover both, but when
|
|
886
|
+
// the same uuid has a .db, prefer it (full coverage) over the .pb (best
|
|
887
|
+
// effort). brain/history sources already in `seen` still win over either.
|
|
611
888
|
const convRoot = conversationsRoot();
|
|
612
889
|
if (fs.existsSync(convRoot)) {
|
|
613
890
|
let entries: fs.Dirent[] = [];
|
|
614
891
|
try { entries = fs.readdirSync(convRoot, { withFileTypes: true }); } catch { /* ignore */ }
|
|
615
892
|
|
|
893
|
+
// Group by uuid so a .db supersedes a sibling .pb of the same session.
|
|
894
|
+
const byUuid = new Map<string, { db?: string; pb?: string }>();
|
|
616
895
|
for (const entry of entries) {
|
|
617
|
-
if (!entry.isFile()
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
896
|
+
if (!entry.isFile()) continue;
|
|
897
|
+
const dbMatch = /^([0-9a-f-]+)\.db$/i.exec(entry.name);
|
|
898
|
+
const pbMatch = /^([0-9a-f-]+)\.pb$/i.exec(entry.name);
|
|
899
|
+
if (dbMatch && isUuidLike(dbMatch[1])) {
|
|
900
|
+
const g = byUuid.get(dbMatch[1]) ?? {};
|
|
901
|
+
g.db = path.join(convRoot, entry.name);
|
|
902
|
+
byUuid.set(dbMatch[1], g);
|
|
903
|
+
} else if (pbMatch && isUuidLike(pbMatch[1])) {
|
|
904
|
+
const g = byUuid.get(pbMatch[1]) ?? {};
|
|
905
|
+
g.pb = path.join(convRoot, entry.name);
|
|
906
|
+
byUuid.set(pbMatch[1], g);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
for (const [uuid, files] of byUuid.entries()) {
|
|
911
|
+
if (seen.has(uuid)) continue;
|
|
912
|
+
|
|
913
|
+
if (files.db) {
|
|
914
|
+
// Parse the db so listSessions reports accurate counts/preview and the
|
|
915
|
+
// session surfaces with full coverage (assistant answers included).
|
|
916
|
+
const workspace = workspaceBySession.get(uuid);
|
|
917
|
+
const messages = parseConversationDb(files.db, uuid, workspace);
|
|
918
|
+
const dbMtime = statMtimeMs(files.db);
|
|
919
|
+
if (messages && messages.length > 0) {
|
|
920
|
+
const lastMsg = messages[messages.length - 1];
|
|
921
|
+
const firstMsg = messages[0];
|
|
922
|
+
results.push({
|
|
923
|
+
historySessionId: uuid,
|
|
924
|
+
sessionId: uuid,
|
|
925
|
+
sourcePath: files.db,
|
|
926
|
+
sourceMtimeMs: dbMtime,
|
|
927
|
+
messageCount: messages.length,
|
|
928
|
+
firstMessageAt: firstMsg.receivedAt || dbMtime,
|
|
929
|
+
lastMessageAt: lastMsg.receivedAt || dbMtime,
|
|
930
|
+
sessionTitle: lastMsg.content,
|
|
931
|
+
preview: lastMsg.content,
|
|
932
|
+
workspace,
|
|
933
|
+
agent: 'antigravity-cli',
|
|
934
|
+
source: 'provider-native',
|
|
935
|
+
nativeHistoryCoverage: 'full',
|
|
936
|
+
});
|
|
937
|
+
seen.add(uuid);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
// db unreadable/empty (e.g. sqlite binding unavailable) → fall through
|
|
941
|
+
// to the .pb best-effort entry below if one exists.
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (files.pb) {
|
|
945
|
+
const pbMtime = statMtimeMs(files.pb);
|
|
946
|
+
results.push({
|
|
947
|
+
historySessionId: uuid,
|
|
948
|
+
sessionId: uuid,
|
|
949
|
+
sourcePath: files.pb,
|
|
950
|
+
sourceMtimeMs: pbMtime,
|
|
951
|
+
messageCount: 0,
|
|
952
|
+
firstMessageAt: pbMtime,
|
|
953
|
+
lastMessageAt: pbMtime,
|
|
954
|
+
agent: 'antigravity-cli',
|
|
955
|
+
source: 'provider-native',
|
|
956
|
+
nativeHistoryCoverage: 'best-effort',
|
|
957
|
+
partialReason: 'antigravity_cli_pb_raw_text_extraction',
|
|
958
|
+
});
|
|
959
|
+
seen.add(uuid);
|
|
960
|
+
}
|
|
638
961
|
}
|
|
639
962
|
}
|
|
640
963
|
|
|
@@ -97,7 +97,7 @@ function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: strin
|
|
|
97
97
|
switch (reader) {
|
|
98
98
|
case 'claude-cli': return resolveClaudePath(workspace, sessionId);
|
|
99
99
|
case 'codex-cli': return resolveCodexPath(workspace, sessionId, sessionStartedAtMs);
|
|
100
|
-
case 'antigravity-cli': return resolveAntigravityPath(workspace);
|
|
100
|
+
case 'antigravity-cli': return resolveAntigravityPath(workspace, sessionId);
|
|
101
101
|
case 'hermes-cli': return resolveHermesPath(workspace, sessionId);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
@@ -225,21 +225,40 @@ function resolveRealPath(value: string): string {
|
|
|
225
225
|
try { return fs.realpathSync(value); } catch { return value; }
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
function resolveAntigravityPath(workspace: string): string | null {
|
|
228
|
+
function resolveAntigravityPath(workspace: string, sessionId: string): string | null {
|
|
229
229
|
void workspace;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
.
|
|
238
|
-
.
|
|
239
|
-
for (const e of entries) {
|
|
240
|
-
const t = path.join(e.p, '.system_generated', 'logs', 'transcript.jsonl');
|
|
241
|
-
if (fs.existsSync(t)) return t;
|
|
230
|
+
const agyRoot = path.join(os.homedir(), '.gemini', 'antigravity-cli');
|
|
231
|
+
|
|
232
|
+
// (1) Exact session bind: current antigravity writes a per-session SQLite db
|
|
233
|
+
// at conversations/<uuid>.db. If the caller knows the session id and the
|
|
234
|
+
// db exists, bind straight to it — this is the authoritative source and
|
|
235
|
+
// carries the assistant answers the brain/pty path was losing.
|
|
236
|
+
if (sessionId && isUuidLikeSessionId(sessionId)) {
|
|
237
|
+
const dbPath = path.join(agyRoot, 'conversations', `${sessionId}.db`);
|
|
238
|
+
if (fs.existsSync(dbPath)) return dbPath;
|
|
242
239
|
}
|
|
240
|
+
|
|
241
|
+
// (2) brain/<uuid>/.system_generated/logs/transcript.jsonl (legacy full source).
|
|
242
|
+
const brainRoot = path.join(agyRoot, 'brain');
|
|
243
|
+
if (fs.existsSync(brainRoot)) {
|
|
244
|
+
const cutoff = Date.now() - RECENT_WINDOW_MS;
|
|
245
|
+
const entries = fs.readdirSync(brainRoot, { withFileTypes: true })
|
|
246
|
+
.filter(e => e.isDirectory())
|
|
247
|
+
.map(e => ({ p: path.join(brainRoot, e.name), mtime: safeMtime(path.join(brainRoot, e.name)) }))
|
|
248
|
+
.filter(e => e.mtime >= cutoff)
|
|
249
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
250
|
+
for (const e of entries) {
|
|
251
|
+
const t = path.join(e.p, '.system_generated', 'logs', 'transcript.jsonl');
|
|
252
|
+
if (fs.existsSync(t)) return t;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// (3) No brain transcript and no bound session id: fall back to the most
|
|
257
|
+
// recently touched conversations/<uuid>.db (within the recency window).
|
|
258
|
+
const convRoot = path.join(agyRoot, 'conversations');
|
|
259
|
+
const newestDb = newestRecentFile(convRoot, /^[0-9a-f-]+\.db$/i);
|
|
260
|
+
if (newestDb) return newestDb;
|
|
261
|
+
|
|
243
262
|
return null;
|
|
244
263
|
}
|
|
245
264
|
|