@os-eco/overstory-cli 0.6.1 → 0.6.5
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 +8 -7
- package/package.json +12 -4
- package/src/agents/checkpoint.test.ts +2 -2
- package/src/agents/hooks-deployer.test.ts +131 -16
- package/src/agents/hooks-deployer.ts +33 -1
- package/src/agents/identity.test.ts +27 -27
- package/src/agents/identity.ts +10 -10
- package/src/agents/lifecycle.test.ts +6 -6
- package/src/agents/lifecycle.ts +2 -2
- package/src/agents/manifest.test.ts +86 -0
- package/src/agents/overlay.test.ts +9 -9
- package/src/agents/overlay.ts +4 -4
- package/src/commands/agents.test.ts +8 -8
- package/src/commands/agents.ts +62 -91
- package/src/commands/clean.test.ts +36 -51
- package/src/commands/clean.ts +28 -49
- package/src/commands/completions.ts +14 -0
- package/src/commands/coordinator.test.ts +133 -26
- package/src/commands/coordinator.ts +101 -64
- package/src/commands/costs.test.ts +47 -47
- package/src/commands/costs.ts +96 -75
- package/src/commands/dashboard.test.ts +2 -2
- package/src/commands/dashboard.ts +75 -95
- package/src/commands/doctor.test.ts +2 -2
- package/src/commands/doctor.ts +92 -79
- package/src/commands/errors.test.ts +2 -2
- package/src/commands/errors.ts +56 -50
- package/src/commands/feed.test.ts +2 -2
- package/src/commands/feed.ts +86 -83
- package/src/commands/group.ts +167 -177
- package/src/commands/hooks.test.ts +2 -2
- package/src/commands/hooks.ts +52 -42
- package/src/commands/init.test.ts +19 -19
- package/src/commands/init.ts +7 -16
- package/src/commands/inspect.test.ts +18 -18
- package/src/commands/inspect.ts +55 -58
- package/src/commands/log.test.ts +26 -31
- package/src/commands/log.ts +97 -91
- package/src/commands/logs.test.ts +1 -1
- package/src/commands/logs.ts +101 -104
- package/src/commands/mail.test.ts +5 -5
- package/src/commands/mail.ts +157 -169
- package/src/commands/merge.test.ts +28 -66
- package/src/commands/merge.ts +21 -51
- package/src/commands/metrics.test.ts +8 -8
- package/src/commands/metrics.ts +34 -35
- package/src/commands/monitor.test.ts +3 -3
- package/src/commands/monitor.ts +57 -62
- package/src/commands/nudge.test.ts +1 -1
- package/src/commands/nudge.ts +41 -89
- package/src/commands/prime.test.ts +19 -51
- package/src/commands/prime.ts +13 -50
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +79 -86
- package/src/commands/run.test.ts +1 -1
- package/src/commands/run.ts +97 -77
- package/src/commands/sling.test.ts +201 -5
- package/src/commands/sling.ts +37 -64
- package/src/commands/spec.test.ts +14 -40
- package/src/commands/spec.ts +32 -101
- package/src/commands/status.test.ts +97 -1
- package/src/commands/status.ts +63 -58
- package/src/commands/stop.test.ts +22 -40
- package/src/commands/stop.ts +18 -33
- package/src/commands/supervisor.test.ts +12 -14
- package/src/commands/supervisor.ts +144 -165
- package/src/commands/trace.test.ts +15 -15
- package/src/commands/trace.ts +59 -82
- package/src/commands/watch.test.ts +2 -2
- package/src/commands/watch.ts +38 -45
- package/src/commands/worktree.test.ts +213 -37
- package/src/commands/worktree.ts +110 -55
- package/src/config.test.ts +96 -0
- package/src/doctor/consistency.test.ts +14 -14
- package/src/doctor/databases.test.ts +22 -2
- package/src/doctor/databases.ts +16 -0
- package/src/doctor/dependencies.test.ts +55 -1
- package/src/doctor/dependencies.ts +113 -18
- package/src/doctor/merge-queue.test.ts +4 -4
- package/src/e2e/init-sling-lifecycle.test.ts +8 -8
- package/src/errors.ts +1 -1
- package/src/index.ts +223 -213
- package/src/logging/color.test.ts +74 -91
- package/src/logging/color.ts +52 -46
- package/src/logging/reporter.test.ts +10 -10
- package/src/logging/reporter.ts +6 -5
- package/src/mail/broadcast.test.ts +1 -1
- package/src/mail/client.test.ts +6 -6
- package/src/mail/store.test.ts +3 -3
- package/src/merge/queue.test.ts +73 -7
- package/src/merge/queue.ts +17 -2
- package/src/merge/resolver.test.ts +159 -7
- package/src/merge/resolver.ts +46 -2
- package/src/metrics/store.test.ts +44 -44
- package/src/metrics/store.ts +2 -2
- package/src/metrics/summary.test.ts +35 -35
- package/src/mulch/client.test.ts +1 -1
- package/src/schema-consistency.test.ts +239 -0
- package/src/sessions/compat.test.ts +3 -3
- package/src/sessions/compat.ts +2 -2
- package/src/sessions/store.test.ts +41 -4
- package/src/sessions/store.ts +13 -2
- package/src/types.ts +14 -14
- package/src/watchdog/daemon.test.ts +10 -10
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -1
- package/src/worktree/manager.test.ts +20 -20
- package/src/worktree/manager.ts +120 -4
- package/src/worktree/tmux.test.ts +98 -9
- package/src/worktree/tmux.ts +18 -0
|
@@ -33,7 +33,7 @@ afterEach(async () => {
|
|
|
33
33
|
function makeSession(overrides: Partial<SessionMetrics> = {}): SessionMetrics {
|
|
34
34
|
return {
|
|
35
35
|
agentName: "test-agent",
|
|
36
|
-
|
|
36
|
+
taskId: "test-task-123",
|
|
37
37
|
capability: "builder",
|
|
38
38
|
startedAt: new Date("2026-01-01T00:00:00Z").toISOString(),
|
|
39
39
|
completedAt: new Date("2026-01-01T00:05:00Z").toISOString(),
|
|
@@ -66,9 +66,9 @@ describe("generateSummary", () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
test("counts total and completed sessions correctly", () => {
|
|
69
|
-
store.recordSession(makeSession({
|
|
70
|
-
store.recordSession(makeSession({
|
|
71
|
-
store.recordSession(makeSession({
|
|
69
|
+
store.recordSession(makeSession({ taskId: "task-1", completedAt: "2026-01-01T00:05:00Z" }));
|
|
70
|
+
store.recordSession(makeSession({ taskId: "task-2", completedAt: null }));
|
|
71
|
+
store.recordSession(makeSession({ taskId: "task-3", completedAt: "2026-01-01T00:10:00Z" }));
|
|
72
72
|
|
|
73
73
|
const summary = generateSummary(store);
|
|
74
74
|
|
|
@@ -79,21 +79,21 @@ describe("generateSummary", () => {
|
|
|
79
79
|
test("groups by capability with correct counts and avg durations", () => {
|
|
80
80
|
store.recordSession(
|
|
81
81
|
makeSession({
|
|
82
|
-
|
|
82
|
+
taskId: "task-1",
|
|
83
83
|
capability: "builder",
|
|
84
84
|
durationMs: 100_000,
|
|
85
85
|
}),
|
|
86
86
|
);
|
|
87
87
|
store.recordSession(
|
|
88
88
|
makeSession({
|
|
89
|
-
|
|
89
|
+
taskId: "task-2",
|
|
90
90
|
capability: "builder",
|
|
91
91
|
durationMs: 200_000,
|
|
92
92
|
}),
|
|
93
93
|
);
|
|
94
94
|
store.recordSession(
|
|
95
95
|
makeSession({
|
|
96
|
-
|
|
96
|
+
taskId: "task-3",
|
|
97
97
|
capability: "scout",
|
|
98
98
|
durationMs: 50_000,
|
|
99
99
|
}),
|
|
@@ -112,10 +112,10 @@ describe("generateSummary", () => {
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
test("respects the limit parameter for recentSessions", () => {
|
|
115
|
-
store.recordSession(makeSession({
|
|
116
|
-
store.recordSession(makeSession({
|
|
117
|
-
store.recordSession(makeSession({
|
|
118
|
-
store.recordSession(makeSession({
|
|
115
|
+
store.recordSession(makeSession({ taskId: "task-1" }));
|
|
116
|
+
store.recordSession(makeSession({ taskId: "task-2" }));
|
|
117
|
+
store.recordSession(makeSession({ taskId: "task-3" }));
|
|
118
|
+
store.recordSession(makeSession({ taskId: "task-4" }));
|
|
119
119
|
|
|
120
120
|
const summary = generateSummary(store, 2);
|
|
121
121
|
|
|
@@ -124,9 +124,9 @@ describe("generateSummary", () => {
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
test("sessions without completedAt counted in total but not completed", () => {
|
|
127
|
-
store.recordSession(makeSession({
|
|
128
|
-
store.recordSession(makeSession({
|
|
129
|
-
store.recordSession(makeSession({
|
|
127
|
+
store.recordSession(makeSession({ taskId: "task-1", completedAt: null }));
|
|
128
|
+
store.recordSession(makeSession({ taskId: "task-2", completedAt: null }));
|
|
129
|
+
store.recordSession(makeSession({ taskId: "task-3", completedAt: "2026-01-01T00:05:00Z" }));
|
|
130
130
|
|
|
131
131
|
const summary = generateSummary(store);
|
|
132
132
|
|
|
@@ -137,7 +137,7 @@ describe("generateSummary", () => {
|
|
|
137
137
|
test("aggregates token totals across all sessions", () => {
|
|
138
138
|
store.recordSession(
|
|
139
139
|
makeSession({
|
|
140
|
-
|
|
140
|
+
taskId: "task-1",
|
|
141
141
|
inputTokens: 10_000,
|
|
142
142
|
outputTokens: 2_000,
|
|
143
143
|
cacheReadTokens: 50_000,
|
|
@@ -147,7 +147,7 @@ describe("generateSummary", () => {
|
|
|
147
147
|
);
|
|
148
148
|
store.recordSession(
|
|
149
149
|
makeSession({
|
|
150
|
-
|
|
150
|
+
taskId: "task-2",
|
|
151
151
|
inputTokens: 20_000,
|
|
152
152
|
outputTokens: 3_000,
|
|
153
153
|
cacheReadTokens: 80_000,
|
|
@@ -166,7 +166,7 @@ describe("generateSummary", () => {
|
|
|
166
166
|
});
|
|
167
167
|
|
|
168
168
|
test("token totals are zero when no sessions have token data", () => {
|
|
169
|
-
store.recordSession(makeSession({
|
|
169
|
+
store.recordSession(makeSession({ taskId: "task-1" }));
|
|
170
170
|
|
|
171
171
|
const summary = generateSummary(store);
|
|
172
172
|
|
|
@@ -180,14 +180,14 @@ describe("generateSummary", () => {
|
|
|
180
180
|
test("token totals skip null cost entries gracefully", () => {
|
|
181
181
|
store.recordSession(
|
|
182
182
|
makeSession({
|
|
183
|
-
|
|
183
|
+
taskId: "task-1",
|
|
184
184
|
inputTokens: 100,
|
|
185
185
|
estimatedCostUsd: 0.5,
|
|
186
186
|
}),
|
|
187
187
|
);
|
|
188
188
|
store.recordSession(
|
|
189
189
|
makeSession({
|
|
190
|
-
|
|
190
|
+
taskId: "task-2",
|
|
191
191
|
inputTokens: 200,
|
|
192
192
|
estimatedCostUsd: null, // no cost data
|
|
193
193
|
}),
|
|
@@ -202,7 +202,7 @@ describe("generateSummary", () => {
|
|
|
202
202
|
test("capability breakdown excludes incomplete sessions from avgDurationMs", () => {
|
|
203
203
|
store.recordSession(
|
|
204
204
|
makeSession({
|
|
205
|
-
|
|
205
|
+
taskId: "task-1",
|
|
206
206
|
capability: "builder",
|
|
207
207
|
durationMs: 100_000,
|
|
208
208
|
completedAt: null,
|
|
@@ -210,14 +210,14 @@ describe("generateSummary", () => {
|
|
|
210
210
|
);
|
|
211
211
|
store.recordSession(
|
|
212
212
|
makeSession({
|
|
213
|
-
|
|
213
|
+
taskId: "task-2",
|
|
214
214
|
capability: "builder",
|
|
215
215
|
durationMs: 200_000,
|
|
216
216
|
}),
|
|
217
217
|
);
|
|
218
218
|
store.recordSession(
|
|
219
219
|
makeSession({
|
|
220
|
-
|
|
220
|
+
taskId: "task-3",
|
|
221
221
|
capability: "builder",
|
|
222
222
|
durationMs: 300_000,
|
|
223
223
|
}),
|
|
@@ -242,8 +242,8 @@ describe("formatSummary", () => {
|
|
|
242
242
|
});
|
|
243
243
|
|
|
244
244
|
test("shows total/completed/average duration", () => {
|
|
245
|
-
store.recordSession(makeSession({
|
|
246
|
-
store.recordSession(makeSession({
|
|
245
|
+
store.recordSession(makeSession({ taskId: "task-1", durationMs: 100_000 }));
|
|
246
|
+
store.recordSession(makeSession({ taskId: "task-2", durationMs: 200_000 }));
|
|
247
247
|
|
|
248
248
|
const summary = generateSummary(store);
|
|
249
249
|
const formatted = formatSummary(summary);
|
|
@@ -254,8 +254,8 @@ describe("formatSummary", () => {
|
|
|
254
254
|
});
|
|
255
255
|
|
|
256
256
|
test("shows capability breakdown", () => {
|
|
257
|
-
store.recordSession(makeSession({
|
|
258
|
-
store.recordSession(makeSession({
|
|
257
|
+
store.recordSession(makeSession({ taskId: "task-1", capability: "builder" }));
|
|
258
|
+
store.recordSession(makeSession({ taskId: "task-2", capability: "scout" }));
|
|
259
259
|
|
|
260
260
|
const summary = generateSummary(store);
|
|
261
261
|
const formatted = formatSummary(summary);
|
|
@@ -268,14 +268,14 @@ describe("formatSummary", () => {
|
|
|
268
268
|
test("shows recent sessions with status (done vs running)", () => {
|
|
269
269
|
store.recordSession(
|
|
270
270
|
makeSession({
|
|
271
|
-
|
|
271
|
+
taskId: "task-1",
|
|
272
272
|
agentName: "agent-done",
|
|
273
273
|
completedAt: "2026-01-01T00:05:00Z",
|
|
274
274
|
}),
|
|
275
275
|
);
|
|
276
276
|
store.recordSession(
|
|
277
277
|
makeSession({
|
|
278
|
-
|
|
278
|
+
taskId: "task-2",
|
|
279
279
|
agentName: "agent-running",
|
|
280
280
|
completedAt: null,
|
|
281
281
|
}),
|
|
@@ -293,7 +293,7 @@ describe("formatSummary", () => {
|
|
|
293
293
|
});
|
|
294
294
|
|
|
295
295
|
test("formatDuration: <1000ms shows ms", () => {
|
|
296
|
-
store.recordSession(makeSession({
|
|
296
|
+
store.recordSession(makeSession({ taskId: "task-1", durationMs: 500 }));
|
|
297
297
|
|
|
298
298
|
const summary = generateSummary(store);
|
|
299
299
|
const formatted = formatSummary(summary);
|
|
@@ -302,7 +302,7 @@ describe("formatSummary", () => {
|
|
|
302
302
|
});
|
|
303
303
|
|
|
304
304
|
test("formatDuration: <60000ms shows seconds", () => {
|
|
305
|
-
store.recordSession(makeSession({
|
|
305
|
+
store.recordSession(makeSession({ taskId: "task-1", durationMs: 5_500 }));
|
|
306
306
|
|
|
307
307
|
const summary = generateSummary(store);
|
|
308
308
|
const formatted = formatSummary(summary);
|
|
@@ -311,7 +311,7 @@ describe("formatSummary", () => {
|
|
|
311
311
|
});
|
|
312
312
|
|
|
313
313
|
test("formatDuration: >=60000ms shows minutes+seconds", () => {
|
|
314
|
-
store.recordSession(makeSession({
|
|
314
|
+
store.recordSession(makeSession({ taskId: "task-1", durationMs: 125_000 }));
|
|
315
315
|
|
|
316
316
|
const summary = generateSummary(store);
|
|
317
317
|
const formatted = formatSummary(summary);
|
|
@@ -322,7 +322,7 @@ describe("formatSummary", () => {
|
|
|
322
322
|
test("shows token usage section when sessions have token data", () => {
|
|
323
323
|
store.recordSession(
|
|
324
324
|
makeSession({
|
|
325
|
-
|
|
325
|
+
taskId: "task-1",
|
|
326
326
|
inputTokens: 15_000,
|
|
327
327
|
outputTokens: 3_000,
|
|
328
328
|
cacheReadTokens: 100_000,
|
|
@@ -344,7 +344,7 @@ describe("formatSummary", () => {
|
|
|
344
344
|
});
|
|
345
345
|
|
|
346
346
|
test("hides token usage section when no token data exists", () => {
|
|
347
|
-
store.recordSession(makeSession({
|
|
347
|
+
store.recordSession(makeSession({ taskId: "task-1" }));
|
|
348
348
|
|
|
349
349
|
const summary = generateSummary(store);
|
|
350
350
|
const formatted = formatSummary(summary);
|
|
@@ -355,7 +355,7 @@ describe("formatSummary", () => {
|
|
|
355
355
|
test("shows per-session cost in recent sessions", () => {
|
|
356
356
|
store.recordSession(
|
|
357
357
|
makeSession({
|
|
358
|
-
|
|
358
|
+
taskId: "task-1",
|
|
359
359
|
agentName: "agent-costly",
|
|
360
360
|
inputTokens: 10_000,
|
|
361
361
|
outputTokens: 2_000,
|
|
@@ -373,7 +373,7 @@ describe("formatSummary", () => {
|
|
|
373
373
|
test("formats large token counts with M suffix", () => {
|
|
374
374
|
store.recordSession(
|
|
375
375
|
makeSession({
|
|
376
|
-
|
|
376
|
+
taskId: "task-1",
|
|
377
377
|
inputTokens: 2_500_000,
|
|
378
378
|
outputTokens: 500_000,
|
|
379
379
|
cacheReadTokens: 0,
|
package/src/mulch/client.test.ts
CHANGED
|
@@ -360,7 +360,7 @@ describe("createMulchClient", () => {
|
|
|
360
360
|
await addProc.exited;
|
|
361
361
|
|
|
362
362
|
const client = createMulchClient(tempDir);
|
|
363
|
-
// The flag is passed correctly, but may fail if the
|
|
363
|
+
// The flag is passed correctly, but may fail if the task ID is invalid
|
|
364
364
|
// or if other required fields are missing. This test documents that the
|
|
365
365
|
// flag is properly passed to the CLI.
|
|
366
366
|
try {
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL schema consistency tests.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that SQL CREATE TABLE column names match the TypeScript row interfaces
|
|
5
|
+
* and row-to-object conversion functions across all four SQLite stores.
|
|
6
|
+
* Prevents regressions like the bead_id/task_id column rename that caused runtime failures.
|
|
7
|
+
*
|
|
8
|
+
* Strategy: create each store (which runs CREATE TABLE), then open a second
|
|
9
|
+
* read-only connection to the same temp file and query PRAGMA table_info().
|
|
10
|
+
* bun:sqlite with WAL mode allows concurrent readers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Database } from "bun:sqlite";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
15
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { createEventStore } from "./events/store.ts";
|
|
19
|
+
import { createMailStore } from "./mail/store.ts";
|
|
20
|
+
import { createMergeQueue } from "./merge/queue.ts";
|
|
21
|
+
import { createMetricsStore } from "./metrics/store.ts";
|
|
22
|
+
import { createSessionStore } from "./sessions/store.ts";
|
|
23
|
+
|
|
24
|
+
/** Extract sorted column names from a table via PRAGMA table_info(). */
|
|
25
|
+
function getTableColumns(db: Database, tableName: string): string[] {
|
|
26
|
+
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{ name: string }>;
|
|
27
|
+
return rows.map((r) => r.name).sort();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("SQL schema consistency", () => {
|
|
31
|
+
let tmpDir: string;
|
|
32
|
+
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
tmpDir = await mkdtemp(join(tmpdir(), "overstory-schema-test-"));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("SessionStore", () => {
|
|
42
|
+
test("sessions table columns match SessionRow interface", () => {
|
|
43
|
+
const dbPath = join(tmpDir, "sessions.db");
|
|
44
|
+
const store = createSessionStore(dbPath);
|
|
45
|
+
|
|
46
|
+
const db = new Database(dbPath, { readonly: true });
|
|
47
|
+
const actual = getTableColumns(db, "sessions");
|
|
48
|
+
db.close();
|
|
49
|
+
store.close();
|
|
50
|
+
|
|
51
|
+
// Columns from SessionRow interface in src/sessions/store.ts
|
|
52
|
+
const expected = [
|
|
53
|
+
"agent_name",
|
|
54
|
+
"branch_name",
|
|
55
|
+
"capability",
|
|
56
|
+
"depth",
|
|
57
|
+
"escalation_level",
|
|
58
|
+
"id",
|
|
59
|
+
"last_activity",
|
|
60
|
+
"parent_agent",
|
|
61
|
+
"pid",
|
|
62
|
+
"run_id",
|
|
63
|
+
"stalled_since",
|
|
64
|
+
"started_at",
|
|
65
|
+
"state",
|
|
66
|
+
"task_id",
|
|
67
|
+
"tmux_session",
|
|
68
|
+
"worktree_path",
|
|
69
|
+
].sort();
|
|
70
|
+
|
|
71
|
+
expect(actual).toEqual(expected);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("runs table columns match RunRow interface", () => {
|
|
75
|
+
const dbPath = join(tmpDir, "sessions.db");
|
|
76
|
+
const store = createSessionStore(dbPath);
|
|
77
|
+
|
|
78
|
+
const db = new Database(dbPath, { readonly: true });
|
|
79
|
+
const actual = getTableColumns(db, "runs");
|
|
80
|
+
db.close();
|
|
81
|
+
store.close();
|
|
82
|
+
|
|
83
|
+
// Columns from RunRow interface in src/sessions/store.ts
|
|
84
|
+
const expected = [
|
|
85
|
+
"agent_count",
|
|
86
|
+
"completed_at",
|
|
87
|
+
"coordinator_session_id",
|
|
88
|
+
"id",
|
|
89
|
+
"started_at",
|
|
90
|
+
"status",
|
|
91
|
+
].sort();
|
|
92
|
+
|
|
93
|
+
expect(actual).toEqual(expected);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("EventStore", () => {
|
|
98
|
+
test("events table columns match EventRow interface", () => {
|
|
99
|
+
const dbPath = join(tmpDir, "events.db");
|
|
100
|
+
const store = createEventStore(dbPath);
|
|
101
|
+
|
|
102
|
+
const db = new Database(dbPath, { readonly: true });
|
|
103
|
+
const actual = getTableColumns(db, "events");
|
|
104
|
+
db.close();
|
|
105
|
+
store.close();
|
|
106
|
+
|
|
107
|
+
// Columns from EventRow interface in src/events/store.ts
|
|
108
|
+
const expected = [
|
|
109
|
+
"agent_name",
|
|
110
|
+
"created_at",
|
|
111
|
+
"data",
|
|
112
|
+
"event_type",
|
|
113
|
+
"id",
|
|
114
|
+
"level",
|
|
115
|
+
"run_id",
|
|
116
|
+
"session_id",
|
|
117
|
+
"tool_args",
|
|
118
|
+
"tool_duration_ms",
|
|
119
|
+
"tool_name",
|
|
120
|
+
].sort();
|
|
121
|
+
|
|
122
|
+
expect(actual).toEqual(expected);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("MetricsStore", () => {
|
|
127
|
+
test("sessions table columns match metrics SessionRow interface", () => {
|
|
128
|
+
const dbPath = join(tmpDir, "metrics.db");
|
|
129
|
+
const store = createMetricsStore(dbPath);
|
|
130
|
+
|
|
131
|
+
const db = new Database(dbPath, { readonly: true });
|
|
132
|
+
const actual = getTableColumns(db, "sessions");
|
|
133
|
+
db.close();
|
|
134
|
+
store.close();
|
|
135
|
+
|
|
136
|
+
// Columns from SessionRow interface in src/metrics/store.ts
|
|
137
|
+
const expected = [
|
|
138
|
+
"agent_name",
|
|
139
|
+
"cache_creation_tokens",
|
|
140
|
+
"cache_read_tokens",
|
|
141
|
+
"capability",
|
|
142
|
+
"completed_at",
|
|
143
|
+
"duration_ms",
|
|
144
|
+
"estimated_cost_usd",
|
|
145
|
+
"exit_code",
|
|
146
|
+
"input_tokens",
|
|
147
|
+
"merge_result",
|
|
148
|
+
"model_used",
|
|
149
|
+
"output_tokens",
|
|
150
|
+
"parent_agent",
|
|
151
|
+
"run_id",
|
|
152
|
+
"started_at",
|
|
153
|
+
"task_id",
|
|
154
|
+
].sort();
|
|
155
|
+
|
|
156
|
+
expect(actual).toEqual(expected);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("token_snapshots table columns match SnapshotRow interface", () => {
|
|
160
|
+
const dbPath = join(tmpDir, "metrics.db");
|
|
161
|
+
const store = createMetricsStore(dbPath);
|
|
162
|
+
|
|
163
|
+
const db = new Database(dbPath, { readonly: true });
|
|
164
|
+
const actual = getTableColumns(db, "token_snapshots");
|
|
165
|
+
db.close();
|
|
166
|
+
store.close();
|
|
167
|
+
|
|
168
|
+
// Columns from SnapshotRow interface in src/metrics/store.ts
|
|
169
|
+
const expected = [
|
|
170
|
+
"agent_name",
|
|
171
|
+
"cache_creation_tokens",
|
|
172
|
+
"cache_read_tokens",
|
|
173
|
+
"created_at",
|
|
174
|
+
"estimated_cost_usd",
|
|
175
|
+
"id",
|
|
176
|
+
"input_tokens",
|
|
177
|
+
"model_used",
|
|
178
|
+
"output_tokens",
|
|
179
|
+
].sort();
|
|
180
|
+
|
|
181
|
+
expect(actual).toEqual(expected);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("MailStore", () => {
|
|
186
|
+
test("messages table columns match MessageRow interface", () => {
|
|
187
|
+
const dbPath = join(tmpDir, "mail.db");
|
|
188
|
+
const store = createMailStore(dbPath);
|
|
189
|
+
|
|
190
|
+
const db = new Database(dbPath, { readonly: true });
|
|
191
|
+
const actual = getTableColumns(db, "messages");
|
|
192
|
+
db.close();
|
|
193
|
+
store.close();
|
|
194
|
+
|
|
195
|
+
// Columns from MessageRow interface in src/mail/store.ts
|
|
196
|
+
const expected = [
|
|
197
|
+
"body",
|
|
198
|
+
"created_at",
|
|
199
|
+
"from_agent",
|
|
200
|
+
"id",
|
|
201
|
+
"payload",
|
|
202
|
+
"priority",
|
|
203
|
+
"read",
|
|
204
|
+
"subject",
|
|
205
|
+
"thread_id",
|
|
206
|
+
"to_agent",
|
|
207
|
+
"type",
|
|
208
|
+
].sort();
|
|
209
|
+
|
|
210
|
+
expect(actual).toEqual(expected);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("MergeQueue", () => {
|
|
215
|
+
test("merge_queue table columns match MergeQueueRow interface", () => {
|
|
216
|
+
const dbPath = join(tmpDir, "merge-queue.db");
|
|
217
|
+
const queue = createMergeQueue(dbPath);
|
|
218
|
+
|
|
219
|
+
const db = new Database(dbPath, { readonly: true });
|
|
220
|
+
const actual = getTableColumns(db, "merge_queue");
|
|
221
|
+
db.close();
|
|
222
|
+
queue.close();
|
|
223
|
+
|
|
224
|
+
// Columns from MergeQueueRow interface in src/merge/queue.ts
|
|
225
|
+
const expected = [
|
|
226
|
+
"agent_name",
|
|
227
|
+
"branch_name",
|
|
228
|
+
"enqueued_at",
|
|
229
|
+
"files_modified",
|
|
230
|
+
"id",
|
|
231
|
+
"resolved_tier",
|
|
232
|
+
"status",
|
|
233
|
+
"task_id",
|
|
234
|
+
].sort();
|
|
235
|
+
|
|
236
|
+
expect(actual).toEqual(expected);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -39,7 +39,7 @@ function makeJsonSession(overrides: Record<string, unknown> = {}): Record<string
|
|
|
39
39
|
capability: "builder",
|
|
40
40
|
worktreePath: "/tmp/worktrees/test-agent",
|
|
41
41
|
branchName: "overstory/test-agent/task-1",
|
|
42
|
-
|
|
42
|
+
taskId: "task-1",
|
|
43
43
|
tmuxSession: "overstory-test-agent",
|
|
44
44
|
state: "working",
|
|
45
45
|
pid: 12345,
|
|
@@ -198,7 +198,7 @@ describe("data integrity", () => {
|
|
|
198
198
|
capability: "scout",
|
|
199
199
|
worktreePath: "/tmp/worktrees/full-agent",
|
|
200
200
|
branchName: "overstory/full-agent/task-42",
|
|
201
|
-
|
|
201
|
+
taskId: "task-42",
|
|
202
202
|
tmuxSession: "overstory-full-agent",
|
|
203
203
|
state: "stalled",
|
|
204
204
|
pid: 99999,
|
|
@@ -221,7 +221,7 @@ describe("data integrity", () => {
|
|
|
221
221
|
expect(result?.capability).toBe("scout");
|
|
222
222
|
expect(result?.worktreePath).toBe("/tmp/worktrees/full-agent");
|
|
223
223
|
expect(result?.branchName).toBe("overstory/full-agent/task-42");
|
|
224
|
-
expect(result?.
|
|
224
|
+
expect(result?.taskId).toBe("task-42");
|
|
225
225
|
expect(result?.tmuxSession).toBe("overstory-full-agent");
|
|
226
226
|
expect(result?.state).toBe("stalled");
|
|
227
227
|
expect(result?.pid).toBe(99999);
|
package/src/sessions/compat.ts
CHANGED
|
@@ -25,7 +25,7 @@ function normalizeSession(raw: Record<string, unknown>): AgentSession {
|
|
|
25
25
|
capability: raw.capability as string,
|
|
26
26
|
worktreePath: raw.worktreePath as string,
|
|
27
27
|
branchName: raw.branchName as string,
|
|
28
|
-
|
|
28
|
+
taskId: raw.taskId as string,
|
|
29
29
|
tmuxSession: raw.tmuxSession as string,
|
|
30
30
|
state: raw.state as AgentSession["state"],
|
|
31
31
|
pid: (raw.pid as number | null) ?? null,
|
|
@@ -86,7 +86,7 @@ export function openSessionStore(overstoryDir: string): {
|
|
|
86
86
|
// If the DB already existed AND has data, it is authoritative -- no migration needed.
|
|
87
87
|
// If the DB file exists but is empty (e.g., created by init before any sessions were
|
|
88
88
|
// recorded), fall through to check sessions.json for importable records (overstory-036f).
|
|
89
|
-
if (dbExists && store.
|
|
89
|
+
if (dbExists && store.count() > 0) {
|
|
90
90
|
return { store, migrated: false };
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -35,7 +35,7 @@ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
|
|
|
35
35
|
capability: "builder",
|
|
36
36
|
worktreePath: "/tmp/worktrees/test-agent",
|
|
37
37
|
branchName: "overstory/test-agent/task-1",
|
|
38
|
-
|
|
38
|
+
taskId: "task-1",
|
|
39
39
|
tmuxSession: "overstory-test-agent",
|
|
40
40
|
state: "booting",
|
|
41
41
|
pid: 12345,
|
|
@@ -81,7 +81,7 @@ describe("upsert", () => {
|
|
|
81
81
|
capability: "scout",
|
|
82
82
|
worktreePath: "/tmp/worktrees/roundtrip",
|
|
83
83
|
branchName: "overstory/roundtrip-agent/task-42",
|
|
84
|
-
|
|
84
|
+
taskId: "task-42",
|
|
85
85
|
tmuxSession: "overstory-roundtrip-agent",
|
|
86
86
|
state: "working",
|
|
87
87
|
pid: 99999,
|
|
@@ -231,6 +231,43 @@ describe("getAll", () => {
|
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
// === count ===
|
|
235
|
+
|
|
236
|
+
describe("count", () => {
|
|
237
|
+
test("returns 0 on empty database", () => {
|
|
238
|
+
expect(store.count()).toBe(0);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("returns correct count after inserts", () => {
|
|
242
|
+
store.upsert(makeSession({ agentName: "a1", id: "s-1" }));
|
|
243
|
+
expect(store.count()).toBe(1);
|
|
244
|
+
|
|
245
|
+
store.upsert(makeSession({ agentName: "a2", id: "s-2" }));
|
|
246
|
+
expect(store.count()).toBe(2);
|
|
247
|
+
|
|
248
|
+
store.upsert(makeSession({ agentName: "a3", id: "s-3" }));
|
|
249
|
+
expect(store.count()).toBe(3);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("count reflects removals", () => {
|
|
253
|
+
store.upsert(makeSession({ agentName: "a1", id: "s-1" }));
|
|
254
|
+
store.upsert(makeSession({ agentName: "a2", id: "s-2" }));
|
|
255
|
+
|
|
256
|
+
store.remove("a1");
|
|
257
|
+
expect(store.count()).toBe(1);
|
|
258
|
+
|
|
259
|
+
store.remove("a2");
|
|
260
|
+
expect(store.count()).toBe(0);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("count matches getAll().length", () => {
|
|
264
|
+
for (let i = 0; i < 5; i++) {
|
|
265
|
+
store.upsert(makeSession({ agentName: `agent-${i}`, id: `s-${i}` }));
|
|
266
|
+
}
|
|
267
|
+
expect(store.count()).toBe(store.getAll().length);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
234
271
|
// === getByRun ===
|
|
235
272
|
|
|
236
273
|
describe("getByRun", () => {
|
|
@@ -510,11 +547,11 @@ describe("edge cases", () => {
|
|
|
510
547
|
});
|
|
511
548
|
|
|
512
549
|
test("empty string fields are stored correctly", () => {
|
|
513
|
-
const session = makeSession({
|
|
550
|
+
const session = makeSession({ taskId: "", capability: "builder" });
|
|
514
551
|
store.upsert(session);
|
|
515
552
|
|
|
516
553
|
const result = store.getByName("test-agent");
|
|
517
|
-
expect(result?.
|
|
554
|
+
expect(result?.taskId).toBe("");
|
|
518
555
|
});
|
|
519
556
|
});
|
|
520
557
|
|
package/src/sessions/store.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface SessionStore {
|
|
|
18
18
|
getActive(): AgentSession[];
|
|
19
19
|
/** Get all sessions regardless of state. */
|
|
20
20
|
getAll(): AgentSession[];
|
|
21
|
+
/** Get the total number of sessions. Lightweight alternative to getAll().length. */
|
|
22
|
+
count(): number;
|
|
21
23
|
/** Get sessions belonging to a specific run. */
|
|
22
24
|
getByRun(runId: string): AgentSession[];
|
|
23
25
|
/** Update only the state of a session. */
|
|
@@ -111,7 +113,7 @@ function rowToSession(row: SessionRow): AgentSession {
|
|
|
111
113
|
capability: row.capability,
|
|
112
114
|
worktreePath: row.worktree_path,
|
|
113
115
|
branchName: row.branch_name,
|
|
114
|
-
|
|
116
|
+
taskId: row.task_id,
|
|
115
117
|
tmuxSession: row.tmux_session,
|
|
116
118
|
state: row.state as AgentState,
|
|
117
119
|
pid: row.pid,
|
|
@@ -233,6 +235,10 @@ export function createSessionStore(dbPath: string): SessionStore {
|
|
|
233
235
|
SELECT * FROM sessions ORDER BY started_at ASC
|
|
234
236
|
`);
|
|
235
237
|
|
|
238
|
+
const countStmt = db.prepare<{ cnt: number }, Record<string, never>>(
|
|
239
|
+
"SELECT COUNT(*) as cnt FROM sessions",
|
|
240
|
+
);
|
|
241
|
+
|
|
236
242
|
const getByRunStmt = db.prepare<SessionRow, { $run_id: string }>(`
|
|
237
243
|
SELECT * FROM sessions WHERE run_id = $run_id ORDER BY started_at ASC
|
|
238
244
|
`);
|
|
@@ -270,7 +276,7 @@ export function createSessionStore(dbPath: string): SessionStore {
|
|
|
270
276
|
$capability: session.capability,
|
|
271
277
|
$worktree_path: session.worktreePath,
|
|
272
278
|
$branch_name: session.branchName,
|
|
273
|
-
$task_id: session.
|
|
279
|
+
$task_id: session.taskId,
|
|
274
280
|
$tmux_session: session.tmuxSession,
|
|
275
281
|
$state: session.state,
|
|
276
282
|
$pid: session.pid,
|
|
@@ -299,6 +305,11 @@ export function createSessionStore(dbPath: string): SessionStore {
|
|
|
299
305
|
return rows.map(rowToSession);
|
|
300
306
|
},
|
|
301
307
|
|
|
308
|
+
count(): number {
|
|
309
|
+
const row = countStmt.get({});
|
|
310
|
+
return row?.cnt ?? 0;
|
|
311
|
+
},
|
|
312
|
+
|
|
302
313
|
getByRun(runId: string): AgentSession[] {
|
|
303
314
|
const rows = getByRunStmt.all({ $run_id: runId });
|
|
304
315
|
return rows.map(rowToSession);
|