switchroom 0.15.29 → 0.15.31

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.
@@ -29047,10 +29047,43 @@ async function inspectBankHealth(mcpUrl, bankId, opts) {
29047
29047
  lastRefreshedAt: m.last_refreshed_at ?? null,
29048
29048
  createdAt: m.created_at ?? null,
29049
29049
  contentLength: (m.content ?? "").length,
29050
- contentHead: (m.content ?? "").slice(0, 200)
29050
+ contentHead: (m.content ?? "").slice(0, 200),
29051
+ sourceQuery: m.source_query ?? "",
29052
+ refreshMode: m.trigger?.mode ?? null
29051
29053
  }))
29052
29054
  };
29053
29055
  }
29056
+ async function getMentalModelDetail(mcpUrl, bankId, modelId, opts) {
29057
+ const base = hindsightRestBase(mcpUrl);
29058
+ const bank = encodeURIComponent(bankId);
29059
+ const id = encodeURIComponent(modelId);
29060
+ const res = await getJson(`${base}/v1/default/banks/${bank}/mental-models/${id}`, opts);
29061
+ if (!res.ok)
29062
+ return res;
29063
+ const m = res.data;
29064
+ const basedOn = m.reflect_response?.based_on ?? {};
29065
+ const basedOnCounts = {};
29066
+ let totalSourceFacts = 0;
29067
+ for (const [type, facts] of Object.entries(basedOn)) {
29068
+ const n = Array.isArray(facts) ? facts.length : 0;
29069
+ basedOnCounts[type] = n;
29070
+ totalSourceFacts += n;
29071
+ }
29072
+ return {
29073
+ ok: true,
29074
+ model: {
29075
+ id: m.id ?? modelId,
29076
+ name: m.name ?? modelId,
29077
+ sourceQuery: m.source_query ?? "",
29078
+ content: m.content ?? "",
29079
+ lastRefreshedAt: m.last_refreshed_at ?? null,
29080
+ createdAt: m.created_at ?? null,
29081
+ refreshMode: m.trigger?.mode ?? null,
29082
+ basedOnCounts,
29083
+ totalSourceFacts
29084
+ }
29085
+ };
29086
+ }
29054
29087
  function ageDays(iso, now = new Date) {
29055
29088
  if (!iso)
29056
29089
  return null;
@@ -50513,8 +50546,8 @@ var {
50513
50546
  } = import__.default;
50514
50547
 
50515
50548
  // src/build-info.ts
50516
- var VERSION = "0.15.29";
50517
- var COMMIT_SHA = "eddde22d";
50549
+ var VERSION = "0.15.31";
50550
+ var COMMIT_SHA = "9c6a82c6";
50518
50551
 
50519
50552
  // src/cli/agent.ts
50520
50553
  init_source();
@@ -75087,6 +75120,24 @@ async function handleMemoryBuildProfile(config, body, deps) {
75087
75120
  return { ok: false, error: String(err.message ?? err) };
75088
75121
  }
75089
75122
  }
75123
+ async function handleGetMentalModel(config, bank, modelId, deps) {
75124
+ if (!bank)
75125
+ return { ok: false, error: "Query must include `bank`." };
75126
+ if (!modelId)
75127
+ return { ok: false, error: "Query must include `id`." };
75128
+ if (!isKnownBank(config, bank))
75129
+ return { ok: false, error: `Unknown bank: ${bank}` };
75130
+ const url = resolveMemoryUrl(config);
75131
+ const detail = deps?.detail ?? getMentalModelDetail;
75132
+ try {
75133
+ const res = await detail(url, bank, modelId, { fetchImpl: deps?.fetchImpl });
75134
+ if (!res.ok)
75135
+ return { ok: false, error: res.reason };
75136
+ return { ok: true, model: res.model };
75137
+ } catch (err) {
75138
+ return { ok: false, error: String(err.message ?? err) };
75139
+ }
75140
+ }
75090
75141
  var ATTENTION_SEVERITY_RANK = {
75091
75142
  critical: 0,
75092
75143
  warn: 1,
@@ -75973,6 +76024,9 @@ function parseRoute(pathname, method) {
75973
76024
  if (method === "POST" && pathname === "/api/memory/build-profile") {
75974
76025
  return { handler: "memoryBuildProfile", params: {} };
75975
76026
  }
76027
+ if (method === "GET" && pathname === "/api/memory/model") {
76028
+ return { handler: "getMentalModel", params: {} };
76029
+ }
75976
76030
  if (method === "GET" && pathname === "/api/google-accounts") {
75977
76031
  return { handler: "getGoogleAccounts", params: {} };
75978
76032
  }
@@ -76272,6 +76326,14 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
76272
76326
  return jsonResponse(result, result.ok ? 200 : 400);
76273
76327
  })();
76274
76328
  }
76329
+ case "getMentalModel": {
76330
+ return (async () => {
76331
+ const bank = url.searchParams.get("bank") ?? "";
76332
+ const id = url.searchParams.get("id") ?? "";
76333
+ const result = await handleGetMentalModel(freshConfig(), bank, id);
76334
+ return jsonResponse(result, result.ok ? 200 : 400);
76335
+ })();
76336
+ }
76275
76337
  case "getAgentAccounts": {
76276
76338
  const agentName = route.params.name;
76277
76339
  if (!config.agents[agentName]) {
@@ -636,12 +636,67 @@
636
636
  // JSON for a single-quoted onclick attribute: escape the quote
637
637
  // chars that could break out of the attribute (', &, <, >).
638
638
  const attrJson = (v) => escapeHtml(JSON.stringify(v));
639
- const cards = (m.banks || []).map(b => {
639
+
640
+ // --- "How memory works" explainer (live fleet totals) ---
641
+ // The Memory tab's job: make the invisible pipeline legible. The
642
+ // operator should understand WHAT the agents remember, the WHY behind
643
+ // each model, and HOW the pipeline turns conversations into recall.
644
+ const banks = m.banks || [];
645
+ const totals = banks.reduce((a, b) => {
646
+ a.docs += b.totalDocuments || 0;
647
+ a.facts += b.totalFacts || 0;
648
+ a.models += (b.mentalModels || []).length;
649
+ return a;
650
+ }, { docs: 0, facts: 0, models: 0 });
651
+ const fmtNum = (n) => (n || 0).toLocaleString();
652
+ const stage = (label, count, desc) => `<div style="flex:1 1 140px;min-width:130px;background:var(--surface-hover);border-radius:8px;padding:.6rem .7rem">
653
+ <div style="font-weight:600">${escapeHtml(label)}${count !== '' ? ` <span style="color:var(--blue)">${count}</span>` : ''}</div>
654
+ <div style="color:var(--text-dim);font-size:.78em;margin-top:.25rem;line-height:1.35">${escapeHtml(desc)}</div>
655
+ </div>`;
656
+ const arrow = `<div style="display:flex;align-items:center;color:var(--text-dim);font-size:1.1em">→</div>`;
657
+ const explainer = `<div class="agent-card" style="margin-bottom:1rem">
658
+ <div class="card-header" style="cursor:default">
659
+ <span class="agent-name">How memory works</span>
660
+ <span style="color:var(--text-dim);font-size:.85em;margin-left:.5rem">${banks.length} bank${banks.length === 1 ? '' : 's'} · hindsight</span>
661
+ </div>
662
+ <div style="padding:0 1.25rem 1rem">
663
+ <div style="display:flex;flex-wrap:wrap;align-items:stretch;gap:.5rem;margin:.2rem 0 .7rem">
664
+ ${stage('Conversations', fmtNum(totals.docs), 'Every agent turn is retained verbatim as a document.')}
665
+ ${arrow}
666
+ ${stage('Facts', fmtNum(totals.facts), "Hindsight extracts durable facts from each conversation — on its OWN model, never the agent's quota.")}
667
+ ${arrow}
668
+ ${stage('Mental models', fmtNum(totals.models), 'Facts are synthesized into named models, each answering one recall question.')}
669
+ ${arrow}
670
+ ${stage('Recall', '', 'On each turn the agent pulls the relevant models back into context — it never re-reads raw history.')}
671
+ </div>
672
+ <div style="color:var(--text-dim);font-size:.85em">Expand any model below to read what it knows and where it came from. A <span style="color:var(--yellow)">stale</span> or empty model means the agent is reasoning from an out-of-date picture.</div>
673
+ </div>
674
+ </div>`;
675
+
676
+ const cards = banks.map((b, bi) => {
640
677
  const bankJs = attrJson(b.bank);
641
- const models = (b.mentalModels || []).map(mm => {
678
+ const models = (b.mentalModels || []).map((mm, mi) => {
642
679
  const ts = mm.lastRefreshedAt || mm.createdAt;
643
680
  const stale = ts && (Date.now() - Date.parse(ts)) > 7 * 86400000;
644
- return `<div class="meta-item"><label>${escapeHtml(mm.name)} </label><span style="${stale ? 'color:var(--yellow)' : ''}">${fmtAge(ts) || 'never refreshed'}</span></div>`;
681
+ const detailId = `memdetail-${bi}-${mi}`;
682
+ const idJs = attrJson(mm.id);
683
+ const ageLabel = fmtAge(ts) || 'never refreshed';
684
+ const why = mm.sourceQuery
685
+ ? `<div style="color:var(--text-dim);font-size:.84em;margin-top:.15rem">answers: “${escapeHtml(mm.sourceQuery)}”</div>`
686
+ : '';
687
+ const modeBadge = mm.refreshMode
688
+ ? `<span style="color:var(--text-dim);font-size:.78em;border:1px solid var(--border);border-radius:5px;padding:0 .35rem">${escapeHtml(mm.refreshMode)}</span>`
689
+ : '';
690
+ return `<div style="border-top:1px solid var(--border);padding:.5rem 0">
691
+ <div style="display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap">
692
+ <strong>${escapeHtml(mm.name)}</strong>
693
+ <span style="font-size:.82em;${stale ? 'color:var(--yellow)' : 'color:var(--text-dim)'}">${ageLabel}${stale ? ' · stale' : ''}</span>
694
+ ${modeBadge}
695
+ <button class="btn" type="button" style="margin-left:auto;padding:.12rem .55rem;font-size:.8em" onclick='memViewModel(${bankJs}, ${idJs}, "${detailId}", this)'>view</button>
696
+ </div>
697
+ ${why}
698
+ <div id="${detailId}" style="display:none;margin-top:.5rem"></div>
699
+ </div>`;
645
700
  }).join('');
646
701
  const gapLine = b.recentUnextractedCount > 0
647
702
  ? `<div style="color:var(--red);margin-top:.4rem">⚠ ${b.recentUnextractedCount} recent conversation(s) stored but NOT extracted (oldest ${fmtDay(b.oldestUnextractedAt)}) — invisible to recall until reprocessed</div>`
@@ -694,14 +749,14 @@
694
749
  <div class="meta-item"><label>Latest activity </label><span>${fmtDay(b.newestDocumentAt)} ${fmtAge(b.newestDocumentAt) ? '(' + fmtAge(b.newestDocumentAt) + ')' : ''}</span></div>
695
750
  <div class="meta-item"><label>Mental models </label><span>${(b.mentalModels || []).length}${b.staleMentalModelCount ? ` <span style="color:var(--yellow)">(${b.staleMentalModelCount} stale)</span>` : ''}</span></div>
696
751
  </div>
697
- ${models ? `<div class="card-meta" style="padding:.4rem 0 0">${models}</div>` : ''}
752
+ ${models ? `<div style="margin-top:.5rem"><div style="font-size:.74em;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.1rem">Mental models — what this fleet remembers</div>${models}</div>` : ''}
698
753
  ${corruptLine}
699
754
  ${gapLine}
700
755
  ${buttonRow}
701
756
  </div>
702
757
  </div>`;
703
758
  }).join('');
704
- container.innerHTML = `<div class="agents-grid">${cards || '<div style="color:var(--text-dim)">No agent banks configured.</div>'}</div>`;
759
+ container.innerHTML = explainer + `<div class="agents-grid">${cards || '<div style="color:var(--text-dim)">No agent banks configured.</div>'}</div>`;
705
760
  }
706
761
 
707
762
  // --- Memory remediation actions ---
@@ -750,6 +805,58 @@
750
805
  () => 'Profile build triggered');
751
806
  }
752
807
 
808
+ // --- View one mental model's full content + provenance ---
809
+ // Lazy: the memory-health list carries only summaries (name/why/age) so
810
+ // it stays light across many banks; the full text is pulled on demand
811
+ // when the operator clicks "view". Pure READ of hindsight's REST — no
812
+ // model call, no quota. Re-click toggles collapse (content cached after
813
+ // the first load).
814
+ async function memViewModel(bank, id, detailId, btn) {
815
+ const el = document.getElementById(detailId);
816
+ if (!el) return;
817
+ if (el.dataset.open === '1') {
818
+ el.style.display = 'none';
819
+ el.dataset.open = '0';
820
+ if (btn) btn.textContent = 'view';
821
+ return;
822
+ }
823
+ el.style.display = 'block';
824
+ el.dataset.open = '1';
825
+ if (btn) btn.textContent = 'hide';
826
+ if (el.dataset.loaded === '1') return;
827
+ el.innerHTML = '<div style="color:var(--text-dim);font-size:.85em">Loading…</div>';
828
+ try {
829
+ const res = await fetch(`${API}/api/memory/model?bank=${encodeURIComponent(bank)}&id=${encodeURIComponent(id)}`, { headers: authHeaders() });
830
+ const data = await res.json().catch(() => ({}));
831
+ if (!res.ok || !data.ok || !data.model) {
832
+ el.innerHTML = `<div style="color:var(--red);font-size:.85em">Couldn't load model: ${escapeHtml(data.error || ('HTTP ' + res.status))}</div>`;
833
+ return;
834
+ }
835
+ el.innerHTML = renderModelDetail(data.model);
836
+ el.dataset.loaded = '1';
837
+ } catch (err) {
838
+ el.innerHTML = `<div style="color:var(--red);font-size:.85em">Couldn't load model: ${escapeHtml(err.message)}</div>`;
839
+ }
840
+ }
841
+
842
+ // The "why + how" of a single model: where it came from (provenance) and
843
+ // the full content the agent actually recalls.
844
+ function renderModelDetail(model) {
845
+ const prov = Object.entries(model.basedOnCounts || {})
846
+ .filter(([, n]) => n > 0)
847
+ .sort((a, b) => b[1] - a[1])
848
+ .map(([type, n]) => `${n} ${escapeHtml(type.replace(/-/g, ' '))}`)
849
+ .join(' · ');
850
+ const provLine = (model.totalSourceFacts || 0) > 0
851
+ ? `<div style="color:var(--text-dim);font-size:.82em;margin-bottom:.4rem">Synthesized from ${model.totalSourceFacts} source fact${model.totalSourceFacts === 1 ? '' : 's'}: ${prov}</div>`
852
+ : `<div style="color:var(--text-dim);font-size:.82em;margin-bottom:.4rem">No source facts recorded — likely a seed or manually set value.</div>`;
853
+ const content = (model.content || '').trim();
854
+ const body = content
855
+ ? `<pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:.7rem;max-height:340px;overflow:auto;font-size:.82em;line-height:1.45;margin:0">${escapeHtml(content)}</pre>`
856
+ : `<div style="color:var(--yellow);font-size:.85em">This model has no content — it's empty, so the agent recalls nothing from it.</div>`;
857
+ return provLine + body;
858
+ }
859
+
753
860
  async function fetchConnections() {
754
861
  // Each fetch falls back independently (.catch → default). A single
755
862
  // network blip — e.g. one endpoint momentarily unreachable — must NOT
@@ -22199,10 +22199,22 @@ function scheduleFrameBytes(view) {
22199
22199
  function boundScheduleView(entries, recentByAgent) {
22200
22200
  const slimEntries = entries.map(slimScheduleEntry);
22201
22201
  const truncSummary = (r) => typeof r.outputSummary === "string" && r.outputSummary.length > SCHEDULE_OUTPUT_SUMMARY_MAX_CHARS ? { ...r, outputSummary: r.outputSummary.slice(0, SCHEDULE_OUTPUT_SUMMARY_MAX_CHARS) } : { ...r };
22202
+ const currentKeysByAgent = new Map;
22203
+ for (const e of entries) {
22204
+ let s = currentKeysByAgent.get(e.agent);
22205
+ if (!s) {
22206
+ s = new Set;
22207
+ currentKeysByAgent.set(e.agent, s);
22208
+ }
22209
+ s.add(e.promptKey);
22210
+ }
22202
22211
  const boundedRecent = {};
22203
22212
  for (const [agent, rows] of Object.entries(recentByAgent)) {
22213
+ const currentKeys = currentKeysByAgent.get(agent) ?? new Set;
22204
22214
  const byKey = new Map;
22205
22215
  for (const r of rows) {
22216
+ if (!currentKeys.has(r.promptKey))
22217
+ continue;
22206
22218
  const arr = byKey.get(r.promptKey);
22207
22219
  if (arr)
22208
22220
  arr.push(r);
@@ -22214,7 +22226,8 @@ function boundScheduleView(entries, recentByAgent) {
22214
22226
  for (const r of arr.slice(-SCHEDULE_MAX_FIRES_PER_CRON))
22215
22227
  kept.push(truncSummary(r));
22216
22228
  }
22217
- boundedRecent[agent] = kept;
22229
+ if (kept.length > 0)
22230
+ boundedRecent[agent] = kept;
22218
22231
  }
22219
22232
  let view = { entries: slimEntries, recentByAgent: boundedRecent };
22220
22233
  if (scheduleFrameBytes(view) <= SCHEDULE_FRAME_BUDGET_BYTES)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.15.29",
3
+ "version": "0.15.31",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54460,10 +54460,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
54460
54460
  }
54461
54461
 
54462
54462
  // ../src/build-info.ts
54463
- var VERSION = "0.15.29";
54464
- var COMMIT_SHA = "eddde22d";
54465
- var COMMIT_DATE = "2026-06-15T09:43:58Z";
54466
- var LATEST_PR = 2376;
54463
+ var VERSION = "0.15.31";
54464
+ var COMMIT_SHA = "9c6a82c6";
54465
+ var COMMIT_DATE = "2026-06-15T11:52:29Z";
54466
+ var LATEST_PR = 2380;
54467
54467
  var COMMITS_AHEAD_OF_TAG = 0;
54468
54468
 
54469
54469
  // gateway/boot-version.ts