@cortexkit/pi-magic-context 0.49.16 → 0.49.18

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.
Files changed (51) hide show
  1. package/dist/auto-search-worker.js +76 -63
  2. package/dist/chunk-embedding-write-worker.js +10 -10
  3. package/dist/{compaction-marker-candidates-k4tqg4jb.js → compaction-marker-candidates-wjzqbspm.js} +2 -2
  4. package/dist/compaction-marker-candidates-worker.js +2 -2
  5. package/dist/embedding-backfill-vwag34t0.js +23 -0
  6. package/dist/embedding-worker-process.js +1 -1
  7. package/dist/embedding-worker.js +4 -4
  8. package/dist/historian-calibration-extension.js +4 -1
  9. package/dist/{index-g4jf772p.js → index-1kcfwk9c.js} +22 -2
  10. package/dist/{index-657xh9w4.js → index-2mmy81qt.js} +2 -2
  11. package/dist/{index-x3k1530z.js → index-4bv5bttd.js} +3 -3
  12. package/dist/index-4knyh1kt.js +1387 -0
  13. package/dist/{index-ja1c33qa.js → index-d21pdr8v.js} +2 -2
  14. package/dist/{index-xmzhrht0.js → index-d43hmn31.js} +29 -18
  15. package/dist/{index-6dz2ed1v.js → index-d8gc0112.js} +2 -2
  16. package/dist/{index-hy44m6jc.js → index-e1rza8gp.js} +1 -1
  17. package/dist/{index-ajfpyk0x.js → index-fa485mqs.js} +59 -1
  18. package/dist/{index-fhyy4ave.js → index-fvjthct0.js} +851 -73
  19. package/dist/{index-y5c73caa.js → index-g7p75667.js} +32 -3
  20. package/dist/{index-tympym62.js → index-gdjv1nnd.js} +2 -2
  21. package/dist/{index-v8d4vgam.js → index-gp928w10.js} +6 -6
  22. package/dist/{index-46q4e104.js → index-qpq6w67c.js} +1 -1
  23. package/dist/{index-44h64qza.js → index-r9f4f6s1.js} +14 -10
  24. package/dist/{index-dne4ja7a.js → index-rhprywn6.js} +4 -4
  25. package/dist/{index-asrxgdt4.js → index-rrwv9y2k.js} +8 -0
  26. package/dist/{index-mx59cve4.js → index-svgkv4g2.js} +1 -1
  27. package/dist/{index-qqxqwqhp.js → index-vktahpdj.js} +1 -1
  28. package/dist/{index-n3r2d2pr.js → index-vmcgew7x.js} +10 -7
  29. package/dist/{index-afy3dzf9.js → index-xnch62s6.js} +12 -6
  30. package/dist/{index-4178cbfw.js → index-xxn1tpnv.js} +124 -2039
  31. package/dist/{index-s1xn8qfw.js → index-yygsh681.js} +6 -3
  32. package/dist/{index-p3evwech.js → index-z2rkv0mb.js} +3 -3
  33. package/dist/index.js +1109 -355
  34. package/dist/{message-fts-rowid-map-mbanfkt1.js → message-fts-rowid-map-cvpy2dmb.js} +5 -3
  35. package/dist/{message-index-easvmz22.js → message-index-74qdqmq7.js} +11 -11
  36. package/dist/{message-index-bounded-xzerjxmp.js → message-index-bounded-g9hgresf.js} +15 -15
  37. package/dist/{message-time-backfill-3q4kemfc.js → message-time-backfill-ssshbdy8.js} +1 -1
  38. package/dist/migration-worker.js +7 -7
  39. package/dist/{project-embedding-registry-xwnmqmx8.js → project-embedding-registry-vf1ncptn.js} +10 -10
  40. package/dist/raw-ordinal-warmup-worker.js +10 -10
  41. package/dist/{read-session-chunk-1sb5n7zs.js → read-session-chunk-chepqmw5.js} +5 -5
  42. package/dist/{read-session-raw-jz5e9jz0.js → read-session-raw-9s3xn7fc.js} +2 -2
  43. package/dist/{search-x3fa0561.js → search-6ztbx25v.js} +17 -17
  44. package/dist/{search-archive-3ma5n0wr.js → search-archive-jg4a1kcx.js} +6 -6
  45. package/dist/{sqlite-adjpy399.js → sqlite-fsp0sf3r.js} +1 -1
  46. package/dist/{storage-memory-axnsk5t1.js → storage-memory-yxxc5v4n.js} +2 -2
  47. package/dist/{storage-tags-jtessrec.js → storage-tags-1kets0q8.js} +3 -3
  48. package/dist/subagent-entry.js +22 -21
  49. package/dist/tag-snapshot-worker-pi.js +9 -9
  50. package/package.json +6 -1
  51. package/dist/embedding-backfill-c4bdw8pp.js +0 -23
@@ -9,23 +9,32 @@ import {
9
9
  readProcessStartTime
10
10
  } from "./index-apgwdby5.js";
11
11
  import {
12
- removeSystemReminders,
13
- hasMeaningfulUserText2,
14
- extractTexts2
15
- } from "./index-g4jf772p.js";
12
+ resolveOpenCodeDbPath,
13
+ assertOpenCodeStoreGeneration
14
+ } from "./index-8976mezv.js";
16
15
  import {
16
+ Database2,
17
+ getSqliteDatabasePath2,
18
+ prepareCachedStatement2,
17
19
  withSqliteBackgroundWriter2,
18
20
  withoutSqliteTransformPass2
19
- } from "./index-asrxgdt4.js";
21
+ } from "./index-rrwv9y2k.js";
22
+ import {
23
+ removeSystemReminders,
24
+ removeNestedBlocks,
25
+ hasMeaningfulUserText2,
26
+ extractTexts2
27
+ } from "./index-1kcfwk9c.js";
20
28
  import {
21
29
  recordMessageFtsRowid2,
22
30
  recordIndexedMessageTime2,
23
31
  messageFtsOrdinalRangeIsMapped2,
32
+ getMessageFtsRowidMapBackfillProgress2,
24
33
  deleteUnmappedMessageFtsRows2
25
- } from "./index-s1xn8qfw.js";
34
+ } from "./index-yygsh681.js";
26
35
  import {
27
36
  deleteChunkEmbedBackoffForSession
28
- } from "./index-afy3dzf9.js";
37
+ } from "./index-xnch62s6.js";
29
38
  import {
30
39
  closeQuietly,
31
40
  TAG_TOMBSTONE_OWNER_PREFIX,
@@ -37,7 +46,10 @@ import {
37
46
  } from "./index-jrap43gc.js";
38
47
  import {
39
48
  cleanUserText2
40
- } from "./index-44h64qza.js";
49
+ } from "./index-r9f4f6s1.js";
50
+ import {
51
+ sameProjectDirectory
52
+ } from "./index-wrr9rp9a.js";
41
53
  import {
42
54
  logSlowWriteTransaction
43
55
  } from "./index-d4dt19md.js";
@@ -51,7 +63,7 @@ import {
51
63
 
52
64
  // ../plugin/src/hooks/magic-context/lkg-recovery-fence.ts
53
65
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
54
- import { lstatSync, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
66
+ import { lstatSync, readdirSync as readdirSync2, readFileSync as readFileSync2, rmdirSync, rmSync as rmSync2 } from "node:fs";
55
67
  import { basename, dirname, join as join2 } from "node:path";
56
68
 
57
69
  // ../plugin/src/shared/bounded-session-map.ts
@@ -141,7 +153,16 @@ function withStorageFileMutex(directory, work, timeoutMs = 1000) {
141
153
  const birth = processBirth(process.pid);
142
154
  const ticket = { choosing: true, number: 0, birth, checksum: "" };
143
155
  const encodedTicket = () => JSON.stringify({ ...ticket, checksum: ticketChecksum(name, ticket) });
144
- writeStorageFileSync(ownPath, encodedTicket(), { flag: "wx", forcePrivate: true });
156
+ for (let attempt = 0;; attempt++) {
157
+ try {
158
+ writeStorageFileSync(ownPath, encodedTicket(), { flag: "wx", forcePrivate: true });
159
+ break;
160
+ } catch (error) {
161
+ if (error.code !== "ENOENT" || attempt >= 2)
162
+ throw error;
163
+ ensurePrivateStorageDirectorySync(directory);
164
+ }
165
+ }
145
166
  const deadline = performance.now() + timeoutMs;
146
167
  const peers = () => {
147
168
  const found = [];
@@ -438,15 +459,619 @@ function commitLkgWithRecoveryFence(args) {
438
459
  function clearLkgRecoveryFence(sessionId, remove, shouldClear) {
439
460
  if (!durableFencesSupported())
440
461
  return remove?.();
441
- return locked(sessionId, () => {
442
- const result = remove?.();
443
- if (shouldClear && !shouldClear(result))
444
- return result;
445
- rmSync2(lkgRecoveryFencePath(sessionId), { force: true });
446
- syncStorageDirectorySync(dirname(lkgRecoveryFencePath(sessionId)));
462
+ const path = lkgRecoveryFencePath(sessionId);
463
+ const missing = (file) => {
464
+ try {
465
+ lstatSync(file);
466
+ return false;
467
+ } catch (error) {
468
+ return error.code === "ENOENT";
469
+ }
470
+ };
471
+ if (!remove && !shouldClear && missing(path) && missing(`${path}.locks`)) {
447
472
  clearLkgFenceProcessState(sessionId);
473
+ return;
474
+ }
475
+ try {
476
+ return locked(sessionId, () => {
477
+ const result = remove?.();
478
+ if (shouldClear && !shouldClear(result))
479
+ return result;
480
+ rmSync2(lkgRecoveryFencePath(sessionId), { force: true });
481
+ syncStorageDirectorySync(dirname(lkgRecoveryFencePath(sessionId)));
482
+ clearLkgFenceProcessState(sessionId);
483
+ return result;
484
+ });
485
+ } finally {
486
+ try {
487
+ rmdirSync(`${path}.locks`);
488
+ } catch {}
489
+ }
490
+ }
491
+
492
+ // ../plugin/src/v2/store-reader.ts
493
+ import { createHash as createHash3 } from "node:crypto";
494
+ import { statSync } from "node:fs";
495
+ import { hostname } from "node:os";
496
+ import { resolve } from "node:path";
497
+ function gaDatabasePath(dataHome, channel = "latest", env = process.env) {
498
+ return resolveOpenCodeDbPath("v2", { dataHome, channel, env }).path;
499
+ }
500
+ var RAW_MESSAGE_TYPES = [
501
+ "user",
502
+ "synthetic",
503
+ "assistant",
504
+ "skill",
505
+ "shell",
506
+ "system"
507
+ ];
508
+ var RAW_MESSAGE_TYPE_PARAMETERS = RAW_MESSAGE_TYPES.map(() => "?").join(", ");
509
+ var V2_MESSAGE_PAGE_SQL = `WITH bounds AS (
510
+ SELECT
511
+ CASE WHEN ? = 0 THEN -1 ELSE COALESCE((
512
+ SELECT seq FROM session_message
513
+ WHERE session_id = ? AND type IN (${RAW_MESSAGE_TYPE_PARAMETERS})
514
+ ORDER BY seq ASC LIMIT 1 OFFSET ?
515
+ ), (SELECT MAX(seq) FROM session_message WHERE session_id = ?), -1) END AS after_seq,
516
+ COALESCE((
517
+ SELECT seq FROM session_message
518
+ WHERE session_id = ? AND type IN (${RAW_MESSAGE_TYPE_PARAMETERS})
519
+ ORDER BY seq ASC LIMIT 1 OFFSET ?
520
+ ), ?) AS watermark_seq
521
+ )
522
+ SELECT id, session_id, type, seq, time_created, data FROM session_message, bounds
523
+ WHERE session_id = ?
524
+ AND type IN (${RAW_MESSAGE_TYPE_PARAMETERS})
525
+ AND seq > bounds.after_seq
526
+ AND seq <= bounds.watermark_seq
527
+ ORDER BY seq ASC LIMIT ?`;
528
+ var V2_STORE_READER_DEBUG_COUNTER_KEY = "magic-context.v2.store-reader-debug";
529
+ var debugSymbol = Symbol.for(V2_STORE_READER_DEBUG_COUNTER_KEY);
530
+ var debugGlobal = globalThis;
531
+ var QUERY_STATEMENTS = {
532
+ messagePage: V2_MESSAGE_PAGE_SQL,
533
+ messageCount: "SELECT COUNT(*) AS count FROM session_message WHERE session_id = ? AND type IN (?)",
534
+ storedMessageCount: "SELECT COUNT(*) AS count FROM session_message WHERE session_id = ?",
535
+ messageById: "SELECT id, session_id, type, seq, time_created, data FROM session_message WHERE session_id = ? AND id = ? AND type IN (?) LIMIT 1",
536
+ messageExistsById: "SELECT 1 FROM session_message WHERE id = ? AND session_id = ? AND type IN (?) LIMIT 1",
537
+ messageOrdinalById: `WITH target AS (
538
+ SELECT seq FROM session_message WHERE session_id = ? AND id = ? AND type IN (?) LIMIT 1
539
+ )
540
+ SELECT (SELECT COUNT(*) FROM session_message WHERE session_id = ? AND type IN (?) AND seq <= target.seq) AS ordinal FROM target`,
541
+ messageIdOrdinals: "SELECT id FROM session_message WHERE session_id = ? AND type IN (?) ORDER BY seq ASC LIMIT ? OFFSET ?",
542
+ messageOrdinalPage: "SELECT id, type, seq, json_valid(data) AS valid FROM session_message WHERE session_id = ? AND seq > ? ORDER BY seq ASC, id ASC LIMIT ?",
543
+ rawRowsThrough: "SELECT id, session_id, type, seq, time_created, data FROM session_message WHERE session_id = ? AND type IN (?) AND seq <= ? ORDER BY seq DESC LIMIT ?"
544
+ };
545
+ function debugCounters() {
546
+ const counters = debugGlobal[debugSymbol] ??= {
547
+ decodedRows: 0,
548
+ operations: {},
549
+ openReaders: 0,
550
+ maxOpenReaders: 0,
551
+ readersOpened: 0,
552
+ readersClosed: 0
553
+ };
554
+ counters.openReaders ??= 0;
555
+ counters.maxOpenReaders ??= 0;
556
+ counters.readersOpened ??= 0;
557
+ counters.readersClosed ??= 0;
558
+ return counters;
559
+ }
560
+ function returnedRowCount(value) {
561
+ if (Array.isArray(value))
562
+ return value.length;
563
+ if (value === null || value === undefined || value === false)
564
+ return 0;
565
+ return 1;
566
+ }
567
+ function trackDecodeOperation(name, operation) {
568
+ const counters = debugCounters();
569
+ const before = counters.decodedRows;
570
+ const started = performance.now();
571
+ let value;
572
+ try {
573
+ value = operation();
574
+ return value;
575
+ } finally {
576
+ const decodedRows = counters.decodedRows - before;
577
+ const current = counters.operations[name] ?? {
578
+ calls: 0,
579
+ decodedRows: 0,
580
+ maxDecodedRows: 0
581
+ };
582
+ current.calls += 1;
583
+ current.decodedRows += decodedRows;
584
+ current.maxDecodedRows = Math.max(current.maxDecodedRows, decodedRows);
585
+ counters.operations[name] = current;
586
+ const statement = QUERY_STATEMENTS[name];
587
+ if (counters.captureQueries && statement) {
588
+ (counters.queries ??= []).push({
589
+ operation: name,
590
+ statement,
591
+ rows: returnedRowCount(value),
592
+ elapsedMs: performance.now() - started
593
+ });
594
+ }
595
+ }
596
+ }
597
+ function decode(row) {
598
+ debugCounters().decodedRows += 1;
599
+ const data = JSON.parse(row.data);
600
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
601
+ throw new Error(`Invalid session_message data at seq ${row.seq}`);
602
+ }
603
+ if (row.type === "idle" && !["succeeded", "failed", "interrupted"].includes(String(data.outcome))) {
604
+ throw new Error(`Invalid idle outcome at seq ${row.seq}`);
605
+ }
606
+ return { ...row, data };
607
+ }
608
+ function connect(path, identity = "") {
609
+ const stat = statSync(path);
610
+ const acquiredIdentity = identity || `${stat.dev}:${stat.ino}:${stat.birthtimeMs}`;
611
+ const db = new Database2(path, { readonly: true, fileMustExist: true });
612
+ try {
613
+ assertOpenCodeStoreGeneration(db, "v2", path);
614
+ } catch (error) {
615
+ db.close();
616
+ throw error;
617
+ }
618
+ const counters = debugCounters();
619
+ counters.openReaders += 1;
620
+ counters.readersOpened += 1;
621
+ counters.maxOpenReaders = Math.max(counters.maxOpenReaders, counters.openReaders);
622
+ return { db, statements: new Map, identity: acquiredIdentity, leases: 0, retired: false };
623
+ }
624
+ function closeConnection(connection) {
625
+ connection.statements.clear();
626
+ try {
627
+ connection.db.close();
628
+ } finally {
629
+ const counters = debugCounters();
630
+ counters.openReaders = Math.max(0, counters.openReaders - 1);
631
+ counters.readersClosed += 1;
632
+ }
633
+ }
634
+ class V2StoreReader {
635
+ path;
636
+ pool;
637
+ db;
638
+ connection;
639
+ closed = false;
640
+ constructor(path, pool) {
641
+ this.path = path;
642
+ this.pool = pool;
643
+ this.connection = pool ? pool.acquire(path) : connect(path);
644
+ this.db = this.connection.db;
645
+ }
646
+ hostIdentity() {
647
+ return createHash3("sha256").update(JSON.stringify([hostname(), resolve(this.path), this.connection.identity])).digest("hex");
648
+ }
649
+ prepare(sql) {
650
+ if (this.closed)
651
+ throw new Error("Store reader is closed");
652
+ const statements = this.connection.statements;
653
+ let statement = statements.get(sql);
654
+ if (!statement) {
655
+ statement = this.db.prepare(sql);
656
+ if (statements.size >= 128)
657
+ statements.clear();
658
+ statements.set(sql, statement);
659
+ }
660
+ return statement;
661
+ }
662
+ close() {
663
+ if (this.closed)
664
+ return;
665
+ this.closed = true;
666
+ if (this.pool)
667
+ this.pool.release(this.connection);
668
+ else
669
+ closeConnection(this.connection);
670
+ }
671
+ page(sessionID, options = {}) {
672
+ return trackDecodeOperation("page", () => {
673
+ const limit = options.limit ?? 100;
674
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4)
675
+ throw new Error("Invalid page limit");
676
+ const after = options.after ?? -1;
677
+ if (!Number.isSafeInteger(after))
678
+ throw new Error("Invalid seq cursor");
679
+ if (options.through !== undefined && !Number.isSafeInteger(options.through))
680
+ throw new Error("Invalid seq upper bound");
681
+ const predicates = ["session_id = ?", "seq > ?"];
682
+ const parameters = [sessionID, after];
683
+ if (options.through !== undefined) {
684
+ predicates.push("seq <= ?");
685
+ parameters.push(options.through);
686
+ }
687
+ if (options.type) {
688
+ predicates.push("type = ?");
689
+ parameters.push(options.type);
690
+ }
691
+ const rows = this.prepare(`SELECT id, session_id, type, seq, time_created, data FROM session_message
692
+ WHERE ${predicates.join(" AND ")}
693
+ ORDER BY seq ASC LIMIT ?`).all(...parameters, limit).map(decode);
694
+ return { rows, cursor: rows.at(-1)?.seq };
695
+ });
696
+ }
697
+ messagePage(sessionID, afterOrdinal, limit, finalWatermark, after) {
698
+ return trackDecodeOperation("messagePage", () => {
699
+ if (!Number.isSafeInteger(afterOrdinal) || afterOrdinal < 0)
700
+ throw new Error("Invalid raw-message ordinal cursor");
701
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4)
702
+ throw new Error("Invalid page limit");
703
+ if (!Number.isSafeInteger(finalWatermark) || finalWatermark < 0)
704
+ throw new Error("Invalid raw-message watermark");
705
+ const pageSize = Math.min(limit, finalWatermark - afterOrdinal);
706
+ if (pageSize <= 0)
707
+ return [];
708
+ if (after) {
709
+ const anchor = this.prepare("SELECT seq FROM session_message WHERE session_id = ? AND id = ?").get(sessionID, after.id);
710
+ if (anchor) {
711
+ return this.prepare(`SELECT id, session_id, type, seq, time_created, data
712
+ FROM session_message WHERE session_id = ? AND seq > ?
713
+ AND type IN (${RAW_MESSAGE_TYPE_PARAMETERS}) ORDER BY seq ASC LIMIT ?`).all(sessionID, anchor.seq, ...RAW_MESSAGE_TYPES, pageSize).map(decode);
714
+ }
715
+ }
716
+ const maximumSeq = Number.MAX_SAFE_INTEGER;
717
+ const rows = this.prepare(V2_MESSAGE_PAGE_SQL).all(afterOrdinal, sessionID, ...RAW_MESSAGE_TYPES, Math.max(0, afterOrdinal - 1), sessionID, sessionID, ...RAW_MESSAGE_TYPES, finalWatermark - 1, maximumSeq, sessionID, ...RAW_MESSAGE_TYPES, pageSize);
718
+ return rows.map(decode);
719
+ });
720
+ }
721
+ messageCount(sessionID) {
722
+ return trackDecodeOperation("messageCount", () => {
723
+ const rawTypes = RAW_MESSAGE_TYPES.map(() => "?").join(", ");
724
+ const row = this.prepare(`SELECT COUNT(*) AS count FROM session_message
725
+ WHERE session_id = ? AND type IN (${rawTypes})`).get(sessionID, ...RAW_MESSAGE_TYPES);
726
+ return typeof row?.count === "number" ? row.count : 0;
727
+ });
728
+ }
729
+ sessionDirectoryPage(afterSessionID, limit) {
730
+ const rows = this.prepare(`SELECT id, COALESCE(directory, '') AS directory FROM session_v2
731
+ WHERE id > ? ORDER BY id ASC LIMIT ?`).all(afterSessionID ?? "", limit);
732
+ return rows.map((row) => ({ sessionId: row.id, directory: row.directory }));
733
+ }
734
+ latestMessageTime(sessionID) {
735
+ const row = this.prepare("SELECT MAX(time_created) AS time FROM session_message WHERE session_id = ?").get(sessionID);
736
+ return row?.time ?? undefined;
737
+ }
738
+ isSubagent(sessionID) {
739
+ const row = this.prepare("SELECT parent_id FROM session_v2 WHERE id = ?").get(sessionID);
740
+ if (!row)
741
+ return null;
742
+ return typeof row.parent_id === "string" && row.parent_id.length > 0;
743
+ }
744
+ sessionIds() {
745
+ return this.prepare("SELECT id FROM session_v2").all().map((row) => row.id);
746
+ }
747
+ latestRootSessionInDirectory(directory) {
748
+ const columns = new Set(this.prepare("PRAGMA table_info(session_v2)").all().map((column) => column.name));
749
+ const archived = columns.has("time_archived") ? " AND time_archived IS NULL" : "";
750
+ const order = columns.has("time_updated") ? "time_updated DESC, id DESC" : "id DESC";
751
+ const page = this.prepare(`SELECT id, COALESCE(directory, '') AS directory FROM session_v2
752
+ WHERE parent_id IS NULL${archived}
753
+ AND COALESCE(json_extract(metadata, '$.magic_context'), '') <> 'hidden-run'
754
+ ORDER BY ${order} LIMIT ? OFFSET ?`);
755
+ const pageSize = 500;
756
+ for (let offset = 0;; offset += pageSize) {
757
+ const rows = page.all(pageSize, offset);
758
+ const match = rows.find((row) => sameProjectDirectory(row.directory, directory));
759
+ if (match)
760
+ return match.id;
761
+ if (rows.length < pageSize)
762
+ return;
763
+ }
764
+ }
765
+ rootSessionActivity(sessionIDs) {
766
+ const roots = new Map;
767
+ for (let offset = 0;offset < sessionIDs.length; offset += 500) {
768
+ const chunk = sessionIDs.slice(offset, offset + 500);
769
+ const rows = this.prepare(`SELECT id, (
770
+ SELECT MAX(time_created) FROM session_message
771
+ WHERE session_id = session_v2.id AND type = 'user'
772
+ ) AS time FROM session_v2
773
+ WHERE parent_id IS NULL
774
+ AND COALESCE(json_extract(metadata, '$.magic_context'), '') <> 'hidden-run'
775
+ AND id IN (${chunk.map(() => "?").join(",")})`).all(...chunk);
776
+ for (const row of rows)
777
+ if (row.time !== null)
778
+ roots.set(row.id, row.time);
779
+ }
780
+ return roots;
781
+ }
782
+ oldestUserMessageTimesSince(sessionIDs, sinceMs) {
783
+ const result = new Map;
784
+ for (let offset = 0;offset < sessionIDs.length; offset += 500) {
785
+ const chunk = sessionIDs.slice(offset, offset + 500);
786
+ const rows = this.prepare(`SELECT session_id, MIN(time_created) AS time FROM session_message
787
+ WHERE type = 'user' AND time_created > ? AND session_id IN (${chunk.map(() => "?").join(",")})
788
+ GROUP BY session_id`).all(sinceMs, ...chunk);
789
+ for (const row of rows)
790
+ result.set(row.session_id, row.time);
791
+ }
448
792
  return result;
449
- });
793
+ }
794
+ retrospectiveUserPage(sessionID, options) {
795
+ const { boundaryMs, before, limit, maxChars, truncationMarker } = options;
796
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4)
797
+ throw new Error("Invalid page limit");
798
+ if (!Number.isSafeInteger(maxChars) || maxChars <= truncationMarker.length)
799
+ throw new Error("Invalid text limit");
800
+ const direction = before ? "DESC" : "ASC";
801
+ const head = Math.floor((maxChars - truncationMarker.length) / 2);
802
+ const tail = maxChars - truncationMarker.length - head;
803
+ return this.prepare(`WITH users AS (
804
+ SELECT session_id, seq, time_created, data FROM session_message
805
+ WHERE session_id = ? AND type = 'user' AND time_created ${before ? "<=" : ">"}
806
+ ?
807
+ ORDER BY time_created ${direction}, seq ${direction} LIMIT ?
808
+ ), texts AS (
809
+ SELECT session_id, seq, time_created, CASE
810
+ WHEN json_extract(data, '$.synthetic') = 1 OR json_extract(data, '$.ignored') = 1 THEN ''
811
+ WHEN json_type(data, '$.text') = 'text' THEN json_extract(data, '$.text')
812
+ ELSE COALESCE((SELECT group_concat(json_extract(value, '$.text'), char(10))
813
+ FROM json_each(users.data, '$.content')
814
+ WHERE json_extract(value, '$.type') IN ('text', 'input_text')
815
+ AND COALESCE(json_extract(value, '$.synthetic'), 0) <> 1
816
+ AND COALESCE(json_extract(value, '$.ignored'), 0) <> 1), '') END AS text
817
+ FROM users WHERE json_valid(data)
818
+ ) SELECT seq, time_created, CASE WHEN length(text) > ?
819
+ THEN substr(text, 1, ?) || ? || substr(text, -?) ELSE text END AS text,
820
+ EXISTS (
821
+ SELECT 1 FROM session_message assistant_reply
822
+ WHERE assistant_reply.session_id = texts.session_id
823
+ AND assistant_reply.type = 'assistant'
824
+ AND assistant_reply.seq < texts.seq
825
+ AND EXISTS (
826
+ SELECT 1 FROM session_message prior_user
827
+ WHERE prior_user.session_id = texts.session_id
828
+ AND prior_user.type = 'user'
829
+ AND prior_user.seq < assistant_reply.seq
830
+ )
831
+ ) AS follows_assistant_reply
832
+ FROM texts ORDER BY time_created ${direction}, seq ${direction}`).all(sessionID, boundaryMs, limit, maxChars, head, truncationMarker, tail);
833
+ }
834
+ storedRowsAfter(sessionID, after) {
835
+ const row = this.prepare("SELECT COUNT(*) AS count FROM session_message WHERE session_id = ? AND seq > ?").get(sessionID, after);
836
+ return row.count;
837
+ }
838
+ storedMessageCount(sessionID) {
839
+ return trackDecodeOperation("storedMessageCount", () => {
840
+ const row = this.prepare("SELECT COUNT(*) AS count FROM session_message WHERE session_id = ?").get(sessionID);
841
+ return typeof row?.count === "number" ? row.count : 0;
842
+ });
843
+ }
844
+ messageById(sessionID, id) {
845
+ return trackDecodeOperation("messageById", () => {
846
+ const rawTypes = RAW_MESSAGE_TYPES.map(() => "?").join(", ");
847
+ const row = this.prepare(`SELECT id, session_id, type, seq, time_created, data FROM session_message
848
+ WHERE session_id = ? AND id = ? AND type IN (${rawTypes}) LIMIT 1`).get(sessionID, id, ...RAW_MESSAGE_TYPES);
849
+ return row ? decode(row) : null;
850
+ });
851
+ }
852
+ messageTimes(sessionID, ids) {
853
+ const times = new Map;
854
+ for (let offset = 0;offset < ids.length; offset += 500) {
855
+ const batch = ids.slice(offset, offset + 500);
856
+ const rows = this.prepare(`SELECT id, COALESCE(time_created, json_extract(data, '$.time.created')) AS created,
857
+ json_extract(data, '$.time.completed') AS completed
858
+ FROM session_message WHERE session_id = ? AND id IN (${batch.map(() => "?").join(",")})`).all(sessionID, ...batch);
859
+ for (const row of rows)
860
+ times.set(row.id, {
861
+ created: typeof row.created === "number" ? row.created : undefined,
862
+ completed: typeof row.completed === "number" ? row.completed : undefined
863
+ });
864
+ }
865
+ return times;
866
+ }
867
+ messageExistsById(sessionID, id) {
868
+ return trackDecodeOperation("messageExistsById", () => {
869
+ const rawTypes = RAW_MESSAGE_TYPES.map(() => "?").join(", ");
870
+ const row = this.prepare(`SELECT 1 FROM session_message
871
+ WHERE id = ? AND session_id = ? AND type IN (${rawTypes}) LIMIT 1`).get(id, sessionID, ...RAW_MESSAGE_TYPES);
872
+ return row != null;
873
+ });
874
+ }
875
+ messageOrdinalById(sessionID, id) {
876
+ return trackDecodeOperation("messageOrdinalById", () => {
877
+ const rawTypes = RAW_MESSAGE_TYPES.map(() => "?").join(", ");
878
+ const row = this.prepare(`WITH target AS (
879
+ SELECT seq FROM session_message
880
+ WHERE session_id = ? AND id = ? AND type IN (${rawTypes})
881
+ LIMIT 1
882
+ )
883
+ SELECT (
884
+ SELECT COUNT(*) FROM session_message
885
+ WHERE session_id = ?
886
+ AND type IN (${rawTypes})
887
+ AND seq <= target.seq
888
+ ) AS ordinal
889
+ FROM target`).get(sessionID, id, ...RAW_MESSAGE_TYPES, sessionID, ...RAW_MESSAGE_TYPES);
890
+ return typeof row?.ordinal === "number" ? row.ordinal : null;
891
+ });
892
+ }
893
+ messageIdOrdinals(sessionID, fromOrdinal, toOrdinal) {
894
+ return trackDecodeOperation("messageIdOrdinals", () => {
895
+ if (!Number.isSafeInteger(fromOrdinal) || fromOrdinal < 1)
896
+ throw new Error("Invalid raw-message range start");
897
+ if (!Number.isSafeInteger(toOrdinal) || toOrdinal < fromOrdinal)
898
+ throw new Error("Invalid raw-message range end");
899
+ const rawTypes = RAW_MESSAGE_TYPES.map(() => "?").join(", ");
900
+ const rows = this.prepare(`SELECT id FROM session_message
901
+ WHERE session_id = ? AND type IN (${rawTypes})
902
+ ORDER BY seq ASC LIMIT ? OFFSET ?`).all(sessionID, ...RAW_MESSAGE_TYPES, toOrdinal - fromOrdinal + 1, fromOrdinal - 1);
903
+ return new Map(rows.map((row, index) => [row.id, fromOrdinal + index]));
904
+ });
905
+ }
906
+ messageOrdinalPage(sessionID, after, limit) {
907
+ return trackDecodeOperation("messageOrdinalPage", () => {
908
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4)
909
+ throw new Error("Invalid page limit");
910
+ const rows = after ? this.prepare(`SELECT id, type, seq, json_valid(data) AS valid
911
+ FROM session_message
912
+ WHERE session_id = ? AND (seq > ? OR (seq = ? AND id > ?))
913
+ ORDER BY seq ASC, id ASC LIMIT ?`).all(sessionID, after.timeCreated, after.timeCreated, after.id, limit) : this.prepare(`SELECT id, type, seq, json_valid(data) AS valid
914
+ FROM session_message
915
+ WHERE session_id = ?
916
+ ORDER BY seq ASC, id ASC LIMIT ?`).all(sessionID, limit);
917
+ const rawTypes = new Set(RAW_MESSAGE_TYPES);
918
+ return rows.map((row) => ({
919
+ id: row.id,
920
+ timeCreated: row.seq,
921
+ contributesOrdinal: rawTypes.has(row.type),
922
+ hasValidInfo: row.valid === 1
923
+ }));
924
+ });
925
+ }
926
+ range(sessionID, after, through) {
927
+ return trackDecodeOperation("range", () => through <= after ? [] : this.all(sessionID, after, undefined, through));
928
+ }
929
+ spanRowStamps(sessionID, after, through) {
930
+ if (!Number.isSafeInteger(after) || !Number.isSafeInteger(through))
931
+ throw new Error("Invalid seq span");
932
+ const rows = this.prepare(`SELECT seq, id, time_updated AS updated, length(CAST(data AS BLOB)) AS bytes
933
+ FROM session_message WHERE session_id = ? AND seq > ? AND seq <= ?
934
+ ORDER BY seq ASC`).all(sessionID, after, through);
935
+ const stamps = new Map;
936
+ for (const row of rows)
937
+ stamps.set(row.seq, `${row.id}\x00${row.updated ?? ""}\x00${row.bytes ?? ""}`);
938
+ return stamps;
939
+ }
940
+ rawRowsThrough(sessionID, throughSeq, limit) {
941
+ return trackDecodeOperation("rawRowsThrough", () => {
942
+ if (!Number.isSafeInteger(throughSeq))
943
+ throw new Error("Invalid seq upper bound");
944
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4)
945
+ throw new Error("Invalid page limit");
946
+ const rawTypes = RAW_MESSAGE_TYPES.map(() => "?").join(", ");
947
+ return this.prepare(`SELECT id, session_id, type, seq, time_created, data FROM session_message
948
+ WHERE session_id = ? AND type IN (${rawTypes}) AND seq <= ?
949
+ ORDER BY seq DESC LIMIT ?`).all(sessionID, ...RAW_MESSAGE_TYPES, throughSeq, limit).map(decode);
950
+ });
951
+ }
952
+ instructionRowsThrough(sessionID, throughSeq) {
953
+ return trackDecodeOperation("instructionRowsThrough", () => {
954
+ if (!Number.isSafeInteger(throughSeq))
955
+ throw new Error("Invalid seq upper bound");
956
+ return this.prepare(`SELECT id, session_id, type, seq, time_created, data FROM session_message
957
+ WHERE session_id = ? AND type = 'system' AND seq <= ?
958
+ ORDER BY seq ASC`).all(sessionID, throughSeq).map(decode);
959
+ });
960
+ }
961
+ sequenceForId(sessionID, id) {
962
+ if (!id)
963
+ return;
964
+ const row = this.prepare("SELECT seq FROM session_message WHERE session_id = ? AND id = ? LIMIT 1").get(sessionID, id);
965
+ return typeof row?.seq === "number" ? row.seq : undefined;
966
+ }
967
+ syntheticMessageIDs(sessionID) {
968
+ const rows = this.prepare("SELECT id FROM session_message WHERE session_id = ? AND type = 'synthetic'").all(sessionID);
969
+ return new Set(rows.map((row) => row.id));
970
+ }
971
+ forkOrigin(sessionID) {
972
+ const columns = new Set(this.prepare("PRAGMA table_info(session_v2)").all().map((column) => column.name));
973
+ if (!columns.has("fork_session_id") || !columns.has("fork_boundary"))
974
+ return null;
975
+ const row = this.prepare("SELECT fork_session_id, fork_boundary FROM session_v2 WHERE id = ?").get(sessionID);
976
+ if (!row?.fork_session_id || !row.fork_boundary)
977
+ return null;
978
+ try {
979
+ const boundary = JSON.parse(row.fork_boundary);
980
+ if (boundary.type !== "before" && boundary.type !== "through" || typeof boundary.messageID !== "string" || boundary.messageID.length === 0)
981
+ return null;
982
+ return {
983
+ parentSessionID: row.fork_session_id,
984
+ boundary: { type: boundary.type, messageID: boundary.messageID }
985
+ };
986
+ } catch {
987
+ return null;
988
+ }
989
+ }
990
+ sessionExists(sessionID) {
991
+ return this.prepare("SELECT 1 FROM session_v2 WHERE id = ?").get(sessionID) != null;
992
+ }
993
+ rowStampsThrough(sessionID, throughSeq) {
994
+ return this.prepare(`SELECT id, type, seq, time_created FROM session_message
995
+ WHERE session_id = ? AND seq <= ? ORDER BY seq ASC`).all(sessionID, throughSeq);
996
+ }
997
+ latestSequenceForIds(sessionID, ids) {
998
+ let latest = -1;
999
+ for (let offset = 0;offset < ids.length; offset += 500) {
1000
+ const chunk = ids.slice(offset, offset + 500);
1001
+ const row = this.prepare(`SELECT MAX(seq) AS seq FROM session_message
1002
+ WHERE session_id = ? AND id IN (${chunk.map(() => "?").join(", ")})`).get(sessionID, ...chunk);
1003
+ if (typeof row?.seq === "number")
1004
+ latest = Math.max(latest, row.seq);
1005
+ }
1006
+ return latest;
1007
+ }
1008
+ compactionByStatus(sessionID, status) {
1009
+ const row = this.prepare(`SELECT id, session_id, type, seq, time_created, data FROM session_message
1010
+ WHERE session_id = ? AND type = 'compaction'
1011
+ AND json_extract(data, '$.status') = ?
1012
+ ORDER BY seq DESC LIMIT 1`).get(sessionID, status);
1013
+ return row ? decode(row) : undefined;
1014
+ }
1015
+ latestCompaction(sessionID) {
1016
+ return trackDecodeOperation("latestCompaction", () => this.compactionByStatus(sessionID, "completed"));
1017
+ }
1018
+ latestRunningCompaction(sessionID) {
1019
+ return trackDecodeOperation("latestRunningCompaction", () => this.compactionByStatus(sessionID, "running"));
1020
+ }
1021
+ idleRows(sessionID, after = -1) {
1022
+ return trackDecodeOperation("idleRows", () => this.all(sessionID, after, "idle"));
1023
+ }
1024
+ earliestSequence(sessionID) {
1025
+ const row = this.prepare("SELECT MIN(seq) AS seq FROM session_message WHERE session_id = ?").get(sessionID);
1026
+ return typeof row?.seq === "number" ? row.seq : undefined;
1027
+ }
1028
+ latestSequence(sessionID) {
1029
+ const row = this.prepare("SELECT MAX(seq) AS seq FROM session_message WHERE session_id = ?").get(sessionID);
1030
+ return typeof row?.seq === "number" ? row.seq : -1;
1031
+ }
1032
+ assistantSince(sessionID, afterSeq) {
1033
+ return trackDecodeOperation("assistantSince", () => {
1034
+ const rows = this.prepare(`SELECT id, session_id, type, seq, time_created, data FROM session_message
1035
+ WHERE session_id = ? AND type = 'assistant' AND seq > ?
1036
+ ORDER BY seq ASC`).all(sessionID, afterSeq);
1037
+ return rows.map((row) => decode(row));
1038
+ });
1039
+ }
1040
+ latestAssistant(sessionID) {
1041
+ return trackDecodeOperation("latestAssistant", () => {
1042
+ const row = this.prepare(`SELECT id, session_id, type, seq, time_created, data FROM session_message
1043
+ WHERE session_id = ? AND type = 'assistant'
1044
+ ORDER BY seq DESC LIMIT 1`).get(sessionID);
1045
+ return row ? decode(row) : undefined;
1046
+ });
1047
+ }
1048
+ latestIdle(sessionID) {
1049
+ return trackDecodeOperation("latestIdle", () => {
1050
+ const row = this.prepare(`SELECT id, session_id, type, seq, time_created, data FROM session_message
1051
+ WHERE session_id = ? AND type = 'idle'
1052
+ ORDER BY seq DESC LIMIT 1`).get(sessionID);
1053
+ return row ? decode(row) : undefined;
1054
+ });
1055
+ }
1056
+ window(sessionID) {
1057
+ return trackDecodeOperation("window", () => this.db.transaction(() => {
1058
+ const cut = this.latestCompaction(sessionID);
1059
+ return this.all(sessionID, cut ? cut.seq - 1 : -1);
1060
+ }).deferred());
1061
+ }
1062
+ history(sessionID) {
1063
+ return trackDecodeOperation("history", () => this.all(sessionID, -1));
1064
+ }
1065
+ all(sessionID, after, type, through) {
1066
+ const rows = [];
1067
+ for (;; ) {
1068
+ const page = this.page(sessionID, { after, through, type });
1069
+ rows.push(...page.rows);
1070
+ if (page.rows.length < 100 || page.cursor === undefined)
1071
+ return rows;
1072
+ after = page.cursor;
1073
+ }
1074
+ }
450
1075
  }
451
1076
 
452
1077
  // ../plugin/src/features/magic-context/session-activity.ts
@@ -578,6 +1203,22 @@ var SESSION_SCOPED_TABLES = [
578
1203
  { table: "lkg_slots" },
579
1204
  { table: "lkg_slot_chunks" }
580
1205
  ];
1206
+ var cleanupStatements = new WeakMap;
1207
+ function cleanupStatement(db, sql) {
1208
+ let cache = cleanupStatements.get(db);
1209
+ if (!cache) {
1210
+ cache = new Map;
1211
+ cleanupStatements.set(db, cache);
1212
+ }
1213
+ let statement = cache.get(sql);
1214
+ if (!statement) {
1215
+ if (cache.size >= 256)
1216
+ cache.clear();
1217
+ statement = prepareCachedStatement2(db, sql);
1218
+ cache.set(sql, statement);
1219
+ }
1220
+ return statement;
1221
+ }
581
1222
  function deleteSessionScopedRows(db, sessionIds, harness, options = {}) {
582
1223
  if (sessionIds.length === 0)
583
1224
  return 0;
@@ -585,7 +1226,7 @@ function deleteSessionScopedRows(db, sessionIds, harness, options = {}) {
585
1226
  if (options.rustModuleCleanupAcknowledged !== true) {
586
1227
  const placeholders = sessionIds.map(() => "?").join(", ");
587
1228
  const harnessPredicate = harness === undefined ? "" : " AND harness = ?";
588
- const protectedRows = db.prepare(`SELECT session_id
1229
+ const protectedRows = cleanupStatement(db, `SELECT session_id
589
1230
  FROM pending_session_cleanup
590
1231
  WHERE session_id IN (${placeholders})
591
1232
  AND harness LIKE '%:rust'${harnessPredicate}`).all(...sessionIds, ...harness === undefined ? [] : [`${harness}:rust`]);
@@ -597,11 +1238,11 @@ function deleteSessionScopedRows(db, sessionIds, harness, options = {}) {
597
1238
  const placeholders = deletableSessionIds.map(() => "?").join(", ");
598
1239
  const harnessTables = SESSION_SCOPED_TABLES.filter((definition) => definition.harnessScoped);
599
1240
  if (options.preserveTagRevisionTombstones) {
600
- const owners = db.prepare(harnessTables.map((definition) => `SELECT session_id, harness FROM ${definition.table} WHERE session_id IN (${placeholders})${definition.extraPredicate ? ` AND ${definition.extraPredicate}` : ""}`).join(" UNION ")).all(...harnessTables.flatMap(() => deletableSessionIds));
601
- const record = db.prepare("INSERT OR IGNORE INTO schema_migrations_meta (key, value) VALUES (?, ?)");
1241
+ const owners = cleanupStatement(db, harnessTables.map((definition) => `SELECT session_id, harness FROM ${definition.table} WHERE session_id IN (${placeholders})${definition.extraPredicate ? ` AND ${definition.extraPredicate}` : ""}`).join(" UNION ")).all(...harnessTables.flatMap(() => deletableSessionIds));
1242
+ const record = cleanupStatement(db, "INSERT OR IGNORE INTO schema_migrations_meta (key, value) VALUES (?, ?)");
602
1243
  for (const owner of owners)
603
1244
  record.run(tagTombstoneOwnerKey(owner.session_id, owner.harness), owner.harness);
604
- const fallback = db.prepare(`INSERT INTO schema_migrations_meta (key, value)
1245
+ const fallback = cleanupStatement(db, `INSERT INTO schema_migrations_meta (key, value)
605
1246
  SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM schema_migrations_meta WHERE key >= ? AND key < ?)`);
606
1247
  for (const id of deletableSessionIds)
607
1248
  fallback.run(tagTombstoneOwnerKey(id, getHarness2()), getHarness2(), ...tagTombstoneOwnerRange(id));
@@ -610,7 +1251,7 @@ function deleteSessionScopedRows(db, sessionIds, harness, options = {}) {
610
1251
  deleteChunkEmbedBackoffForSession(db, sessionId);
611
1252
  for (const definition of SESSION_SCOPED_TABLES) {
612
1253
  if (definition.table === "message_history_fts") {
613
- db.prepare(`DELETE FROM message_history_fts
1254
+ cleanupStatement(db, `DELETE FROM message_history_fts
614
1255
  WHERE rowid IN (
615
1256
  SELECT fts_rowid
616
1257
  FROM message_fts_rowid_map
@@ -625,7 +1266,7 @@ function deleteSessionScopedRows(db, sessionIds, harness, options = {}) {
625
1266
  const bindHarness = harness !== undefined && definition.harnessScoped === true;
626
1267
  if (bindHarness)
627
1268
  predicates.push("harness = ?");
628
- const statement = db.prepare(`DELETE FROM ${definition.table} WHERE ${predicates.join(" AND ")}`);
1269
+ const statement = cleanupStatement(db, `DELETE FROM ${definition.table} WHERE ${predicates.join(" AND ")}`);
629
1270
  if (bindHarness)
630
1271
  statement.run(...deletableSessionIds, harness);
631
1272
  else
@@ -633,9 +1274,9 @@ function deleteSessionScopedRows(db, sessionIds, harness, options = {}) {
633
1274
  }
634
1275
  deleteSessionActivity(db, deletableSessionIds);
635
1276
  if (!options.preserveTagRevisionTombstones) {
636
- const retireOwner = db.prepare(harness === undefined ? "DELETE FROM schema_migrations_meta WHERE key >= ? AND key < ?" : "DELETE FROM schema_migrations_meta WHERE key = ?");
1277
+ const retireOwner = cleanupStatement(db, harness === undefined ? "DELETE FROM schema_migrations_meta WHERE key >= ? AND key < ?" : "DELETE FROM schema_migrations_meta WHERE key = ?");
637
1278
  const survivingRows = harnessTables.map((definition) => `EXISTS (SELECT 1 FROM ${definition.table} WHERE session_id = ?${definition.extraPredicate ? ` AND ${definition.extraPredicate}` : ""})`).join(" OR ");
638
- const retire = db.prepare(`DELETE FROM schema_migrations_meta WHERE key IN (?, ?)
1279
+ const retire = cleanupStatement(db, `DELETE FROM schema_migrations_meta WHERE key IN (?, ?)
639
1280
  AND NOT (${survivingRows})
640
1281
  AND NOT EXISTS (SELECT 1 FROM schema_migrations_meta AS owners WHERE owners.key >= ? AND owners.key < ?)`);
641
1282
  for (const id of deletableSessionIds) {
@@ -646,10 +1287,59 @@ function deleteSessionScopedRows(db, sessionIds, harness, options = {}) {
646
1287
  retire.run(tagIdentityRevisionKey(id), tagRevisionKey(id), ...harnessTables.map(() => id), ...tagTombstoneOwnerRange(id));
647
1288
  }
648
1289
  }
649
- for (const sessionId of deletableSessionIds)
650
- clearLkgRecoveryFence(sessionId);
1290
+ if (!options.deferRecoveryFenceCleanup)
1291
+ for (const sessionId of deletableSessionIds)
1292
+ clearLkgRecoveryFence(sessionId);
651
1293
  return deletableSessionIds.length;
652
1294
  }
1295
+ var SESSION_CLEANUP_SLICE_ROWS = 64;
1296
+ function deleteSessionScopedRowsSlice(db, sessionId, harness) {
1297
+ if (cleanupStatement(db, "SELECT 1 FROM pending_session_cleanup WHERE session_id = ? AND harness = ?").get(sessionId, `${harness}:rust`))
1298
+ return { complete: true, deleted: 0 };
1299
+ cleanupStatement(db, "INSERT OR IGNORE INTO schema_migrations_meta(key,value) VALUES (?,?)").run(tagTombstoneOwnerKey(sessionId, harness), harness);
1300
+ let remaining = SESSION_CLEANUP_SLICE_ROWS;
1301
+ const started = performance.now();
1302
+ remaining -= deleteChunkEmbedBackoffForSession(db, sessionId, remaining);
1303
+ if (remaining <= 0)
1304
+ return { complete: false, deleted: 0 };
1305
+ for (const definition of SESSION_SCOPED_TABLES) {
1306
+ if (definition.table === "message_history_fts") {
1307
+ const rows = cleanupStatement(db, "SELECT fts_rowid AS id FROM message_fts_rowid_map WHERE session_id = ? LIMIT ?").all(sessionId, remaining);
1308
+ const removeFts = cleanupStatement(db, "DELETE FROM message_history_fts WHERE rowid = ?");
1309
+ const removeMap = cleanupStatement(db, "DELETE FROM message_fts_rowid_map WHERE session_id = ? AND fts_rowid = ?");
1310
+ for (const row of rows) {
1311
+ removeFts.run(row.id);
1312
+ removeMap.run(sessionId, row.id);
1313
+ }
1314
+ remaining -= rows.length;
1315
+ if (remaining > 0 && !getMessageFtsRowidMapBackfillProgress2(db).completed)
1316
+ remaining -= cleanupStatement(db, `DELETE FROM message_history_fts WHERE rowid IN (
1317
+ SELECT rowid FROM message_history_fts WHERE session_id = ? LIMIT ?
1318
+ )`).run(sessionId, remaining).changes;
1319
+ } else {
1320
+ const predicates = ["session_id = ?"];
1321
+ if (definition.extraPredicate)
1322
+ predicates.push(definition.extraPredicate);
1323
+ if (definition.harnessScoped)
1324
+ predicates.push("harness = ?");
1325
+ const args = [sessionId, ...definition.harnessScoped ? [harness] : [], remaining];
1326
+ const withoutRowid = definition.table === "temporal_decisions" || definition.table === "session_replay_decisions";
1327
+ const key = withoutRowid ? "message_id" : "rowid";
1328
+ remaining -= cleanupStatement(db, `DELETE FROM ${definition.table}
1329
+ WHERE ${withoutRowid ? "session_id = ? AND " : ""}${key} IN (
1330
+ SELECT ${key} FROM ${definition.table} WHERE ${predicates.join(" AND ")} LIMIT ?
1331
+ )`).run(...withoutRowid ? [sessionId] : [], ...args).changes;
1332
+ }
1333
+ if (remaining <= 0 || performance.now() - started >= 10)
1334
+ return { complete: false, deleted: 0 };
1335
+ }
1336
+ return {
1337
+ complete: true,
1338
+ deleted: deleteSessionScopedRows(db, [sessionId], harness, {
1339
+ deferRecoveryFenceCleanup: true
1340
+ })
1341
+ };
1342
+ }
653
1343
 
654
1344
  // ../plugin/src/features/magic-context/compression-depth-storage.ts
655
1345
  var incrementDepthStatements = new WeakMap;
@@ -675,7 +1365,54 @@ function clearCompressionDepthRange(db, sessionId, startOrdinal, endOrdinal) {
675
1365
  }
676
1366
 
677
1367
  // ../plugin/src/features/magic-context/message-index.ts
678
- import { createHash as createHash3 } from "node:crypto";
1368
+ import { createHash as createHash4 } from "node:crypto";
1369
+
1370
+ // ../plugin/src/shared/search-text.ts
1371
+ function removeTaskNotifications(text) {
1372
+ return removeNestedBlocks(text, ["task", "task_cohort"]);
1373
+ }
1374
+ function cleanSearchText(text) {
1375
+ return removeNestedBlocks(text, ["system-reminder", "task", "task_cohort"]).replace(/\s+/g, " ").trim();
1376
+ }
1377
+ var QUERY_STOP_WORDS = new Set("a an and are as at be been by can could did do does for from how i in is it me my of on or our please should that the their them there these they this to us was we were what when where which who why will with would you your".split(" "));
1378
+ function terms(text) {
1379
+ return [
1380
+ ...new Set((text.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []).filter((term) => !QUERY_STOP_WORDS.has(term)))
1381
+ ];
1382
+ }
1383
+ function keywordRelevance(query, content) {
1384
+ const needles = terms(query);
1385
+ if (needles.length === 0)
1386
+ return 0;
1387
+ const words = new Set(terms(cleanSearchText(content)));
1388
+ return 0.8 * needles.filter((term) => words.has(term)).length / needles.length;
1389
+ }
1390
+ function matchedSearchSnippet(content, query, cap = 220) {
1391
+ const text = cleanSearchText(content);
1392
+ const needles = new Set(terms(query));
1393
+ const matches = [...text.matchAll(/[\p{L}\p{N}_]+/gu)].filter((match) => needles.has(match[0].toLowerCase()));
1394
+ if (matches.length === 0)
1395
+ return "";
1396
+ let bestStart = matches[0].index;
1397
+ let bestCoverage = -1;
1398
+ for (const match of matches) {
1399
+ const start = Math.max(0, match.index - Math.floor(cap / 5));
1400
+ const coverage = keywordRelevance(query, text.slice(start, start + cap));
1401
+ if (coverage > bestCoverage) {
1402
+ bestStart = start;
1403
+ bestCoverage = coverage;
1404
+ }
1405
+ }
1406
+ if (bestStart > 0) {
1407
+ const boundary = text.indexOf(" ", bestStart);
1408
+ if (boundary >= 0 && boundary < matches[matches.length - 1].index)
1409
+ bestStart = boundary + 1;
1410
+ }
1411
+ const passage = text.slice(bestStart, bestStart + cap).trim();
1412
+ return `${bestStart > 0 ? "…" : ""}${passage}${bestStart + cap < text.length ? "…" : ""}`;
1413
+ }
1414
+
1415
+ // ../plugin/src/features/magic-context/message-index.ts
679
1416
  var MESSAGE_HISTORY_ORPHAN_SWEEP_BATCH_SIZE = 200;
680
1417
  var MESSAGE_HISTORY_ORPHAN_SAFETY_AGE_MS = 24 * 60 * 60 * 1000;
681
1418
  var MESSAGE_HISTORY_ORPHAN_SWEEP_COOLDOWN_MS = 10 * 60 * 1000;
@@ -904,7 +1641,7 @@ function toMessageIndexSource2(message) {
904
1641
  role: message.role,
905
1642
  createdAt: message.createdAt,
906
1643
  sourceVersion: normalizeSourceVersion(message.version),
907
- contentHash: createHash3("sha256").update(content).digest("hex"),
1644
+ contentHash: createHash4("sha256").update(content).digest("hex"),
908
1645
  content
909
1646
  };
910
1647
  }
@@ -1006,10 +1743,10 @@ function getIndexableContent2(role, parts) {
1006
1743
  if (!hasMeaningfulUserText2(parts)) {
1007
1744
  return "";
1008
1745
  }
1009
- return extractTexts2(parts).map(cleanUserText2).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
1746
+ return extractTexts2(parts).map(removeTaskNotifications).map(cleanUserText2).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
1010
1747
  }
1011
1748
  if (role === "assistant") {
1012
- return extractTexts2(parts).map(removeSystemReminders).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
1749
+ return extractTexts2(parts).map(removeTaskNotifications).map(removeSystemReminders).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
1013
1750
  }
1014
1751
  return "";
1015
1752
  }
@@ -1144,7 +1881,7 @@ function indexItemsAfterOrdinal(db, sessionId, items, finalWatermark, toSource)
1144
1881
  return inserted;
1145
1882
  }
1146
1883
  function harnessSupportsOpenCodeOrphanSweep(harness) {
1147
- return harness === "opencode";
1884
+ return harness === "opencode" || harness === "opencode2";
1148
1885
  }
1149
1886
  function getMessageHistoryOrphanSweepState(db, harness) {
1150
1887
  return db.prepare("SELECT cursor_session_id, last_swept_at FROM message_history_orphan_sweep WHERE harness = ?").get(harness) ?? {};
@@ -1173,7 +1910,7 @@ function getOpenCodeSessionScopedCandidateSourceSql(harness) {
1173
1910
  UNION
1174
1911
  `);
1175
1912
  }
1176
- function sweepOrphanedOpenCodeMessageIndexesInBackground(db, openReadableOpenCodeDb, options = {}) {
1913
+ function* sweepOrphanedOpenCodeMessageIndexesInBackground(db, openReadableOpenCodeDb, options = {}) {
1177
1914
  const now = options.now ?? Date.now();
1178
1915
  const batchSize = Math.max(1, Math.floor(options.batchSize ?? MESSAGE_HISTORY_ORPHAN_SWEEP_BATCH_SIZE));
1179
1916
  const safetyAgeMs = Math.max(0, options.safetyAgeMs ?? MESSAGE_HISTORY_ORPHAN_SAFETY_AGE_MS);
@@ -1187,15 +1924,6 @@ function sweepOrphanedOpenCodeMessageIndexesInBackground(db, openReadableOpenCod
1187
1924
  }
1188
1925
  if (!harnessSupportsOpenCodeOrphanSweep(harness)) {
1189
1926
  persistMessageHistoryOrphanSweepState(db, cursor, now + unavailableReprobeMs - cooldownMs, harness);
1190
- if (harness === "opencode2") {
1191
- return {
1192
- status: "unavailable",
1193
- scanned: 0,
1194
- deleted: 0,
1195
- cursor,
1196
- reason: "OpenCode 2 keeps its sessions in session_v2, which this sweep does not read"
1197
- };
1198
- }
1199
1927
  return { status: "unavailable", scanned: 0, deleted: 0, cursor };
1200
1928
  }
1201
1929
  let openCodeDb = null;
@@ -1208,7 +1936,27 @@ function sweepOrphanedOpenCodeMessageIndexesInBackground(db, openReadableOpenCod
1208
1936
  persistMessageHistoryOrphanSweepState(db, cursor, now + unavailableReprobeMs - cooldownMs, harness);
1209
1937
  return { status: "source_unavailable", scanned: 0, deleted: 0, cursor };
1210
1938
  }
1939
+ let v2Reader;
1211
1940
  try {
1941
+ let sessionExists;
1942
+ if (harness === "opencode2") {
1943
+ try {
1944
+ const path = getSqliteDatabasePath2(openCodeDb);
1945
+ if (!path)
1946
+ throw new Error("OpenCode 2 source has no durable path");
1947
+ v2Reader = new V2StoreReader(path);
1948
+ const liveIds = new Set(v2Reader.sessionIds());
1949
+ if (liveIds.size === 0)
1950
+ throw new Error("OpenCode 2 source has no sessions");
1951
+ sessionExists = (id) => liveIds.has(id);
1952
+ } catch {
1953
+ persistMessageHistoryOrphanSweepState(db, cursor, now + unavailableReprobeMs - cooldownMs, harness);
1954
+ return { status: "source_unavailable", scanned: 0, deleted: 0, cursor };
1955
+ }
1956
+ } else {
1957
+ const query = openCodeDb.prepare("SELECT 1 FROM session WHERE id = ? LIMIT 1");
1958
+ sessionExists = (id) => !!query.get(id);
1959
+ }
1212
1960
  const cutoff = now - safetyAgeMs;
1213
1961
  const candidateSourceSql = getOpenCodeSessionScopedCandidateSourceSql(harness);
1214
1962
  const candidates = db.prepare(`SELECT session_id
@@ -1223,39 +1971,56 @@ function sweepOrphanedOpenCodeMessageIndexesInBackground(db, openReadableOpenCod
1223
1971
  )
1224
1972
  ORDER BY session_id ASC
1225
1973
  LIMIT ?`).all(cursor, cutoff, batchSize);
1226
- const sessionExists = openCodeDb.prepare("SELECT 1 FROM session WHERE id = ? LIMIT 1");
1227
- const missingSessionIds = candidates.filter((candidate) => !sessionExists.get(candidate.session_id)).map((candidate) => candidate.session_id);
1974
+ const missingSessionIds = candidates.filter((candidate) => !sessionExists(candidate.session_id)).map((candidate) => candidate.session_id);
1228
1975
  const nextCursor = candidates.length < batchSize ? "" : candidates[candidates.length - 1]?.session_id ?? cursor;
1229
1976
  const completedAt = candidates.length < batchSize ? now : null;
1230
- db.exec("BEGIN IMMEDIATE");
1231
- const transactionStartedAt = performance.now();
1232
- let committed = false;
1233
1977
  let deleted = 0;
1234
- try {
1235
- const stillEligible = db.prepare(`SELECT 1
1236
- FROM (${candidateSourceSql}) AS session_candidates
1237
- WHERE session_id = ?
1238
- AND NOT EXISTS (
1239
- SELECT 1
1240
- FROM message_history_index
1241
- WHERE message_history_index.session_id = session_candidates.session_id
1242
- AND message_history_index.harness = '${harness}'
1243
- AND message_history_index.updated_at > ?
1244
- )
1245
- LIMIT 1`);
1246
- const eligibleSessionIds = missingSessionIds.filter((sessionId) => !sessionExists.get(sessionId) && stillEligible.get(sessionId, cutoff));
1247
- deleted = deleteSessionScopedRows(db, eligibleSessionIds, harness);
1248
- persistMessageHistoryOrphanSweepState(db, nextCursor, completedAt, harness);
1249
- db.exec("COMMIT");
1250
- committed = true;
1251
- logSlowWriteTransaction("message_index_orphan_sweep", transactionStartedAt);
1252
- } finally {
1253
- if (!committed) {
1978
+ let slices = 0;
1979
+ const stillEligible = db.prepare(`SELECT 1 WHERE NOT EXISTS (
1980
+ SELECT 1 FROM message_history_index
1981
+ WHERE session_id = ? AND harness = '${harness}' AND updated_at > ?
1982
+ )`);
1983
+ for (const sessionId of missingSessionIds) {
1984
+ let complete = false;
1985
+ do {
1986
+ let removed = 0;
1987
+ db.exec("BEGIN IMMEDIATE");
1988
+ const transactionStartedAt = performance.now();
1989
+ let committed = false;
1254
1990
  try {
1255
- db.exec("ROLLBACK");
1256
- } catch {}
1257
- }
1991
+ const hostStillMissing = v2Reader ? !v2Reader.sessionExists(sessionId) : !sessionExists(sessionId);
1992
+ if (hostStillMissing && stillEligible.get(sessionId, cutoff)) {
1993
+ const slice = deleteSessionScopedRowsSlice(db, sessionId, harness);
1994
+ removed = slice.deleted;
1995
+ complete = slice.complete;
1996
+ } else
1997
+ complete = true;
1998
+ db.exec("COMMIT");
1999
+ committed = true;
2000
+ logSlowWriteTransaction("message_index_orphan_sweep", transactionStartedAt);
2001
+ } finally {
2002
+ if (!committed) {
2003
+ try {
2004
+ db.exec("ROLLBACK");
2005
+ } catch {}
2006
+ }
2007
+ }
2008
+ deleted += removed;
2009
+ if (removed)
2010
+ clearLkgRecoveryFence(sessionId);
2011
+ yield;
2012
+ if (++slices >= Math.max(1, options.maxSlices ?? 512)) {
2013
+ return {
2014
+ status: "swept",
2015
+ scanned: candidates.length,
2016
+ deleted,
2017
+ cursor,
2018
+ partial: true
2019
+ };
2020
+ }
2021
+ } while (!complete);
1258
2022
  }
2023
+ persistMessageHistoryOrphanSweepState(db, nextCursor, completedAt, harness);
1259
2024
  return {
1260
2025
  status: "swept",
1261
2026
  scanned: candidates.length,
@@ -1263,11 +2028,24 @@ function sweepOrphanedOpenCodeMessageIndexesInBackground(db, openReadableOpenCod
1263
2028
  cursor: nextCursor
1264
2029
  };
1265
2030
  } finally {
2031
+ v2Reader?.close();
1266
2032
  closeQuietly(openCodeDb);
1267
2033
  }
1268
2034
  }
1269
- function sweepOrphanedOpenCodeMessageIndexes2(db, openReadableOpenCodeDb, options = {}) {
1270
- return withoutSqliteTransformPass2(() => withSqliteBackgroundWriter2(() => sweepOrphanedOpenCodeMessageIndexesInBackground(db, openReadableOpenCodeDb, options)));
2035
+ async function sweepOrphanedOpenCodeMessageIndexesCooperatively2(db, openSource, options = {}) {
2036
+ return withoutSqliteTransformPass2(() => withSqliteBackgroundWriter2(async () => {
2037
+ const sweep = sweepOrphanedOpenCodeMessageIndexesInBackground(db, openSource, options);
2038
+ try {
2039
+ for (;; ) {
2040
+ const step = sweep.next();
2041
+ if (step.done)
2042
+ return step.value;
2043
+ await new Promise((resolve) => setTimeout(resolve, 0));
2044
+ }
2045
+ } finally {
2046
+ sweep.return({ status: "source_unavailable", scanned: 0, deleted: 0, cursor: "" });
2047
+ }
2048
+ }));
1271
2049
  }
1272
2050
 
1273
- export { BoundedSessionMap, LkgRecoveryFenceError, clearLkgFenceProcessState, lkgFenceRequiresRebuild, lkgRecoveryFenceUnclean, ownsLkgDirtyFence, invalidateCleanLkgRecoveryFence, markLkgRecoveryDirty, commitLkgWithRecoveryFence, clearLkgRecoveryFence, readSessionActivity, observeSessionActivity, backfillSessionActivity, deleteSessionScopedRows, clearCompressionDepth, clearCompressionDepthRange, MESSAGE_INDEX_MAX_FTS_BYTES2, messageIndexSkipKey2, toMessageIndexSource2, getMessageIndexSourceIdentity2, isMessageIndexSourceCurrent2, getLastIndexedOrdinal2, getIndexedMessageCorpusSize2, markMessageIndexDirty2, getMessageIndexReconciliationStartOrdinal2, isMessageIndexReconciledThrough2, deleteIndexedMessage2, clearIndexedMessagesInTransaction2, clearIndexedMessages2, getIndexableContent2, indexSingleMessage2, indexMessagesAfterOrdinal2, indexSourcesAfterOrdinal2, sweepOrphanedOpenCodeMessageIndexes2 };
2051
+ export { BoundedSessionMap, LkgRecoveryFenceError, clearLkgFenceProcessState, lkgFenceRequiresRebuild, lkgRecoveryFenceUnclean, ownsLkgDirtyFence, invalidateCleanLkgRecoveryFence, markLkgRecoveryDirty, commitLkgWithRecoveryFence, clearLkgRecoveryFence, gaDatabasePath, V2StoreReader, readSessionActivity, observeSessionActivity, backfillSessionActivity, deleteSessionScopedRows, clearCompressionDepth, clearCompressionDepthRange, cleanSearchText, keywordRelevance, matchedSearchSnippet, MESSAGE_INDEX_MAX_FTS_BYTES2, messageIndexSkipKey2, toMessageIndexSource2, getMessageIndexSourceIdentity2, isMessageIndexSourceCurrent2, getLastIndexedOrdinal2, getIndexedMessageCorpusSize2, markMessageIndexDirty2, getMessageIndexReconciliationStartOrdinal2, isMessageIndexReconciledThrough2, deleteIndexedMessage2, clearIndexedMessagesInTransaction2, clearIndexedMessages2, getIndexableContent2, indexSingleMessage2, indexMessagesAfterOrdinal2, indexSourcesAfterOrdinal2, sweepOrphanedOpenCodeMessageIndexesCooperatively2 };