roboticus-api 0.11.3

HTTP routes, WebSocket, auth, rate limiting, and dashboard for the Roboticus agent runtime
Documentation
App._memoryCategory = '';
App._memoryFilter = '';
App._memoryPage = 0;

App.renderMemory = function() {
      var tab = this._memoryTab;
      var sessionId = this._memorySessionId || '';
      var cat = this._memoryCategory || '';
      var analyticsPromise = api('/api/stats/memory-analytics').catch(function() { return null; });
      var promises;
      if (tab === 'search') {
        promises = Promise.resolve({ entries: [], _mode: 'search' });
      } else if (tab === 'working') {
        promises = api('/api/memory/working' + (sessionId ? '/' + encodeURIComponent(sessionId) : ''))
          .catch(function() { return { entries: [] }; })
          .then(function(d) { d._mode = 'working'; return d; });
      } else if (tab === 'semantic') {
        var catPromise = api('/api/memory/semantic/categories').catch(function() { return { categories: [] }; });
        var entriesUrl = cat ? '/api/memory/semantic/' + encodeURIComponent(cat) : '/api/memory/semantic?limit=100';
        var dataPromise = api(entriesUrl).catch(function() { return { entries: [] }; });
        promises = Promise.all([catPromise, dataPromise]).then(function(r) {
          return { categories: r[0].categories || [], entries: r[1].entries || [], _mode: 'semantic' };
        });
      } else {
        promises = api('/api/memory/episodic').catch(function() { return { entries: [] }; })
          .then(function(d) { d._mode = 'episodic'; return d; });
      }
      var self = this;
      return Promise.all([promises, analyticsPromise]).then(function(pair) {
        var data = pair[0];
        var analytics = pair[1] || {};
        var allEntries = data.entries || data.results || [];
        var mode = data._mode || tab;
        var MEM_PAGE_SIZE = 25;
        var filterQ = (self._memoryFilter || '').toLowerCase();
        var entries = filterQ ? allEntries.filter(function(e) { var txt = (e.content || e.value || e.key || '').toLowerCase(); return txt.indexOf(filterQ) >= 0; }) : allEntries;
        var memPage = self._memoryPage || 0;
        var memTotalPages = Math.ceil(entries.length / MEM_PAGE_SIZE) || 1;
        if (memPage >= memTotalPages) memPage = 0;
        var memPaged = entries.slice(memPage * MEM_PAGE_SIZE, (memPage + 1) * MEM_PAGE_SIZE);
        function memFilterBar() { return '<div style="margin-bottom:0.75rem"><input type="text" id="mem-filter-input" placeholder="Filter entries\u2026" value="' + esc(self._memoryFilter || '') + '" style="padding:0.4rem 0.75rem;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);width:100%;max-width:300px;font-family:var(--font);font-size:0.8125rem"></div>'; }
        function memPager() { if (memTotalPages <= 1) return ''; return '<div style="display:flex;gap:0.5rem;align-items:center;margin-top:0.75rem;justify-content:center"><button class="btn secondary" id="mem-prev" style="font-size:0.75rem;padding:0.2rem 0.6rem"' + (memPage === 0 ? ' disabled' : '') + '>\u2190 Prev</button><span style="font-size:0.75rem;color:var(--muted)">Page ' + (memPage + 1) + ' of ' + memTotalPages + ' (' + entries.length + ' entries)</span><button class="btn secondary" id="mem-next" style="font-size:0.75rem;padding:0.2rem 0.6rem"' + (memPage >= memTotalPages - 1 ? ' disabled' : '') + '>Next \u2192</button></div>'; }

        var tabButtons = '<div class="tabs">'
          + '<button class="' + (tab === 'working' ? 'active' : '') + '" data-tab="working">Working</button>'
          + '<button class="' + (tab === 'episodic' ? 'active' : '') + '" data-tab="episodic">Episodic</button>'
          + '<button class="' + (tab === 'semantic' ? 'active' : '') + '" data-tab="semantic">Semantic</button>'
          + '<button class="' + (tab === 'search' ? 'active' : '') + '" data-tab="search">Search</button>'
          + '</div>';

        // ── Retrieval Health strip ────────────────────────────────────────────
        var healthStripHtml = '';
        if (analytics && (analytics.total_turns != null)) {
          var hitRatePct = analytics.hit_rate != null ? (analytics.hit_rate * 100).toFixed(1) + '%' : '\u2014';
          var avgSim = analytics.avg_similarity != null ? Number(analytics.avg_similarity).toFixed(3) : '\u2014';
          var budgetPct = analytics.avg_budget_utilization != null ? (analytics.avg_budget_utilization * 100).toFixed(1) + '%' : '\u2014';
          var tierDist = analytics.tier_distribution || {};
          var tierKeys = ['episodic', 'semantic', 'procedural', 'relationship', 'working'];
          var tierItems = tierKeys.map(function(k) {
            var v = tierDist[k];
            return v != null ? '<span style="font-size:0.6875rem;color:var(--muted)">' + k + ': <strong style="color:var(--text)">' + v + '</strong></span>' : null;
          }).filter(Boolean).join('<span style="color:var(--border);margin:0 0.25rem">/</span>');
          healthStripHtml = '<div style="display:flex;flex-wrap:wrap;align-items:center;gap:1rem;padding:0.6rem 0.85rem;margin-bottom:0.85rem;background:var(--surface-2);border:1px solid var(--border-ghost);border-radius:var(--radius)">'
            + '<span style="font-family:var(--font-headline);font-size:0.5625rem;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;color:var(--muted);white-space:nowrap">Retrieval Health</span>'
            + '<div style="display:flex;flex-wrap:wrap;gap:1.25rem;align-items:center">'
            + '<div style="display:flex;flex-direction:column;align-items:center"><span style="font-size:0.9375rem;font-weight:700;color:var(--accent)">' + hitRatePct + '</span><span style="font-size:0.625rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em">Hit Rate</span></div>'
            + '<div style="display:flex;flex-direction:column;align-items:center"><span style="font-size:0.9375rem;font-weight:700;color:var(--tertiary)">' + avgSim + '</span><span style="font-size:0.625rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em">Avg Similarity</span></div>'
            + '<div style="display:flex;flex-direction:column;align-items:center"><span style="font-size:0.9375rem;font-weight:700;color:var(--secondary)">' + budgetPct + '</span><span style="font-size:0.625rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em">Budget Util</span></div>'
            + (tierItems ? '<div style="display:flex;flex-direction:column"><span style="font-size:0.625rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.2rem">Tier Breakdown</span><div style="display:flex;flex-wrap:wrap;gap:0.4rem;align-items:center">' + tierItems + '</div></div>' : '')
            + '</div></div>';
        }

        var filterHtml = '';
        var semanticNavHtml = '';
        if (mode === 'semantic' && data.categories && data.categories.length > 0) {
          var cats = data.categories;
          var totalEntries = cats.reduce(function(s, c) { return s + c.count; }, 0);
          var navRows = '<div data-mem-cat="" style="cursor:pointer;padding:0.65rem 0.8rem;border:1px solid ' + (!cat ? 'var(--accent)' : 'var(--border-soft)') + ';border-radius:6px;background:' + (!cat ? 'var(--accent-dim)' : 'rgba(255,255,255,0.01)') + ';display:flex;justify-content:space-between;align-items:center;gap:0.75rem">'
            + '<div><div style="font-size:0.78rem;font-weight:600;color:var(--text)">All Categories</div><div style="font-size:0.68rem;color:var(--muted)">Browse the full semantic memory set</div></div>'
            + '<span class="badge ' + (!cat ? 'active' : 'muted') + '" style="white-space:nowrap">' + totalEntries + '</span>'
            + '</div>';
          cats.forEach(function(c) {
            var active = cat === c.category;
            navRows += '<div data-mem-cat="' + esc(c.category) + '" style="cursor:pointer;padding:0.65rem 0.8rem;border:1px solid ' + (active ? 'var(--accent)' : 'var(--border-soft)') + ';border-radius:6px;background:' + (active ? 'var(--accent-dim)' : 'rgba(255,255,255,0.01)') + ';display:flex;justify-content:space-between;align-items:center;gap:0.75rem">'
              + '<div><div style="font-size:0.78rem;font-weight:600;color:var(--text)">' + esc(c.category) + '</div><div style="font-size:0.68rem;color:var(--muted)">Semantic category</div></div>'
              + '<span class="badge ' + (active ? 'active' : 'muted') + '" style="white-space:nowrap">' + c.count + '</span>'
              + '</div>';
          });
          semanticNavHtml = '<div class="card" style="padding:0.85rem">'
            + '<div class="card-title" style="margin-bottom:0.25rem">Categories</div>'
            + '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.75rem">Navigate semantic memory by category.</div>'
            + '<div style="display:flex;flex-direction:column;gap:0.45rem">' + navRows + '</div>'
            + '</div>';
        }

        if (mode === 'semantic') {
          var list = memPaged.map(function(e) {
            return '<div class="card" style="margin-bottom:0.5rem">'
              + '<div style="display:flex;justify-content:space-between;align-items:baseline">'
              + '<strong style="color:var(--text);font-size:0.875rem">' + esc(e.key || '') + '</strong>'
              + '<span class="badge muted" style="font-size:0.7rem">' + esc(e.category || '') + '</span>'
              + '</div>'
              + '<div style="margin-top:4px">' + formatMemContent(e.value || e.content || '') + '</div>'
              + '<div style="margin-top:6px"><span class="badge">conf: ' + esc(String(e.confidence != null ? e.confidence : '—')) + '</span></div>'
              + '</div>';
          }).join('') || '<p style="color:var(--muted)">No entries' + (cat ? ' in "' + esc(cat) + '"' : '') + '. Try a different category filter.</p>';
          var contentHtml = '<div>'
            + (cat ? '<div style="margin-bottom:0.75rem;font-size:0.78rem;color:var(--muted)">Showing category: <strong style="color:var(--text)">' + esc(cat) + '</strong></div>' : '')
            + memFilterBar()
            + '<div id="memory-list">' + list + '</div>'
            + memPager()
            + '</div>';
          if (semanticNavHtml) {
            return healthStripHtml + tabButtons + '<div style="display:grid;grid-template-columns:minmax(240px,280px) minmax(0,1fr);gap:1rem;align-items:start">' + semanticNavHtml + contentHtml + '</div>';
          }
          return healthStripHtml + tabButtons + filterHtml + memFilterBar() + '<div id="memory-list">' + list + '</div>' + memPager();
        }

        if (mode === 'working') {
          var sessionIds = [];
          entries.forEach(function(e) { if (e.session_id && sessionIds.indexOf(e.session_id) === -1) sessionIds.push(e.session_id); });
          if (sessionIds.length > 1 || (sessionIds.length === 1 && !sessionId)) {
            filterHtml = '<div style="margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">'
              + '<label style="font-size:0.8rem;color:var(--muted)">Session:</label>'
              + '<select id="mem-session-select" style="flex:1;max-width:400px;padding:0.4rem;border:1px solid var(--border);border-radius:4px;background:var(--surface);color:var(--text);font-family:var(--mono);font-size:0.8rem">'
              + '<option value="">All sessions (' + sessionIds.length + ')</option>';
            sessionIds.forEach(function(sid) {
              var firstEntry = entries.find(function(e) { return e.session_id === sid; });
              var dateLabel = firstEntry && firstEntry.created_at ? firstEntry.created_at.substring(0, 16).replace('T', ' ') : '';
              var label = dateLabel + ' (' + sid.substring(0, 8) + '\u2026)';
              var sel = sessionId === sid ? ' selected' : '';
              filterHtml += '<option value="' + esc(sid) + '"' + sel + '>' + esc(label) + '</option>';
            });
            filterHtml += '</select></div>';
          }
          var list = memPaged.map(function(e) {
            var ts = e.created_at ? e.created_at.substring(0, 16).replace('T', ' ') : '';
            return '<div class="card" style="margin-bottom:0.5rem">'
              + '<div>' + formatMemContent(e.content || '') + '</div>'
              + '<div style="margin-top:6px">'
              + '<span class="badge muted">' + esc(e.entry_type || '—') + '</span> '
              + '<span class="badge">imp: ' + esc(String(e.importance != null ? e.importance : '—')) + '</span> '
              + '<span style="font-size:0.7rem;color:var(--muted)">' + esc((e.session_id || '').substring(0, 8)) + '</span>'
              + (ts ? ' <span style="font-size:0.7rem;color:var(--muted)">' + esc(ts) + '</span>' : '')
              + '</div></div>';
          }).join('') || '<p style="color:var(--muted)">No working memory entries. Open a session and send a message to populate this view.</p>';
          return healthStripHtml + tabButtons + filterHtml + memFilterBar() + '<div id="memory-list">' + list + '</div>' + memPager();
        }

        if (mode === 'search') {
          var searchHtml = '<div style="margin-bottom:1rem"><input type="text" id="memory-search-q" placeholder="Search across all memory\u2026" style="padding:0.5rem 1rem;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);width:100%;max-width:400px;font-family:var(--font)"><button class="btn" style="margin-left:8px" id="memory-search-btn">' + uiBtnLabel('search', 'Search') + '</button></div>';
          return healthStripHtml + tabButtons + searchHtml + '<div id="memory-search-results"><p style="color:var(--muted)">Enter a query and click Search (example: <code>token budget</code>).</p></div>';
        }

        var list = memPaged.map(function(e) {
          var type = e.entry_type || e.classification || e.category || '\u2014';
          var imp = e.importance != null ? e.importance : (e.confidence != null ? e.confidence : '\u2014');
          var content = e.content || e.value || '';
          if (!content && e.key) content = e.key + ': ' + (e.summary || '');
          if (!content) { try { var ek = Object.keys(e).filter(function(k) { return k !== 'id' && k !== 'created_at' && k !== 'updated_at'; }); content = ek.map(function(k) { return k + ': ' + String(e[k] || ''); }).join(' | '); } catch(_) { content = '(no content)'; } }
          var ts = e.created_at ? e.created_at.substring(0, 16).replace('T', ' ') : (e.updated_at ? e.updated_at.substring(0, 16).replace('T', ' ') : '');
          return '<div class="card" style="margin-bottom:0.5rem"><div>' + formatMemContent(content) + '</div><div style="margin-top:6px"><span class="badge muted">' + esc(type) + '</span> <span class="badge">imp: ' + esc(String(imp)) + '</span>' + (ts ? ' <span style="font-size:0.7rem;color:var(--muted)">' + esc(ts) + '</span>' : '') + '</div></div>';
        }).join('') || '<p style="color:var(--muted)">No entries found for this memory view yet.</p>';
        return healthStripHtml + tabButtons + memFilterBar() + '<div id="memory-list">' + list + '</div>' + memPager();
      });
};