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.
- package/dist/agent-scheduler/index.js +23 -1
- package/dist/auth-broker/index.js +43 -3
- package/dist/cli/drive-write-pretool.mjs +23 -2
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +375 -18
- package/dist/cli/ui/index.html +67 -1
- package/dist/host-control/main.js +5 -1
- package/dist/vault/approvals/kernel-server.js +1 -0
- package/dist/vault/broker/server.js +2 -1
- package/package.json +1 -1
- package/profiles/default/CLAUDE.md.hbs +18 -0
- package/telegram-plugin/auth-snapshot-format.ts +9 -0
- package/telegram-plugin/auto-fallback-fleet.ts +59 -0
- package/telegram-plugin/dist/gateway/gateway.js +347 -21
- package/telegram-plugin/gateway/auth-broker-client.ts +2 -0
- package/telegram-plugin/gateway/auth-command.ts +35 -2
- package/telegram-plugin/gateway/gateway.ts +236 -22
- package/telegram-plugin/gateway/model-command.ts +182 -0
- package/telegram-plugin/quota-watch.ts +141 -3
- package/telegram-plugin/tests/auth-quota-util-cell.test.ts +23 -0
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +71 -0
- package/telegram-plugin/tests/model-command.test.ts +205 -0
- package/telegram-plugin/tests/quota-watch.test.ts +266 -0
- package/telegram-plugin/welcome-text.ts +7 -1
package/dist/cli/switchroom.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
23572
|
-
|
|
23573
|
-
|
|
23574
|
-
|
|
23575
|
-
|
|
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.
|
|
49941
|
-
var COMMIT_SHA = "
|
|
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) {
|