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 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
- setTimeout(() => {
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
- }, 500);
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
- stmts.deleteEvents.run(sessionId);
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
- 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]);
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
- stmts.upsertState.run(filePath, lines.length, mtime);
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
- const result = indexFile(db, path.join(dir.path, file), dir.agent, stmts, archiveMode, config);
447
- if (!result.skipped) totalSessions++;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentacta",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Audit trail and search engine for AI agent sessions",
5
5
  "main": "index.js",
6
6
  "bin": {
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
- const res = await fetch(API + path);
8
- return res.json();
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
- content.innerHTML = '<div class="loading">Loading</div>';
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
- html += data.events.map(renderEvent).join('');
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
- const backBtn = $('#backBtn');
833
- if (backBtn && window._currentSessionId) {
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
+ }