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.
- package/dist/agent-scheduler/index.js +37 -1
- package/dist/auth-broker/index.js +37 -1
- package/dist/cli/notion-write-pretool.mjs +37 -1
- package/dist/cli/switchroom.js +239 -38
- package/dist/cli/ui/index.html +1 -1
- package/dist/host-control/main.js +37 -1
- package/dist/vault/approvals/kernel-server.js +41 -2
- package/dist/vault/broker/server.js +41 -2
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +6 -0
- package/profiles/default/CLAUDE.md.hbs +3 -3
- package/telegram-plugin/dist/gateway/gateway.js +43 -7
- package/vendor/hindsight-memory/scripts/lib/gateway_ipc.py +29 -0
- package/vendor/hindsight-memory/scripts/recall.py +65 -2
- package/vendor/hindsight-memory/scripts/tests/test_sender_routing.py +127 -0
|
@@ -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
|
|
|
@@ -13097,6 +13133,9 @@ function getNonce(db, request_id) {
|
|
|
13097
13133
|
import { createHash } from "node:crypto";
|
|
13098
13134
|
init_merge();
|
|
13099
13135
|
|
|
13136
|
+
// src/config/users.ts
|
|
13137
|
+
init_merge();
|
|
13138
|
+
|
|
13100
13139
|
// src/memory/hindsight.ts
|
|
13101
13140
|
var 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.";
|
|
13102
13141
|
|
|
@@ -11299,7 +11299,7 @@ var init_zod = __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
|
|
|
@@ -12641,6 +12677,9 @@ function identify(socketPath, socket, execFileSyncOverride) {
|
|
|
12641
12677
|
return { uid, pid, exe, systemdUnit };
|
|
12642
12678
|
}
|
|
12643
12679
|
|
|
12680
|
+
// src/config/users.ts
|
|
12681
|
+
init_merge();
|
|
12682
|
+
|
|
12644
12683
|
// src/memory/hindsight.ts
|
|
12645
12684
|
var 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.";
|
|
12646
12685
|
|
package/package.json
CHANGED
|
@@ -355,6 +355,12 @@ export HINDSIGHT_RECALL_SKIP_TRIVIAL={{hindsightRecallSkipTrivial}}
|
|
|
355
355
|
{{#if hindsightTopicAliasesJsonQ}}
|
|
356
356
|
export HINDSIGHT_TOPIC_ALIASES_JSON={{{hindsightTopicAliasesJsonQ}}}
|
|
357
357
|
{{/if}}
|
|
358
|
+
# Switchroom (per-speaker memory routing): {sender: bank} map so recall.py
|
|
359
|
+
# routes recall to the speaker's profile bank. Emitted only when the agent
|
|
360
|
+
# has a memory.recall.sender_banks map; absent for single-user agents.
|
|
361
|
+
{{#if hindsightSenderBanksJsonQ}}
|
|
362
|
+
export HINDSIGHT_SENDER_BANKS_JSON={{{hindsightSenderBanksJsonQ}}}
|
|
363
|
+
{{/if}}
|
|
358
364
|
# PR6 — topic filter mode for cross-topic memory recall. Default
|
|
359
365
|
# "soft-preamble": all topic-tagged memories surface and the model
|
|
360
366
|
# decides relevance via the preamble. "hard-filter": drop memories
|
|
@@ -61,10 +61,10 @@ Hindsight is a memory bank with semantic search, knowledge graph, entity resolut
|
|
|
61
61
|
- `mcp__hindsight__retain` — store a new memory. The plugin automatically retains the conversation transcript every ~10 turns via the Stop hook, so you usually don't need this. Call manually for significant decisions, corrections, or facts you want immediately searchable.
|
|
62
62
|
- `mcp__hindsight__reflect` — Hindsight's LLM-powered "answer this query using the bank's content + directives". Use when the user asks a question that requires synthesis across multiple past memories.
|
|
63
63
|
|
|
64
|
-
### Mental Models
|
|
65
|
-
A mental model is a pre-computed semantic summary backed by reflection over the bank
|
|
64
|
+
### Mental Models
|
|
65
|
+
A mental model is a pre-computed semantic summary backed by reflection over the bank — a way to maintain a standing answer to a recurring question, semantically populated and refreshed.
|
|
66
66
|
|
|
67
|
-
- `mcp__hindsight__create_mental_model(name, source_query)` — create one. When the user shares a fact about themselves (preferences, background, goals), don't write a file —
|
|
67
|
+
- `mcp__hindsight__create_mental_model(name, source_query)` — create one for a recurring synthesis you need. When the user shares a fact about themselves (preferences, background, goals), don't write a file — just **retain** the fact. You do NOT need to build or maintain a per-agent "user profile": who the user is lives in dedicated per-user profile banks that the operator curates out-of-band, and recall surfaces it automatically.
|
|
68
68
|
|
|
69
69
|
### Directives (replaces feedback rules)
|
|
70
70
|
Hard rules the agent must follow during reflect — guardrails that are always applied.
|
|
@@ -23802,7 +23802,7 @@ var init_dist = __esm(() => {
|
|
|
23802
23802
|
});
|
|
23803
23803
|
|
|
23804
23804
|
// ../src/config/schema.ts
|
|
23805
|
-
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;
|
|
23805
|
+
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;
|
|
23806
23806
|
var init_schema = __esm(() => {
|
|
23807
23807
|
init_zod();
|
|
23808
23808
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -23923,6 +23923,8 @@ var init_schema = __esm(() => {
|
|
|
23923
23923
|
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."),
|
|
23924
23924
|
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`."),
|
|
23925
23925
|
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)."),
|
|
23926
|
+
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)."),
|
|
23927
|
+
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`."),
|
|
23926
23928
|
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."),
|
|
23927
23929
|
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).")
|
|
23928
23930
|
}).optional().describe("Auto-recall tuning knobs")
|
|
@@ -24136,8 +24138,12 @@ var init_schema = __esm(() => {
|
|
|
24136
24138
|
message: "release.channel and release.pin are mutually exclusive"
|
|
24137
24139
|
});
|
|
24138
24140
|
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).");
|
|
24141
|
+
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.");
|
|
24142
|
+
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.");
|
|
24139
24143
|
profileFields = {
|
|
24140
24144
|
extends: exports_external.string().optional(),
|
|
24145
|
+
serves: servesField,
|
|
24146
|
+
knows: knowsField,
|
|
24141
24147
|
bot_token: exports_external.string().optional(),
|
|
24142
24148
|
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)."),
|
|
24143
24149
|
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."),
|
|
@@ -24158,7 +24164,9 @@ var init_schema = __esm(() => {
|
|
|
24158
24164
|
recall: exports_external.object({
|
|
24159
24165
|
max_memories: exports_external.number().int().min(0).optional(),
|
|
24160
24166
|
cache_ttl_secs: exports_external.number().int().min(0).optional(),
|
|
24161
|
-
min_overlap: exports_external.number().min(0).max(1).optional()
|
|
24167
|
+
min_overlap: exports_external.number().min(0).max(1).optional(),
|
|
24168
|
+
additional_banks: exports_external.array(exports_external.string()).optional(),
|
|
24169
|
+
sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
24162
24170
|
}).optional()
|
|
24163
24171
|
}).optional(),
|
|
24164
24172
|
schedule: exports_external.array(ScheduleEntrySchema).optional(),
|
|
@@ -24203,6 +24211,8 @@ var init_schema = __esm(() => {
|
|
|
24203
24211
|
AgentDefaultsSchema = exports_external.object(defaultsFields).optional();
|
|
24204
24212
|
AgentSchema = exports_external.object({
|
|
24205
24213
|
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."),
|
|
24214
|
+
serves: servesField,
|
|
24215
|
+
knows: knowsField,
|
|
24206
24216
|
bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
|
|
24207
24217
|
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."),
|
|
24208
24218
|
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')."),
|
|
@@ -24376,6 +24386,11 @@ var init_schema = __esm(() => {
|
|
|
24376
24386
|
CronConfigSchema = exports_external.object({
|
|
24377
24387
|
egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
|
|
24378
24388
|
});
|
|
24389
|
+
UserSchema = exports_external.object({
|
|
24390
|
+
name: exports_external.string().optional().describe("Display name for the user."),
|
|
24391
|
+
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."),
|
|
24392
|
+
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`).")
|
|
24393
|
+
});
|
|
24379
24394
|
SwitchroomConfigSchema = exports_external.object({
|
|
24380
24395
|
switchroom: exports_external.object({
|
|
24381
24396
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -24422,10 +24437,31 @@ var init_schema = __esm(() => {
|
|
|
24422
24437
|
})).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."),
|
|
24423
24438
|
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."),
|
|
24424
24439
|
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."),
|
|
24440
|
+
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."),
|
|
24425
24441
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
24426
24442
|
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)"
|
|
24427
24443
|
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
24428
24444
|
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.")
|
|
24445
|
+
}).superRefine((cfg, ctx) => {
|
|
24446
|
+
const userKeys = new Set(Object.keys(cfg.users ?? {}));
|
|
24447
|
+
const checkServes = (serves, path) => {
|
|
24448
|
+
(serves ?? []).forEach((s, i) => {
|
|
24449
|
+
if (!userKeys.has(s)) {
|
|
24450
|
+
ctx.addIssue({
|
|
24451
|
+
code: exports_external.ZodIssueCode.custom,
|
|
24452
|
+
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?)",
|
|
24453
|
+
path: [...path, i]
|
|
24454
|
+
});
|
|
24455
|
+
}
|
|
24456
|
+
});
|
|
24457
|
+
};
|
|
24458
|
+
checkServes(cfg.defaults?.serves, ["defaults", "serves"]);
|
|
24459
|
+
for (const [name, p] of Object.entries(cfg.profiles ?? {})) {
|
|
24460
|
+
checkServes(p.serves, ["profiles", name, "serves"]);
|
|
24461
|
+
}
|
|
24462
|
+
for (const [name, a] of Object.entries(cfg.agents ?? {})) {
|
|
24463
|
+
checkServes(a.serves, ["agents", name, "serves"]);
|
|
24464
|
+
}
|
|
24429
24465
|
});
|
|
24430
24466
|
});
|
|
24431
24467
|
|
|
@@ -54595,11 +54631,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
54595
54631
|
}
|
|
54596
54632
|
|
|
54597
54633
|
// ../src/build-info.ts
|
|
54598
|
-
var VERSION = "0.15.
|
|
54599
|
-
var COMMIT_SHA = "
|
|
54600
|
-
var COMMIT_DATE = "2026-06-
|
|
54601
|
-
var LATEST_PR =
|
|
54602
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
54634
|
+
var VERSION = "0.15.45";
|
|
54635
|
+
var COMMIT_SHA = "41082e8b";
|
|
54636
|
+
var COMMIT_DATE = "2026-06-19T09:03:39Z";
|
|
54637
|
+
var LATEST_PR = 2448;
|
|
54638
|
+
var COMMITS_AHEAD_OF_TAG = 0;
|
|
54603
54639
|
|
|
54604
54640
|
// gateway/boot-version.ts
|
|
54605
54641
|
function formatRelativeAgo(iso) {
|
|
@@ -89,6 +89,35 @@ def extract_topic_from_prompt(
|
|
|
89
89
|
return chat_id, thread_id
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
# Switchroom (per-speaker memory routing, RFC
|
|
93
|
+
# reference/rfcs/per-speaker-memory-routing.md): extract the sender identity
|
|
94
|
+
# (`user=` attribute) from the channel envelope. The gateway emits
|
|
95
|
+
# `user = from.username ?? String(from.id)`, so this is a username when the
|
|
96
|
+
# sender has one, else their numeric Telegram user id. Used to route recall to
|
|
97
|
+
# the speaker's profile bank — additive recall scoping, never an auth boundary.
|
|
98
|
+
_USER_RE = re.compile(
|
|
99
|
+
r"<channel\b[^>]*\buser=[\"']([^\"']+)[\"']",
|
|
100
|
+
re.IGNORECASE,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def extract_user_from_prompt(prompt: str) -> Optional[str]:
|
|
105
|
+
"""Pull the sender `user` out of the `<channel ...>` envelope.
|
|
106
|
+
|
|
107
|
+
Returns None when the prompt isn't channel-wrapped (interactive
|
|
108
|
+
sessions, non-Telegram channels, test fixtures). The value is the
|
|
109
|
+
sender's username when set, else their numeric Telegram user id.
|
|
110
|
+
"""
|
|
111
|
+
if not prompt or not isinstance(prompt, str):
|
|
112
|
+
return None
|
|
113
|
+
head = prompt[:1024]
|
|
114
|
+
match = _USER_RE.search(head)
|
|
115
|
+
if not match:
|
|
116
|
+
return None
|
|
117
|
+
user = match.group(1).strip()
|
|
118
|
+
return user or None
|
|
119
|
+
|
|
120
|
+
|
|
92
121
|
def gateway_socket_path() -> Optional[str]:
|
|
93
122
|
"""Resolve the gateway socket path for the current agent.
|
|
94
123
|
|
|
@@ -59,7 +59,7 @@ from lib.content import (
|
|
|
59
59
|
)
|
|
60
60
|
from lib.daemon import get_api_url
|
|
61
61
|
from lib.directives import fetch_active_directives, format_active_directives_block
|
|
62
|
-
from lib.gateway_ipc import extract_chat_id_from_prompt, extract_topic_from_prompt, update_placeholder
|
|
62
|
+
from lib.gateway_ipc import extract_chat_id_from_prompt, extract_topic_from_prompt, extract_user_from_prompt, update_placeholder
|
|
63
63
|
from lib.state import read_state, write_state
|
|
64
64
|
|
|
65
65
|
LAST_RECALL_STATE = "last_recall.json"
|
|
@@ -188,12 +188,54 @@ def _cache_ttl_secs() -> int:
|
|
|
188
188
|
return 0
|
|
189
189
|
|
|
190
190
|
|
|
191
|
+
def _normalize_sender(sender: str) -> str:
|
|
192
|
+
"""Drop a single leading '@'. The gateway emits a bare username
|
|
193
|
+
(`from.username`), but operators naturally write `@handle` in config —
|
|
194
|
+
normalizing both sides lets either form match."""
|
|
195
|
+
return sender[1:] if sender.startswith("@") else sender
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _resolve_sender_bank(
|
|
199
|
+
sender_banks_json: str,
|
|
200
|
+
active_sender: str | None,
|
|
201
|
+
bank_id: str,
|
|
202
|
+
additional_banks: list,
|
|
203
|
+
) -> list:
|
|
204
|
+
"""Per-speaker memory routing: if `active_sender` maps to a bank in the
|
|
205
|
+
HINDSIGHT_SENDER_BANKS_JSON map, return ``additional_banks`` with that
|
|
206
|
+
bank appended (additive — skips dup/self). A leading '@' on either the
|
|
207
|
+
map keys or the sender is normalized away, so ``@lisa``, ``lisa``, and
|
|
208
|
+
the gateway's bare-emitted ``lisa`` all resolve together. Failure-safe:
|
|
209
|
+
any bad input (missing sender/env, non-dict JSON, decode error) returns
|
|
210
|
+
``additional_banks`` unchanged. Never replaces the agent's own bank and
|
|
211
|
+
never touches auth — additive recall scoping only (single-tenant).
|
|
212
|
+
"""
|
|
213
|
+
if not active_sender or not sender_banks_json:
|
|
214
|
+
return additional_banks
|
|
215
|
+
try:
|
|
216
|
+
sender_banks = json.loads(sender_banks_json)
|
|
217
|
+
except (json.JSONDecodeError, ValueError, TypeError):
|
|
218
|
+
return additional_banks
|
|
219
|
+
if not isinstance(sender_banks, dict):
|
|
220
|
+
return additional_banks
|
|
221
|
+
normalized = {_normalize_sender(str(k)): v for k, v in sender_banks.items()}
|
|
222
|
+
sender_bank = normalized.get(_normalize_sender(active_sender))
|
|
223
|
+
if (
|
|
224
|
+
sender_bank
|
|
225
|
+
and sender_bank != bank_id
|
|
226
|
+
and sender_bank not in additional_banks
|
|
227
|
+
):
|
|
228
|
+
return [*additional_banks, sender_bank]
|
|
229
|
+
return additional_banks
|
|
230
|
+
|
|
231
|
+
|
|
191
232
|
def _cache_key(
|
|
192
233
|
session_id: str,
|
|
193
234
|
prompt: str,
|
|
194
235
|
bank_id: str,
|
|
195
236
|
extra_banks: list,
|
|
196
237
|
active_thread_id: str | None = None,
|
|
238
|
+
active_sender: str | None = None,
|
|
197
239
|
) -> str:
|
|
198
240
|
"""Stable hash for cache keying. Session_id is included so a new
|
|
199
241
|
session always misses, regardless of the TTL setting. Extra banks
|
|
@@ -204,6 +246,11 @@ def _cache_key(
|
|
|
204
246
|
but different topic) don't collide on the cache. Empty/None
|
|
205
247
|
collapses to the empty string — backward-compatible for
|
|
206
248
|
fleet-shared / DM agents where no thread_id is present.
|
|
249
|
+
|
|
250
|
+
Switchroom (per-speaker routing): `active_sender` is included for the
|
|
251
|
+
same reason — in a multi-user session two speakers sending the same
|
|
252
|
+
prompt resolve to different recall banks, so the sender must be part of
|
|
253
|
+
the key or one speaker's recall would be served to the other.
|
|
207
254
|
"""
|
|
208
255
|
parts = [
|
|
209
256
|
session_id or "",
|
|
@@ -211,6 +258,7 @@ def _cache_key(
|
|
|
211
258
|
bank_id or "",
|
|
212
259
|
",".join(sorted(extra_banks or [])),
|
|
213
260
|
active_thread_id or "",
|
|
261
|
+
active_sender or "",
|
|
214
262
|
]
|
|
215
263
|
payload = "\x1f".join(parts)
|
|
216
264
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
@@ -640,11 +688,26 @@ def main():
|
|
|
640
688
|
bank_id = derive_bank_id(hook_input, config)
|
|
641
689
|
additional_banks = config.get("recallAdditionalBanks", []) or []
|
|
642
690
|
|
|
691
|
+
# Switchroom (per-speaker memory routing, RFC
|
|
692
|
+
# reference/rfcs/per-speaker-memory-routing.md): when this agent serves
|
|
693
|
+
# multiple trusted users, also recall the *speaker's* profile bank. The
|
|
694
|
+
# sender is in the `<channel user="...">` envelope; the sender→bank map is
|
|
695
|
+
# injected as HINDSIGHT_SENDER_BANKS_JSON ({"<username-or-id>": "<bank>"}).
|
|
696
|
+
# Additive — never replaces the agent's own bank, never an auth boundary
|
|
697
|
+
# (single-tenant). Failure-safe + silent on every error path.
|
|
698
|
+
active_sender = extract_user_from_prompt(prompt)
|
|
699
|
+
additional_banks = _resolve_sender_bank(
|
|
700
|
+
os.environ.get("HINDSIGHT_SENDER_BANKS_JSON", ""),
|
|
701
|
+
active_sender,
|
|
702
|
+
bank_id,
|
|
703
|
+
additional_banks,
|
|
704
|
+
)
|
|
705
|
+
|
|
643
706
|
# Switchroom #424 phase 4.1 — cache check BEFORE any HTTP traffic.
|
|
644
707
|
# Whole-session-scoped, opt-in via HINDSIGHT_RECALL_CACHE_TTL_SECS.
|
|
645
708
|
cache_ttl = _cache_ttl_secs()
|
|
646
709
|
cache_key = (
|
|
647
|
-
_cache_key(session_id, prompt, bank_id, additional_banks, active_thread_id)
|
|
710
|
+
_cache_key(session_id, prompt, bank_id, additional_banks, active_thread_id, active_sender)
|
|
648
711
|
if cache_ttl > 0
|
|
649
712
|
else ""
|
|
650
713
|
)
|