agentacta 2026.4.8 → 2026.5.23

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