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