switchroom 0.15.43 → 0.15.45

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.
@@ -13520,7 +13520,7 @@ var init_zod = __esm(() => {
13520
13520
  });
13521
13521
 
13522
13522
  // src/config/schema.ts
13523
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
13523
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
13524
13524
  var init_schema = __esm(() => {
13525
13525
  init_zod();
13526
13526
  CodeRepoEntrySchema = exports_external.object({
@@ -13641,6 +13641,8 @@ var init_schema = __esm(() => {
13641
13641
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
13642
13642
  min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0\u20131.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default \u2014 " + "current behaviour). Try 0.10\u20130.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
13643
13643
  types: exports_external.array(exports_external.string()).optional().describe("Hindsight fact types to recall. Switchroom default is " + '["world", "experience", "observation"] \u2014 the synthesized ' + "`observation` tier is on by default. Set to " + '["world", "experience"] to opt out of observation-backed ' + "recall for this agent (or fleet-wide under defaults)."),
13644
+ additional_banks: exports_external.array(exports_external.string()).optional().describe("Extra Hindsight banks to recall from on every turn, merged into " + "the agent's own bank results \u2014 e.g. a shared operator/household " + "profile bank authored via `switchroom memory profile`. Each is " + "recalled with an 8s timeout and is non-fatal on failure. Stays " + "within the single tenant: all banks are the operator's data, in " + "the operator's Hindsight instance (see the `single-tenant` " + "invariant). Defaults to [] (no extra banks)."),
13645
+ sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Per-speaker recall routing: a map of Telegram sender \u2192 extra " + "recall bank. When a message arrives, the agent also recalls the " + "speaker's bank (matched by Telegram username \u2014 a leading @ is " + "optional \u2014 or numeric user_id), merged " + "into its own results \u2014 so each trusted user gets their own " + "profile context. Additive recall scoping within the single " + "tenant: never an access boundary (who may drive an agent stays " + "the per-agent user assignment in `access.allowFrom`). Author the " + "banks via `switchroom memory profile`."),
13644
13646
  skip_trivial: exports_external.boolean().optional().describe("Skip recall on plausibly-stateless trivial turns (time/date/" + "greeting). Switchroom default true \u2014 saves the recall arm + " + "injected tokens on turns that never need memory, guarded so it " + "never skips a turn that references user/project/session state. " + "Set false to always run recall."),
13645
13647
  topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) \u2192 soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: \u2026' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
13646
13648
  }).optional().describe("Auto-recall tuning knobs")
@@ -13854,8 +13856,12 @@ var init_schema = __esm(() => {
13854
13856
  message: "release.channel and release.pin are mutually exclusive"
13855
13857
  });
13856
13858
  NetworkIsolationSchema = exports_external.enum(["host", "strict"]).optional().describe("Container network mode (sec WS6-F1 #1390 / feature #1413). " + "'host' (DEFAULT when unset): `network_mode: host` \u2014 the agent " + "shares the host network stack; hindsight 127.0.0.1:18888 and " + "operator-LAN devices are reachable, but there is NO network " + "isolation from sibling agents or host services (the documented, " + "deliberate shared-host tradeoff). 'strict': the agent joins its " + "OWN dedicated docker bridge network instead \u2014 it cannot reach " + "sibling agents; host services are reached via " + "`host.docker.internal`. OPT-IN: validate hindsight / operator-" + "LAN / cron / boot-self-test paths for your deployment before " + "adopting fleet-wide (default-flip is deferred to that validation " + "cycle, #1413). Cascades override (agent \u2192 profile \u2192 defaults).");
13859
+ servesField = exports_external.array(exports_external.string()).optional().describe("Users (keys in the top-level `users:` block) this agent works for. When " + "a served user messages this agent, their profile_bank is recalled " + "(speaker routing \u2192 memory.recall.sender_banks). Unions with any explicit " + "memory.recall.sender_banks. NOTE: this does not yet generate access " + "(allowFrom) \u2014 pair agent access as today; allowFrom generation is a " + "later phase.");
13860
+ knowsField = exports_external.array(exports_external.string()).optional().describe("Users or banks this agent always knows as subjects \u2014 recalled and " + "recall-ranked even when that person is not the speaker (\u2192 " + "memory.recall.additional_banks). A `users:` key resolves to that user's " + "profile_bank; any other string is used as a raw bank name (e.g. a `kids` " + "profile bank with no Telegram identity). Unions with any explicit " + "memory.recall.additional_banks.");
13857
13861
  profileFields = {
13858
13862
  extends: exports_external.string().optional(),
13863
+ serves: servesField,
13864
+ knows: knowsField,
13859
13865
  bot_token: exports_external.string().optional(),
13860
13866
  release: ReleaseBlock.optional().describe("Release-channel pin / pointer. Either `channel` (dev|rc|latest) or " + "`pin` (sha-<hex>|v<semver>) \u2014 mutually exclusive. Per-agent value " + "REPLACES the root entirely (no field merge)."),
13861
13867
  timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the agent " + "container's `environment.TZ` in compose so subprocess `date`/" + "`Date.now()` are correct. If unset at every cascade layer, switchroom " + "auto-detects from /etc/timezone and warns on `reconcile` when the " + "detected zone is UTC."),
@@ -13876,7 +13882,9 @@ var init_schema = __esm(() => {
13876
13882
  recall: exports_external.object({
13877
13883
  max_memories: exports_external.number().int().min(0).optional(),
13878
13884
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
13879
- min_overlap: exports_external.number().min(0).max(1).optional()
13885
+ min_overlap: exports_external.number().min(0).max(1).optional(),
13886
+ additional_banks: exports_external.array(exports_external.string()).optional(),
13887
+ sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional()
13880
13888
  }).optional()
13881
13889
  }).optional(),
13882
13890
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
@@ -13921,6 +13929,8 @@ var init_schema = __esm(() => {
13921
13929
  AgentDefaultsSchema = exports_external.object(defaultsFields).optional();
13922
13930
  AgentSchema = exports_external.object({
13923
13931
  extends: exports_external.string().optional().describe("Name of a profile to inherit from (e.g., 'coding', 'health-coach'). " + "Profiles may be defined inline under switchroom.yaml `profiles:` or as a " + "filesystem directory `profiles/<name>/`. Defaults to DEFAULT_PROFILE " + "('default') when unset."),
13932
+ serves: servesField,
13933
+ knows: knowsField,
13924
13934
  bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
13925
13935
  release: ReleaseBlock.optional().describe("Per-agent release-channel pin / pointer. REPLACES the root " + "`release` block entirely (no field merge) \u2014 a pinned agent does " + "not inherit the fleet channel, and vice versa."),
13926
13936
  bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
@@ -14094,6 +14104,11 @@ var init_schema = __esm(() => {
14094
14104
  CronConfigSchema = exports_external.object({
14095
14105
  egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
14096
14106
  });
14107
+ UserSchema = exports_external.object({
14108
+ name: exports_external.string().optional().describe("Display name for the user."),
14109
+ telegram_ids: exports_external.array(exports_external.string()).min(1).describe("Telegram username(s) and/or numeric user id(s) identifying this user " + "(a leading @ is optional). Matched against the message sender for " + "per-speaker memory routing."),
14110
+ profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`).")
14111
+ });
14097
14112
  SwitchroomConfigSchema = exports_external.object({
14098
14113
  switchroom: exports_external.object({
14099
14114
  version: exports_external.literal(1).describe("Config schema version"),
@@ -14140,10 +14155,31 @@ var init_schema = __esm(() => {
14140
14155
  })).optional().describe("RFC #1873: per-Microsoft-account ACL. Maps account email \u2192 list of " + "agents permitted to use that account's broker credentials. Written " + "by `switchroom auth microsoft enable|disable`; read by the broker " + "on get-credentials with provider=microsoft."),
14141
14156
  defaults: AgentDefaultsSchema.describe("Implicit bottom-of-cascade profile applied to every agent before " + "per-agent config and `extends:` resolution. Tools, mcp_servers, and " + "schedule are unioned/concatenated; scalars and nested objects are " + "shallow-merged with per-agent values winning."),
14142
14157
  profiles: exports_external.record(exports_external.string(), ProfileSchema).optional().describe("Named profile definitions. Agents reference via `extends: <name>`. " + "Inline profiles declared here take priority over filesystem " + "profiles/<name>/ directories when both exist."),
14158
+ users: exports_external.record(exports_external.string(), UserSchema).optional().describe("Trusted users the fleet serves \u2014 each a Telegram identity plus a " + "memory profile bank. Assigned to agents via `serves` / `knows`. The " + "operator's own trusted people (single-tenant), not multi-tenant. See " + "reference/rfcs/user-concept.md."),
14143
14159
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
14144
14160
  message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
14145
14161
  }), AgentSchema).describe("Map of agent name to agent configuration"),
14146
14162
  cron: CronConfigSchema.optional().describe("Cheap-cron settings (reference/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (\u00a76.1). Required to enable any http-diff poll; not agent-writable.")
14163
+ }).superRefine((cfg, ctx) => {
14164
+ const userKeys = new Set(Object.keys(cfg.users ?? {}));
14165
+ const checkServes = (serves, path) => {
14166
+ (serves ?? []).forEach((s, i) => {
14167
+ if (!userKeys.has(s)) {
14168
+ ctx.addIssue({
14169
+ code: exports_external.ZodIssueCode.custom,
14170
+ message: `serves references unknown user "${s}" \u2014 add it to the top-level ` + "`users:` block (or did you mean `knows` for a raw bank name?)",
14171
+ path: [...path, i]
14172
+ });
14173
+ }
14174
+ });
14175
+ };
14176
+ checkServes(cfg.defaults?.serves, ["defaults", "serves"]);
14177
+ for (const [name, p] of Object.entries(cfg.profiles ?? {})) {
14178
+ checkServes(p.serves, ["profiles", name, "serves"]);
14179
+ }
14180
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
14181
+ checkServes(a.serves, ["agents", name, "serves"]);
14182
+ }
14147
14183
  });
14148
14184
  });
14149
14185
 
@@ -14980,6 +15016,40 @@ var init_loader = __esm(() => {
14980
15016
  };
14981
15017
  });
14982
15018
 
15019
+ // src/config/users.ts
15020
+ function resolveUsers(config, agentName) {
15021
+ const users = config.users ?? {};
15022
+ const agentRaw = config.agents?.[agentName];
15023
+ const agentConfig = agentRaw ? resolveAgentConfig(config.defaults, config.profiles, agentRaw) : undefined;
15024
+ const uniq = (xs) => [...new Set(xs)];
15025
+ const serves = uniq([
15026
+ ...config.defaults?.serves ?? [],
15027
+ ...agentConfig?.serves ?? []
15028
+ ]);
15029
+ const knows = uniq([
15030
+ ...config.defaults?.knows ?? [],
15031
+ ...agentConfig?.knows ?? []
15032
+ ]);
15033
+ const senderBanks = {};
15034
+ for (const userName of serves) {
15035
+ const u = users[userName];
15036
+ if (!u)
15037
+ continue;
15038
+ for (const id of u.telegram_ids) {
15039
+ senderBanks[id] = u.profile_bank;
15040
+ }
15041
+ }
15042
+ Object.assign(senderBanks, agentConfig?.memory?.recall?.sender_banks ?? {});
15043
+ const additionalBanks = uniq([
15044
+ ...knows.map((entry) => users[entry]?.profile_bank ?? entry),
15045
+ ...agentConfig?.memory?.recall?.additional_banks ?? []
15046
+ ]);
15047
+ return { senderBanks, additionalBanks };
15048
+ }
15049
+ var init_users = __esm(() => {
15050
+ init_merge();
15051
+ });
15052
+
14983
15053
  // src/memory/hindsight.ts
14984
15054
  function generateHindsightMcpConfig(collection, memoryConfig) {
14985
15055
  const url = memoryConfig.config?.url ?? "http://localhost:8888/mcp/";
@@ -14995,6 +15065,27 @@ function getCollectionForAgent(agentName, config) {
14995
15065
  const agentConfig = config.agents[agentName];
14996
15066
  return agentConfig?.memory?.collection ?? agentName;
14997
15067
  }
15068
+ function collectProfileBanks(config, opts = {}) {
15069
+ const { excludeAgentCollections = true } = opts;
15070
+ const banks = new Set;
15071
+ for (const user of Object.values(config.users ?? {})) {
15072
+ if (user.profile_bank)
15073
+ banks.add(user.profile_bank);
15074
+ }
15075
+ for (const agentName of Object.keys(config.agents)) {
15076
+ const { senderBanks, additionalBanks } = resolveUsers(config, agentName);
15077
+ for (const b of Object.values(senderBanks))
15078
+ banks.add(b);
15079
+ for (const b of additionalBanks)
15080
+ banks.add(b);
15081
+ }
15082
+ if (excludeAgentCollections) {
15083
+ for (const agentName of Object.keys(config.agents)) {
15084
+ banks.delete(getCollectionForAgent(agentName, config));
15085
+ }
15086
+ }
15087
+ return banks;
15088
+ }
14998
15089
  function isStrictIsolation(agentName, config) {
14999
15090
  const agentConfig = config.agents[agentName];
15000
15091
  return agentConfig?.memory?.isolation === "strict";
@@ -15424,6 +15515,7 @@ async function addMemoryTag(apiUrl, bankId, memoryId, tag, opts) {
15424
15515
  }
15425
15516
  var DEFAULT_RETAIN_MISSION, DEMOTE_FROM_RECALL_TAG = "[demote-from-recall]";
15426
15517
  var init_hindsight = __esm(() => {
15518
+ init_users();
15427
15519
  DEFAULT_RETAIN_MISSION = "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.";
15428
15520
  });
15429
15521
 
@@ -32928,9 +33020,14 @@ async function checkBankIngestHealth(config, url, opts) {
32928
33020
  const bankId = agentConfig.memory?.collection ?? agentName;
32929
33021
  banks.set(bankId, [...banks.get(bankId) ?? [], agentName]);
32930
33022
  }
33023
+ const profileBanks = collectProfileBanks(config);
33024
+ for (const bank of profileBanks) {
33025
+ if (!banks.has(bank))
33026
+ banks.set(bank, []);
33027
+ }
32931
33028
  const inspected = await Promise.all([...banks].map(async ([bankId, agents]) => [bankId, agents, await inspectBankHealth(url, bankId, { fetchImpl: opts?.fetchImpl })]));
32932
33029
  for (const [bankId, agents, h] of inspected) {
32933
- const label = `bank ${bankId}` + (agents[0] !== bankId ? ` (${agents.join(", ")})` : "");
33030
+ const label = `bank ${bankId}` + (profileBanks.has(bankId) ? " (profile)" : agents[0] !== bankId ? ` (${agents.join(", ")})` : "");
32934
33031
  if (!h.ok) {
32935
33032
  results.push({
32936
33033
  name: label,
@@ -50767,8 +50864,8 @@ import { existsSync, readFileSync } from "node:fs";
50767
50864
  import { dirname, join } from "node:path";
50768
50865
 
50769
50866
  // src/build-info.ts
50770
- var VERSION = "0.15.43";
50771
- var COMMIT_SHA = "5480a4c3";
50867
+ var VERSION = "0.15.45";
50868
+ var COMMIT_SHA = "41082e8b";
50772
50869
 
50773
50870
  // src/cli/resolve-version.ts
50774
50871
  function readPackageVersion() {
@@ -50899,6 +50996,7 @@ var LEGACY_CRON_SCRIPT_BASENAME_RE = /^cron-(\d+)\.sh$/;
50899
50996
  init_schema();
50900
50997
  init_cron_routing();
50901
50998
  init_tier_selector();
50999
+ init_users();
50902
51000
  init_merge();
50903
51001
  init_timezone();
50904
51002
 
@@ -52920,13 +53018,14 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig) {
52920
53018
  rmSync3(destPath, { recursive: true, force: true });
52921
53019
  }
52922
53020
  copyDirRecursive2(sourcePath, destPath);
52923
- applyHindsightSettingsOverrides(destPath);
53021
+ const additionalBanks = resolveUsers(switchroomConfig, agentName).additionalBanks;
53022
+ applyHindsightSettingsOverrides(destPath, additionalBanks);
52924
53023
  const bankId = agentMemory?.collection ?? agentName;
52925
53024
  const mcpUrl = memory.config?.url ?? "http://127.0.0.1:8888/mcp/";
52926
53025
  const apiBaseUrl = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
52927
53026
  return { pluginDir: destPath, apiBaseUrl, bankId };
52928
53027
  }
52929
- function applyHindsightSettingsOverrides(pluginDestPath) {
53028
+ function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks) {
52930
53029
  const settingsPath = join10(pluginDestPath, "settings.json");
52931
53030
  if (!existsSync15(settingsPath))
52932
53031
  return;
@@ -52947,6 +53046,9 @@ function applyHindsightSettingsOverrides(pluginDestPath) {
52947
53046
  settings.recallMinOverlap = 0.1;
52948
53047
  settings.recallTypes = ["world", "experience", "observation"];
52949
53048
  settings.recallSkipTrivial = true;
53049
+ if (additionalBanks.length > 0) {
53050
+ settings.recallAdditionalBanks = [...additionalBanks];
53051
+ }
52950
53052
  writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + `
52951
53053
  `, "utf-8");
52952
53054
  }
@@ -52972,6 +53074,7 @@ function buildWorkspaceContext(args) {
52972
53074
  hindsightRecallTypes,
52973
53075
  hindsightRecallSkipTrivial,
52974
53076
  hindsightTopicAliasesJson,
53077
+ hindsightSenderBanksJson,
52975
53078
  hindsightTopicFilterMode
52976
53079
  } = args;
52977
53080
  return {
@@ -53015,6 +53118,7 @@ function buildWorkspaceContext(args) {
53015
53118
  hindsightRecallTypes,
53016
53119
  hindsightRecallSkipTrivial,
53017
53120
  hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
53121
+ hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
53018
53122
  hindsightTopicFilterMode,
53019
53123
  switchroomConfigPathQ: switchroomConfigPath ? shellSingleQuote(resolve11(switchroomConfigPath)) : undefined,
53020
53124
  hostHomeQ: hostHomeQForBake(),
@@ -53224,6 +53328,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
53224
53328
  const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
53225
53329
  const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
53226
53330
  const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
53331
+ const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
53332
+ const hindsightSenderBanksJson = senderBanks && Object.keys(senderBanks).length > 0 ? JSON.stringify(senderBanks) : undefined;
53227
53333
  const hindsightTopicFilterMode = agentConfig.memory?.recall?.topic_filter_mode;
53228
53334
  const context = buildWorkspaceContext({
53229
53335
  name,
@@ -53247,6 +53353,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
53247
53353
  hindsightRecallTypes,
53248
53354
  hindsightRecallSkipTrivial,
53249
53355
  hindsightTopicAliasesJson,
53356
+ hindsightSenderBanksJson,
53250
53357
  hindsightTopicFilterMode
53251
53358
  });
53252
53359
  const dirs = [
@@ -53659,15 +53766,6 @@ ${body}
53659
53766
  }).catch((err) => {
53660
53767
  console.warn(` ${source_default.yellow("\u26a0")} Bank mission update error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
53661
53768
  });
53662
- ensureUserProfileMentalModel(apiUrl, hindsightBankId, { timeoutMs: 5000 }).then((result) => {
53663
- if (result.ok) {
53664
- console.log(` ${source_default.green("\u2713")} User-profile Mental Model ready for ${formatAgentBankLabel(name, hindsightBankId)}`);
53665
- } else {
53666
- console.warn(` ${source_default.yellow("\u26a0")} Failed to create user-profile MM for ${formatAgentBankLabel(name, hindsightBankId)}: ${result.reason}`);
53667
- }
53668
- }).catch((err) => {
53669
- console.warn(` ${source_default.yellow("\u26a0")} User-profile MM error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
53670
- });
53671
53769
  });
53672
53770
  }
53673
53771
  for (const f of rewrittenWithBackup) {
@@ -53723,14 +53821,6 @@ function buildSettingsHooksBlock(p) {
53723
53821
  async: true
53724
53822
  });
53725
53823
  }
53726
- if (hindsightEnabled) {
53727
- stopHooks.push({
53728
- type: "command",
53729
- command: wrap("hook:user-profile-refresh", `bash "${join10(DOCKER_BIN_PATH, "user-profile-refresh-hook.sh")}"`),
53730
- timeout: 10,
53731
- async: true
53732
- });
53733
- }
53734
53824
  if (useSwitchroomPlugin) {
53735
53825
  stopHooks.push({
53736
53826
  type: "command",
@@ -54081,6 +54171,8 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
54081
54171
  const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
54082
54172
  const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
54083
54173
  const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
54174
+ const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
54175
+ const hindsightSenderBanksJson = senderBanks && Object.keys(senderBanks).length > 0 ? JSON.stringify(senderBanks) : undefined;
54084
54176
  const hindsightTopicFilterMode = agentConfig.memory?.recall?.topic_filter_mode;
54085
54177
  if (agentConfig.repos) {
54086
54178
  for (const [slug, entry] of Object.entries(agentConfig.repos)) {
@@ -54117,6 +54209,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
54117
54209
  hindsightRecallTypes,
54118
54210
  hindsightRecallSkipTrivial,
54119
54211
  hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
54212
+ hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
54120
54213
  hindsightTopicFilterMode,
54121
54214
  hostHomeQ: hostHomeQForBake(),
54122
54215
  modelQ: shellSingleQuote(resolveMainModel(agentConfig.model)),
@@ -54506,6 +54599,7 @@ ${body}
54506
54599
  hindsightRecallTypes,
54507
54600
  hindsightRecallSkipTrivial,
54508
54601
  hindsightTopicAliasesJson,
54602
+ hindsightSenderBanksJson,
54509
54603
  hindsightTopicFilterMode
54510
54604
  });
54511
54605
  mkdirSync10(reconcileWorkspaceDir, { recursive: true });
@@ -54599,15 +54693,6 @@ ${body}
54599
54693
  console.warn(` ${source_default.yellow("\u26a0")} Bank mission update error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
54600
54694
  });
54601
54695
  }
54602
- ensureUserProfileMentalModel(apiUrl, hindsightBankId, { timeoutMs: 5000 }).then((result) => {
54603
- if (result.ok) {
54604
- console.log(` ${source_default.green("\u2713")} User-profile Mental Model ready for ${formatAgentBankLabel(name, hindsightBankId)}`);
54605
- } else {
54606
- console.warn(` ${source_default.yellow("\u26a0")} Failed to create user-profile MM for ${formatAgentBankLabel(name, hindsightBankId)}: ${result.reason}`);
54607
- }
54608
- }).catch((err) => {
54609
- console.warn(` ${source_default.yellow("\u26a0")} User-profile MM error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
54610
- });
54611
54696
  });
54612
54697
  }
54613
54698
  return {
@@ -68895,8 +68980,10 @@ var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
68895
68980
  var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
68896
68981
  var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 12;
68897
68982
  var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 3;
68898
- var HINDSIGHT_DEFAULT_MEM_LIMIT = "4g";
68899
- var HINDSIGHT_DEFAULT_MEM_RESERVATION = "2g";
68983
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 6;
68984
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 300;
68985
+ var HINDSIGHT_DEFAULT_MEM_LIMIT = "8g";
68986
+ var HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g";
68900
68987
  var HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000;
68901
68988
  var HINDSIGHT_DEFAULT_SHM_SIZE = "2g";
68902
68989
  var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
@@ -68985,7 +69072,11 @@ function startHindsight(ports) {
68985
69072
  "-e",
68986
69073
  `HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
68987
69074
  "-e",
68988
- `HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`
69075
+ `HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
69076
+ "-e",
69077
+ `HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
69078
+ "-e",
69079
+ `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`
68989
69080
  ];
68990
69081
  const args = [
68991
69082
  "run",
@@ -69086,6 +69177,8 @@ function generateHindsightComposeSnippet() {
69086
69177
  ` - HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
69087
69178
  ` - HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
69088
69179
  ` - HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
69180
+ ` - HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
69181
+ ` - HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`,
69089
69182
  ` mem_limit: ${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
69090
69183
  ` mem_reservation: ${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
69091
69184
  ` pids_limit: ${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
@@ -69249,6 +69342,26 @@ Searching all eligible collections:
69249
69342
  console.log(` ${row}`);
69250
69343
  }
69251
69344
  console.log();
69345
+ const profileBanks = collectProfileBanks(config);
69346
+ if (profileBanks.size > 0) {
69347
+ const owners = new Map;
69348
+ for (const [uname, u] of Object.entries(config.users ?? {})) {
69349
+ if (u.profile_bank) {
69350
+ owners.set(u.profile_bank, [
69351
+ ...owners.get(u.profile_bank) ?? [],
69352
+ uname
69353
+ ]);
69354
+ }
69355
+ }
69356
+ console.log(source_default.bold(` Profile banks (per-user / shared):
69357
+ `));
69358
+ for (const bank of [...profileBanks].sort()) {
69359
+ const who = owners.get(bank);
69360
+ const label = who ? `user: ${who.join(", ")}` : "shared";
69361
+ console.log(` ${bank.padEnd(widths[1])} ${source_default.gray(label)}`);
69362
+ }
69363
+ console.log();
69364
+ }
69252
69365
  console.log(source_default.bold(` Hindsight CLI commands:
69253
69366
  `));
69254
69367
  for (const name of agentNames) {
@@ -69487,6 +69600,88 @@ Demoting memory ${source_default.cyan(memoryId)}`));
69487
69600
  console.error(source_default.gray(" The Hindsight MCP `update_memory` tool may not be exposed by your deployment, or the memory ID may be wrong. Try `switchroom memory recall-log " + agent + " --json` to confirm the ID surfaced recently."));
69488
69601
  process.exit(1);
69489
69602
  }));
69603
+ const restBase = (url) => (url ?? "http://127.0.0.1:8888/mcp/").replace(/\/mcp\/?$/, "").replace(/\/$/, "");
69604
+ const VALID_BANK = /^[a-zA-Z0-9_.-]+$/;
69605
+ const profile = memory.command("profile").description("Author + inspect operator profile banks (shared/per-user memory; wire via memory.recall.additional_banks)");
69606
+ profile.command("add <bank> <fact...>").description("Add an operator-authored fact to a profile bank (creates the bank on first write)").option("--timeout <ms>", "HTTP timeout in milliseconds (retain runs synchronously; default: 60000)", "60000").action(withConfigError(async (bank, factWords, opts) => {
69607
+ const config = getConfig(program3);
69608
+ if (!bank || !VALID_BANK.test(bank)) {
69609
+ console.error(source_default.red("Bank name must be non-empty and contain only letters, digits, '.', '_', or '-'."));
69610
+ process.exit(1);
69611
+ }
69612
+ const content = factWords.join(" ").trim();
69613
+ if (content.length === 0) {
69614
+ console.error(source_default.red("A non-empty fact is required."));
69615
+ process.exit(1);
69616
+ }
69617
+ const base = restBase(config.memory?.config?.url);
69618
+ const url = `${base}/v1/default/banks/${encodeURIComponent(bank)}/memories`;
69619
+ const timeoutMs = Math.max(1000, parseInt(opts.timeout, 10) || 60000);
69620
+ const ctrl = new AbortController;
69621
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
69622
+ try {
69623
+ const res = await fetch(url, {
69624
+ method: "POST",
69625
+ headers: { "Content-Type": "application/json" },
69626
+ body: JSON.stringify({
69627
+ items: [{ content, tags: ["operator-authored", "profile"] }],
69628
+ async: false
69629
+ }),
69630
+ signal: ctrl.signal
69631
+ });
69632
+ if (!res.ok) {
69633
+ console.error(source_default.red(`\u2717 Retain failed: HTTP ${res.status}`), source_default.gray(await res.text().catch(() => "")));
69634
+ process.exit(1);
69635
+ }
69636
+ console.log(source_default.green(`\u2713 Added to profile bank "${bank}"`));
69637
+ console.log(source_default.gray(` ${content}`));
69638
+ console.log(source_default.gray(" (fact extraction runs in the background \u2014 `profile list` may lag a few seconds)"));
69639
+ console.log(source_default.gray(`
69640
+ Wire it into agents via memory.recall.additional_banks: ["${bank}"] in switchroom.yaml,
69641
+ then \`switchroom apply\` + restart the agent(s).`));
69642
+ } catch (e) {
69643
+ console.error(source_default.red("\u2717 Retain failed:"), source_default.gray(e instanceof Error ? e.message : String(e)));
69644
+ process.exit(1);
69645
+ } finally {
69646
+ clearTimeout(t);
69647
+ }
69648
+ }));
69649
+ profile.command("list <bank>").description("List the memory units in a profile bank").option("--limit <n>", "Max units to show (default: 50)", "50").option("--timeout <ms>", "HTTP timeout in milliseconds (default: 10000)", "10000").action(withConfigError(async (bank, opts) => {
69650
+ const config = getConfig(program3);
69651
+ if (!bank || !VALID_BANK.test(bank)) {
69652
+ console.error(source_default.red("Bank name must contain only letters, digits, '.', '_', or '-'."));
69653
+ process.exit(1);
69654
+ }
69655
+ const base = restBase(config.memory?.config?.url);
69656
+ const limit = Math.max(1, parseInt(opts.limit, 10) || 50);
69657
+ const url = `${base}/v1/default/banks/${encodeURIComponent(bank)}/memories/list?limit=${limit}`;
69658
+ const timeoutMs = Math.max(1000, parseInt(opts.timeout, 10) || 1e4);
69659
+ const ctrl = new AbortController;
69660
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
69661
+ try {
69662
+ const res = await fetch(url, { signal: ctrl.signal });
69663
+ if (!res.ok) {
69664
+ console.error(source_default.red(`\u2717 List failed: HTTP ${res.status}`), source_default.gray(await res.text().catch(() => "")));
69665
+ process.exit(1);
69666
+ }
69667
+ const data = await res.json();
69668
+ const units = data.results ?? data.memories ?? data.units ?? [];
69669
+ console.log(source_default.bold(`
69670
+ Profile bank "${bank}" \u2014 ${units.length} unit(s)
69671
+ `));
69672
+ for (const u of units) {
69673
+ const ft = (u.fact_type ?? "?").padEnd(11);
69674
+ const text = String(u.content ?? u.text ?? "").replace(/\s+/g, " ").slice(0, 100);
69675
+ console.log(` ${source_default.gray(ft)} ${text}`);
69676
+ }
69677
+ console.log();
69678
+ } catch (e) {
69679
+ console.error(source_default.red("\u2717 List failed:"), source_default.gray(e instanceof Error ? e.message : String(e)));
69680
+ process.exit(1);
69681
+ } finally {
69682
+ clearTimeout(t);
69683
+ }
69684
+ }));
69490
69685
  }
69491
69686
 
69492
69687
  // src/cli/web.ts
@@ -75529,6 +75724,11 @@ async function handleGetMemoryHealth(config, opts) {
75529
75724
  const bank = getCollectionForAgent(agentName, config);
75530
75725
  banks.set(bank, [...banks.get(bank) ?? [], agentName]);
75531
75726
  }
75727
+ const profileBankSet = collectProfileBanks(config);
75728
+ for (const bank of profileBankSet) {
75729
+ if (!banks.has(bank))
75730
+ banks.set(bank, []);
75731
+ }
75532
75732
  const rows = await Promise.all([...banks].map(async ([bank, agents]) => {
75533
75733
  const h = await inspectBankHealth(url, bank, { fetchImpl: opts?.fetchImpl });
75534
75734
  const gaps = recentUnextracted(h.unextractedDocuments, 30, now);
@@ -75573,7 +75773,8 @@ async function handleGetMemoryHealth(config, opts) {
75573
75773
  staleMentalModelCount: stale.length,
75574
75774
  corruptedMentalModelNames: corrupted.map((m) => m.name),
75575
75775
  status,
75576
- statusDetail
75776
+ statusDetail,
75777
+ kind: profileBankSet.has(bank) ? "profile" : "agent"
75577
75778
  };
75578
75779
  }));
75579
75780
  rows.sort((a, b) => a.bank.localeCompare(b.bank));
@@ -75587,7 +75788,7 @@ function isKnownBank(config, bank) {
75587
75788
  if (getCollectionForAgent(agentName, config) === bank)
75588
75789
  return true;
75589
75790
  }
75590
- return false;
75791
+ return collectProfileBanks(config).has(bank);
75591
75792
  }
75592
75793
  async function handleMemoryReprocess(config, body, deps) {
75593
75794
  const bank = typeof body.bank === "string" ? body.bank : "";
@@ -898,7 +898,7 @@
898
898
  return `<div class="agent-card">
899
899
  <div class="card-header" style="cursor:default">
900
900
  ${statusDot(b.status)}<span class="agent-name">${escapeHtml(b.bank)}</span>
901
- <span style="color:var(--text-dim);font-size:.85em;margin-left:.5rem">${escapeHtml((b.agents || []).join(', '))}</span>
901
+ <span style="color:var(--text-dim);font-size:.85em;margin-left:.5rem">${b.kind === 'profile' ? '<em>profile bank</em>' : escapeHtml((b.agents || []).join(', '))}</span>
902
902
  </div>
903
903
  <div style="padding:0 1.25rem 1rem">
904
904
  <div style="color:var(--text-dim);margin:.1rem 0 .3rem">${escapeHtml(b.statusDetail || '')}</div>
@@ -13812,6 +13812,8 @@ var AgentMemorySchema = exports_external.object({
13812
13812
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
13813
13813
  min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default — " + "current behaviour). Try 0.10–0.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
13814
13814
  types: exports_external.array(exports_external.string()).optional().describe("Hindsight fact types to recall. Switchroom default is " + '["world", "experience", "observation"] — the synthesized ' + "`observation` tier is on by default. Set to " + '["world", "experience"] to opt out of observation-backed ' + "recall for this agent (or fleet-wide under defaults)."),
13815
+ additional_banks: exports_external.array(exports_external.string()).optional().describe("Extra Hindsight banks to recall from on every turn, merged into " + "the agent's own bank results — e.g. a shared operator/household " + "profile bank authored via `switchroom memory profile`. Each is " + "recalled with an 8s timeout and is non-fatal on failure. Stays " + "within the single tenant: all banks are the operator's data, in " + "the operator's Hindsight instance (see the `single-tenant` " + "invariant). Defaults to [] (no extra banks)."),
13816
+ sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Per-speaker recall routing: a map of Telegram sender → extra " + "recall bank. When a message arrives, the agent also recalls the " + "speaker's bank (matched by Telegram username — a leading @ is " + "optional — or numeric user_id), merged " + "into its own results — so each trusted user gets their own " + "profile context. Additive recall scoping within the single " + "tenant: never an access boundary (who may drive an agent stays " + "the per-agent user assignment in `access.allowFrom`). Author the " + "banks via `switchroom memory profile`."),
13815
13817
  skip_trivial: exports_external.boolean().optional().describe("Skip recall on plausibly-stateless trivial turns (time/date/" + "greeting). Switchroom default true — saves the recall arm + " + "injected tokens on turns that never need memory, guarded so it " + "never skips a turn that references user/project/session state. " + "Set false to always run recall."),
13816
13818
  topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) → soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: …' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
13817
13819
  }).optional().describe("Auto-recall tuning knobs")
@@ -14025,8 +14027,12 @@ var ReleaseBlock = exports_external.object({
14025
14027
  message: "release.channel and release.pin are mutually exclusive"
14026
14028
  });
14027
14029
  var NetworkIsolationSchema = exports_external.enum(["host", "strict"]).optional().describe("Container network mode (sec WS6-F1 #1390 / feature #1413). " + "'host' (DEFAULT when unset): `network_mode: host` — the agent " + "shares the host network stack; hindsight 127.0.0.1:18888 and " + "operator-LAN devices are reachable, but there is NO network " + "isolation from sibling agents or host services (the documented, " + "deliberate shared-host tradeoff). 'strict': the agent joins its " + "OWN dedicated docker bridge network instead — it cannot reach " + "sibling agents; host services are reached via " + "`host.docker.internal`. OPT-IN: validate hindsight / operator-" + "LAN / cron / boot-self-test paths for your deployment before " + "adopting fleet-wide (default-flip is deferred to that validation " + "cycle, #1413). Cascades override (agent → profile → defaults).");
14030
+ var servesField = exports_external.array(exports_external.string()).optional().describe("Users (keys in the top-level `users:` block) this agent works for. When " + "a served user messages this agent, their profile_bank is recalled " + "(speaker routing → memory.recall.sender_banks). Unions with any explicit " + "memory.recall.sender_banks. NOTE: this does not yet generate access " + "(allowFrom) — pair agent access as today; allowFrom generation is a " + "later phase.");
14031
+ var knowsField = exports_external.array(exports_external.string()).optional().describe("Users or banks this agent always knows as subjects — recalled and " + "recall-ranked even when that person is not the speaker (→ " + "memory.recall.additional_banks). A `users:` key resolves to that user's " + "profile_bank; any other string is used as a raw bank name (e.g. a `kids` " + "profile bank with no Telegram identity). Unions with any explicit " + "memory.recall.additional_banks.");
14028
14032
  var profileFields = {
14029
14033
  extends: exports_external.string().optional(),
14034
+ serves: servesField,
14035
+ knows: knowsField,
14030
14036
  bot_token: exports_external.string().optional(),
14031
14037
  release: ReleaseBlock.optional().describe("Release-channel pin / pointer. Either `channel` (dev|rc|latest) or " + "`pin` (sha-<hex>|v<semver>) — mutually exclusive. Per-agent value " + "REPLACES the root entirely (no field merge)."),
14032
14038
  timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("IANA timezone name (e.g. 'Australia/Melbourne', 'America/New_York', " + "'UTC'). Used to generate the per-turn local-time hint the agent's " + "UserPromptSubmit timezone hook emits, and baked into the agent " + "container's `environment.TZ` in compose so subprocess `date`/" + "`Date.now()` are correct. If unset at every cascade layer, switchroom " + "auto-detects from /etc/timezone and warns on `reconcile` when the " + "detected zone is UTC."),
@@ -14047,7 +14053,9 @@ var profileFields = {
14047
14053
  recall: exports_external.object({
14048
14054
  max_memories: exports_external.number().int().min(0).optional(),
14049
14055
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
14050
- min_overlap: exports_external.number().min(0).max(1).optional()
14056
+ min_overlap: exports_external.number().min(0).max(1).optional(),
14057
+ additional_banks: exports_external.array(exports_external.string()).optional(),
14058
+ sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional()
14051
14059
  }).optional()
14052
14060
  }).optional(),
14053
14061
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
@@ -14092,6 +14100,8 @@ var { extends: _omitExtends, ...defaultsFields } = profileFields;
14092
14100
  var AgentDefaultsSchema = exports_external.object(defaultsFields).optional();
14093
14101
  var AgentSchema = exports_external.object({
14094
14102
  extends: exports_external.string().optional().describe("Name of a profile to inherit from (e.g., 'coding', 'health-coach'). " + "Profiles may be defined inline under switchroom.yaml `profiles:` or as a " + "filesystem directory `profiles/<name>/`. Defaults to DEFAULT_PROFILE " + "('default') when unset."),
14103
+ serves: servesField,
14104
+ knows: knowsField,
14095
14105
  bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
14096
14106
  release: ReleaseBlock.optional().describe("Per-agent release-channel pin / pointer. REPLACES the root " + "`release` block entirely (no field merge) — a pinned agent does " + "not inherit the fleet channel, and vice versa."),
14097
14107
  bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
@@ -14265,6 +14275,11 @@ var CronEgressSchema = exports_external.object({
14265
14275
  var CronConfigSchema = exports_external.object({
14266
14276
  egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
14267
14277
  });
14278
+ var UserSchema = exports_external.object({
14279
+ name: exports_external.string().optional().describe("Display name for the user."),
14280
+ telegram_ids: exports_external.array(exports_external.string()).min(1).describe("Telegram username(s) and/or numeric user id(s) identifying this user " + "(a leading @ is optional). Matched against the message sender for " + "per-speaker memory routing."),
14281
+ profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`).")
14282
+ });
14268
14283
  var SwitchroomConfigSchema = exports_external.object({
14269
14284
  switchroom: exports_external.object({
14270
14285
  version: exports_external.literal(1).describe("Config schema version"),
@@ -14311,10 +14326,31 @@ var SwitchroomConfigSchema = exports_external.object({
14311
14326
  })).optional().describe("RFC #1873: per-Microsoft-account ACL. Maps account email → list of " + "agents permitted to use that account's broker credentials. Written " + "by `switchroom auth microsoft enable|disable`; read by the broker " + "on get-credentials with provider=microsoft."),
14312
14327
  defaults: AgentDefaultsSchema.describe("Implicit bottom-of-cascade profile applied to every agent before " + "per-agent config and `extends:` resolution. Tools, mcp_servers, and " + "schedule are unioned/concatenated; scalars and nested objects are " + "shallow-merged with per-agent values winning."),
14313
14328
  profiles: exports_external.record(exports_external.string(), ProfileSchema).optional().describe("Named profile definitions. Agents reference via `extends: <name>`. " + "Inline profiles declared here take priority over filesystem " + "profiles/<name>/ directories when both exist."),
14329
+ users: exports_external.record(exports_external.string(), UserSchema).optional().describe("Trusted users the fleet serves — each a Telegram identity plus a " + "memory profile bank. Assigned to agents via `serves` / `knows`. The " + "operator's own trusted people (single-tenant), not multi-tenant. See " + "reference/rfcs/user-concept.md."),
14314
14330
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
14315
14331
  message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
14316
14332
  }), AgentSchema).describe("Map of agent name to agent configuration"),
14317
14333
  cron: CronConfigSchema.optional().describe("Cheap-cron settings (reference/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (§6.1). Required to enable any http-diff poll; not agent-writable.")
14334
+ }).superRefine((cfg, ctx) => {
14335
+ const userKeys = new Set(Object.keys(cfg.users ?? {}));
14336
+ const checkServes = (serves, path) => {
14337
+ (serves ?? []).forEach((s, i) => {
14338
+ if (!userKeys.has(s)) {
14339
+ ctx.addIssue({
14340
+ code: exports_external.ZodIssueCode.custom,
14341
+ message: `serves references unknown user "${s}" — add it to the top-level ` + "`users:` block (or did you mean `knows` for a raw bank name?)",
14342
+ path: [...path, i]
14343
+ });
14344
+ }
14345
+ });
14346
+ };
14347
+ checkServes(cfg.defaults?.serves, ["defaults", "serves"]);
14348
+ for (const [name, p] of Object.entries(cfg.profiles ?? {})) {
14349
+ checkServes(p.serves, ["profiles", name, "serves"]);
14350
+ }
14351
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
14352
+ checkServes(a.serves, ["agents", name, "serves"]);
14353
+ }
14318
14354
  });
14319
14355
 
14320
14356
  // src/config/paths.ts