@foothill/agent-move 1.0.10 → 1.0.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/.github/screenshot.png +0 -0
- package/package.json +1 -1
- package/packages/client/dist/assets/{BufferResource-Dfd5uHKt.js → BufferResource-Dhljy8H8.js} +1 -1
- package/packages/client/dist/assets/{CanvasRenderer-7Cv6xZVP.js → CanvasRenderer-Bpr11iOT.js} +1 -1
- package/packages/client/dist/assets/{Filter-CBX7EB7j.js → Filter-DL2yN3-o.js} +1 -1
- package/packages/client/dist/assets/{RenderTargetSystem-ko-v73NG.js → RenderTargetSystem-BTwylEdr.js} +1 -1
- package/packages/client/dist/assets/{WebGLRenderer-vhPQEPUG.js → WebGLRenderer-wH1P7d1x.js} +1 -1
- package/packages/client/dist/assets/{WebGPURenderer-Dwywvwqe.js → WebGPURenderer-C7n8jUXC.js} +1 -1
- package/packages/client/dist/assets/{browserAll-QyCAT8_K.js → browserAll-CgAMpWnT.js} +1 -1
- package/packages/client/dist/assets/index-DG7HqEmM.js +1338 -0
- package/packages/client/dist/assets/index-Nz5TZeB1.css +1 -0
- package/packages/client/dist/assets/{webworkerAll-hM-gNP7L.js → webworkerAll-wrP2P1GC.js} +1 -1
- package/packages/client/dist/index.html +16 -2
- package/packages/server/dist/index.d.ts.map +1 -1
- package/packages/server/dist/index.js +954 -31
- package/packages/server/dist/index.js.map +1 -1
- package/packages/server/dist/routes/sessions-api.d.ts +5 -0
- package/packages/server/dist/routes/sessions-api.d.ts.map +1 -0
- package/packages/server/dist/routes/sessions-api.js +88 -0
- package/packages/server/dist/routes/sessions-api.js.map +1 -0
- package/packages/server/dist/state/activity-processor.d.ts.map +1 -1
- package/packages/server/dist/state/activity-processor.js +0 -2
- package/packages/server/dist/state/activity-processor.js.map +1 -1
- package/packages/server/dist/state/agent-state-manager.d.ts.map +1 -1
- package/packages/server/dist/state/agent-state-manager.js +3 -5
- package/packages/server/dist/state/agent-state-manager.js.map +1 -1
- package/packages/server/dist/state/identity-manager.d.ts.map +1 -1
- package/packages/server/dist/state/identity-manager.js +0 -3
- package/packages/server/dist/state/identity-manager.js.map +1 -1
- package/packages/server/dist/storage/session-recorder.d.ts +38 -0
- package/packages/server/dist/storage/session-recorder.d.ts.map +1 -0
- package/packages/server/dist/storage/session-recorder.js +941 -0
- package/packages/server/dist/storage/session-recorder.js.map +1 -0
- package/packages/server/dist/storage/session-store.d.ts +60 -0
- package/packages/server/dist/storage/session-store.d.ts.map +1 -0
- package/packages/server/dist/storage/session-store.js +330 -0
- package/packages/server/dist/storage/session-store.js.map +1 -0
- package/packages/server/dist/watcher/opencode/opencode-watcher.d.ts +15 -0
- package/packages/server/dist/watcher/opencode/opencode-watcher.d.ts.map +1 -1
- package/packages/server/dist/watcher/opencode/opencode-watcher.js +61 -4
- package/packages/server/dist/watcher/opencode/opencode-watcher.js.map +1 -1
- package/packages/server/dist/ws/broadcaster.d.ts.map +1 -1
- package/packages/server/dist/ws/broadcaster.js +3 -18
- package/packages/server/dist/ws/broadcaster.js.map +1 -1
- package/packages/shared/dist/constants/tools.d.ts +4 -0
- package/packages/shared/dist/constants/tools.d.ts.map +1 -1
- package/packages/shared/dist/constants/tools.js +4 -0
- package/packages/shared/dist/constants/tools.js.map +1 -1
- package/packages/shared/dist/index.d.ts +2 -1
- package/packages/shared/dist/index.d.ts.map +1 -1
- package/packages/shared/dist/index.js +1 -1
- package/packages/shared/dist/index.js.map +1 -1
- package/packages/shared/dist/types/session-record.d.ts +87 -0
- package/packages/shared/dist/types/session-record.d.ts.map +1 -0
- package/packages/shared/dist/types/session-record.js +2 -0
- package/packages/shared/dist/types/session-record.js.map +1 -0
- package/packages/shared/dist/types/websocket.d.ts +3 -0
- package/packages/shared/dist/types/websocket.d.ts.map +1 -1
- package/packages/client/dist/assets/index-BPJtz4FL.js +0 -722
- package/packages/client/dist/assets/index-CMmR_RuS.css +0 -1
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
// dist/storage/session-recorder.js
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { mkdirSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
var ZONES = [
|
|
8
|
+
// Row 0
|
|
9
|
+
{
|
|
10
|
+
id: "search",
|
|
11
|
+
label: "Search",
|
|
12
|
+
description: "Grep, WebSearch \u2014 Research & lookup",
|
|
13
|
+
icon: "\u{1F4DA}",
|
|
14
|
+
color: 15381256,
|
|
15
|
+
colStart: 0,
|
|
16
|
+
colSpan: 5,
|
|
17
|
+
rowStart: 0,
|
|
18
|
+
rowSpan: 1,
|
|
19
|
+
x: 0,
|
|
20
|
+
y: 0,
|
|
21
|
+
width: 0,
|
|
22
|
+
height: 0
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "terminal",
|
|
26
|
+
label: "Terminal",
|
|
27
|
+
description: "Bash commands \u2014 Server room",
|
|
28
|
+
icon: "\u{1F4BB}",
|
|
29
|
+
color: 2278750,
|
|
30
|
+
colStart: 5,
|
|
31
|
+
colSpan: 3,
|
|
32
|
+
rowStart: 0,
|
|
33
|
+
rowSpan: 1,
|
|
34
|
+
x: 0,
|
|
35
|
+
y: 0,
|
|
36
|
+
width: 0,
|
|
37
|
+
height: 0
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "web",
|
|
41
|
+
label: "Web",
|
|
42
|
+
description: "WebFetch, Browser \u2014 Network hub",
|
|
43
|
+
icon: "\u{1F310}",
|
|
44
|
+
color: 9133302,
|
|
45
|
+
colStart: 8,
|
|
46
|
+
colSpan: 4,
|
|
47
|
+
rowStart: 0,
|
|
48
|
+
rowSpan: 1,
|
|
49
|
+
x: 0,
|
|
50
|
+
y: 0,
|
|
51
|
+
width: 0,
|
|
52
|
+
height: 0
|
|
53
|
+
},
|
|
54
|
+
// Row 1
|
|
55
|
+
{
|
|
56
|
+
id: "files",
|
|
57
|
+
label: "Files",
|
|
58
|
+
description: "Read, Write, Edit, Glob \u2014 File storage",
|
|
59
|
+
icon: "\u{1F4C1}",
|
|
60
|
+
color: 3900150,
|
|
61
|
+
colStart: 0,
|
|
62
|
+
colSpan: 4,
|
|
63
|
+
rowStart: 1,
|
|
64
|
+
rowSpan: 1,
|
|
65
|
+
x: 0,
|
|
66
|
+
y: 0,
|
|
67
|
+
width: 0,
|
|
68
|
+
height: 0
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "thinking",
|
|
72
|
+
label: "Thinking",
|
|
73
|
+
description: "Planning, Questions \u2014 Conference area",
|
|
74
|
+
icon: "\u{1F4AD}",
|
|
75
|
+
color: 16347926,
|
|
76
|
+
colStart: 4,
|
|
77
|
+
colSpan: 5,
|
|
78
|
+
rowStart: 1,
|
|
79
|
+
rowSpan: 1,
|
|
80
|
+
x: 0,
|
|
81
|
+
y: 0,
|
|
82
|
+
width: 0,
|
|
83
|
+
height: 0
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "messaging",
|
|
87
|
+
label: "Messaging",
|
|
88
|
+
description: "SendMessage, Teams \u2014 Chat & relax",
|
|
89
|
+
icon: "\u{1F4AC}",
|
|
90
|
+
color: 15485081,
|
|
91
|
+
colStart: 9,
|
|
92
|
+
colSpan: 3,
|
|
93
|
+
rowStart: 1,
|
|
94
|
+
rowSpan: 1,
|
|
95
|
+
x: 0,
|
|
96
|
+
y: 0,
|
|
97
|
+
width: 0,
|
|
98
|
+
height: 0
|
|
99
|
+
},
|
|
100
|
+
// Row 2
|
|
101
|
+
{
|
|
102
|
+
id: "spawn",
|
|
103
|
+
label: "Spawn",
|
|
104
|
+
description: "Agent spawn/despawn \u2014 Entry portal",
|
|
105
|
+
icon: "\u{1F300}",
|
|
106
|
+
color: 11032055,
|
|
107
|
+
colStart: 0,
|
|
108
|
+
colSpan: 3,
|
|
109
|
+
rowStart: 2,
|
|
110
|
+
rowSpan: 1,
|
|
111
|
+
x: 0,
|
|
112
|
+
y: 0,
|
|
113
|
+
width: 0,
|
|
114
|
+
height: 0
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "idle",
|
|
118
|
+
label: "Idle",
|
|
119
|
+
description: "Idle agents rest here \u2014 Kitchen & lounge",
|
|
120
|
+
icon: "\u2615",
|
|
121
|
+
color: 7041664,
|
|
122
|
+
colStart: 3,
|
|
123
|
+
colSpan: 5,
|
|
124
|
+
rowStart: 2,
|
|
125
|
+
rowSpan: 1,
|
|
126
|
+
x: 0,
|
|
127
|
+
y: 0,
|
|
128
|
+
width: 0,
|
|
129
|
+
height: 0
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "tasks",
|
|
133
|
+
label: "Tasks",
|
|
134
|
+
description: "TaskCreate, TaskUpdate \u2014 Kanban & planning",
|
|
135
|
+
icon: "\u{1F4CB}",
|
|
136
|
+
color: 1357990,
|
|
137
|
+
colStart: 8,
|
|
138
|
+
colSpan: 4,
|
|
139
|
+
rowStart: 2,
|
|
140
|
+
rowSpan: 1,
|
|
141
|
+
x: 0,
|
|
142
|
+
y: 0,
|
|
143
|
+
width: 0,
|
|
144
|
+
height: 0
|
|
145
|
+
}
|
|
146
|
+
];
|
|
147
|
+
var ZONE_MAP = new Map(ZONES.map((z) => [z.id, z]));
|
|
148
|
+
var MODEL_PRICING = {
|
|
149
|
+
// ── Anthropic Claude ────────────────────────────────────────────────────────
|
|
150
|
+
"claude-opus-4-6": { input: 15, output: 75 },
|
|
151
|
+
"claude-opus-4-5-20250620": { input: 15, output: 75 },
|
|
152
|
+
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
153
|
+
"claude-sonnet-4-5-20250514": { input: 3, output: 15 },
|
|
154
|
+
"claude-sonnet-4-0-20250514": { input: 3, output: 15 },
|
|
155
|
+
"claude-haiku-4-5-20251001": { input: 1, output: 5 },
|
|
156
|
+
"claude-3-7-sonnet-20250219": { input: 3, output: 15 },
|
|
157
|
+
"claude-3-5-sonnet-20241022": { input: 3, output: 15 },
|
|
158
|
+
"claude-3-5-haiku-20241022": { input: 1, output: 5 },
|
|
159
|
+
"claude-3-opus-20240229": { input: 15, output: 75 },
|
|
160
|
+
// ── OpenAI ──────────────────────────────────────────────────────────────────
|
|
161
|
+
"gpt-4o": { input: 2.5, output: 10 },
|
|
162
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
163
|
+
"gpt-4-turbo": { input: 10, output: 30 },
|
|
164
|
+
"gpt-4": { input: 30, output: 60 },
|
|
165
|
+
"o1": { input: 15, output: 60 },
|
|
166
|
+
"o1-mini": { input: 1.1, output: 4.4 },
|
|
167
|
+
"o3": { input: 10, output: 40 },
|
|
168
|
+
"o3-mini": { input: 1.1, output: 4.4 },
|
|
169
|
+
"o4-mini": { input: 1.1, output: 4.4 },
|
|
170
|
+
// ── Google Gemini ───────────────────────────────────────────────────────────
|
|
171
|
+
"gemini-2.5-pro": { input: 1.25, output: 10 },
|
|
172
|
+
"gemini-2.5-flash": { input: 0.15, output: 0.6 },
|
|
173
|
+
"gemini-2.0-flash": { input: 0.1, output: 0.4 },
|
|
174
|
+
"gemini-2.0-flash-lite": { input: 0.075, output: 0.3 },
|
|
175
|
+
"gemini-1.5-pro": { input: 1.25, output: 5 },
|
|
176
|
+
"gemini-1.5-flash": { input: 0.075, output: 0.3 },
|
|
177
|
+
// ── DeepSeek ────────────────────────────────────────────────────────────────
|
|
178
|
+
"deepseek-chat": { input: 0.27, output: 1.1 },
|
|
179
|
+
// DeepSeek V3
|
|
180
|
+
"deepseek-reasoner": { input: 0.55, output: 2.19 },
|
|
181
|
+
// DeepSeek R1
|
|
182
|
+
// ── xAI Grok ────────────────────────────────────────────────────────────────
|
|
183
|
+
"grok-3": { input: 3, output: 15 },
|
|
184
|
+
"grok-3-mini": { input: 0.3, output: 0.5 },
|
|
185
|
+
"grok-2": { input: 2, output: 10 },
|
|
186
|
+
// ── Mistral ─────────────────────────────────────────────────────────────────
|
|
187
|
+
"mistral-large": { input: 2, output: 6 },
|
|
188
|
+
"mistral-small": { input: 0.1, output: 0.3 },
|
|
189
|
+
"codestral": { input: 0.1, output: 0.3 }
|
|
190
|
+
};
|
|
191
|
+
var DEFAULT_PRICING = { input: 3, output: 15 };
|
|
192
|
+
function getModelPricing(model) {
|
|
193
|
+
if (!model)
|
|
194
|
+
return DEFAULT_PRICING;
|
|
195
|
+
const m = model.toLowerCase();
|
|
196
|
+
if (MODEL_PRICING[m])
|
|
197
|
+
return MODEL_PRICING[m];
|
|
198
|
+
for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
|
|
199
|
+
if (m.startsWith(key) || m.includes(key))
|
|
200
|
+
return pricing;
|
|
201
|
+
}
|
|
202
|
+
if (m.includes("opus"))
|
|
203
|
+
return MODEL_PRICING["claude-opus-4-6"];
|
|
204
|
+
if (m.includes("haiku"))
|
|
205
|
+
return MODEL_PRICING["claude-haiku-4-5-20251001"];
|
|
206
|
+
if (m.includes("sonnet"))
|
|
207
|
+
return MODEL_PRICING["claude-sonnet-4-6"];
|
|
208
|
+
if (m.includes("o3-mini") || m.includes("o4-mini"))
|
|
209
|
+
return MODEL_PRICING["o3-mini"];
|
|
210
|
+
if (m.includes("o1-mini"))
|
|
211
|
+
return MODEL_PRICING["o1-mini"];
|
|
212
|
+
if (m.includes("gemini-2.5"))
|
|
213
|
+
return MODEL_PRICING["gemini-2.5-pro"];
|
|
214
|
+
if (m.includes("gemini-2"))
|
|
215
|
+
return MODEL_PRICING["gemini-2.0-flash"];
|
|
216
|
+
if (m.includes("gemini"))
|
|
217
|
+
return MODEL_PRICING["gemini-1.5-pro"];
|
|
218
|
+
if (m.includes("gpt-4o"))
|
|
219
|
+
return MODEL_PRICING["gpt-4o"];
|
|
220
|
+
if (m.includes("deepseek"))
|
|
221
|
+
return MODEL_PRICING["deepseek-chat"];
|
|
222
|
+
if (m.includes("grok"))
|
|
223
|
+
return MODEL_PRICING["grok-3"];
|
|
224
|
+
if (m.includes("mistral"))
|
|
225
|
+
return MODEL_PRICING["mistral-large"];
|
|
226
|
+
return DEFAULT_PRICING;
|
|
227
|
+
}
|
|
228
|
+
function computeAgentCost(tokens) {
|
|
229
|
+
const pricing = getModelPricing(tokens.model);
|
|
230
|
+
return tokens.totalInputTokens / 1e6 * pricing.input + tokens.totalOutputTokens / 1e6 * pricing.output + tokens.cacheReadTokens / 1e6 * pricing.input * 0.1 + tokens.cacheCreationTokens / 1e6 * pricing.input * 1.25;
|
|
231
|
+
}
|
|
232
|
+
var DB_DIR = join(homedir(), ".agent-move");
|
|
233
|
+
var DB_PATH = join(DB_DIR, "sessions.db");
|
|
234
|
+
var SCHEMA_VERSION = 2;
|
|
235
|
+
var SessionStore = class {
|
|
236
|
+
db;
|
|
237
|
+
constructor(dbPath) {
|
|
238
|
+
mkdirSync(DB_DIR, { recursive: true });
|
|
239
|
+
this.db = new Database(dbPath ?? DB_PATH);
|
|
240
|
+
this.db.pragma("journal_mode = WAL");
|
|
241
|
+
this.db.pragma("busy_timeout = 5000");
|
|
242
|
+
this.initSchema();
|
|
243
|
+
}
|
|
244
|
+
initSchema() {
|
|
245
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)`);
|
|
246
|
+
const row = this.db.prepare("SELECT version FROM schema_version").get();
|
|
247
|
+
const currentVersion = row?.version ?? 0;
|
|
248
|
+
if (currentVersion < SCHEMA_VERSION) {
|
|
249
|
+
this.db.exec(`
|
|
250
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
251
|
+
id TEXT PRIMARY KEY,
|
|
252
|
+
source TEXT NOT NULL,
|
|
253
|
+
root_session_id TEXT NOT NULL,
|
|
254
|
+
project_name TEXT NOT NULL,
|
|
255
|
+
project_path TEXT NOT NULL,
|
|
256
|
+
started_at INTEGER NOT NULL,
|
|
257
|
+
ended_at INTEGER NOT NULL,
|
|
258
|
+
duration_ms INTEGER NOT NULL,
|
|
259
|
+
total_cost REAL NOT NULL DEFAULT 0,
|
|
260
|
+
total_input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
261
|
+
total_output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
262
|
+
total_cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
263
|
+
total_cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
|
264
|
+
total_tool_uses INTEGER NOT NULL DEFAULT 0,
|
|
265
|
+
agent_count INTEGER NOT NULL DEFAULT 0,
|
|
266
|
+
model TEXT,
|
|
267
|
+
label TEXT,
|
|
268
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
269
|
+
agents_json TEXT NOT NULL DEFAULT '[]',
|
|
270
|
+
tool_chain_json TEXT NOT NULL DEFAULT '{}'
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
CREATE TABLE IF NOT EXISTS timeline_events (
|
|
274
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
275
|
+
session_id TEXT NOT NULL,
|
|
276
|
+
timestamp INTEGER NOT NULL,
|
|
277
|
+
agent_id TEXT NOT NULL,
|
|
278
|
+
kind TEXT NOT NULL,
|
|
279
|
+
zone TEXT,
|
|
280
|
+
tool TEXT,
|
|
281
|
+
tool_args TEXT,
|
|
282
|
+
text_content TEXT,
|
|
283
|
+
input_tokens INTEGER,
|
|
284
|
+
output_tokens INTEGER
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
-- Live (in-progress) sessions: written incrementally so data survives crashes
|
|
288
|
+
CREATE TABLE IF NOT EXISTS live_sessions (
|
|
289
|
+
root_session_id TEXT PRIMARY KEY,
|
|
290
|
+
session_id TEXT NOT NULL,
|
|
291
|
+
source TEXT NOT NULL DEFAULT 'claude',
|
|
292
|
+
project_name TEXT NOT NULL,
|
|
293
|
+
project_path TEXT NOT NULL,
|
|
294
|
+
started_at INTEGER NOT NULL,
|
|
295
|
+
last_activity_at INTEGER NOT NULL,
|
|
296
|
+
agents_json TEXT NOT NULL DEFAULT '[]'
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
CREATE TABLE IF NOT EXISTS live_timeline_events (
|
|
300
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
301
|
+
root_session_id TEXT NOT NULL,
|
|
302
|
+
timestamp INTEGER NOT NULL,
|
|
303
|
+
agent_id TEXT NOT NULL,
|
|
304
|
+
kind TEXT NOT NULL,
|
|
305
|
+
zone TEXT,
|
|
306
|
+
tool TEXT,
|
|
307
|
+
tool_args TEXT,
|
|
308
|
+
text_content TEXT,
|
|
309
|
+
input_tokens INTEGER,
|
|
310
|
+
output_tokens INTEGER
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
CREATE INDEX IF NOT EXISTS idx_timeline_session ON timeline_events(session_id);
|
|
314
|
+
CREATE INDEX IF NOT EXISTS idx_timeline_timestamp ON timeline_events(session_id, timestamp);
|
|
315
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_name);
|
|
316
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
|
|
317
|
+
CREATE INDEX IF NOT EXISTS idx_live_timeline_root ON live_timeline_events(root_session_id);
|
|
318
|
+
`);
|
|
319
|
+
if (currentVersion === 0) {
|
|
320
|
+
this.db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(SCHEMA_VERSION);
|
|
321
|
+
} else {
|
|
322
|
+
this.db.prepare("UPDATE schema_version SET version = ?").run(SCHEMA_VERSION);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** Save a complete recorded session with its timeline */
|
|
327
|
+
saveSession(session, timeline) {
|
|
328
|
+
const insertSession = this.db.prepare(`
|
|
329
|
+
INSERT OR REPLACE INTO sessions (
|
|
330
|
+
id, source, root_session_id, project_name, project_path,
|
|
331
|
+
started_at, ended_at, duration_ms,
|
|
332
|
+
total_cost, total_input_tokens, total_output_tokens,
|
|
333
|
+
total_cache_read_tokens, total_cache_creation_tokens,
|
|
334
|
+
total_tool_uses, agent_count, model, label, tags,
|
|
335
|
+
agents_json, tool_chain_json
|
|
336
|
+
) VALUES (
|
|
337
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
338
|
+
)
|
|
339
|
+
`);
|
|
340
|
+
const insertEvent = this.db.prepare(`
|
|
341
|
+
INSERT INTO timeline_events (
|
|
342
|
+
session_id, timestamp, agent_id, kind, zone, tool, tool_args,
|
|
343
|
+
text_content, input_tokens, output_tokens
|
|
344
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
345
|
+
`);
|
|
346
|
+
const txn = this.db.transaction(() => {
|
|
347
|
+
this.db.prepare("DELETE FROM timeline_events WHERE session_id = ?").run(session.id);
|
|
348
|
+
insertSession.run(session.id, session.source, session.rootSessionId, session.projectName, session.projectPath, session.startedAt, session.endedAt, session.durationMs, session.totalCost, session.totalInputTokens, session.totalOutputTokens, session.totalCacheReadTokens, session.totalCacheCreationTokens, session.totalToolUses, session.agentCount, session.model, session.label, JSON.stringify(session.tags), JSON.stringify(session.agents), JSON.stringify(session.toolChain));
|
|
349
|
+
for (const evt of timeline) {
|
|
350
|
+
insertEvent.run(session.id, evt.timestamp, evt.agentId, evt.kind, evt.zone ?? null, evt.tool ?? null, evt.toolArgs ?? null, evt.text ?? null, evt.inputTokens ?? null, evt.outputTokens ?? null);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
txn();
|
|
354
|
+
}
|
|
355
|
+
/** List sessions with optional filtering */
|
|
356
|
+
listSessions(opts) {
|
|
357
|
+
const limit = opts?.limit ?? 50;
|
|
358
|
+
const offset = opts?.offset ?? 0;
|
|
359
|
+
let sql = `SELECT id, source, project_name, started_at, ended_at, duration_ms,
|
|
360
|
+
total_cost, total_tool_uses, agent_count, model, label, tags
|
|
361
|
+
FROM sessions`;
|
|
362
|
+
const params = [];
|
|
363
|
+
if (opts?.project) {
|
|
364
|
+
sql += " WHERE project_name = ?";
|
|
365
|
+
params.push(opts.project);
|
|
366
|
+
}
|
|
367
|
+
sql += " ORDER BY started_at DESC LIMIT ? OFFSET ?";
|
|
368
|
+
params.push(limit, offset);
|
|
369
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
370
|
+
return rows.map((r) => ({
|
|
371
|
+
id: r.id,
|
|
372
|
+
source: r.source,
|
|
373
|
+
projectName: r.project_name,
|
|
374
|
+
startedAt: r.started_at,
|
|
375
|
+
endedAt: r.ended_at,
|
|
376
|
+
durationMs: r.duration_ms,
|
|
377
|
+
totalCost: r.total_cost,
|
|
378
|
+
totalToolUses: r.total_tool_uses,
|
|
379
|
+
agentCount: r.agent_count,
|
|
380
|
+
model: r.model,
|
|
381
|
+
label: r.label,
|
|
382
|
+
tags: JSON.parse(r.tags)
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
/** Get a full session by ID (without timeline) */
|
|
386
|
+
getSession(id) {
|
|
387
|
+
const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
388
|
+
if (!row)
|
|
389
|
+
return null;
|
|
390
|
+
return {
|
|
391
|
+
id: row.id,
|
|
392
|
+
source: row.source,
|
|
393
|
+
rootSessionId: row.root_session_id,
|
|
394
|
+
projectName: row.project_name,
|
|
395
|
+
projectPath: row.project_path,
|
|
396
|
+
startedAt: row.started_at,
|
|
397
|
+
endedAt: row.ended_at,
|
|
398
|
+
durationMs: row.duration_ms,
|
|
399
|
+
totalCost: row.total_cost,
|
|
400
|
+
totalInputTokens: row.total_input_tokens,
|
|
401
|
+
totalOutputTokens: row.total_output_tokens,
|
|
402
|
+
totalCacheReadTokens: row.total_cache_read_tokens,
|
|
403
|
+
totalCacheCreationTokens: row.total_cache_creation_tokens,
|
|
404
|
+
totalToolUses: row.total_tool_uses,
|
|
405
|
+
agentCount: row.agent_count,
|
|
406
|
+
model: row.model,
|
|
407
|
+
label: row.label,
|
|
408
|
+
tags: JSON.parse(row.tags),
|
|
409
|
+
agents: JSON.parse(row.agents_json),
|
|
410
|
+
toolChain: JSON.parse(row.tool_chain_json)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/** Get timeline events for a session */
|
|
414
|
+
getTimeline(sessionId, opts) {
|
|
415
|
+
const limit = opts?.limit ?? 1e4;
|
|
416
|
+
const offset = opts?.offset ?? 0;
|
|
417
|
+
const rows = this.db.prepare(`
|
|
418
|
+
SELECT timestamp, agent_id, kind, zone, tool, tool_args, text_content,
|
|
419
|
+
input_tokens, output_tokens
|
|
420
|
+
FROM timeline_events
|
|
421
|
+
WHERE session_id = ?
|
|
422
|
+
ORDER BY timestamp ASC
|
|
423
|
+
LIMIT ? OFFSET ?
|
|
424
|
+
`).all(sessionId, limit, offset);
|
|
425
|
+
return rows.map((r) => ({
|
|
426
|
+
timestamp: r.timestamp,
|
|
427
|
+
agentId: r.agent_id,
|
|
428
|
+
kind: r.kind,
|
|
429
|
+
...r.zone && { zone: r.zone },
|
|
430
|
+
...r.tool && { tool: r.tool },
|
|
431
|
+
...r.tool_args && { toolArgs: r.tool_args },
|
|
432
|
+
...r.text_content && { text: r.text_content },
|
|
433
|
+
...r.input_tokens != null && { inputTokens: r.input_tokens },
|
|
434
|
+
...r.output_tokens != null && { outputTokens: r.output_tokens }
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
/** Delete a session and its timeline */
|
|
438
|
+
deleteSession(id) {
|
|
439
|
+
let changed = false;
|
|
440
|
+
this.db.transaction(() => {
|
|
441
|
+
this.db.prepare("DELETE FROM timeline_events WHERE session_id = ?").run(id);
|
|
442
|
+
const result = this.db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
|
|
443
|
+
changed = result.changes > 0;
|
|
444
|
+
})();
|
|
445
|
+
return changed;
|
|
446
|
+
}
|
|
447
|
+
/** Update session label */
|
|
448
|
+
updateLabel(id, label) {
|
|
449
|
+
const result = this.db.prepare("UPDATE sessions SET label = ? WHERE id = ?").run(label, id);
|
|
450
|
+
return result.changes > 0;
|
|
451
|
+
}
|
|
452
|
+
/** Update session tags */
|
|
453
|
+
updateTags(id, tags) {
|
|
454
|
+
const result = this.db.prepare("UPDATE sessions SET tags = ? WHERE id = ?").run(JSON.stringify(tags), id);
|
|
455
|
+
return result.changes > 0;
|
|
456
|
+
}
|
|
457
|
+
/** Get total session count (for pagination) */
|
|
458
|
+
getSessionCount(project) {
|
|
459
|
+
if (project) {
|
|
460
|
+
const row2 = this.db.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_name = ?").get(project);
|
|
461
|
+
return row2.count;
|
|
462
|
+
}
|
|
463
|
+
const row = this.db.prepare("SELECT COUNT(*) as count FROM sessions").get();
|
|
464
|
+
return row.count;
|
|
465
|
+
}
|
|
466
|
+
// ── Live session methods (incremental writes for crash safety) ──
|
|
467
|
+
/** Create or update a live session entry */
|
|
468
|
+
upsertLiveSession(rootSessionId, data) {
|
|
469
|
+
this.db.prepare(`
|
|
470
|
+
INSERT INTO live_sessions (root_session_id, session_id, source, project_name, project_path, started_at, last_activity_at)
|
|
471
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
472
|
+
ON CONFLICT(root_session_id) DO UPDATE SET
|
|
473
|
+
session_id = excluded.session_id,
|
|
474
|
+
source = excluded.source,
|
|
475
|
+
project_name = excluded.project_name,
|
|
476
|
+
project_path = excluded.project_path,
|
|
477
|
+
last_activity_at = excluded.last_activity_at
|
|
478
|
+
`).run(rootSessionId, data.sessionId, data.source, data.projectName, data.projectPath, data.startedAt, Date.now());
|
|
479
|
+
}
|
|
480
|
+
/** Update the agent snapshot for a live session */
|
|
481
|
+
updateLiveAgents(rootSessionId, agentsJson) {
|
|
482
|
+
this.db.prepare(`
|
|
483
|
+
UPDATE live_sessions SET agents_json = ?, last_activity_at = ? WHERE root_session_id = ?
|
|
484
|
+
`).run(agentsJson, Date.now(), rootSessionId);
|
|
485
|
+
}
|
|
486
|
+
/** Append a timeline event to the live buffer */
|
|
487
|
+
appendLiveTimelineEvent(rootSessionId, evt) {
|
|
488
|
+
this.db.prepare(`
|
|
489
|
+
INSERT INTO live_timeline_events (root_session_id, timestamp, agent_id, kind, zone, tool, tool_args, text_content, input_tokens, output_tokens)
|
|
490
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
491
|
+
`).run(rootSessionId, evt.timestamp, evt.agentId, evt.kind, evt.zone ?? null, evt.tool ?? null, evt.toolArgs ?? null, evt.text ?? null, evt.inputTokens ?? null, evt.outputTokens ?? null);
|
|
492
|
+
}
|
|
493
|
+
/** Get all live timeline events for a root session */
|
|
494
|
+
getLiveTimeline(rootSessionId) {
|
|
495
|
+
const rows = this.db.prepare(`
|
|
496
|
+
SELECT timestamp, agent_id, kind, zone, tool, tool_args, text_content, input_tokens, output_tokens
|
|
497
|
+
FROM live_timeline_events WHERE root_session_id = ? ORDER BY timestamp ASC
|
|
498
|
+
`).all(rootSessionId);
|
|
499
|
+
return rows.map((r) => ({
|
|
500
|
+
timestamp: r.timestamp,
|
|
501
|
+
agentId: r.agent_id,
|
|
502
|
+
kind: r.kind,
|
|
503
|
+
...r.zone && { zone: r.zone },
|
|
504
|
+
...r.tool && { tool: r.tool },
|
|
505
|
+
...r.tool_args && { toolArgs: r.tool_args },
|
|
506
|
+
...r.text_content && { text: r.text_content },
|
|
507
|
+
...r.input_tokens != null && { inputTokens: r.input_tokens },
|
|
508
|
+
...r.output_tokens != null && { outputTokens: r.output_tokens }
|
|
509
|
+
}));
|
|
510
|
+
}
|
|
511
|
+
/** Remove live session data after finalization (atomic to prevent orphans on crash) */
|
|
512
|
+
removeLiveSession(rootSessionId) {
|
|
513
|
+
this.db.transaction(() => {
|
|
514
|
+
this.db.prepare("DELETE FROM live_timeline_events WHERE root_session_id = ?").run(rootSessionId);
|
|
515
|
+
this.db.prepare("DELETE FROM live_sessions WHERE root_session_id = ?").run(rootSessionId);
|
|
516
|
+
})();
|
|
517
|
+
}
|
|
518
|
+
/** List currently active (in-progress) live sessions */
|
|
519
|
+
listLiveSessions() {
|
|
520
|
+
const rows = this.db.prepare("SELECT root_session_id, source, project_name, started_at, last_activity_at, agents_json FROM live_sessions ORDER BY started_at DESC").all();
|
|
521
|
+
return rows.map((r) => {
|
|
522
|
+
let agentCount = 0;
|
|
523
|
+
try {
|
|
524
|
+
agentCount = JSON.parse(r.agents_json).length;
|
|
525
|
+
} catch {
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
rootSessionId: r.root_session_id,
|
|
529
|
+
source: r.source,
|
|
530
|
+
projectName: r.project_name,
|
|
531
|
+
startedAt: r.started_at,
|
|
532
|
+
lastActivityAt: r.last_activity_at,
|
|
533
|
+
agentCount
|
|
534
|
+
};
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
/** Get all orphaned live sessions (for crash recovery on startup) */
|
|
538
|
+
getOrphanedLiveSessions() {
|
|
539
|
+
return this.db.prepare("SELECT * FROM live_sessions").all().map((r) => ({
|
|
540
|
+
rootSessionId: r.root_session_id,
|
|
541
|
+
sessionId: r.session_id,
|
|
542
|
+
source: r.source,
|
|
543
|
+
projectName: r.project_name,
|
|
544
|
+
projectPath: r.project_path,
|
|
545
|
+
startedAt: r.started_at,
|
|
546
|
+
lastActivityAt: r.last_activity_at,
|
|
547
|
+
agentsJson: r.agents_json
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
close() {
|
|
551
|
+
this.db.close();
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
var FINALIZE_DELAY_MS = 3e3;
|
|
555
|
+
var AGENT_FLUSH_INTERVAL_MS = 3e4;
|
|
556
|
+
var SessionRecorder = class {
|
|
557
|
+
store;
|
|
558
|
+
staging = /* @__PURE__ */ new Map();
|
|
559
|
+
stateManager;
|
|
560
|
+
flushTimer;
|
|
561
|
+
constructor(stateManager) {
|
|
562
|
+
this.stateManager = stateManager;
|
|
563
|
+
this.store = new SessionStore();
|
|
564
|
+
this.recoverOrphans();
|
|
565
|
+
stateManager.on("agent:spawn", (event) => this.onSpawn(event));
|
|
566
|
+
stateManager.on("agent:update", (event) => this.onUpdate(event));
|
|
567
|
+
stateManager.on("agent:idle", (event) => this.onIdle(event));
|
|
568
|
+
stateManager.on("agent:shutdown", (event) => this.onShutdown(event));
|
|
569
|
+
this.flushTimer = setInterval(() => this.flushAllAgentStates(), AGENT_FLUSH_INTERVAL_MS);
|
|
570
|
+
}
|
|
571
|
+
getStore() {
|
|
572
|
+
return this.store;
|
|
573
|
+
}
|
|
574
|
+
/** Record the currently active session (manual trigger) */
|
|
575
|
+
recordCurrentSession(rootSessionId) {
|
|
576
|
+
const staging = this.staging.get(rootSessionId);
|
|
577
|
+
if (!staging)
|
|
578
|
+
return null;
|
|
579
|
+
for (const agent of this.stateManager.getAll()) {
|
|
580
|
+
if (agent.rootSessionId === rootSessionId) {
|
|
581
|
+
const history = this.stateManager.getHistory(agent.id);
|
|
582
|
+
staging.agents.set(agent.id, {
|
|
583
|
+
state: { ...agent },
|
|
584
|
+
history: [...history],
|
|
585
|
+
endedAt: Date.now()
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
staging.toolChain = this.stateManager.getToolChainSnapshot();
|
|
590
|
+
return this.finalize(rootSessionId);
|
|
591
|
+
}
|
|
592
|
+
/** Recover orphaned live sessions from DB (previous crash/restart) */
|
|
593
|
+
recoverOrphans() {
|
|
594
|
+
const orphans = this.store.getOrphanedLiveSessions();
|
|
595
|
+
for (const orphan of orphans) {
|
|
596
|
+
console.log(`Recovering orphaned session: ${orphan.projectName} (root: ${orphan.rootSessionId.slice(0, 12)}...)`);
|
|
597
|
+
let agents = [];
|
|
598
|
+
try {
|
|
599
|
+
agents = JSON.parse(orphan.agentsJson);
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
const timeline = this.store.getLiveTimeline(orphan.rootSessionId);
|
|
603
|
+
const endedAt = orphan.lastActivityAt;
|
|
604
|
+
let totalInputTokens = 0;
|
|
605
|
+
let totalOutputTokens = 0;
|
|
606
|
+
let totalCacheRead = 0;
|
|
607
|
+
let totalCacheCreation = 0;
|
|
608
|
+
let totalToolUses = 0;
|
|
609
|
+
let primaryModel = null;
|
|
610
|
+
for (const ag of agents) {
|
|
611
|
+
totalInputTokens += ag.totalInputTokens;
|
|
612
|
+
totalOutputTokens += ag.totalOutputTokens;
|
|
613
|
+
totalCacheRead += ag.cacheReadTokens;
|
|
614
|
+
totalCacheCreation += ag.cacheCreationTokens;
|
|
615
|
+
totalToolUses += ag.toolUseCount;
|
|
616
|
+
if (!primaryModel && ag.model)
|
|
617
|
+
primaryModel = ag.model;
|
|
618
|
+
}
|
|
619
|
+
if (agents.length === 0) {
|
|
620
|
+
this.store.removeLiveSession(orphan.rootSessionId);
|
|
621
|
+
console.log(`Skipped empty orphaned session: ${orphan.rootSessionId.slice(0, 12)}...`);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const sessionId = randomUUID();
|
|
625
|
+
const totalCost = agents.reduce((sum, ag) => sum + ag.cost, 0);
|
|
626
|
+
const recorded = {
|
|
627
|
+
id: sessionId,
|
|
628
|
+
source: orphan.source,
|
|
629
|
+
rootSessionId: orphan.rootSessionId,
|
|
630
|
+
projectName: orphan.projectName,
|
|
631
|
+
projectPath: orphan.projectPath,
|
|
632
|
+
startedAt: orphan.startedAt,
|
|
633
|
+
endedAt,
|
|
634
|
+
durationMs: endedAt - orphan.startedAt,
|
|
635
|
+
totalCost,
|
|
636
|
+
totalInputTokens,
|
|
637
|
+
totalOutputTokens,
|
|
638
|
+
totalCacheReadTokens: totalCacheRead,
|
|
639
|
+
totalCacheCreationTokens: totalCacheCreation,
|
|
640
|
+
totalToolUses,
|
|
641
|
+
agentCount: agents.length,
|
|
642
|
+
model: primaryModel,
|
|
643
|
+
agents,
|
|
644
|
+
toolChain: { transitions: [], tools: [], toolCounts: {}, toolSuccesses: {}, toolFailures: {}, toolAvgDuration: {} },
|
|
645
|
+
label: "(recovered)",
|
|
646
|
+
tags: []
|
|
647
|
+
};
|
|
648
|
+
try {
|
|
649
|
+
this.store.saveSession(recorded, timeline);
|
|
650
|
+
console.log(`Recovered session: ${sessionId} (${orphan.projectName}, ${timeline.length} events)`);
|
|
651
|
+
} catch (err) {
|
|
652
|
+
console.error("Failed to recover session:", err);
|
|
653
|
+
}
|
|
654
|
+
this.store.removeLiveSession(orphan.rootSessionId);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
getOrCreateStaging(agent) {
|
|
658
|
+
const rootId = agent.rootSessionId;
|
|
659
|
+
let session = this.staging.get(rootId);
|
|
660
|
+
if (!session) {
|
|
661
|
+
const sessionId = randomUUID();
|
|
662
|
+
const source = rootId.startsWith("oc:") ? "opencode" : "claude";
|
|
663
|
+
session = {
|
|
664
|
+
rootSessionId: rootId,
|
|
665
|
+
sessionId,
|
|
666
|
+
projectName: agent.projectName,
|
|
667
|
+
projectPath: agent.projectPath,
|
|
668
|
+
source,
|
|
669
|
+
startedAt: agent.spawnedAt,
|
|
670
|
+
agents: /* @__PURE__ */ new Map(),
|
|
671
|
+
toolChain: null,
|
|
672
|
+
finalizeTimer: null
|
|
673
|
+
};
|
|
674
|
+
this.staging.set(rootId, session);
|
|
675
|
+
this.store.upsertLiveSession(rootId, {
|
|
676
|
+
sessionId,
|
|
677
|
+
source,
|
|
678
|
+
projectName: agent.projectName,
|
|
679
|
+
projectPath: agent.projectPath,
|
|
680
|
+
startedAt: agent.spawnedAt
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return session;
|
|
684
|
+
}
|
|
685
|
+
onSpawn(event) {
|
|
686
|
+
const agent = event.agent;
|
|
687
|
+
const session = this.getOrCreateStaging(agent);
|
|
688
|
+
if (session.finalizeTimer) {
|
|
689
|
+
clearTimeout(session.finalizeTimer);
|
|
690
|
+
session.finalizeTimer = null;
|
|
691
|
+
}
|
|
692
|
+
const timelineEvent = {
|
|
693
|
+
timestamp: event.timestamp,
|
|
694
|
+
agentId: agent.id,
|
|
695
|
+
kind: "spawn",
|
|
696
|
+
zone: agent.currentZone
|
|
697
|
+
};
|
|
698
|
+
this.store.appendLiveTimelineEvent(session.rootSessionId, timelineEvent);
|
|
699
|
+
}
|
|
700
|
+
onUpdate(event) {
|
|
701
|
+
const agent = event.agent;
|
|
702
|
+
const session = this.staging.get(agent.rootSessionId);
|
|
703
|
+
if (!session)
|
|
704
|
+
return;
|
|
705
|
+
if (agent.currentTool) {
|
|
706
|
+
const timelineEvent = {
|
|
707
|
+
timestamp: event.timestamp,
|
|
708
|
+
agentId: agent.id,
|
|
709
|
+
kind: "tool",
|
|
710
|
+
zone: agent.currentZone,
|
|
711
|
+
tool: agent.currentTool,
|
|
712
|
+
toolArgs: agent.currentActivity ?? void 0
|
|
713
|
+
};
|
|
714
|
+
this.store.appendLiveTimelineEvent(session.rootSessionId, timelineEvent);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
onIdle(event) {
|
|
718
|
+
const agent = event.agent;
|
|
719
|
+
const session = this.staging.get(agent.rootSessionId);
|
|
720
|
+
if (!session)
|
|
721
|
+
return;
|
|
722
|
+
this.store.appendLiveTimelineEvent(session.rootSessionId, {
|
|
723
|
+
timestamp: event.timestamp,
|
|
724
|
+
agentId: agent.id,
|
|
725
|
+
kind: "idle",
|
|
726
|
+
zone: "idle"
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
onShutdown(event) {
|
|
730
|
+
const agent = event.agent;
|
|
731
|
+
const rootId = agent.rootSessionId;
|
|
732
|
+
const session = this.staging.get(rootId);
|
|
733
|
+
if (!session)
|
|
734
|
+
return;
|
|
735
|
+
const history = this.stateManager.getHistory(agent.id);
|
|
736
|
+
session.agents.set(agent.id, {
|
|
737
|
+
state: { ...agent },
|
|
738
|
+
history: [...history],
|
|
739
|
+
endedAt: event.timestamp
|
|
740
|
+
});
|
|
741
|
+
session.toolChain = this.stateManager.getToolChainSnapshot();
|
|
742
|
+
this.store.appendLiveTimelineEvent(rootId, {
|
|
743
|
+
timestamp: event.timestamp,
|
|
744
|
+
agentId: agent.id,
|
|
745
|
+
kind: "shutdown"
|
|
746
|
+
});
|
|
747
|
+
this.flushAgentState(session);
|
|
748
|
+
const activeAgents = this.stateManager.getAll().filter((a) => a.rootSessionId === rootId && a.id !== agent.id);
|
|
749
|
+
if (activeAgents.length === 0) {
|
|
750
|
+
if (session.finalizeTimer)
|
|
751
|
+
clearTimeout(session.finalizeTimer);
|
|
752
|
+
session.finalizeTimer = setTimeout(() => {
|
|
753
|
+
this.finalize(rootId);
|
|
754
|
+
}, FINALIZE_DELAY_MS);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/** Flush agent state snapshots to DB for crash safety */
|
|
758
|
+
flushAgentState(session) {
|
|
759
|
+
const agents = [];
|
|
760
|
+
for (const [, staging] of session.agents) {
|
|
761
|
+
const s = staging.state;
|
|
762
|
+
agents.push({
|
|
763
|
+
agentId: s.id,
|
|
764
|
+
agentName: s.agentName,
|
|
765
|
+
role: s.role,
|
|
766
|
+
model: s.model,
|
|
767
|
+
spawnedAt: s.spawnedAt,
|
|
768
|
+
endedAt: staging.endedAt,
|
|
769
|
+
totalInputTokens: s.totalInputTokens,
|
|
770
|
+
totalOutputTokens: s.totalOutputTokens,
|
|
771
|
+
cacheReadTokens: s.cacheReadTokens,
|
|
772
|
+
cacheCreationTokens: s.cacheCreationTokens,
|
|
773
|
+
toolUseCount: s.toolUseCount,
|
|
774
|
+
cost: computeAgentCost(s)
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
for (const agent of this.stateManager.getAll()) {
|
|
778
|
+
if (agent.rootSessionId === session.rootSessionId && !session.agents.has(agent.id)) {
|
|
779
|
+
agents.push({
|
|
780
|
+
agentId: agent.id,
|
|
781
|
+
agentName: agent.agentName,
|
|
782
|
+
role: agent.role,
|
|
783
|
+
model: agent.model,
|
|
784
|
+
spawnedAt: agent.spawnedAt,
|
|
785
|
+
endedAt: Date.now(),
|
|
786
|
+
totalInputTokens: agent.totalInputTokens,
|
|
787
|
+
totalOutputTokens: agent.totalOutputTokens,
|
|
788
|
+
cacheReadTokens: agent.cacheReadTokens,
|
|
789
|
+
cacheCreationTokens: agent.cacheCreationTokens,
|
|
790
|
+
toolUseCount: agent.toolUseCount,
|
|
791
|
+
cost: computeAgentCost(agent)
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
this.store.updateLiveAgents(session.rootSessionId, JSON.stringify(agents));
|
|
796
|
+
}
|
|
797
|
+
/** Periodically flush all active sessions' agent states */
|
|
798
|
+
flushAllAgentStates() {
|
|
799
|
+
for (const session of this.staging.values()) {
|
|
800
|
+
this.flushAgentState(session);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
/** Finalize and persist a staged session. Returns the session ID. */
|
|
804
|
+
finalize(rootSessionId) {
|
|
805
|
+
const session = this.staging.get(rootSessionId);
|
|
806
|
+
if (!session) {
|
|
807
|
+
this.staging.delete(rootSessionId);
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
if (session.finalizeTimer) {
|
|
811
|
+
clearTimeout(session.finalizeTimer);
|
|
812
|
+
session.finalizeTimer = null;
|
|
813
|
+
}
|
|
814
|
+
const sessionId = session.sessionId;
|
|
815
|
+
const now = Date.now();
|
|
816
|
+
const agents = [];
|
|
817
|
+
let totalInputTokens = 0;
|
|
818
|
+
let totalOutputTokens = 0;
|
|
819
|
+
let totalCacheRead = 0;
|
|
820
|
+
let totalCacheCreation = 0;
|
|
821
|
+
let totalToolUses = 0;
|
|
822
|
+
let primaryModel = null;
|
|
823
|
+
let earliestSpawn = Infinity;
|
|
824
|
+
let latestEnd = 0;
|
|
825
|
+
for (const [, staging] of session.agents) {
|
|
826
|
+
const s = staging.state;
|
|
827
|
+
const cost = computeAgentCost(s);
|
|
828
|
+
agents.push({
|
|
829
|
+
agentId: s.id,
|
|
830
|
+
agentName: s.agentName,
|
|
831
|
+
role: s.role,
|
|
832
|
+
model: s.model,
|
|
833
|
+
spawnedAt: s.spawnedAt,
|
|
834
|
+
endedAt: staging.endedAt,
|
|
835
|
+
totalInputTokens: s.totalInputTokens,
|
|
836
|
+
totalOutputTokens: s.totalOutputTokens,
|
|
837
|
+
cacheReadTokens: s.cacheReadTokens,
|
|
838
|
+
cacheCreationTokens: s.cacheCreationTokens,
|
|
839
|
+
toolUseCount: s.toolUseCount,
|
|
840
|
+
cost
|
|
841
|
+
});
|
|
842
|
+
totalInputTokens += s.totalInputTokens;
|
|
843
|
+
totalOutputTokens += s.totalOutputTokens;
|
|
844
|
+
totalCacheRead += s.cacheReadTokens;
|
|
845
|
+
totalCacheCreation += s.cacheCreationTokens;
|
|
846
|
+
totalToolUses += s.toolUseCount;
|
|
847
|
+
if (s.spawnedAt < earliestSpawn)
|
|
848
|
+
earliestSpawn = s.spawnedAt;
|
|
849
|
+
if (staging.endedAt > latestEnd)
|
|
850
|
+
latestEnd = staging.endedAt;
|
|
851
|
+
if (s.role === "main" && s.model)
|
|
852
|
+
primaryModel = s.model;
|
|
853
|
+
if (!primaryModel && s.model)
|
|
854
|
+
primaryModel = s.model;
|
|
855
|
+
}
|
|
856
|
+
if (agents.length === 0) {
|
|
857
|
+
this.store.removeLiveSession(rootSessionId);
|
|
858
|
+
this.staging.delete(rootSessionId);
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
const endedAt = latestEnd || now;
|
|
862
|
+
const startedAt = earliestSpawn === Infinity ? session.startedAt : earliestSpawn;
|
|
863
|
+
const recorded = {
|
|
864
|
+
id: sessionId,
|
|
865
|
+
source: session.source,
|
|
866
|
+
rootSessionId: session.rootSessionId,
|
|
867
|
+
projectName: session.projectName,
|
|
868
|
+
projectPath: session.projectPath,
|
|
869
|
+
startedAt,
|
|
870
|
+
endedAt,
|
|
871
|
+
durationMs: endedAt - startedAt,
|
|
872
|
+
// Sum per-agent costs (each agent already computed with its own model's pricing)
|
|
873
|
+
totalCost: agents.reduce((sum, ag) => sum + ag.cost, 0),
|
|
874
|
+
totalInputTokens,
|
|
875
|
+
totalOutputTokens,
|
|
876
|
+
totalCacheReadTokens: totalCacheRead,
|
|
877
|
+
totalCacheCreationTokens: totalCacheCreation,
|
|
878
|
+
totalToolUses,
|
|
879
|
+
agentCount: agents.length,
|
|
880
|
+
model: primaryModel,
|
|
881
|
+
agents,
|
|
882
|
+
toolChain: session.toolChain ?? {
|
|
883
|
+
transitions: [],
|
|
884
|
+
tools: [],
|
|
885
|
+
toolCounts: {},
|
|
886
|
+
toolSuccesses: {},
|
|
887
|
+
toolFailures: {},
|
|
888
|
+
toolAvgDuration: {}
|
|
889
|
+
},
|
|
890
|
+
label: null,
|
|
891
|
+
tags: []
|
|
892
|
+
};
|
|
893
|
+
const liveTimeline = this.store.getLiveTimeline(rootSessionId);
|
|
894
|
+
const historyTimeline = this.buildTimelineFromHistory(session);
|
|
895
|
+
const timeline = historyTimeline.length > 0 ? historyTimeline : liveTimeline;
|
|
896
|
+
try {
|
|
897
|
+
this.store.saveSession(recorded, timeline);
|
|
898
|
+
this.store.removeLiveSession(rootSessionId);
|
|
899
|
+
console.log(`Session recorded: ${sessionId} (${session.projectName}, ${agents.length} agents, ${totalToolUses} tools, $${recorded.totalCost.toFixed(4)})`);
|
|
900
|
+
} catch (err) {
|
|
901
|
+
console.error("Failed to save session:", err);
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
this.staging.delete(rootSessionId);
|
|
905
|
+
return sessionId;
|
|
906
|
+
}
|
|
907
|
+
/** Build a merged timeline from all agents' activity histories */
|
|
908
|
+
buildTimelineFromHistory(session) {
|
|
909
|
+
const events = [];
|
|
910
|
+
for (const [agentId, staging] of session.agents) {
|
|
911
|
+
for (const entry of staging.history) {
|
|
912
|
+
events.push({
|
|
913
|
+
timestamp: entry.timestamp,
|
|
914
|
+
agentId,
|
|
915
|
+
kind: entry.kind,
|
|
916
|
+
...entry.zone && { zone: entry.zone },
|
|
917
|
+
...entry.tool && { tool: entry.tool },
|
|
918
|
+
...entry.toolArgs && { toolArgs: entry.toolArgs },
|
|
919
|
+
...entry.text && { text: entry.text },
|
|
920
|
+
...entry.inputTokens != null && { inputTokens: entry.inputTokens },
|
|
921
|
+
...entry.outputTokens != null && { outputTokens: entry.outputTokens }
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
events.sort((a, b) => a.timestamp - b.timestamp);
|
|
926
|
+
return events;
|
|
927
|
+
}
|
|
928
|
+
dispose() {
|
|
929
|
+
clearInterval(this.flushTimer);
|
|
930
|
+
for (const session of this.staging.values()) {
|
|
931
|
+
if (session.finalizeTimer)
|
|
932
|
+
clearTimeout(session.finalizeTimer);
|
|
933
|
+
this.flushAgentState(session);
|
|
934
|
+
}
|
|
935
|
+
this.staging.clear();
|
|
936
|
+
this.store.close();
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
export {
|
|
940
|
+
SessionRecorder
|
|
941
|
+
};
|