botholomew 0.18.2 → 0.18.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botholomew",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
4
4
  "description": "An autonomous AI agent for knowledge work — works your task queue while you sleep.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/chat/agent.ts CHANGED
@@ -7,8 +7,13 @@ import type {
7
7
  ToolUseBlock,
8
8
  } from "@anthropic-ai/sdk/resources/messages";
9
9
  import type { McpxClient } from "@evantahler/mcpx";
10
- import type { MembotClient } from "membot";
11
10
  import type { BotholomewConfig } from "../config/schemas.ts";
11
+ import {
12
+ openMembot,
13
+ resolveMembotDir,
14
+ sharedWithMem,
15
+ type WithMem,
16
+ } from "../mem/client.ts";
12
17
  import { logInteraction } from "../threads/store.ts";
13
18
  import { registerAllTools } from "../tools/registry.ts";
14
19
  import {
@@ -211,7 +216,6 @@ export async function runChatTurn(input: {
211
216
  messages: MessageParam[];
212
217
  projectDir: string;
213
218
  config: Required<BotholomewConfig>;
214
- mem: MembotClient;
215
219
  threadId: string;
216
220
  mcpxClient: McpxClient | null;
217
221
  callbacks: ChatTurnCallbacks;
@@ -223,12 +227,43 @@ export async function runChatTurn(input: {
223
227
  * Production callers should leave both unset. */
224
228
  _testClient?: Anthropic;
225
229
  _testMaxInputTokens?: number;
230
+ /** Test seam: when set, the turn uses this `withMem` instead of opening its
231
+ * own membot client. Production callers leave this unset. */
232
+ _testWithMem?: WithMem;
233
+ }): Promise<void> {
234
+ // Open membot for the duration of this turn so the DuckDB file lock is held
235
+ // only while the turn is actively executing — idle chat sessions leave the
236
+ // shared `~/.membot` store available to other Botholomew processes.
237
+ if (input._testWithMem) {
238
+ await runChatTurnBody({ ...input, withMem: input._testWithMem });
239
+ return;
240
+ }
241
+ const mem = openMembot(resolveMembotDir(input.projectDir, input.config));
242
+ await mem.connect();
243
+ try {
244
+ await runChatTurnBody({ ...input, withMem: sharedWithMem(mem) });
245
+ } finally {
246
+ await mem.close();
247
+ }
248
+ }
249
+
250
+ async function runChatTurnBody(input: {
251
+ messages: MessageParam[];
252
+ projectDir: string;
253
+ config: Required<BotholomewConfig>;
254
+ withMem: WithMem;
255
+ threadId: string;
256
+ mcpxClient: McpxClient | null;
257
+ callbacks: ChatTurnCallbacks;
258
+ session?: ChatSession;
259
+ _testClient?: Anthropic;
260
+ _testMaxInputTokens?: number;
226
261
  }): Promise<void> {
227
262
  const {
228
263
  messages,
229
264
  projectDir,
230
265
  config,
231
- mem,
266
+ withMem,
232
267
  threadId,
233
268
  mcpxClient,
234
269
  callbacks,
@@ -419,7 +454,7 @@ export async function runChatTurn(input: {
419
454
  toolUseBlocks.map(async (toolUse) => {
420
455
  const start = Date.now();
421
456
  const result = await executeChatToolCall(toolUse, {
422
- mem,
457
+ withMem,
423
458
  projectDir,
424
459
  config,
425
460
  mcpxClient,
@@ -470,7 +505,7 @@ export async function runChatTurn(input: {
470
505
  }
471
506
 
472
507
  interface ChatToolCallCtx {
473
- mem: MembotClient;
508
+ withMem: WithMem;
474
509
  projectDir: string;
475
510
  config: Required<BotholomewConfig>;
476
511
  mcpxClient: McpxClient | null;
@@ -1,10 +1,8 @@
1
1
  import type { MessageStream } from "@anthropic-ai/sdk/lib/MessageStream";
2
2
  import type { MessageParam } from "@anthropic-ai/sdk/resources/messages";
3
- import type { MembotClient } from "membot";
4
3
  import { loadConfig } from "../config/loader.ts";
5
4
  import type { BotholomewConfig } from "../config/schemas.ts";
6
5
  import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
7
- import { openMembot, resolveMembotDir } from "../mem/client.ts";
8
6
  import { loadSkills } from "../skills/loader.ts";
9
7
  import type { SkillDefinition } from "../skills/parser.ts";
10
8
  import {
@@ -19,7 +17,6 @@ import { generateThreadTitle } from "../utils/title.ts";
19
17
  import { type ChatTurnCallbacks, runChatTurn } from "./agent.ts";
20
18
 
21
19
  export interface ChatSession {
22
- mem: MembotClient;
23
20
  threadId: string;
24
21
  projectDir: string;
25
22
  config: Required<BotholomewConfig>;
@@ -60,8 +57,6 @@ export async function startChatSession(
60
57
  );
61
58
  }
62
59
 
63
- const mem = openMembot(resolveMembotDir(projectDir, config));
64
- await mem.connect();
65
60
  await ensureThreadsDir(projectDir);
66
61
 
67
62
  let threadId: string;
@@ -106,11 +101,9 @@ export async function startChatSession(
106
101
 
107
102
  const cleanup = async () => {
108
103
  await mcpxClient?.close();
109
- await mem.close();
110
104
  };
111
105
 
112
106
  return {
113
- mem,
114
107
  threadId,
115
108
  projectDir,
116
109
  config,
@@ -158,7 +151,6 @@ export async function sendMessage(
158
151
  messages: session.messages,
159
152
  projectDir: session.projectDir,
160
153
  config: session.config,
161
- mem: session.mem,
162
154
  threadId: session.threadId,
163
155
  mcpxClient: session.mcpxClient,
164
156
  callbacks,
package/src/mem/client.ts CHANGED
@@ -31,3 +31,52 @@ export function resolveMembotDir(
31
31
  export function openMembot(dataDir: string): MembotClient {
32
32
  return new MembotClient({ configFlag: dataDir });
33
33
  }
34
+
35
+ /**
36
+ * A scope-bound membot accessor passed via `ToolContext.withMem`. Each call
37
+ * runs `fn` with a live `MembotClient` and is responsible for whatever
38
+ * open/close lifecycle is appropriate for the surrounding scope.
39
+ */
40
+ export type WithMem = <T>(fn: (mem: MembotClient) => Promise<T>) => Promise<T>;
41
+
42
+ /**
43
+ * Build a `WithMem` that forwards to an already-open client, **serializing**
44
+ * concurrent callers through a per-client queue. Used inside a chat turn or
45
+ * worker tick that opens membot once and shares it across all tool calls
46
+ * within that scope.
47
+ *
48
+ * Why serialize: every `MembotClient.<op>` releases the underlying DuckDB
49
+ * instance in `finally` (so other processes can grab the file lock). When
50
+ * two ops run on the same client in parallel via `Promise.all`, the first to
51
+ * finish closes the connection out from under the slower one, and the slower
52
+ * op's next query hangs on a disposed handle. Queuing here keeps the "many
53
+ * parallel tool calls in one chat turn" pattern correct without forcing
54
+ * unrelated (non-membot) tool calls to serialize.
55
+ */
56
+ export function sharedWithMem(mem: MembotClient): WithMem {
57
+ let queue: Promise<unknown> = Promise.resolve();
58
+ return <T>(fn: (m: MembotClient) => Promise<T>): Promise<T> => {
59
+ const result = queue.then(() => fn(mem));
60
+ // One failed op shouldn't poison the rest of the queue.
61
+ queue = result.catch(() => {});
62
+ return result as Promise<T>;
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Build a `WithMem` that opens a fresh `MembotClient` per call, runs `fn`,
68
+ * and closes it in `finally`. Used in sparse, user-triggered contexts (e.g.
69
+ * the TUI ContextPanel) where holding the DuckDB file lock between ops would
70
+ * block other Botholomew processes from the shared `~/.membot` store.
71
+ */
72
+ export function scopedWithMem(dataDir: string): WithMem {
73
+ return async (fn) => {
74
+ const mem = openMembot(dataDir);
75
+ try {
76
+ await mem.connect();
77
+ return await fn(mem);
78
+ } finally {
79
+ await mem.close();
80
+ }
81
+ };
82
+ }
@@ -86,8 +86,10 @@ export function adaptOperation(
86
86
  outputSchema: membotOutputSchema,
87
87
  execute: async (input, ctx: ToolContext) => {
88
88
  try {
89
- const method = ctx.mem[methodName] as (i: unknown) => Promise<unknown>;
90
- const data = await method.call(ctx.mem, input);
89
+ const data = await ctx.withMem(async (mem) => {
90
+ const method = mem[methodName] as (i: unknown) => Promise<unknown>;
91
+ return method.call(mem, input);
92
+ });
91
93
  return { is_error: false, data };
92
94
  } catch (err) {
93
95
  if (isHelpfulError(err)) {
@@ -27,12 +27,14 @@ export const membotCopyTool = {
27
27
  outputSchema,
28
28
  execute: async (input, ctx) => {
29
29
  try {
30
- const src = await ctx.mem.read({ logical_path: input.from_logical_path });
31
- const written = await ctx.mem.write({
32
- logical_path: input.to_logical_path,
33
- content: src.content ?? "",
34
- change_note:
35
- input.change_note ?? `copied from ${input.from_logical_path}`,
30
+ const written = await ctx.withMem(async (mem) => {
31
+ const src = await mem.read({ logical_path: input.from_logical_path });
32
+ return mem.write({
33
+ logical_path: input.to_logical_path,
34
+ content: src.content ?? "",
35
+ change_note:
36
+ input.change_note ?? `copied from ${input.from_logical_path}`,
37
+ });
36
38
  });
37
39
  return {
38
40
  is_error: false,
@@ -25,7 +25,9 @@ export const membotCountLinesTool = {
25
25
  outputSchema,
26
26
  execute: async (input, ctx) => {
27
27
  try {
28
- const result = await ctx.mem.read({ logical_path: input.logical_path });
28
+ const result = await ctx.withMem((mem) =>
29
+ mem.read({ logical_path: input.logical_path }),
30
+ );
29
31
  const content = result.content ?? "";
30
32
  const lineCount = content === "" ? 0 : content.split("\n").length;
31
33
  return {
@@ -38,12 +38,14 @@ export const membotEditTool = {
38
38
  outputSchema,
39
39
  execute: async (input, ctx) => {
40
40
  try {
41
- const current = await ctx.mem.read({ logical_path: input.logical_path });
42
- const next = applyLinePatches(current.content ?? "", input.patches);
43
- const result = await ctx.mem.write({
44
- logical_path: input.logical_path,
45
- content: next,
46
- change_note: input.change_note,
41
+ const result = await ctx.withMem(async (mem) => {
42
+ const current = await mem.read({ logical_path: input.logical_path });
43
+ const next = applyLinePatches(current.content ?? "", input.patches);
44
+ return mem.write({
45
+ logical_path: input.logical_path,
46
+ content: next,
47
+ change_note: input.change_note,
48
+ });
47
49
  });
48
50
  return {
49
51
  is_error: false,
@@ -23,7 +23,9 @@ export const membotExistsTool = {
23
23
  outputSchema,
24
24
  execute: async (input, ctx) => {
25
25
  try {
26
- await ctx.mem.info({ logical_path: input.logical_path });
26
+ await ctx.withMem((mem) =>
27
+ mem.info({ logical_path: input.logical_path }),
28
+ );
27
29
  return {
28
30
  is_error: false,
29
31
  exists: true,
@@ -143,11 +143,13 @@ export const membotPipeTool = {
143
143
  }
144
144
 
145
145
  try {
146
- const written = await ctx.mem.write({
147
- logical_path: input.logical_path,
148
- content: innerOutput,
149
- change_note: input.change_note,
150
- });
146
+ const written = await ctx.withMem((mem) =>
147
+ mem.write({
148
+ logical_path: input.logical_path,
149
+ content: innerOutput,
150
+ change_note: input.change_note,
151
+ }),
152
+ );
151
153
  return {
152
154
  is_error: false,
153
155
  logical_path: written.logical_path,
package/src/tools/tool.ts CHANGED
@@ -1,17 +1,18 @@
1
1
  import type { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/messages";
2
2
  import type { McpxClient } from "@evantahler/mcpx";
3
- import type { MembotClient } from "membot";
4
3
  import { z } from "zod";
5
4
  import type { BotholomewConfig } from "../config/schemas.ts";
5
+ import type { WithMem } from "../mem/client.ts";
6
6
 
7
7
  export interface ToolContext {
8
8
  /**
9
- * Per-process membot client. Backs every `membot_*` tool. Membot manages
10
- * its own DuckDB connection lifecycle internally (lazy claim, release
11
- * between operations), so tools just call `ctx.mem.<op>(...)` directly —
12
- * no per-call open/close needed.
9
+ * Scope-bound membot accessor. Each `membot_*` tool wraps its work in
10
+ * `ctx.withMem((mem) => mem.<op>(...))`. Backed by `sharedWithMem` inside a
11
+ * chat turn / worker tick (one connection, many ops) or `scopedWithMem` in
12
+ * sparse callers like the TUI panel (open per op, release the DuckDB file
13
+ * lock between calls).
13
14
  */
14
- mem: MembotClient;
15
+ withMem: WithMem;
15
16
  projectDir: string;
16
17
  config: Required<BotholomewConfig>;
17
18
  mcpxClient: McpxClient | null;
@@ -1,8 +1,7 @@
1
1
  import { Box, Text, useInput } from "ink";
2
- import type { MembotClient } from "membot";
3
2
  import { memo, useCallback, useEffect, useMemo, useState } from "react";
4
3
  import { loadConfig } from "../../config/loader.ts";
5
- import { openMembot, resolveMembotDir } from "../../mem/client.ts";
4
+ import { resolveMembotDir, scopedWithMem } from "../../mem/client.ts";
6
5
  import {
7
6
  detailPaneBorderProps,
8
7
  type FocusState,
@@ -47,25 +46,26 @@ export const ContextPanel = memo(function ContextPanel({
47
46
  const { rows: termRows, cols: termCols } = useTerminalSize();
48
47
  const detailWidth = Math.max(1, termCols - SIDEBAR_WIDTH - 5);
49
48
 
50
- // One MembotClient per panel mount. Membot manages its DB lock per-op so
51
- // sharing the file with the chat session / workers is safe. The data dir is
52
- // resolved from `membot_scope` (global ~/.membot, project <projectDir>)
53
- // so the tab agrees with the worker / chat session / CLI.
54
- const [client, setClient] = useState<MembotClient | null>(null);
49
+ // Open a fresh membot client per op (list/read/delete) instead of holding
50
+ // one for the panel's lifetime. Holding the DuckDB file lock for the panel
51
+ // mount would block other Botholomew processes (workers, chat turns, the
52
+ // membot CLI) from the shared `~/.membot` store while this panel sits idle.
53
+ const [membotDir, setMembotDir] = useState<string | null>(null);
55
54
  useEffect(() => {
56
55
  let cancelled = false;
57
- let opened: MembotClient | null = null;
58
56
  (async () => {
59
57
  const config = await loadConfig(projectDir);
60
58
  if (cancelled) return;
61
- opened = openMembot(resolveMembotDir(projectDir, config));
62
- setClient(opened);
59
+ setMembotDir(resolveMembotDir(projectDir, config));
63
60
  })();
64
61
  return () => {
65
62
  cancelled = true;
66
- if (opened) void opened.close();
67
63
  };
68
64
  }, [projectDir]);
65
+ const withMem = useMemo(
66
+ () => (membotDir ? scopedWithMem(membotDir) : null),
67
+ [membotDir],
68
+ );
69
69
 
70
70
  const [entries, setEntries] = useState<ContextEntry[]>([]);
71
71
  const [selectedIndex, setSelectedIndex] = useState(0);
@@ -80,9 +80,9 @@ export const ContextPanel = memo(function ContextPanel({
80
80
  const visibleRows = Math.max(1, termRows - 6);
81
81
 
82
82
  const refresh = useCallback(async () => {
83
- if (!client) return;
83
+ if (!withMem) return;
84
84
  try {
85
- const out = await client.list({ limit: 500 });
85
+ const out = await withMem((mem) => mem.list({ limit: 500 }));
86
86
  const list = out.entries.map((e) => ({
87
87
  logical_path: e.logical_path,
88
88
  version_id: e.version_id,
@@ -99,7 +99,7 @@ export const ContextPanel = memo(function ContextPanel({
99
99
  setSelectedIndex(0);
100
100
  setSidebarScrollOffset(0);
101
101
  }
102
- }, [client]);
102
+ }, [withMem]);
103
103
 
104
104
  useEffect(() => {
105
105
  refresh();
@@ -117,14 +117,13 @@ export const ContextPanel = memo(function ContextPanel({
117
117
 
118
118
  useEffect(() => {
119
119
  let cancelled = false;
120
- if (!selectedEntry || !client) {
120
+ if (!selectedEntry || !withMem) {
121
121
  setFileContent(null);
122
122
  setDetailScroll(0);
123
123
  return;
124
124
  }
125
125
  setDetailScroll(0);
126
- client
127
- .read({ logical_path: selectedEntry.logical_path })
126
+ withMem((mem) => mem.read({ logical_path: selectedEntry.logical_path }))
128
127
  .then((result) => {
129
128
  if (cancelled) return;
130
129
  setFileContent({
@@ -142,7 +141,7 @@ export const ContextPanel = memo(function ContextPanel({
142
141
  return () => {
143
142
  cancelled = true;
144
143
  };
145
- }, [client, selectedEntry]);
144
+ }, [withMem, selectedEntry]);
146
145
 
147
146
  const detailLines = useMemo(() => {
148
147
  if (!fileContent || !selectedEntry) return [];
@@ -167,11 +166,11 @@ export const ContextPanel = memo(function ContextPanel({
167
166
 
168
167
  const deleteConfirm = useDeleteConfirm(() => {
169
168
  const entry = selectedEntryRef.current;
170
- if (!entry || !client) return;
169
+ if (!entry || !withMem) return;
171
170
  const path = entry.logical_path;
172
171
  (async () => {
173
172
  try {
174
- await client.remove({ paths: [path] });
173
+ await withMem((mem) => mem.remove({ paths: [path] }));
175
174
  } catch {
176
175
  // ignore — refresh will reflect any partial state
177
176
  }
@@ -2,7 +2,6 @@ import { hostname } from "node:os";
2
2
  import ansis from "ansis";
3
3
  import { loadConfig } from "../config/loader.ts";
4
4
  import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
5
- import { openMembot, resolveMembotDir } from "../mem/client.ts";
6
5
  import { logger } from "../utils/logger.ts";
7
6
  import { uuidv7 } from "../utils/uuid.ts";
8
7
  import { markWorkerStopped, registerWorker } from "../workers/store.ts";
@@ -87,10 +86,6 @@ export async function startWorker(
87
86
  const evalSchedules = options.evalSchedules ?? !taskId;
88
87
 
89
88
  const config = await loadConfig(projectDir);
90
- const mem = openMembot(resolveMembotDir(projectDir, config));
91
- // Surface init-time failures (bad config, locked DB) up front rather than
92
- // letting the first tool call do it.
93
- await mem.connect();
94
89
 
95
90
  const mcpxClient = await createMcpxClient(resolveMcpxDir(projectDir, config));
96
91
  if (mcpxClient) {
@@ -127,7 +122,6 @@ export async function startWorker(
127
122
  stopHeartbeat();
128
123
  stopReaper();
129
124
  await mcpxClient?.close();
130
- await mem.close();
131
125
  try {
132
126
  await markWorkerStopped(projectDir, workerId);
133
127
  } catch (err) {
@@ -150,7 +144,6 @@ export async function startWorker(
150
144
  if (taskId) {
151
145
  await runSpecificTask({
152
146
  projectDir,
153
- mem,
154
147
  config,
155
148
  workerId,
156
149
  taskId,
@@ -160,7 +153,6 @@ export async function startWorker(
160
153
  } else {
161
154
  await tick({
162
155
  projectDir,
163
- mem,
164
156
  config,
165
157
  workerId,
166
158
  mcpxClient,
@@ -181,7 +173,6 @@ export async function startWorker(
181
173
  try {
182
174
  didWork = await tick({
183
175
  projectDir,
184
- mem,
185
176
  config,
186
177
  workerId,
187
178
  mcpxClient,
@@ -207,6 +198,5 @@ export async function startWorker(
207
198
  logger.warn(`failed to mark worker stopped: ${err}`);
208
199
  }
209
200
  await mcpxClient?.close();
210
- await mem.close();
211
201
  }
212
202
  }
package/src/worker/llm.ts CHANGED
@@ -5,8 +5,8 @@ import type {
5
5
  ToolUseBlock,
6
6
  } from "@anthropic-ai/sdk/resources/messages";
7
7
  import type { McpxClient } from "@evantahler/mcpx";
8
- import type { MembotClient } from "membot";
9
8
  import type { BotholomewConfig } from "../config/schemas.ts";
9
+ import type { WithMem } from "../mem/client.ts";
10
10
  import type { Task } from "../tasks/schema.ts";
11
11
  import { getTask } from "../tasks/store.ts";
12
12
  import { logInteraction } from "../threads/store.ts";
@@ -50,7 +50,7 @@ export async function runAgentLoop(input: {
50
50
  systemPrompt: string;
51
51
  task: Task;
52
52
  config: Required<BotholomewConfig>;
53
- mem: MembotClient;
53
+ withMem: WithMem;
54
54
  threadId: string;
55
55
  projectDir: string;
56
56
  workerId?: string;
@@ -61,7 +61,7 @@ export async function runAgentLoop(input: {
61
61
  systemPrompt,
62
62
  task,
63
63
  config,
64
- mem,
64
+ withMem,
65
65
  threadId,
66
66
  projectDir,
67
67
  workerId,
@@ -205,7 +205,7 @@ export async function runAgentLoop(input: {
205
205
  toolUseBlocks.map(async (toolUse) => {
206
206
  const start = Date.now();
207
207
  const result = await executeToolCall(toolUse, {
208
- mem,
208
+ withMem,
209
209
  projectDir,
210
210
  config,
211
211
  mcpxClient: input.mcpxClient ?? null,
@@ -264,7 +264,7 @@ interface ToolCallResult {
264
264
  }
265
265
 
266
266
  interface ToolCallCtx {
267
- mem: MembotClient;
267
+ withMem: WithMem;
268
268
  projectDir: string;
269
269
  config: Required<BotholomewConfig>;
270
270
  mcpxClient: McpxClient | null;
@@ -1,6 +1,11 @@
1
1
  import type { McpxClient } from "@evantahler/mcpx";
2
- import type { MembotClient } from "membot";
3
2
  import type { BotholomewConfig } from "../config/schemas.ts";
3
+ import {
4
+ openMembot,
5
+ resolveMembotDir,
6
+ sharedWithMem,
7
+ type WithMem,
8
+ } from "../mem/client.ts";
4
9
  import type { Task } from "../tasks/schema.ts";
5
10
  import {
6
11
  claimNextTask,
@@ -19,7 +24,6 @@ import { processSchedules } from "./schedules.ts";
19
24
 
20
25
  export interface TickOptions {
21
26
  projectDir: string;
22
- mem: MembotClient;
23
27
  config: Required<BotholomewConfig>;
24
28
  workerId: string;
25
29
  mcpxClient?: McpxClient | null;
@@ -31,11 +35,15 @@ export interface TickOptions {
31
35
  /**
32
36
  * Run one unit of work for a worker: optionally evaluate schedules, claim
33
37
  * the next eligible task, and process it. Returns true if work was done.
38
+ *
39
+ * Opens a membot client for the duration of this tick and closes it on the
40
+ * way out so the DuckDB file lock is released between ticks — other
41
+ * Botholomew processes (other workers, chat, the membot CLI) can read the
42
+ * shared `~/.membot` store while this worker is idle.
34
43
  */
35
44
  export async function tick(opts: TickOptions): Promise<boolean> {
36
45
  const {
37
46
  projectDir,
38
- mem,
39
47
  config,
40
48
  workerId,
41
49
  mcpxClient,
@@ -74,15 +82,21 @@ export async function tick(opts: TickOptions): Promise<boolean> {
74
82
  return false;
75
83
  }
76
84
 
77
- await runClaimedTask({
78
- projectDir,
79
- mem,
80
- config,
81
- workerId,
82
- mcpxClient,
83
- callbacks,
84
- task,
85
- });
85
+ const mem = openMembot(resolveMembotDir(projectDir, config));
86
+ await mem.connect();
87
+ try {
88
+ await runClaimedTask({
89
+ projectDir,
90
+ withMem: sharedWithMem(mem),
91
+ config,
92
+ workerId,
93
+ mcpxClient,
94
+ callbacks,
95
+ task,
96
+ });
97
+ } finally {
98
+ await mem.close();
99
+ }
86
100
 
87
101
  const elapsed = ((Date.now() - tickStart) / 1000).toFixed(1);
88
102
  logger.phase("tick-end", `#${tickNum} ${elapsed}s didWork=true`);
@@ -95,7 +109,6 @@ export async function tick(opts: TickOptions): Promise<boolean> {
95
109
  */
96
110
  export async function runSpecificTask(opts: {
97
111
  projectDir: string;
98
- mem: MembotClient;
99
112
  config: Required<BotholomewConfig>;
100
113
  workerId: string;
101
114
  taskId: string;
@@ -113,28 +126,34 @@ export async function runSpecificTask(opts: {
113
126
  );
114
127
  return false;
115
128
  }
116
- await runClaimedTask({
117
- projectDir: opts.projectDir,
118
- mem: opts.mem,
119
- config: opts.config,
120
- workerId: opts.workerId,
121
- mcpxClient: opts.mcpxClient,
122
- callbacks: opts.callbacks,
123
- task,
124
- });
129
+ const mem = openMembot(resolveMembotDir(opts.projectDir, opts.config));
130
+ await mem.connect();
131
+ try {
132
+ await runClaimedTask({
133
+ projectDir: opts.projectDir,
134
+ withMem: sharedWithMem(mem),
135
+ config: opts.config,
136
+ workerId: opts.workerId,
137
+ mcpxClient: opts.mcpxClient,
138
+ callbacks: opts.callbacks,
139
+ task,
140
+ });
141
+ } finally {
142
+ await mem.close();
143
+ }
125
144
  return true;
126
145
  }
127
146
 
128
147
  async function runClaimedTask(opts: {
129
148
  projectDir: string;
130
- mem: MembotClient;
149
+ withMem: WithMem;
131
150
  config: Required<BotholomewConfig>;
132
151
  workerId: string;
133
152
  mcpxClient?: McpxClient | null;
134
153
  callbacks?: WorkerStreamCallbacks;
135
154
  task: Task;
136
155
  }): Promise<void> {
137
- const { projectDir, mem, config, workerId, mcpxClient, callbacks, task } =
156
+ const { projectDir, withMem, config, workerId, mcpxClient, callbacks, task } =
138
157
  opts;
139
158
 
140
159
  logger.info(`Claimed task: ${task.name} (${task.id})`);
@@ -172,7 +191,7 @@ async function runClaimedTask(opts: {
172
191
  systemPrompt,
173
192
  task,
174
193
  config,
175
- mem,
194
+ withMem,
176
195
  threadId,
177
196
  projectDir,
178
197
  workerId,