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 +1 -1
- package/src/chat/agent.ts +40 -5
- package/src/chat/session.ts +0 -8
- package/src/mem/client.ts +49 -0
- package/src/tools/membot/adapter.ts +4 -2
- package/src/tools/membot/copy.ts +8 -6
- package/src/tools/membot/count_lines.ts +3 -1
- package/src/tools/membot/edit.ts +8 -6
- package/src/tools/membot/exists.ts +3 -1
- package/src/tools/membot/pipe.ts +7 -5
- package/src/tools/tool.ts +7 -6
- package/src/tui/components/ContextPanel.tsx +19 -20
- package/src/worker/index.ts +0 -10
- package/src/worker/llm.ts +5 -5
- package/src/worker/tick.ts +44 -25
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
508
|
+
withMem: WithMem;
|
|
474
509
|
projectDir: string;
|
|
475
510
|
config: Required<BotholomewConfig>;
|
|
476
511
|
mcpxClient: McpxClient | null;
|
package/src/chat/session.ts
CHANGED
|
@@ -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
|
|
90
|
-
|
|
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)) {
|
package/src/tools/membot/copy.ts
CHANGED
|
@@ -27,12 +27,14 @@ export const membotCopyTool = {
|
|
|
27
27
|
outputSchema,
|
|
28
28
|
execute: async (input, ctx) => {
|
|
29
29
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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 {
|
package/src/tools/membot/edit.ts
CHANGED
|
@@ -38,12 +38,14 @@ export const membotEditTool = {
|
|
|
38
38
|
outputSchema,
|
|
39
39
|
execute: async (input, ctx) => {
|
|
40
40
|
try {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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,
|
package/src/tools/membot/pipe.ts
CHANGED
|
@@ -143,11 +143,13 @@ export const membotPipeTool = {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
try {
|
|
146
|
-
const written = await ctx.mem
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
const [
|
|
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
|
-
|
|
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 (!
|
|
83
|
+
if (!withMem) return;
|
|
84
84
|
try {
|
|
85
|
-
const out = await
|
|
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
|
-
}, [
|
|
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 || !
|
|
120
|
+
if (!selectedEntry || !withMem) {
|
|
121
121
|
setFileContent(null);
|
|
122
122
|
setDetailScroll(0);
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
setDetailScroll(0);
|
|
126
|
-
|
|
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
|
-
}, [
|
|
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 || !
|
|
169
|
+
if (!entry || !withMem) return;
|
|
171
170
|
const path = entry.logical_path;
|
|
172
171
|
(async () => {
|
|
173
172
|
try {
|
|
174
|
-
await
|
|
173
|
+
await withMem((mem) => mem.remove({ paths: [path] }));
|
|
175
174
|
} catch {
|
|
176
175
|
// ignore — refresh will reflect any partial state
|
|
177
176
|
}
|
package/src/worker/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
withMem: WithMem;
|
|
268
268
|
projectDir: string;
|
|
269
269
|
config: Required<BotholomewConfig>;
|
|
270
270
|
mcpxClient: McpxClient | null;
|
package/src/worker/tick.ts
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
194
|
+
withMem,
|
|
176
195
|
threadId,
|
|
177
196
|
projectDir,
|
|
178
197
|
workerId,
|