@os-eco/overstory-cli 0.7.7 → 0.7.8
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 +101 -1
- package/package.json +1 -1
- package/src/commands/coordinator.test.ts +131 -2
- package/src/commands/coordinator.ts +40 -9
- package/src/commands/costs.test.ts +5 -0
- package/src/commands/costs.ts +1 -1
- package/src/commands/log.ts +2 -0
- package/src/commands/sling.test.ts +63 -1
- package/src/commands/sling.ts +37 -2
- package/src/config.test.ts +68 -0
- package/src/config.ts +16 -0
- package/src/index.ts +2 -1
- package/src/metrics/pricing.test.ts +258 -0
- package/src/metrics/store.test.ts +227 -0
- package/src/metrics/store.ts +40 -5
- package/src/schema-consistency.test.ts +1 -0
- package/src/types.ts +8 -0
- package/src/worktree/tmux.test.ts +49 -0
- package/src/worktree/tmux.ts +33 -0
package/src/metrics/store.ts
CHANGED
|
@@ -21,8 +21,9 @@ export interface MetricsStore {
|
|
|
21
21
|
purge(options: { all?: boolean; agent?: string }): number;
|
|
22
22
|
/** Record a token usage snapshot for a running agent. */
|
|
23
23
|
recordSnapshot(snapshot: TokenSnapshot): void;
|
|
24
|
-
/** Get the most recent snapshot per active agent (one row per agent).
|
|
25
|
-
|
|
24
|
+
/** Get the most recent snapshot per active agent (one row per agent).
|
|
25
|
+
* When runId is provided, restricts to snapshots recorded for that run. */
|
|
26
|
+
getLatestSnapshots(runId?: string): TokenSnapshot[];
|
|
26
27
|
/** Get the timestamp of the most recent snapshot for an agent, or null. */
|
|
27
28
|
getLatestSnapshotTime(agentName: string): string | null;
|
|
28
29
|
/** Delete snapshots matching criteria. Returns number of rows deleted. */
|
|
@@ -60,6 +61,7 @@ interface SnapshotRow {
|
|
|
60
61
|
cache_creation_tokens: number;
|
|
61
62
|
estimated_cost_usd: number | null;
|
|
62
63
|
model_used: string | null;
|
|
64
|
+
run_id: string | null;
|
|
63
65
|
created_at: string;
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -94,6 +96,7 @@ CREATE TABLE IF NOT EXISTS token_snapshots (
|
|
|
94
96
|
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
|
95
97
|
estimated_cost_usd REAL,
|
|
96
98
|
model_used TEXT,
|
|
99
|
+
run_id TEXT,
|
|
97
100
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
|
|
98
101
|
)`;
|
|
99
102
|
|
|
@@ -136,6 +139,18 @@ function migrateRunIdColumn(db: Database): void {
|
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Migrate an existing token_snapshots table to include the run_id column.
|
|
144
|
+
* Safe to call multiple times — only adds the column if missing.
|
|
145
|
+
*/
|
|
146
|
+
function migrateSnapshotRunIdColumn(db: Database): void {
|
|
147
|
+
const rows = db.prepare("PRAGMA table_info(token_snapshots)").all() as Array<{ name: string }>;
|
|
148
|
+
const existingColumns = new Set(rows.map((r) => r.name));
|
|
149
|
+
if (!existingColumns.has("run_id")) {
|
|
150
|
+
db.exec("ALTER TABLE token_snapshots ADD COLUMN run_id TEXT");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
139
154
|
/**
|
|
140
155
|
* Migrate an existing sessions table to include token columns.
|
|
141
156
|
* Safe to call multiple times — only adds columns that are missing.
|
|
@@ -183,6 +198,7 @@ function rowToSnapshot(row: SnapshotRow): TokenSnapshot {
|
|
|
183
198
|
cacheCreationTokens: row.cache_creation_tokens,
|
|
184
199
|
estimatedCostUsd: row.estimated_cost_usd,
|
|
185
200
|
modelUsed: row.model_used,
|
|
201
|
+
runId: row.run_id,
|
|
186
202
|
createdAt: row.created_at,
|
|
187
203
|
};
|
|
188
204
|
}
|
|
@@ -210,6 +226,7 @@ export function createMetricsStore(dbPath: string): MetricsStore {
|
|
|
210
226
|
migrateBeadIdToTaskId(db);
|
|
211
227
|
migrateTokenColumns(db);
|
|
212
228
|
migrateRunIdColumn(db);
|
|
229
|
+
migrateSnapshotRunIdColumn(db);
|
|
213
230
|
|
|
214
231
|
// Prepare statements for all queries
|
|
215
232
|
const insertStmt = db.prepare<
|
|
@@ -282,13 +299,14 @@ export function createMetricsStore(dbPath: string): MetricsStore {
|
|
|
282
299
|
$cache_creation_tokens: number;
|
|
283
300
|
$estimated_cost_usd: number | null;
|
|
284
301
|
$model_used: string | null;
|
|
302
|
+
$run_id: string | null;
|
|
285
303
|
$created_at: string;
|
|
286
304
|
}
|
|
287
305
|
>(`
|
|
288
306
|
INSERT INTO token_snapshots
|
|
289
|
-
(agent_name, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, estimated_cost_usd, model_used, created_at)
|
|
307
|
+
(agent_name, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, estimated_cost_usd, model_used, run_id, created_at)
|
|
290
308
|
VALUES
|
|
291
|
-
($agent_name, $input_tokens, $output_tokens, $cache_read_tokens, $cache_creation_tokens, $estimated_cost_usd, $model_used, $created_at)
|
|
309
|
+
($agent_name, $input_tokens, $output_tokens, $cache_read_tokens, $cache_creation_tokens, $estimated_cost_usd, $model_used, $run_id, $created_at)
|
|
292
310
|
`);
|
|
293
311
|
|
|
294
312
|
const latestSnapshotsStmt = db.prepare<SnapshotRow, Record<string, never>>(`
|
|
@@ -301,6 +319,18 @@ export function createMetricsStore(dbPath: string): MetricsStore {
|
|
|
301
319
|
) latest ON s.agent_name = latest.agent_name AND s.created_at = latest.max_created_at
|
|
302
320
|
`);
|
|
303
321
|
|
|
322
|
+
const latestSnapshotsByRunStmt = db.prepare<SnapshotRow, { $run_id: string }>(`
|
|
323
|
+
SELECT s.*
|
|
324
|
+
FROM token_snapshots s
|
|
325
|
+
INNER JOIN (
|
|
326
|
+
SELECT agent_name, MAX(created_at) as max_created_at
|
|
327
|
+
FROM token_snapshots
|
|
328
|
+
WHERE run_id = $run_id
|
|
329
|
+
GROUP BY agent_name
|
|
330
|
+
) latest ON s.agent_name = latest.agent_name AND s.created_at = latest.max_created_at
|
|
331
|
+
WHERE s.run_id = $run_id
|
|
332
|
+
`);
|
|
333
|
+
|
|
304
334
|
const latestSnapshotTimeStmt = db.prepare<
|
|
305
335
|
{ created_at: string } | null,
|
|
306
336
|
{ $agent_name: string }
|
|
@@ -401,11 +431,16 @@ export function createMetricsStore(dbPath: string): MetricsStore {
|
|
|
401
431
|
$cache_creation_tokens: snapshot.cacheCreationTokens,
|
|
402
432
|
$estimated_cost_usd: snapshot.estimatedCostUsd,
|
|
403
433
|
$model_used: snapshot.modelUsed,
|
|
434
|
+
$run_id: snapshot.runId,
|
|
404
435
|
$created_at: snapshot.createdAt,
|
|
405
436
|
});
|
|
406
437
|
},
|
|
407
438
|
|
|
408
|
-
getLatestSnapshots(): TokenSnapshot[] {
|
|
439
|
+
getLatestSnapshots(runId?: string): TokenSnapshot[] {
|
|
440
|
+
if (runId !== undefined) {
|
|
441
|
+
const rows = latestSnapshotsByRunStmt.all({ $run_id: runId });
|
|
442
|
+
return rows.map(rowToSnapshot);
|
|
443
|
+
}
|
|
409
444
|
const rows = latestSnapshotsStmt.all({});
|
|
410
445
|
return rows.map(rowToSnapshot);
|
|
411
446
|
},
|
package/src/types.ts
CHANGED
|
@@ -105,6 +105,13 @@ export interface OverstoryConfig {
|
|
|
105
105
|
printCommand?: string;
|
|
106
106
|
/** Pi runtime configuration for model alias expansion. */
|
|
107
107
|
pi?: PiRuntimeConfig;
|
|
108
|
+
/**
|
|
109
|
+
* Delay in milliseconds between creating a tmux session and polling
|
|
110
|
+
* for TUI readiness. Gives slow shells (oh-my-zsh, starship, etc.)
|
|
111
|
+
* time to finish initializing before the agent command starts.
|
|
112
|
+
* Default: 0 (no delay).
|
|
113
|
+
*/
|
|
114
|
+
shellInitDelayMs?: number;
|
|
108
115
|
};
|
|
109
116
|
}
|
|
110
117
|
|
|
@@ -435,6 +442,7 @@ export interface TokenSnapshot {
|
|
|
435
442
|
estimatedCostUsd: number | null;
|
|
436
443
|
modelUsed: string | null;
|
|
437
444
|
createdAt: string;
|
|
445
|
+
runId: string | null;
|
|
438
446
|
}
|
|
439
447
|
|
|
440
448
|
// === Task Groups (Batch Coordination) ===
|
|
@@ -3,6 +3,7 @@ import { AgentError } from "../errors.ts";
|
|
|
3
3
|
import type { ReadyState } from "../runtimes/types.ts";
|
|
4
4
|
import {
|
|
5
5
|
capturePaneContent,
|
|
6
|
+
checkSessionState,
|
|
6
7
|
createSession,
|
|
7
8
|
ensureTmuxAvailable,
|
|
8
9
|
getDescendantPids,
|
|
@@ -792,6 +793,54 @@ describe("isSessionAlive", () => {
|
|
|
792
793
|
});
|
|
793
794
|
});
|
|
794
795
|
|
|
796
|
+
describe("checkSessionState", () => {
|
|
797
|
+
let spawnSpy: ReturnType<typeof spyOn>;
|
|
798
|
+
|
|
799
|
+
beforeEach(() => {
|
|
800
|
+
spawnSpy = spyOn(Bun, "spawn");
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
afterEach(() => {
|
|
804
|
+
spawnSpy.mockRestore();
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("returns alive when tmux has-session succeeds", async () => {
|
|
808
|
+
spawnSpy.mockReturnValue(mockSpawnResult("", "", 0));
|
|
809
|
+
const state = await checkSessionState("overstory-test-coordinator");
|
|
810
|
+
expect(state).toBe("alive");
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test("returns no_server when tmux reports no server running", async () => {
|
|
814
|
+
spawnSpy.mockReturnValue(
|
|
815
|
+
mockSpawnResult("", "no server running on /tmp/tmux-1000/default\n", 1),
|
|
816
|
+
);
|
|
817
|
+
const state = await checkSessionState("overstory-test-coordinator");
|
|
818
|
+
expect(state).toBe("no_server");
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("returns no_server when tmux reports no sessions", async () => {
|
|
822
|
+
spawnSpy.mockReturnValue(mockSpawnResult("", "no sessions\n", 1));
|
|
823
|
+
const state = await checkSessionState("overstory-test-coordinator");
|
|
824
|
+
expect(state).toBe("no_server");
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
test("returns dead when session not found", async () => {
|
|
828
|
+
spawnSpy.mockReturnValue(
|
|
829
|
+
mockSpawnResult("", "can't find session: overstory-test-coordinator\n", 1),
|
|
830
|
+
);
|
|
831
|
+
const state = await checkSessionState("overstory-test-coordinator");
|
|
832
|
+
expect(state).toBe("dead");
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
test("returns dead for generic tmux failure", async () => {
|
|
836
|
+
spawnSpy.mockReturnValue(
|
|
837
|
+
mockSpawnResult("", "error connecting to /tmp/tmux-1000/default\n", 1),
|
|
838
|
+
);
|
|
839
|
+
const state = await checkSessionState("overstory-test-coordinator");
|
|
840
|
+
expect(state).toBe("dead");
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
795
844
|
describe("sendKeys", () => {
|
|
796
845
|
let spawnSpy: ReturnType<typeof spyOn>;
|
|
797
846
|
|
package/src/worktree/tmux.ts
CHANGED
|
@@ -409,6 +409,39 @@ export async function isSessionAlive(name: string): Promise<boolean> {
|
|
|
409
409
|
return exitCode === 0;
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Detailed session state for distinguishing failure modes.
|
|
414
|
+
*
|
|
415
|
+
* - `"alive"` -- tmux session exists and is reachable.
|
|
416
|
+
* - `"dead"` -- tmux server is running but the session does not exist.
|
|
417
|
+
* - `"no_server"` -- tmux server is not running at all.
|
|
418
|
+
*/
|
|
419
|
+
export type SessionState = "alive" | "dead" | "no_server";
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Check tmux session state with detailed failure mode reporting.
|
|
423
|
+
*
|
|
424
|
+
* Unlike `isSessionAlive()` which returns a simple boolean, this function
|
|
425
|
+
* distinguishes between three states:
|
|
426
|
+
* - `"alive"`: session exists -- the agent may still be running.
|
|
427
|
+
* - `"dead"`: tmux server is running but session is gone -- agent exited or was killed.
|
|
428
|
+
* - `"no_server"`: tmux server itself is not running -- all sessions are gone.
|
|
429
|
+
*
|
|
430
|
+
* Callers can use this to provide targeted error messages and decide whether
|
|
431
|
+
* stale session records should be cleaned up vs flagged as errors.
|
|
432
|
+
*
|
|
433
|
+
* @param name - Session name to check
|
|
434
|
+
* @returns The session state
|
|
435
|
+
*/
|
|
436
|
+
export async function checkSessionState(name: string): Promise<SessionState> {
|
|
437
|
+
const { exitCode, stderr } = await runCommand(["tmux", "has-session", "-t", name]);
|
|
438
|
+
if (exitCode === 0) return "alive";
|
|
439
|
+
if (stderr.includes("no server running") || stderr.includes("no sessions")) {
|
|
440
|
+
return "no_server";
|
|
441
|
+
}
|
|
442
|
+
return "dead";
|
|
443
|
+
}
|
|
444
|
+
|
|
412
445
|
/**
|
|
413
446
|
* Capture the visible content of a tmux session's pane.
|
|
414
447
|
*
|