agentacta 2026.4.8 → 2026.4.10

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.
@@ -0,0 +1,879 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.discoverSessionDirs = discoverSessionDirs;
7
+ exports.listJsonlFiles = listJsonlFiles;
8
+ exports.indexFile = indexFile;
9
+ exports.indexCronRunFile = indexCronRunFile;
10
+ exports.indexAll = indexAll;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const db_js_1 = require("./db.js");
14
+ const config_js_1 = require("./config.js");
15
+ const REINDEX = process.argv.includes('--reindex');
16
+ const WATCH = process.argv.includes('--watch');
17
+ function listJsonlFiles(baseDir, recursive = false) {
18
+ if (!node_fs_1.default.existsSync(baseDir))
19
+ return [];
20
+ const out = [];
21
+ function walk(dir) {
22
+ for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
23
+ const full = node_path_1.default.join(dir, entry.name);
24
+ if (entry.isDirectory()) {
25
+ if (recursive)
26
+ walk(full);
27
+ continue;
28
+ }
29
+ if (entry.isFile() && entry.name.endsWith('.jsonl'))
30
+ out.push(full);
31
+ }
32
+ }
33
+ walk(baseDir);
34
+ return out;
35
+ }
36
+ function discoverSessionDirs(config) {
37
+ const dirs = [];
38
+ const home = process.env.HOME;
39
+ const codexSessionsPath = node_path_1.default.join(home, '.codex/sessions');
40
+ const cronRunsPath = node_path_1.default.join(home, '.openclaw/cron/runs');
41
+ function normalizedPath(p) {
42
+ return node_path_1.default.resolve(p).replace(/[\\\/]+$/, '');
43
+ }
44
+ function hasDir(targetPath, sourceType = 'transcript') {
45
+ const wanted = normalizedPath(targetPath);
46
+ return dirs.some(d => normalizedPath(d.path) === wanted && (d.sourceType || 'transcript') === sourceType);
47
+ }
48
+ function addDir(dir) {
49
+ if (!dir || !dir.path)
50
+ return;
51
+ if (hasDir(dir.path, dir.sourceType || 'transcript'))
52
+ return;
53
+ dirs.push(dir);
54
+ }
55
+ // Expand a single path into session dirs, handling Claude Code's per-project structure
56
+ function expandPath(p) {
57
+ if (!node_fs_1.default.existsSync(p))
58
+ return;
59
+ const stat = node_fs_1.default.statSync(p);
60
+ if (!stat.isDirectory())
61
+ return;
62
+ const normalized = normalizedPath(p);
63
+ const normalizedCodex = normalizedPath(codexSessionsPath);
64
+ // Claude Code: ~/.claude/projects contains per-project subdirs with JSONL files
65
+ if (normalized.endsWith('/.claude/projects')) {
66
+ for (const proj of node_fs_1.default.readdirSync(p)) {
67
+ const projDir = node_path_1.default.join(p, proj);
68
+ if (node_fs_1.default.statSync(projDir).isDirectory()) {
69
+ const hasJsonl = node_fs_1.default.readdirSync(projDir).some(f => f.endsWith('.jsonl'));
70
+ if (hasJsonl)
71
+ addDir({ path: projDir, agent: 'claude-code' });
72
+ }
73
+ }
74
+ }
75
+ else if (normalized === normalizedCodex) {
76
+ // Codex CLI stores nested YYYY/MM/DD directories and must be recursive.
77
+ addDir({ path: p, agent: 'codex-cli', recursive: true });
78
+ }
79
+ else {
80
+ addDir({ path: p, agent: node_path_1.default.basename(node_path_1.default.dirname(p)) });
81
+ }
82
+ }
83
+ // Config sessionsPath or env var override
84
+ const sessionsOverride = process.env.AGENTACTA_SESSIONS_PATH || (config && config.sessionsPath);
85
+ if (sessionsOverride) {
86
+ const overridePaths = Array.isArray(sessionsOverride)
87
+ ? sessionsOverride
88
+ : sessionsOverride.split(':');
89
+ overridePaths.forEach(expandPath);
90
+ }
91
+ // Auto-discover: ~/.openclaw/agents/*/sessions/
92
+ const oclawAgents = node_path_1.default.join(home, '.openclaw/agents');
93
+ if (node_fs_1.default.existsSync(oclawAgents)) {
94
+ for (const agent of node_fs_1.default.readdirSync(oclawAgents)) {
95
+ const sp = node_path_1.default.join(oclawAgents, agent, 'sessions');
96
+ if (node_fs_1.default.existsSync(sp) && node_fs_1.default.statSync(sp).isDirectory()) {
97
+ addDir({ path: sp, agent });
98
+ }
99
+ }
100
+ }
101
+ // Auto-discover: ~/.claude/projects/
102
+ expandPath(node_path_1.default.join(home, '.claude/projects'));
103
+ // Scan ~/.codex/sessions recursively (Codex CLI stores nested YYYY/MM/DD/*.jsonl)
104
+ const codexSessions = codexSessionsPath;
105
+ if (node_fs_1.default.existsSync(codexSessions) && node_fs_1.default.statSync(codexSessions).isDirectory()) {
106
+ addDir({ path: codexSessions, agent: 'codex-cli', recursive: true });
107
+ }
108
+ // Fallback synthetic source for cron-backed runs that have metadata but no transcript JSONL.
109
+ if (node_fs_1.default.existsSync(cronRunsPath) && node_fs_1.default.statSync(cronRunsPath).isDirectory()) {
110
+ addDir({ path: cronRunsPath, agent: 'cron', sourceType: 'cron-run' });
111
+ }
112
+ if (!dirs.length) {
113
+ // Fallback to hardcoded
114
+ const fallback = node_path_1.default.join(home, '.openclaw/agents/main/sessions');
115
+ if (node_fs_1.default.existsSync(fallback))
116
+ addDir({ path: fallback, agent: 'main' });
117
+ }
118
+ return dirs;
119
+ }
120
+ function isHeartbeat(text) {
121
+ if (!text)
122
+ return false;
123
+ const lower = text.toLowerCase();
124
+ return lower.includes('heartbeat') || lower.includes('heartbeat_ok');
125
+ }
126
+ function isBoilerplatePrompt(text) {
127
+ if (!text)
128
+ return false;
129
+ const lower = text.toLowerCase();
130
+ return lower.includes('<permissions instructions>')
131
+ || lower.includes('filesystem sandboxing defines which files can be read or written')
132
+ || lower.includes('# agents.md instructions for ');
133
+ }
134
+ function isSummaryCandidate(text) {
135
+ if (!text || text.trim().length <= 10)
136
+ return false;
137
+ if (isHeartbeat(text))
138
+ return false;
139
+ if (isBoilerplatePrompt(text))
140
+ return false;
141
+ return true;
142
+ }
143
+ function stripLeadingDatetimePrefix(text) {
144
+ if (!text)
145
+ return text;
146
+ return text
147
+ .replace(/^\[(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}[^\]]*\]\s*/i, '')
148
+ .trim();
149
+ }
150
+ function extractContent(msg) {
151
+ if (!msg || typeof msg !== 'object')
152
+ return '';
153
+ const m = msg;
154
+ if (!m.content)
155
+ return '';
156
+ if (typeof m.content === 'string')
157
+ return m.content;
158
+ if (Array.isArray(m.content)) {
159
+ return m.content.filter(b => b.type === 'text').map(b => b.text || '').join('\n');
160
+ }
161
+ return '';
162
+ }
163
+ function extractToolCalls(msg) {
164
+ if (!msg || typeof msg !== 'object')
165
+ return [];
166
+ const m = msg;
167
+ if (!Array.isArray(m.content))
168
+ return [];
169
+ return m.content
170
+ .filter(b => b.type === 'tool_use' || b.type === 'toolCall')
171
+ .map(b => ({
172
+ id: b.id || b.toolCallId || '',
173
+ name: b.name || '',
174
+ args: JSON.stringify(b.input || b.arguments || {})
175
+ }));
176
+ }
177
+ function extractToolResult(msg) {
178
+ if (!msg)
179
+ return null;
180
+ const m = msg;
181
+ if (m.role === 'toolResult' || m.role === 'tool') {
182
+ const content = Array.isArray(m.content)
183
+ ? m.content.map(b => b.text || '').join('\n')
184
+ : (typeof m.content === 'string' ? m.content : '');
185
+ return { toolCallId: m.toolCallId || '', toolName: m.toolName || '', content: content.slice(0, 10000) };
186
+ }
187
+ return null;
188
+ }
189
+ function extractCodexMessageText(content) {
190
+ if (!content)
191
+ return '';
192
+ if (typeof content === 'string')
193
+ return content;
194
+ if (!Array.isArray(content))
195
+ return '';
196
+ return content
197
+ .map((part) => {
198
+ if (!part || typeof part !== 'object')
199
+ return '';
200
+ if (typeof part.text === 'string')
201
+ return part.text;
202
+ if (typeof part.output_text === 'string')
203
+ return part.output_text;
204
+ if (typeof part.input_text === 'string')
205
+ return part.input_text;
206
+ return '';
207
+ })
208
+ .filter(Boolean)
209
+ .join('\n');
210
+ }
211
+ function extractFilePaths(toolName, toolArgs) {
212
+ const paths = [];
213
+ if (!toolArgs)
214
+ return paths;
215
+ const maybePath = (value) => {
216
+ if (typeof value !== 'string')
217
+ return;
218
+ if (value.startsWith('/') || value.startsWith('~/') || value.startsWith('./') || value.startsWith('../')) {
219
+ paths.push(value);
220
+ return;
221
+ }
222
+ if (value.includes('/') || value.includes('\\'))
223
+ paths.push(value);
224
+ };
225
+ const visit = (obj) => {
226
+ if (!obj || typeof obj !== 'object')
227
+ return;
228
+ if (Array.isArray(obj)) {
229
+ for (const item of obj)
230
+ visit(item);
231
+ return;
232
+ }
233
+ for (const [key, value] of Object.entries(obj)) {
234
+ if (typeof value === 'string') {
235
+ if (['path', 'file_path', 'filePath', 'file', 'filename', 'cwd', 'workdir', 'directory', 'dir'].includes(key)) {
236
+ maybePath(value);
237
+ }
238
+ }
239
+ else if (value && typeof value === 'object') {
240
+ visit(value);
241
+ }
242
+ }
243
+ };
244
+ try {
245
+ const args = typeof toolArgs === 'string' ? JSON.parse(toolArgs) : toolArgs;
246
+ visit(args);
247
+ }
248
+ catch {
249
+ // ignore parse errors
250
+ }
251
+ return [...new Set(paths)];
252
+ }
253
+ function aliasProject(project, config) {
254
+ if (!project)
255
+ return project;
256
+ const aliases = (config && config.projectAliases && typeof config.projectAliases === 'object') ? config.projectAliases : {};
257
+ return aliases[project] || project;
258
+ }
259
+ function extractProjectFromPath(filePath, config) {
260
+ if (!filePath || typeof filePath !== 'string')
261
+ return null;
262
+ const normalized = filePath.replace(/\\/g, '/');
263
+ // Relative paths are usually from workspace cwd -> treat as workspace activity
264
+ if (!normalized.startsWith('/') && !normalized.startsWith('~'))
265
+ return aliasProject('workspace', config);
266
+ const rel = normalized
267
+ .replace(/^\/home\/[^/]+\//, '')
268
+ .replace(/^\/Users\/[^/]+\//, '')
269
+ .replace(/^~\//, '');
270
+ const parts = rel.split('/').filter(Boolean);
271
+ if (!parts.length)
272
+ return null;
273
+ // Common repo location: ~/Developer/<repo>/...
274
+ if (parts[0] === 'Developer' && parts[1])
275
+ return aliasProject(parts[1], config);
276
+ // Symphony worktrees: ~/symphony-workspaces/<issue>/...
277
+ if (parts[0] === 'symphony-workspaces' && parts[1])
278
+ return aliasProject(parts[1], config);
279
+ // OpenClaw workspace and agent stores
280
+ if (parts[0] === '.openclaw' && parts[1] === 'workspace')
281
+ return aliasProject('workspace', config);
282
+ if (parts[0] === '.openclaw' && parts[1] === 'agents' && parts[2])
283
+ return aliasProject(`agent:${parts[2]}`, config);
284
+ // Claude Code projects
285
+ if (parts[0] === '.claude' && parts[1] === 'projects' && parts[2])
286
+ return aliasProject(`claude:${parts[2]}`, config);
287
+ // Shared files area
288
+ if (parts[0] === 'Shared')
289
+ return aliasProject('shared', config);
290
+ return null;
291
+ }
292
+ function indexCronRunFile(db, filePath, agentName, stmts) {
293
+ const stat = node_fs_1.default.statSync(filePath);
294
+ const mtime = stat.mtime.toISOString();
295
+ if (!REINDEX) {
296
+ const state = stmts.getState.get(filePath);
297
+ if (state && state.last_modified === mtime)
298
+ return { skipped: true };
299
+ }
300
+ const raw = node_fs_1.default.readFileSync(filePath, 'utf8').trim();
301
+ if (!raw)
302
+ return { skipped: true };
303
+ let meta;
304
+ try {
305
+ meta = JSON.parse(raw.split('\n').find(Boolean));
306
+ }
307
+ catch {
308
+ return { skipped: true };
309
+ }
310
+ const sessionId = meta.sessionId;
311
+ if (!sessionId)
312
+ return { skipped: true };
313
+ // Guard: don't overwrite a session that was already indexed from a real transcript.
314
+ // Check both event presence AND session_type — a transcript session with zero events
315
+ // (e.g. header-only file) should still win over synthetic cron metadata.
316
+ const existingSession = db.prepare('SELECT session_type FROM sessions WHERE id = ?').get(sessionId);
317
+ if (existingSession && existingSession.session_type !== 'cron') {
318
+ stmts.upsertState.run(filePath, 1, mtime);
319
+ return { skipped: true, preferredTranscript: true, sessionId };
320
+ }
321
+ const existingRealSession = db.prepare('SELECT EXISTS(SELECT 1 FROM events WHERE session_id = ?) AS has_events').get(sessionId);
322
+ if (existingRealSession && existingRealSession.has_events) {
323
+ stmts.upsertState.run(filePath, 1, mtime);
324
+ return { skipped: true, preferredTranscript: true, sessionId };
325
+ }
326
+ const ts = typeof meta.ts === 'number' ? new Date(meta.ts).toISOString() : new Date().toISOString();
327
+ const runAt = typeof meta.runAtMs === 'number' ? new Date(meta.runAtMs).toISOString() : ts;
328
+ const durationMs = typeof meta.durationMs === 'number' ? meta.durationMs : null;
329
+ const endTime = ts;
330
+ const startTime = durationMs ? new Date(new Date(endTime).getTime() - durationMs).toISOString() : runAt;
331
+ const summary = stripLeadingDatetimePrefix(meta.summary || 'Cron run');
332
+ const sessionKey = typeof meta.sessionKey === 'string' ? meta.sessionKey : '';
333
+ const sessionKeyParts = sessionKey.split(':');
334
+ const inferredAgent = sessionKeyParts[0] === 'agent' && sessionKeyParts[1] ? sessionKeyParts[1] : agentName;
335
+ const model = meta.model || meta.provider || null;
336
+ const totalInputTokens = meta.usage && typeof meta.usage.input_tokens === 'number' ? meta.usage.input_tokens : 0;
337
+ const totalOutputTokens = meta.usage && typeof meta.usage.output_tokens === 'number' ? meta.usage.output_tokens : 0;
338
+ const totalTokens = meta.usage && typeof meta.usage.total_tokens === 'number'
339
+ ? meta.usage.total_tokens
340
+ : totalInputTokens + totalOutputTokens;
341
+ const commitIndex = db.transaction(() => {
342
+ if (stmts.deleteArchive)
343
+ stmts.deleteArchive.run(sessionId);
344
+ stmts.deleteFileActivity.run(sessionId);
345
+ stmts.deleteEvents.run(sessionId);
346
+ stmts.deleteSession.run(sessionId);
347
+ stmts.upsertSession.run(sessionId, startTime, endTime, 0, 0, model, summary, inferredAgent, 'cron', 0, totalTokens, totalInputTokens, totalOutputTokens, 0, 0, null, null, null, model ? JSON.stringify([model]) : null, null);
348
+ stmts.upsertState.run(filePath, 1, mtime);
349
+ });
350
+ commitIndex();
351
+ return { sessionId, synthetic: true };
352
+ }
353
+ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
354
+ const stat = node_fs_1.default.statSync(filePath);
355
+ const mtime = stat.mtime.toISOString();
356
+ if (!REINDEX) {
357
+ const state = stmts.getState.get(filePath);
358
+ if (state && state.last_modified === mtime)
359
+ return { skipped: true };
360
+ }
361
+ const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
362
+ const lines = raw.trim().split('\n').filter(Boolean);
363
+ if (lines.length === 0)
364
+ return { skipped: true };
365
+ let sessionId = null;
366
+ let sessionStart = null;
367
+ let sessionEnd = null;
368
+ let msgCount = 0;
369
+ let toolCount = 0;
370
+ let model = null;
371
+ const modelsSet = new Set();
372
+ let summary = '';
373
+ let sessionType = null;
374
+ let agent = agentName;
375
+ let totalCost = 0;
376
+ let totalTokens = 0;
377
+ let totalInputTokens = 0;
378
+ let totalOutputTokens = 0;
379
+ let totalCacheReadTokens = 0;
380
+ let totalCacheWriteTokens = 0;
381
+ let initialPrompt = null;
382
+ let firstMessageId = null;
383
+ let firstMessageTimestamp = null;
384
+ let codexProvider = null;
385
+ let codexSource = null;
386
+ let codexOriginator = null;
387
+ let sawSnapshotRecord = false;
388
+ let sawNonSnapshotRecord = false;
389
+ let firstLine;
390
+ try {
391
+ firstLine = JSON.parse(lines[0]);
392
+ }
393
+ catch {
394
+ return { skipped: true };
395
+ }
396
+ let isClaudeCode = false;
397
+ let isCodexCli = false;
398
+ if (firstLine.type === 'session') {
399
+ // OpenClaw format
400
+ sessionId = firstLine.id || null;
401
+ sessionStart = firstLine.timestamp || null;
402
+ if (firstLine.agent)
403
+ agent = firstLine.agent;
404
+ if (firstLine.sessionType)
405
+ sessionType = firstLine.sessionType;
406
+ if (sessionId && sessionId.includes('subagent'))
407
+ sessionType = 'subagent';
408
+ }
409
+ else if (firstLine.type === 'user' || firstLine.type === 'assistant' || firstLine.type === 'file-history-snapshot' || firstLine.type === 'queue-operation') {
410
+ // Claude Code format — no session header, extract from first message or queue-operation line
411
+ isClaudeCode = true;
412
+ for (const line of lines) {
413
+ let obj;
414
+ try {
415
+ obj = JSON.parse(line);
416
+ }
417
+ catch {
418
+ continue;
419
+ }
420
+ if (obj.sessionId && obj.timestamp) {
421
+ sessionId = obj.sessionId;
422
+ sessionStart = obj.timestamp;
423
+ break;
424
+ }
425
+ if ((obj.type === 'user' || obj.type === 'assistant') && obj.sessionId) {
426
+ sessionId = obj.sessionId;
427
+ sessionStart = obj.timestamp || null;
428
+ break;
429
+ }
430
+ }
431
+ if (!sessionId) {
432
+ // Fallback: use filename as session ID
433
+ sessionId = node_path_1.default.basename(filePath, '.jsonl');
434
+ sessionStart = new Date(firstLine.timestamp || Date.now()).toISOString();
435
+ }
436
+ }
437
+ else if (firstLine.type === 'session_meta') {
438
+ // Codex CLI format
439
+ isCodexCli = true;
440
+ const meta = (firstLine.payload || {});
441
+ sessionId = meta.id || node_path_1.default.basename(filePath, '.jsonl');
442
+ sessionStart = meta.timestamp || firstLine.timestamp || new Date().toISOString();
443
+ sessionType = 'codex-direct';
444
+ agent = 'codex-cli';
445
+ if (meta.model) {
446
+ model = meta.model;
447
+ modelsSet.add(meta.model);
448
+ }
449
+ codexProvider = meta.model_provider || null;
450
+ codexSource = meta.source || null;
451
+ codexOriginator = meta.originator || null;
452
+ if (codexOriginator && codexOriginator.includes('symphony'))
453
+ sessionType = 'codex-symphony';
454
+ }
455
+ else {
456
+ return { skipped: true };
457
+ }
458
+ // --- Parse the entire file BEFORE any DB operations ---
459
+ const pendingEvents = [];
460
+ const fileActivities = [];
461
+ const projectCounts = new Map();
462
+ // Seed project from session cwd when available (helps chat-only sessions)
463
+ const sessionCwd = (firstLine && firstLine.cwd) || (firstLine && firstLine.payload && firstLine.payload.cwd);
464
+ if (sessionCwd) {
465
+ const p = extractProjectFromPath(sessionCwd, config);
466
+ if (p)
467
+ projectCounts.set(p, 1);
468
+ }
469
+ for (const line of lines) {
470
+ let obj;
471
+ try {
472
+ obj = JSON.parse(line);
473
+ }
474
+ catch {
475
+ continue;
476
+ }
477
+ if (obj.type === 'file-history-snapshot')
478
+ sawSnapshotRecord = true;
479
+ else
480
+ sawNonSnapshotRecord = true;
481
+ if (isCodexCli) {
482
+ if (obj.type === 'session_meta') {
483
+ const meta = (obj.payload || {});
484
+ if (meta.id)
485
+ sessionId = meta.id;
486
+ if (meta.timestamp && !sessionStart)
487
+ sessionStart = meta.timestamp;
488
+ if (meta.model) {
489
+ if (!model)
490
+ model = meta.model;
491
+ modelsSet.add(meta.model);
492
+ }
493
+ if (meta.model_provider)
494
+ codexProvider = meta.model_provider;
495
+ if (meta.source)
496
+ codexSource = meta.source;
497
+ if (meta.originator)
498
+ codexOriginator = meta.originator;
499
+ if (codexOriginator && codexOriginator.includes('symphony'))
500
+ sessionType = 'codex-symphony';
501
+ if (meta.model_provider && !model)
502
+ model = meta.model_provider;
503
+ continue;
504
+ }
505
+ if (obj.type === 'turn_context' && obj.payload) {
506
+ const tc = obj.payload;
507
+ if (tc.model && typeof tc.model === 'string') {
508
+ if (!model || model === codexProvider)
509
+ model = tc.model;
510
+ modelsSet.add(tc.model);
511
+ }
512
+ continue;
513
+ }
514
+ if (obj.type === 'response_item' && obj.payload) {
515
+ const p = obj.payload;
516
+ const ts = obj.timestamp || sessionStart;
517
+ const eventId = `evt-${obj.type}-${Date.parse(ts) || Math.random()}`;
518
+ if (p.type === 'function_call') {
519
+ const toolName = p.name || p.tool_name || '';
520
+ const toolArgs = typeof p.arguments === 'string' ? p.arguments : JSON.stringify(p.arguments || {});
521
+ const callBaseId = p.call_id || p.id || eventId;
522
+ pendingEvents.push([`${callBaseId}:call`, sessionId, ts, 'tool_call', 'assistant', null, toolName, toolArgs, null]);
523
+ toolCount++;
524
+ const fps = extractFilePaths(toolName, toolArgs);
525
+ for (const fp of fps) {
526
+ fileActivities.push([sessionId, fp, 'read', ts]);
527
+ const project = extractProjectFromPath(fp, config);
528
+ if (project)
529
+ projectCounts.set(project, (projectCounts.get(project) || 0) + 1);
530
+ }
531
+ sessionEnd = ts;
532
+ continue;
533
+ }
534
+ if (p.type === 'function_call_output') {
535
+ const output = (typeof p.output === 'string' ? p.output : JSON.stringify(p.output || '')).slice(0, 10000);
536
+ const resultBaseId = p.call_id || p.id || eventId;
537
+ pendingEvents.push([`${resultBaseId}:result`, sessionId, ts, 'tool_result', 'tool', output, p.name || p.tool_name || '', null, output]);
538
+ sessionEnd = ts;
539
+ continue;
540
+ }
541
+ if (p.type === 'message') {
542
+ const rawRole = p.role || 'assistant';
543
+ const role = rawRole === 'assistant' ? 'assistant' : 'user';
544
+ const content = extractCodexMessageText(p.content);
545
+ if (content) {
546
+ pendingEvents.push([p.id || eventId, sessionId, ts, 'message', role, content, null, null, null]);
547
+ msgCount++;
548
+ if (!summary && role === 'user' && isSummaryCandidate(content))
549
+ summary = content.slice(0, 200);
550
+ if (!initialPrompt && role === 'user' && isSummaryCandidate(content)) {
551
+ initialPrompt = content.slice(0, 500);
552
+ firstMessageId = p.id || eventId;
553
+ firstMessageTimestamp = ts;
554
+ }
555
+ }
556
+ sessionEnd = ts;
557
+ continue;
558
+ }
559
+ }
560
+ if (obj.type === 'event_msg' && obj.payload) {
561
+ const p = obj.payload;
562
+ const ts = obj.timestamp || sessionStart;
563
+ const eventId = `evt-${p.type || 'event'}-${Date.parse(ts) || Math.random()}`;
564
+ if (p.type === 'agent_message' && p.message) {
565
+ pendingEvents.push([eventId, sessionId, ts, 'message', 'assistant', p.message, null, null, null]);
566
+ msgCount++;
567
+ sessionEnd = ts;
568
+ continue;
569
+ }
570
+ if (p.type === 'user_message' && p.message) {
571
+ pendingEvents.push([eventId, sessionId, ts, 'message', 'user', p.message, null, null, null]);
572
+ msgCount++;
573
+ if (!summary && isSummaryCandidate(p.message))
574
+ summary = p.message.slice(0, 200);
575
+ if (!initialPrompt && isSummaryCandidate(p.message)) {
576
+ initialPrompt = p.message.slice(0, 500);
577
+ firstMessageId = eventId;
578
+ firstMessageTimestamp = ts;
579
+ }
580
+ sessionEnd = ts;
581
+ continue;
582
+ }
583
+ }
584
+ continue;
585
+ }
586
+ if (obj.type === 'session' || obj.type === 'model_change' || obj.type === 'thinking_level_change' || obj.type === 'custom' || obj.type === 'file-history-snapshot') {
587
+ if (obj.type === 'model_change' && obj.modelId) {
588
+ if (!model)
589
+ model = obj.modelId; // First model for backwards compat
590
+ modelsSet.add(obj.modelId); // Collect all unique models
591
+ }
592
+ continue;
593
+ }
594
+ // Normalize: Claude Code uses top-level type "user"/"assistant" with message object
595
+ // OpenClaw uses type "message" with message.role
596
+ let msg;
597
+ let ts;
598
+ if (obj.type === 'message' && obj.message) {
599
+ msg = obj.message;
600
+ ts = obj.timestamp;
601
+ }
602
+ else if ((obj.type === 'user' || obj.type === 'assistant') && obj.message) {
603
+ // Claude Code format: wrap into consistent shape
604
+ msg = obj.message;
605
+ if (!msg.role)
606
+ msg.role = obj.type === 'user' ? 'user' : 'assistant';
607
+ ts = obj.timestamp;
608
+ }
609
+ else {
610
+ continue;
611
+ }
612
+ if (msg) {
613
+ sessionEnd = ts || null;
614
+ // Extract model from assistant messages
615
+ if (msg.role === 'assistant' && msg.model && msg.model !== 'delivery-mirror' && !msg.model.startsWith('<')) {
616
+ if (!model)
617
+ model = msg.model; // Keep first model for backwards compat
618
+ modelsSet.add(msg.model); // Collect all unique models
619
+ }
620
+ // Cost tracking
621
+ const usage = msg.usage;
622
+ if (usage) {
623
+ const cost = usage.cost;
624
+ if (cost && typeof cost.total === 'number') {
625
+ totalCost += cost.total;
626
+ }
627
+ if (typeof usage.totalTokens === 'number') {
628
+ totalTokens += usage.totalTokens;
629
+ }
630
+ // OpenClaw format
631
+ if (typeof usage.input === 'number')
632
+ totalInputTokens += usage.input;
633
+ if (typeof usage.output === 'number')
634
+ totalOutputTokens += usage.output;
635
+ if (typeof usage.cacheRead === 'number')
636
+ totalCacheReadTokens += usage.cacheRead;
637
+ if (typeof usage.cacheWrite === 'number')
638
+ totalCacheWriteTokens += usage.cacheWrite;
639
+ // Claude Code format
640
+ if (typeof usage.input_tokens === 'number')
641
+ totalInputTokens += usage.input_tokens;
642
+ if (typeof usage.output_tokens === 'number')
643
+ totalOutputTokens += usage.output_tokens;
644
+ if (typeof usage.cache_read_input_tokens === 'number')
645
+ totalCacheReadTokens += usage.cache_read_input_tokens;
646
+ if (typeof usage.cache_creation_input_tokens === 'number')
647
+ totalCacheWriteTokens += usage.cache_creation_input_tokens;
648
+ }
649
+ const eventId = obj.id || obj.uuid || `evt-${Date.parse(ts) || Math.random()}`;
650
+ const tr = extractToolResult(msg);
651
+ if (tr) {
652
+ pendingEvents.push([eventId, sessionId, ts, 'tool_result', 'tool', tr.content, tr.toolName, null, tr.content]);
653
+ continue;
654
+ }
655
+ const content = extractContent(msg);
656
+ const role = msg.role || 'unknown';
657
+ if (content) {
658
+ pendingEvents.push([eventId, sessionId, ts, 'message', role, content, null, null, null]);
659
+ msgCount++;
660
+ // Better summary: skip heartbeat/boilerplate messages
661
+ if (!summary && role === 'user' && isSummaryCandidate(content)) {
662
+ summary = content.slice(0, 200);
663
+ }
664
+ // Capture initial prompt from first substantial user message
665
+ if (!initialPrompt && role === 'user' && isSummaryCandidate(content)) {
666
+ initialPrompt = content.slice(0, 500); // Limit to 500 chars
667
+ firstMessageId = eventId;
668
+ firstMessageTimestamp = ts || null;
669
+ }
670
+ }
671
+ const tools = extractToolCalls(msg);
672
+ for (const tool of tools) {
673
+ pendingEvents.push([tool.id || `${eventId}-${tool.name}`, sessionId, ts, 'tool_call', role, null, tool.name, tool.args, null]);
674
+ toolCount++;
675
+ // File activity tracking
676
+ const fps = extractFilePaths(tool.name, tool.args);
677
+ for (const fp of fps) {
678
+ const op = tool.name.includes('write') || tool.name === 'Write' ? 'write'
679
+ : tool.name.includes('edit') || tool.name === 'Edit' ? 'edit'
680
+ : 'read';
681
+ fileActivities.push([sessionId, fp, op, ts]);
682
+ const project = extractProjectFromPath(fp, config);
683
+ if (project)
684
+ projectCounts.set(project, (projectCounts.get(project) || 0) + 1);
685
+ }
686
+ }
687
+ }
688
+ }
689
+ // Classify snapshot-only Claude files explicitly (avoid heartbeat mislabel)
690
+ if (isClaudeCode && sawSnapshotRecord && !sawNonSnapshotRecord) {
691
+ sessionType = 'snapshot';
692
+ if (!summary)
693
+ summary = 'Claude file snapshot';
694
+ }
695
+ // Normalize summary text
696
+ if (summary)
697
+ summary = stripLeadingDatetimePrefix(summary);
698
+ // If no real summary found, set a sensible default
699
+ if (!summary) {
700
+ if (isCodexCli) {
701
+ const parts = ['Codex CLI session'];
702
+ if (codexProvider)
703
+ parts.push(`provider=${codexProvider}`);
704
+ if (codexSource)
705
+ parts.push(`source=${codexSource}`);
706
+ if (codexOriginator)
707
+ parts.push(`originator=${codexOriginator}`);
708
+ summary = parts.join(' · ');
709
+ }
710
+ else {
711
+ summary = 'Heartbeat session';
712
+ }
713
+ }
714
+ // Infer session type from first user message content
715
+ if (!sessionType && initialPrompt) {
716
+ const p = initialPrompt.toLowerCase();
717
+ if (p.includes('[cron:'))
718
+ sessionType = 'cron';
719
+ else if (p.includes('heartbeat') && p.includes('heartbeat_ok'))
720
+ sessionType = 'heartbeat';
721
+ }
722
+ if (!sessionType && !initialPrompt)
723
+ sessionType = 'heartbeat';
724
+ // Detect subagent: task-style prompts injected by sessions_spawn
725
+ if (!sessionType && initialPrompt) {
726
+ const p = initialPrompt.trim();
727
+ if (/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-/.test(p) && !p.includes('[System Message]')) {
728
+ sessionType = 'subagent';
729
+ }
730
+ }
731
+ const modelsJson = modelsSet.size > 0 ? JSON.stringify([...modelsSet]) : null;
732
+ const projects = [...projectCounts.entries()]
733
+ .sort((a, b) => (b[1] - a[1]) || a[0].localeCompare(b[0]))
734
+ .map(([name]) => name);
735
+ const projectsJson = projects.length > 0 ? JSON.stringify(projects) : null;
736
+ // --- All DB operations in a single transaction for atomicity ---
737
+ const commitIndex = db.transaction(() => {
738
+ stmts.deleteEvents.run(sessionId);
739
+ stmts.deleteFileActivity.run(sessionId);
740
+ if (stmts.deleteArchive)
741
+ stmts.deleteArchive.run(sessionId);
742
+ stmts.deleteSession.run(sessionId);
743
+ stmts.upsertSession.run(sessionId, sessionStart, sessionEnd, msgCount, toolCount, model, summary, agent, sessionType, totalCost, totalTokens, totalInputTokens, totalOutputTokens, totalCacheReadTokens, totalCacheWriteTokens, initialPrompt, firstMessageId, firstMessageTimestamp, modelsJson, projectsJson);
744
+ for (const ev of pendingEvents)
745
+ stmts.insertEvent.run(...ev);
746
+ for (const fa of fileActivities)
747
+ stmts.insertFileActivity.run(...fa);
748
+ // Archive mode: store raw JSONL lines
749
+ if (archiveMode && stmts.insertArchive) {
750
+ for (let i = 0; i < lines.length; i++) {
751
+ stmts.insertArchive.run(sessionId, i + 1, lines[i]);
752
+ }
753
+ }
754
+ stmts.upsertState.run(filePath, lines.length, mtime);
755
+ });
756
+ commitIndex();
757
+ return { sessionId: sessionId, msgCount, toolCount };
758
+ }
759
+ function run() {
760
+ const config = (0, config_js_1.loadConfig)();
761
+ (0, db_js_1.init)();
762
+ const db = (0, db_js_1.open)();
763
+ const archiveMode = config.storage === 'archive';
764
+ console.log(`AgentActa indexer running in ${config.storage} mode`);
765
+ const stmts = (0, db_js_1.createStmts)(db);
766
+ const sessionDirs = discoverSessionDirs(config);
767
+ console.log(`Discovered ${sessionDirs.length} session directories:`);
768
+ sessionDirs.forEach(d => console.log(` ${d.agent}: ${d.path}`));
769
+ let allFiles = [];
770
+ for (const dir of sessionDirs) {
771
+ const files = listJsonlFiles(dir.path, !!dir.recursive)
772
+ .map(filePath => ({ path: filePath, agent: dir.agent, sourceType: dir.sourceType || 'transcript' }));
773
+ allFiles.push(...files);
774
+ }
775
+ console.log(`Found ${allFiles.length} session files`);
776
+ const indexMany = db.transaction(() => {
777
+ let indexed = 0;
778
+ for (const f of allFiles) {
779
+ const result = f.sourceType === 'cron-run'
780
+ ? indexCronRunFile(db, f.path, f.agent, stmts)
781
+ : indexFile(db, f.path, f.agent, stmts, archiveMode, config);
782
+ if (!result.skipped) {
783
+ indexed++;
784
+ if (indexed % 10 === 0)
785
+ process.stdout.write('.');
786
+ }
787
+ }
788
+ return indexed;
789
+ });
790
+ const count = indexMany();
791
+ console.log(`\nIndexed ${count} sessions`);
792
+ const stats = db.prepare('SELECT COUNT(*) as sessions FROM sessions').get();
793
+ const evStats = db.prepare('SELECT COUNT(*) as events FROM events').get();
794
+ console.log(`Total: ${stats.sessions} sessions, ${evStats.events} events`);
795
+ if (WATCH) {
796
+ console.log('\nWatching for changes...');
797
+ const rescanTimers = new Map();
798
+ for (const dir of sessionDirs) {
799
+ node_fs_1.default.watch(dir.path, { persistent: true }, (_eventType, filename) => {
800
+ // Recursive sources (e.g. ~/.codex/sessions/YYYY/MM/DD/*.jsonl):
801
+ // fs.watch on Linux does not watch nested dirs recursively, so on any root event
802
+ // run a debounced full rescan of known JSONL files under this source.
803
+ if (dir.recursive) {
804
+ const key = dir.path;
805
+ const existing = rescanTimers.get(key);
806
+ if (existing)
807
+ clearTimeout(existing);
808
+ const t = setTimeout(() => {
809
+ try {
810
+ const files = listJsonlFiles(dir.path, true);
811
+ let changed = 0;
812
+ for (const filePath of files) {
813
+ const result = dir.sourceType === 'cron-run'
814
+ ? indexCronRunFile(db, filePath, dir.agent, stmts)
815
+ : indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
816
+ if (!result.skipped)
817
+ changed++;
818
+ }
819
+ if (changed > 0)
820
+ console.log(`Re-indexed ${changed} files (${dir.agent})`);
821
+ }
822
+ catch (err) {
823
+ console.error(`Error rescanning ${dir.path}:`, err.message);
824
+ }
825
+ }, 500);
826
+ rescanTimers.set(key, t);
827
+ return;
828
+ }
829
+ if (!filename || !filename.endsWith('.jsonl'))
830
+ return;
831
+ const filePath = node_path_1.default.join(dir.path, filename);
832
+ if (!node_fs_1.default.existsSync(filePath))
833
+ return;
834
+ setTimeout(() => {
835
+ try {
836
+ const result = dir.sourceType === 'cron-run'
837
+ ? indexCronRunFile(db, filePath, dir.agent, stmts)
838
+ : indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
839
+ if (!result.skipped)
840
+ console.log(`Re-indexed: ${filename} (${dir.agent})`);
841
+ }
842
+ catch (err) {
843
+ console.error(`Error re-indexing ${filename}:`, err.message);
844
+ }
845
+ }, 500);
846
+ });
847
+ }
848
+ }
849
+ else {
850
+ db.close();
851
+ }
852
+ }
853
+ function indexAll(db, config) {
854
+ const sessionDirs = discoverSessionDirs(config);
855
+ const archiveMode = config.storage === 'archive';
856
+ const stmts = (0, db_js_1.createStmts)(db);
857
+ let totalSessions = 0;
858
+ for (const dir of sessionDirs) {
859
+ const files = listJsonlFiles(dir.path, !!dir.recursive);
860
+ for (const filePath of files) {
861
+ try {
862
+ const result = dir.sourceType === 'cron-run'
863
+ ? indexCronRunFile(db, filePath, dir.agent, stmts)
864
+ : indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
865
+ if (!result.skipped)
866
+ totalSessions++;
867
+ }
868
+ catch (err) {
869
+ console.error(`Error indexing ${node_path_1.default.basename(filePath)}:`, err.message);
870
+ }
871
+ }
872
+ }
873
+ const stats = db.prepare('SELECT COUNT(*) as sessions FROM sessions').get();
874
+ const evStats = db.prepare('SELECT COUNT(*) as events FROM events').get();
875
+ return { sessions: stats.sessions, events: evStats.events, newSessions: totalSessions };
876
+ }
877
+ if (require.main === module)
878
+ run();
879
+ //# sourceMappingURL=indexer.js.map