botholomew 0.7.8 → 0.7.9

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/src/db/tasks.ts CHANGED
@@ -109,6 +109,7 @@ export async function listTasks(
109
109
  status?: Task["status"];
110
110
  priority?: Task["priority"];
111
111
  limit?: number;
112
+ offset?: number;
112
113
  },
113
114
  ): Promise<Task[]> {
114
115
  const { where, params } = buildWhereClause([
@@ -116,13 +117,12 @@ export async function listTasks(
116
117
  ["priority", filters?.priority],
117
118
  ]);
118
119
  const limit = filters?.limit ? `LIMIT ${sanitizeInt(filters.limit)}` : "";
120
+ const offset = filters?.offset ? `OFFSET ${sanitizeInt(filters.offset)}` : "";
119
121
 
120
122
  const rows = await db.queryAll<TaskRow>(
121
123
  `SELECT * FROM tasks ${where}
122
- ORDER BY
123
- CASE priority WHEN 'high' THEN 0 WHEN 'medium' THEN 1 WHEN 'low' THEN 2 END,
124
- created_at ASC
125
- ${limit}`,
124
+ ORDER BY created_at DESC, id DESC
125
+ ${limit} ${offset}`,
126
126
  ...params,
127
127
  );
128
128
  return rows.map(rowToTask);
package/src/db/threads.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { DbConnection } from "./connection.ts";
2
- import { buildWhereClause } from "./query.ts";
2
+ import { buildWhereClause, sanitizeInt } from "./query.ts";
3
3
  import { uuidv7 } from "./uuid.ts";
4
4
 
5
5
  export interface Thread {
@@ -256,18 +256,20 @@ export async function listThreads(
256
256
  type?: Thread["type"];
257
257
  taskId?: string;
258
258
  limit?: number;
259
+ offset?: number;
259
260
  },
260
261
  ): Promise<Thread[]> {
261
262
  const { where, params } = buildWhereClause([
262
263
  ["type", filters?.type],
263
264
  ["task_id", filters?.taskId],
264
265
  ]);
265
- const limit = filters?.limit ? `LIMIT ${filters.limit}` : "";
266
+ const limit = filters?.limit ? `LIMIT ${sanitizeInt(filters.limit)}` : "";
267
+ const offset = filters?.offset ? `OFFSET ${sanitizeInt(filters.offset)}` : "";
266
268
 
267
269
  const rows = await db.queryAll<ThreadRow>(
268
270
  `SELECT * FROM threads ${where}
269
- ORDER BY started_at DESC
270
- ${limit}`,
271
+ ORDER BY started_at DESC, id DESC
272
+ ${limit} ${offset}`,
271
273
  ...params,
272
274
  );
273
275
  return rows.map(rowToThread);
package/src/tools/tool.ts CHANGED
@@ -5,7 +5,15 @@ import type { BotholomewConfig } from "../config/schemas.ts";
5
5
  import type { DbConnection } from "../db/connection.ts";
6
6
 
7
7
  export interface ToolContext {
8
+ /**
9
+ * Short-lived DB connection scoped to this tool call. Safe for single-query
10
+ * tools. Do NOT hold it across slow work (network, embedding, long loops) —
11
+ * the instance-level file lock stays held until every connection closes.
12
+ * For long-running tools, use `dbPath` with `withDb` per logical operation.
13
+ */
8
14
  conn: DbConnection;
15
+ /** Path to the DuckDB file. Use with `withDb` for long-running tools. */
16
+ dbPath: string;
9
17
  projectDir: string;
10
18
  config: Required<BotholomewConfig>;
11
19
  mcpxClient: McpxClient | null;
package/src/tui/App.tsx CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  startChatSession,
8
8
  } from "../chat/session.ts";
9
9
  import { MAX_INLINE_CHARS, PAGE_SIZE_CHARS } from "../daemon/large-results.ts";
10
+ import { withDb } from "../db/connection.ts";
10
11
  import type { Interaction } from "../db/threads.ts";
11
12
  import { getThread } from "../db/threads.ts";
12
13
  import {
@@ -163,7 +164,9 @@ export function App({
163
164
  sessionRef.current = session;
164
165
 
165
166
  if (session.messages.length > 0) {
166
- const threadData = await getThread(session.conn, session.threadId);
167
+ const threadData = await withDb(session.dbPath, (conn) =>
168
+ getThread(conn, session.threadId),
169
+ );
167
170
  if (threadData) {
168
171
  setMessages(
169
172
  restoreMessagesFromInteractions(threadData.interactions),
@@ -409,7 +412,9 @@ export function App({
409
412
  const refreshTitle = async () => {
410
413
  const session = sessionRef.current;
411
414
  if (!session) return;
412
- const result = await getThread(session.conn, session.threadId);
415
+ const result = await withDb(session.dbPath, (conn) =>
416
+ getThread(conn, session.threadId),
417
+ );
413
418
  if (mounted && result?.thread.title) {
414
419
  setChatTitle(result.thread.title);
415
420
  }
@@ -547,18 +552,18 @@ export function App({
547
552
  [exit, processQueue, syncQueue],
548
553
  );
549
554
 
550
- const sessionConn = sessionRef.current?.conn;
555
+ const sessionDbPath = sessionRef.current?.dbPath;
551
556
  const inputBarHeader = useMemo(
552
557
  () =>
553
- sessionConn ? (
558
+ sessionDbPath ? (
554
559
  <StatusBar
555
560
  projectDir={projectDir}
556
- conn={sessionConn}
561
+ dbPath={sessionDbPath}
557
562
  chatTitle={chatTitle}
558
563
  onDaemonStatusChange={setDaemonRunning}
559
564
  />
560
565
  ) : null,
561
- [projectDir, sessionConn, chatTitle],
566
+ [projectDir, sessionDbPath, chatTitle],
562
567
  );
563
568
 
564
569
  const sessionSkills = ready ? sessionRef.current?.skills : undefined;
@@ -602,7 +607,7 @@ export function App({
602
607
  );
603
608
  }
604
609
 
605
- const conn = sessionRef.current.conn;
610
+ const dbPath = sessionRef.current.dbPath;
606
611
  const threadId = sessionRef.current.threadId;
607
612
 
608
613
  return (
@@ -642,14 +647,14 @@ export function App({
642
647
  flexDirection="column"
643
648
  flexGrow={1}
644
649
  >
645
- <ContextPanel conn={conn} isActive={activeTab === 3} />
650
+ <ContextPanel dbPath={dbPath} isActive={activeTab === 3} />
646
651
  </Box>
647
652
  <Box
648
653
  display={activeTab === 4 ? "flex" : "none"}
649
654
  flexDirection="column"
650
655
  flexGrow={1}
651
656
  >
652
- <TaskPanel conn={conn} isActive={activeTab === 4} />
657
+ <TaskPanel dbPath={dbPath} isActive={activeTab === 4} />
653
658
  </Box>
654
659
  <Box
655
660
  display={activeTab === 5 ? "flex" : "none"}
@@ -657,7 +662,7 @@ export function App({
657
662
  flexGrow={1}
658
663
  >
659
664
  <ThreadPanel
660
- conn={conn}
665
+ dbPath={dbPath}
661
666
  activeThreadId={threadId}
662
667
  isActive={activeTab === 5}
663
668
  />
@@ -667,7 +672,7 @@ export function App({
667
672
  flexDirection="column"
668
673
  flexGrow={1}
669
674
  >
670
- <SchedulePanel conn={conn} isActive={activeTab === 6} />
675
+ <SchedulePanel dbPath={dbPath} isActive={activeTab === 6} />
671
676
  </Box>
672
677
  <Box
673
678
  display={activeTab === 7 ? "flex" : "none"}
@@ -1,6 +1,6 @@
1
1
  import { Box, Text, useInput, useStdout } from "ink";
2
2
  import { memo, useCallback, useEffect, useMemo, useState } from "react";
3
- import type { DbConnection } from "../../db/connection.ts";
3
+ import { withDb } from "../../db/connection.ts";
4
4
  import {
5
5
  type ContextItem,
6
6
  deleteContextItem,
@@ -11,7 +11,7 @@ import {
11
11
  } from "../../db/context.ts";
12
12
 
13
13
  interface ContextPanelProps {
14
- conn: DbConnection;
14
+ dbPath: string;
15
15
  isActive: boolean;
16
16
  }
17
17
 
@@ -32,7 +32,7 @@ type Entry = DirEntry | FileEntry;
32
32
  const CHROME_LINES = 8;
33
33
 
34
34
  export const ContextPanel = memo(function ContextPanel({
35
- conn,
35
+ dbPath,
36
36
  isActive,
37
37
  }: ContextPanelProps) {
38
38
  const { stdout } = useStdout();
@@ -64,10 +64,10 @@ export const ContextPanel = memo(function ContextPanel({
64
64
 
65
65
  const loadEntries = useCallback(
66
66
  async (path: string) => {
67
- const dirs = await getDistinctDirectories(conn, path);
68
- const files = await listContextItemsByPrefix(conn, path, {
69
- recursive: false,
70
- });
67
+ const [dirs, files] = await withDb(dbPath, async (conn) => [
68
+ await getDistinctDirectories(conn, path),
69
+ await listContextItemsByPrefix(conn, path, { recursive: false }),
70
+ ]);
71
71
 
72
72
  const dirEntries: DirEntry[] = dirs.map((d) => ({
73
73
  type: "directory",
@@ -84,7 +84,7 @@ export const ContextPanel = memo(function ContextPanel({
84
84
  setScrollOffset(0);
85
85
  setPreview(null);
86
86
  },
87
- [conn],
87
+ [dbPath],
88
88
  );
89
89
 
90
90
  useEffect(() => {
@@ -99,13 +99,15 @@ export const ContextPanel = memo(function ContextPanel({
99
99
  setSearchResults(null);
100
100
  return;
101
101
  }
102
- const results = await searchContextByKeyword(conn, query.trim(), 50);
102
+ const results = await withDb(dbPath, (conn) =>
103
+ searchContextByKeyword(conn, query.trim(), 50),
104
+ );
103
105
  setSearchResults(results);
104
106
  setCursor(0);
105
107
  setScrollOffset(0);
106
108
  setPreview(null);
107
109
  },
108
- [conn],
110
+ [dbPath],
109
111
  );
110
112
 
111
113
  // Compute the items list and visible window for the current view
@@ -171,11 +173,13 @@ export const ContextPanel = memo(function ContextPanel({
171
173
  if (input === "y" || input === "d") {
172
174
  const entry = entries[cursor];
173
175
  if (entry) {
174
- if (entry.type === "directory") {
175
- deleteContextItemsByPrefix(conn, entry.path);
176
- } else {
177
- deleteContextItem(conn, entry.item.id);
178
- }
176
+ void withDb(dbPath, async (conn) => {
177
+ if (entry.type === "directory") {
178
+ await deleteContextItemsByPrefix(conn, entry.path);
179
+ } else {
180
+ await deleteContextItem(conn, entry.item.id);
181
+ }
182
+ });
179
183
  setConfirmDelete(false);
180
184
  loadEntries(currentPath);
181
185
  }
@@ -1,6 +1,6 @@
1
1
  import { Box, Text, useInput, useStdout } from "ink";
2
2
  import { memo, useCallback, useEffect, useMemo, useState } from "react";
3
- import type { DbConnection } from "../../db/connection.ts";
3
+ import { withDb } from "../../db/connection.ts";
4
4
  import {
5
5
  deleteSchedule,
6
6
  listSchedules,
@@ -10,7 +10,7 @@ import {
10
10
  import { ansi, theme } from "../theme.ts";
11
11
 
12
12
  interface SchedulePanelProps {
13
- conn: DbConnection;
13
+ dbPath: string;
14
14
  isActive: boolean;
15
15
  }
16
16
 
@@ -108,7 +108,7 @@ function cycleFilter<T>(current: T | null, values: readonly T[]): T | null {
108
108
  }
109
109
 
110
110
  export const SchedulePanel = memo(function SchedulePanel({
111
- conn,
111
+ dbPath,
112
112
  isActive,
113
113
  }: SchedulePanelProps) {
114
114
  const { stdout } = useStdout();
@@ -127,7 +127,9 @@ export const SchedulePanel = memo(function SchedulePanel({
127
127
  const refresh = async () => {
128
128
  const filters: { enabled?: boolean } = {};
129
129
  if (enabledFilter !== null) filters.enabled = enabledFilter;
130
- const result = await listSchedules(conn, filters);
130
+ const result = await withDb(dbPath, (conn) =>
131
+ listSchedules(conn, filters),
132
+ );
131
133
  if (mounted) {
132
134
  setSchedules(result);
133
135
  setSelectedIndex((prev) =>
@@ -142,7 +144,7 @@ export const SchedulePanel = memo(function SchedulePanel({
142
144
  mounted = false;
143
145
  clearInterval(interval);
144
146
  };
145
- }, [conn, enabledFilter, refreshTick]);
147
+ }, [dbPath, enabledFilter, refreshTick]);
146
148
 
147
149
  const selectedSchedule = schedules[selectedIndex];
148
150
 
@@ -182,7 +184,9 @@ export const SchedulePanel = memo(function SchedulePanel({
182
184
  if (confirmDelete) {
183
185
  if (input === "y" || input === "d") {
184
186
  if (selectedSchedule) {
185
- deleteSchedule(conn, selectedSchedule.id).then(() => {
187
+ withDb(dbPath, (conn) =>
188
+ deleteSchedule(conn, selectedSchedule.id),
189
+ ).then(() => {
186
190
  forceRefresh();
187
191
  });
188
192
  }
@@ -242,9 +246,11 @@ export const SchedulePanel = memo(function SchedulePanel({
242
246
  return;
243
247
  }
244
248
  if (input === "e" && selectedSchedule) {
245
- updateSchedule(conn, selectedSchedule.id, {
246
- enabled: !selectedSchedule.enabled,
247
- }).then(() => {
249
+ withDb(dbPath, (conn) =>
250
+ updateSchedule(conn, selectedSchedule.id, {
251
+ enabled: !selectedSchedule.enabled,
252
+ }),
253
+ ).then(() => {
248
254
  forceRefresh();
249
255
  });
250
256
  return;
@@ -1,13 +1,13 @@
1
1
  import { Box, Text } from "ink";
2
2
  import { useEffect, useState } from "react";
3
- import type { DbConnection } from "../../db/connection.ts";
3
+ import { withDb } from "../../db/connection.ts";
4
4
  import { listTasks } from "../../db/tasks.ts";
5
5
  import { getDaemonStatus } from "../../utils/pid.ts";
6
6
  import { LogoChar } from "./Logo.tsx";
7
7
 
8
8
  interface StatusBarProps {
9
9
  projectDir: string;
10
- conn: DbConnection;
10
+ dbPath: string;
11
11
  chatTitle?: string;
12
12
  onDaemonStatusChange?: (running: boolean) => void;
13
13
  }
@@ -20,7 +20,7 @@ interface Status {
20
20
 
21
21
  export function StatusBar({
22
22
  projectDir,
23
- conn,
23
+ dbPath,
24
24
  chatTitle,
25
25
  onDaemonStatusChange,
26
26
  }: StatusBarProps) {
@@ -35,8 +35,10 @@ export function StatusBar({
35
35
 
36
36
  const refresh = async () => {
37
37
  const daemon = await getDaemonStatus(projectDir);
38
- const pending = await listTasks(conn, { status: "pending" });
39
- const inProgress = await listTasks(conn, { status: "in_progress" });
38
+ const [pending, inProgress] = await withDb(dbPath, async (conn) => [
39
+ await listTasks(conn, { status: "pending" }),
40
+ await listTasks(conn, { status: "in_progress" }),
41
+ ]);
40
42
  if (mounted) {
41
43
  const daemonRunning = daemon !== null;
42
44
  setStatus({
@@ -54,7 +56,7 @@ export function StatusBar({
54
56
  mounted = false;
55
57
  clearInterval(interval);
56
58
  };
57
- }, [projectDir, conn, onDaemonStatusChange]);
59
+ }, [projectDir, dbPath, onDaemonStatusChange]);
58
60
 
59
61
  return (
60
62
  <Box paddingX={0}>
@@ -1,6 +1,6 @@
1
1
  import { Box, Text, useInput, useStdout } from "ink";
2
2
  import { memo, useCallback, useEffect, useMemo, useState } from "react";
3
- import type { DbConnection } from "../../db/connection.ts";
3
+ import { withDb } from "../../db/connection.ts";
4
4
  import {
5
5
  deleteTask,
6
6
  listTasks,
@@ -11,7 +11,7 @@ import {
11
11
  import { ansi, theme } from "../theme.ts";
12
12
 
13
13
  interface TaskPanelProps {
14
- conn: DbConnection;
14
+ dbPath: string;
15
15
  isActive: boolean;
16
16
  }
17
17
 
@@ -145,7 +145,7 @@ function cycleFilter<T>(current: T | null, values: readonly T[]): T | null {
145
145
  }
146
146
 
147
147
  export const TaskPanel = memo(function TaskPanel({
148
- conn,
148
+ dbPath,
149
149
  isActive,
150
150
  }: TaskPanelProps) {
151
151
  const { stdout } = useStdout();
@@ -171,7 +171,7 @@ export const TaskPanel = memo(function TaskPanel({
171
171
  } = {};
172
172
  if (statusFilter) filters.status = statusFilter;
173
173
  if (priorityFilter) filters.priority = priorityFilter;
174
- const result = await listTasks(conn, filters);
174
+ const result = await withDb(dbPath, (conn) => listTasks(conn, filters));
175
175
  if (mounted) {
176
176
  setTasks(result);
177
177
  setSelectedIndex((prev) =>
@@ -186,7 +186,7 @@ export const TaskPanel = memo(function TaskPanel({
186
186
  mounted = false;
187
187
  clearInterval(interval);
188
188
  };
189
- }, [conn, statusFilter, priorityFilter, refreshTick]);
189
+ }, [dbPath, statusFilter, priorityFilter, refreshTick]);
190
190
 
191
191
  const selectedTask = tasks[selectedIndex];
192
192
 
@@ -275,7 +275,7 @@ export const TaskPanel = memo(function TaskPanel({
275
275
  return;
276
276
  }
277
277
  if (input === "d" && selectedTask) {
278
- deleteTask(conn, selectedTask.id).then(() => {
278
+ withDb(dbPath, (conn) => deleteTask(conn, selectedTask.id)).then(() => {
279
279
  forceRefresh();
280
280
  });
281
281
  return;
@@ -1,6 +1,6 @@
1
1
  import { Box, Text, useInput, useStdout } from "ink";
2
2
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import type { DbConnection } from "../../db/connection.ts";
3
+ import { withDb } from "../../db/connection.ts";
4
4
  import {
5
5
  deleteThread,
6
6
  getInteractionsAfter,
@@ -13,7 +13,7 @@ import {
13
13
  import { ansi, theme } from "../theme.ts";
14
14
 
15
15
  interface ThreadPanelProps {
16
- conn: DbConnection;
16
+ dbPath: string;
17
17
  activeThreadId: string;
18
18
  isActive: boolean;
19
19
  }
@@ -181,7 +181,7 @@ function cycleFilter<T>(current: T | null, values: readonly T[]): T | null {
181
181
  }
182
182
 
183
183
  export const ThreadPanel = memo(function ThreadPanel({
184
- conn,
184
+ dbPath,
185
185
  activeThreadId,
186
186
  isActive,
187
187
  }: ThreadPanelProps) {
@@ -210,7 +210,7 @@ export const ThreadPanel = memo(function ThreadPanel({
210
210
  const refresh = async () => {
211
211
  const filters: { type?: Thread["type"] } = {};
212
212
  if (typeFilter) filters.type = typeFilter;
213
- const result = await listThreads(conn, filters);
213
+ const result = await withDb(dbPath, (conn) => listThreads(conn, filters));
214
214
  if (mounted) {
215
215
  setThreads(result);
216
216
  setSelectedIndex((prev) =>
@@ -225,7 +225,7 @@ export const ThreadPanel = memo(function ThreadPanel({
225
225
  mounted = false;
226
226
  clearInterval(interval);
227
227
  };
228
- }, [conn, typeFilter, refreshTick]);
228
+ }, [dbPath, typeFilter, refreshTick]);
229
229
 
230
230
  // Filter threads by search query
231
231
  const filteredThreads = useMemo(() => {
@@ -245,16 +245,18 @@ export const ThreadPanel = memo(function ThreadPanel({
245
245
  return;
246
246
  }
247
247
 
248
- getThread(conn, selectedThread.id).then((result) => {
249
- if (mounted && result) {
250
- setSelectedDetail(result);
251
- }
252
- });
248
+ withDb(dbPath, (conn) => getThread(conn, selectedThread.id)).then(
249
+ (result) => {
250
+ if (mounted && result) {
251
+ setSelectedDetail(result);
252
+ }
253
+ },
254
+ );
253
255
 
254
256
  return () => {
255
257
  mounted = false;
256
258
  };
257
- }, [conn, selectedThread?.id, following]);
259
+ }, [dbPath, selectedThread?.id, following]);
258
260
 
259
261
  // Follow mode: poll for new interactions every 1s
260
262
  // biome-ignore lint/correctness/useExhaustiveDependencies: following and selectedThread?.id are the intentional triggers
@@ -264,10 +266,12 @@ export const ThreadPanel = memo(function ThreadPanel({
264
266
 
265
267
  const poll = async () => {
266
268
  try {
267
- const newInteractions = await getInteractionsAfter(
268
- conn,
269
- selectedThread.id,
270
- lastSeenSequenceRef.current,
269
+ const newInteractions = await withDb(dbPath, (conn) =>
270
+ getInteractionsAfter(
271
+ conn,
272
+ selectedThread.id,
273
+ lastSeenSequenceRef.current,
274
+ ),
271
275
  );
272
276
  if (!mounted || newInteractions.length === 0) return;
273
277
 
@@ -286,11 +290,15 @@ export const ThreadPanel = memo(function ThreadPanel({
286
290
  // Auto-scroll will be handled by the detailLines/maxDetailScroll recalc
287
291
  setDetailScroll(Number.MAX_SAFE_INTEGER);
288
292
 
289
- const ended = await isThreadEnded(conn, selectedThread.id);
293
+ const ended = await withDb(dbPath, (conn) =>
294
+ isThreadEnded(conn, selectedThread.id),
295
+ );
290
296
  if (mounted && ended) {
291
297
  setFollowing(false);
292
298
  // Refresh the thread to get the ended_at timestamp
293
- const result = await getThread(conn, selectedThread.id);
299
+ const result = await withDb(dbPath, (conn) =>
300
+ getThread(conn, selectedThread.id),
301
+ );
294
302
  if (mounted && result) {
295
303
  setSelectedDetail(result);
296
304
  }
@@ -306,7 +314,7 @@ export const ThreadPanel = memo(function ThreadPanel({
306
314
  mounted = false;
307
315
  clearInterval(interval);
308
316
  };
309
- }, [conn, following, selectedThread?.id]);
317
+ }, [dbPath, following, selectedThread?.id]);
310
318
 
311
319
  const isActiveSelected = selectedThread?.id === activeThreadId;
312
320
 
@@ -375,7 +383,9 @@ export const ThreadPanel = memo(function ThreadPanel({
375
383
  if (confirmDelete) {
376
384
  if (input === "y" || input === "d") {
377
385
  if (selectedThread && !isActiveSelected) {
378
- deleteThread(conn, selectedThread.id).then(() => {
386
+ withDb(dbPath, (conn) =>
387
+ deleteThread(conn, selectedThread.id),
388
+ ).then(() => {
379
389
  forceRefresh();
380
390
  });
381
391
  }
@@ -1,16 +1,18 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
2
  import type { BotholomewConfig } from "../config/schemas.ts";
3
- import type { DbConnection } from "../db/connection.ts";
3
+ import { withDb } from "../db/connection.ts";
4
4
  import { updateThreadTitle } from "../db/threads.ts";
5
5
  import { logger } from "./logger.ts";
6
6
 
7
7
  /**
8
8
  * Generate a short title for a thread using the chunker model (Haiku).
9
9
  * Fire-and-forget — errors are logged at debug level and never propagated.
10
+ * Opens its own short-lived DB connection for the write so callers can
11
+ * safely `void`-chain without holding a connection during the LLM call.
10
12
  */
11
13
  export async function generateThreadTitle(
12
14
  config: Required<BotholomewConfig>,
13
- conn: DbConnection,
15
+ dbPath: string,
14
16
  threadId: string,
15
17
  context: string,
16
18
  ): Promise<void> {
@@ -39,7 +41,7 @@ export async function generateThreadTitle(
39
41
  .trim();
40
42
 
41
43
  if (title) {
42
- await updateThreadTitle(conn, threadId, title);
44
+ await withDb(dbPath, (conn) => updateThreadTitle(conn, threadId, title));
43
45
  }
44
46
  } catch (err) {
45
47
  logger.warn(`Failed to generate thread title: ${err}`);