@adhdev/daemon-core 0.9.82-rc.443 → 0.9.82-rc.445
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/index.d.ts +1 -1
- package/dist/index.js +350 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +342 -22
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-ledger.d.ts +39 -1
- package/dist/providers/cli-provider-instance.d.ts +1 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +28 -2
- package/package.json +2 -2
- package/src/commands/high-family/mesh-coordinator-launch.ts +4 -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/providers/cli-provider-instance.ts +35 -9
- package/src/providers/native-history/antigravity-cli-transcript.ts +367 -25
- package/src/providers/native-history/dispatcher.ts +42 -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,8 @@
|
|
|
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';
|
|
60
|
+
import { LOG } from '../../logging/logger.js';
|
|
34
61
|
|
|
35
62
|
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
36
63
|
|
|
@@ -382,6 +409,250 @@ function parsePbFile(
|
|
|
382
409
|
];
|
|
383
410
|
}
|
|
384
411
|
|
|
412
|
+
// ─── SQLite (.db) conversation reader ────────────────────────────────────────
|
|
413
|
+
//
|
|
414
|
+
// Recent antigravity stores each conversation in a per-session SQLite db at
|
|
415
|
+
// conversations/<uuid>.db. See the file header for the schema. We decode the
|
|
416
|
+
// protobuf `step_payload` blobs with a minimal, dependency-free field walker —
|
|
417
|
+
// we only need two leaf strings (user prompt / assistant answer), so a full
|
|
418
|
+
// proto schema is unnecessary.
|
|
419
|
+
|
|
420
|
+
/** Antigravity step_type values that map to a chat message. */
|
|
421
|
+
const AGY_STEP_TYPE_USER = 14;
|
|
422
|
+
const AGY_STEP_TYPE_MODEL = 15;
|
|
423
|
+
|
|
424
|
+
interface ProtoField {
|
|
425
|
+
field: number;
|
|
426
|
+
wireType: number;
|
|
427
|
+
/** For wireType 2 (length-delimited): the raw bytes. */
|
|
428
|
+
bytes?: Buffer;
|
|
429
|
+
/** For wireType 0 (varint): the value. */
|
|
430
|
+
varint?: number;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Read a base-128 varint starting at `offset`. Returns [value, nextOffset].
|
|
435
|
+
* Values are read as JS numbers (safe: the fields we consume are small).
|
|
436
|
+
*/
|
|
437
|
+
function readVarint(buf: Buffer, offset: number): [number, number] {
|
|
438
|
+
let result = 0;
|
|
439
|
+
let shift = 0;
|
|
440
|
+
let i = offset;
|
|
441
|
+
while (i < buf.length) {
|
|
442
|
+
const byte = buf[i];
|
|
443
|
+
i += 1;
|
|
444
|
+
result += (byte & 0x7f) * Math.pow(2, shift);
|
|
445
|
+
if ((byte & 0x80) === 0) return [result, i];
|
|
446
|
+
shift += 7;
|
|
447
|
+
if (shift > 63) break; // malformed / oversized
|
|
448
|
+
}
|
|
449
|
+
return [result, i];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Decode the top-level fields of a protobuf message. Best-effort: stops on the
|
|
454
|
+
* first malformed byte rather than throwing, so a partially-corrupt blob still
|
|
455
|
+
* yields the fields decoded so far.
|
|
456
|
+
*/
|
|
457
|
+
function decodeProtoFields(buf: Buffer): ProtoField[] {
|
|
458
|
+
const fields: ProtoField[] = [];
|
|
459
|
+
let i = 0;
|
|
460
|
+
while (i < buf.length) {
|
|
461
|
+
const [key, afterKey] = readVarint(buf, i);
|
|
462
|
+
if (afterKey === i) break;
|
|
463
|
+
i = afterKey;
|
|
464
|
+
const field = Math.floor(key / 8);
|
|
465
|
+
const wireType = key & 7;
|
|
466
|
+
if (field <= 0) break;
|
|
467
|
+
if (wireType === 0) {
|
|
468
|
+
const [value, next] = readVarint(buf, i);
|
|
469
|
+
if (next === i) break;
|
|
470
|
+
i = next;
|
|
471
|
+
fields.push({ field, wireType, varint: value });
|
|
472
|
+
} else if (wireType === 2) {
|
|
473
|
+
const [len, afterLen] = readVarint(buf, i);
|
|
474
|
+
i = afterLen;
|
|
475
|
+
if (len < 0 || i + len > buf.length) break;
|
|
476
|
+
fields.push({ field, wireType, bytes: buf.subarray(i, i + len) });
|
|
477
|
+
i += len;
|
|
478
|
+
} else if (wireType === 5) {
|
|
479
|
+
i += 4;
|
|
480
|
+
} else if (wireType === 1) {
|
|
481
|
+
i += 8;
|
|
482
|
+
} else {
|
|
483
|
+
break; // wireType 3/4 (groups) — unused by antigravity payloads
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return fields;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Return the bytes of the first length-delimited field with number `field`. */
|
|
490
|
+
function firstLenField(buf: Buffer, field: number): Buffer | null {
|
|
491
|
+
for (const f of decodeProtoFields(buf)) {
|
|
492
|
+
if (f.field === field && f.wireType === 2 && f.bytes) return f.bytes;
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Heuristic: is this buffer (mostly) printable UTF-8 text? */
|
|
498
|
+
function looksLikeText(buf: Buffer): boolean {
|
|
499
|
+
if (buf.length === 0) return false;
|
|
500
|
+
let printable = 0;
|
|
501
|
+
for (let i = 0; i < buf.length; i++) {
|
|
502
|
+
const b = buf[i];
|
|
503
|
+
// ASCII printable + common whitespace, or any high byte (UTF-8 lead/cont).
|
|
504
|
+
if ((b >= 32 && b <= 126) || b === 9 || b === 10 || b === 13 || b >= 0x80) printable += 1;
|
|
505
|
+
}
|
|
506
|
+
return printable / buf.length >= 0.9;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Antigravity prefixes some assistant answers with a literal `MARKER_V1`
|
|
511
|
+
* sentinel followed by a blank line. Strip it so the user-visible bubble starts
|
|
512
|
+
* at the real text.
|
|
513
|
+
*/
|
|
514
|
+
function stripAnswerMarker(text: string): string {
|
|
515
|
+
return text.replace(/^\s*MARKER_V1\s*/, '');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Extract the assistant's final natural-language answer from a step_type 15
|
|
520
|
+
* payload: field 20 → field 1 (identical to field 8). Field 20 → field 3 is the
|
|
521
|
+
* private reasoning summary and is deliberately skipped. Returns '' if absent
|
|
522
|
+
* (e.g. a pure reasoning / tool-only model step, which carries no user-visible
|
|
523
|
+
* answer text).
|
|
524
|
+
*/
|
|
525
|
+
function extractModelAnswer(payload: Buffer): string {
|
|
526
|
+
const inner = firstLenField(payload, 20);
|
|
527
|
+
if (!inner) return '';
|
|
528
|
+
const answer = firstLenField(inner, 1) ?? firstLenField(inner, 8);
|
|
529
|
+
if (!answer || !looksLikeText(answer)) return '';
|
|
530
|
+
return stripAnswerMarker(answer.toString('utf-8')).trim();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Extract the user prompt from a step_type 14 payload: field 19 → field 2 (the
|
|
535
|
+
* clean prompt text; field 19 → field 3 wraps the same string with a leading
|
|
536
|
+
* newline and is used only as a fallback). The USER_REQUEST XML wrapper, when
|
|
537
|
+
* present, is unwrapped to match the brain-transcript reader's output.
|
|
538
|
+
*/
|
|
539
|
+
function extractUserPrompt(payload: Buffer): string {
|
|
540
|
+
const inner = firstLenField(payload, 19);
|
|
541
|
+
if (!inner) return '';
|
|
542
|
+
const raw = firstLenField(inner, 2) ?? firstLenField(inner, 3);
|
|
543
|
+
if (!raw || !looksLikeText(raw)) return '';
|
|
544
|
+
const text = raw.toString('utf-8').trim();
|
|
545
|
+
if (!text) return '';
|
|
546
|
+
return extractUserRequestContent(text);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
interface AgyDbStepRow {
|
|
550
|
+
idx: number;
|
|
551
|
+
step_type: number;
|
|
552
|
+
step_payload: Buffer | null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Parse a per-session conversations/<uuid>.db (SQLite) into NativeHistoryMessages.
|
|
557
|
+
* Returns null when the db is unreadable, empty, or yields no chat messages.
|
|
558
|
+
*/
|
|
559
|
+
function parseConversationDb(
|
|
560
|
+
filePath: string,
|
|
561
|
+
sessionId: string,
|
|
562
|
+
workspace?: string,
|
|
563
|
+
): NativeHistoryMessage[] | null {
|
|
564
|
+
let db: any;
|
|
565
|
+
try {
|
|
566
|
+
const Database = loadBetterSqlite3();
|
|
567
|
+
db = new Database(filePath, { readonly: true, fileMustExist: true });
|
|
568
|
+
} catch (err) {
|
|
569
|
+
// better-sqlite3 unavailable (ABI mismatch / not installed in this bundle)
|
|
570
|
+
// or the db handle failed to open. This is the silent-degrade that made a
|
|
571
|
+
// live read_chat return 0 assistant messages with no trace — the answers
|
|
572
|
+
// are on disk but unreadable. Log it (once-ish, at WARN) so the failure is
|
|
573
|
+
// greppable in daemon logs and distinguishable from "no db file". The
|
|
574
|
+
// reader still degrades gracefully (returns null → dispatcher falls back to
|
|
575
|
+
// brain/.pb), but the operator now knows WHY the .db path produced nothing.
|
|
576
|
+
LOG.warn(
|
|
577
|
+
'NativeHistory',
|
|
578
|
+
`antigravity .db reader could not open ${path.basename(filePath)}: ${err instanceof Error ? err.message : String(err)} (better-sqlite3 load/open failed — assistant answers in this .db will not surface)`,
|
|
579
|
+
);
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let rows: AgyDbStepRow[];
|
|
584
|
+
try {
|
|
585
|
+
rows = db
|
|
586
|
+
.prepare(
|
|
587
|
+
`SELECT idx, step_type, step_payload
|
|
588
|
+
FROM steps
|
|
589
|
+
WHERE step_type IN (${AGY_STEP_TYPE_USER}, ${AGY_STEP_TYPE_MODEL})
|
|
590
|
+
ORDER BY idx ASC`,
|
|
591
|
+
)
|
|
592
|
+
.all() as AgyDbStepRow[];
|
|
593
|
+
} catch (err) {
|
|
594
|
+
// `steps` table absent / unexpected schema — a real (but recoverable)
|
|
595
|
+
// shape mismatch, not the binding-missing case above. Log at debug so a
|
|
596
|
+
// schema drift in a future antigravity release is diagnosable without
|
|
597
|
+
// spamming logs for every legacy db.
|
|
598
|
+
LOG.debug(
|
|
599
|
+
'NativeHistory',
|
|
600
|
+
`antigravity .db ${path.basename(filePath)} has no readable steps table: ${err instanceof Error ? err.message : String(err)}`,
|
|
601
|
+
);
|
|
602
|
+
return null;
|
|
603
|
+
} finally {
|
|
604
|
+
try { db.close(); } catch { /* ignore */ }
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (!Array.isArray(rows) || rows.length === 0) return null;
|
|
608
|
+
|
|
609
|
+
const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
|
|
610
|
+
const baseTs = statMtimeMs(filePath) || Date.now();
|
|
611
|
+
const messages: NativeHistoryMessage[] = [];
|
|
612
|
+
|
|
613
|
+
for (const row of rows) {
|
|
614
|
+
const payload = row.step_payload;
|
|
615
|
+
if (!payload || !Buffer.isBuffer(payload) || payload.length === 0) continue;
|
|
616
|
+
|
|
617
|
+
// Steps are ordered by idx; the db carries no per-step timestamp we can
|
|
618
|
+
// trust as ms, so synthesize a monotonically increasing receivedAt that
|
|
619
|
+
// preserves order (idx-derived) around the file mtime.
|
|
620
|
+
const receivedAt = baseTs + messages.length;
|
|
621
|
+
|
|
622
|
+
if (row.step_type === AGY_STEP_TYPE_USER) {
|
|
623
|
+
const content = extractUserPrompt(payload);
|
|
624
|
+
if (!content) continue;
|
|
625
|
+
const msg: NativeHistoryMessage = {
|
|
626
|
+
ts: new Date(receivedAt).toISOString(),
|
|
627
|
+
receivedAt,
|
|
628
|
+
role: 'user',
|
|
629
|
+
content,
|
|
630
|
+
kind: 'standard',
|
|
631
|
+
agent: 'antigravity-cli',
|
|
632
|
+
historySessionId: sessionId,
|
|
633
|
+
};
|
|
634
|
+
if (normalizedWorkspace) msg.workspace = normalizedWorkspace;
|
|
635
|
+
messages.push(msg);
|
|
636
|
+
} else if (row.step_type === AGY_STEP_TYPE_MODEL) {
|
|
637
|
+
const content = extractModelAnswer(payload);
|
|
638
|
+
if (!content) continue; // reasoning-only / tool-only model step — no answer text
|
|
639
|
+
const msg: NativeHistoryMessage = {
|
|
640
|
+
ts: new Date(receivedAt).toISOString(),
|
|
641
|
+
receivedAt,
|
|
642
|
+
role: 'assistant',
|
|
643
|
+
content,
|
|
644
|
+
kind: 'standard',
|
|
645
|
+
agent: 'antigravity-cli',
|
|
646
|
+
historySessionId: sessionId,
|
|
647
|
+
};
|
|
648
|
+
if (normalizedWorkspace) msg.workspace = normalizedWorkspace;
|
|
649
|
+
messages.push(msg);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return messages.length > 0 ? messages : null;
|
|
654
|
+
}
|
|
655
|
+
|
|
385
656
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
386
657
|
|
|
387
658
|
/**
|
|
@@ -390,6 +661,7 @@ function parsePbFile(
|
|
|
390
661
|
* `sessionPath` is the absolute path to one of:
|
|
391
662
|
* - A brain transcript JSONL: ~/.gemini/antigravity-cli/brain/<uuid>/.system_generated/logs/transcript*.jsonl
|
|
392
663
|
* - The shared history.jsonl: ~/.gemini/antigravity-cli/history.jsonl
|
|
664
|
+
* - A conversation SQLite db: ~/.gemini/antigravity-cli/conversations/<uuid>.db
|
|
393
665
|
* - A conversation protobuf: ~/.gemini/antigravity-cli/conversations/<uuid>.pb
|
|
394
666
|
*
|
|
395
667
|
* The session UUID is inferred from the directory name (brain), filename (pb), or
|
|
@@ -430,7 +702,26 @@ export function readSession(
|
|
|
430
702
|
};
|
|
431
703
|
}
|
|
432
704
|
|
|
433
|
-
// ── Case
|
|
705
|
+
// ── Case 2a: .db conversation file (current per-session SQLite format) ───
|
|
706
|
+
if (sessionPath.endsWith('.db')) {
|
|
707
|
+
const dbSessionId = sessionId || path.basename(sessionPath, '.db');
|
|
708
|
+
if (!isUuidLike(dbSessionId)) return null;
|
|
709
|
+
|
|
710
|
+
const messages = parseConversationDb(sessionPath, dbSessionId, workspace);
|
|
711
|
+
if (!messages || messages.length === 0) return null;
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
messages,
|
|
715
|
+
providerSessionId: dbSessionId,
|
|
716
|
+
source: 'provider-native',
|
|
717
|
+
sourcePath: sessionPath,
|
|
718
|
+
sourceMtimeMs,
|
|
719
|
+
nativeHistoryCoverage: 'full',
|
|
720
|
+
workspace,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ── Case 2b: .pb conversation file (legacy protobuf) ─────────────────────
|
|
434
725
|
if (sessionPath.endsWith('.pb')) {
|
|
435
726
|
const pbSessionId = sessionId || path.basename(sessionPath, '.pb');
|
|
436
727
|
if (!isUuidLike(pbSessionId)) return null;
|
|
@@ -607,34 +898,85 @@ export async function listSessions(_watchPath: string): Promise<NativeHistorySes
|
|
|
607
898
|
seen.add(sessionId);
|
|
608
899
|
}
|
|
609
900
|
|
|
610
|
-
// ── Step 3: .pb
|
|
901
|
+
// ── Step 3: conversations/<uuid>.db and .pb without any other source ────
|
|
902
|
+
//
|
|
903
|
+
// Current antigravity writes per-session SQLite dbs; older sessions kept a
|
|
904
|
+
// .pb protobuf. Both can coexist in conversations/. Discover both, but when
|
|
905
|
+
// the same uuid has a .db, prefer it (full coverage) over the .pb (best
|
|
906
|
+
// effort). brain/history sources already in `seen` still win over either.
|
|
611
907
|
const convRoot = conversationsRoot();
|
|
612
908
|
if (fs.existsSync(convRoot)) {
|
|
613
909
|
let entries: fs.Dirent[] = [];
|
|
614
910
|
try { entries = fs.readdirSync(convRoot, { withFileTypes: true }); } catch { /* ignore */ }
|
|
615
911
|
|
|
912
|
+
// Group by uuid so a .db supersedes a sibling .pb of the same session.
|
|
913
|
+
const byUuid = new Map<string, { db?: string; pb?: string }>();
|
|
616
914
|
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
|
-
|
|
915
|
+
if (!entry.isFile()) continue;
|
|
916
|
+
const dbMatch = /^([0-9a-f-]+)\.db$/i.exec(entry.name);
|
|
917
|
+
const pbMatch = /^([0-9a-f-]+)\.pb$/i.exec(entry.name);
|
|
918
|
+
if (dbMatch && isUuidLike(dbMatch[1])) {
|
|
919
|
+
const g = byUuid.get(dbMatch[1]) ?? {};
|
|
920
|
+
g.db = path.join(convRoot, entry.name);
|
|
921
|
+
byUuid.set(dbMatch[1], g);
|
|
922
|
+
} else if (pbMatch && isUuidLike(pbMatch[1])) {
|
|
923
|
+
const g = byUuid.get(pbMatch[1]) ?? {};
|
|
924
|
+
g.pb = path.join(convRoot, entry.name);
|
|
925
|
+
byUuid.set(pbMatch[1], g);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
for (const [uuid, files] of byUuid.entries()) {
|
|
930
|
+
if (seen.has(uuid)) continue;
|
|
931
|
+
|
|
932
|
+
if (files.db) {
|
|
933
|
+
// Parse the db so listSessions reports accurate counts/preview and the
|
|
934
|
+
// session surfaces with full coverage (assistant answers included).
|
|
935
|
+
const workspace = workspaceBySession.get(uuid);
|
|
936
|
+
const messages = parseConversationDb(files.db, uuid, workspace);
|
|
937
|
+
const dbMtime = statMtimeMs(files.db);
|
|
938
|
+
if (messages && messages.length > 0) {
|
|
939
|
+
const lastMsg = messages[messages.length - 1];
|
|
940
|
+
const firstMsg = messages[0];
|
|
941
|
+
results.push({
|
|
942
|
+
historySessionId: uuid,
|
|
943
|
+
sessionId: uuid,
|
|
944
|
+
sourcePath: files.db,
|
|
945
|
+
sourceMtimeMs: dbMtime,
|
|
946
|
+
messageCount: messages.length,
|
|
947
|
+
firstMessageAt: firstMsg.receivedAt || dbMtime,
|
|
948
|
+
lastMessageAt: lastMsg.receivedAt || dbMtime,
|
|
949
|
+
sessionTitle: lastMsg.content,
|
|
950
|
+
preview: lastMsg.content,
|
|
951
|
+
workspace,
|
|
952
|
+
agent: 'antigravity-cli',
|
|
953
|
+
source: 'provider-native',
|
|
954
|
+
nativeHistoryCoverage: 'full',
|
|
955
|
+
});
|
|
956
|
+
seen.add(uuid);
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
// db unreadable/empty (e.g. sqlite binding unavailable) → fall through
|
|
960
|
+
// to the .pb best-effort entry below if one exists.
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (files.pb) {
|
|
964
|
+
const pbMtime = statMtimeMs(files.pb);
|
|
965
|
+
results.push({
|
|
966
|
+
historySessionId: uuid,
|
|
967
|
+
sessionId: uuid,
|
|
968
|
+
sourcePath: files.pb,
|
|
969
|
+
sourceMtimeMs: pbMtime,
|
|
970
|
+
messageCount: 0,
|
|
971
|
+
firstMessageAt: pbMtime,
|
|
972
|
+
lastMessageAt: pbMtime,
|
|
973
|
+
agent: 'antigravity-cli',
|
|
974
|
+
source: 'provider-native',
|
|
975
|
+
nativeHistoryCoverage: 'best-effort',
|
|
976
|
+
partialReason: 'antigravity_cli_pb_raw_text_extraction',
|
|
977
|
+
});
|
|
978
|
+
seen.add(uuid);
|
|
979
|
+
}
|
|
638
980
|
}
|
|
639
981
|
}
|
|
640
982
|
|
|
@@ -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,45 @@ 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
|
+
// Only bind to a brain transcript that is NON-EMPTY: current antigravity
|
|
243
|
+
// writes this file but leaves it 0 bytes (all real conversation data now
|
|
244
|
+
// lives in the per-session .db), so an empty transcript here would
|
|
245
|
+
// otherwise shadow the .db fallback below and return no messages. Skip
|
|
246
|
+
// empty transcripts so an unbound read still reaches the .db.
|
|
247
|
+
const brainRoot = path.join(agyRoot, 'brain');
|
|
248
|
+
if (fs.existsSync(brainRoot)) {
|
|
249
|
+
const cutoff = Date.now() - RECENT_WINDOW_MS;
|
|
250
|
+
const entries = fs.readdirSync(brainRoot, { withFileTypes: true })
|
|
251
|
+
.filter(e => e.isDirectory())
|
|
252
|
+
.map(e => ({ p: path.join(brainRoot, e.name), mtime: safeMtime(path.join(brainRoot, e.name)) }))
|
|
253
|
+
.filter(e => e.mtime >= cutoff)
|
|
254
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
255
|
+
for (const e of entries) {
|
|
256
|
+
const t = path.join(e.p, '.system_generated', 'logs', 'transcript.jsonl');
|
|
257
|
+
if (fs.existsSync(t) && safeSize(t) > 0) return t;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// (3) No brain transcript and no bound session id: fall back to the most
|
|
262
|
+
// recently touched conversations/<uuid>.db (within the recency window).
|
|
263
|
+
const convRoot = path.join(agyRoot, 'conversations');
|
|
264
|
+
const newestDb = newestRecentFile(convRoot, /^[0-9a-f-]+\.db$/i);
|
|
265
|
+
if (newestDb) return newestDb;
|
|
266
|
+
|
|
243
267
|
return null;
|
|
244
268
|
}
|
|
245
269
|
|
|
@@ -335,6 +359,10 @@ function safeMtime(p: string): number {
|
|
|
335
359
|
try { return Math.floor(fs.statSync(p).mtimeMs); } catch { return 0; }
|
|
336
360
|
}
|
|
337
361
|
|
|
362
|
+
function safeSize(p: string): number {
|
|
363
|
+
try { return fs.statSync(p).size; } catch { return 0; }
|
|
364
|
+
}
|
|
365
|
+
|
|
338
366
|
function normalizeRole(r: any): 'user' | 'assistant' | 'system' {
|
|
339
367
|
const s = String(r ?? '').toLowerCase();
|
|
340
368
|
if (s === 'user' || s === 'human') return 'user';
|