switchroom 0.15.31 → 0.15.33
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 +71 -42
- package/dist/cli/ui/index.html +217 -59
- package/dist/host-control/main.js +18 -2
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +4 -4
package/dist/cli/switchroom.js
CHANGED
|
@@ -23724,11 +23724,12 @@ function generateCompose(opts) {
|
|
|
23724
23724
|
const containerNamePrefix = opts.containerNamePrefix ?? "switchroom";
|
|
23725
23725
|
const hostControlEnabled = config.host_control?.enabled !== false;
|
|
23726
23726
|
const hostHomeForChecks = opts.homeDir ?? process.env.HOME ?? "";
|
|
23727
|
+
const probeHome = opts.probeHomeDir ?? hostHomeForChecks;
|
|
23727
23728
|
const switchroomConfigPath = resolveConfigMountSource(opts.switchroomConfigPath, homePrefix);
|
|
23728
23729
|
const bundledSkillsPoolDir = opts.bundledSkillsPoolDir ?? getBundledSkillsPoolDir();
|
|
23729
23730
|
let resolvedAnalyticsId = null;
|
|
23730
|
-
if (
|
|
23731
|
-
const idPath = join10(
|
|
23731
|
+
if (probeHome !== "") {
|
|
23732
|
+
const idPath = join10(probeHome, ".switchroom", "analytics-id");
|
|
23732
23733
|
if (existsSync15(idPath)) {
|
|
23733
23734
|
try {
|
|
23734
23735
|
const raw = readFileSync13(idPath, "utf-8").trim();
|
|
@@ -23911,7 +23912,7 @@ function generateCompose(opts) {
|
|
|
23911
23912
|
if (a.strippedCaps.length > 0) {
|
|
23912
23913
|
warn(`compose: stripping cap_add ${JSON.stringify(a.strippedCaps)} from agent "${a.name}" (Docker mode forbids capability extras; see RFC \u00a7security)`);
|
|
23913
23914
|
}
|
|
23914
|
-
emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefix, hostHomeForChecks, switchroomConfigPath, containerNamePrefix, {
|
|
23915
|
+
emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefix, hostHomeForChecks, probeHome, switchroomConfigPath, containerNamePrefix, {
|
|
23915
23916
|
analyticsId: resolvedAnalyticsId,
|
|
23916
23917
|
telemetryDisabled,
|
|
23917
23918
|
posthogKeyOverride,
|
|
@@ -23941,7 +23942,7 @@ function generateCompose(opts) {
|
|
|
23941
23942
|
return lines.join(`
|
|
23942
23943
|
`);
|
|
23943
23944
|
}
|
|
23944
|
-
function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefix, hostHomeForChecks, switchroomConfigPath, containerNamePrefix, posthog, bundledSkillsPoolDir, hostControlEnabled, operatorUid) {
|
|
23945
|
+
function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefix, hostHomeForChecks, probeHome, switchroomConfigPath, containerNamePrefix, posthog, bundledSkillsPoolDir, hostControlEnabled, operatorUid) {
|
|
23945
23946
|
lines.push(` agent-${a.name}:`);
|
|
23946
23947
|
emitImageOrBuild(lines, "agent", imageTag, buildMode, buildContext);
|
|
23947
23948
|
lines.push(` container_name: ${containerNamePrefix}-${a.name}`);
|
|
@@ -24053,14 +24054,14 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
24053
24054
|
lines.push(` - /:/host:rw`);
|
|
24054
24055
|
}
|
|
24055
24056
|
if (a.admin === true) {
|
|
24056
|
-
if (existsSync15(`${
|
|
24057
|
+
if (existsSync15(`${probeHome}/.switchroom/vault-audit.log`)) {
|
|
24057
24058
|
lines.push(` - ${homePrefix}/.switchroom/vault-audit.log:/state/agent/home/.switchroom/vault-audit.log:ro`);
|
|
24058
24059
|
}
|
|
24059
|
-
if (existsSync15(`${
|
|
24060
|
+
if (existsSync15(`${probeHome}/.switchroom/host-control-audit.log`)) {
|
|
24060
24061
|
lines.push(` - ${homePrefix}/.switchroom/host-control-audit.log:/state/agent/home/.switchroom/host-control-audit.log:ro`);
|
|
24061
24062
|
}
|
|
24062
24063
|
}
|
|
24063
|
-
if (hostControlEnabled && existsSync15(`${
|
|
24064
|
+
if (hostControlEnabled && existsSync15(`${probeHome}/.switchroom/hostd/${a.name}`)) {
|
|
24064
24065
|
lines.push(` - ${homePrefix}/.switchroom/hostd/${a.name}:/run/switchroom/hostd/${a.name}`);
|
|
24065
24066
|
}
|
|
24066
24067
|
if (a.bindMounts.length > 0) {
|
|
@@ -24078,38 +24079,38 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
24078
24079
|
lines.push(` - ${homePrefix}/.switchroom/logs/${a.name}:/var/log/switchroom`);
|
|
24079
24080
|
lines.push(` - ${homePrefix}/.switchroom/agents/${a.name}:${homePrefix}/.switchroom/agents/${a.name}`);
|
|
24080
24081
|
lines.push(` - ${homePrefix}/.claude/projects/${a.name}:${homePrefix}/.claude/projects/${a.name}`);
|
|
24081
|
-
if (existsSync15(`${
|
|
24082
|
+
if (existsSync15(`${probeHome}/.switchroom/skills`)) {
|
|
24082
24083
|
lines.push(` - ${homePrefix}/.switchroom/skills:${homePrefix}/.switchroom/skills:ro`);
|
|
24083
24084
|
}
|
|
24084
|
-
if (existsSync15(`${
|
|
24085
|
+
if (existsSync15(`${probeHome}/.switchroom/mcp-launchers`)) {
|
|
24085
24086
|
lines.push(` - ${homePrefix}/.switchroom/mcp-launchers:${homePrefix}/.switchroom/mcp-launchers:ro`);
|
|
24086
24087
|
}
|
|
24087
|
-
if (existsSync15(`${
|
|
24088
|
+
if (existsSync15(`${probeHome}/.switchroom/fleet`)) {
|
|
24088
24089
|
lines.push(` - ${homePrefix}/.switchroom/fleet:${homePrefix}/.switchroom/fleet:ro`);
|
|
24089
24090
|
}
|
|
24090
|
-
if (existsSync15(`${
|
|
24091
|
+
if (existsSync15(`${probeHome}/.switchroom/credentials/${a.name}`)) {
|
|
24091
24092
|
lines.push(` - ${homePrefix}/.switchroom/credentials/${a.name}:${homePrefix}/.switchroom/credentials:ro`);
|
|
24092
24093
|
}
|
|
24093
24094
|
try {
|
|
24094
|
-
mkdirSync11(`${
|
|
24095
|
+
mkdirSync11(`${probeHome}/.switchroom/audit/${a.name}`, { recursive: true });
|
|
24095
24096
|
} catch {}
|
|
24096
24097
|
try {
|
|
24097
|
-
mkdirSync11(`${
|
|
24098
|
+
mkdirSync11(`${probeHome}/.switchroom/agents/${a.name}/schedule.d`, { recursive: true });
|
|
24098
24099
|
} catch {}
|
|
24099
24100
|
lines.push(` - ${homePrefix}/.switchroom/audit/${a.name}:${homePrefix}/.switchroom/audit/${a.name}:rw`);
|
|
24100
|
-
if (existsSync15(`${
|
|
24101
|
+
if (existsSync15(`${probeHome}/.switchroom-config`)) {
|
|
24101
24102
|
try {
|
|
24102
|
-
mkdirSync11(`${
|
|
24103
|
+
mkdirSync11(`${probeHome}/.switchroom-config/agents/${a.name}/personal-skills`, { recursive: true });
|
|
24103
24104
|
} catch {}
|
|
24104
24105
|
lines.push(` - ${homePrefix}/.switchroom-config/agents/${a.name}/personal-skills:${homePrefix}/.switchroom-config/agents/${a.name}/personal-skills:rw`);
|
|
24105
24106
|
}
|
|
24106
|
-
if (existsSync15(`${
|
|
24107
|
+
if (existsSync15(`${probeHome}/.switchroom/bin/webkite`)) {
|
|
24107
24108
|
lines.push(` - ${homePrefix}/.switchroom/bin/webkite:/usr/local/bin/webkite:ro`);
|
|
24108
24109
|
}
|
|
24109
|
-
if (existsSync15(`${
|
|
24110
|
+
if (existsSync15(`${probeHome}/.cloakbrowser`)) {
|
|
24110
24111
|
lines.push(` - ${homePrefix}/.cloakbrowser:/state/agent/home/.cloakbrowser:ro`);
|
|
24111
24112
|
}
|
|
24112
|
-
if (existsSync15(`${
|
|
24113
|
+
if (existsSync15(`${probeHome}/.switchroom/webkite/config.toml`)) {
|
|
24113
24114
|
lines.push(` - ${homePrefix}/.switchroom/webkite/config.toml:/state/agent/home/.config/webkite/config.toml:ro`);
|
|
24114
24115
|
}
|
|
24115
24116
|
if (bundledSkillsPoolDir && existsSync15(bundledSkillsPoolDir) && !bundledSkillsPoolDir.startsWith(`${hostHomeForChecks}/.switchroom/skills`)) {
|
|
@@ -28993,6 +28994,26 @@ async function getJson(url, opts) {
|
|
|
28993
28994
|
return { ok: false, reason: String(err.message ?? err) };
|
|
28994
28995
|
}
|
|
28995
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
|
+
}
|
|
28996
29017
|
async function inspectBankHealth(mcpUrl, bankId, opts) {
|
|
28997
29018
|
const base = hindsightRestBase(mcpUrl);
|
|
28998
29019
|
const bank = encodeURIComponent(bankId);
|
|
@@ -29041,16 +29062,22 @@ async function inspectBankHealth(mcpUrl, bankId, opts) {
|
|
|
29041
29062
|
pendingOperations: stats.data.pending_operations ?? 0,
|
|
29042
29063
|
newestDocumentAt,
|
|
29043
29064
|
unextractedDocuments: unextracted,
|
|
29044
|
-
mentalModels: (models.data.items ?? []).filter((m) => typeof m?.id === "string" && typeof m?.name === "string").map((m) =>
|
|
29045
|
-
|
|
29046
|
-
|
|
29047
|
-
|
|
29048
|
-
|
|
29049
|
-
|
|
29050
|
-
|
|
29051
|
-
|
|
29052
|
-
|
|
29053
|
-
|
|
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
|
+
})
|
|
29054
29081
|
};
|
|
29055
29082
|
}
|
|
29056
29083
|
async function getMentalModelDetail(mcpUrl, bankId, modelId, opts) {
|
|
@@ -29061,14 +29088,7 @@ async function getMentalModelDetail(mcpUrl, bankId, modelId, opts) {
|
|
|
29061
29088
|
if (!res.ok)
|
|
29062
29089
|
return res;
|
|
29063
29090
|
const m = res.data;
|
|
29064
|
-
const
|
|
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
|
-
}
|
|
29091
|
+
const { basedOnCounts, totalSourceFacts, derivedFromModelIds } = summarizeBasedOn(m.reflect_response);
|
|
29072
29092
|
return {
|
|
29073
29093
|
ok: true,
|
|
29074
29094
|
model: {
|
|
@@ -29080,7 +29100,8 @@ async function getMentalModelDetail(mcpUrl, bankId, modelId, opts) {
|
|
|
29080
29100
|
createdAt: m.created_at ?? null,
|
|
29081
29101
|
refreshMode: m.trigger?.mode ?? null,
|
|
29082
29102
|
basedOnCounts,
|
|
29083
|
-
totalSourceFacts
|
|
29103
|
+
totalSourceFacts,
|
|
29104
|
+
derivedFromModelIds
|
|
29084
29105
|
}
|
|
29085
29106
|
};
|
|
29086
29107
|
}
|
|
@@ -50128,6 +50149,7 @@ var init_server3 = __esm(() => {
|
|
|
50128
50149
|
// src/mcp/hostd/server.ts
|
|
50129
50150
|
var exports_server2 = {};
|
|
50130
50151
|
__export(exports_server2, {
|
|
50152
|
+
wireTimeoutForOp: () => wireTimeoutForOp,
|
|
50131
50153
|
runHostdMcpServer: () => runHostdMcpServer,
|
|
50132
50154
|
resolveAuditLogPath: () => resolveAuditLogPath,
|
|
50133
50155
|
getLastUpdateApplyStatus: () => getLastUpdateApplyStatus,
|
|
@@ -50142,6 +50164,9 @@ function selfSocketPath() {
|
|
|
50142
50164
|
function makeRequestId(prefix) {
|
|
50143
50165
|
return `${prefix}-${Date.now()}-${randomBytes15(4).toString("hex")}`;
|
|
50144
50166
|
}
|
|
50167
|
+
function wireTimeoutForOp(op) {
|
|
50168
|
+
return WIRE_TIMEOUT_MS_BY_OP[op] ?? DEFAULT_WIRE_TIMEOUT_MS;
|
|
50169
|
+
}
|
|
50145
50170
|
async function dispatchTool2(name, args) {
|
|
50146
50171
|
if (name === "get_status") {
|
|
50147
50172
|
return getLastUpdateApplyStatus();
|
|
@@ -50278,7 +50303,7 @@ async function dispatchTool2(name, args) {
|
|
|
50278
50303
|
}
|
|
50279
50304
|
let resp;
|
|
50280
50305
|
try {
|
|
50281
|
-
resp = await hostdRequest({ socketPath: sockPath, timeoutMs:
|
|
50306
|
+
resp = await hostdRequest({ socketPath: sockPath, timeoutMs: wireTimeoutForOp(req.op) }, req);
|
|
50282
50307
|
} catch (err2) {
|
|
50283
50308
|
return errorText2(`hostd wire error (request_id=${req.request_id}): ` + `${err2.message}`);
|
|
50284
50309
|
}
|
|
@@ -50355,7 +50380,7 @@ async function runHostdMcpServer() {
|
|
|
50355
50380
|
const transport = new StdioServerTransport;
|
|
50356
50381
|
await server.connect(transport);
|
|
50357
50382
|
}
|
|
50358
|
-
var SELF_AGENT, TOOLS2;
|
|
50383
|
+
var SELF_AGENT, DEFAULT_WIRE_TIMEOUT_MS = 1e4, WIRE_TIMEOUT_MS_BY_OP, TOOLS2;
|
|
50359
50384
|
var init_server4 = __esm(() => {
|
|
50360
50385
|
init_server2();
|
|
50361
50386
|
init_stdio2();
|
|
@@ -50363,6 +50388,9 @@ var init_server4 = __esm(() => {
|
|
|
50363
50388
|
init_client4();
|
|
50364
50389
|
init_audit_reader();
|
|
50365
50390
|
SELF_AGENT = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
50391
|
+
WIRE_TIMEOUT_MS_BY_OP = {
|
|
50392
|
+
config_propose_edit: 11 * 60 * 1000
|
|
50393
|
+
};
|
|
50366
50394
|
TOOLS2 = [
|
|
50367
50395
|
{
|
|
50368
50396
|
name: "agent_restart",
|
|
@@ -50493,7 +50521,7 @@ var init_server4 = __esm(() => {
|
|
|
50493
50521
|
},
|
|
50494
50522
|
{
|
|
50495
50523
|
name: "config_propose_edit",
|
|
50496
|
-
description: "Propose a unified-diff patch against /state/config/switchroom.yaml. " + "The host validates the patch (applies cleanly + post-patch yaml parses " + "against the config schema + no secret leak), raises a Telegram approval " + "card in the OPERATOR's primary chat (NOT yours \u2014 the requesting agent's " + "chat is not the approval surface), and on Allow applies it in place and " + "reconciles (rolling back if reconcile fails); returns " + `result:"completed" on success. Use this \u2014 behind the operator's tap \u2014 ` + "to amend config instead of asking the operator to hand-edit the yaml. " + "Admin agents may propose ANY field; non-admin agents are confined to " + "their own agents.<self>.tools.allow. Requires " + "hostd.config_edit_enabled=true (operator opt-in; default off) \u2014 returns " + "E_CONFIG_EDIT_DISABLED otherwise. An applied edit is not live in the " + "running agent until it restarts (the approval card names which agents " + "to bounce).",
|
|
50524
|
+
description: "Propose a unified-diff patch against /state/config/switchroom.yaml. " + "The host validates the patch (applies cleanly + post-patch yaml parses " + "against the config schema + no secret leak), raises a Telegram approval " + "card in the OPERATOR's primary chat (NOT yours \u2014 the requesting agent's " + "chat is not the approval surface), and on Allow applies it in place and " + "reconciles (rolling back if reconcile fails); returns " + `result:"completed" on success. Use this \u2014 behind the operator's tap \u2014 ` + "to amend config instead of asking the operator to hand-edit the yaml. " + "Admin agents may propose ANY field; non-admin agents are confined to " + "their own agents.<self>.tools.allow. Requires " + "hostd.config_edit_enabled=true (operator opt-in; default off) \u2014 returns " + "E_CONFIG_EDIT_DISABLED otherwise. An applied edit is not live in the " + "running agent until it restarts (the approval card names which agents " + "to bounce). This call BLOCKS until the operator taps Allow/Deny (or the " + "~10-min approval window expires) \u2014 that is expected, NOT a hang. Issue " + "it ONCE and wait for the single result; do NOT re-fire while waiting " + "(a duplicate identical proposal is collapsed onto the pending one, but " + "re-firing only adds confusion). On a genuine failure you get a " + "structured error (E_*); surface that honestly rather than falling back " + "to asking the operator to hand-edit the yaml.",
|
|
50497
50525
|
inputSchema: {
|
|
50498
50526
|
type: "object",
|
|
50499
50527
|
required: ["unified_diff", "reason", "target_path"],
|
|
@@ -50546,8 +50574,8 @@ var {
|
|
|
50546
50574
|
} = import__.default;
|
|
50547
50575
|
|
|
50548
50576
|
// src/build-info.ts
|
|
50549
|
-
var VERSION = "0.15.
|
|
50550
|
-
var COMMIT_SHA = "
|
|
50577
|
+
var VERSION = "0.15.33";
|
|
50578
|
+
var COMMIT_SHA = "9681f212";
|
|
50551
50579
|
|
|
50552
50580
|
// src/cli/agent.ts
|
|
50553
50581
|
init_source();
|
|
@@ -54637,6 +54665,7 @@ async function writeComposeFile(opts) {
|
|
|
54637
54665
|
buildMode: opts.buildMode ?? "pull",
|
|
54638
54666
|
buildContext: opts.buildContext,
|
|
54639
54667
|
homeDir: process.env.SWITCHROOM_HOST_HOME || homedir6(),
|
|
54668
|
+
probeHomeDir: homedir6(),
|
|
54640
54669
|
switchroomConfigPath: resolvedConfigPath,
|
|
54641
54670
|
operatorUid
|
|
54642
54671
|
});
|
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
|
|
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>`
|
|
792
|
+
? `<div class="mm-why">answers: “${escapeHtml(mm.sourceQuery)}”</div>`
|
|
689
793
|
: '';
|
|
690
|
-
return `<div
|
|
691
|
-
<div
|
|
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;
|
|
@@ -15087,7 +15087,7 @@ import {
|
|
|
15087
15087
|
closeSync as closeSync2
|
|
15088
15088
|
} from "node:fs";
|
|
15089
15089
|
import { join as join3, dirname as dirname4, resolve as resolve5 } from "node:path";
|
|
15090
|
-
import { randomUUID as randomUUID2, randomBytes } from "node:crypto";
|
|
15090
|
+
import { createHash as createHash5, randomUUID as randomUUID2, randomBytes } from "node:crypto";
|
|
15091
15091
|
|
|
15092
15092
|
// src/host-control/protocol.ts
|
|
15093
15093
|
var MAX_FRAME_BYTES = 64 * 1024;
|
|
@@ -21729,6 +21729,23 @@ class HostdServer {
|
|
|
21729
21729
|
]).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
|
|
21730
21730
|
}
|
|
21731
21731
|
const callerName = caller.kind === "agent" ? caller.name : "operator";
|
|
21732
|
+
const dedupeKey = `${callerName}:${createHash5("sha256").update(req.args.unified_diff).digest("hex")}`;
|
|
21733
|
+
const pending = this.inflightConfigProposals.get(dedupeKey);
|
|
21734
|
+
if (pending) {
|
|
21735
|
+
process.stderr.write(`hostd: config_propose_edit — collapsed identical in-flight proposal from ${callerName} (dedupe)
|
|
21736
|
+
`);
|
|
21737
|
+
return await pending;
|
|
21738
|
+
}
|
|
21739
|
+
const run = this.runConfigProposeApprovalAndApply(req, caller, callerName, configPath, verdict.postApplyContent, started);
|
|
21740
|
+
this.inflightConfigProposals.set(dedupeKey, run);
|
|
21741
|
+
try {
|
|
21742
|
+
return await run;
|
|
21743
|
+
} finally {
|
|
21744
|
+
this.inflightConfigProposals.delete(dedupeKey);
|
|
21745
|
+
}
|
|
21746
|
+
}
|
|
21747
|
+
inflightConfigProposals = new Map;
|
|
21748
|
+
async runConfigProposeApprovalAndApply(req, caller, callerName, configPath, postApply, started) {
|
|
21732
21749
|
const approvalId = (this.opts.generateApprovalId ?? defaultApprovalId)();
|
|
21733
21750
|
const approval = await this.opts.approvalGateway.requestApproval({
|
|
21734
21751
|
requestId: approvalId,
|
|
@@ -21765,7 +21782,6 @@ class HostdServer {
|
|
|
21765
21782
|
});
|
|
21766
21783
|
return this.reconcileFailedRolledBack(`snapshot read failed: ${e.message}`, req, caller, started);
|
|
21767
21784
|
}
|
|
21768
|
-
const postApply = verdict.postApplyContent;
|
|
21769
21785
|
try {
|
|
21770
21786
|
writeFileInPlacePreservingInode(configPath, postApply);
|
|
21771
21787
|
} catch (e) {
|
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.33";
|
|
54464
|
+
var COMMIT_SHA = "9681f212";
|
|
54465
|
+
var COMMIT_DATE = "2026-06-15T12:48:47Z";
|
|
54466
|
+
var LATEST_PR = 2386;
|
|
54467
54467
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
54468
54468
|
|
|
54469
54469
|
// gateway/boot-version.ts
|