pi-mega-compact 0.8.21 → 0.8.23

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 (84) hide show
  1. package/LICENSE +6 -2
  2. package/README.md +1 -1
  3. package/dist/extensions/dashboard-server/dashboard-client-core.js +201 -0
  4. package/dist/extensions/dashboard-server/dashboard-client-game.js +241 -0
  5. package/dist/extensions/dashboard-server/dashboard-client-repos.js +212 -0
  6. package/dist/extensions/dashboard-server/dashboard-client.js +19 -0
  7. package/dist/extensions/dashboard-server/html.js +2 -621
  8. package/dist/extensions/dashboard-server/routes-core.js +62 -0
  9. package/dist/extensions/dashboard-server/routes-game.js +323 -0
  10. package/dist/extensions/dashboard-server/routes-repo.js +170 -0
  11. package/dist/extensions/dashboard-server/routes-sessions.js +159 -0
  12. package/dist/extensions/dashboard-server/routes.js +10 -0
  13. package/dist/extensions/dashboard-server/server.js +26 -623
  14. package/dist/extensions/mega-commands.js +4 -3
  15. package/dist/extensions/mega-events/agent-handlers.js +2 -1
  16. package/dist/extensions/mega-events/compact-handlers.js +26 -0
  17. package/dist/extensions/mega-events/session-handlers.js +2 -1
  18. package/dist/extensions/mega-pipeline/compact.js +3 -2
  19. package/dist/extensions/mega-runtime/state.js +7 -7
  20. package/dist/src/dedup/raptor/multilevel.js +172 -0
  21. package/dist/src/dedup/raptor/multilevel.test.js +203 -0
  22. package/dist/src/dedup/raptor/promote.test.js +5 -5
  23. package/dist/src/dedup/raptor/retrieval.js +1 -1
  24. package/dist/src/dedup/sprint12.test.js +7 -7
  25. package/dist/src/dedup-engine.test.js +29 -29
  26. package/dist/src/e2e.test.js +38 -38
  27. package/dist/src/engine.js +3 -3
  28. package/dist/src/engine.test.js +6 -6
  29. package/dist/src/importance.js +197 -0
  30. package/dist/src/importance.test.js +372 -0
  31. package/dist/src/ratio.bench.test.js +18 -18
  32. package/dist/src/recall.js +6 -5
  33. package/dist/src/recall.test.js +85 -27
  34. package/dist/src/sprint14.test.js +2 -2
  35. package/dist/src/store/migrate.test.js +5 -5
  36. package/dist/src/store/sprint10.test.js +5 -5
  37. package/dist/src/store/sqlite/global-index.js +5 -174
  38. package/dist/src/store/sqlite/global-sessions.js +190 -0
  39. package/dist/src/vector-read.js +168 -0
  40. package/dist/src/vector-search.js +191 -0
  41. package/dist/src/vectorStore.js +10 -297
  42. package/dist/src/vectorStore.test.js +32 -32
  43. package/extensions/dashboard-server/dashboard-client-core.ts +202 -0
  44. package/extensions/dashboard-server/dashboard-client-game.ts +242 -0
  45. package/extensions/dashboard-server/dashboard-client-repos.ts +213 -0
  46. package/extensions/dashboard-server/dashboard-client.ts +21 -0
  47. package/extensions/dashboard-server/html.ts +2 -621
  48. package/extensions/dashboard-server/routes-core.ts +113 -0
  49. package/extensions/dashboard-server/routes-game.ts +386 -0
  50. package/extensions/dashboard-server/routes-repo.ts +212 -0
  51. package/extensions/dashboard-server/routes-sessions.ts +195 -0
  52. package/extensions/dashboard-server/routes.ts +13 -0
  53. package/extensions/dashboard-server/server.ts +37 -700
  54. package/extensions/mega-commands.ts +4 -3
  55. package/extensions/mega-events/agent-handlers.ts +2 -1
  56. package/extensions/mega-events/compact-handlers.ts +28 -0
  57. package/extensions/mega-events/session-handlers.ts +2 -1
  58. package/extensions/mega-pipeline/compact.ts +3 -2
  59. package/extensions/mega-runtime/state.ts +7 -7
  60. package/extensions/openclaw-mega-compact.ts +2 -2
  61. package/package.json +2 -2
  62. package/src/dedup/raptor/multilevel.test.ts +278 -0
  63. package/src/dedup/raptor/multilevel.ts +246 -0
  64. package/src/dedup/raptor/promote.test.ts +5 -5
  65. package/src/dedup/raptor/retrieval.ts +1 -1
  66. package/src/dedup/sprint12.test.ts +7 -7
  67. package/src/dedup-engine.test.ts +30 -30
  68. package/src/e2e.test.ts +38 -38
  69. package/src/engine.test.ts +6 -6
  70. package/src/engine.ts +3 -3
  71. package/src/importance.test.ts +538 -0
  72. package/src/importance.ts +312 -0
  73. package/src/ratio.bench.test.ts +18 -18
  74. package/src/recall.test.ts +101 -29
  75. package/src/recall.ts +9 -9
  76. package/src/sprint14.test.ts +2 -2
  77. package/src/store/migrate.test.ts +5 -5
  78. package/src/store/sprint10.test.ts +5 -5
  79. package/src/store/sqlite/global-index.ts +18 -290
  80. package/src/store/sqlite/global-sessions.ts +291 -0
  81. package/src/vector-read.ts +237 -0
  82. package/src/vector-search.ts +231 -0
  83. package/src/vectorStore.test.ts +32 -32
  84. package/src/vectorStore.ts +29 -356
@@ -8,7 +8,7 @@ import assert from "node:assert/strict";
8
8
  import { mkdtempSync, rmSync, readFileSync, existsSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
- import { VectorStore } from "./vectorStore.js";
11
+ import { VectorStore, vectorSearch } from "./vectorStore.js";
12
12
  import { defaultEmbedder } from "./embedder.js";
13
13
  import { loadDedupConfig, type DedupConfigShape } from "./config/dedup.js";
14
14
  import {
@@ -49,7 +49,7 @@ test("flag matrix: all 16 L0/L1/L2/RAPTOR enable combos are safe", () => {
49
49
  s.add({ sessionId: "s", summary: "x", regionText: `region A for combo ${combos} about the cache`, timestamp: 1 });
50
50
  const r2 = s.add({ sessionId: "s", summary: "x", regionText: `region B for combo ${combos} about the parser`, timestamp: 2 });
51
51
  // search must not throw under any combination
52
- const hits = s.search("s", "cache", 3);
52
+ const hits = vectorSearch(s, "s", "cache", 3);
53
53
  assert.ok(Array.isArray(hits));
54
54
  // With all tiers off, the second add is always "new" (no collapse).
55
55
  if (!l0 && !l1 && !l2) assert.equal(r2.deduped, false);
@@ -16,7 +16,7 @@ import assert from "node:assert/strict";
16
16
  import { mkdtempSync, rmSync, existsSync } from "node:fs";
17
17
  import { tmpdir } from "node:os";
18
18
  import { join } from "node:path";
19
- import { VectorStore } from "../vectorStore.js";
19
+ import { VectorStore, vectorMarkInjected, vectorWasInjected, vectorSearch } from "../vectorStore.js";
20
20
  import { writeGzJson } from "../store.js";
21
21
  import type { StoredCheckpoint } from "../store.js";
22
22
  import { migrateJsonToSqlite, readLegacyCheckpointFile } from "../store/migrate.js";
@@ -123,7 +123,7 @@ test("cross-process recall: fresh VectorStore over same dir recalls prior checkp
123
123
 
124
124
  // Process B: brand-new VectorStore, same stateDir.
125
125
  const b = new VectorStore({ stateDir: dir });
126
- const hits = b.search(sid, "cross process recall proof", 5);
126
+ const hits = vectorSearch(b, sid, "cross process recall proof", 5);
127
127
  assert.equal(hits.length, 1, "checkpoint survives cross-process reopen");
128
128
  assert.equal(hits[0].checkpoint.checkpointId, "chkpt_001");
129
129
  assert.ok(hits[0].score > 0.5, "recall is relevant");
@@ -142,13 +142,13 @@ test("cross-process recall: injected state persists across reopen", () => {
142
142
  regionText: "injected state persists across process reopen test",
143
143
  timestamp: 1,
144
144
  });
145
- a.markInjected(sid, added.checkpoint.checkpointId);
146
- assert.equal(a.wasInjected(sid, added.checkpoint.checkpointId), true);
145
+ vectorMarkInjected(a, sid, added.checkpoint.checkpointId);
146
+ assert.equal(vectorWasInjected(a, sid, added.checkpoint.checkpointId), true);
147
147
 
148
148
  closeStore(dir);
149
149
 
150
150
  const b = new VectorStore({ stateDir: dir });
151
- assert.equal(b.wasInjected(sid, added.checkpoint.checkpointId), true, "injection remembered");
151
+ assert.equal(vectorWasInjected(b, sid, added.checkpoint.checkpointId), true, "injection remembered");
152
152
 
153
153
  closeStore(dir);
154
154
  });
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { mkdtempSync, rmSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { VectorStore } from "../vectorStore.js";
6
+ import { VectorStore, vectorList, vectorRepoStats } from "../vectorStore.js";
7
7
  import { normalize } from "../dedup/normalize.js";
8
8
  import { openBloom, closeBloom } from "./bloom.js";
9
9
  import { backfillContentHashes, isBackfillComplete } from "./backfill.js";
@@ -29,7 +29,7 @@ test("Sprint 10 L0: case/whitespace/ANSI variants dedup to one row", () => {
29
29
  const r = s.add({ sessionId: "sess_norm", summary: "x", regionText: v, timestamp: added + 1 });
30
30
  if (!r.deduped) added++;
31
31
  }
32
- assert.equal(s.list("sess_norm").length, 1);
32
+ assert.equal(vectorList(s, "sess_norm").length, 1);
33
33
  });
34
34
 
35
35
  test("normalize case-folds so Foo/foo/FOO are equal", () => {
@@ -74,7 +74,7 @@ test("duplicate add is idempotent (no partial rows, single checkpoint)", () => {
74
74
  const b = s.add({ sessionId: "sess_atom", summary: "x", regionText: raw, timestamp: 2 });
75
75
  assert.equal(a.deduped, false);
76
76
  assert.equal(b.deduped, true);
77
- assert.equal(s.list("sess_atom").length, 1);
77
+ assert.equal(vectorList(s, "sess_atom").length, 1);
78
78
  });
79
79
 
80
80
  // --- Backfill orchestrator -------------------------------------------------
@@ -196,14 +196,14 @@ test("Sprint 10 migration: pre-0.4.2 db gains original_token_estimate and repoSt
196
196
  originalTokenEstimate: 500,
197
197
  timestamp: 1,
198
198
  });
199
- const repo = vs.repoStats();
199
+ const repo = vectorRepoStats(vs);
200
200
  // No "no such column" crash = migration succeeded; totals reflect the new col.
201
201
  assert.equal(repo.checkpointCount, 1);
202
202
  assert.equal(repo.originalTokens, 500, "originalTokens read from migrated column");
203
203
  // Re-open a second time to prove idempotency (column already exists).
204
204
  closeStore(dir);
205
205
  const vs2 = new VectorStore({ dedupSim: 0.9, stateDir: dir });
206
- assert.equal(vs2.repoStats().originalTokens, 500, "stable across re-open");
206
+ assert.equal(vectorRepoStats(vs2).originalTokens, 500, "stable across re-open");
207
207
  });
208
208
 
209
209
  // --- cleanup ---------------------------------------------------------------
@@ -12,10 +12,27 @@
12
12
  * infrequent idempotent upserts (ON CONFLICT). Fully local (PREVENT-PI-004).
13
13
  */
14
14
  import { DatabaseSync } from "node:sqlite";
15
- import { appendFileSync, existsSync, mkdirSync } from "node:fs";
16
15
  import { homedir, tmpdir } from "node:os";
16
+ import { existsSync, mkdirSync } from "node:fs";
17
17
  import { join } from "node:path";
18
18
 
19
+ // Back-compat re-exports: session heartbeat + timeseries helpers moved to
20
+ // global-sessions.ts (file-size split). Existing imports from this module
21
+ // keep resolving; behavior is unchanged.
22
+ export {
23
+ recordSessionHeartbeat,
24
+ appendTokenSample,
25
+ pruneStaleSessions,
26
+ pruneTokenSamples,
27
+ readActiveSessions,
28
+ readSessionTimeseries,
29
+ clearSessionHeartbeat,
30
+ type ActiveSessionRow,
31
+ type TokenSamplePoint,
32
+ type SessionSeries,
33
+ type SessionTimeseriesResult,
34
+ } from "./global-sessions.js";
35
+
19
36
  /** Resolve the machine-wide index directory (env-overridable). */
20
37
  export function getIndexDir(): string {
21
38
  const override = process.env.MEGACOMPACT_INDEX_DIR;
@@ -329,292 +346,3 @@ export function countInjectedGlobal(indexDir: string = getIndexDir()): number {
329
346
  return row?.n ?? 0;
330
347
  }
331
348
 
332
- // ---------------------------------------------------------------------------
333
- // S39: session heartbeats + token time-series (multi-pi stacked memory graph)
334
- //
335
- // `session_heartbeats`: one row per (pid, session_id), upserted on every
336
- // material snapshot() change. This is the reliable liveness signal the
337
- // dashboard /api/sessions endpoint reads (repo_registry.last_seen only updates
338
- // on repo-switch / bindRepo, so a long-running single-repo process looks
339
- // inactive; these heartbeats update on every material change).
340
- //
341
- // `token_samples`: append-only rows with (session_id, tokens, percent, ts) for
342
- // the stacked-memory graph. Garbage-collected by pruneTokenSamples.
343
- //
344
- // All queries use @named/$named bind parameters (PREVENT-002); local node:sqlite
345
- // + WAL (PREVENT-PI-004), multi-process safe.
346
- // ---------------------------------------------------------------------------
347
-
348
- /** A live active session row (joined heartbeat + latest token sample). */
349
- export interface ActiveSessionRow {
350
- pid: number;
351
- sessionId: string;
352
- repoRoot: string | null;
353
- stateDir: string | null;
354
- ctxWindow: number;
355
- lastSeen: number;
356
- tokens: number | null;
357
- percent: number | null;
358
- }
359
-
360
- /** A single time-series data point for a session. */
361
- export interface TokenSamplePoint {
362
- ts: number;
363
- tokens: number;
364
- percent: number;
365
- }
366
-
367
- /** A recharts-ready per-session series with a stable color. */
368
- export interface SessionSeries {
369
- sessionId: string;
370
- label: string;
371
- color: string;
372
- data: TokenSamplePoint[];
373
- }
374
-
375
- /** Timeseries response shape (recharts-ready). */
376
- export interface SessionTimeseriesResult {
377
- series: SessionSeries[];
378
- totals: { ts: number; tokens: number }[];
379
- }
380
-
381
- /** Stable color palette for per-session series (hash-based, no randomness). */
382
- const SESSION_COLORS = [
383
- "#60a5fa", // blue-400
384
- "#34d399", // emerald-400
385
- "#fbbf24", // amber-400
386
- "#f87171", // red-400
387
- "#a78bfa", // violet-400
388
- "#f472b6", // pink-400
389
- "#22d3ee", // cyan-400
390
- "#a3e635", // lime-400
391
- ];
392
-
393
- /** Hash a sessionId to a stable color index. */
394
- function sessionColor(sessionId: string): string {
395
- let h = 0;
396
- for (let i = 0; i < sessionId.length; i++) {
397
- h = (h * 31 + sessionId.charCodeAt(i)) | 0;
398
- }
399
- return SESSION_COLORS[Math.abs(h) % SESSION_COLORS.length];
400
- }
401
-
402
- /**
403
- * Record (upsert) a session heartbeat. Called from snapshot() on every material
404
- * change. PRIMARY KEY (pid, session_id) means concurrent pi processes each get
405
- * their own row. Non-fatal on conflict (INSERT ... ON CONFLICT DO UPDATE).
406
- */
407
- export function recordSessionHeartbeat(
408
- pid: number,
409
- sessionId: string,
410
- repoRoot: string,
411
- stateDir: string,
412
- ctxWindow: number,
413
- indexDir: string = getIndexDir(),
414
- ): void {
415
- const db = openIndexStore(indexDir);
416
- const now = Date.now();
417
- db.prepare(
418
- `INSERT INTO session_heartbeats (pid, session_id, repo_root, state_dir, ctx_window, last_seen)
419
- VALUES (@pid, @session_id, @repo_root, @state_dir, @ctx_window, @last_seen)
420
- ON CONFLICT(pid, session_id) DO UPDATE SET
421
- repo_root = excluded.repo_root,
422
- state_dir = excluded.state_dir,
423
- ctx_window = excluded.ctx_window,
424
- last_seen = excluded.last_seen`,
425
- ).run({
426
- pid,
427
- session_id: sessionId,
428
- repo_root: repoRoot,
429
- state_dir: stateDir,
430
- ctx_window: ctxWindow,
431
- last_seen: now,
432
- });
433
- }
434
-
435
- /**
436
- * Append a token sample row + optionally a session_sample line to events.log
437
- * (for SSE real-time push via /api/events). The eventsLogPath is optional —
438
- * callers without an events.log (e.g. tests) can omit it.
439
- */
440
- export function appendTokenSample(
441
- sessionId: string,
442
- repoRoot: string,
443
- tokens: number,
444
- percent: number,
445
- ctxWindow: number,
446
- eventsLogPath: string | null,
447
- indexDir: string = getIndexDir(),
448
- ): void {
449
- const db = openIndexStore(indexDir);
450
- const now = Date.now();
451
- db.prepare(
452
- `INSERT INTO token_samples (session_id, repo_root, tokens, percent, ctx_window, ts)
453
- VALUES (@session_id, @repo_root, @tokens, @percent, @ctx_window, @ts)`,
454
- ).run({
455
- session_id: sessionId,
456
- repo_root: repoRoot,
457
- tokens,
458
- percent,
459
- ctx_window: ctxWindow,
460
- ts: now,
461
- });
462
- // Step 5: also append a session_sample JSON line to events.log so the
463
- // existing /api/events SSE tail streams it for free (real-time chart push).
464
- // Mirrors the DashboardEmitter events.log append pattern in
465
- // extensions/mega-dashboard.ts:{ event(type, data) }: a JSON object with
466
- // `ts` (ISO 8601 string), `type`, and the event-specific payload. The ISO
467
- // timestamp matches the shape of every other SSE variant (SseSessionSample
468
- // contract; every SSE variant's `ts` is a string); a numeric ms `ts` would
469
- // violate the contract union's "every SSE variant has a ts field of type
470
- // string" invariant and break DashboardEmitter consumers.
471
- if (eventsLogPath) {
472
- try {
473
- const dir = eventsLogPath.includes("/") ? eventsLogPath.slice(0, eventsLogPath.lastIndexOf("/")) : ".";
474
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
475
- appendFileSync(
476
- eventsLogPath,
477
- JSON.stringify({
478
- ts: new Date(now).toISOString(),
479
- type: "session_sample",
480
- sessionId,
481
- tokens,
482
- percent,
483
- }) + "\n",
484
- );
485
- } catch {
486
- /* non-fatal: SSE push is best-effort */
487
- }
488
- }
489
- }
490
-
491
- /**
492
- * Prune stale session heartbeats (sessions not seen within maxAgeMs).
493
- * Default 30-min retention. Called by /api/sessions.
494
- */
495
- export function pruneStaleSessions(
496
- maxAgeMs: number = 1_800_000,
497
- indexDir: string = getIndexDir(),
498
- ): number {
499
- const db = openIndexStore(indexDir);
500
- const cutoff = Date.now() - maxAgeMs;
501
- const result = db.prepare(
502
- "DELETE FROM session_heartbeats WHERE last_seen < @cutoff",
503
- ).run({ cutoff });
504
- return Number(result.changes);
505
- }
506
-
507
- /**
508
- * Prune old token samples older than maxAgeMs. Default 30-min retention.
509
- * Called by /api/sessions/timeseries.
510
- */
511
- export function pruneTokenSamples(
512
- maxAgeMs: number = 1_800_000,
513
- indexDir: string = getIndexDir(),
514
- ): number {
515
- const db = openIndexStore(indexDir);
516
- const cutoff = Date.now() - maxAgeMs;
517
- const result = db.prepare(
518
- "DELETE FROM token_samples WHERE ts < @cutoff",
519
- ).run({ cutoff });
520
- return Number(result.changes);
521
- }
522
-
523
- /**
524
- * Read all active sessions with their latest token sample (if any).
525
- * JOINs session_heartbeats with the latest token_samples per session_id
526
- * via a correlated subquery. Returns rows sorted by last_seen descending.
527
- */
528
- export function readActiveSessions(
529
- indexDir: string = getIndexDir(),
530
- ): ActiveSessionRow[] {
531
- const db = openIndexStore(indexDir);
532
- const rows = db.prepare(
533
- `SELECT h.pid, h.session_id, h.repo_root, h.state_dir, h.ctx_window, h.last_seen,
534
- s.tokens, s.percent
535
- FROM session_heartbeats h
536
- LEFT JOIN token_samples s ON s.id = (
537
- SELECT id FROM token_samples t
538
- WHERE t.session_id = h.session_id
539
- ORDER BY t.ts DESC LIMIT 1
540
- )
541
- ORDER BY h.last_seen DESC`,
542
- ).all() as Array<{
543
- pid: number;
544
- session_id: string;
545
- repo_root: string | null;
546
- state_dir: string | null;
547
- ctx_window: number;
548
- last_seen: number;
549
- tokens: number | null;
550
- percent: number | null;
551
- }>;
552
- return rows.map((r) => ({
553
- pid: r.pid,
554
- sessionId: r.session_id,
555
- repoRoot: r.repo_root,
556
- stateDir: r.state_dir,
557
- ctxWindow: r.ctx_window ?? 0,
558
- lastSeen: r.last_seen,
559
- tokens: r.tokens,
560
- percent: r.percent,
561
- }));
562
- }
563
-
564
- /**
565
- * Read token samples since sinceMs, returning a recharts-ready stacked shape:
566
- * per-session `SessionSeries` (with stable color) + a `totals` array
567
- * [{ts, tokens}] (sum of all sessions at each timestamp).
568
- */
569
- export function readSessionTimeseries(
570
- sinceMs: number,
571
- indexDir: string = getIndexDir(),
572
- ): SessionTimeseriesResult {
573
- const db = openIndexStore(indexDir);
574
- const rows = db.prepare(
575
- `SELECT session_id, tokens, percent, ts FROM token_samples WHERE ts >= @since ORDER BY ts ASC`,
576
- ).all({ since: sinceMs }) as Array<{
577
- session_id: string;
578
- tokens: number;
579
- percent: number;
580
- ts: number;
581
- }>;
582
- // Group by session_id → series; + accumulate totals per timestamp.
583
- const seriesMap = new Map<string, TokenSamplePoint[]>();
584
- const totalsMap = new Map<number, number>();
585
- for (const r of rows) {
586
- let pts = seriesMap.get(r.session_id);
587
- if (!pts) {
588
- pts = [];
589
- seriesMap.set(r.session_id, pts);
590
- }
591
- pts.push({ ts: r.ts, tokens: r.tokens, percent: r.percent });
592
- totalsMap.set(r.ts, (totalsMap.get(r.ts) ?? 0) + r.tokens);
593
- }
594
- const series: SessionSeries[] = [];
595
- for (const [sessionId, data] of seriesMap) {
596
- const label = sessionId.length > 12 ? sessionId.slice(0, 12) : sessionId;
597
- series.push({ sessionId, label, color: sessionColor(sessionId), data });
598
- }
599
- // Sort series by first-timestamp for stable legend order.
600
- series.sort((a, b) => (a.data[0]?.ts ?? 0) - (b.data[0]?.ts ?? 0));
601
- const totals = Array.from(totalsMap.entries())
602
- .sort((a, b) => a[0] - b[0])
603
- .map(([ts, tokens]) => ({ ts, tokens }));
604
- return { series, totals };
605
- }
606
-
607
- /**
608
- * Clear a session's heartbeat row (e.g. on clean shutdown / session reset).
609
- * Non-fatal: no-op if the row doesn't exist.
610
- */
611
- export function clearSessionHeartbeat(
612
- pid: number,
613
- sessionId: string,
614
- indexDir: string = getIndexDir(),
615
- ): void {
616
- const db = openIndexStore(indexDir);
617
- db.prepare(
618
- "DELETE FROM session_heartbeats WHERE pid = @pid AND session_id = @session_id",
619
- ).run({ pid, session_id: sessionId });
620
- }