switchroom 0.15.32 → 0.15.34
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 +41 -21
- package/dist/cli/ui/index.html +271 -72
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +4 -4
package/dist/cli/switchroom.js
CHANGED
|
@@ -28994,6 +28994,26 @@ async function getJson(url, opts) {
|
|
|
28994
28994
|
return { ok: false, reason: String(err.message ?? err) };
|
|
28995
28995
|
}
|
|
28996
28996
|
}
|
|
28997
|
+
function summarizeBasedOn(reflect) {
|
|
28998
|
+
const basedOn = reflect?.based_on ?? {};
|
|
28999
|
+
const basedOnCounts = {};
|
|
29000
|
+
let totalSourceFacts = 0;
|
|
29001
|
+
const derivedFromModelIds = [];
|
|
29002
|
+
for (const [type, facts] of Object.entries(basedOn ?? {})) {
|
|
29003
|
+
if (!Array.isArray(facts))
|
|
29004
|
+
continue;
|
|
29005
|
+
basedOnCounts[type] = facts.length;
|
|
29006
|
+
totalSourceFacts += facts.length;
|
|
29007
|
+
if (type === "mental-models") {
|
|
29008
|
+
for (const f of facts) {
|
|
29009
|
+
const id = f?.id;
|
|
29010
|
+
if (typeof id === "string" && id)
|
|
29011
|
+
derivedFromModelIds.push(id);
|
|
29012
|
+
}
|
|
29013
|
+
}
|
|
29014
|
+
}
|
|
29015
|
+
return { basedOnCounts, totalSourceFacts, derivedFromModelIds };
|
|
29016
|
+
}
|
|
28997
29017
|
async function inspectBankHealth(mcpUrl, bankId, opts) {
|
|
28998
29018
|
const base = hindsightRestBase(mcpUrl);
|
|
28999
29019
|
const bank = encodeURIComponent(bankId);
|
|
@@ -29042,16 +29062,22 @@ async function inspectBankHealth(mcpUrl, bankId, opts) {
|
|
|
29042
29062
|
pendingOperations: stats.data.pending_operations ?? 0,
|
|
29043
29063
|
newestDocumentAt,
|
|
29044
29064
|
unextractedDocuments: unextracted,
|
|
29045
|
-
mentalModels: (models.data.items ?? []).filter((m) => typeof m?.id === "string" && typeof m?.name === "string").map((m) =>
|
|
29046
|
-
|
|
29047
|
-
|
|
29048
|
-
|
|
29049
|
-
|
|
29050
|
-
|
|
29051
|
-
|
|
29052
|
-
|
|
29053
|
-
|
|
29054
|
-
|
|
29065
|
+
mentalModels: (models.data.items ?? []).filter((m) => typeof m?.id === "string" && typeof m?.name === "string").map((m) => {
|
|
29066
|
+
const { basedOnCounts, totalSourceFacts, derivedFromModelIds } = summarizeBasedOn(m.reflect_response);
|
|
29067
|
+
return {
|
|
29068
|
+
id: m.id,
|
|
29069
|
+
name: m.name,
|
|
29070
|
+
lastRefreshedAt: m.last_refreshed_at ?? null,
|
|
29071
|
+
createdAt: m.created_at ?? null,
|
|
29072
|
+
contentLength: (m.content ?? "").length,
|
|
29073
|
+
contentHead: (m.content ?? "").slice(0, 200),
|
|
29074
|
+
sourceQuery: m.source_query ?? "",
|
|
29075
|
+
refreshMode: m.trigger?.mode ?? null,
|
|
29076
|
+
basedOnCounts,
|
|
29077
|
+
totalSourceFacts,
|
|
29078
|
+
derivedFromModelIds
|
|
29079
|
+
};
|
|
29080
|
+
})
|
|
29055
29081
|
};
|
|
29056
29082
|
}
|
|
29057
29083
|
async function getMentalModelDetail(mcpUrl, bankId, modelId, opts) {
|
|
@@ -29062,14 +29088,7 @@ async function getMentalModelDetail(mcpUrl, bankId, modelId, opts) {
|
|
|
29062
29088
|
if (!res.ok)
|
|
29063
29089
|
return res;
|
|
29064
29090
|
const m = res.data;
|
|
29065
|
-
const
|
|
29066
|
-
const basedOnCounts = {};
|
|
29067
|
-
let totalSourceFacts = 0;
|
|
29068
|
-
for (const [type, facts] of Object.entries(basedOn)) {
|
|
29069
|
-
const n = Array.isArray(facts) ? facts.length : 0;
|
|
29070
|
-
basedOnCounts[type] = n;
|
|
29071
|
-
totalSourceFacts += n;
|
|
29072
|
-
}
|
|
29091
|
+
const { basedOnCounts, totalSourceFacts, derivedFromModelIds } = summarizeBasedOn(m.reflect_response);
|
|
29073
29092
|
return {
|
|
29074
29093
|
ok: true,
|
|
29075
29094
|
model: {
|
|
@@ -29081,7 +29100,8 @@ async function getMentalModelDetail(mcpUrl, bankId, modelId, opts) {
|
|
|
29081
29100
|
createdAt: m.created_at ?? null,
|
|
29082
29101
|
refreshMode: m.trigger?.mode ?? null,
|
|
29083
29102
|
basedOnCounts,
|
|
29084
|
-
totalSourceFacts
|
|
29103
|
+
totalSourceFacts,
|
|
29104
|
+
derivedFromModelIds
|
|
29085
29105
|
}
|
|
29086
29106
|
};
|
|
29087
29107
|
}
|
|
@@ -50554,8 +50574,8 @@ var {
|
|
|
50554
50574
|
} = import__.default;
|
|
50555
50575
|
|
|
50556
50576
|
// src/build-info.ts
|
|
50557
|
-
var VERSION = "0.15.
|
|
50558
|
-
var COMMIT_SHA = "
|
|
50577
|
+
var VERSION = "0.15.34";
|
|
50578
|
+
var COMMIT_SHA = "508eb512";
|
|
50559
50579
|
|
|
50560
50580
|
// src/cli/agent.ts
|
|
50561
50581
|
init_source();
|
package/dist/cli/ui/index.html
CHANGED
|
@@ -485,6 +485,59 @@
|
|
|
485
485
|
.card-meta { gap: 0.3rem 1rem; }
|
|
486
486
|
nav.tabs { padding: 0 1rem; overflow-x: auto; }
|
|
487
487
|
}
|
|
488
|
+
|
|
489
|
+
/* --- Memory tab: banded cards, provenance bars, model relationships --- */
|
|
490
|
+
.mm-band { padding: .55rem 0; border-top: 1px solid var(--border); }
|
|
491
|
+
.mm-band-h { font-size: .68rem; letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim); margin-bottom: .4rem; display: flex; align-items: center; gap: .4rem; }
|
|
492
|
+
.mm-band-h .n { color: var(--text); opacity: .6; }
|
|
493
|
+
.mm-stat-line { color: var(--text); font-size: .9em; line-height: 1.5; }
|
|
494
|
+
.mm-stat-line .dim { color: var(--text-dim); }
|
|
495
|
+
/* stacked provenance bar (segments sized by source-fact composition) */
|
|
496
|
+
.prov-bar { display: flex; height: 7px; border-radius: 4px; overflow: hidden; background: var(--border); margin: .35rem 0 .15rem; }
|
|
497
|
+
.prov-bar > span { display: block; min-width: 2px; }
|
|
498
|
+
.prov-obs { background: var(--blue); }
|
|
499
|
+
.prov-dir { background: var(--green); }
|
|
500
|
+
.prov-der { background: var(--yellow); }
|
|
501
|
+
.prov-legend { font-size: .72rem; color: var(--text-dim); display: flex; flex-wrap: wrap; gap: .15rem .8rem; margin-top: .3rem; }
|
|
502
|
+
.prov-legend i { width: .6rem; height: .6rem; border-radius: 2px; display: inline-block; vertical-align: middle; margin-right: .25rem; }
|
|
503
|
+
/* freshness heat-strip: one cell per model */
|
|
504
|
+
.heat { display: flex; gap: 2px; flex-wrap: wrap; margin-top: .4rem; }
|
|
505
|
+
.heat span { width: 16px; height: 8px; border-radius: 2px; }
|
|
506
|
+
.heat .fresh { background: var(--green); }
|
|
507
|
+
.heat .stale { background: var(--yellow); }
|
|
508
|
+
.heat .cold { background: var(--text-dim); opacity: .45; }
|
|
509
|
+
.heat .corrupt { background: var(--red); }
|
|
510
|
+
/* model rows + cluster (hub→leaf) tree connectors */
|
|
511
|
+
.mm-row { padding: .4rem 0; }
|
|
512
|
+
.mm-row + .mm-row { border-top: 1px solid var(--border); }
|
|
513
|
+
.mm-cluster { border-top: 1px solid var(--border); }
|
|
514
|
+
.mm-cluster .mm-row + .mm-row { border-top: none; }
|
|
515
|
+
.mm-top { display: flex; align-items: baseline; gap: .45rem; flex-wrap: wrap; }
|
|
516
|
+
.mm-name { font-weight: 600; }
|
|
517
|
+
.mm-meta { font-size: .8em; color: var(--text-dim); margin-left: auto; white-space: nowrap; }
|
|
518
|
+
.mm-why { color: var(--text-dim); font-size: .84em; margin-top: .12rem; }
|
|
519
|
+
.mm-leaf { padding-left: 1.15rem; position: relative; }
|
|
520
|
+
.mm-leaf::before { content: ""; position: absolute; left: .35rem; top: 0; bottom: 0; border-left: 2px solid var(--border); }
|
|
521
|
+
.mm-leaf:last-child::before { bottom: calc(100% - 1.05rem); }
|
|
522
|
+
.mm-leaf::after { content: ""; position: absolute; left: .35rem; top: 1.05rem; width: .5rem; border-top: 2px solid var(--border); }
|
|
523
|
+
/* relationship chips */
|
|
524
|
+
.mm-chips { margin-top: .25rem; font-size: .8em; color: var(--text-dim); display: flex; flex-wrap: wrap; gap: .25rem; align-items: center; }
|
|
525
|
+
.mm-chip { background: var(--surface-hover); border: 1px solid var(--border); border-radius: 5px; padding: 0 .35rem; cursor: pointer; color: var(--text); }
|
|
526
|
+
.mm-chip:hover { border-color: var(--blue); }
|
|
527
|
+
.mm-chip.raw { cursor: default; color: var(--text-dim); }
|
|
528
|
+
.mm-chip.muted { background: transparent; border: 1px dashed var(--border); cursor: default; color: var(--text-dim); }
|
|
529
|
+
/* model badges */
|
|
530
|
+
.mm-badge { font-size: .7rem; border: 1px solid var(--border); border-radius: 5px; padding: 0 .3rem; color: var(--text-dim); }
|
|
531
|
+
.mm-badge.stale { color: var(--yellow); border-color: var(--yellow); }
|
|
532
|
+
.mm-badge.corrupt { color: var(--red); border-color: var(--red); }
|
|
533
|
+
.mm-badge.hub { color: var(--blue); border-color: var(--blue); }
|
|
534
|
+
/* flash when a chip scrolls you to its target row */
|
|
535
|
+
@keyframes mmflash { 0% { background: rgba(96,165,250,.28); } 100% { background: transparent; } }
|
|
536
|
+
.mm-flash { animation: mmflash 1.3s ease-out; border-radius: 6px; }
|
|
537
|
+
/* attention band (problems promoted to top of card) */
|
|
538
|
+
.mm-attn { border-left: 3px solid var(--red); padding: .4rem .6rem; margin: .15rem 0 .35rem; background: rgba(248,113,113,.07); border-radius: 0 6px 6px 0; font-size: .86em; }
|
|
539
|
+
.mm-attn.warn { border-left-color: var(--yellow); background: rgba(251,191,36,.06); }
|
|
540
|
+
.mm-actions { display: flex; flex-wrap: wrap; gap: .4rem; }
|
|
488
541
|
</style>
|
|
489
542
|
</head>
|
|
490
543
|
<body>
|
|
@@ -620,6 +673,7 @@
|
|
|
620
673
|
container.innerHTML = renderProblem(problemFor('hindsight-down', { url: m.url }));
|
|
621
674
|
return;
|
|
622
675
|
}
|
|
676
|
+
const banks = m.banks || [];
|
|
623
677
|
const statusDot = (s) => `<span class="status-dot ${s === 'ok' ? 'active' : s === 'warn' ? 'auth-warning' : 'inactive'}" style="display:inline-block;vertical-align:middle"></span>`;
|
|
624
678
|
const fmtDay = (iso) => iso ? iso.slice(0, 10) : '—';
|
|
625
679
|
const fmtAge = (iso) => {
|
|
@@ -628,32 +682,50 @@
|
|
|
628
682
|
if (isNaN(d)) return '';
|
|
629
683
|
return d < 1 ? 'today' : `${Math.round(d)}d ago`;
|
|
630
684
|
};
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
685
|
+
const fmtNum = (n) => (n || 0).toLocaleString();
|
|
686
|
+
const fmtK = (n) => (n >= 1000 ? (n / 1000).toFixed(n >= 10000 ? 0 : 1) + 'k' : String(n || 0));
|
|
687
|
+
const isStaleTs = (iso) => iso && (Date.now() - Date.parse(iso)) > 7 * 86400000;
|
|
634
688
|
const hasUserProfile = (b) => (b.mentalModels || []).some(mm =>
|
|
635
689
|
/^user[- ]?profile$/i.test(String(mm.name || '')));
|
|
636
|
-
// JSON for a single-quoted onclick attribute
|
|
637
|
-
// chars that could break out of the attribute (', &, <, >).
|
|
690
|
+
// JSON for a single-quoted onclick attribute (escapeHtml covers ' & < > ").
|
|
638
691
|
const attrJson = (v) => escapeHtml(JSON.stringify(v));
|
|
692
|
+
// Fold hindsight's fact types into 3 provenance buckets: observations,
|
|
693
|
+
// directives/other facts, and derived-from-other-models.
|
|
694
|
+
const provSegments = (counts) => {
|
|
695
|
+
const c = counts || {};
|
|
696
|
+
const obs = c.observation || 0;
|
|
697
|
+
const models = c['mental-models'] || 0;
|
|
698
|
+
let other = 0;
|
|
699
|
+
for (const [k, v] of Object.entries(c)) {
|
|
700
|
+
if (k !== 'observation' && k !== 'mental-models') other += v || 0;
|
|
701
|
+
}
|
|
702
|
+
return { obs, other, models, total: obs + other + models };
|
|
703
|
+
};
|
|
704
|
+
const provBar = (counts, h) => {
|
|
705
|
+
const s = provSegments(counts);
|
|
706
|
+
if (s.total === 0) return '';
|
|
707
|
+
const seg = (n, cls) => n > 0 ? `<span class="${cls}" style="flex:${n}" title="${n}"></span>` : '';
|
|
708
|
+
return `<div class="prov-bar"${h ? ` style="height:${h}px"` : ''}>${seg(s.obs, 'prov-obs')}${seg(s.other, 'prov-dir')}${seg(s.models, 'prov-der')}</div>`;
|
|
709
|
+
};
|
|
639
710
|
|
|
640
|
-
// --- "How memory works" explainer
|
|
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 || [];
|
|
711
|
+
// --- "How memory works" explainer + shared provenance legend ---
|
|
645
712
|
const totals = banks.reduce((a, b) => {
|
|
646
713
|
a.docs += b.totalDocuments || 0;
|
|
647
714
|
a.facts += b.totalFacts || 0;
|
|
648
715
|
a.models += (b.mentalModels || []).length;
|
|
649
716
|
return a;
|
|
650
717
|
}, { docs: 0, facts: 0, models: 0 });
|
|
651
|
-
const fmtNum = (n) => (n || 0).toLocaleString();
|
|
652
718
|
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
719
|
<div style="font-weight:600">${escapeHtml(label)}${count !== '' ? ` <span style="color:var(--blue)">${count}</span>` : ''}</div>
|
|
654
720
|
<div style="color:var(--text-dim);font-size:.78em;margin-top:.25rem;line-height:1.35">${escapeHtml(desc)}</div>
|
|
655
721
|
</div>`;
|
|
656
722
|
const arrow = `<div style="display:flex;align-items:center;color:var(--text-dim);font-size:1.1em">→</div>`;
|
|
723
|
+
const legend = `<div class="prov-legend">
|
|
724
|
+
<span style="color:var(--text)">Source mix:</span>
|
|
725
|
+
<span><i class="prov-obs"></i>observations</span>
|
|
726
|
+
<span><i class="prov-dir"></i>directives & facts</span>
|
|
727
|
+
<span><i class="prov-der"></i>from other models</span>
|
|
728
|
+
</div>`;
|
|
657
729
|
const explainer = `<div class="agent-card" style="margin-bottom:1rem">
|
|
658
730
|
<div class="card-header" style="cursor:default">
|
|
659
731
|
<span class="agent-name">How memory works</span>
|
|
@@ -665,100 +737,186 @@
|
|
|
665
737
|
${arrow}
|
|
666
738
|
${stage('Facts', fmtNum(totals.facts), "Hindsight extracts durable facts from each conversation — on its OWN model, never the agent's quota.")}
|
|
667
739
|
${arrow}
|
|
668
|
-
${stage('Mental models', fmtNum(totals.models), 'Facts are synthesized into named models, each answering one recall question.')}
|
|
740
|
+
${stage('Mental models', fmtNum(totals.models), 'Facts (and other models) are synthesized into named models, each answering one recall question.')}
|
|
669
741
|
${arrow}
|
|
670
742
|
${stage('Recall', '', 'On each turn the agent pulls the relevant models back into context — it never re-reads raw history.')}
|
|
671
743
|
</div>
|
|
672
|
-
<div style="color:var(--text-dim);font-size:.85em">
|
|
744
|
+
<div style="color:var(--text-dim);font-size:.85em">A <span style="color:var(--blue)">hub</span> model synthesizes other models (the <span style="color:var(--text)">draws on</span> links); a <span style="color:var(--yellow)">stale</span> or empty model means the agent is reasoning from an out-of-date picture. The bars below show each model's source mix.</div>
|
|
745
|
+
${legend}
|
|
673
746
|
</div>
|
|
674
747
|
</div>`;
|
|
675
748
|
|
|
676
749
|
const cards = banks.map((b, bi) => {
|
|
677
750
|
const bankJs = attrJson(b.bank);
|
|
678
|
-
const models =
|
|
751
|
+
const models = b.mentalModels || [];
|
|
752
|
+
const corrupt = new Set(b.corruptedMentalModelNames || []);
|
|
753
|
+
const byId = new Map(models.map(mm => [mm.id, mm]));
|
|
754
|
+
// model→model edges, self-references and dangling ids filtered for hub-ness
|
|
755
|
+
const edgesOf = (mm) => (mm.derivedFromModelIds || []).filter(id => id && id !== mm.id);
|
|
756
|
+
const anyEdges = models.some(mm => edgesOf(mm).length > 0);
|
|
757
|
+
const hubs = models.filter(mm => edgesOf(mm).length > 0);
|
|
758
|
+
|
|
759
|
+
const freshRank = (mm) => corrupt.has(mm.name) ? 2 : isStaleTs(mm.lastRefreshedAt || mm.createdAt) ? 1 : 0;
|
|
760
|
+
const tsMs = (mm) => { const ts = mm.lastRefreshedAt || mm.createdAt; return ts ? Date.parse(ts) : 0; };
|
|
761
|
+
const freshCmp = (a, c) => freshRank(a) - freshRank(c) || tsMs(c) - tsMs(a);
|
|
762
|
+
|
|
763
|
+
// One model row. isLeaf indents it under a hub with a tree connector.
|
|
764
|
+
const modelRow = (mm, opts) => {
|
|
765
|
+
opts = opts || {};
|
|
679
766
|
const ts = mm.lastRefreshedAt || mm.createdAt;
|
|
680
|
-
const stale =
|
|
681
|
-
const
|
|
767
|
+
const stale = isStaleTs(ts);
|
|
768
|
+
const corr = corrupt.has(mm.name);
|
|
769
|
+
const isHub = edgesOf(mm).length > 0;
|
|
770
|
+
const safe = (s) => String(s).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
771
|
+
const detailId = `memdetail-${bi}-${safe(mm.id)}`;
|
|
772
|
+
const rowId = `mmrow-${bi}-${safe(mm.id)}`;
|
|
682
773
|
const idJs = attrJson(mm.id);
|
|
683
|
-
const
|
|
774
|
+
const meta = [fmtK(mm.contentLength) + 'c', mm.refreshMode, fmtAge(ts) || 'never'].filter(Boolean).join(' · ');
|
|
775
|
+
const badges = [
|
|
776
|
+
isHub ? `<span class="mm-badge hub" title="synthesizes other models">hub</span>` : '',
|
|
777
|
+
corr ? `<span class="mm-badge corrupt">corrupted</span>` : (stale ? `<span class="mm-badge stale">stale</span>` : ''),
|
|
778
|
+
].join('');
|
|
779
|
+
let chips = '';
|
|
780
|
+
if (isHub) {
|
|
781
|
+
const items = edgesOf(mm).map(id => {
|
|
782
|
+
const t = byId.get(id);
|
|
783
|
+
return t
|
|
784
|
+
? `<span class="mm-chip" onclick='focusModel(${bi}, ${attrJson(t.id)})'>${escapeHtml(t.name)}</span>`
|
|
785
|
+
: `<span class="mm-chip raw" title="not a model in this bank">${escapeHtml(id)}</span>`;
|
|
786
|
+
});
|
|
787
|
+
const s = provSegments(mm.basedOnCounts);
|
|
788
|
+
if (s.obs + s.other > 0) items.push(`<span class="mm-chip muted" title="also synthesized from raw facts">+ raw facts</span>`);
|
|
789
|
+
chips = `<div class="mm-chips"><span>draws on:</span>${items.join('')}</div>`;
|
|
790
|
+
}
|
|
684
791
|
const why = mm.sourceQuery
|
|
685
|
-
? `<div
|
|
792
|
+
? `<div class="mm-why">answers: “${escapeHtml(mm.sourceQuery)}”</div>`
|
|
686
793
|
: '';
|
|
687
|
-
|
|
688
|
-
|
|
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>
|
|
794
|
+
return `<div class="mm-row${opts.leaf ? ' mm-leaf' : ''}" id="${rowId}">
|
|
795
|
+
<div class="mm-top"><span class="mm-name">${escapeHtml(mm.name)}</span>${badges}<span class="mm-meta">${escapeHtml(meta)}</span></div>
|
|
697
796
|
${why}
|
|
698
|
-
|
|
797
|
+
${provBar(mm.basedOnCounts)}
|
|
798
|
+
${chips}
|
|
799
|
+
<div style="margin-top:.3rem"><button class="btn" type="button" style="padding:.1rem .5rem;font-size:.78em" onclick='memViewModel(${bankJs}, ${idJs}, "${detailId}", this)'>view content</button></div>
|
|
800
|
+
<div id="${detailId}" style="display:none;margin-top:.4rem"></div>
|
|
699
801
|
</div>`;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
// Order: each hub followed by its leaves (a cluster), then orphans
|
|
805
|
+
// freshest-first. No edges anywhere → a flat freshness-ordered list
|
|
806
|
+
// (no spine, no chips) — the common, calm case.
|
|
807
|
+
const rendered = new Set();
|
|
808
|
+
const blocks = [];
|
|
809
|
+
if (anyEdges) {
|
|
810
|
+
for (const hub of hubs) {
|
|
811
|
+
if (rendered.has(hub.id)) continue;
|
|
812
|
+
rendered.add(hub.id);
|
|
813
|
+
let cluster = modelRow(hub, {});
|
|
814
|
+
for (const id of edgesOf(hub)) {
|
|
815
|
+
const leaf = byId.get(id);
|
|
816
|
+
if (!leaf || rendered.has(leaf.id)) continue;
|
|
817
|
+
rendered.add(leaf.id);
|
|
818
|
+
cluster += modelRow(leaf, { leaf: true });
|
|
819
|
+
}
|
|
820
|
+
blocks.push(`<div class="mm-cluster">${cluster}</div>`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const orphans = models.filter(mm => !rendered.has(mm.id)).sort(freshCmp);
|
|
824
|
+
for (const o of orphans) blocks.push(modelRow(o, {}));
|
|
825
|
+
const modelsHtml = blocks.join('');
|
|
826
|
+
|
|
827
|
+
// Attention band — problems promoted to the top of the card.
|
|
828
|
+
const warnLines = [];
|
|
829
|
+
if ((b.corruptedMentalModelNames || []).length > 0) {
|
|
830
|
+
warnLines.push(`corrupted: ${escapeHtml(b.corruptedMentalModelNames.join(', '))} — content is an LLM failure message; refresh once quota recovers`);
|
|
831
|
+
}
|
|
832
|
+
if (b.recentUnextractedCount > 0) {
|
|
833
|
+
warnLines.push(`${b.recentUnextractedCount} recent conversation(s) stored but NOT extracted (oldest ${fmtDay(b.oldestUnextractedAt)}) — invisible to recall until reprocessed`);
|
|
834
|
+
}
|
|
835
|
+
const attn = warnLines.length
|
|
836
|
+
? `<div class="mm-attn ${b.status === 'fail' ? '' : 'warn'}">${warnLines.map(w => `<div>⚠ ${w}</div>`).join('')}</div>`
|
|
706
837
|
: '';
|
|
707
|
-
|
|
708
|
-
//
|
|
709
|
-
|
|
838
|
+
|
|
839
|
+
// Bank-summary band: dense stat line + aggregate source-mix bar + freshness heat-strip.
|
|
840
|
+
const agg = {};
|
|
841
|
+
for (const mm of models) for (const [k, v] of Object.entries(mm.basedOnCounts || {})) agg[k] = (agg[k] || 0) + (v || 0);
|
|
842
|
+
const statLine = `${fmtNum(b.totalDocuments)} <span class="dim">conversations</span> · ${fmtNum(b.totalFacts)} <span class="dim">facts</span> · <span class="dim">latest</span> ${fmtDay(b.newestDocumentAt)}${fmtAge(b.newestDocumentAt) ? ` <span class="dim">(${fmtAge(b.newestDocumentAt)})</span>` : ''} · ${models.length} <span class="dim">model${models.length === 1 ? '' : 's'}</span>${b.staleMentalModelCount ? ` <span style="color:var(--yellow)">(${b.staleMentalModelCount} stale)</span>` : ''}`;
|
|
843
|
+
const heatCell = (mm) => {
|
|
844
|
+
const ts = mm.lastRefreshedAt || mm.createdAt;
|
|
845
|
+
const age = ts ? (Date.now() - Date.parse(ts)) / 86400000 : null;
|
|
846
|
+
const cls = corrupt.has(mm.name) ? 'corrupt' : age === null ? 'cold' : age > 7 ? 'stale' : 'fresh';
|
|
847
|
+
return `<span class="${cls}" title="${escapeHtml(mm.name)} — ${fmtAge(ts) || 'never refreshed'}"></span>`;
|
|
848
|
+
};
|
|
849
|
+
const heat = models.length ? `<div class="heat" title="freshness, one cell per model">${models.map(heatCell).join('')}</div>` : '';
|
|
850
|
+
const aggBar = provBar(agg, 9);
|
|
851
|
+
|
|
852
|
+
// Mental-models band header — count + a hub-summary when relationships exist.
|
|
853
|
+
const mmHeader = `Mental models <span class="n">${models.length}</span>` +
|
|
854
|
+
(anyEdges ? `<span class="mm-badge hub" style="margin-left:auto">${hubs.length} synthesize${hubs.length === 1 ? 's' : ''} others</span>` : '');
|
|
855
|
+
|
|
856
|
+
// --- Remediation buttons (unchanged behaviour) ---
|
|
710
857
|
const buttons = [];
|
|
711
|
-
// Reprocess the extraction-gap docs.
|
|
712
858
|
if (b.recentUnextractedCount > 0) {
|
|
713
859
|
const n = Math.min(b.recentUnextractedCount, (b.unextractedDocIds || []).length) || b.recentUnextractedCount;
|
|
714
860
|
buttons.push(`<button class="btn" type="button" onclick='memReprocess(${bankJs}, this)'>Reprocess ${n} doc${n === 1 ? '' : 's'}</button>`);
|
|
715
861
|
}
|
|
716
|
-
// Refresh each corrupted/stale model (map corrupted NAME → id).
|
|
717
862
|
const affectedIds = new Set();
|
|
718
863
|
for (const name of (b.corruptedMentalModelNames || [])) {
|
|
719
|
-
const mm =
|
|
864
|
+
const mm = models.find(x => x.name === name);
|
|
720
865
|
if (mm && mm.id) affectedIds.add(mm.id);
|
|
721
866
|
}
|
|
722
|
-
for (const mm of
|
|
723
|
-
|
|
724
|
-
const stale = ts && (Date.now() - Date.parse(ts)) > 7 * 86400000;
|
|
725
|
-
if (stale && mm.id) affectedIds.add(mm.id);
|
|
867
|
+
for (const mm of models) {
|
|
868
|
+
if (isStaleTs(mm.lastRefreshedAt || mm.createdAt) && mm.id) affectedIds.add(mm.id);
|
|
726
869
|
}
|
|
727
|
-
for (const mm of
|
|
870
|
+
for (const mm of models) {
|
|
728
871
|
if (!affectedIds.has(mm.id)) continue;
|
|
729
872
|
const idJs = attrJson(mm.id), nameJs = attrJson(mm.name);
|
|
730
873
|
buttons.push(`<button class="btn" type="button" onclick='memRefreshModel(${bankJs}, ${idJs}, ${nameJs}, this)'>Refresh ${escapeHtml(mm.name)}</button>`);
|
|
731
874
|
}
|
|
732
|
-
// Build the user-profile model when the bank has data but no profile.
|
|
733
875
|
if (b.totalDocuments > 0 && !hasUserProfile(b)) {
|
|
734
876
|
buttons.push(`<button class="btn" type="button" onclick='memBuildProfile(${bankJs}, this)'>Build profile</button>`);
|
|
735
877
|
}
|
|
736
|
-
const
|
|
737
|
-
? `<div class="
|
|
878
|
+
const actionsBand = buttons.length
|
|
879
|
+
? `<div class="mm-band"><div class="mm-band-h">Actions</div><div class="mm-actions">${buttons.join('')}</div></div>`
|
|
738
880
|
: '';
|
|
881
|
+
|
|
739
882
|
return `<div class="agent-card">
|
|
740
883
|
<div class="card-header" style="cursor:default">
|
|
741
884
|
${statusDot(b.status)}<span class="agent-name">${escapeHtml(b.bank)}</span>
|
|
742
885
|
<span style="color:var(--text-dim);font-size:.85em;margin-left:.5rem">${escapeHtml((b.agents || []).join(', '))}</span>
|
|
743
886
|
</div>
|
|
744
887
|
<div style="padding:0 1.25rem 1rem">
|
|
745
|
-
<div style="color:var(--text-dim);margin
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
<div class="
|
|
749
|
-
<div class="
|
|
750
|
-
|
|
888
|
+
<div style="color:var(--text-dim);margin:.1rem 0 .3rem">${escapeHtml(b.statusDetail || '')}</div>
|
|
889
|
+
${attn}
|
|
890
|
+
<div class="mm-band" style="border-top:none">
|
|
891
|
+
<div class="mm-band-h">Bank summary</div>
|
|
892
|
+
<div class="mm-stat-line">${statLine}</div>
|
|
893
|
+
${aggBar}
|
|
894
|
+
${heat}
|
|
751
895
|
</div>
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
896
|
+
<div class="mm-band">
|
|
897
|
+
<div class="mm-band-h">${mmHeader}</div>
|
|
898
|
+
${modelsHtml || '<div style="color:var(--text-dim);font-size:.85em">No mental models yet — facts are still accumulating.</div>'}
|
|
899
|
+
</div>
|
|
900
|
+
${actionsBand}
|
|
756
901
|
</div>
|
|
757
902
|
</div>`;
|
|
758
903
|
}).join('');
|
|
759
904
|
container.innerHTML = explainer + `<div class="agents-grid">${cards || '<div style="color:var(--text-dim)">No agent banks configured.</div>'}</div>`;
|
|
760
905
|
}
|
|
761
906
|
|
|
907
|
+
// Chip-as-cross-link: scroll to the named model's row in the same card
|
|
908
|
+
// and flash it. Pure DOM — no graph canvas. `bi` is the bank index, `id`
|
|
909
|
+
// the target model id (must match modelRow's id sanitization exactly).
|
|
910
|
+
function focusModel(bi, id) {
|
|
911
|
+
const rowId = `mmrow-${bi}-${String(id).replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
912
|
+
const el = document.getElementById(rowId);
|
|
913
|
+
if (!el) return;
|
|
914
|
+
el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
915
|
+
el.classList.remove('mm-flash');
|
|
916
|
+
void el.offsetWidth; // restart the animation
|
|
917
|
+
el.classList.add('mm-flash');
|
|
918
|
+
}
|
|
919
|
+
|
|
762
920
|
// --- Memory remediation actions ---
|
|
763
921
|
// Each pokes a hindsight REST/MCP endpoint via the web's POST routes.
|
|
764
922
|
// Hindsight does the LLM extraction on its OWN claude-code provider;
|
|
@@ -857,23 +1015,41 @@
|
|
|
857
1015
|
return provLine + body;
|
|
858
1016
|
}
|
|
859
1017
|
|
|
860
|
-
async function fetchConnections() {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
//
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
|
|
1018
|
+
async function fetchConnections(opts) {
|
|
1019
|
+
opts = opts || {};
|
|
1020
|
+
const attempt = opts.attempt || 0;
|
|
1021
|
+
// Distinguish a FAILED fetch from a genuinely-empty result. The old
|
|
1022
|
+
// `safe` collapsed both to []/{}, so a transient broker/web blip
|
|
1023
|
+
// rendered identically to "nothing configured" — and because this tab
|
|
1024
|
+
// fetches only on open (NOT the 10s fleet poll), a one-time failure
|
|
1025
|
+
// stuck until a manual re-click. Track ok per fetch so renderConnections
|
|
1026
|
+
// can show an honest "couldn't load" state, and self-heal with bounded
|
|
1027
|
+
// retries. (A single endpoint failing still renders the others.)
|
|
1028
|
+
const safe = (p, fallback) => p
|
|
1029
|
+
.then(r => r.ok ? r.json().then(d => ({ ok: true, data: d })) : { ok: false, data: fallback })
|
|
1030
|
+
.catch(() => ({ ok: false, data: fallback }));
|
|
867
1031
|
try {
|
|
868
|
-
const [
|
|
1032
|
+
const [g, ms, n, ag] = await Promise.all([
|
|
869
1033
|
safe(fetch(`${API}/api/google-accounts`, { headers: authHeaders() }), []),
|
|
870
1034
|
safe(fetch(`${API}/api/microsoft-accounts`, { headers: authHeaders() }), []),
|
|
871
1035
|
safe(fetch(`${API}/api/notion-workspace`, { headers: authHeaders() }), { configured: false, databases: [] }),
|
|
872
1036
|
safe(fetch(`${API}/api/agents`, { headers: authHeaders() }), []),
|
|
873
1037
|
]);
|
|
874
|
-
|
|
875
|
-
|
|
1038
|
+
// Notion legitimately reports unconfigured — not a failure. The OAuth
|
|
1039
|
+
// providers + the agent list are the ones whose failure must not read
|
|
1040
|
+
// as "empty".
|
|
1041
|
+
const fetchFailed = !g.ok || !ms.ok || !ag.ok;
|
|
1042
|
+
renderConnections({
|
|
1043
|
+
google: g.data, microsoft: ms.data, notion: n.data,
|
|
1044
|
+
agentNames: (ag.data || []).map(a => a.name).sort(),
|
|
1045
|
+
googleFailed: !g.ok, microsoftFailed: !ms.ok, fetchFailed,
|
|
1046
|
+
});
|
|
876
1047
|
clearError();
|
|
1048
|
+
// Self-heal a transient blip without a manual re-click. Bounded
|
|
1049
|
+
// backoff (3s, 6s, 9s); stops the instant a fetch succeeds.
|
|
1050
|
+
if (fetchFailed && attempt < 3) {
|
|
1051
|
+
setTimeout(() => fetchConnections({ attempt: attempt + 1 }), 3000 * (attempt + 1));
|
|
1052
|
+
}
|
|
877
1053
|
} catch (err) {
|
|
878
1054
|
showError(`Failed to fetch connections: ${err.message}`);
|
|
879
1055
|
}
|
|
@@ -1408,6 +1584,14 @@
|
|
|
1408
1584
|
detail: 'These accounts are from the config; the broker did not return live slot state, so connection status may be stale.',
|
|
1409
1585
|
remediation: { kind: 'none', label: 'Refresh the tab once the broker is reachable to see live status.' },
|
|
1410
1586
|
};
|
|
1587
|
+
case 'connections-unreachable':
|
|
1588
|
+
return {
|
|
1589
|
+
title: "Couldn't load live connection data — retrying",
|
|
1590
|
+
detail: 'The dashboard could not reach the account data source (the broker or web container may be restarting). ' +
|
|
1591
|
+
'This is NOT "no accounts configured" — your connected accounts are unchanged. ' +
|
|
1592
|
+
'Auto-retrying; the tab fills in once it recovers.',
|
|
1593
|
+
remediation: { kind: 'none', label: 'Or refresh the page if it persists.' },
|
|
1594
|
+
};
|
|
1411
1595
|
default:
|
|
1412
1596
|
return { title: String(kind || 'Problem'), remediation: { kind: 'none' } };
|
|
1413
1597
|
}
|
|
@@ -1935,13 +2119,21 @@
|
|
|
1935
2119
|
const notion = data.notion || { configured: false, databases: [] };
|
|
1936
2120
|
const agentNames = data.agentNames || [];
|
|
1937
2121
|
|
|
2122
|
+
// When a provider's fetch FAILED (not genuinely empty), its empty-state
|
|
2123
|
+
// must say "couldn't load", never "none configured" — otherwise a
|
|
2124
|
+
// transient blip reads as "you have no accounts".
|
|
1938
2125
|
const googleSection = _connectionSection(
|
|
1939
2126
|
'Google',
|
|
1940
|
-
|
|
2127
|
+
data.googleFailed
|
|
2128
|
+
? "Couldn't load Google accounts — the data source was unreachable (retrying)."
|
|
2129
|
+
: 'No Google accounts. Add one under <code>google_accounts:</code> and run <code>switchroom auth google account add</code>.',
|
|
1941
2130
|
google.map(a => renderOAuthAccountCard(a, { showType: false, provider: 'google', agentNames })).join(''),
|
|
1942
2131
|
);
|
|
1943
2132
|
|
|
1944
2133
|
const msCards = microsoft.map(a => renderOAuthAccountCard(a, { showType: true, provider: 'microsoft', agentNames })).join('');
|
|
2134
|
+
const msEmpty = data.microsoftFailed
|
|
2135
|
+
? "Couldn't load Microsoft accounts — the data source was unreachable (retrying)."
|
|
2136
|
+
: 'No Microsoft accounts yet — click <b>Connect a Microsoft account</b> above (or <code>/connect microsoft</code> from Telegram).';
|
|
1945
2137
|
const microsoftSection = `
|
|
1946
2138
|
<div style="margin-bottom:1.5rem">
|
|
1947
2139
|
<h3 style="margin:0 0 .6rem;font-size:.95rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em">
|
|
@@ -1951,7 +2143,7 @@
|
|
|
1951
2143
|
<div id="ms-connect-card"></div>
|
|
1952
2144
|
${msCards
|
|
1953
2145
|
? `<div class="accounts-grid">${msCards}</div>`
|
|
1954
|
-
: `<div class="loading" style="padding:.8rem"
|
|
2146
|
+
: `<div class="loading" style="padding:.8rem">${msEmpty}</div>`}
|
|
1955
2147
|
</div>`;
|
|
1956
2148
|
|
|
1957
2149
|
let notionCards = '';
|
|
@@ -1998,7 +2190,14 @@
|
|
|
1998
2190
|
? renderProblem(problemFor('connections-degraded', {}))
|
|
1999
2191
|
: '';
|
|
2000
2192
|
|
|
2001
|
-
|
|
2193
|
+
// A failed fetch must NOT masquerade as "nothing configured" — lead
|
|
2194
|
+
// with an honest banner so the operator knows the empty look is a
|
|
2195
|
+
// load failure (auto-retrying), not an absence of accounts.
|
|
2196
|
+
const unreachableBanner = data.fetchFailed
|
|
2197
|
+
? renderProblem(problemFor('connections-unreachable', {}))
|
|
2198
|
+
: '';
|
|
2199
|
+
|
|
2200
|
+
container.innerHTML = unreachableBanner + degradedBanner + googleSection + microsoftSection + notionSection;
|
|
2002
2201
|
}
|
|
2003
2202
|
|
|
2004
2203
|
function renderSchedule(data) {
|
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.34";
|
|
54464
|
+
var COMMIT_SHA = "508eb512";
|
|
54465
|
+
var COMMIT_DATE = "2026-06-16T02:47:01Z";
|
|
54466
|
+
var LATEST_PR = 2389;
|
|
54467
54467
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
54468
54468
|
|
|
54469
54469
|
// gateway/boot-version.ts
|