switchroom 0.15.30 → 0.15.32
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
|
@@ -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`)) {
|
|
@@ -29047,10 +29048,43 @@ async function inspectBankHealth(mcpUrl, bankId, opts) {
|
|
|
29047
29048
|
lastRefreshedAt: m.last_refreshed_at ?? null,
|
|
29048
29049
|
createdAt: m.created_at ?? null,
|
|
29049
29050
|
contentLength: (m.content ?? "").length,
|
|
29050
|
-
contentHead: (m.content ?? "").slice(0, 200)
|
|
29051
|
+
contentHead: (m.content ?? "").slice(0, 200),
|
|
29052
|
+
sourceQuery: m.source_query ?? "",
|
|
29053
|
+
refreshMode: m.trigger?.mode ?? null
|
|
29051
29054
|
}))
|
|
29052
29055
|
};
|
|
29053
29056
|
}
|
|
29057
|
+
async function getMentalModelDetail(mcpUrl, bankId, modelId, opts) {
|
|
29058
|
+
const base = hindsightRestBase(mcpUrl);
|
|
29059
|
+
const bank = encodeURIComponent(bankId);
|
|
29060
|
+
const id = encodeURIComponent(modelId);
|
|
29061
|
+
const res = await getJson(`${base}/v1/default/banks/${bank}/mental-models/${id}`, opts);
|
|
29062
|
+
if (!res.ok)
|
|
29063
|
+
return res;
|
|
29064
|
+
const m = res.data;
|
|
29065
|
+
const basedOn = m.reflect_response?.based_on ?? {};
|
|
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
|
+
}
|
|
29073
|
+
return {
|
|
29074
|
+
ok: true,
|
|
29075
|
+
model: {
|
|
29076
|
+
id: m.id ?? modelId,
|
|
29077
|
+
name: m.name ?? modelId,
|
|
29078
|
+
sourceQuery: m.source_query ?? "",
|
|
29079
|
+
content: m.content ?? "",
|
|
29080
|
+
lastRefreshedAt: m.last_refreshed_at ?? null,
|
|
29081
|
+
createdAt: m.created_at ?? null,
|
|
29082
|
+
refreshMode: m.trigger?.mode ?? null,
|
|
29083
|
+
basedOnCounts,
|
|
29084
|
+
totalSourceFacts
|
|
29085
|
+
}
|
|
29086
|
+
};
|
|
29087
|
+
}
|
|
29054
29088
|
function ageDays(iso, now = new Date) {
|
|
29055
29089
|
if (!iso)
|
|
29056
29090
|
return null;
|
|
@@ -50095,6 +50129,7 @@ var init_server3 = __esm(() => {
|
|
|
50095
50129
|
// src/mcp/hostd/server.ts
|
|
50096
50130
|
var exports_server2 = {};
|
|
50097
50131
|
__export(exports_server2, {
|
|
50132
|
+
wireTimeoutForOp: () => wireTimeoutForOp,
|
|
50098
50133
|
runHostdMcpServer: () => runHostdMcpServer,
|
|
50099
50134
|
resolveAuditLogPath: () => resolveAuditLogPath,
|
|
50100
50135
|
getLastUpdateApplyStatus: () => getLastUpdateApplyStatus,
|
|
@@ -50109,6 +50144,9 @@ function selfSocketPath() {
|
|
|
50109
50144
|
function makeRequestId(prefix) {
|
|
50110
50145
|
return `${prefix}-${Date.now()}-${randomBytes15(4).toString("hex")}`;
|
|
50111
50146
|
}
|
|
50147
|
+
function wireTimeoutForOp(op) {
|
|
50148
|
+
return WIRE_TIMEOUT_MS_BY_OP[op] ?? DEFAULT_WIRE_TIMEOUT_MS;
|
|
50149
|
+
}
|
|
50112
50150
|
async function dispatchTool2(name, args) {
|
|
50113
50151
|
if (name === "get_status") {
|
|
50114
50152
|
return getLastUpdateApplyStatus();
|
|
@@ -50245,7 +50283,7 @@ async function dispatchTool2(name, args) {
|
|
|
50245
50283
|
}
|
|
50246
50284
|
let resp;
|
|
50247
50285
|
try {
|
|
50248
|
-
resp = await hostdRequest({ socketPath: sockPath, timeoutMs:
|
|
50286
|
+
resp = await hostdRequest({ socketPath: sockPath, timeoutMs: wireTimeoutForOp(req.op) }, req);
|
|
50249
50287
|
} catch (err2) {
|
|
50250
50288
|
return errorText2(`hostd wire error (request_id=${req.request_id}): ` + `${err2.message}`);
|
|
50251
50289
|
}
|
|
@@ -50322,7 +50360,7 @@ async function runHostdMcpServer() {
|
|
|
50322
50360
|
const transport = new StdioServerTransport;
|
|
50323
50361
|
await server.connect(transport);
|
|
50324
50362
|
}
|
|
50325
|
-
var SELF_AGENT, TOOLS2;
|
|
50363
|
+
var SELF_AGENT, DEFAULT_WIRE_TIMEOUT_MS = 1e4, WIRE_TIMEOUT_MS_BY_OP, TOOLS2;
|
|
50326
50364
|
var init_server4 = __esm(() => {
|
|
50327
50365
|
init_server2();
|
|
50328
50366
|
init_stdio2();
|
|
@@ -50330,6 +50368,9 @@ var init_server4 = __esm(() => {
|
|
|
50330
50368
|
init_client4();
|
|
50331
50369
|
init_audit_reader();
|
|
50332
50370
|
SELF_AGENT = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
50371
|
+
WIRE_TIMEOUT_MS_BY_OP = {
|
|
50372
|
+
config_propose_edit: 11 * 60 * 1000
|
|
50373
|
+
};
|
|
50333
50374
|
TOOLS2 = [
|
|
50334
50375
|
{
|
|
50335
50376
|
name: "agent_restart",
|
|
@@ -50460,7 +50501,7 @@ var init_server4 = __esm(() => {
|
|
|
50460
50501
|
},
|
|
50461
50502
|
{
|
|
50462
50503
|
name: "config_propose_edit",
|
|
50463
|
-
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).",
|
|
50504
|
+
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.",
|
|
50464
50505
|
inputSchema: {
|
|
50465
50506
|
type: "object",
|
|
50466
50507
|
required: ["unified_diff", "reason", "target_path"],
|
|
@@ -50513,8 +50554,8 @@ var {
|
|
|
50513
50554
|
} = import__.default;
|
|
50514
50555
|
|
|
50515
50556
|
// src/build-info.ts
|
|
50516
|
-
var VERSION = "0.15.
|
|
50517
|
-
var COMMIT_SHA = "
|
|
50557
|
+
var VERSION = "0.15.32";
|
|
50558
|
+
var COMMIT_SHA = "61984cef";
|
|
50518
50559
|
|
|
50519
50560
|
// src/cli/agent.ts
|
|
50520
50561
|
init_source();
|
|
@@ -54604,6 +54645,7 @@ async function writeComposeFile(opts) {
|
|
|
54604
54645
|
buildMode: opts.buildMode ?? "pull",
|
|
54605
54646
|
buildContext: opts.buildContext,
|
|
54606
54647
|
homeDir: process.env.SWITCHROOM_HOST_HOME || homedir6(),
|
|
54648
|
+
probeHomeDir: homedir6(),
|
|
54607
54649
|
switchroomConfigPath: resolvedConfigPath,
|
|
54608
54650
|
operatorUid
|
|
54609
54651
|
});
|
|
@@ -75087,6 +75129,24 @@ async function handleMemoryBuildProfile(config, body, deps) {
|
|
|
75087
75129
|
return { ok: false, error: String(err.message ?? err) };
|
|
75088
75130
|
}
|
|
75089
75131
|
}
|
|
75132
|
+
async function handleGetMentalModel(config, bank, modelId, deps) {
|
|
75133
|
+
if (!bank)
|
|
75134
|
+
return { ok: false, error: "Query must include `bank`." };
|
|
75135
|
+
if (!modelId)
|
|
75136
|
+
return { ok: false, error: "Query must include `id`." };
|
|
75137
|
+
if (!isKnownBank(config, bank))
|
|
75138
|
+
return { ok: false, error: `Unknown bank: ${bank}` };
|
|
75139
|
+
const url = resolveMemoryUrl(config);
|
|
75140
|
+
const detail = deps?.detail ?? getMentalModelDetail;
|
|
75141
|
+
try {
|
|
75142
|
+
const res = await detail(url, bank, modelId, { fetchImpl: deps?.fetchImpl });
|
|
75143
|
+
if (!res.ok)
|
|
75144
|
+
return { ok: false, error: res.reason };
|
|
75145
|
+
return { ok: true, model: res.model };
|
|
75146
|
+
} catch (err) {
|
|
75147
|
+
return { ok: false, error: String(err.message ?? err) };
|
|
75148
|
+
}
|
|
75149
|
+
}
|
|
75090
75150
|
var ATTENTION_SEVERITY_RANK = {
|
|
75091
75151
|
critical: 0,
|
|
75092
75152
|
warn: 1,
|
|
@@ -75973,6 +76033,9 @@ function parseRoute(pathname, method) {
|
|
|
75973
76033
|
if (method === "POST" && pathname === "/api/memory/build-profile") {
|
|
75974
76034
|
return { handler: "memoryBuildProfile", params: {} };
|
|
75975
76035
|
}
|
|
76036
|
+
if (method === "GET" && pathname === "/api/memory/model") {
|
|
76037
|
+
return { handler: "getMentalModel", params: {} };
|
|
76038
|
+
}
|
|
75976
76039
|
if (method === "GET" && pathname === "/api/google-accounts") {
|
|
75977
76040
|
return { handler: "getGoogleAccounts", params: {} };
|
|
75978
76041
|
}
|
|
@@ -76272,6 +76335,14 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
|
|
|
76272
76335
|
return jsonResponse(result, result.ok ? 200 : 400);
|
|
76273
76336
|
})();
|
|
76274
76337
|
}
|
|
76338
|
+
case "getMentalModel": {
|
|
76339
|
+
return (async () => {
|
|
76340
|
+
const bank = url.searchParams.get("bank") ?? "";
|
|
76341
|
+
const id = url.searchParams.get("id") ?? "";
|
|
76342
|
+
const result = await handleGetMentalModel(freshConfig(), bank, id);
|
|
76343
|
+
return jsonResponse(result, result.ok ? 200 : 400);
|
|
76344
|
+
})();
|
|
76345
|
+
}
|
|
76275
76346
|
case "getAgentAccounts": {
|
|
76276
76347
|
const agentName = route.params.name;
|
|
76277
76348
|
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
|
|
@@ -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.32";
|
|
54464
|
+
var COMMIT_SHA = "61984cef";
|
|
54465
|
+
var COMMIT_DATE = "2026-06-15T12:32:06Z";
|
|
54466
|
+
var LATEST_PR = 2384;
|
|
54467
54467
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
54468
54468
|
|
|
54469
54469
|
// gateway/boot-version.ts
|