botholomew 0.12.3 → 0.13.0
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/README.md +91 -68
- package/package.json +3 -3
- package/src/chat/agent.ts +42 -82
- package/src/chat/session.ts +29 -25
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +177 -926
- package/src/commands/db.ts +9 -13
- package/src/commands/init.ts +4 -1
- package/src/commands/nuke.ts +57 -90
- package/src/commands/schedule.ts +103 -124
- package/src/commands/skill.ts +2 -2
- package/src/commands/task.ts +86 -95
- package/src/commands/thread.ts +107 -112
- package/src/commands/worker.ts +88 -88
- package/src/constants.ts +93 -16
- package/src/context/capabilities.ts +10 -10
- package/src/context/fetcher.ts +9 -10
- package/src/context/reindex.ts +189 -0
- package/src/context/store.ts +630 -0
- package/src/db/doctor.ts +1 -8
- package/src/db/embeddings.ts +227 -175
- package/src/db/sql/19-disk_backed_index.sql +36 -0
- package/src/db/sql/20-drop_db_tables_for_files.sql +19 -0
- package/src/fs/atomic.ts +217 -0
- package/src/fs/compat.ts +86 -0
- package/src/fs/sandbox.ts +279 -0
- package/src/init/index.ts +69 -52
- package/src/init/templates.ts +1 -1
- package/src/mcpx/client.ts +1 -1
- package/src/schedules/schema.ts +19 -0
- package/src/schedules/store.ts +296 -0
- package/src/skills/commands.ts +1 -3
- package/src/tasks/schema.ts +47 -0
- package/src/tasks/store.ts +486 -0
- package/src/threads/store.ts +559 -0
- package/src/tools/capabilities/refresh.ts +42 -21
- package/src/tools/context/pipe.ts +15 -71
- package/src/tools/context/update-beliefs.ts +3 -3
- package/src/tools/context/update-goals.ts +3 -3
- package/src/tools/dir/create.ts +26 -23
- package/src/tools/dir/size.ts +46 -17
- package/src/tools/dir/tree.ts +73 -279
- package/src/tools/file/copy.ts +50 -24
- package/src/tools/file/count-lines.ts +34 -10
- package/src/tools/file/delete.ts +44 -23
- package/src/tools/file/edit.ts +39 -14
- package/src/tools/file/exists.ts +12 -26
- package/src/tools/file/info.ts +25 -85
- package/src/tools/file/move.ts +39 -24
- package/src/tools/file/read.ts +32 -80
- package/src/tools/file/write.ts +14 -91
- package/src/tools/registry.ts +3 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schedule/list.ts +7 -3
- package/src/tools/search/fuse.ts +12 -33
- package/src/tools/search/index.ts +36 -43
- package/src/tools/search/regexp.ts +29 -17
- package/src/tools/search/semantic.ts +137 -51
- package/src/tools/skill/delete.ts +1 -1
- package/src/tools/skill/list.ts +1 -1
- package/src/tools/skill/write.ts +1 -1
- package/src/tools/task/create.ts +41 -16
- package/src/tools/task/delete.ts +3 -3
- package/src/tools/task/list.ts +6 -3
- package/src/tools/task/update.ts +31 -9
- package/src/tools/task/view.ts +6 -6
- package/src/tools/thread/list.ts +2 -2
- package/src/tools/thread/search.ts +208 -0
- package/src/tools/thread/view.ts +50 -5
- package/src/tools/worker/spawn.ts +28 -14
- package/src/tui/App.tsx +12 -19
- package/src/tui/components/ContextPanel.tsx +83 -316
- package/src/tui/components/SchedulePanel.tsx +34 -48
- package/src/tui/components/StatusBar.tsx +15 -15
- package/src/tui/components/TaskPanel.tsx +34 -38
- package/src/tui/components/ThreadPanel.tsx +29 -38
- package/src/tui/components/WorkerPanel.tsx +21 -19
- package/src/tui/markdown.ts +2 -8
- package/src/types/file-imports.d.ts +9 -0
- package/src/utils/title.ts +5 -7
- package/src/utils/v7-date.ts +47 -0
- package/src/worker/heartbeat.ts +46 -24
- package/src/worker/index.ts +13 -15
- package/src/worker/llm.ts +30 -37
- package/src/worker/prompt.ts +19 -41
- package/src/worker/schedules.ts +48 -69
- package/src/worker/spawn.ts +11 -11
- package/src/worker/tick.ts +39 -43
- package/src/workers/store.ts +247 -0
- package/src/commands/tools.ts +0 -367
- package/src/context/describer.ts +0 -140
- package/src/context/drives.ts +0 -110
- package/src/context/ingest.ts +0 -162
- package/src/context/refresh.ts +0 -183
- package/src/db/context.ts +0 -637
- package/src/db/daemon-state.ts +0 -6
- package/src/db/reembed.ts +0 -113
- package/src/db/schedules.ts +0 -213
- package/src/db/tasks.ts +0 -347
- package/src/db/threads.ts +0 -276
- package/src/db/workers.ts +0 -212
- package/src/tools/context/list-drives.ts +0 -36
- package/src/tools/context/refresh.ts +0 -165
- package/src/tools/context/search.ts +0 -54
package/src/db/threads.ts
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import type { DbConnection } from "./connection.ts";
|
|
2
|
-
import { buildWhereClause, sanitizeInt } from "./query.ts";
|
|
3
|
-
import { uuidv7 } from "./uuid.ts";
|
|
4
|
-
|
|
5
|
-
export interface Thread {
|
|
6
|
-
id: string;
|
|
7
|
-
type: "worker_tick" | "chat_session";
|
|
8
|
-
task_id: string | null;
|
|
9
|
-
title: string;
|
|
10
|
-
started_at: Date;
|
|
11
|
-
ended_at: Date | null;
|
|
12
|
-
metadata: string | null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface Interaction {
|
|
16
|
-
id: string;
|
|
17
|
-
thread_id: string;
|
|
18
|
-
sequence: number;
|
|
19
|
-
role: "user" | "assistant" | "system" | "tool";
|
|
20
|
-
kind:
|
|
21
|
-
| "message"
|
|
22
|
-
| "thinking"
|
|
23
|
-
| "tool_use"
|
|
24
|
-
| "tool_result"
|
|
25
|
-
| "context_update"
|
|
26
|
-
| "status_change";
|
|
27
|
-
content: string;
|
|
28
|
-
tool_name: string | null;
|
|
29
|
-
tool_input: string | null;
|
|
30
|
-
duration_ms: number | null;
|
|
31
|
-
token_count: number | null;
|
|
32
|
-
created_at: Date;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface ThreadRow {
|
|
36
|
-
id: string;
|
|
37
|
-
type: string;
|
|
38
|
-
task_id: string | null;
|
|
39
|
-
title: string;
|
|
40
|
-
started_at: string;
|
|
41
|
-
ended_at: string | null;
|
|
42
|
-
metadata: string | null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface InteractionRow {
|
|
46
|
-
id: string;
|
|
47
|
-
thread_id: string;
|
|
48
|
-
sequence: number;
|
|
49
|
-
role: string;
|
|
50
|
-
kind: string;
|
|
51
|
-
content: string;
|
|
52
|
-
tool_name: string | null;
|
|
53
|
-
tool_input: string | null;
|
|
54
|
-
duration_ms: number | null;
|
|
55
|
-
token_count: number | null;
|
|
56
|
-
created_at: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function rowToThread(row: ThreadRow): Thread {
|
|
60
|
-
return {
|
|
61
|
-
id: row.id,
|
|
62
|
-
type: row.type as Thread["type"],
|
|
63
|
-
task_id: row.task_id,
|
|
64
|
-
title: row.title,
|
|
65
|
-
started_at: new Date(row.started_at),
|
|
66
|
-
ended_at: row.ended_at ? new Date(row.ended_at) : null,
|
|
67
|
-
metadata: row.metadata,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function rowToInteraction(row: InteractionRow): Interaction {
|
|
72
|
-
return {
|
|
73
|
-
id: row.id,
|
|
74
|
-
thread_id: row.thread_id,
|
|
75
|
-
sequence: row.sequence,
|
|
76
|
-
role: row.role as Interaction["role"],
|
|
77
|
-
kind: row.kind as Interaction["kind"],
|
|
78
|
-
content: row.content,
|
|
79
|
-
tool_name: row.tool_name,
|
|
80
|
-
tool_input: row.tool_input,
|
|
81
|
-
duration_ms: row.duration_ms,
|
|
82
|
-
token_count: row.token_count,
|
|
83
|
-
created_at: new Date(row.created_at),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function createThread(
|
|
88
|
-
db: DbConnection,
|
|
89
|
-
type: Thread["type"],
|
|
90
|
-
taskId?: string,
|
|
91
|
-
title?: string,
|
|
92
|
-
): Promise<string> {
|
|
93
|
-
const id = uuidv7();
|
|
94
|
-
await db.queryRun(
|
|
95
|
-
`INSERT INTO threads (id, type, task_id, title)
|
|
96
|
-
VALUES (?1, ?2, ?3, ?4)`,
|
|
97
|
-
id,
|
|
98
|
-
type,
|
|
99
|
-
taskId ?? null,
|
|
100
|
-
title ?? "",
|
|
101
|
-
);
|
|
102
|
-
return id;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export async function logInteraction(
|
|
106
|
-
db: DbConnection,
|
|
107
|
-
threadId: string,
|
|
108
|
-
params: {
|
|
109
|
-
role: Interaction["role"];
|
|
110
|
-
kind: Interaction["kind"];
|
|
111
|
-
content: string;
|
|
112
|
-
toolName?: string;
|
|
113
|
-
toolInput?: string;
|
|
114
|
-
durationMs?: number;
|
|
115
|
-
tokenCount?: number;
|
|
116
|
-
},
|
|
117
|
-
): Promise<string> {
|
|
118
|
-
// Get next sequence number
|
|
119
|
-
const seqRow = await db.queryGet<{ next_seq: number }>(
|
|
120
|
-
"SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM interactions WHERE thread_id = ?1",
|
|
121
|
-
threadId,
|
|
122
|
-
);
|
|
123
|
-
const sequence = seqRow?.next_seq ?? 1;
|
|
124
|
-
|
|
125
|
-
const id = uuidv7();
|
|
126
|
-
await db.queryRun(
|
|
127
|
-
`INSERT INTO interactions (id, thread_id, sequence, role, kind, content, tool_name, tool_input, duration_ms, token_count)
|
|
128
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)`,
|
|
129
|
-
id,
|
|
130
|
-
threadId,
|
|
131
|
-
sequence,
|
|
132
|
-
params.role,
|
|
133
|
-
params.kind,
|
|
134
|
-
params.content,
|
|
135
|
-
params.toolName ?? null,
|
|
136
|
-
params.toolInput ?? null,
|
|
137
|
-
params.durationMs ?? null,
|
|
138
|
-
params.tokenCount ?? null,
|
|
139
|
-
);
|
|
140
|
-
return id;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function endThread(
|
|
144
|
-
db: DbConnection,
|
|
145
|
-
threadId: string,
|
|
146
|
-
): Promise<void> {
|
|
147
|
-
await db.queryRun(
|
|
148
|
-
"UPDATE threads SET ended_at = current_timestamp::VARCHAR WHERE id = ?1",
|
|
149
|
-
threadId,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export async function reopenThread(
|
|
154
|
-
db: DbConnection,
|
|
155
|
-
threadId: string,
|
|
156
|
-
): Promise<void> {
|
|
157
|
-
await db.queryRun(
|
|
158
|
-
"UPDATE threads SET ended_at = NULL WHERE id = ?1",
|
|
159
|
-
threadId,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function updateThreadTitle(
|
|
164
|
-
db: DbConnection,
|
|
165
|
-
threadId: string,
|
|
166
|
-
title: string,
|
|
167
|
-
): Promise<void> {
|
|
168
|
-
await db.queryRun(
|
|
169
|
-
"UPDATE threads SET title = ?2 WHERE id = ?1",
|
|
170
|
-
threadId,
|
|
171
|
-
title,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export async function getThread(
|
|
176
|
-
db: DbConnection,
|
|
177
|
-
threadId: string,
|
|
178
|
-
): Promise<{ thread: Thread; interactions: Interaction[] } | null> {
|
|
179
|
-
const threadRow = await db.queryGet<ThreadRow>(
|
|
180
|
-
"SELECT * FROM threads WHERE id = ?1",
|
|
181
|
-
threadId,
|
|
182
|
-
);
|
|
183
|
-
if (!threadRow) return null;
|
|
184
|
-
|
|
185
|
-
const interactionRows = await db.queryAll<InteractionRow>(
|
|
186
|
-
"SELECT * FROM interactions WHERE thread_id = ?1 ORDER BY sequence ASC",
|
|
187
|
-
threadId,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
thread: rowToThread(threadRow),
|
|
192
|
-
interactions: interactionRows.map(rowToInteraction),
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export async function deleteThread(
|
|
197
|
-
db: DbConnection,
|
|
198
|
-
threadId: string,
|
|
199
|
-
): Promise<boolean> {
|
|
200
|
-
await db.queryRun("DELETE FROM interactions WHERE thread_id = ?1", threadId);
|
|
201
|
-
const result = await db.queryRun(
|
|
202
|
-
"DELETE FROM threads WHERE id = ?1",
|
|
203
|
-
threadId,
|
|
204
|
-
);
|
|
205
|
-
return result.changes > 0;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export async function deleteAllThreads(
|
|
209
|
-
db: DbConnection,
|
|
210
|
-
): Promise<{ threads: number; interactions: number }> {
|
|
211
|
-
const interactions = await db.queryRun("DELETE FROM interactions");
|
|
212
|
-
const threads = await db.queryRun("DELETE FROM threads");
|
|
213
|
-
return {
|
|
214
|
-
threads: threads.changes,
|
|
215
|
-
interactions: interactions.changes,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export async function getInteractionsAfter(
|
|
220
|
-
db: DbConnection,
|
|
221
|
-
threadId: string,
|
|
222
|
-
afterSequence: number,
|
|
223
|
-
): Promise<Interaction[]> {
|
|
224
|
-
const rows = await db.queryAll<InteractionRow>(
|
|
225
|
-
`SELECT * FROM interactions WHERE thread_id = ?1 AND sequence > ?2 ORDER BY sequence ASC`,
|
|
226
|
-
threadId,
|
|
227
|
-
afterSequence,
|
|
228
|
-
);
|
|
229
|
-
return rows.map(rowToInteraction);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export async function getActiveThread(
|
|
233
|
-
db: DbConnection,
|
|
234
|
-
): Promise<Thread | null> {
|
|
235
|
-
const row = await db.queryGet<ThreadRow>(
|
|
236
|
-
`SELECT * FROM threads WHERE ended_at IS NULL ORDER BY started_at DESC LIMIT 1`,
|
|
237
|
-
);
|
|
238
|
-
return row ? rowToThread(row) : null;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export async function isThreadEnded(
|
|
242
|
-
db: DbConnection,
|
|
243
|
-
threadId: string,
|
|
244
|
-
): Promise<boolean> {
|
|
245
|
-
const row = await db.queryGet<{ ended_at: string | null }>(
|
|
246
|
-
`SELECT ended_at FROM threads WHERE id = ?1`,
|
|
247
|
-
threadId,
|
|
248
|
-
);
|
|
249
|
-
if (!row) return true;
|
|
250
|
-
return row.ended_at !== null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export async function listThreads(
|
|
254
|
-
db: DbConnection,
|
|
255
|
-
filters?: {
|
|
256
|
-
type?: Thread["type"];
|
|
257
|
-
taskId?: string;
|
|
258
|
-
limit?: number;
|
|
259
|
-
offset?: number;
|
|
260
|
-
},
|
|
261
|
-
): Promise<Thread[]> {
|
|
262
|
-
const { where, params } = buildWhereClause([
|
|
263
|
-
["type", filters?.type],
|
|
264
|
-
["task_id", filters?.taskId],
|
|
265
|
-
]);
|
|
266
|
-
const limit = filters?.limit ? `LIMIT ${sanitizeInt(filters.limit)}` : "";
|
|
267
|
-
const offset = filters?.offset ? `OFFSET ${sanitizeInt(filters.offset)}` : "";
|
|
268
|
-
|
|
269
|
-
const rows = await db.queryAll<ThreadRow>(
|
|
270
|
-
`SELECT * FROM threads ${where}
|
|
271
|
-
ORDER BY started_at DESC, id DESC
|
|
272
|
-
${limit} ${offset}`,
|
|
273
|
-
...params,
|
|
274
|
-
);
|
|
275
|
-
return rows.map(rowToThread);
|
|
276
|
-
}
|
package/src/db/workers.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import type { DbConnection } from "./connection.ts";
|
|
2
|
-
import { buildWhereClause, sanitizeInt } from "./query.ts";
|
|
3
|
-
|
|
4
|
-
export const WORKER_MODES = ["persist", "once"] as const;
|
|
5
|
-
export const WORKER_STATUSES = ["running", "stopped", "dead"] as const;
|
|
6
|
-
|
|
7
|
-
export interface Worker {
|
|
8
|
-
id: string;
|
|
9
|
-
pid: number;
|
|
10
|
-
hostname: string;
|
|
11
|
-
mode: (typeof WORKER_MODES)[number];
|
|
12
|
-
task_id: string | null;
|
|
13
|
-
status: (typeof WORKER_STATUSES)[number];
|
|
14
|
-
started_at: Date;
|
|
15
|
-
last_heartbeat_at: Date;
|
|
16
|
-
stopped_at: Date | null;
|
|
17
|
-
log_path: string | null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface WorkerRow {
|
|
21
|
-
id: string;
|
|
22
|
-
pid: number;
|
|
23
|
-
hostname: string;
|
|
24
|
-
mode: string;
|
|
25
|
-
task_id: string | null;
|
|
26
|
-
status: string;
|
|
27
|
-
started_at: string;
|
|
28
|
-
last_heartbeat_at: string;
|
|
29
|
-
stopped_at: string | null;
|
|
30
|
-
log_path: string | null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function rowToWorker(row: WorkerRow): Worker {
|
|
34
|
-
return {
|
|
35
|
-
id: row.id,
|
|
36
|
-
pid: row.pid,
|
|
37
|
-
hostname: row.hostname,
|
|
38
|
-
mode: row.mode as Worker["mode"],
|
|
39
|
-
task_id: row.task_id,
|
|
40
|
-
status: row.status as Worker["status"],
|
|
41
|
-
started_at: new Date(row.started_at),
|
|
42
|
-
last_heartbeat_at: new Date(row.last_heartbeat_at),
|
|
43
|
-
stopped_at: row.stopped_at ? new Date(row.stopped_at) : null,
|
|
44
|
-
log_path: row.log_path,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export async function registerWorker(
|
|
49
|
-
db: DbConnection,
|
|
50
|
-
params: {
|
|
51
|
-
id: string;
|
|
52
|
-
pid: number;
|
|
53
|
-
hostname: string;
|
|
54
|
-
mode: Worker["mode"];
|
|
55
|
-
taskId?: string | null;
|
|
56
|
-
logPath?: string | null;
|
|
57
|
-
},
|
|
58
|
-
): Promise<Worker> {
|
|
59
|
-
const row = await db.queryGet<WorkerRow>(
|
|
60
|
-
`INSERT INTO workers (id, pid, hostname, mode, task_id, status, log_path)
|
|
61
|
-
VALUES (?1, ?2, ?3, ?4, ?5, 'running', ?6)
|
|
62
|
-
RETURNING *`,
|
|
63
|
-
params.id,
|
|
64
|
-
params.pid,
|
|
65
|
-
params.hostname,
|
|
66
|
-
params.mode,
|
|
67
|
-
params.taskId ?? null,
|
|
68
|
-
params.logPath ?? null,
|
|
69
|
-
);
|
|
70
|
-
if (!row) throw new Error("INSERT did not return a row");
|
|
71
|
-
return rowToWorker(row);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function heartbeat(db: DbConnection, id: string): Promise<void> {
|
|
75
|
-
await db.queryRun(
|
|
76
|
-
`UPDATE workers
|
|
77
|
-
SET last_heartbeat_at = current_timestamp::VARCHAR
|
|
78
|
-
WHERE id = ?1 AND status = 'running'`,
|
|
79
|
-
id,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function markWorkerStopped(
|
|
84
|
-
db: DbConnection,
|
|
85
|
-
id: string,
|
|
86
|
-
): Promise<void> {
|
|
87
|
-
await db.queryRun(
|
|
88
|
-
`UPDATE workers
|
|
89
|
-
SET status = 'stopped',
|
|
90
|
-
stopped_at = current_timestamp::VARCHAR
|
|
91
|
-
WHERE id = ?1 AND status = 'running'`,
|
|
92
|
-
id,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export async function markWorkerDead(
|
|
97
|
-
db: DbConnection,
|
|
98
|
-
id: string,
|
|
99
|
-
): Promise<void> {
|
|
100
|
-
await db.queryRun(
|
|
101
|
-
`UPDATE workers
|
|
102
|
-
SET status = 'dead',
|
|
103
|
-
stopped_at = current_timestamp::VARCHAR
|
|
104
|
-
WHERE id = ?1 AND status = 'running'`,
|
|
105
|
-
id,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Find running workers whose heartbeat is older than `staleAfterSeconds`,
|
|
111
|
-
* mark them dead, and release any tasks/schedule claims they held back
|
|
112
|
-
* to the pool. Returns the ids of reaped workers.
|
|
113
|
-
*/
|
|
114
|
-
export async function reapDeadWorkers(
|
|
115
|
-
db: DbConnection,
|
|
116
|
-
staleAfterSeconds: number,
|
|
117
|
-
): Promise<string[]> {
|
|
118
|
-
const stale = await db.queryAll<{ id: string }>(
|
|
119
|
-
`UPDATE workers
|
|
120
|
-
SET status = 'dead',
|
|
121
|
-
stopped_at = current_timestamp::VARCHAR
|
|
122
|
-
WHERE status = 'running'
|
|
123
|
-
AND last_heartbeat_at::TIMESTAMP
|
|
124
|
-
< current_timestamp - to_seconds(CAST(?1 AS BIGINT))
|
|
125
|
-
RETURNING id`,
|
|
126
|
-
staleAfterSeconds,
|
|
127
|
-
);
|
|
128
|
-
const reapedIds = stale.map((r) => r.id);
|
|
129
|
-
if (reapedIds.length === 0) return reapedIds;
|
|
130
|
-
|
|
131
|
-
for (const reapedId of reapedIds) {
|
|
132
|
-
await db.queryRun(
|
|
133
|
-
`UPDATE tasks
|
|
134
|
-
SET status = 'pending',
|
|
135
|
-
claimed_by = NULL,
|
|
136
|
-
claimed_at = NULL,
|
|
137
|
-
updated_at = current_timestamp::VARCHAR
|
|
138
|
-
WHERE claimed_by = ?1 AND status = 'in_progress'`,
|
|
139
|
-
reapedId,
|
|
140
|
-
);
|
|
141
|
-
await db.queryRun(
|
|
142
|
-
`UPDATE schedules
|
|
143
|
-
SET claimed_by = NULL,
|
|
144
|
-
claimed_at = NULL
|
|
145
|
-
WHERE claimed_by = ?1`,
|
|
146
|
-
reapedId,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
return reapedIds;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Delete cleanly-stopped workers (status='stopped') whose `stopped_at` is
|
|
154
|
-
* older than `afterSeconds`. Dead workers are intentionally left alone —
|
|
155
|
-
* they're forensic evidence that something crashed.
|
|
156
|
-
* Returns the ids that were pruned.
|
|
157
|
-
*/
|
|
158
|
-
export async function pruneStoppedWorkers(
|
|
159
|
-
db: DbConnection,
|
|
160
|
-
afterSeconds: number,
|
|
161
|
-
): Promise<string[]> {
|
|
162
|
-
const rows = await db.queryAll<{ id: string }>(
|
|
163
|
-
`DELETE FROM workers
|
|
164
|
-
WHERE status = 'stopped'
|
|
165
|
-
AND stopped_at IS NOT NULL
|
|
166
|
-
AND stopped_at::TIMESTAMP
|
|
167
|
-
< current_timestamp - to_seconds(CAST(?1 AS BIGINT))
|
|
168
|
-
RETURNING id`,
|
|
169
|
-
afterSeconds,
|
|
170
|
-
);
|
|
171
|
-
return rows.map((r) => r.id);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export async function listWorkers(
|
|
175
|
-
db: DbConnection,
|
|
176
|
-
filters?: {
|
|
177
|
-
status?: Worker["status"];
|
|
178
|
-
limit?: number;
|
|
179
|
-
offset?: number;
|
|
180
|
-
},
|
|
181
|
-
): Promise<Worker[]> {
|
|
182
|
-
const { where, params } = buildWhereClause([["status", filters?.status]]);
|
|
183
|
-
const limit = filters?.limit ? `LIMIT ${sanitizeInt(filters.limit)}` : "";
|
|
184
|
-
const offset = filters?.offset ? `OFFSET ${sanitizeInt(filters.offset)}` : "";
|
|
185
|
-
|
|
186
|
-
const rows = await db.queryAll<WorkerRow>(
|
|
187
|
-
`SELECT * FROM workers ${where}
|
|
188
|
-
ORDER BY started_at DESC, id DESC
|
|
189
|
-
${limit} ${offset}`,
|
|
190
|
-
...params,
|
|
191
|
-
);
|
|
192
|
-
return rows.map(rowToWorker);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export async function getWorker(
|
|
196
|
-
db: DbConnection,
|
|
197
|
-
id: string,
|
|
198
|
-
): Promise<Worker | null> {
|
|
199
|
-
const row = await db.queryGet<WorkerRow>(
|
|
200
|
-
"SELECT * FROM workers WHERE id = ?1",
|
|
201
|
-
id,
|
|
202
|
-
);
|
|
203
|
-
return row ? rowToWorker(row) : null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export async function deleteWorker(
|
|
207
|
-
db: DbConnection,
|
|
208
|
-
id: string,
|
|
209
|
-
): Promise<boolean> {
|
|
210
|
-
const result = await db.queryRun("DELETE FROM workers WHERE id = ?1", id);
|
|
211
|
-
return result.changes > 0;
|
|
212
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { listDriveSummaries } from "../../db/context.ts";
|
|
3
|
-
import type { ToolDefinition } from "../tool.ts";
|
|
4
|
-
|
|
5
|
-
const inputSchema = z.object({});
|
|
6
|
-
|
|
7
|
-
const outputSchema = z.object({
|
|
8
|
-
drives: z.array(
|
|
9
|
-
z.object({
|
|
10
|
-
drive: z.string(),
|
|
11
|
-
count: z.number(),
|
|
12
|
-
}),
|
|
13
|
-
),
|
|
14
|
-
is_error: z.boolean(),
|
|
15
|
-
hint: z.string().optional(),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export const contextListDrivesTool = {
|
|
19
|
-
name: "context_list_drives",
|
|
20
|
-
description:
|
|
21
|
-
"List every drive that currently has content, with its item count. Use this to discover which values to pass as `drive` on other context tools (disk / url / agent / google-docs / github / …).",
|
|
22
|
-
group: "context",
|
|
23
|
-
inputSchema,
|
|
24
|
-
outputSchema,
|
|
25
|
-
execute: async (_input, ctx) => {
|
|
26
|
-
const drives = await listDriveSummaries(ctx.conn);
|
|
27
|
-
if (drives.length === 0) {
|
|
28
|
-
return {
|
|
29
|
-
drives: [],
|
|
30
|
-
is_error: false,
|
|
31
|
-
hint: "No context has been ingested yet. The user can run `botholomew context add <path-or-url>` to add content.",
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
return { drives, is_error: false };
|
|
35
|
-
},
|
|
36
|
-
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { parseDriveRef } from "../../context/drives.ts";
|
|
3
|
-
import { refreshContextItems } from "../../context/refresh.ts";
|
|
4
|
-
import {
|
|
5
|
-
type ContextItem,
|
|
6
|
-
listContextItems,
|
|
7
|
-
listContextItemsByPrefix,
|
|
8
|
-
resolveContextItem,
|
|
9
|
-
} from "../../db/context.ts";
|
|
10
|
-
import { buildContextTree } from "../dir/tree.ts";
|
|
11
|
-
import type { ToolDefinition } from "../tool.ts";
|
|
12
|
-
|
|
13
|
-
const inputSchema = z.object({
|
|
14
|
-
ref: z
|
|
15
|
-
.string()
|
|
16
|
-
.optional()
|
|
17
|
-
.describe(
|
|
18
|
-
"UUID or 'drive:/path' of a single item, or 'drive:/prefix' to refresh a subtree. Mutually exclusive with `all`.",
|
|
19
|
-
),
|
|
20
|
-
all: z
|
|
21
|
-
.boolean()
|
|
22
|
-
.optional()
|
|
23
|
-
.describe(
|
|
24
|
-
"Refresh every item that has an external origin (drive != 'agent'). Mutually exclusive with `ref`.",
|
|
25
|
-
),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const outputSchema = z.object({
|
|
29
|
-
checked: z.number(),
|
|
30
|
-
updated: z.number(),
|
|
31
|
-
unchanged: z.number(),
|
|
32
|
-
missing: z.number(),
|
|
33
|
-
reembedded: z.number(),
|
|
34
|
-
chunks: z.number(),
|
|
35
|
-
embeddings_skipped: z.boolean(),
|
|
36
|
-
items: z.array(
|
|
37
|
-
z.object({
|
|
38
|
-
id: z.string(),
|
|
39
|
-
drive: z.string(),
|
|
40
|
-
path: z.string(),
|
|
41
|
-
ref: z.string(),
|
|
42
|
-
status: z.enum(["updated", "unchanged", "missing", "error"]),
|
|
43
|
-
error: z.string().optional(),
|
|
44
|
-
}),
|
|
45
|
-
),
|
|
46
|
-
message: z.string(),
|
|
47
|
-
is_error: z.boolean(),
|
|
48
|
-
tree: z
|
|
49
|
-
.string()
|
|
50
|
-
.optional()
|
|
51
|
-
.describe(
|
|
52
|
-
"Snapshot of the context filesystem after the refresh so you can see what's currently stored.",
|
|
53
|
-
),
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const empty = {
|
|
57
|
-
checked: 0,
|
|
58
|
-
updated: 0,
|
|
59
|
-
unchanged: 0,
|
|
60
|
-
missing: 0,
|
|
61
|
-
reembedded: 0,
|
|
62
|
-
chunks: 0,
|
|
63
|
-
embeddings_skipped: false,
|
|
64
|
-
items: [],
|
|
65
|
-
tree: undefined as string | undefined,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const contextRefreshTool = {
|
|
69
|
-
name: "context_refresh",
|
|
70
|
-
description:
|
|
71
|
-
"[[ bash equivalent command: curl ]] Re-import items from their origin (disk / URL / MCP) and re-embed changed items. Use `ref` for a single item or subtree, or `all: true` for every non-agent item. URL fetches use the project's MCPX client when available and fall back to plain HTTP.",
|
|
72
|
-
group: "context",
|
|
73
|
-
inputSchema,
|
|
74
|
-
outputSchema,
|
|
75
|
-
execute: async (input, ctx) => {
|
|
76
|
-
if (!input.ref && !input.all) {
|
|
77
|
-
return {
|
|
78
|
-
...empty,
|
|
79
|
-
message: "Provide a `ref` or set `all: true`.",
|
|
80
|
-
is_error: true,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
if (input.ref && input.all) {
|
|
84
|
-
return {
|
|
85
|
-
...empty,
|
|
86
|
-
message: "`ref` and `all` are mutually exclusive.",
|
|
87
|
-
is_error: true,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
let items: ContextItem[];
|
|
92
|
-
if (input.all) {
|
|
93
|
-
items = await listContextItems(ctx.conn);
|
|
94
|
-
} else {
|
|
95
|
-
const ref = input.ref as string;
|
|
96
|
-
const exact = await resolveContextItem(ctx.conn, ref);
|
|
97
|
-
if (exact) {
|
|
98
|
-
items = [exact];
|
|
99
|
-
} else {
|
|
100
|
-
const parsed = parseDriveRef(ref);
|
|
101
|
-
items = parsed
|
|
102
|
-
? await listContextItemsByPrefix(
|
|
103
|
-
ctx.conn,
|
|
104
|
-
parsed.drive,
|
|
105
|
-
parsed.path,
|
|
106
|
-
{
|
|
107
|
-
recursive: true,
|
|
108
|
-
},
|
|
109
|
-
)
|
|
110
|
-
: [];
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (items.length === 0) {
|
|
115
|
-
return {
|
|
116
|
-
...empty,
|
|
117
|
-
message: `No context items match \`${input.ref ?? "all"}\`.`,
|
|
118
|
-
is_error: true,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const refreshable = items.filter((i) => i.drive !== "agent");
|
|
123
|
-
if (refreshable.length === 0) {
|
|
124
|
-
return {
|
|
125
|
-
...empty,
|
|
126
|
-
message:
|
|
127
|
-
"No refreshable items — everything matched lives on drive=agent (agent-authored content has no external origin).",
|
|
128
|
-
is_error: false,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const result = await refreshContextItems(
|
|
133
|
-
ctx.conn,
|
|
134
|
-
refreshable,
|
|
135
|
-
ctx.config,
|
|
136
|
-
ctx.mcpxClient,
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
const parts = [
|
|
140
|
-
`Checked ${result.checked}`,
|
|
141
|
-
`${result.updated} updated`,
|
|
142
|
-
`${result.unchanged} unchanged`,
|
|
143
|
-
`${result.missing} missing`,
|
|
144
|
-
`${result.reembedded} re-embedded (${result.chunks} chunks)`,
|
|
145
|
-
];
|
|
146
|
-
if (result.embeddings_skipped) {
|
|
147
|
-
parts.push("embeddings skipped (no OpenAI API key configured)");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// For a single-ref refresh, render that ref's drive; for `all: true`,
|
|
151
|
-
// render the top-level drive summary so the scope of the tree matches
|
|
152
|
-
// the scope of the operation.
|
|
153
|
-
const treeDrive = input.all
|
|
154
|
-
? undefined
|
|
155
|
-
: (result.items[0]?.drive ?? undefined);
|
|
156
|
-
const { tree } = await buildContextTree(ctx.conn, { drive: treeDrive });
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
...result,
|
|
160
|
-
message: parts.join(", "),
|
|
161
|
-
is_error: false,
|
|
162
|
-
tree,
|
|
163
|
-
};
|
|
164
|
-
},
|
|
165
|
-
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|