opencode-sesh 2.1.6 → 2.1.7

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/AGENTS.md CHANGED
@@ -197,7 +197,16 @@ when opencode exits, the picker reopens with the query preserved.
197
197
  - **Picker search:** pages the global session list (`SESSION_PAGE_LIMIT`, capped
198
198
  at `SESSION_MAX` and reported when truncated) rather than a fixed-size
199
199
  window, then indexes transcript text for **every** session in batches
200
- (`TRANSCRIPT_BATCH`) with bounded remote concurrency (`REMOTE_CONCURRENCY`).
200
+ (`TRANSCRIPT_BATCH`, also capped by `TRANSCRIPT_BATCH_BYTES` of message
201
+ payload; a single larger session forms its own batch) with bounded remote
202
+ concurrency (`REMOTE_CONCURRENCY`). Transcript text is cached in memory for
203
+ the plugin lifetime (never on disk, cleared on dispose); a worker signature
204
+ query (session timestamp, message count, latest message timestamp, payload
205
+ size; chunked by `SIGNATURE_CHUNK`) lets reopened pickers reuse unchanged
206
+ sessions, extract only new or changed ones, and prune vanished ones. Sessions
207
+ indexed through the remote path are not cached. If the signature query fails
208
+ (`octet_length` needs SQLite ≥ 3.43; macOS Bun uses the system SQLite),
209
+ extraction still runs locally, batched by count only, and caches nothing.
201
210
  Local extraction is one SQL statement per batch in the DB worker
202
211
  (`transcriptTextQuery`, JSON-guarded) that returns only `{sid, text}` rows,
203
212
  so tool payloads never reach the UI thread; if the worker rejects, sessions
package/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.1.7] - 2026-11-02
9
+
10
+ ### Changed
11
+
12
+ - **tui:** Reuse unchanged transcript text and bound index batches by bytes (#37)
13
+
14
+ ### Fixed
15
+
16
+ - **tui:** Stop the DB worker and sidebar polls on plugin dispose (#36)
17
+
8
18
  ## [2.1.6] - 2026-11-02
9
19
 
10
20
  ### Changed
@@ -548,6 +558,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
548
558
  - Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
549
559
  CI.
550
560
 
561
+ [2.1.7]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.6...v2.1.7
551
562
  [2.1.6]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.5...v2.1.6
552
563
  [2.1.5]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.4...v2.1.5
553
564
  [2.1.4]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.3...v2.1.4
package/dist/tui.js CHANGED
@@ -605,6 +605,8 @@ async function fetchEntries(api, active = () => true) {
605
605
  }
606
606
  const SESSION_ID_PATTERN = /^ses_[A-Za-z0-9]+$/;
607
607
  const TRANSCRIPT_BATCH = 64;
608
+ const TRANSCRIPT_BATCH_BYTES = 8 * 1024 * 1024;
609
+ const SIGNATURE_CHUNK = 500;
608
610
  const REMOTE_CONCURRENCY = 8;
609
611
  const nextTick = () => new Promise(resolve => setTimeout(resolve, 0));
610
612
  let resolvedDbPath;
@@ -663,9 +665,11 @@ function createDbWorker() {
663
665
  // Read-only queries against the local store, served by one lazily started
664
666
  // worker. Any failure (no worker, no bun:sqlite, query error, timeout) rejects;
665
667
  // a timeout or worker error discards the worker so the next call starts afresh.
668
+ // `close` is final: later calls reject and never start a worker.
666
669
  function createDbClient(createWorker = createDbWorker, timeoutMs = DB_QUERY_TIMEOUT_MS) {
667
670
  let worker;
668
671
  let nextId = 0;
672
+ let closed = false;
669
673
  const pending = new Map();
670
674
  const reset = error => {
671
675
  const dead = worker;
@@ -702,6 +706,10 @@ function createDbClient(createWorker = createDbWorker, timeoutMs = DB_QUERY_TIME
702
706
  return created;
703
707
  };
704
708
  const run = (sql, params = []) => new Promise((resolve, reject) => {
709
+ if (closed) {
710
+ reject(new Error("local store closed"));
711
+ return;
712
+ }
705
713
  try {
706
714
  worker ??= start();
707
715
  } catch (error) {
@@ -729,10 +737,16 @@ function createDbClient(createWorker = createDbWorker, timeoutMs = DB_QUERY_TIME
729
737
  });
730
738
  return {
731
739
  run,
732
- close: () => reset(new Error("local store closed"))
740
+ close: () => {
741
+ closed = true;
742
+ reset(new Error("local store closed"));
743
+ }
733
744
  };
734
745
  }
735
- const dbClient = createDbClient();
746
+
747
+ // Each plugin setup installs its own client and closes only that one on
748
+ // dispose: a reload reuses this module instance.
749
+ let dbClient = createDbClient();
736
750
  function createSessionPoll(api, refreshPins) {
737
751
  // Listener -> whether it reads `snapshot.waiting`. The needs-input query
738
752
  // costs worker CPU on every poll, so it only runs while such a listener exists.
@@ -948,11 +962,33 @@ function transcriptTextQuery(count) {
948
962
  SELECT sid, text FROM parts WHERE text IS NOT NULL AND text <> '' ORDER BY sid, seq, rid, part`;
949
963
  }
950
964
 
965
+ // Transcript text per session for the plugin lifetime; memory only, so
966
+ // conversation text is never written anywhere. `sig` identifies the extracted
967
+ // state of the session (see transcriptSignatureQuery).
968
+ const transcriptCache = new Map();
969
+ const clearTranscriptCache = () => transcriptCache.clear();
970
+
971
+ // Cheap change detector mirroring the fzf extraction cache: message count and
972
+ // latest message timestamp, plus the payload size that bounds extraction
973
+ // batches. octet_length() reads the stored size, never the payload; length()
974
+ // would read every payload to count characters.
975
+ function transcriptSignatureQuery(count) {
976
+ const placeholders = Array.from({
977
+ length: count
978
+ }, () => "?").join(",");
979
+ return `SELECT session_id AS sid, COUNT(*) AS n, MAX(time_updated) AS m, SUM(octet_length(data)) AS bytes
980
+ FROM session_message
981
+ WHERE session_id IN (${placeholders}) AND type IN ('user', 'assistant')
982
+ GROUP BY session_id`;
983
+ }
984
+
951
985
  // Index titles/directories immediately, then add transcript text progressively
952
986
  // in batches so the render thread can breathe. Covers every entry, not a fixed
953
987
  // newest window; `onProgress` lets the picker show coverage. Local extraction
954
- // runs through `run` (the worker client) and returns text only.
955
- async function buildSearchIndex(api, entries, onProgress, active = () => true, run = dbClient.run) {
988
+ // runs through `run` (the worker client) and returns text only; sessions whose
989
+ // signature is unchanged reuse their cached text, and extraction batches are
990
+ // bounded by session count and payload bytes.
991
+ async function buildSearchIndex(api, entries, onProgress, active = () => true, run = dbClient.run, cache = transcriptCache) {
956
992
  const index = new Map();
957
993
  for (const entry of entries) {
958
994
  index.set(entry.id, `${entry.title} ${entry.dir}`);
@@ -971,9 +1007,54 @@ async function buildSearchIndex(api, entries, onProgress, active = () => true, r
971
1007
  report(false);
972
1008
  if (!active()) return index;
973
1009
  try {
974
- for (let i = 0; i < ids.length; i += TRANSCRIPT_BATCH) {
1010
+ // `entries` must be the full session list: sessions missing from it are pruned.
1011
+ const idSet = new Set(ids);
1012
+ for (const sid of cache.keys()) if (!idSet.has(sid)) cache.delete(sid);
1013
+ // Undefined when the signature query fails (octet_length needs SQLite
1014
+ // >= 3.43, and macOS Bun uses the system SQLite): extract everything
1015
+ // locally, batched by count only, and cache nothing.
1016
+ let signatures = new Map();
1017
+ try {
1018
+ for (let i = 0; i < ids.length; i += SIGNATURE_CHUNK) {
1019
+ const chunk = ids.slice(i, i + SIGNATURE_CHUNK);
1020
+ const rows = await run(transcriptSignatureQuery(chunk.length), chunk);
1021
+ if (!active()) return index;
1022
+ for (const row of rows) signatures.set(row.sid, {
1023
+ sig: `${row.n}:${row.m}:${row.bytes}`,
1024
+ bytes: row.bytes ?? 0
1025
+ });
1026
+ }
1027
+ } catch {
1028
+ if (!active()) return index;
1029
+ signatures = undefined;
1030
+ }
1031
+ const sigs = signatures && new Map(entries.map(entry => [entry.id, `${entry.updated}:${signatures.get(entry.id)?.sig ?? "0:0:0"}`]));
1032
+ const stale = [];
1033
+ for (const id of ids) {
1034
+ const hit = cache.get(id);
1035
+ if (hit && sigs && hit.sig === sigs.get(id)) {
1036
+ if (hit.text) index.set(id, `${index.get(id) ?? ""} ${hit.text}`.trim());
1037
+ remaining.delete(id);
1038
+ indexed += 1;
1039
+ } else stale.push(id);
1040
+ }
1041
+ if (indexed > 0) report(false);
1042
+ const batches = [];
1043
+ let current = [];
1044
+ let currentBytes = 0;
1045
+ for (const id of stale) {
1046
+ const bytes = signatures?.get(id)?.bytes ?? 0;
1047
+ if (current.length > 0 && (current.length >= TRANSCRIPT_BATCH || currentBytes + bytes > TRANSCRIPT_BATCH_BYTES)) {
1048
+ batches.push(current);
1049
+ current = [];
1050
+ currentBytes = 0;
1051
+ }
1052
+ current.push(id);
1053
+ currentBytes += bytes;
1054
+ }
1055
+ if (current.length > 0) batches.push(current);
1056
+ for (const batch of batches) {
975
1057
  if (!active()) break;
976
- const batch = ids.slice(i, i + TRANSCRIPT_BATCH);
977
1058
  const rows = await run(transcriptTextQuery(batch.length), batch);
978
1059
  if (!active()) break;
979
1060
  const texts = new Map();
@@ -981,8 +1062,15 @@ async function buildSearchIndex(api, entries, onProgress, active = () => true, r
981
1062
  const list = texts.get(row.sid);
982
1063
  if (list) list.push(row.text);else texts.set(row.sid, [row.text]);
983
1064
  }
984
- for (const [sid, list] of texts) index.set(sid, [index.get(sid) ?? "", ...list].join(" ").trim());
985
- for (const id of batch) remaining.delete(id);
1065
+ for (const id of batch) {
1066
+ const list = texts.get(id) ?? [];
1067
+ if (sigs) cache.set(id, {
1068
+ sig: sigs.get(id),
1069
+ text: list.join(" ").trim()
1070
+ });
1071
+ if (list.length > 0) index.set(id, [index.get(id) ?? "", ...list].join(" ").trim());
1072
+ remaining.delete(id);
1073
+ }
986
1074
  indexed += batch.length;
987
1075
  report(false);
988
1076
  await nextTick();
@@ -4306,7 +4394,11 @@ function createApi(ctx) {
4306
4394
  const [dialogOpen, setDialogOpen] = createSignal(false);
4307
4395
  let sourceTheme;
4308
4396
  let currentTheme;
4397
+ const slotDisposers = [];
4309
4398
  return {
4399
+ dispose() {
4400
+ for (const dispose of slotDisposers.splice(0)) dispose();
4401
+ },
4310
4402
  theme: {
4311
4403
  get current() {
4312
4404
  if (sourceTheme !== ctx.theme) {
@@ -4441,20 +4533,20 @@ function createApi(ctx) {
4441
4533
  slots: {
4442
4534
  register(cfg) {
4443
4535
  if (cfg.slots.sidebar_content && ctx.options?.showSidebar !== false) {
4444
- ctx.ui.slot({
4536
+ slotDisposers.push(ctx.ui.slot({
4445
4537
  ...(ctx.options?.sidebarPlacement === "prepend" ? {
4446
4538
  prepend: "sidebar.content"
4447
4539
  } : {
4448
4540
  append: "sidebar.content"
4449
4541
  }),
4450
4542
  render: cfg.slots.sidebar_content
4451
- });
4543
+ }));
4452
4544
  }
4453
4545
  if (cfg.slots.home_bottom) {
4454
- ctx.ui.slot({
4546
+ slotDisposers.push(ctx.ui.slot({
4455
4547
  prepend: "home.footer",
4456
4548
  render: cfg.slots.home_bottom
4457
- });
4549
+ }));
4458
4550
  }
4459
4551
  }
4460
4552
  },
@@ -4568,6 +4660,7 @@ function createApi(ctx) {
4568
4660
  export default Plugin.define({
4569
4661
  id: "sesh-panel",
4570
4662
  async setup(ctx) {
4663
+ const client = dbClient = createDbClient();
4571
4664
  const api = createApi(ctx);
4572
4665
  let initialized = false;
4573
4666
  const disposeSlot = ctx.ui.slot({
@@ -4584,8 +4677,10 @@ export default Plugin.define({
4584
4677
  return () => {
4585
4678
  if (api.ui.dialog.open) api.ui.dialog.clear();
4586
4679
  disposeSlot();
4680
+ api.dispose();
4587
4681
  removeRootMouse?.();
4588
- dbClient.close();
4682
+ clearTranscriptCache();
4683
+ client.close();
4589
4684
  };
4590
4685
  }
4591
4686
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "description": "OpenCode session browser: search, preview, and resume every session across every project from the terminal or TUI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -503,6 +503,8 @@ async function fetchEntries(api: TuiPluginApi, active = () => true): Promise<Ent
503
503
 
504
504
  const SESSION_ID_PATTERN = /^ses_[A-Za-z0-9]+$/
505
505
  const TRANSCRIPT_BATCH = 64
506
+ const TRANSCRIPT_BATCH_BYTES = 8 * 1024 * 1024
507
+ const SIGNATURE_CHUNK = 500
506
508
  const REMOTE_CONCURRENCY = 8
507
509
 
508
510
  type IndexProgress = { indexed: number; total: number; complete: boolean }
@@ -565,10 +567,12 @@ function createDbWorker(): any {
565
567
  // Read-only queries against the local store, served by one lazily started
566
568
  // worker. Any failure (no worker, no bun:sqlite, query error, timeout) rejects;
567
569
  // a timeout or worker error discards the worker so the next call starts afresh.
570
+ // `close` is final: later calls reject and never start a worker.
568
571
  function createDbClient(createWorker: () => any = createDbWorker, timeoutMs = DB_QUERY_TIMEOUT_MS) {
569
572
  type Pending = { resolve: (rows: unknown[]) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }
570
573
  let worker: any
571
574
  let nextId = 0
575
+ let closed = false
572
576
  const pending = new Map<number, Pending>()
573
577
  const reset = (error: Error) => {
574
578
  const dead = worker
@@ -601,6 +605,10 @@ function createDbClient(createWorker: () => any = createDbWorker, timeoutMs = DB
601
605
  }
602
606
  const run: DbRun = (sql, params = []) =>
603
607
  new Promise((resolve, reject) => {
608
+ if (closed) {
609
+ reject(new Error("local store closed"))
610
+ return
611
+ }
604
612
  try {
605
613
  worker ??= start()
606
614
  } catch (error) {
@@ -617,10 +625,12 @@ function createDbClient(createWorker: () => any = createDbWorker, timeoutMs = DB
617
625
  reset(error as Error)
618
626
  }
619
627
  })
620
- return { run, close: () => reset(new Error("local store closed")) }
628
+ return { run, close: () => { closed = true; reset(new Error("local store closed")) } }
621
629
  }
622
630
 
623
- const dbClient = createDbClient()
631
+ // Each plugin setup installs its own client and closes only that one on
632
+ // dispose: a reload reuses this module instance.
633
+ let dbClient = createDbClient()
624
634
 
625
635
  type SessionSnapshot = { entries: Entry[]; waiting: Map<string, string>; failed: boolean }
626
636
 
@@ -830,16 +840,37 @@ function transcriptTextQuery(count: number): string {
830
840
  SELECT sid, text FROM parts WHERE text IS NOT NULL AND text <> '' ORDER BY sid, seq, rid, part`
831
841
  }
832
842
 
843
+ // Transcript text per session for the plugin lifetime; memory only, so
844
+ // conversation text is never written anywhere. `sig` identifies the extracted
845
+ // state of the session (see transcriptSignatureQuery).
846
+ const transcriptCache = new Map<string, { sig: string; text: string }>()
847
+ const clearTranscriptCache = () => transcriptCache.clear()
848
+
849
+ // Cheap change detector mirroring the fzf extraction cache: message count and
850
+ // latest message timestamp, plus the payload size that bounds extraction
851
+ // batches. octet_length() reads the stored size, never the payload; length()
852
+ // would read every payload to count characters.
853
+ function transcriptSignatureQuery(count: number): string {
854
+ const placeholders = Array.from({ length: count }, () => "?").join(",")
855
+ return `SELECT session_id AS sid, COUNT(*) AS n, MAX(time_updated) AS m, SUM(octet_length(data)) AS bytes
856
+ FROM session_message
857
+ WHERE session_id IN (${placeholders}) AND type IN ('user', 'assistant')
858
+ GROUP BY session_id`
859
+ }
860
+
833
861
  // Index titles/directories immediately, then add transcript text progressively
834
862
  // in batches so the render thread can breathe. Covers every entry, not a fixed
835
863
  // newest window; `onProgress` lets the picker show coverage. Local extraction
836
- // runs through `run` (the worker client) and returns text only.
864
+ // runs through `run` (the worker client) and returns text only; sessions whose
865
+ // signature is unchanged reuse their cached text, and extraction batches are
866
+ // bounded by session count and payload bytes.
837
867
  async function buildSearchIndex(
838
868
  api: TuiPluginApi,
839
869
  entries: Entry[],
840
870
  onProgress?: (progress: IndexProgress, index: Map<string, string>) => void,
841
871
  active = () => true,
842
872
  run: DbRun = dbClient.run,
873
+ cache = transcriptCache,
843
874
  ): Promise<Map<string, string>> {
844
875
  const index = new Map<string, string>()
845
876
  for (const entry of entries) {
@@ -854,9 +885,53 @@ async function buildSearchIndex(
854
885
 
855
886
  if (!active()) return index
856
887
  try {
857
- for (let i = 0; i < ids.length; i += TRANSCRIPT_BATCH) {
888
+ // `entries` must be the full session list: sessions missing from it are pruned.
889
+ const idSet = new Set(ids)
890
+ for (const sid of cache.keys()) if (!idSet.has(sid)) cache.delete(sid)
891
+ // Undefined when the signature query fails (octet_length needs SQLite
892
+ // >= 3.43, and macOS Bun uses the system SQLite): extract everything
893
+ // locally, batched by count only, and cache nothing.
894
+ let signatures: Map<string, { sig: string; bytes: number }> | undefined = new Map()
895
+ try {
896
+ for (let i = 0; i < ids.length; i += SIGNATURE_CHUNK) {
897
+ const chunk = ids.slice(i, i + SIGNATURE_CHUNK)
898
+ const rows = (await run(transcriptSignatureQuery(chunk.length), chunk)) as { sid: string; n: number; m: number; bytes: number }[]
899
+ if (!active()) return index
900
+ for (const row of rows) signatures.set(row.sid, { sig: `${row.n}:${row.m}:${row.bytes}`, bytes: row.bytes ?? 0 })
901
+ }
902
+ } catch {
903
+ if (!active()) return index
904
+ signatures = undefined
905
+ }
906
+ const sigs = signatures && new Map(entries.map((entry) => [entry.id, `${entry.updated}:${signatures!.get(entry.id)?.sig ?? "0:0:0"}`]))
907
+ const stale: string[] = []
908
+ for (const id of ids) {
909
+ const hit = cache.get(id)
910
+ if (hit && sigs && hit.sig === sigs.get(id)) {
911
+ if (hit.text) index.set(id, `${index.get(id) ?? ""} ${hit.text}`.trim())
912
+ remaining.delete(id)
913
+ indexed += 1
914
+ } else stale.push(id)
915
+ }
916
+ if (indexed > 0) report(false)
917
+
918
+ const batches: string[][] = []
919
+ let current: string[] = []
920
+ let currentBytes = 0
921
+ for (const id of stale) {
922
+ const bytes = signatures?.get(id)?.bytes ?? 0
923
+ if (current.length > 0 && (current.length >= TRANSCRIPT_BATCH || currentBytes + bytes > TRANSCRIPT_BATCH_BYTES)) {
924
+ batches.push(current)
925
+ current = []
926
+ currentBytes = 0
927
+ }
928
+ current.push(id)
929
+ currentBytes += bytes
930
+ }
931
+ if (current.length > 0) batches.push(current)
932
+
933
+ for (const batch of batches) {
858
934
  if (!active()) break
859
- const batch = ids.slice(i, i + TRANSCRIPT_BATCH)
860
935
  const rows = (await run(transcriptTextQuery(batch.length), batch)) as { sid: string; text: string }[]
861
936
  if (!active()) break
862
937
  const texts = new Map<string, string[]>()
@@ -865,8 +940,12 @@ async function buildSearchIndex(
865
940
  if (list) list.push(row.text)
866
941
  else texts.set(row.sid, [row.text])
867
942
  }
868
- for (const [sid, list] of texts) index.set(sid, [index.get(sid) ?? "", ...list].join(" ").trim())
869
- for (const id of batch) remaining.delete(id)
943
+ for (const id of batch) {
944
+ const list = texts.get(id) ?? []
945
+ if (sigs) cache.set(id, { sig: sigs.get(id)!, text: list.join(" ").trim() })
946
+ if (list.length > 0) index.set(id, [index.get(id) ?? "", ...list].join(" ").trim())
947
+ remaining.delete(id)
948
+ }
870
949
  indexed += batch.length
871
950
  report(false)
872
951
  await nextTick()
@@ -3352,7 +3431,11 @@ function createApi(ctx: Plugin.Context) {
3352
3431
  const [dialogOpen, setDialogOpen] = createSignal(false)
3353
3432
  let sourceTheme: Plugin.Context["theme"] | undefined
3354
3433
  let currentTheme: ReturnType<typeof adaptTheme>
3434
+ const slotDisposers: Array<() => void> = []
3355
3435
  return {
3436
+ dispose() {
3437
+ for (const dispose of slotDisposers.splice(0)) dispose()
3438
+ },
3356
3439
  theme: {
3357
3440
  get current() {
3358
3441
  if (sourceTheme !== ctx.theme) {
@@ -3476,18 +3559,18 @@ function createApi(ctx: Plugin.Context) {
3476
3559
  slots: {
3477
3560
  register(cfg: { order?: number; slots: Record<string, () => any> }) {
3478
3561
  if (cfg.slots.sidebar_content && ctx.options?.showSidebar !== false) {
3479
- ctx.ui.slot({
3562
+ slotDisposers.push(ctx.ui.slot({
3480
3563
  ...(ctx.options?.sidebarPlacement === "prepend"
3481
3564
  ? { prepend: "sidebar.content" as const }
3482
3565
  : { append: "sidebar.content" as const }),
3483
3566
  render: cfg.slots.sidebar_content as any,
3484
- })
3567
+ }))
3485
3568
  }
3486
3569
  if (cfg.slots.home_bottom) {
3487
- ctx.ui.slot({
3570
+ slotDisposers.push(ctx.ui.slot({
3488
3571
  prepend: "home.footer",
3489
3572
  render: cfg.slots.home_bottom as any,
3490
- })
3573
+ }))
3491
3574
  }
3492
3575
  },
3493
3576
  },
@@ -3581,6 +3664,7 @@ function createApi(ctx: Plugin.Context) {
3581
3664
  export default Plugin.define({
3582
3665
  id: "sesh-panel",
3583
3666
  async setup(ctx) {
3667
+ const client = dbClient = createDbClient()
3584
3668
  const api = createApi(ctx)
3585
3669
  let initialized = false
3586
3670
  const disposeSlot = ctx.ui.slot({
@@ -3597,8 +3681,10 @@ export default Plugin.define({
3597
3681
  return () => {
3598
3682
  if (api.ui.dialog.open) api.ui.dialog.clear()
3599
3683
  disposeSlot()
3684
+ api.dispose()
3600
3685
  removeRootMouse?.()
3601
- dbClient.close()
3686
+ clearTranscriptCache()
3687
+ client.close()
3602
3688
  }
3603
3689
  },
3604
3690
  })