switchroom 0.15.0 → 0.15.2

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.
@@ -13912,6 +13912,7 @@ var init_schema = __esm(() => {
13912
13912
  dangerous_mode: exports_external.boolean().optional().describe("If true, include --dangerously-skip-permissions in start.sh"),
13913
13913
  network_isolation: NetworkIsolationSchema,
13914
13914
  admin: exports_external.boolean().optional().describe("If true, the agent's Telegram gateway intercepts admin slash commands " + "(/agents, /logs, /restart, /delete, /update, /auth, /reconcile, etc.) " + "locally before forwarding to Claude. Commands are handled silently \u2014 " + "Claude never sees them. Requires the agent to use the switchroom-telegram " + "plugin. When false or absent, all messages pass through to Claude unchanged."),
13915
+ root: exports_external.boolean().optional().describe("If true, this is a ROOT-tier debugging agent: a root-privileged " + "container (runs as uid 0, mounts /var/run/docker.sock, the whole " + "~/.switchroom tree, and the host root filesystem at /host) so you " + "can DM it to debug the whole fleet \u2014 read any agent's logs, " + "docker exec into peers, edit host files \u2014 instead of SSHing into " + "the host as root. Implies admin: true (all admin slash commands). " + "Standing root power, audited via the agent's own session transcript " + "and shell history; there is no per-action approval tap. Per-agent " + "only (never set at defaults/profile layers). Grant to exactly one " + "trusted operator-private agent \u2014 it ingests other agents' output, " + "which is attacker-influenced text. See docs/root-agent.md."),
13915
13916
  settings_raw: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Escape hatch: raw object deep-merged into the generated " + "settings.json as the final step. Use for Claude Code settings " + "keys switchroom doesn't wrap directly (e.g. effort, apiKeyHelper). " + "Power-user-only \u2014 prefer the typed fields when they exist."),
13916
13917
  claude_md_raw: exports_external.string().optional().describe("Escape hatch: markdown text appended verbatim to CLAUDE.md on " + "initial scaffold. Not re-applied on reconcile (CLAUDE.md is " + "user-protected). Use for one-off persona tuning that isn't " + "worth a template."),
13917
13918
  cli_args: exports_external.array(exports_external.string()).optional().describe("Escape hatch: extra arguments appended to the `exec claude` " + "invocation in start.sh. Use for Claude Code CLI flags switchroom " + "doesn't expose directly (e.g. --effort high, " + "--exclude-dynamic-system-prompt-sections)."),
@@ -23247,7 +23248,8 @@ function describeAgents(config) {
23247
23248
  resources,
23248
23249
  strippedCaps,
23249
23250
  networkIsolation: resolved.network_isolation === "strict" ? "strict" : "host",
23250
- admin: agent.admin === true,
23251
+ admin: agent.admin === true || agent.root === true,
23252
+ root: agent.root === true,
23251
23253
  bindMounts: agent.bind_mounts ? [...agent.bind_mounts] : [],
23252
23254
  userEnv: { ...resolved.env ?? {} },
23253
23255
  timezone: resolveTimezone(config, resolved)
@@ -23559,7 +23561,11 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23559
23561
  lines.push(` tty: true`);
23560
23562
  lines.push(` stdin_open: true`);
23561
23563
  lines.push(` stop_grace_period: 45s`);
23562
- lines.push(` user: "${a.uid}:${a.uid}"`);
23564
+ if (a.root) {
23565
+ lines.push(` user: "0:0"`);
23566
+ } else {
23567
+ lines.push(` user: "${a.uid}:${a.uid}"`);
23568
+ }
23563
23569
  lines.push(` mem_limit: ${a.resources.memLimit}`);
23564
23570
  if (a.resources.memReservation !== undefined) {
23565
23571
  lines.push(` mem_reservation: ${a.resources.memReservation}`);
@@ -23568,11 +23574,13 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23568
23574
  lines.push(` pids_limit: ${a.resources.pidsLimit}`);
23569
23575
  }
23570
23576
  lines.push(` cpus: ${a.resources.cpus.toFixed(1)}`);
23571
- lines.push(` security_opt:`);
23572
- lines.push(` - "no-new-privileges:true"`);
23573
- lines.push(` cap_drop:`);
23574
- lines.push(` - "ALL"`);
23575
- lines.push(` read_only: true`);
23577
+ if (!a.root) {
23578
+ lines.push(` security_opt:`);
23579
+ lines.push(` - "no-new-privileges:true"`);
23580
+ lines.push(` cap_drop:`);
23581
+ lines.push(` - "ALL"`);
23582
+ lines.push(` read_only: true`);
23583
+ }
23576
23584
  lines.push(` tmpfs:`);
23577
23585
  lines.push(` - /tmp:size=1g,mode=1777`);
23578
23586
  lines.push(` depends_on:`);
@@ -23615,6 +23623,9 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23615
23623
  if (switchroomConfigPath) {
23616
23624
  env2.SWITCHROOM_CONFIG = "/state/config/switchroom.yaml";
23617
23625
  }
23626
+ if (a.root === true) {
23627
+ env2.SWITCHROOM_AGENT_ROOT = "true";
23628
+ }
23618
23629
  if (a.admin === true) {
23619
23630
  env2.SWITCHROOM_AGENT_ADMIN = "true";
23620
23631
  }
@@ -23632,6 +23643,11 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23632
23643
  lines.push(` - broker-${a.name}-sock:/run/switchroom/broker`);
23633
23644
  lines.push(` - kernel-${a.name}-sock:/run/switchroom/kernel`);
23634
23645
  lines.push(` - auth-broker-${a.name}-sock:/run/switchroom/auth-broker`);
23646
+ if (a.root === true) {
23647
+ lines.push(` - /var/run/docker.sock:/var/run/docker.sock:rw`);
23648
+ lines.push(` - ${homePrefix}/.switchroom:/host-home/.switchroom:rw`);
23649
+ lines.push(` - /:/host:rw`);
23650
+ }
23635
23651
  if (a.admin === true) {
23636
23652
  if (existsSync14(`${hostHomeForChecks}/.switchroom/vault-audit.log`)) {
23637
23653
  lines.push(` - ${homePrefix}/.switchroom/vault-audit.log:/state/agent/home/.switchroom/vault-audit.log:ro`);
@@ -25561,7 +25577,7 @@ function decodeResponse2(line) {
25561
25577
  }
25562
25578
  return ResponseSchema2.parse(parsed);
25563
25579
  }
25564
- var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
25580
+ var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, ClaimNotificationRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, ClaimNotificationDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
25565
25581
  var init_protocol2 = __esm(() => {
25566
25582
  init_zod();
25567
25583
  MAX_FRAME_BYTES2 = 64 * 1024;
@@ -25677,6 +25693,13 @@ var init_protocol2 = __esm(() => {
25677
25693
  accounts: exports_external.array(exports_external.string().min(1)).min(1).max(32),
25678
25694
  timeoutMs: exports_external.number().int().positive().max(60000).optional()
25679
25695
  });
25696
+ ClaimNotificationRequestSchema = exports_external.object({
25697
+ v: exports_external.literal(PROTOCOL_VERSION),
25698
+ op: exports_external.literal("claim-notification"),
25699
+ id: exports_external.string().min(1),
25700
+ key: exports_external.string().min(1).max(512),
25701
+ windowMs: exports_external.number().int().positive().max(86400000)
25702
+ });
25680
25703
  RequestSchema2 = exports_external.discriminatedUnion("op", [
25681
25704
  GetCredentialsRequestSchema,
25682
25705
  ListStateRequestSchema,
@@ -25688,7 +25711,8 @@ var init_protocol2 = __esm(() => {
25688
25711
  SetOverrideRequestSchema,
25689
25712
  ListGoogleAccountsRequestSchema,
25690
25713
  ListMicrosoftAccountsRequestSchema,
25691
- ProbeQuotaRequestSchema
25714
+ ProbeQuotaRequestSchema,
25715
+ ClaimNotificationRequestSchema
25692
25716
  ]);
25693
25717
  GetCredentialsDataSchema = exports_external.object({
25694
25718
  account: exports_external.string(),
@@ -25744,6 +25768,9 @@ var init_protocol2 = __esm(() => {
25744
25768
  agent: exports_external.string(),
25745
25769
  account: exports_external.string().nullable()
25746
25770
  });
25771
+ ClaimNotificationDataSchema = exports_external.object({
25772
+ granted: exports_external.boolean()
25773
+ });
25747
25774
  GoogleAccountStateSchema = exports_external.object({
25748
25775
  account: exports_external.string(),
25749
25776
  expiresAt: exports_external.number(),
@@ -25925,6 +25952,16 @@ class AuthBrokerClient {
25925
25952
  const data = await this.send(req);
25926
25953
  return data;
25927
25954
  }
25955
+ async claimNotification(key, windowMs) {
25956
+ const data = await this.send({
25957
+ v: PROTOCOL_VERSION,
25958
+ id: randomUUID(),
25959
+ op: "claim-notification",
25960
+ key,
25961
+ windowMs
25962
+ });
25963
+ return data;
25964
+ }
25928
25965
  async refreshAccount(account) {
25929
25966
  const data = await this.send({
25930
25967
  v: PROTOCOL_VERSION,
@@ -28683,6 +28720,118 @@ var init_protocol3 = __esm(() => {
28683
28720
  ResponseSchema3 = exports_external.object(ResponseEnvelope);
28684
28721
  });
28685
28722
 
28723
+ // src/memory/bank-health.ts
28724
+ function hindsightRestBase(mcpUrl) {
28725
+ return mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
28726
+ }
28727
+ async function getJson(url, opts) {
28728
+ const fetchImpl = opts?.fetchImpl ?? fetch;
28729
+ const timeoutMs = opts?.timeoutMs ?? 15000;
28730
+ const controller = new AbortController;
28731
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
28732
+ try {
28733
+ const resp = await fetchImpl(url, { signal: controller.signal });
28734
+ clearTimeout(timeout);
28735
+ if (!resp.ok)
28736
+ return { ok: false, reason: `HTTP ${resp.status}` };
28737
+ return { ok: true, data: await resp.json() };
28738
+ } catch (err) {
28739
+ clearTimeout(timeout);
28740
+ if (err.name === "AbortError")
28741
+ return { ok: false, reason: "Timeout" };
28742
+ return { ok: false, reason: String(err.message ?? err) };
28743
+ }
28744
+ }
28745
+ async function inspectBankHealth(mcpUrl, bankId, opts) {
28746
+ const base = hindsightRestBase(mcpUrl);
28747
+ const bank = encodeURIComponent(bankId);
28748
+ const empty = {
28749
+ bankId,
28750
+ ok: false,
28751
+ totalDocuments: 0,
28752
+ totalFacts: 0,
28753
+ pendingOperations: 0,
28754
+ newestDocumentAt: null,
28755
+ unextractedDocuments: [],
28756
+ mentalModels: []
28757
+ };
28758
+ const stats = await getJson(`${base}/v1/default/banks/${bank}/stats`, opts);
28759
+ if (!stats.ok)
28760
+ return { ...empty, reason: stats.reason };
28761
+ const docs = await getJson(`${base}/v1/default/banks/${bank}/documents?limit=${DOCUMENTS_PAGE_LIMIT}`, opts);
28762
+ if (!docs.ok)
28763
+ return { ...empty, reason: docs.reason };
28764
+ const models = await getJson(`${base}/v1/default/banks/${bank}/mental-models`, opts);
28765
+ if (!models.ok)
28766
+ return { ...empty, reason: models.reason };
28767
+ const docItems = docs.data.items ?? [];
28768
+ let newestDocumentAt = null;
28769
+ const unextracted = [];
28770
+ for (const d of docItems) {
28771
+ const createdAt = d.created_at ?? "";
28772
+ if (createdAt && (!newestDocumentAt || createdAt > newestDocumentAt)) {
28773
+ newestDocumentAt = createdAt;
28774
+ }
28775
+ if ((d.memory_unit_count ?? 0) === 0 && d.id) {
28776
+ unextracted.push({
28777
+ id: d.id,
28778
+ createdAt,
28779
+ textLength: d.text_length ?? 0,
28780
+ memoryUnitCount: 0
28781
+ });
28782
+ }
28783
+ }
28784
+ unextracted.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
28785
+ return {
28786
+ bankId,
28787
+ ok: true,
28788
+ totalDocuments: stats.data.total_documents ?? docItems.length,
28789
+ totalFacts: stats.data.total_nodes ?? 0,
28790
+ pendingOperations: stats.data.pending_operations ?? 0,
28791
+ newestDocumentAt,
28792
+ unextractedDocuments: unextracted,
28793
+ mentalModels: (models.data.items ?? []).filter((m) => typeof m?.id === "string" && typeof m?.name === "string").map((m) => ({
28794
+ id: m.id,
28795
+ name: m.name,
28796
+ lastRefreshedAt: m.last_refreshed_at ?? null,
28797
+ createdAt: m.created_at ?? null,
28798
+ contentLength: (m.content ?? "").length,
28799
+ contentHead: (m.content ?? "").slice(0, 200)
28800
+ }))
28801
+ };
28802
+ }
28803
+ function ageDays(iso, now = new Date) {
28804
+ if (!iso)
28805
+ return null;
28806
+ const t = Date.parse(iso);
28807
+ if (Number.isNaN(t))
28808
+ return null;
28809
+ return Math.max(0, (now.getTime() - t) / 86400000);
28810
+ }
28811
+ function corruptedMentalModels(models) {
28812
+ const failurePhrase = /out of (extra )?usage|hit your (usage |session )?limit|resets \d|quota exceeded|rate.?limit/i;
28813
+ return models.filter((m) => {
28814
+ if (m.contentLength === 0)
28815
+ return m.lastRefreshedAt !== null;
28816
+ return m.contentLength < 300 && failurePhrase.test(m.contentHead);
28817
+ });
28818
+ }
28819
+ function staleMentalModels(models, staleDays = 7, now = new Date) {
28820
+ return models.filter((m) => {
28821
+ const age = ageDays(m.lastRefreshedAt ?? m.createdAt, now);
28822
+ return age !== null && age > staleDays;
28823
+ });
28824
+ }
28825
+ function recentUnextracted(docs, withinDays = 30, now = new Date, minTextLength = 1000) {
28826
+ return docs.filter((d) => {
28827
+ if (d.textLength < minTextLength)
28828
+ return false;
28829
+ const age = ageDays(d.createdAt, now);
28830
+ return age !== null && age <= withinDays;
28831
+ });
28832
+ }
28833
+ var DOCUMENTS_PAGE_LIMIT = 500;
28834
+
28686
28835
  // src/host-control/audit-reader.ts
28687
28836
  import { homedir as homedir20 } from "node:os";
28688
28837
  import { join as join38 } from "node:path";
@@ -31673,6 +31822,7 @@ __export(exports_doctor, {
31673
31822
  checkHindsightConsumer: () => checkHindsightConsumer,
31674
31823
  checkDepsCacheWritable: () => checkDepsCacheWritable,
31675
31824
  checkConfig: () => checkConfig,
31825
+ checkBankIngestHealth: () => checkBankIngestHealth,
31676
31826
  checkAgents: () => checkAgents,
31677
31827
  MFF_VAULT_KEY: () => MFF_VAULT_KEY
31678
31828
  });
@@ -32232,6 +32382,82 @@ function probeAuthBrokerSocket(consumerName) {
32232
32382
  return "unreachable";
32233
32383
  return "missing";
32234
32384
  }
32385
+ async function checkBankIngestHealth(config, url, opts) {
32386
+ const results = [];
32387
+ const now = opts?.now ?? new Date;
32388
+ const banks = new Map;
32389
+ for (const [agentName, agentConfig] of Object.entries(config.agents)) {
32390
+ const bankId = agentConfig.memory?.collection ?? agentName;
32391
+ banks.set(bankId, [...banks.get(bankId) ?? [], agentName]);
32392
+ }
32393
+ const inspected = await Promise.all([...banks].map(async ([bankId, agents]) => [bankId, agents, await inspectBankHealth(url, bankId, { fetchImpl: opts?.fetchImpl })]));
32394
+ for (const [bankId, agents, h] of inspected) {
32395
+ const label = `bank ${bankId}` + (agents[0] !== bankId ? ` (${agents.join(", ")})` : "");
32396
+ if (!h.ok) {
32397
+ results.push({
32398
+ name: label,
32399
+ status: "warn",
32400
+ detail: `inspection failed: ${h.reason}`
32401
+ });
32402
+ continue;
32403
+ }
32404
+ if (h.totalDocuments === 0) {
32405
+ results.push({
32406
+ name: label,
32407
+ status: "warn",
32408
+ detail: "bank is empty (no documents retained yet)"
32409
+ });
32410
+ continue;
32411
+ }
32412
+ const gaps = recentUnextracted(h.unextractedDocuments, 30, now);
32413
+ const stale = staleMentalModels(h.mentalModels, 7, now);
32414
+ const corrupted = corruptedMentalModels(h.mentalModels);
32415
+ const newestAge = ageDays(h.newestDocumentAt, now);
32416
+ const summary = `${h.totalDocuments} docs \u00b7 ${h.totalFacts} facts \u00b7 ` + `newest ${h.newestDocumentAt?.slice(0, 10) ?? "?"} \u00b7 ` + `${h.mentalModels.length} mental models`;
32417
+ if (corrupted.length > 0) {
32418
+ results.push({
32419
+ name: label,
32420
+ status: "fail",
32421
+ detail: `${corrupted.length} mental model(s) hold a persisted LLM-failure message instead of real content (${corrupted.map((m) => m.name).join(", ")}) \u2014 that garbage is injected into every agent turn`,
32422
+ fix: "A refresh ran while the LLM was quota-walled and the error string was stored as content. " + "Once quota recovers: POST /v1/default/banks/<bank>/mental-models/<id>/refresh and verify the content regenerated."
32423
+ });
32424
+ continue;
32425
+ }
32426
+ if (gaps.length > 0) {
32427
+ const oldest = gaps[0];
32428
+ results.push({
32429
+ name: label,
32430
+ status: "fail",
32431
+ detail: `${gaps.length} document(s) in the last 30d retained with ZERO extracted facts (oldest ${oldest.createdAt.slice(0, 10)}) \u2014 conversations from those turns are invisible to recall`,
32432
+ fix: "Extraction silently failed (check hindsight logs for quota/429/shm). Re-extract each: " + `curl -X POST '${hindsightDocReprocessUrl(url, bankId, oldest.id)}' (repeat per doc id)`
32433
+ });
32434
+ continue;
32435
+ }
32436
+ if (h.pendingOperations > 0 && newestAge !== null && newestAge > 1) {
32437
+ results.push({
32438
+ name: label,
32439
+ status: "warn",
32440
+ detail: `${h.pendingOperations} pending operation(s) with no new documents for ${Math.round(newestAge)}d \u2014 pipeline may be stuck`
32441
+ });
32442
+ continue;
32443
+ }
32444
+ if (stale.length > 0) {
32445
+ results.push({
32446
+ name: label,
32447
+ status: "warn",
32448
+ detail: `${summary} \u00b7 ${stale.length} stale (>7d): ${stale.map((m) => m.name).join(", ")}`,
32449
+ fix: "POST /v1/default/banks/<bank>/mental-models/<id>/refresh to regenerate"
32450
+ });
32451
+ continue;
32452
+ }
32453
+ results.push({ name: label, status: "ok", detail: summary });
32454
+ }
32455
+ return results;
32456
+ }
32457
+ function hindsightDocReprocessUrl(mcpUrl, bankId, docId) {
32458
+ const base = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
32459
+ return `${base}/v1/default/banks/${encodeURIComponent(bankId)}/documents/${encodeURIComponent(docId)}/reprocess`;
32460
+ }
32235
32461
  async function checkHindsight(config) {
32236
32462
  if (!isHindsightEnabled(config)) {
32237
32463
  return [];
@@ -32282,6 +32508,7 @@ async function checkHindsight(config) {
32282
32508
  }
32283
32509
  results.push(checkHindsightConsumer(config));
32284
32510
  results.push(...checkHindsightContainerHealth());
32511
+ results.push(...await checkBankIngestHealth(config, url));
32285
32512
  for (const [agentName, agentConfig] of Object.entries(config.agents)) {
32286
32513
  const bankId = agentConfig.memory?.collection ?? agentName;
32287
32514
  const hasBankMission = !!agentConfig.memory?.bank_mission;
@@ -49937,8 +50164,8 @@ var {
49937
50164
  } = import__.default;
49938
50165
 
49939
50166
  // src/build-info.ts
49940
- var VERSION = "0.15.0";
49941
- var COMMIT_SHA = "5841c1d5";
50167
+ var VERSION = "0.15.2";
50168
+ var COMMIT_SHA = "95461524";
49942
50169
 
49943
50170
  // src/cli/agent.ts
49944
50171
  init_source();
@@ -51751,7 +51978,8 @@ function buildWorkspaceContext(args) {
51751
51978
  botToken: resolvedBotToken ?? rawBotToken,
51752
51979
  forumChatId: telegramConfig.forum_chat_id,
51753
51980
  dangerousMode: agentConfig.dangerous_mode === true,
51754
- admin: agentConfig.admin === true,
51981
+ admin: agentConfig.admin === true || agentConfig.root === true,
51982
+ root: agentConfig.root === true,
51755
51983
  useSwitchroomPlugin: usesSwitchroomTelegramPlugin(agentConfig),
51756
51984
  useHotReloadStable: agentConfig.channels?.telegram?.hotReloadStable === true,
51757
51985
  telegramEnabledFlag: agentConfig.channels?.telegram?.enabled === false ? "false" : "true",
@@ -52103,7 +52331,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
52103
52331
  alwaysLoad: true
52104
52332
  }
52105
52333
  };
52106
- if (agentConfig.admin === true) {
52334
+ if (agentConfig.admin === true || agentConfig.root === true) {
52107
52335
  mcpServers["hostd"] = {
52108
52336
  command: switchroomCliPath,
52109
52337
  args: ["mcp", "hostd"],
@@ -52924,7 +53152,8 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
52924
53152
  model: agentConfig.model,
52925
53153
  schedule: agentConfig.schedule,
52926
53154
  useSwitchroomPlugin: usesSwitchroomTelegramPlugin(agentConfig),
52927
- admin: agentConfig.admin === true
53155
+ admin: agentConfig.admin === true || agentConfig.root === true,
53156
+ root: agentConfig.root === true
52928
53157
  };
52929
53158
  let rendered = renderTemplate(claudeMdSrc, claudeContext);
52930
53159
  const vaultProtocol = renderVaultProtocolFragment(claudeContext);
@@ -53153,7 +53382,7 @@ ${body}
53153
53382
  alwaysLoad: true
53154
53383
  }
53155
53384
  };
53156
- if (agentConfig.admin === true) {
53385
+ if (agentConfig.admin === true || agentConfig.root === true) {
53157
53386
  mcpServers["hostd"] = {
53158
53387
  command: switchroomCliPath,
53159
53388
  args: ["mcp", "hostd"],
@@ -56199,10 +56428,12 @@ function registerAgentCommand(program3) {
56199
56428
  const agentConfig = config.agents[name];
56200
56429
  const status = statuses[name];
56201
56430
  const sched = schedulerStates[name];
56431
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
56202
56432
  return {
56203
56433
  name,
56204
56434
  status: status?.active ?? "unknown",
56205
56435
  uptime: formatUptime2(status?.uptime ?? null),
56436
+ model: resolved.model ?? SWITCHROOM_DEFAULT_MAIN_MODEL,
56206
56437
  extends: agentConfig.extends ?? "default",
56207
56438
  topic_name: agentConfig.topic_name,
56208
56439
  topic_emoji: agentConfig.topic_emoji,
@@ -59358,15 +59589,27 @@ function formatQuotaReset(state) {
59358
59589
  const mins = Math.floor(remainingMs % 3600000 / 60000);
59359
59590
  return `${hours}h ${mins}m`;
59360
59591
  }
59592
+ function formatQuotaUtilCell(a, now = Date.now()) {
59593
+ const lq = a.last_quota;
59594
+ if (!lq)
59595
+ return "no data";
59596
+ const ageMs = Math.max(0, now - lq.capturedAt);
59597
+ const mins = Math.floor(ageMs / 60000);
59598
+ const ageStr = mins < 1 ? "just now" : mins < 60 ? `${mins}m ago` : mins < 1440 ? `${Math.floor(mins / 60)}h ago` : `${Math.floor(mins / 1440)}d ago`;
59599
+ const five = Math.round(lq.fiveHourUtilizationPct);
59600
+ const seven = Math.round(lq.sevenDayUtilizationPct);
59601
+ return `${five}%\u00b7${seven}% (${ageStr})`;
59602
+ }
59361
59603
  function printAccountsTable(state) {
59362
- console.log(source_default.bold(" ACCOUNT STATUS EXPIRES QUOTA-RESET"));
59604
+ console.log(source_default.bold(" ACCOUNT STATUS EXPIRES QUOTA 5h\u00b77d QUOTA-RESET"));
59363
59605
  for (const a of state.accounts) {
59364
59606
  const marker = a.label === state.active ? source_default.green("\u25cf") : a.exhausted ? source_default.red("!") : source_default.gray("\u2713");
59365
59607
  const status = a.label === state.active ? source_default.green("active ") : a.exhausted ? source_default.red("exhausted") : "available";
59366
59608
  const label = a.label.padEnd(32);
59367
59609
  const exp = formatExpiry2(a.expiresAt).padEnd(10);
59610
+ const util3 = formatQuotaUtilCell(a).padEnd(20);
59368
59611
  const quota = formatQuotaReset(a);
59369
- console.log(` ${marker} ${label} ${status} ${exp} ${quota}`);
59612
+ console.log(` ${marker} ${label} ${status} ${exp} ${util3} ${quota}`);
59370
59613
  }
59371
59614
  }
59372
59615
  function printAgentsTable(state) {
@@ -63714,7 +63957,7 @@ class VaultBroker {
63714
63957
  const isGrantMgmtOp = req.op === "mint_grant" || req.op === "list_grants" || req.op === "revoke_grant";
63715
63958
  let mintPassphraseAttested = false;
63716
63959
  if (isGrantMgmtOp) {
63717
- const isAdminAgent = agentName !== null && this.config?.agents?.[agentName]?.admin === true;
63960
+ const isAdminAgent = agentName !== null && (this.config?.agents?.[agentName]?.admin === true || this.config?.agents?.[agentName]?.root === true);
63718
63961
  if ((req.op === "mint_grant" || req.op === "list_grants") && req.passphrase !== undefined && req.passphrase !== "") {
63719
63962
  if (req.attest_via_posture === true) {
63720
63963
  writeAudit({
@@ -73145,6 +73388,66 @@ async function handleGetApprovals() {
73145
73388
  const sorted = [...decisions].sort((a, b) => b.granted_at - a.granted_at);
73146
73389
  return { reachable: true, decisions: sorted };
73147
73390
  }
73391
+ async function handleGetMemoryHealth(config, opts) {
73392
+ const url = config.memory?.config?.url ?? "http://127.0.0.1:18888/mcp/";
73393
+ const now = opts?.now ?? new Date;
73394
+ let reachable = false;
73395
+ try {
73396
+ reachable = (await probeHindsight(url, { fetchImpl: opts?.fetchImpl })).ok;
73397
+ } catch {
73398
+ reachable = false;
73399
+ }
73400
+ if (!reachable)
73401
+ return { reachable, url, banks: [] };
73402
+ const banks = new Map;
73403
+ for (const agentName of Object.keys(config.agents)) {
73404
+ const bank = getCollectionForAgent(agentName, config);
73405
+ banks.set(bank, [...banks.get(bank) ?? [], agentName]);
73406
+ }
73407
+ const rows = await Promise.all([...banks].map(async ([bank, agents]) => {
73408
+ const h = await inspectBankHealth(url, bank, { fetchImpl: opts?.fetchImpl });
73409
+ const gaps = recentUnextracted(h.unextractedDocuments, 30, now);
73410
+ const stale = staleMentalModels(h.mentalModels, 7, now);
73411
+ const corrupted = corruptedMentalModels(h.mentalModels);
73412
+ let status = "ok";
73413
+ let statusDetail = "facts flowing";
73414
+ if (!h.ok) {
73415
+ status = "warn";
73416
+ statusDetail = `inspection failed: ${h.reason ?? "unknown"}`;
73417
+ } else if (corrupted.length > 0) {
73418
+ status = "fail";
73419
+ statusDetail = `${corrupted.length} mental model(s) corrupted by an LLM failure message \u2014 injected into every turn until refreshed`;
73420
+ } else if (gaps.length > 0) {
73421
+ status = "fail";
73422
+ statusDetail = `${gaps.length} recent conversation(s) stored with zero extracted facts \u2014 invisible to recall`;
73423
+ } else if (stale.length > 0) {
73424
+ status = "warn";
73425
+ statusDetail = `${stale.length} mental model(s) not refreshed in >7d`;
73426
+ } else if (h.totalDocuments === 0) {
73427
+ status = "warn";
73428
+ statusDetail = "bank is empty";
73429
+ }
73430
+ return {
73431
+ bank,
73432
+ agents,
73433
+ ok: h.ok,
73434
+ reason: h.reason,
73435
+ totalDocuments: h.totalDocuments,
73436
+ totalFacts: h.totalFacts,
73437
+ pendingOperations: h.pendingOperations,
73438
+ newestDocumentAt: h.newestDocumentAt,
73439
+ recentUnextractedCount: gaps.length,
73440
+ oldestUnextractedAt: gaps[0]?.createdAt ?? null,
73441
+ mentalModels: h.mentalModels,
73442
+ staleMentalModelCount: stale.length,
73443
+ corruptedMentalModelNames: corrupted.map((m) => m.name),
73444
+ status,
73445
+ statusDetail
73446
+ };
73447
+ }));
73448
+ rows.sort((a, b) => a.bank.localeCompare(b.bank));
73449
+ return { reachable, url, banks: rows };
73450
+ }
73148
73451
 
73149
73452
  // src/web/webhook-handler.ts
73150
73453
  import { appendFileSync as appendFileSync3, existsSync as existsSync48, mkdirSync as mkdirSync27, readFileSync as readFileSync43, writeFileSync as writeFileSync25 } from "fs";
@@ -73787,6 +74090,9 @@ function parseRoute(pathname, method) {
73787
74090
  if (method === "GET" && pathname === "/api/system-health") {
73788
74091
  return { handler: "getSystemHealth", params: {} };
73789
74092
  }
74093
+ if (method === "GET" && pathname === "/api/memory-health") {
74094
+ return { handler: "getMemoryHealth", params: {} };
74095
+ }
73790
74096
  if (method === "GET" && pathname === "/api/google-accounts") {
73791
74097
  return { handler: "getGoogleAccounts", params: {} };
73792
74098
  }
@@ -73948,6 +74254,8 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
73948
74254
  }
73949
74255
  case "getSystemHealth":
73950
74256
  return (async () => jsonResponse(await handleGetSystemHealth(config)))();
74257
+ case "getMemoryHealth":
74258
+ return (async () => jsonResponse(await handleGetMemoryHealth(freshConfig())))();
73951
74259
  case "getGoogleAccounts":
73952
74260
  return (async () => jsonResponse(await handleGetGoogleAccounts(freshConfig())))();
73953
74261
  case "getMicrosoftAccounts":
@@ -79557,7 +79865,14 @@ async function ensureSwitchroomFolder2(deps, agentName) {
79557
79865
  const top = await ensureFolder2(deps, "Switchroom", "root");
79558
79866
  return ensureFolder2(deps, agentName, top.id);
79559
79867
  }
79868
+ var GDRIVE_MULTIPART_MAX_BYTES = 5 * 1024 * 1024;
79560
79869
  async function uploadFile2(deps, parentId, filename, bytes, mimeType = "application/octet-stream") {
79870
+ if (bytes.byteLength <= GDRIVE_MULTIPART_MAX_BYTES) {
79871
+ return uploadMultipart(deps, parentId, filename, bytes, mimeType);
79872
+ }
79873
+ return uploadResumable(deps, parentId, filename, bytes, mimeType);
79874
+ }
79875
+ async function uploadMultipart(deps, parentId, filename, bytes, mimeType = "application/octet-stream") {
79561
79876
  const f = deps.fetchImpl ?? fetch;
79562
79877
  const boundary = "switchroom-deliver-boundary";
79563
79878
  const metadata = JSON.stringify({ name: filename, parents: [parentId] });
@@ -79589,6 +79904,48 @@ Content-Type: ${mimeType}\r
79589
79904
  }
79590
79905
  return await resp.json();
79591
79906
  }
79907
+ async function uploadResumable(deps, parentId, filename, bytes, mimeType = "application/octet-stream") {
79908
+ const f = deps.fetchImpl ?? fetch;
79909
+ const init = await f(`${UPLOAD}/files?uploadType=resumable&fields=id,name,webViewLink`, {
79910
+ method: "POST",
79911
+ headers: {
79912
+ Authorization: `Bearer ${deps.accessToken}`,
79913
+ "Content-Type": "application/json; charset=UTF-8",
79914
+ "X-Upload-Content-Type": mimeType
79915
+ },
79916
+ body: JSON.stringify({ name: filename, parents: [parentId] })
79917
+ });
79918
+ if (!init.ok) {
79919
+ throw new Error(`Drive resumable init failed: HTTP ${init.status} \u2014 ${await readBody2(init)}`);
79920
+ }
79921
+ const sessionUrl = init.headers.get("location") ?? init.headers.get("Location");
79922
+ if (!sessionUrl) {
79923
+ throw new Error("Drive resumable init returned no session URL (Location header)");
79924
+ }
79925
+ const CHUNK = 8 * 1024 * 1024;
79926
+ const total = bytes.byteLength;
79927
+ let lastFile = null;
79928
+ for (let start = 0;start < total; start += CHUNK) {
79929
+ const end = Math.min(start + CHUNK, total);
79930
+ const chunk2 = bytes.subarray(start, end);
79931
+ const put = await f(sessionUrl, {
79932
+ method: "PUT",
79933
+ headers: {
79934
+ "Content-Length": String(chunk2.byteLength),
79935
+ "Content-Range": `bytes ${start}-${end - 1}/${total}`
79936
+ },
79937
+ body: chunk2
79938
+ });
79939
+ if (put.status === 200 || put.status === 201) {
79940
+ lastFile = await put.json();
79941
+ } else if (put.status !== 308) {
79942
+ throw new Error(`Drive resumable chunk failed: HTTP ${put.status} \u2014 ${await readBody2(put)}`);
79943
+ }
79944
+ }
79945
+ if (!lastFile)
79946
+ throw new Error("Drive resumable upload completed without a final file resource");
79947
+ return lastFile;
79948
+ }
79592
79949
  async function createShareLink2(deps, file, scopes = ["anyone"]) {
79593
79950
  const f = deps.fetchImpl ?? fetch;
79594
79951
  for (const type of scopes) {