agentacta 1.2.1 → 1.2.2
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/index.js +21 -2
- package/indexer.js +27 -20
- package/package.json +1 -1
- package/public/app.js +97 -13
- package/public/style.css +18 -0
package/index.js
CHANGED
|
@@ -146,20 +146,28 @@ for (const dir of sessionDirs) {
|
|
|
146
146
|
|
|
147
147
|
console.log(`Watching ${sessionDirs.length} session directories`);
|
|
148
148
|
|
|
149
|
+
// Debounce map: filePath -> timeout handle
|
|
150
|
+
const _reindexTimers = new Map();
|
|
151
|
+
const REINDEX_DEBOUNCE_MS = 2000;
|
|
152
|
+
|
|
149
153
|
for (const dir of sessionDirs) {
|
|
150
154
|
try {
|
|
151
155
|
fs.watch(dir.path, { persistent: false }, (eventType, filename) => {
|
|
152
156
|
if (!filename || !filename.endsWith('.jsonl')) return;
|
|
153
157
|
const filePath = path.join(dir.path, filename);
|
|
154
158
|
if (!fs.existsSync(filePath)) return;
|
|
155
|
-
|
|
159
|
+
|
|
160
|
+
// Debounce: cancel pending re-index for this file, schedule a new one
|
|
161
|
+
if (_reindexTimers.has(filePath)) clearTimeout(_reindexTimers.get(filePath));
|
|
162
|
+
_reindexTimers.set(filePath, setTimeout(() => {
|
|
163
|
+
_reindexTimers.delete(filePath);
|
|
156
164
|
try {
|
|
157
165
|
const result = indexFile(db, filePath, dir.agent, stmts, ARCHIVE_MODE);
|
|
158
166
|
if (!result.skipped) console.log(`Live re-indexed: ${filename} (${dir.agent})`);
|
|
159
167
|
} catch (err) {
|
|
160
168
|
console.error(`Error re-indexing ${filename}:`, err.message);
|
|
161
169
|
}
|
|
162
|
-
},
|
|
170
|
+
}, REINDEX_DEBOUNCE_MS));
|
|
163
171
|
});
|
|
164
172
|
console.log(` Watching: ${dir.path}`);
|
|
165
173
|
} catch (err) {
|
|
@@ -403,6 +411,17 @@ const server = http.createServer((req, res) => {
|
|
|
403
411
|
});
|
|
404
412
|
|
|
405
413
|
const HOST = process.env.AGENTACTA_HOST || '127.0.0.1';
|
|
414
|
+
server.on('error', (err) => {
|
|
415
|
+
if (err.code === 'EADDRINUSE') {
|
|
416
|
+
console.error(`Port ${PORT} in use, retrying in 2s...`);
|
|
417
|
+
setTimeout(() => {
|
|
418
|
+
server.close();
|
|
419
|
+
server.listen(PORT, HOST);
|
|
420
|
+
}, 2000);
|
|
421
|
+
} else {
|
|
422
|
+
throw err;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
406
425
|
server.listen(PORT, HOST, () => console.log(`AgentActa running on http://${HOST}:${PORT}`));
|
|
407
426
|
|
|
408
427
|
// Graceful shutdown
|
package/indexer.js
CHANGED
|
@@ -208,11 +208,7 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
208
208
|
return { skipped: true };
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
stmts.deleteSession.run(sessionId);
|
|
213
|
-
stmts.deleteFileActivity.run(sessionId);
|
|
214
|
-
if (stmts.deleteArchive) stmts.deleteArchive.run(sessionId);
|
|
215
|
-
|
|
211
|
+
// --- Parse the entire file BEFORE any DB operations ---
|
|
216
212
|
const pendingEvents = [];
|
|
217
213
|
const fileActivities = [];
|
|
218
214
|
const projectCounts = new Map();
|
|
@@ -338,11 +334,8 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
338
334
|
}
|
|
339
335
|
if (!sessionType && !initialPrompt) sessionType = 'heartbeat';
|
|
340
336
|
// Detect subagent: task-style prompts injected by sessions_spawn
|
|
341
|
-
// These typically start with a date/time stamp (e.g. "[Wed 2026-...")
|
|
342
|
-
// But exclude System Messages (cron announcements injected into main session)
|
|
343
337
|
if (!sessionType && initialPrompt) {
|
|
344
338
|
const p = initialPrompt.trim();
|
|
345
|
-
// Sub-agent prompts start with "[Wed 2026-..." but NOT "[... [System Message]"
|
|
346
339
|
if (/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-/.test(p) && !p.includes('[System Message]')) {
|
|
347
340
|
sessionType = 'subagent';
|
|
348
341
|
}
|
|
@@ -354,18 +347,28 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
354
347
|
.map(([name]) => name);
|
|
355
348
|
const projectsJson = projects.length > 0 ? JSON.stringify(projects) : null;
|
|
356
349
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
350
|
+
// --- All DB operations in a single transaction for atomicity ---
|
|
351
|
+
const commitIndex = db.transaction(() => {
|
|
352
|
+
stmts.deleteEvents.run(sessionId);
|
|
353
|
+
stmts.deleteFileActivity.run(sessionId);
|
|
354
|
+
if (stmts.deleteArchive) stmts.deleteArchive.run(sessionId);
|
|
355
|
+
stmts.deleteSession.run(sessionId);
|
|
356
|
+
|
|
357
|
+
stmts.upsertSession.run(sessionId, sessionStart, sessionEnd, msgCount, toolCount, model, summary, agent, sessionType, totalCost, totalTokens, totalInputTokens, totalOutputTokens, totalCacheReadTokens, totalCacheWriteTokens, initialPrompt, firstMessageId, firstMessageTimestamp, modelsJson, projectsJson);
|
|
358
|
+
for (const ev of pendingEvents) stmts.insertEvent.run(...ev);
|
|
359
|
+
for (const fa of fileActivities) stmts.insertFileActivity.run(...fa);
|
|
360
|
+
|
|
361
|
+
// Archive mode: store raw JSONL lines
|
|
362
|
+
if (archiveMode && stmts.insertArchive) {
|
|
363
|
+
for (let i = 0; i < lines.length; i++) {
|
|
364
|
+
stmts.insertArchive.run(sessionId, i + 1, lines[i]);
|
|
365
|
+
}
|
|
365
366
|
}
|
|
366
|
-
}
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
stmts.upsertState.run(filePath, lines.length, mtime);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
commitIndex();
|
|
369
372
|
|
|
370
373
|
return { sessionId, msgCount, toolCount };
|
|
371
374
|
}
|
|
@@ -443,8 +446,12 @@ function indexAll(db, config) {
|
|
|
443
446
|
for (const dir of sessionDirs) {
|
|
444
447
|
const files = fs.readdirSync(dir.path).filter(f => f.endsWith('.jsonl'));
|
|
445
448
|
for (const file of files) {
|
|
446
|
-
|
|
447
|
-
|
|
449
|
+
try {
|
|
450
|
+
const result = indexFile(db, path.join(dir.path, file), dir.agent, stmts, archiveMode, config);
|
|
451
|
+
if (!result.skipped) totalSessions++;
|
|
452
|
+
} catch (err) {
|
|
453
|
+
console.error(`Error indexing ${file}:`, err.message);
|
|
454
|
+
}
|
|
448
455
|
}
|
|
449
456
|
}
|
|
450
457
|
const stats = db.prepare('SELECT COUNT(*) as sessions FROM sessions').get();
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -4,8 +4,26 @@ const content = $('#content');
|
|
|
4
4
|
const API = '/api';
|
|
5
5
|
|
|
6
6
|
async function api(path) {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
let res;
|
|
8
|
+
try {
|
|
9
|
+
res = await fetch(API + path);
|
|
10
|
+
} catch (err) {
|
|
11
|
+
// Network error (server down, offline, etc.)
|
|
12
|
+
return { _error: true, error: 'Network error' };
|
|
13
|
+
}
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
try {
|
|
16
|
+
const body = await res.json();
|
|
17
|
+
return { _error: true, error: body.error || `HTTP ${res.status}`, status: res.status };
|
|
18
|
+
} catch {
|
|
19
|
+
return { _error: true, error: `HTTP ${res.status}`, status: res.status };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return await res.json();
|
|
24
|
+
} catch {
|
|
25
|
+
return { _error: true, error: 'Invalid JSON response' };
|
|
26
|
+
}
|
|
9
27
|
}
|
|
10
28
|
|
|
11
29
|
function fmtTime(ts) {
|
|
@@ -256,6 +274,10 @@ async function showSearchHome() {
|
|
|
256
274
|
|
|
257
275
|
const stats = await api('/stats');
|
|
258
276
|
const sessions = await api('/sessions?limit=5');
|
|
277
|
+
if (stats._error || sessions._error) {
|
|
278
|
+
el.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
259
281
|
|
|
260
282
|
let suggestions = [];
|
|
261
283
|
try { const r = await fetch('/api/suggestions'); const d = await r.json(); suggestions = d.suggestions || []; } catch(e) { suggestions = []; }
|
|
@@ -309,7 +331,7 @@ async function doSearch(q) {
|
|
|
309
331
|
|
|
310
332
|
const data = await api(url);
|
|
311
333
|
|
|
312
|
-
if (data.error) { el.innerHTML = `<div class="empty"><p>${escHtml(data.error)}</p></div>`; return; }
|
|
334
|
+
if (data._error || data.error) { el.innerHTML = `<div class="empty"><p>${escHtml(data.error || 'Server error')}</p></div>`; return; }
|
|
313
335
|
if (!data.results.length) { el.innerHTML = '<div class="empty"><h2>No results</h2><p>Try a different search term or adjust filters</p></div>'; return; }
|
|
314
336
|
|
|
315
337
|
let header = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
|
|
@@ -345,6 +367,10 @@ async function viewSessions() {
|
|
|
345
367
|
window._currentSessionId = null;
|
|
346
368
|
content.innerHTML = '<div class="loading">Loading</div>';
|
|
347
369
|
const data = await api('/sessions?limit=200');
|
|
370
|
+
if (data._error) {
|
|
371
|
+
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
348
374
|
|
|
349
375
|
let html = `<div class="page-title">Sessions</div>`;
|
|
350
376
|
html += data.sessions.map(renderSessionItem).join('');
|
|
@@ -358,10 +384,10 @@ async function viewSessions() {
|
|
|
358
384
|
|
|
359
385
|
async function viewSession(id) {
|
|
360
386
|
window._currentSessionId = id;
|
|
361
|
-
|
|
387
|
+
window.scrollTo(0, 0);
|
|
362
388
|
const data = await api(`/sessions/${id}`);
|
|
363
389
|
|
|
364
|
-
if (data.error) { content.innerHTML = `<div class="empty"><h2>${data.error}</h2></div>`; return; }
|
|
390
|
+
if (data._error || data.error) { content.innerHTML = `<div class="empty"><h2>${escHtml(data.error || 'Unable to load')}</h2></div>`; return; }
|
|
365
391
|
|
|
366
392
|
const s = data.session;
|
|
367
393
|
const cost = fmtCost(s.total_cost);
|
|
@@ -399,11 +425,50 @@ async function viewSession(id) {
|
|
|
399
425
|
<div class="section-label">Events</div>
|
|
400
426
|
`;
|
|
401
427
|
|
|
402
|
-
|
|
428
|
+
const PAGE_SIZE = 50;
|
|
429
|
+
const allEvents = data.events;
|
|
430
|
+
let rendered = 0;
|
|
431
|
+
|
|
432
|
+
function renderBatch() {
|
|
433
|
+
const batch = allEvents.slice(rendered, rendered + PAGE_SIZE);
|
|
434
|
+
if (!batch.length) return;
|
|
435
|
+
const frag = document.createElement('div');
|
|
436
|
+
frag.innerHTML = batch.map(renderEvent).join('');
|
|
437
|
+
const container = document.getElementById('eventsContainer');
|
|
438
|
+
if (container) {
|
|
439
|
+
while (frag.firstChild) container.appendChild(frag.firstChild);
|
|
440
|
+
}
|
|
441
|
+
rendered += batch.length;
|
|
442
|
+
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
html += '<div id="eventsContainer">' + allEvents.slice(0, PAGE_SIZE).map(renderEvent).join('') + '</div>';
|
|
446
|
+
rendered = Math.min(PAGE_SIZE, allEvents.length);
|
|
403
447
|
content.innerHTML = html;
|
|
404
448
|
transitionView();
|
|
405
449
|
|
|
450
|
+
let onScroll = null;
|
|
451
|
+
if (allEvents.length > PAGE_SIZE) {
|
|
452
|
+
let loading = false;
|
|
453
|
+
onScroll = () => {
|
|
454
|
+
if (loading || rendered >= allEvents.length) return;
|
|
455
|
+
const scrollBottom = window.innerHeight + window.scrollY;
|
|
456
|
+
const threshold = document.body.offsetHeight - 300;
|
|
457
|
+
if (scrollBottom >= threshold) {
|
|
458
|
+
loading = true;
|
|
459
|
+
renderBatch();
|
|
460
|
+
loading = false;
|
|
461
|
+
if (rendered >= allEvents.length) {
|
|
462
|
+
window.removeEventListener('scroll', onScroll);
|
|
463
|
+
onScroll = null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
468
|
+
}
|
|
469
|
+
|
|
406
470
|
$('#backBtn').addEventListener('click', () => {
|
|
471
|
+
if (onScroll) { window.removeEventListener('scroll', onScroll); onScroll = null; }
|
|
407
472
|
if (window._lastView === 'timeline') viewTimeline();
|
|
408
473
|
else if (window._lastView === 'files') viewFiles();
|
|
409
474
|
else if (window._lastView === 'search') viewSearch(window._lastSearchQuery || '');
|
|
@@ -452,6 +517,10 @@ async function viewSession(id) {
|
|
|
452
517
|
const jumpBtn = $('#jumpToStartBtn');
|
|
453
518
|
if (jumpBtn) {
|
|
454
519
|
jumpBtn.addEventListener('click', () => {
|
|
520
|
+
// Load all remaining events to find the first message
|
|
521
|
+
while (rendered < allEvents.length) {
|
|
522
|
+
renderBatch();
|
|
523
|
+
}
|
|
455
524
|
const firstMessage = document.querySelector(`[data-event-id="${s.first_message_id}"]`);
|
|
456
525
|
if (firstMessage) {
|
|
457
526
|
firstMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
@@ -478,6 +547,10 @@ async function viewTimeline(date) {
|
|
|
478
547
|
transitionView();
|
|
479
548
|
|
|
480
549
|
const data = await api(`/timeline?date=${date}`);
|
|
550
|
+
if (data._error) {
|
|
551
|
+
$('#timelineContent').innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
481
554
|
const el = $('#timelineContent');
|
|
482
555
|
|
|
483
556
|
if (!data.events.length) {
|
|
@@ -495,6 +568,10 @@ async function viewTimeline(date) {
|
|
|
495
568
|
async function viewStats() {
|
|
496
569
|
content.innerHTML = '<div class="loading">Loading</div>';
|
|
497
570
|
const data = await api('/stats');
|
|
571
|
+
if (data._error) {
|
|
572
|
+
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
498
575
|
|
|
499
576
|
let html = `<div class="page-title">Stats</div>
|
|
500
577
|
<div class="stat-grid">
|
|
@@ -554,6 +631,10 @@ async function viewFiles() {
|
|
|
554
631
|
window._lastView = 'files';
|
|
555
632
|
content.innerHTML = '<div class="loading">Loading</div>';
|
|
556
633
|
const data = await api('/files?limit=500');
|
|
634
|
+
if (data._error) {
|
|
635
|
+
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
557
638
|
window._allFiles = data.files || [];
|
|
558
639
|
window._fileSort = window._fileSort || 'touches';
|
|
559
640
|
window._fileFilter = window._fileFilter || '';
|
|
@@ -719,6 +800,10 @@ function renderFileItem(f) {
|
|
|
719
800
|
async function viewFileDetail(filePath) {
|
|
720
801
|
content.innerHTML = '<div class="loading">Loading</div>';
|
|
721
802
|
const data = await api(`/files/sessions?path=${encodeURIComponent(filePath)}`);
|
|
803
|
+
if (data._error) {
|
|
804
|
+
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
722
807
|
|
|
723
808
|
let html = `
|
|
724
809
|
<div class="back-btn" id="backBtn">\u2190 Back</div>
|
|
@@ -794,6 +879,7 @@ viewSearch();
|
|
|
794
879
|
(function initPTR() {
|
|
795
880
|
let startY = 0;
|
|
796
881
|
let pulling = false;
|
|
882
|
+
let refreshing = false;
|
|
797
883
|
const threshold = 80;
|
|
798
884
|
|
|
799
885
|
const indicator = document.createElement('div');
|
|
@@ -823,22 +909,20 @@ viewSearch();
|
|
|
823
909
|
document.addEventListener('touchend', async e => {
|
|
824
910
|
if (!pulling) return;
|
|
825
911
|
pulling = false;
|
|
912
|
+
if (refreshing) { indicator.classList.remove('visible'); return; }
|
|
826
913
|
const diff = e.changedTouches[0].clientY - startY;
|
|
827
914
|
if (diff > threshold && indicator.classList.contains('visible')) {
|
|
915
|
+
refreshing = true;
|
|
828
916
|
indicator.textContent = 'Refreshing\u2026';
|
|
829
917
|
indicator.classList.add('refreshing');
|
|
830
918
|
try {
|
|
831
919
|
await api('/reindex');
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
await viewSession(window._currentSessionId);
|
|
835
|
-
} else {
|
|
836
|
-
const active = $('.nav-item.active');
|
|
837
|
-
if (active) active.click();
|
|
838
|
-
}
|
|
920
|
+
// Just reindex data without re-rendering the view
|
|
921
|
+
// The next manual navigation will pick up new data
|
|
839
922
|
} catch(err) {}
|
|
840
923
|
setTimeout(() => {
|
|
841
924
|
indicator.classList.remove('visible', 'refreshing');
|
|
925
|
+
refreshing = false;
|
|
842
926
|
}, 500);
|
|
843
927
|
} else {
|
|
844
928
|
indicator.classList.remove('visible');
|
package/public/style.css
CHANGED
|
@@ -1349,3 +1349,21 @@ mark {
|
|
|
1349
1349
|
}
|
|
1350
1350
|
}
|
|
1351
1351
|
|
|
1352
|
+
|
|
1353
|
+
/* Load more button */
|
|
1354
|
+
.load-more-btn {
|
|
1355
|
+
text-align: center;
|
|
1356
|
+
padding: var(--space-lg);
|
|
1357
|
+
margin: var(--space-md) 0;
|
|
1358
|
+
color: var(--accent);
|
|
1359
|
+
font-size: 13px;
|
|
1360
|
+
font-weight: 500;
|
|
1361
|
+
cursor: pointer;
|
|
1362
|
+
border: 1px solid var(--border-subtle);
|
|
1363
|
+
border-radius: var(--radius-md);
|
|
1364
|
+
transition: all var(--duration-normal) var(--ease-out);
|
|
1365
|
+
}
|
|
1366
|
+
.load-more-btn:hover {
|
|
1367
|
+
background: var(--bg-active);
|
|
1368
|
+
border-color: var(--border-hover);
|
|
1369
|
+
}
|