switchroom 0.15.42 → 0.15.44

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
 
@@ -50767,8 +50803,8 @@ import { existsSync, readFileSync } from "node:fs";
50767
50803
  import { dirname, join } from "node:path";
50768
50804
 
50769
50805
  // src/build-info.ts
50770
- var VERSION = "0.15.42";
50771
- var COMMIT_SHA = "2c7e12da";
50806
+ var VERSION = "0.15.44";
50807
+ var COMMIT_SHA = "a375378f";
50772
50808
 
50773
50809
  // src/cli/resolve-version.ts
50774
50810
  function readPackageVersion() {
@@ -50899,6 +50935,40 @@ var LEGACY_CRON_SCRIPT_BASENAME_RE = /^cron-(\d+)\.sh$/;
50899
50935
  init_schema();
50900
50936
  init_cron_routing();
50901
50937
  init_tier_selector();
50938
+
50939
+ // src/config/users.ts
50940
+ init_merge();
50941
+ function resolveUsers(config, agentName) {
50942
+ const users = config.users ?? {};
50943
+ const agentRaw = config.agents?.[agentName];
50944
+ const agentConfig = agentRaw ? resolveAgentConfig(config.defaults, config.profiles, agentRaw) : undefined;
50945
+ const uniq = (xs) => [...new Set(xs)];
50946
+ const serves = uniq([
50947
+ ...config.defaults?.serves ?? [],
50948
+ ...agentConfig?.serves ?? []
50949
+ ]);
50950
+ const knows = uniq([
50951
+ ...config.defaults?.knows ?? [],
50952
+ ...agentConfig?.knows ?? []
50953
+ ]);
50954
+ const senderBanks = {};
50955
+ for (const userName of serves) {
50956
+ const u = users[userName];
50957
+ if (!u)
50958
+ continue;
50959
+ for (const id of u.telegram_ids) {
50960
+ senderBanks[id] = u.profile_bank;
50961
+ }
50962
+ }
50963
+ Object.assign(senderBanks, agentConfig?.memory?.recall?.sender_banks ?? {});
50964
+ const additionalBanks = uniq([
50965
+ ...knows.map((entry) => users[entry]?.profile_bank ?? entry),
50966
+ ...agentConfig?.memory?.recall?.additional_banks ?? []
50967
+ ]);
50968
+ return { senderBanks, additionalBanks };
50969
+ }
50970
+
50971
+ // src/agents/scaffold.ts
50902
50972
  init_merge();
50903
50973
  init_timezone();
50904
50974
 
@@ -52920,13 +52990,14 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig) {
52920
52990
  rmSync3(destPath, { recursive: true, force: true });
52921
52991
  }
52922
52992
  copyDirRecursive2(sourcePath, destPath);
52923
- applyHindsightSettingsOverrides(destPath);
52993
+ const additionalBanks = resolveUsers(switchroomConfig, agentName).additionalBanks;
52994
+ applyHindsightSettingsOverrides(destPath, additionalBanks);
52924
52995
  const bankId = agentMemory?.collection ?? agentName;
52925
52996
  const mcpUrl = memory.config?.url ?? "http://127.0.0.1:8888/mcp/";
52926
52997
  const apiBaseUrl = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
52927
52998
  return { pluginDir: destPath, apiBaseUrl, bankId };
52928
52999
  }
52929
- function applyHindsightSettingsOverrides(pluginDestPath) {
53000
+ function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks) {
52930
53001
  const settingsPath = join10(pluginDestPath, "settings.json");
52931
53002
  if (!existsSync15(settingsPath))
52932
53003
  return;
@@ -52947,6 +53018,9 @@ function applyHindsightSettingsOverrides(pluginDestPath) {
52947
53018
  settings.recallMinOverlap = 0.1;
52948
53019
  settings.recallTypes = ["world", "experience", "observation"];
52949
53020
  settings.recallSkipTrivial = true;
53021
+ if (additionalBanks.length > 0) {
53022
+ settings.recallAdditionalBanks = [...additionalBanks];
53023
+ }
52950
53024
  writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + `
52951
53025
  `, "utf-8");
52952
53026
  }
@@ -52972,6 +53046,7 @@ function buildWorkspaceContext(args) {
52972
53046
  hindsightRecallTypes,
52973
53047
  hindsightRecallSkipTrivial,
52974
53048
  hindsightTopicAliasesJson,
53049
+ hindsightSenderBanksJson,
52975
53050
  hindsightTopicFilterMode
52976
53051
  } = args;
52977
53052
  return {
@@ -53015,6 +53090,7 @@ function buildWorkspaceContext(args) {
53015
53090
  hindsightRecallTypes,
53016
53091
  hindsightRecallSkipTrivial,
53017
53092
  hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
53093
+ hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
53018
53094
  hindsightTopicFilterMode,
53019
53095
  switchroomConfigPathQ: switchroomConfigPath ? shellSingleQuote(resolve11(switchroomConfigPath)) : undefined,
53020
53096
  hostHomeQ: hostHomeQForBake(),
@@ -53224,6 +53300,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
53224
53300
  const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
53225
53301
  const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
53226
53302
  const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
53303
+ const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
53304
+ const hindsightSenderBanksJson = senderBanks && Object.keys(senderBanks).length > 0 ? JSON.stringify(senderBanks) : undefined;
53227
53305
  const hindsightTopicFilterMode = agentConfig.memory?.recall?.topic_filter_mode;
53228
53306
  const context = buildWorkspaceContext({
53229
53307
  name,
@@ -53247,6 +53325,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
53247
53325
  hindsightRecallTypes,
53248
53326
  hindsightRecallSkipTrivial,
53249
53327
  hindsightTopicAliasesJson,
53328
+ hindsightSenderBanksJson,
53250
53329
  hindsightTopicFilterMode
53251
53330
  });
53252
53331
  const dirs = [
@@ -54081,6 +54160,8 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
54081
54160
  const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
54082
54161
  const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
54083
54162
  const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
54163
+ const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
54164
+ const hindsightSenderBanksJson = senderBanks && Object.keys(senderBanks).length > 0 ? JSON.stringify(senderBanks) : undefined;
54084
54165
  const hindsightTopicFilterMode = agentConfig.memory?.recall?.topic_filter_mode;
54085
54166
  if (agentConfig.repos) {
54086
54167
  for (const [slug, entry] of Object.entries(agentConfig.repos)) {
@@ -54117,6 +54198,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
54117
54198
  hindsightRecallTypes,
54118
54199
  hindsightRecallSkipTrivial,
54119
54200
  hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
54201
+ hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
54120
54202
  hindsightTopicFilterMode,
54121
54203
  hostHomeQ: hostHomeQForBake(),
54122
54204
  modelQ: shellSingleQuote(resolveMainModel(agentConfig.model)),
@@ -54506,6 +54588,7 @@ ${body}
54506
54588
  hindsightRecallTypes,
54507
54589
  hindsightRecallSkipTrivial,
54508
54590
  hindsightTopicAliasesJson,
54591
+ hindsightSenderBanksJson,
54509
54592
  hindsightTopicFilterMode
54510
54593
  });
54511
54594
  mkdirSync10(reconcileWorkspaceDir, { recursive: true });
@@ -68892,8 +68975,13 @@ var HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true";
68892
68975
  var HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150;
68893
68976
  var HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4;
68894
68977
  var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
68895
- var HINDSIGHT_DEFAULT_MEM_LIMIT = "4g";
68896
- var HINDSIGHT_DEFAULT_MEM_RESERVATION = "2g";
68978
+ var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
68979
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 12;
68980
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 3;
68981
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 6;
68982
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 300;
68983
+ var HINDSIGHT_DEFAULT_MEM_LIMIT = "8g";
68984
+ var HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g";
68897
68985
  var HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000;
68898
68986
  var HINDSIGHT_DEFAULT_SHM_SIZE = "2g";
68899
68987
  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)';
@@ -68976,7 +69064,17 @@ function startHindsight(ports) {
68976
69064
  "-e",
68977
69065
  `HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
68978
69066
  "-e",
68979
- `HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`
69067
+ `HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
69068
+ "-e",
69069
+ `HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
69070
+ "-e",
69071
+ `HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
69072
+ "-e",
69073
+ `HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
69074
+ "-e",
69075
+ `HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
69076
+ "-e",
69077
+ `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`
68980
69078
  ];
68981
69079
  const args = [
68982
69080
  "run",
@@ -69074,6 +69172,11 @@ function generateHindsightComposeSnippet() {
69074
69172
  ` - HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
69075
69173
  ` - HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
69076
69174
  ` - HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
69175
+ ` - HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
69176
+ ` - HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
69177
+ ` - HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
69178
+ ` - HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
69179
+ ` - HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`,
69077
69180
  ` mem_limit: ${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
69078
69181
  ` mem_reservation: ${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
69079
69182
  ` pids_limit: ${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
@@ -69475,6 +69578,88 @@ Demoting memory ${source_default.cyan(memoryId)}`));
69475
69578
  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."));
69476
69579
  process.exit(1);
69477
69580
  }));
69581
+ const restBase = (url) => (url ?? "http://127.0.0.1:8888/mcp/").replace(/\/mcp\/?$/, "").replace(/\/$/, "");
69582
+ const VALID_BANK = /^[a-zA-Z0-9_.-]+$/;
69583
+ const profile = memory.command("profile").description("Author + inspect operator profile banks (shared/per-user memory; wire via memory.recall.additional_banks)");
69584
+ 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) => {
69585
+ const config = getConfig(program3);
69586
+ if (!bank || !VALID_BANK.test(bank)) {
69587
+ console.error(source_default.red("Bank name must be non-empty and contain only letters, digits, '.', '_', or '-'."));
69588
+ process.exit(1);
69589
+ }
69590
+ const content = factWords.join(" ").trim();
69591
+ if (content.length === 0) {
69592
+ console.error(source_default.red("A non-empty fact is required."));
69593
+ process.exit(1);
69594
+ }
69595
+ const base = restBase(config.memory?.config?.url);
69596
+ const url = `${base}/v1/default/banks/${encodeURIComponent(bank)}/memories`;
69597
+ const timeoutMs = Math.max(1000, parseInt(opts.timeout, 10) || 60000);
69598
+ const ctrl = new AbortController;
69599
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
69600
+ try {
69601
+ const res = await fetch(url, {
69602
+ method: "POST",
69603
+ headers: { "Content-Type": "application/json" },
69604
+ body: JSON.stringify({
69605
+ items: [{ content, tags: ["operator-authored", "profile"] }],
69606
+ async: false
69607
+ }),
69608
+ signal: ctrl.signal
69609
+ });
69610
+ if (!res.ok) {
69611
+ console.error(source_default.red(`\u2717 Retain failed: HTTP ${res.status}`), source_default.gray(await res.text().catch(() => "")));
69612
+ process.exit(1);
69613
+ }
69614
+ console.log(source_default.green(`\u2713 Added to profile bank "${bank}"`));
69615
+ console.log(source_default.gray(` ${content}`));
69616
+ console.log(source_default.gray(" (fact extraction runs in the background \u2014 `profile list` may lag a few seconds)"));
69617
+ console.log(source_default.gray(`
69618
+ Wire it into agents via memory.recall.additional_banks: ["${bank}"] in switchroom.yaml,
69619
+ then \`switchroom apply\` + restart the agent(s).`));
69620
+ } catch (e) {
69621
+ console.error(source_default.red("\u2717 Retain failed:"), source_default.gray(e instanceof Error ? e.message : String(e)));
69622
+ process.exit(1);
69623
+ } finally {
69624
+ clearTimeout(t);
69625
+ }
69626
+ }));
69627
+ 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) => {
69628
+ const config = getConfig(program3);
69629
+ if (!bank || !VALID_BANK.test(bank)) {
69630
+ console.error(source_default.red("Bank name must contain only letters, digits, '.', '_', or '-'."));
69631
+ process.exit(1);
69632
+ }
69633
+ const base = restBase(config.memory?.config?.url);
69634
+ const limit = Math.max(1, parseInt(opts.limit, 10) || 50);
69635
+ const url = `${base}/v1/default/banks/${encodeURIComponent(bank)}/memories/list?limit=${limit}`;
69636
+ const timeoutMs = Math.max(1000, parseInt(opts.timeout, 10) || 1e4);
69637
+ const ctrl = new AbortController;
69638
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
69639
+ try {
69640
+ const res = await fetch(url, { signal: ctrl.signal });
69641
+ if (!res.ok) {
69642
+ console.error(source_default.red(`\u2717 List failed: HTTP ${res.status}`), source_default.gray(await res.text().catch(() => "")));
69643
+ process.exit(1);
69644
+ }
69645
+ const data = await res.json();
69646
+ const units = data.results ?? data.memories ?? data.units ?? [];
69647
+ console.log(source_default.bold(`
69648
+ Profile bank "${bank}" \u2014 ${units.length} unit(s)
69649
+ `));
69650
+ for (const u of units) {
69651
+ const ft = (u.fact_type ?? "?").padEnd(11);
69652
+ const text = String(u.content ?? u.text ?? "").replace(/\s+/g, " ").slice(0, 100);
69653
+ console.log(` ${source_default.gray(ft)} ${text}`);
69654
+ }
69655
+ console.log();
69656
+ } catch (e) {
69657
+ console.error(source_default.red("\u2717 List failed:"), source_default.gray(e instanceof Error ? e.message : String(e)));
69658
+ process.exit(1);
69659
+ } finally {
69660
+ clearTimeout(t);
69661
+ }
69662
+ }));
69478
69663
  }
69479
69664
 
69480
69665
  // src/cli/web.ts
@@ -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
@@ -11299,7 +11299,7 @@ var init_dist = __esm(() => {
11299
11299
  });
11300
11300
 
11301
11301
  // src/config/schema.ts
11302
- 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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
11302
+ 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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
11303
11303
  var init_schema = __esm(() => {
11304
11304
  init_zod();
11305
11305
  CodeRepoEntrySchema = exports_external.object({
@@ -11420,6 +11420,8 @@ var init_schema = __esm(() => {
11420
11420
  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."),
11421
11421
  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`."),
11422
11422
  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)."),
11423
+ 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)."),
11424
+ 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`."),
11423
11425
  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."),
11424
11426
  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).")
11425
11427
  }).optional().describe("Auto-recall tuning knobs")
@@ -11633,8 +11635,12 @@ var init_schema = __esm(() => {
11633
11635
  message: "release.channel and release.pin are mutually exclusive"
11634
11636
  });
11635
11637
  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).");
11638
+ 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.");
11639
+ 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.");
11636
11640
  profileFields = {
11637
11641
  extends: exports_external.string().optional(),
11642
+ serves: servesField,
11643
+ knows: knowsField,
11638
11644
  bot_token: exports_external.string().optional(),
11639
11645
  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)."),
11640
11646
  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."),
@@ -11655,7 +11661,9 @@ var init_schema = __esm(() => {
11655
11661
  recall: exports_external.object({
11656
11662
  max_memories: exports_external.number().int().min(0).optional(),
11657
11663
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
11658
- min_overlap: exports_external.number().min(0).max(1).optional()
11664
+ min_overlap: exports_external.number().min(0).max(1).optional(),
11665
+ additional_banks: exports_external.array(exports_external.string()).optional(),
11666
+ sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional()
11659
11667
  }).optional()
11660
11668
  }).optional(),
11661
11669
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
@@ -11700,6 +11708,8 @@ var init_schema = __esm(() => {
11700
11708
  AgentDefaultsSchema = exports_external.object(defaultsFields).optional();
11701
11709
  AgentSchema = exports_external.object({
11702
11710
  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."),
11711
+ serves: servesField,
11712
+ knows: knowsField,
11703
11713
  bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
11704
11714
  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."),
11705
11715
  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')."),
@@ -11873,6 +11883,11 @@ var init_schema = __esm(() => {
11873
11883
  CronConfigSchema = exports_external.object({
11874
11884
  egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
11875
11885
  });
11886
+ UserSchema = exports_external.object({
11887
+ name: exports_external.string().optional().describe("Display name for the user."),
11888
+ 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."),
11889
+ profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`).")
11890
+ });
11876
11891
  SwitchroomConfigSchema = exports_external.object({
11877
11892
  switchroom: exports_external.object({
11878
11893
  version: exports_external.literal(1).describe("Config schema version"),
@@ -11919,10 +11934,31 @@ var init_schema = __esm(() => {
11919
11934
  })).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."),
11920
11935
  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."),
11921
11936
  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."),
11937
+ 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."),
11922
11938
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
11923
11939
  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)"
11924
11940
  }), AgentSchema).describe("Map of agent name to agent configuration"),
11925
11941
  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.")
11942
+ }).superRefine((cfg, ctx) => {
11943
+ const userKeys = new Set(Object.keys(cfg.users ?? {}));
11944
+ const checkServes = (serves, path) => {
11945
+ (serves ?? []).forEach((s, i) => {
11946
+ if (!userKeys.has(s)) {
11947
+ ctx.addIssue({
11948
+ code: exports_external.ZodIssueCode.custom,
11949
+ message: `serves references unknown user "${s}" — add it to the top-level ` + "`users:` block (or did you mean `knows` for a raw bank name?)",
11950
+ path: [...path, i]
11951
+ });
11952
+ }
11953
+ });
11954
+ };
11955
+ checkServes(cfg.defaults?.serves, ["defaults", "serves"]);
11956
+ for (const [name, p] of Object.entries(cfg.profiles ?? {})) {
11957
+ checkServes(p.serves, ["profiles", name, "serves"]);
11958
+ }
11959
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11960
+ checkServes(a.serves, ["agents", name, "serves"]);
11961
+ }
11926
11962
  });
11927
11963
  });
11928
11964