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.
package/dist/cli/switchroom.js
CHANGED
|
@@ -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.
|
|
50517
|
-
var COMMIT_SHA = "
|
|
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]) {
|
package/dist/cli/ui/index.html
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
@@ -54460,10 +54460,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
54460
54460
|
}
|
|
54461
54461
|
|
|
54462
54462
|
// ../src/build-info.ts
|
|
54463
|
-
var VERSION = "0.15.
|
|
54464
|
-
var COMMIT_SHA = "
|
|
54465
|
-
var COMMIT_DATE = "2026-06-
|
|
54466
|
-
var LATEST_PR =
|
|
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
|