agentacta 1.3.3 → 1.4.0

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 CHANGED
@@ -6,6 +6,25 @@ const { loadConfig } = require('./config');
6
6
  const REINDEX = process.argv.includes('--reindex');
7
7
  const WATCH = process.argv.includes('--watch');
8
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
+
9
28
  function discoverSessionDirs(config) {
10
29
  const dirs = [];
11
30
  const home = process.env.HOME;
@@ -48,6 +67,12 @@ function discoverSessionDirs(config) {
48
67
  }
49
68
  }
50
69
 
70
+ // Scan ~/.codex/sessions recursively (Codex CLI stores nested YYYY/MM/DD/*.jsonl)
71
+ const codexSessions = path.join(home, '.codex/sessions');
72
+ if (fs.existsSync(codexSessions) && fs.statSync(codexSessions).isDirectory()) {
73
+ dirs.push({ path: codexSessions, agent: 'codex-cli', recursive: true });
74
+ }
75
+
51
76
  if (!dirs.length) {
52
77
  // Fallback to hardcoded
53
78
  const fallback = path.join(home, '.openclaw/agents/main/sessions');
@@ -63,6 +88,28 @@ function isHeartbeat(text) {
63
88
  return lower.includes('heartbeat') || lower.includes('heartbeat_ok');
64
89
  }
65
90
 
91
+ function isBoilerplatePrompt(text) {
92
+ if (!text) return false;
93
+ const lower = text.toLowerCase();
94
+ return lower.includes('<permissions instructions>')
95
+ || lower.includes('filesystem sandboxing defines which files can be read or written')
96
+ || lower.includes('# agents.md instructions for ');
97
+ }
98
+
99
+ function isSummaryCandidate(text) {
100
+ if (!text || text.trim().length <= 10) return false;
101
+ if (isHeartbeat(text)) return false;
102
+ if (isBoilerplatePrompt(text)) return false;
103
+ return true;
104
+ }
105
+
106
+ function stripLeadingDatetimePrefix(text) {
107
+ if (!text) return text;
108
+ return text
109
+ .replace(/^\[(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}[^\]]*\]\s*/i, '')
110
+ .trim();
111
+ }
112
+
66
113
  function extractContent(msg) {
67
114
  if (!msg || !msg.content) return '';
68
115
  if (typeof msg.content === 'string') return msg.content;
@@ -94,17 +141,60 @@ function extractToolResult(msg) {
94
141
  return null;
95
142
  }
96
143
 
144
+ function extractCodexMessageText(content) {
145
+ if (!content) return '';
146
+ if (typeof content === 'string') return content;
147
+ if (!Array.isArray(content)) return '';
148
+
149
+ return content
150
+ .map((part) => {
151
+ if (!part || typeof part !== 'object') return '';
152
+ if (typeof part.text === 'string') return part.text;
153
+ if (typeof part.output_text === 'string') return part.output_text;
154
+ if (typeof part.input_text === 'string') return part.input_text;
155
+ return '';
156
+ })
157
+ .filter(Boolean)
158
+ .join('\n');
159
+ }
160
+
97
161
  function extractFilePaths(toolName, toolArgs) {
98
162
  const paths = [];
99
163
  if (!toolArgs) return paths;
164
+
165
+ const maybePath = (value) => {
166
+ if (typeof value !== 'string') return;
167
+ if (value.startsWith('/') || value.startsWith('~/') || value.startsWith('./') || value.startsWith('../')) {
168
+ paths.push(value);
169
+ return;
170
+ }
171
+ if (value.includes('/') || value.includes('\\')) paths.push(value);
172
+ };
173
+
174
+ const visit = (obj) => {
175
+ if (!obj || typeof obj !== 'object') return;
176
+ if (Array.isArray(obj)) {
177
+ for (const item of obj) visit(item);
178
+ return;
179
+ }
180
+
181
+ for (const [key, value] of Object.entries(obj)) {
182
+ if (typeof value === 'string') {
183
+ if (['path', 'file_path', 'filePath', 'file', 'filename', 'cwd', 'workdir', 'directory', 'dir'].includes(key)) {
184
+ maybePath(value);
185
+ }
186
+ } else if (value && typeof value === 'object') {
187
+ visit(value);
188
+ }
189
+ }
190
+ };
191
+
100
192
  try {
101
193
  const args = typeof toolArgs === 'string' ? JSON.parse(toolArgs) : toolArgs;
102
- // Common field names for file paths
103
- for (const key of ['path', 'file_path', 'filePath', 'file', 'filename']) {
104
- if (args[key] && typeof args[key] === 'string') paths.push(args[key]);
105
- }
194
+ visit(args);
106
195
  } catch {}
107
- return paths;
196
+
197
+ return [...new Set(paths)];
108
198
  }
109
199
 
110
200
  function aliasProject(project, config) {
@@ -177,9 +267,20 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
177
267
  let initialPrompt = null;
178
268
  let firstMessageId = null;
179
269
  let firstMessageTimestamp = null;
270
+ let codexProvider = null;
271
+ let codexSource = null;
272
+ let sawSnapshotRecord = false;
273
+ let sawNonSnapshotRecord = false;
274
+
275
+ let firstLine;
276
+ try {
277
+ firstLine = JSON.parse(lines[0]);
278
+ } catch {
279
+ return { skipped: true };
280
+ }
180
281
 
181
- const firstLine = JSON.parse(lines[0]);
182
282
  let isClaudeCode = false;
283
+ let isCodexCli = false;
183
284
 
184
285
  if (firstLine.type === 'session') {
185
286
  // OpenClaw format
@@ -204,6 +305,20 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
204
305
  sessionId = path.basename(filePath, '.jsonl');
205
306
  sessionStart = new Date(firstLine.timestamp || Date.now()).toISOString();
206
307
  }
308
+ } else if (firstLine.type === 'session_meta') {
309
+ // Codex CLI format
310
+ isCodexCli = true;
311
+ const meta = firstLine.payload || {};
312
+ sessionId = meta.id || path.basename(filePath, '.jsonl');
313
+ sessionStart = meta.timestamp || firstLine.timestamp || new Date().toISOString();
314
+ sessionType = 'codex-cli';
315
+ agent = 'codex-cli';
316
+ if (meta.model) {
317
+ model = meta.model;
318
+ modelsSet.add(meta.model);
319
+ }
320
+ codexProvider = meta.model_provider || null;
321
+ codexSource = meta.source || null;
207
322
  } else {
208
323
  return { skipped: true };
209
324
  }
@@ -214,8 +329,9 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
214
329
  const projectCounts = new Map();
215
330
 
216
331
  // Seed project from session cwd when available (helps chat-only sessions)
217
- if (firstLine && firstLine.cwd) {
218
- const p = extractProjectFromPath(firstLine.cwd);
332
+ const sessionCwd = (firstLine && firstLine.cwd) || (firstLine && firstLine.payload && firstLine.payload.cwd);
333
+ if (sessionCwd) {
334
+ const p = extractProjectFromPath(sessionCwd, config);
219
335
  if (p) projectCounts.set(p, 1);
220
336
  }
221
337
 
@@ -223,6 +339,111 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
223
339
  let obj;
224
340
  try { obj = JSON.parse(line); } catch { continue; }
225
341
 
342
+ if (obj.type === 'file-history-snapshot') sawSnapshotRecord = true;
343
+ else sawNonSnapshotRecord = true;
344
+
345
+ if (isCodexCli) {
346
+ if (obj.type === 'session_meta') {
347
+ const meta = obj.payload || {};
348
+ if (meta.id) sessionId = meta.id;
349
+ if (meta.timestamp && !sessionStart) sessionStart = meta.timestamp;
350
+ if (meta.model) {
351
+ if (!model) model = meta.model;
352
+ modelsSet.add(meta.model);
353
+ }
354
+ if (meta.model_provider) codexProvider = meta.model_provider;
355
+ if (meta.source) codexSource = meta.source;
356
+ if (meta.model_provider && !model) model = meta.model_provider;
357
+ continue;
358
+ }
359
+
360
+ if (obj.type === 'turn_context' && obj.payload) {
361
+ const tc = obj.payload;
362
+ if (tc.model && typeof tc.model === 'string') {
363
+ if (!model || model === codexProvider) model = tc.model;
364
+ modelsSet.add(tc.model);
365
+ }
366
+ continue;
367
+ }
368
+
369
+ if (obj.type === 'response_item' && obj.payload) {
370
+ const p = obj.payload;
371
+ const ts = obj.timestamp || sessionStart;
372
+ const eventId = `evt-${obj.type}-${Date.parse(ts) || Math.random()}`;
373
+
374
+ if (p.type === 'function_call') {
375
+ const toolName = p.name || p.tool_name || '';
376
+ const toolArgs = typeof p.arguments === 'string' ? p.arguments : JSON.stringify(p.arguments || {});
377
+ const callBaseId = p.call_id || p.id || eventId;
378
+ pendingEvents.push([`${callBaseId}:call`, sessionId, ts, 'tool_call', 'assistant', null, toolName, toolArgs, null]);
379
+ toolCount++;
380
+
381
+ const fps = extractFilePaths(toolName, toolArgs);
382
+ for (const fp of fps) {
383
+ fileActivities.push([sessionId, fp, 'read', ts]);
384
+ const project = extractProjectFromPath(fp, config);
385
+ if (project) projectCounts.set(project, (projectCounts.get(project) || 0) + 1);
386
+ }
387
+ sessionEnd = ts;
388
+ continue;
389
+ }
390
+
391
+ if (p.type === 'function_call_output') {
392
+ const output = (typeof p.output === 'string' ? p.output : JSON.stringify(p.output || '')).slice(0, 10000);
393
+ const resultBaseId = p.call_id || p.id || eventId;
394
+ pendingEvents.push([`${resultBaseId}:result`, sessionId, ts, 'tool_result', 'tool', output, p.name || p.tool_name || '', null, output]);
395
+ sessionEnd = ts;
396
+ continue;
397
+ }
398
+
399
+ if (p.type === 'message') {
400
+ const rawRole = p.role || 'assistant';
401
+ const role = rawRole === 'assistant' ? 'assistant' : 'user';
402
+ const content = extractCodexMessageText(p.content);
403
+ if (content) {
404
+ pendingEvents.push([p.id || eventId, sessionId, ts, 'message', role, content, null, null, null]);
405
+ msgCount++;
406
+ if (!summary && role === 'user' && isSummaryCandidate(content)) summary = content.slice(0, 200);
407
+ if (!initialPrompt && role === 'user' && isSummaryCandidate(content)) {
408
+ initialPrompt = content.slice(0, 500);
409
+ firstMessageId = p.id || eventId;
410
+ firstMessageTimestamp = ts;
411
+ }
412
+ }
413
+ sessionEnd = ts;
414
+ continue;
415
+ }
416
+ }
417
+
418
+ if (obj.type === 'event_msg' && obj.payload) {
419
+ const p = obj.payload;
420
+ const ts = obj.timestamp || sessionStart;
421
+ const eventId = `evt-${p.type || 'event'}-${Date.parse(ts) || Math.random()}`;
422
+
423
+ if (p.type === 'agent_message' && p.message) {
424
+ pendingEvents.push([eventId, sessionId, ts, 'message', 'assistant', p.message, null, null, null]);
425
+ msgCount++;
426
+ sessionEnd = ts;
427
+ continue;
428
+ }
429
+
430
+ if (p.type === 'user_message' && p.message) {
431
+ pendingEvents.push([eventId, sessionId, ts, 'message', 'user', p.message, null, null, null]);
432
+ msgCount++;
433
+ if (!summary && isSummaryCandidate(p.message)) summary = p.message.slice(0, 200);
434
+ if (!initialPrompt && isSummaryCandidate(p.message)) {
435
+ initialPrompt = p.message.slice(0, 500);
436
+ firstMessageId = eventId;
437
+ firstMessageTimestamp = ts;
438
+ }
439
+ sessionEnd = ts;
440
+ continue;
441
+ }
442
+ }
443
+
444
+ continue;
445
+ }
446
+
226
447
  if (obj.type === 'session' || obj.type === 'model_change' || obj.type === 'thinking_level_change' || obj.type === 'custom' || obj.type === 'file-history-snapshot') {
227
448
  if (obj.type === 'model_change' && obj.modelId) {
228
449
  if (!model) model = obj.modelId; // First model for backwards compat
@@ -289,12 +510,12 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
289
510
  if (content) {
290
511
  pendingEvents.push([eventId, sessionId, ts, 'message', role, content, null, null, null]);
291
512
  msgCount++;
292
- // Better summary: skip heartbeat messages
293
- if (!summary && role === 'user' && !isHeartbeat(content)) {
513
+ // Better summary: skip heartbeat/boilerplate messages
514
+ if (!summary && role === 'user' && isSummaryCandidate(content)) {
294
515
  summary = content.slice(0, 200);
295
516
  }
296
517
  // Capture initial prompt from first substantial user message
297
- if (!initialPrompt && role === 'user' && content.trim().length > 10 && !isHeartbeat(content)) {
518
+ if (!initialPrompt && role === 'user' && isSummaryCandidate(content)) {
298
519
  initialPrompt = content.slice(0, 500); // Limit to 500 chars
299
520
  firstMessageId = eventId;
300
521
  firstMessageTimestamp = ts;
@@ -321,9 +542,25 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
321
542
  }
322
543
  }
323
544
 
324
- // If no real summary found, check if it's a heartbeat session
545
+ // Classify snapshot-only Claude files explicitly (avoid heartbeat mislabel)
546
+ if (isClaudeCode && sawSnapshotRecord && !sawNonSnapshotRecord) {
547
+ sessionType = 'snapshot';
548
+ if (!summary) summary = 'Claude file snapshot';
549
+ }
550
+
551
+ // Normalize summary text
552
+ if (summary) summary = stripLeadingDatetimePrefix(summary);
553
+
554
+ // If no real summary found, set a sensible default
325
555
  if (!summary) {
326
- summary = 'Heartbeat session';
556
+ if (isCodexCli) {
557
+ const parts = ['Codex CLI session'];
558
+ if (codexProvider) parts.push(`provider=${codexProvider}`);
559
+ if (codexSource) parts.push(`source=${codexSource}`);
560
+ summary = parts.join(' · ');
561
+ } else {
562
+ summary = 'Heartbeat session';
563
+ }
327
564
  }
328
565
 
329
566
  // Infer session type from first user message content
@@ -389,9 +626,8 @@ function run() {
389
626
 
390
627
  let allFiles = [];
391
628
  for (const dir of sessionDirs) {
392
- const files = fs.readdirSync(dir.path)
393
- .filter(f => f.endsWith('.jsonl'))
394
- .map(f => ({ path: path.join(dir.path, f), agent: dir.agent }));
629
+ const files = listJsonlFiles(dir.path, !!dir.recursive)
630
+ .map(filePath => ({ path: filePath, agent: dir.agent }));
395
631
  allFiles.push(...files);
396
632
  }
397
633
 
@@ -418,8 +654,33 @@ function run() {
418
654
 
419
655
  if (WATCH) {
420
656
  console.log('\nWatching for changes...');
657
+ const rescanTimers = new Map();
658
+
421
659
  for (const dir of sessionDirs) {
422
660
  fs.watch(dir.path, { persistent: true }, (eventType, filename) => {
661
+ // Recursive sources (e.g. ~/.codex/sessions/YYYY/MM/DD/*.jsonl):
662
+ // fs.watch on Linux does not watch nested dirs recursively, so on any root event
663
+ // run a debounced full rescan of known JSONL files under this source.
664
+ if (dir.recursive) {
665
+ const key = dir.path;
666
+ if (rescanTimers.get(key)) clearTimeout(rescanTimers.get(key));
667
+ const t = setTimeout(() => {
668
+ try {
669
+ const files = listJsonlFiles(dir.path, true);
670
+ let changed = 0;
671
+ for (const filePath of files) {
672
+ const result = indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
673
+ if (!result.skipped) changed++;
674
+ }
675
+ if (changed > 0) console.log(`Re-indexed ${changed} files (${dir.agent})`);
676
+ } catch (err) {
677
+ console.error(`Error rescanning ${dir.path}:`, err.message);
678
+ }
679
+ }, 500);
680
+ rescanTimers.set(key, t);
681
+ return;
682
+ }
683
+
423
684
  if (!filename || !filename.endsWith('.jsonl')) return;
424
685
  const filePath = path.join(dir.path, filename);
425
686
  if (!fs.existsSync(filePath)) return;
@@ -444,13 +705,13 @@ function indexAll(db, config) {
444
705
  const stmts = createStmts(db);
445
706
  let totalSessions = 0;
446
707
  for (const dir of sessionDirs) {
447
- const files = fs.readdirSync(dir.path).filter(f => f.endsWith('.jsonl'));
448
- for (const file of files) {
708
+ const files = listJsonlFiles(dir.path, !!dir.recursive);
709
+ for (const filePath of files) {
449
710
  try {
450
- const result = indexFile(db, path.join(dir.path, file), dir.agent, stmts, archiveMode, config);
711
+ const result = indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
451
712
  if (!result.skipped) totalSessions++;
452
713
  } catch (err) {
453
- console.error(`Error indexing ${file}:`, err.message);
714
+ console.error(`Error indexing ${path.basename(filePath)}:`, err.message);
454
715
  }
455
716
  }
456
717
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentacta",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Audit trail and search engine for AI agent sessions",
5
5
  "main": "index.js",
6
6
  "bin": {