agentacta 2026.3.12 โ 2026.3.27
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/README.md +16 -2
- package/config.js +2 -0
- package/db.js +12 -0
- package/delta-attribution-context.js +57 -0
- package/index.js +93 -13
- package/indexer.js +31 -4
- package/insights.js +260 -0
- package/package.json +4 -1
- package/project-attribution.js +443 -0
- package/public/app.js +313 -22
- package/public/index.html +13 -10
- package/public/style.css +197 -16
package/README.md
CHANGED
|
@@ -32,12 +32,15 @@ AgentActa gives you one place to inspect the full trail.
|
|
|
32
32
|
|
|
33
33
|
- ๐ Full-text search across messages, tool calls, and results
|
|
34
34
|
- ๐ Session browser with summaries, token breakdowns, and model info
|
|
35
|
+
- ๐งญ Project-scoped session filtering with per-event attribution
|
|
36
|
+
- ๐ค Clear Codex run visibility for direct and Symphony-origin sessions
|
|
35
37
|
- ๐
Timeline view with live updates for today
|
|
36
38
|
- ๐ File activity across all indexed sessions
|
|
37
39
|
- ๐ Light and dark themes
|
|
38
40
|
- ๐ Stats for sessions, messages, tools, and tokens
|
|
39
41
|
- โก Live indexing via file watching
|
|
40
|
-
- ๐ฑ Mobile-
|
|
42
|
+
- ๐ฑ Mobile-optimized UI with floating navigation
|
|
43
|
+
- ๐ฅ Session health scoring โ reliability scores, issue detection, and per-signal breakdowns
|
|
41
44
|
- ๐ก Search suggestions based on real data
|
|
42
45
|
- โจ๏ธ Command palette (โK / Ctrl+K) for quick navigation
|
|
43
46
|
- ๐จ Theme settings (system, light, dark, OLED)
|
|
@@ -80,12 +83,22 @@ Suggestions come from your own dataset: top tools, common topics, frequently tou
|
|
|
80
83
|
|
|
81
84
|
Browse indexed sessions with auto-generated summaries, token splits (input/output), and model details. Click into any session to see the full event history.
|
|
82
85
|
|
|
83
|
-
Session
|
|
86
|
+
Session detail view supports project-scoped filtering, so mixed-project sessions can be narrowed down without losing the full underlying transcript. The Initial Prompt jump still resolves from full session context even when a project filter is active.
|
|
87
|
+
|
|
88
|
+
Session types get tagged so noisy categories are easier to spot (cron, sub-agent, heartbeat). Codex-backed work is also distinguished more clearly, including direct Codex runs and Symphony-origin Codex sessions.
|
|
84
89
|
|
|
85
90
|
### Timeline
|
|
86
91
|
|
|
87
92
|
Pick a date, see everything that happened, newest first. Today's view updates live as new events come in.
|
|
88
93
|
|
|
94
|
+
### Insights
|
|
95
|
+
|
|
96
|
+
The Insights tab surfaces session health across your entire history.
|
|
97
|
+
|
|
98
|
+
It tracks five issue signals โ repeated tool loops, sessions that produced no output, high error rates, vague instructions, and incomplete sessions. Each signal is severity-scaled so scores reflect how bad the problem actually was, not just whether it occurred.
|
|
99
|
+
|
|
100
|
+
The reliability score (0โ100) is the inverse of the confusion score: higher means the agent completed work cleanly. The issue rate shows what percentage of possible signal types were detected in a session.
|
|
101
|
+
|
|
89
102
|
### File Activity
|
|
90
103
|
|
|
91
104
|
See what files were touched, how often, and by which sessions.
|
|
@@ -163,6 +176,7 @@ Default config (auto-generated on first run โ session directories are detected
|
|
|
163
176
|
| `GET /api/timeline/stream?after=<ts>` | SSE stream for live timeline updates |
|
|
164
177
|
| `POST /api/maintenance` | VACUUM + WAL checkpoint (returns size before/after) |
|
|
165
178
|
| `GET /api/health` | Server status, version, uptime, session count |
|
|
179
|
+
| `GET /api/insights` | Session health summary โ reliability scores, issue counts, top flagged sessions |
|
|
166
180
|
| `GET /api/export/search?q=<query>&format=md` | Export search results |
|
|
167
181
|
|
|
168
182
|
### Context API
|
package/config.js
CHANGED
|
@@ -57,6 +57,8 @@ function loadConfig() {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// In demo mode, ignore file-based sessionsPath so live data doesn't bleed in
|
|
61
|
+
if (process.env.AGENTACTA_DEMO_MODE) delete fileConfig.sessionsPath;
|
|
60
62
|
const config = { ...DEFAULTS, ...fileConfig };
|
|
61
63
|
|
|
62
64
|
// Env var overrides (highest priority)
|
package/db.js
CHANGED
|
@@ -103,6 +103,18 @@ function init(dbPath) {
|
|
|
103
103
|
);
|
|
104
104
|
|
|
105
105
|
CREATE INDEX IF NOT EXISTS idx_archive_session ON archive(session_id);
|
|
106
|
+
|
|
107
|
+
CREATE TABLE IF NOT EXISTS session_insights (
|
|
108
|
+
session_id TEXT PRIMARY KEY,
|
|
109
|
+
signals TEXT,
|
|
110
|
+
confusion_score INTEGER DEFAULT 0,
|
|
111
|
+
flagged INTEGER DEFAULT 0,
|
|
112
|
+
computed_at TEXT,
|
|
113
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_insights_flagged ON session_insights(flagged);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_insights_score ON session_insights(confusion_score DESC);
|
|
106
118
|
`);
|
|
107
119
|
|
|
108
120
|
// Add columns if missing (migration)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function extractCallBaseId(id) {
|
|
4
|
+
if (!id) return '';
|
|
5
|
+
return String(id).replace(/:(call|result)$/, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function loadDeltaAttributionContext(db, sessionId, rows) {
|
|
9
|
+
if (!db || !Array.isArray(rows) || !rows.length) return [];
|
|
10
|
+
|
|
11
|
+
const ordered = [...rows].sort((a, b) => {
|
|
12
|
+
const ta = Date.parse(a?.timestamp || 0) || 0;
|
|
13
|
+
const tb = Date.parse(b?.timestamp || 0) || 0;
|
|
14
|
+
if (ta !== tb) return ta - tb;
|
|
15
|
+
return String(a?.id || '').localeCompare(String(b?.id || ''));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const first = ordered[0];
|
|
19
|
+
const firstTs = first?.timestamp || '1970-01-01T00:00:00.000Z';
|
|
20
|
+
const firstId = first?.id || '';
|
|
21
|
+
const neighborhoodRows = db.prepare(
|
|
22
|
+
`SELECT * FROM events
|
|
23
|
+
WHERE session_id = ?
|
|
24
|
+
AND (timestamp < ? OR (timestamp = ? AND id < ?))
|
|
25
|
+
ORDER BY timestamp DESC, id DESC
|
|
26
|
+
LIMIT 12`
|
|
27
|
+
).all(sessionId, firstTs, firstTs, firstId).reverse();
|
|
28
|
+
|
|
29
|
+
const callIds = [...new Set(
|
|
30
|
+
rows
|
|
31
|
+
.filter(row => row && row.type === 'tool_result')
|
|
32
|
+
.map(row => extractCallBaseId(row.id))
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.map(base => `${base}:call`)
|
|
35
|
+
)];
|
|
36
|
+
|
|
37
|
+
if (!callIds.length) return neighborhoodRows;
|
|
38
|
+
|
|
39
|
+
const placeholders = callIds.map(() => '?').join(',');
|
|
40
|
+
const linkedCallRows = db.prepare(
|
|
41
|
+
`SELECT * FROM events
|
|
42
|
+
WHERE session_id = ?
|
|
43
|
+
AND type = 'tool_call'
|
|
44
|
+
AND id IN (${placeholders})`
|
|
45
|
+
).all(sessionId, ...callIds);
|
|
46
|
+
|
|
47
|
+
const merged = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
for (const row of [...neighborhoodRows, ...linkedCallRows]) {
|
|
50
|
+
if (!row || !row.id || seen.has(row.id)) continue;
|
|
51
|
+
seen.add(row.id);
|
|
52
|
+
merged.push(row);
|
|
53
|
+
}
|
|
54
|
+
return merged;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { loadDeltaAttributionContext };
|
package/index.js
CHANGED
|
@@ -20,12 +20,16 @@ if (process.argv.includes('--demo')) {
|
|
|
20
20
|
}
|
|
21
21
|
process.env.AGENTACTA_SESSIONS_PATH = demoDir;
|
|
22
22
|
process.env.AGENTACTA_DB_PATH = path.join(demoDir, 'demo.db');
|
|
23
|
+
process.env.AGENTACTA_DEMO_MODE = '1'; // signal to config.js to skip file-based sessionsPath
|
|
23
24
|
console.log(`Demo mode: using sessions from ${demoDir}`);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const { loadConfig } = require('./config');
|
|
27
28
|
const { open, init, createStmts } = require('./db');
|
|
28
|
-
const { discoverSessionDirs, indexFile } = require('./indexer');
|
|
29
|
+
const { discoverSessionDirs, listJsonlFiles, indexFile } = require('./indexer');
|
|
30
|
+
const { attributeSessionEvents, attributeEventDelta } = require('./project-attribution');
|
|
31
|
+
const { loadDeltaAttributionContext } = require('./delta-attribution-context');
|
|
32
|
+
const { analyzeSession, analyzeAll, getInsightsSummary } = require('./insights');
|
|
29
33
|
|
|
30
34
|
const config = loadConfig();
|
|
31
35
|
const PORT = config.port;
|
|
@@ -154,26 +158,68 @@ const sessionDirs = discoverSessionDirs(config);
|
|
|
154
158
|
|
|
155
159
|
// Initial indexing pass
|
|
156
160
|
for (const dir of sessionDirs) {
|
|
157
|
-
const files =
|
|
158
|
-
for (const
|
|
161
|
+
const files = listJsonlFiles(dir.path, !!dir.recursive);
|
|
162
|
+
for (const filePath of files) {
|
|
159
163
|
try {
|
|
160
|
-
const result = indexFile(db,
|
|
161
|
-
if (!result.skipped) console.log(`Indexed: ${
|
|
164
|
+
const result = indexFile(db, filePath, dir.agent, stmts, ARCHIVE_MODE, config);
|
|
165
|
+
if (!result.skipped) console.log(`Indexed: ${path.basename(filePath)} (${dir.agent})`);
|
|
162
166
|
} catch (err) {
|
|
163
|
-
console.error(`Error indexing ${
|
|
167
|
+
console.error(`Error indexing ${path.basename(filePath)}:`, err.message);
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
170
|
}
|
|
167
171
|
|
|
172
|
+
// Compute insights for all indexed sessions
|
|
173
|
+
try {
|
|
174
|
+
analyzeAll(db);
|
|
175
|
+
console.log('Insights computed for all sessions');
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error('Error computing insights:', err.message);
|
|
178
|
+
}
|
|
179
|
+
|
|
168
180
|
console.log(`Watching ${sessionDirs.length} session directories`);
|
|
169
181
|
|
|
170
182
|
// Debounce map: filePath -> timeout handle
|
|
171
183
|
const _reindexTimers = new Map();
|
|
172
184
|
const REINDEX_DEBOUNCE_MS = 2000;
|
|
185
|
+
const RECURSIVE_RESCAN_MS = 15000;
|
|
186
|
+
|
|
187
|
+
function reindexRecursiveDir(dir) {
|
|
188
|
+
try {
|
|
189
|
+
const files = listJsonlFiles(dir.path, true);
|
|
190
|
+
let changed = 0;
|
|
191
|
+
const upsert = db.prepare('INSERT OR REPLACE INTO session_insights (session_id, signals, confusion_score, flagged, computed_at) VALUES (?, ?, ?, ?, ?)');
|
|
192
|
+
for (const filePath of files) {
|
|
193
|
+
const result = indexFile(db, filePath, dir.agent, stmts, ARCHIVE_MODE, config);
|
|
194
|
+
if (!result.skipped) {
|
|
195
|
+
changed++;
|
|
196
|
+
if (result.sessionId) {
|
|
197
|
+
try {
|
|
198
|
+
const insight = analyzeSession(db, result.sessionId);
|
|
199
|
+
if (insight) upsert.run(insight.session_id, JSON.stringify(insight.signals), insight.confusion_score, insight.flagged ? 1 : 0, insight.computed_at);
|
|
200
|
+
} catch {}
|
|
201
|
+
sseEmitter.emit('session-update', result.sessionId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (changed > 0) console.log(`Live re-indexed ${changed} files (${dir.agent})`);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.error(`Error rescanning ${dir.path}:`, err.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
173
210
|
|
|
174
211
|
for (const dir of sessionDirs) {
|
|
175
212
|
try {
|
|
176
213
|
fs.watch(dir.path, { persistent: false }, (eventType, filename) => {
|
|
214
|
+
if (dir.recursive) {
|
|
215
|
+
if (_reindexTimers.has(dir.path)) clearTimeout(_reindexTimers.get(dir.path));
|
|
216
|
+
_reindexTimers.set(dir.path, setTimeout(() => {
|
|
217
|
+
_reindexTimers.delete(dir.path);
|
|
218
|
+
reindexRecursiveDir(dir);
|
|
219
|
+
}, REINDEX_DEBOUNCE_MS));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
177
223
|
if (!filename || !filename.endsWith('.jsonl')) return;
|
|
178
224
|
const filePath = path.join(dir.path, filename);
|
|
179
225
|
if (!fs.existsSync(filePath)) return;
|
|
@@ -183,10 +229,13 @@ for (const dir of sessionDirs) {
|
|
|
183
229
|
_reindexTimers.set(filePath, setTimeout(() => {
|
|
184
230
|
_reindexTimers.delete(filePath);
|
|
185
231
|
try {
|
|
186
|
-
const result = indexFile(db, filePath, dir.agent, stmts, ARCHIVE_MODE);
|
|
232
|
+
const result = indexFile(db, filePath, dir.agent, stmts, ARCHIVE_MODE, config);
|
|
187
233
|
if (!result.skipped) {
|
|
188
234
|
console.log(`Live re-indexed: ${filename} (${dir.agent})`);
|
|
189
|
-
if (result.sessionId)
|
|
235
|
+
if (result.sessionId) {
|
|
236
|
+
try { analyzeSession(db, result.sessionId); const upsert = db.prepare('INSERT OR REPLACE INTO session_insights (session_id, signals, confusion_score, flagged, computed_at) VALUES (?, ?, ?, ?, ?)'); const insight = analyzeSession(db, result.sessionId); if (insight) upsert.run(insight.session_id, JSON.stringify(insight.signals), insight.confusion_score, insight.flagged ? 1 : 0, insight.computed_at); } catch {}
|
|
237
|
+
sseEmitter.emit('session-update', result.sessionId);
|
|
238
|
+
}
|
|
190
239
|
}
|
|
191
240
|
} catch (err) {
|
|
192
241
|
console.error(`Error re-indexing ${filename}:`, err.message);
|
|
@@ -194,6 +243,10 @@ for (const dir of sessionDirs) {
|
|
|
194
243
|
}, REINDEX_DEBOUNCE_MS));
|
|
195
244
|
});
|
|
196
245
|
console.log(` Watching: ${dir.path}`);
|
|
246
|
+
if (dir.recursive) {
|
|
247
|
+
const timer = setInterval(() => reindexRecursiveDir(dir), RECURSIVE_RESCAN_MS);
|
|
248
|
+
timer.unref?.();
|
|
249
|
+
}
|
|
197
250
|
} catch (err) {
|
|
198
251
|
console.error(` Failed to watch ${dir.path}:`, err.message);
|
|
199
252
|
}
|
|
@@ -206,6 +259,7 @@ const server = http.createServer((req, res) => {
|
|
|
206
259
|
if (pathname === '/api/reindex') {
|
|
207
260
|
const { indexAll } = require('./indexer');
|
|
208
261
|
const result = indexAll(db, config);
|
|
262
|
+
try { analyzeAll(db); } catch (e) { console.error('Insights recompute error:', e.message); }
|
|
209
263
|
return json(res, { ok: true, sessions: result.sessions, events: result.events });
|
|
210
264
|
}
|
|
211
265
|
|
|
@@ -293,7 +347,7 @@ const server = http.createServer((req, res) => {
|
|
|
293
347
|
|
|
294
348
|
else if (pathname.match(/^\/api\/sessions\/[^/]+\/events$/)) {
|
|
295
349
|
const id = pathname.split('/')[3];
|
|
296
|
-
const session = db.prepare('SELECT
|
|
350
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
297
351
|
if (!session) return json(res, { error: 'Not found' }, 404);
|
|
298
352
|
|
|
299
353
|
const after = query.after || '1970-01-01T00:00:00.000Z';
|
|
@@ -306,12 +360,14 @@ const server = http.createServer((req, res) => {
|
|
|
306
360
|
ORDER BY timestamp ASC, id ASC
|
|
307
361
|
LIMIT ?`
|
|
308
362
|
).all(id, after, after, afterId, limit);
|
|
309
|
-
|
|
363
|
+
const contextRows = loadDeltaAttributionContext(db, id, rows);
|
|
364
|
+
const events = attributeEventDelta(session, rows, contextRows);
|
|
365
|
+
json(res, { events, after, afterId, count: events.length });
|
|
310
366
|
}
|
|
311
367
|
|
|
312
368
|
else if (pathname.match(/^\/api\/sessions\/[^/]+\/stream$/)) {
|
|
313
369
|
const id = pathname.split('/')[3];
|
|
314
|
-
const session = db.prepare('SELECT
|
|
370
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
315
371
|
if (!session) return json(res, { error: 'Not found' }, 404);
|
|
316
372
|
|
|
317
373
|
res.writeHead(200, {
|
|
@@ -331,8 +387,10 @@ const server = http.createServer((req, res) => {
|
|
|
331
387
|
'SELECT * FROM events WHERE session_id = ? AND timestamp > ? ORDER BY timestamp ASC'
|
|
332
388
|
).all(id, lastTs);
|
|
333
389
|
if (rows.length) {
|
|
390
|
+
const contextRows = loadDeltaAttributionContext(db, id, rows);
|
|
391
|
+
const attributedRows = attributeEventDelta(session, rows, contextRows);
|
|
334
392
|
lastTs = rows[rows.length - 1].timestamp;
|
|
335
|
-
res.write(`id: ${lastTs}\ndata: ${JSON.stringify(
|
|
393
|
+
res.write(`id: ${lastTs}\ndata: ${JSON.stringify(attributedRows)}\n\n`);
|
|
336
394
|
}
|
|
337
395
|
} catch (err) {
|
|
338
396
|
console.error('SSE query error:', err.message);
|
|
@@ -356,8 +414,9 @@ const server = http.createServer((req, res) => {
|
|
|
356
414
|
if (!session) { json(res, { error: 'Not found' }, 404); }
|
|
357
415
|
else {
|
|
358
416
|
const events = db.prepare('SELECT * FROM events WHERE session_id = ? ORDER BY timestamp DESC').all(id);
|
|
417
|
+
const attributed = attributeSessionEvents(session, events);
|
|
359
418
|
const hasArchive = ARCHIVE_MODE && db.prepare('SELECT COUNT(*) as c FROM archive WHERE session_id = ?').get(id).c > 0;
|
|
360
|
-
json(res, { session, events, hasArchive });
|
|
419
|
+
json(res, { session, events: attributed.events, projectFilters: attributed.projectFilters, hasArchive });
|
|
361
420
|
}
|
|
362
421
|
}
|
|
363
422
|
else if (pathname.match(/^\/api\/archive\/session\/[^/]+$/)) {
|
|
@@ -726,6 +785,27 @@ const server = http.createServer((req, res) => {
|
|
|
726
785
|
`).all(fp);
|
|
727
786
|
json(res, { file: fp, sessions: rows });
|
|
728
787
|
}
|
|
788
|
+
else if (pathname === '/api/insights') {
|
|
789
|
+
const summary = getInsightsSummary(db);
|
|
790
|
+
return json(res, summary);
|
|
791
|
+
}
|
|
792
|
+
else if (pathname.match(/^\/api\/insights\/session\/[^/]+$/)) {
|
|
793
|
+
const id = pathname.split('/')[4];
|
|
794
|
+
const row = db.prepare('SELECT * FROM session_insights WHERE session_id = ?').get(id);
|
|
795
|
+
if (!row) {
|
|
796
|
+
// Compute on-the-fly if not yet analyzed
|
|
797
|
+
const result = analyzeSession(db, id);
|
|
798
|
+
if (!result) return json(res, { error: 'Session not found' }, 404);
|
|
799
|
+
return json(res, result);
|
|
800
|
+
}
|
|
801
|
+
return json(res, {
|
|
802
|
+
session_id: row.session_id,
|
|
803
|
+
signals: JSON.parse(row.signals || '[]'),
|
|
804
|
+
confusion_score: row.confusion_score,
|
|
805
|
+
flagged: !!row.flagged,
|
|
806
|
+
computed_at: row.computed_at
|
|
807
|
+
});
|
|
808
|
+
}
|
|
729
809
|
else if (!serveStatic(req, res)) {
|
|
730
810
|
const index = path.join(PUBLIC, 'index.html');
|
|
731
811
|
if (fs.existsSync(index)) {
|
package/indexer.js
CHANGED
|
@@ -28,14 +28,26 @@ function listJsonlFiles(baseDir, recursive = false) {
|
|
|
28
28
|
function discoverSessionDirs(config) {
|
|
29
29
|
const dirs = [];
|
|
30
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
|
+
}
|
|
31
41
|
|
|
32
42
|
// Expand a single path into session dirs, handling Claude Code's per-project structure
|
|
33
43
|
function expandPath(p) {
|
|
34
44
|
if (!fs.existsSync(p)) return;
|
|
35
45
|
const stat = fs.statSync(p);
|
|
36
46
|
if (!stat.isDirectory()) return;
|
|
47
|
+
const normalized = normalizedPath(p);
|
|
48
|
+
const normalizedCodex = normalizedPath(codexSessionsPath);
|
|
37
49
|
// Claude Code: ~/.claude/projects contains per-project subdirs with JSONL files
|
|
38
|
-
if (
|
|
50
|
+
if (normalized.endsWith('/.claude/projects')) {
|
|
39
51
|
for (const proj of fs.readdirSync(p)) {
|
|
40
52
|
const projDir = path.join(p, proj);
|
|
41
53
|
if (fs.statSync(projDir).isDirectory()) {
|
|
@@ -43,6 +55,9 @@ function discoverSessionDirs(config) {
|
|
|
43
55
|
if (hasJsonl) dirs.push({ path: projDir, agent: 'claude-code' });
|
|
44
56
|
}
|
|
45
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 });
|
|
46
61
|
} else {
|
|
47
62
|
dirs.push({ path: p, agent: path.basename(path.dirname(p)) });
|
|
48
63
|
}
|
|
@@ -55,6 +70,10 @@ function discoverSessionDirs(config) {
|
|
|
55
70
|
? sessionsOverride
|
|
56
71
|
: sessionsOverride.split(':');
|
|
57
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
|
+
}
|
|
58
77
|
if (dirs.length) return dirs;
|
|
59
78
|
}
|
|
60
79
|
|
|
@@ -73,7 +92,7 @@ function discoverSessionDirs(config) {
|
|
|
73
92
|
expandPath(path.join(home, '.claude/projects'));
|
|
74
93
|
|
|
75
94
|
// Scan ~/.codex/sessions recursively (Codex CLI stores nested YYYY/MM/DD/*.jsonl)
|
|
76
|
-
const codexSessions =
|
|
95
|
+
const codexSessions = codexSessionsPath;
|
|
77
96
|
if (fs.existsSync(codexSessions) && fs.statSync(codexSessions).isDirectory()) {
|
|
78
97
|
dirs.push({ path: codexSessions, agent: 'codex-cli', recursive: true });
|
|
79
98
|
}
|
|
@@ -226,6 +245,8 @@ function extractProjectFromPath(filePath, config) {
|
|
|
226
245
|
|
|
227
246
|
// Common repo location: ~/Developer/<repo>/...
|
|
228
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);
|
|
229
250
|
|
|
230
251
|
// OpenClaw workspace and agent stores
|
|
231
252
|
if (parts[0] === '.openclaw' && parts[1] === 'workspace') return aliasProject('workspace', config);
|
|
@@ -274,6 +295,7 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
274
295
|
let firstMessageTimestamp = null;
|
|
275
296
|
let codexProvider = null;
|
|
276
297
|
let codexSource = null;
|
|
298
|
+
let codexOriginator = null;
|
|
277
299
|
let sawSnapshotRecord = false;
|
|
278
300
|
let sawNonSnapshotRecord = false;
|
|
279
301
|
|
|
@@ -321,7 +343,7 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
321
343
|
const meta = firstLine.payload || {};
|
|
322
344
|
sessionId = meta.id || path.basename(filePath, '.jsonl');
|
|
323
345
|
sessionStart = meta.timestamp || firstLine.timestamp || new Date().toISOString();
|
|
324
|
-
sessionType = 'codex-
|
|
346
|
+
sessionType = 'codex-direct';
|
|
325
347
|
agent = 'codex-cli';
|
|
326
348
|
if (meta.model) {
|
|
327
349
|
model = meta.model;
|
|
@@ -329,6 +351,8 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
329
351
|
}
|
|
330
352
|
codexProvider = meta.model_provider || null;
|
|
331
353
|
codexSource = meta.source || null;
|
|
354
|
+
codexOriginator = meta.originator || null;
|
|
355
|
+
if (codexOriginator && codexOriginator.includes('symphony')) sessionType = 'codex-symphony';
|
|
332
356
|
} else {
|
|
333
357
|
return { skipped: true };
|
|
334
358
|
}
|
|
@@ -363,6 +387,8 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
363
387
|
}
|
|
364
388
|
if (meta.model_provider) codexProvider = meta.model_provider;
|
|
365
389
|
if (meta.source) codexSource = meta.source;
|
|
390
|
+
if (meta.originator) codexOriginator = meta.originator;
|
|
391
|
+
if (codexOriginator && codexOriginator.includes('symphony')) sessionType = 'codex-symphony';
|
|
366
392
|
if (meta.model_provider && !model) model = meta.model_provider;
|
|
367
393
|
continue;
|
|
368
394
|
}
|
|
@@ -567,6 +593,7 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
567
593
|
const parts = ['Codex CLI session'];
|
|
568
594
|
if (codexProvider) parts.push(`provider=${codexProvider}`);
|
|
569
595
|
if (codexSource) parts.push(`source=${codexSource}`);
|
|
596
|
+
if (codexOriginator) parts.push(`originator=${codexOriginator}`);
|
|
570
597
|
summary = parts.join(' ยท ');
|
|
571
598
|
} else {
|
|
572
599
|
summary = 'Heartbeat session';
|
|
@@ -730,6 +757,6 @@ function indexAll(db, config) {
|
|
|
730
757
|
return { sessions: stats.sessions, events: evStats.events, newSessions: totalSessions };
|
|
731
758
|
}
|
|
732
759
|
|
|
733
|
-
module.exports = { discoverSessionDirs, indexFile, indexAll };
|
|
760
|
+
module.exports = { discoverSessionDirs, listJsonlFiles, indexFile, indexAll };
|
|
734
761
|
|
|
735
762
|
if (require.main === module) run();
|