botholomew 0.9.11 → 0.9.12
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/commands/db.ts +22 -11
- package/src/db/doctor.ts +37 -9
- package/src/worker/llm.ts +19 -0
- package/src/worker/tick.ts +3 -0
package/package.json
CHANGED
package/src/commands/db.ts
CHANGED
|
@@ -3,11 +3,12 @@ import type { Command } from "commander";
|
|
|
3
3
|
import { getDbPath } from "../constants.ts";
|
|
4
4
|
import { withDb as coreWithDb } from "../db/connection.ts";
|
|
5
5
|
import {
|
|
6
|
+
isPidAlive,
|
|
6
7
|
type ProbeResult,
|
|
7
8
|
probeAllTables,
|
|
8
9
|
repairDatabase,
|
|
9
10
|
} from "../db/doctor.ts";
|
|
10
|
-
import { listWorkers } from "../db/workers.ts";
|
|
11
|
+
import { listWorkers, type Worker } from "../db/workers.ts";
|
|
11
12
|
import { logger } from "../utils/logger.ts";
|
|
12
13
|
|
|
13
14
|
function statusBadge(status: ProbeResult["status"]): string {
|
|
@@ -78,28 +79,38 @@ async function doctor(program: Command, repair: boolean): Promise<void> {
|
|
|
78
79
|
process.exit(1);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
// Repair requires exclusive access — refuse if any worker is
|
|
82
|
-
//
|
|
82
|
+
// Repair requires exclusive access — refuse if any worker is actually
|
|
83
|
+
// running, otherwise the EXPORT would race with the worker's writes.
|
|
84
|
+
// Stale `status='running'` rows whose PID is dead (the exact case that
|
|
85
|
+
// tends to coexist with workers-table corruption) are reported but do
|
|
86
|
+
// not block repair: trying to flip them to `stopped` would just trip
|
|
87
|
+
// the same corruption we're about to fix.
|
|
83
88
|
const running = await coreWithDb(dbPath, async (conn) => {
|
|
84
89
|
try {
|
|
85
90
|
return await listWorkers(conn, { status: "running" });
|
|
86
91
|
} catch {
|
|
87
|
-
|
|
88
|
-
// fall through and let repair proceed; the user is on their own
|
|
89
|
-
// for confirming no live workers, which `worker reap` would also
|
|
90
|
-
// be unable to do anyway.
|
|
91
|
-
return [];
|
|
92
|
+
return [] as Worker[];
|
|
92
93
|
}
|
|
93
94
|
});
|
|
94
|
-
|
|
95
|
+
const live = running.filter((w) => isPidAlive(w.pid));
|
|
96
|
+
const stale = running.filter((w) => !isPidAlive(w.pid));
|
|
97
|
+
if (live.length > 0) {
|
|
95
98
|
logger.error(
|
|
96
|
-
`${
|
|
99
|
+
`${live.length} worker(s) actually running. Stop them first: botholomew worker stop <id>`,
|
|
97
100
|
);
|
|
98
|
-
for (const w of
|
|
101
|
+
for (const w of live) {
|
|
99
102
|
logger.dim(` ${w.id} (pid ${w.pid}, mode=${w.mode})`);
|
|
100
103
|
}
|
|
101
104
|
process.exit(1);
|
|
102
105
|
}
|
|
106
|
+
if (stale.length > 0) {
|
|
107
|
+
logger.warn(
|
|
108
|
+
`${stale.length} worker row(s) marked 'running' but PID is dead — proceeding (rows will be carried through repair, then reapable):`,
|
|
109
|
+
);
|
|
110
|
+
for (const w of stale) {
|
|
111
|
+
logger.dim(` ${w.id} (pid ${w.pid}, mode=${w.mode})`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
103
114
|
|
|
104
115
|
logger.phase("repair", "EXPORT DATABASE → swap files → IMPORT DATABASE");
|
|
105
116
|
const result = await repairDatabase(dbPath);
|
package/src/db/doctor.ts
CHANGED
|
@@ -85,12 +85,15 @@ export async function probeTable(
|
|
|
85
85
|
}
|
|
86
86
|
`;
|
|
87
87
|
|
|
88
|
+
// Discard the child's stderr. When the probe panics, Bun writes a multi-
|
|
89
|
+
// line crash banner there which would otherwise spill into our table
|
|
90
|
+
// output via the fallback message. The exit code alone tells us what we
|
|
91
|
+
// need to know.
|
|
88
92
|
const proc = Bun.spawn(["bun", "-e", script], {
|
|
89
|
-
stdio: ["ignore", "pipe", "
|
|
93
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
90
94
|
});
|
|
91
|
-
const [stdout,
|
|
95
|
+
const [stdout, exitCode] = await Promise.all([
|
|
92
96
|
new Response(proc.stdout).text(),
|
|
93
|
-
new Response(proc.stderr).text(),
|
|
94
97
|
proc.exited,
|
|
95
98
|
]);
|
|
96
99
|
|
|
@@ -103,20 +106,21 @@ export async function probeTable(
|
|
|
103
106
|
return {
|
|
104
107
|
table,
|
|
105
108
|
status: "missing",
|
|
106
|
-
message: stdout.slice("MISSING:".length),
|
|
109
|
+
message: firstLine(stdout.slice("MISSING:".length)),
|
|
107
110
|
};
|
|
108
111
|
}
|
|
109
112
|
if (stdout.startsWith("CORRUPT:")) {
|
|
110
113
|
return {
|
|
111
114
|
table,
|
|
112
115
|
status: "corrupt",
|
|
113
|
-
message: stdout.slice("CORRUPT:".length),
|
|
116
|
+
message: firstLine(stdout.slice("CORRUPT:".length)),
|
|
114
117
|
};
|
|
115
118
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
return {
|
|
120
|
+
table,
|
|
121
|
+
status: "corrupt",
|
|
122
|
+
message: `child exited with code ${exitCode} (likely native panic)`,
|
|
123
|
+
};
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/**
|
|
@@ -212,3 +216,27 @@ async function pathExists(p: string): Promise<boolean> {
|
|
|
212
216
|
return false;
|
|
213
217
|
}
|
|
214
218
|
}
|
|
219
|
+
|
|
220
|
+
function firstLine(s: string): string {
|
|
221
|
+
const trimmed = s.trim();
|
|
222
|
+
const nl = trimmed.indexOf("\n");
|
|
223
|
+
return nl === -1 ? trimmed : trimmed.slice(0, nl);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Send signal 0 to test whether `pid` corresponds to a live process. Returns
|
|
228
|
+
* false on ESRCH (no such process) and on any other error (including EPERM,
|
|
229
|
+
* which we conservatively treat as "not ours, not relevant"). Used by the
|
|
230
|
+
* doctor's safety gate to distinguish workers actually running from rows
|
|
231
|
+
* that say `status = 'running'` because the worker crashed before flipping
|
|
232
|
+
* its row to `stopped` or `dead`.
|
|
233
|
+
*/
|
|
234
|
+
export function isPidAlive(pid: number): boolean {
|
|
235
|
+
if (!pid || pid < 1) return false;
|
|
236
|
+
try {
|
|
237
|
+
process.kill(pid, 0);
|
|
238
|
+
return true;
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
package/src/worker/llm.ts
CHANGED
|
@@ -11,12 +11,17 @@ import { getTask, type Task } from "../db/tasks.ts";
|
|
|
11
11
|
import { logInteraction } from "../db/threads.ts";
|
|
12
12
|
import { registerAllTools } from "../tools/registry.ts";
|
|
13
13
|
import { getTool, type ToolContext, toAnthropicTools } from "../tools/tool.ts";
|
|
14
|
+
import { logger } from "../utils/logger.ts";
|
|
14
15
|
import { fitToContextWindow, getMaxInputTokens } from "./context.ts";
|
|
15
16
|
import { clearLargeResults, maybeStoreResult } from "./large-results.ts";
|
|
16
17
|
import { createLlmClient } from "./llm-client.ts";
|
|
17
18
|
|
|
18
19
|
registerAllTools();
|
|
19
20
|
|
|
21
|
+
function truncate(s: string, max: number): string {
|
|
22
|
+
return s.length > max ? `${s.slice(0, max)}…` : s;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
export interface WorkerStreamCallbacks {
|
|
21
26
|
onToken: (text: string) => void;
|
|
22
27
|
onToolStart: (name: string, input: string) => void;
|
|
@@ -153,6 +158,9 @@ export async function runAgentLoop(input: {
|
|
|
153
158
|
tokenCount,
|
|
154
159
|
}),
|
|
155
160
|
);
|
|
161
|
+
if (!callbacks) {
|
|
162
|
+
logger.phase("assistant", block.text);
|
|
163
|
+
}
|
|
156
164
|
}
|
|
157
165
|
}
|
|
158
166
|
|
|
@@ -175,6 +183,12 @@ export async function runAgentLoop(input: {
|
|
|
175
183
|
for (const toolUse of toolUseBlocks) {
|
|
176
184
|
const toolInput = JSON.stringify(toolUse.input);
|
|
177
185
|
callbacks?.onToolStart(toolUse.name, toolInput);
|
|
186
|
+
if (!callbacks) {
|
|
187
|
+
logger.phase(
|
|
188
|
+
"tool-call",
|
|
189
|
+
`${toolUse.name} ${truncate(toolInput, 200)}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
178
192
|
await withDb(dbPath, (conn) =>
|
|
179
193
|
logInteraction(conn, threadId, {
|
|
180
194
|
role: "assistant",
|
|
@@ -222,6 +236,11 @@ export async function runAgentLoop(input: {
|
|
|
222
236
|
durationMs,
|
|
223
237
|
}),
|
|
224
238
|
);
|
|
239
|
+
if (!callbacks) {
|
|
240
|
+
const seconds = (durationMs / 1000).toFixed(1);
|
|
241
|
+
const status = result.isError ? "err" : "ok";
|
|
242
|
+
logger.phase("tool-result", `${toolUse.name} ${status} in ${seconds}s`);
|
|
243
|
+
}
|
|
225
244
|
|
|
226
245
|
if (result.terminal && result.agentResult) {
|
|
227
246
|
return result.agentResult;
|
package/src/worker/tick.ts
CHANGED
|
@@ -133,6 +133,9 @@ async function runClaimedTask(opts: {
|
|
|
133
133
|
const { projectDir, dbPath, config, mcpxClient, callbacks, task } = opts;
|
|
134
134
|
|
|
135
135
|
logger.info(`Claimed task: ${task.name} (${task.id})`);
|
|
136
|
+
if (!callbacks && task.description) {
|
|
137
|
+
logger.dim(task.description);
|
|
138
|
+
}
|
|
136
139
|
callbacks?.onTaskStart(task);
|
|
137
140
|
|
|
138
141
|
const threadId = await withDb(dbPath, (conn) =>
|