switchroom 0.20.10 → 0.20.12

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.
Files changed (76) hide show
  1. package/dist/agent-scheduler/index.js +68 -3
  2. package/dist/auth-broker/index.js +211 -46
  3. package/dist/cli/notion-write-pretool.mjs +68 -3
  4. package/dist/cli/self-improve-apply-guard-pretool.mjs +357 -92
  5. package/dist/cli/self-improve-stop.mjs +889 -7
  6. package/dist/cli/skill-validate-pretool.mjs +82 -3
  7. package/dist/cli/switchroom.js +5063 -2961
  8. package/dist/host-control/main.js +93 -27
  9. package/dist/vault/approvals/kernel-server.js +92 -26
  10. package/dist/vault/broker/server.js +92 -26
  11. package/examples/personal-google-workspace-mcp/compose.yaml +1 -1
  12. package/package.json +1 -1
  13. package/profiles/_shared/agent-self-service.md.hbs +15 -22
  14. package/profiles/_shared/delegation-golden-rule.md.hbs +1 -1
  15. package/profiles/_shared/dev-protocol.md.hbs +1 -1
  16. package/profiles/_shared/execution-discipline.md.hbs +4 -4
  17. package/profiles/_shared/vault-protocol.md.hbs +2 -18
  18. package/profiles/default/CLAUDE.md.hbs +3 -5
  19. package/skills/switchroom-architecture/telegram.md +0 -1
  20. package/skills/switchroom-cli/SKILL.md +0 -1
  21. package/telegram-plugin/README.md +2 -11
  22. package/telegram-plugin/auto-fallback-fleet.ts +37 -2
  23. package/telegram-plugin/bridge/bridge.ts +0 -12
  24. package/telegram-plugin/chat-lock.ts +1 -1
  25. package/telegram-plugin/dist/bridge/bridge.js +0 -12
  26. package/telegram-plugin/dist/gateway/gateway.js +1454 -976
  27. package/telegram-plugin/dist/server.js +0 -12
  28. package/telegram-plugin/fallback-card-collapse.ts +1 -0
  29. package/telegram-plugin/gateway/auth-command.ts +11 -1
  30. package/telegram-plugin/gateway/callback-query-handlers.ts +100 -0
  31. package/telegram-plugin/gateway/eval-case-proposal-card.ts +86 -0
  32. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.test.ts +74 -0
  33. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.ts +71 -0
  34. package/telegram-plugin/gateway/gateway.ts +109 -149
  35. package/telegram-plugin/gateway/ipc-protocol.ts +43 -0
  36. package/telegram-plugin/gateway/ipc-server.ts +28 -0
  37. package/telegram-plugin/gateway/liveness-wiring.ts +6 -1
  38. package/telegram-plugin/gateway/narrative-lane.ts +33 -2
  39. package/telegram-plugin/gateway/privacy-reset.test.ts +216 -0
  40. package/telegram-plugin/gateway/privacy-reset.ts +87 -0
  41. package/telegram-plugin/gateway/privacy-state.test.ts +165 -0
  42. package/telegram-plugin/gateway/privacy-state.ts +206 -0
  43. package/telegram-plugin/gateway/self-improve-proposal-wiring.ts +176 -0
  44. package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +24 -14
  45. package/telegram-plugin/gateway/stale-pin-sweep.test.ts +123 -26
  46. package/telegram-plugin/gateway/stale-pin-sweep.ts +52 -35
  47. package/telegram-plugin/gateway/status-pin-store.ts +10 -9
  48. package/telegram-plugin/gateway/stream-render.ts +4 -4
  49. package/telegram-plugin/gateway/throttle-tier-wiring.ts +15 -4
  50. package/telegram-plugin/gateway/turn-record-status.ts +32 -1
  51. package/telegram-plugin/hooks/hooks.json +13 -12
  52. package/telegram-plugin/hooks/narration-classify.mjs +1 -2
  53. package/telegram-plugin/hooks/silent-end-scan.mjs +1 -1
  54. package/telegram-plugin/slot-banner-driver.ts +42 -5
  55. package/telegram-plugin/status-pin.ts +2 -5
  56. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +24 -0
  57. package/telegram-plugin/tests/backstop-exactly-once.test.ts +8 -2
  58. package/telegram-plugin/tests/framework-fallback-duration-guard.test.ts +125 -0
  59. package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +2 -0
  60. package/telegram-plugin/tests/narrative-lane-golden.test.ts +97 -0
  61. package/telegram-plugin/tests/pin-message-tool-retired.test.ts +64 -0
  62. package/telegram-plugin/tests/privacy-reset-call-sites.test.ts +120 -0
  63. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +38 -0
  64. package/telegram-plugin/tests/status-pin-store.test.ts +25 -0
  65. package/telegram-plugin/tests/throttle-tier.test.ts +16 -0
  66. package/telegram-plugin/tests/turn-flush-safety.test.ts +67 -0
  67. package/telegram-plugin/tests/worker-activity-feed.test.ts +40 -1
  68. package/telegram-plugin/throttle-tier.ts +12 -3
  69. package/telegram-plugin/turn-flush-safety.ts +97 -0
  70. package/telegram-plugin/worker-activity-feed.ts +1 -1
  71. package/vendor/hindsight-memory/scripts/recall.py +140 -0
  72. package/vendor/hindsight-memory/scripts/retain.py +306 -0
  73. package/vendor/hindsight-memory/scripts/subagent_retain.py +29 -1
  74. package/vendor/hindsight-memory/scripts/tests/test_private_mode.py +415 -0
  75. package/vendor/hindsight-memory/scripts/tests/test_recall_latency_instrumentation.py +277 -0
  76. package/vendor/hindsight-memory/scripts/tests/test_self_improve_correction_tag.py +167 -0
@@ -10994,7 +10994,7 @@ var init_observation_scopes = __esm(() => {
10994
10994
  });
10995
10995
 
10996
10996
  // src/config/schema.ts
10997
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
10997
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
10998
10998
  var init_schema = __esm(() => {
10999
10999
  init_zod();
11000
11000
  init_observation_scopes();
@@ -11444,7 +11444,7 @@ var init_schema = __esm(() => {
11444
11444
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
11445
11445
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
11446
11446
  }).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
11447
- env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS — switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS — only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS — the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS — the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY — the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE — the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT — upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT — the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S — the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES — the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS — the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
11447
+ env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS — switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS — only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS — the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS — the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY — the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE — the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT — upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT — the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S — the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES — the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS — the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan); and " + "HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE — the Postgres " + "text-search regconfig the native BM25 arms and the search_vector index " + "write run to_tsvector/to_tsquery against; unset means upstream's " + "always-safe `english`, set `hindsight_english` to use switchroom's " + "junk-stopping config (dict + stopword file provisioned durably by the " + "image entrypoint, recall degrading to semantic-only if it is ever " + "missing); plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
11448
11448
  });
11449
11449
  MicrosoftWorkspaceConfigSchema = exports_external.object({
11450
11450
  microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
@@ -11691,8 +11691,10 @@ var init_schema = __esm(() => {
11691
11691
  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')."),
11692
11692
  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("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
11693
11693
  auth: exports_external.object({
11694
- override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
11695
- }).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
11694
+ override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5."),
11695
+ strict: exports_external.boolean().optional().describe("Requires `override`. When true, the pin is a hard binding, not a " + "routing preference: the broker NEVER serves this agent from any " + "other account — no failover to `auth.fallback_order` while the " + "pinned account is quota-walled or exhausted. The agent rides out " + "the wall on its own account (surfacing the normal 429/quota " + "cards) instead of silently borrowing fleet quota. Use for " + "accounts that must never cross a billing/compliance boundary " + "(e.g. an employer-provided subscription)."),
11696
+ exclusive: exports_external.boolean().optional().describe("Requires `override`. When true, the pinned account belongs to " + "THIS agent alone: the broker refuses to serve it to any other " + "agent or consumer, refuses `auth use <label>` (set-active) onto " + "it, and refuses pinning another agent to it. Config that routes " + "others to the account (`auth.active`, `auth.fallback_order`, " + "another agent's `override`, a consumer pin) is rejected at load. " + "Usually paired with `strict: true` for full two-way isolation.")
11697
+ }).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases, " + "with optional `strict:` (never borrow another account) and " + "`exclusive:` (no one else may use the pinned account) hardening. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
11696
11698
  dm_only: exports_external.boolean().optional().describe("Mark this agent as a DM-only bot — has its own bot_token and lives " + "exclusively in a private chat with the operator. Suppresses " + "scaffolding's default behavior of inheriting the global " + "telegram.forum_chat_id into the agent's access.json `groups` entry " + "(the forum chat the bot isn't a member of, which would otherwise " + "trigger a 'boot-probe-failed: 400 chat not found' warning every " + "restart). topic_name is still schema-required but unused — set it " + "to a display label like 'DM' for /switchroom status output."),
11697
11699
  topic_name: exports_external.string().describe("Telegram forum topic display name"),
11698
11700
  topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
@@ -11767,6 +11769,17 @@ var init_schema = __esm(() => {
11767
11769
  tmp_size: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "tmp_size must be a Docker size string like '1g', '4g', '512m'").refine((v) => parseFloat(v) > 0, "tmp_size must be greater than zero").optional()
11768
11770
  }).optional()
11769
11771
  }).superRefine((agent, ctx) => {
11772
+ if (agent.auth && !agent.auth.override) {
11773
+ for (const flag of ["strict", "exclusive"]) {
11774
+ if (agent.auth[flag]) {
11775
+ ctx.addIssue({
11776
+ code: exports_external.ZodIssueCode.custom,
11777
+ message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` — there is no pinned account for it to apply to.`,
11778
+ path: ["auth", flag]
11779
+ });
11780
+ }
11781
+ }
11782
+ }
11770
11783
  if (agent.dm_only !== true)
11771
11784
  return;
11772
11785
  const tg = agent.channels?.telegram;
@@ -11879,6 +11892,15 @@ var init_schema = __esm(() => {
11879
11892
  profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
11880
11893
  person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
11881
11894
  });
11895
+ ConfigRepoConfigSchema = exports_external.object({
11896
+ enabled: exports_external.boolean().default(false).describe("Master switch for config-repo change control. Default false (opt-in). " + "When false the `config-repo sync` verb still runs on demand but the " + "doctor check and (later) the scheduled tick treat the feature as off."),
11897
+ path: exports_external.string().regex(/^[a-zA-Z0-9~._\-/]+$/, "config_repo.path must not contain shell-special characters ($, `, \", ', \\, etc.)").default("~/.switchroom-config").describe("Filesystem path to the operator's private config repo. Must be a git " + "repo (else `switchroom doctor` FAILs). Tilde-expanded at read time."),
11898
+ push: exports_external.boolean().default(true).describe("When true, `config-repo sync` pushes after committing (subject to the " + "`require_private` gate). When false, it commits locally only — useful " + "for offline hosts or a review-before-push workflow."),
11899
+ remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
11900
+ require_private: exports_external.boolean().default(true).describe("Refuse to PUSH unless the GitHub API confirms the remote repo is " + "private. On a public remote — or when the API is unreachable — the " + "push is skipped (commits still land locally) and a WARN is emitted. " + "Fail-safe against exfiltrating memory files / workspace state to a " + "repo that has been flipped public. Default true."),
11901
+ interval_minutes: exports_external.number().int().min(5).max(59).default(30).describe("Cadence (minutes) of the scheduled `config-repo sync` tick installed " + "by `switchroom config-repo --install-cron`. Rendered as a cron " + "`*/N * * * *` minute field, so it must be 5..59 (the 5-min floor " + "matches the rest of the fleet's cron floor). Default 30 (Ken's " + "approved cadence). Changing it takes effect on the next " + "`--install-cron` / `switchroom update` reconcile."),
11902
+ include_vault_backup: exports_external.enum(["off", "daily", "every_tick"]).default("daily").describe("Whether the scheduled tick also runs `switchroom vault backup` " + "(an encrypted vault/memory snapshot into <path>/vault-backups/) " + "before the sync. `off`: sync only. `daily`: an extra once-per-day " + "cron leg runs `vault backup && config-repo sync` (the 30-min legs " + "stay sync-only). `every_tick`: every tick runs the backup first. " + "Default `daily` — the first automated vault backup on this host.")
11903
+ });
11882
11904
  SwitchroomConfigSchema = exports_external.object({
11883
11905
  switchroom: exports_external.object({
11884
11906
  version: exports_external.literal(1).describe("Config schema version"),
@@ -11891,6 +11913,7 @@ var init_schema = __esm(() => {
11891
11913
  memory: MemoryBackendConfigSchema.optional(),
11892
11914
  hindsight: HindsightConfigSchema.optional().describe("Fleet-singleton Hindsight (memory backend) configuration. Currently " + "just the LLM knob (provider + model) used for retain/reflect/" + "consolidation. Both fields optional; when unset the container falls " + "back to provider=claude-code + the hard-coded HINDSIGHT_DEFAULT_MODEL " + "so nothing changes for operators who don't set it. Read at container " + "launch by startHindsight() (src/setup/hindsight.ts) — takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
11893
11915
  vault: VaultConfigSchema.optional(),
11916
+ config_repo: ConfigRepoConfigSchema.optional().describe("Change control for the operator's private ~/.switchroom-config git repo " + "(backup + audit trail of live host config, agent workspace state, and " + "mirrored personal skills). Consumed by `switchroom config-repo sync` and " + "the `config repo` doctor check. Optional and OFF by default."),
11894
11917
  auth: exports_external.object({
11895
11918
  active: exports_external.string().min(1).optional().describe("Fleet-wide active Anthropic account label. Every agent without " + "an explicit `agent.auth.override` uses this account. See " + "docs/auth.md for the full model. Set by `switchroom auth use <label>`."),
11896
11919
  fallback_order: exports_external.array(exports_external.string().min(1)).optional().describe("Ordered list of account labels for `switchroom auth rotate` to cycle " + "through when the active account hits a quota event. First entry is " + "normally the same as `auth.active`. When unset, `rotate` is a no-op."),
@@ -11958,6 +11981,48 @@ var init_schema = __esm(() => {
11958
11981
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11959
11982
  checkServes(a.serves, ["agents", name, "serves"]);
11960
11983
  }
11984
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11985
+ const agentAuth = a.auth;
11986
+ if (!agentAuth?.exclusive || !agentAuth.override)
11987
+ continue;
11988
+ const acct = agentAuth.override;
11989
+ if (cfg.auth?.active === acct) {
11990
+ ctx.addIssue({
11991
+ code: exports_external.ZodIssueCode.custom,
11992
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot be the fleet \`auth.active\``,
11993
+ path: ["auth", "active"]
11994
+ });
11995
+ }
11996
+ const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
11997
+ if (fbIdx !== -1) {
11998
+ ctx.addIssue({
11999
+ code: exports_external.ZodIssueCode.custom,
12000
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot appear in \`auth.fallback_order\``,
12001
+ path: ["auth", "fallback_order", fbIdx]
12002
+ });
12003
+ }
12004
+ for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
12005
+ if (other === name)
12006
+ continue;
12007
+ const otherOverride = oa.auth?.override;
12008
+ if (otherOverride === acct) {
12009
+ ctx.addIssue({
12010
+ code: exports_external.ZodIssueCode.custom,
12011
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — agent '${other}' cannot pin it`,
12012
+ path: ["agents", other, "auth", "override"]
12013
+ });
12014
+ }
12015
+ }
12016
+ (cfg.auth?.consumers ?? []).forEach((c, i) => {
12017
+ if (c.account === acct) {
12018
+ ctx.addIssue({
12019
+ code: exports_external.ZodIssueCode.custom,
12020
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — consumer '${c.name}' cannot pin it`,
12021
+ path: ["auth", "consumers", i, "account"]
12022
+ });
12023
+ }
12024
+ });
12025
+ }
11961
12026
  const microsoftAccounts = cfg.microsoft_accounts;
11962
12027
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11963
12028
  const mw = a.microsoft_workspace;
@@ -18957,6 +19022,27 @@ function allocateAgentUid(name) {
18957
19022
  return AGENT_UID_MIN + u32 % range;
18958
19023
  }
18959
19024
 
19025
+ // src/setup/hindsight-recall-passthrough.ts
19026
+ var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
19027
+ var HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT = "Relevant memories from past conversations (prioritize recent when " + "conflicting). Only use memories that are directly useful to continue " + "this conversation; ignore the rest:";
19028
+ var RECALL_PASSTHROUGH_DEFAULTS = Object.freeze({
19029
+ budget: "low",
19030
+ maxTokens: 1024,
19031
+ preferObservations: true,
19032
+ contextTurns: 2,
19033
+ roles: ["user", "assistant"],
19034
+ promptPreamble: HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT,
19035
+ tags: [],
19036
+ tagsMatch: "any",
19037
+ tagGroups: {},
19038
+ tagWeights: HINDSIGHT_RECALL_TAG_WEIGHT_SEED,
19039
+ additionalBankFilters: {},
19040
+ transcriptFallback: true,
19041
+ transcriptTailBytes: 262144,
19042
+ maxQueryChars: 800,
19043
+ parallel: true
19044
+ });
19045
+
18960
19046
  // src/setup/hindsight-recall-tunables.ts
18961
19047
  var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
18962
19048
  var MIN_RECALL_HOOK_TIMEOUT_SECONDS = RECALL_DEADLINE_HEADROOM_SECONDS + 1;
@@ -19232,7 +19318,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
19232
19318
  "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
19233
19319
  "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S",
19234
19320
  "HINDSIGHT_API_TEMPORAL_LANGUAGES",
19235
- "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS"
19321
+ "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS",
19322
+ "HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE"
19236
19323
  ]);
19237
19324
  var HINDSIGHT_WORKER_SLOT_TYPES = [
19238
19325
  "consolidation",
@@ -19677,27 +19764,6 @@ init_vault();
19677
19764
  // src/setup/onboarding.ts
19678
19765
  init_paths();
19679
19766
 
19680
- // src/setup/hindsight-recall-passthrough.ts
19681
- var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
19682
- var HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT = "Relevant memories from past conversations (prioritize recent when " + "conflicting). Only use memories that are directly useful to continue " + "this conversation; ignore the rest:";
19683
- var RECALL_PASSTHROUGH_DEFAULTS = Object.freeze({
19684
- budget: "low",
19685
- maxTokens: 1024,
19686
- preferObservations: true,
19687
- contextTurns: 2,
19688
- roles: ["user", "assistant"],
19689
- promptPreamble: HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT,
19690
- tags: [],
19691
- tagsMatch: "any",
19692
- tagGroups: {},
19693
- tagWeights: HINDSIGHT_RECALL_TAG_WEIGHT_SEED,
19694
- additionalBankFilters: {},
19695
- transcriptFallback: true,
19696
- transcriptTailBytes: 262144,
19697
- maxQueryChars: 800,
19698
- parallel: true
19699
- });
19700
-
19701
19767
  // src/repos/bare-clone.ts
19702
19768
  init_paths();
19703
19769
 
@@ -22183,11 +22249,30 @@ class AuthBroker {
22183
22249
  return override;
22184
22250
  return auth.active ?? null;
22185
22251
  }
22252
+ exclusiveOwnerOf(account) {
22253
+ for (const [name, agent] of Object.entries(this.config.agents ?? {})) {
22254
+ if (agent.auth?.exclusive && agent.auth?.override === account)
22255
+ return name;
22256
+ }
22257
+ return null;
22258
+ }
22259
+ isServableBy(account, forAgent) {
22260
+ const owner = this.exclusiveOwnerOf(account);
22261
+ return owner === null || owner === forAgent;
22262
+ }
22186
22263
  servingAccount(identity) {
22187
- const account = this.callerAccount(identity);
22188
22264
  if (identity.kind === "operator")
22189
- return account;
22190
- return this.accountWithFailover(account);
22265
+ return this.callerAccount(identity);
22266
+ if (identity.kind === "agent")
22267
+ return this.servedAccountForAgent(identity.name);
22268
+ return this.accountWithFailover(this.callerAccount(identity));
22269
+ }
22270
+ servedAccountForAgent(name) {
22271
+ const agent = (this.config.agents ?? {})[name];
22272
+ const override = agent?.auth?.override;
22273
+ if (override && agent?.auth?.strict)
22274
+ return override;
22275
+ return this.accountWithFailover(override ?? this.config.auth?.active ?? null, name);
22191
22276
  }
22192
22277
  isOverageAllowed(account) {
22193
22278
  return (this.config.auth?.allow_overage_accounts ?? []).includes(account);
@@ -22305,6 +22390,8 @@ class AuthBroker {
22305
22390
  continue;
22306
22391
  if (this.isAccountPremiumWalled(cand))
22307
22392
  continue;
22393
+ if (this.exclusiveOwnerOf(cand))
22394
+ continue;
22308
22395
  if (!accountExists(cand, this.home))
22309
22396
  continue;
22310
22397
  if (!readAccountCredentials(cand, this.home))
@@ -22450,15 +22537,30 @@ class AuthBroker {
22450
22537
  if (reselected && this.accountEligibilityOf(reselected) === "eligible")
22451
22538
  return reselected;
22452
22539
  for (const cand of ring) {
22453
- if (cand && cand !== current && accountExists(cand, this.home) && this.accountEligibilityOf(cand) === "unknown") {
22540
+ if (cand && cand !== current && accountExists(cand, this.home) && !this.exclusiveOwnerOf(cand) && this.accountEligibilityOf(cand) === "unknown") {
22454
22541
  return cand;
22455
22542
  }
22456
22543
  }
22457
22544
  return null;
22458
22545
  }
22459
- accountWithFailover(account) {
22546
+ accountWithFailover(account, forAgent) {
22460
22547
  if (!account)
22461
22548
  return null;
22549
+ if (!this.isServableBy(account, forAgent)) {
22550
+ for (const cand of this.config.auth?.fallback_order ?? []) {
22551
+ if (cand === account || this.isAccountExhausted(cand))
22552
+ continue;
22553
+ if (!this.isServableBy(cand, forAgent))
22554
+ continue;
22555
+ if (readAccountCredentials(cand, this.home))
22556
+ return cand;
22557
+ }
22558
+ const active2 = this.config.auth?.active;
22559
+ if (active2 && active2 !== account && !this.isAccountExhausted(active2) && this.isServableBy(active2, forAgent) && readAccountCredentials(active2, this.home)) {
22560
+ return active2;
22561
+ }
22562
+ return null;
22563
+ }
22462
22564
  const exhausted = this.isAccountExhausted(account);
22463
22565
  if (!exhausted && !this.isAccountSoftAvoided(account))
22464
22566
  return account;
@@ -22466,6 +22568,8 @@ class AuthBroker {
22466
22568
  for (const cand of this.config.auth?.fallback_order ?? []) {
22467
22569
  if (cand === account || this.isAccountExhausted(cand))
22468
22570
  continue;
22571
+ if (!this.isServableBy(cand, forAgent))
22572
+ continue;
22469
22573
  if (this.isAccountSoftAvoided(cand)) {
22470
22574
  softAvoidExcludedAny = true;
22471
22575
  continue;
@@ -22479,6 +22583,8 @@ class AuthBroker {
22479
22583
  for (const cand of this.config.auth?.fallback_order ?? []) {
22480
22584
  if (cand === account || this.isAccountExhausted(cand))
22481
22585
  continue;
22586
+ if (!this.isServableBy(cand, forAgent))
22587
+ continue;
22482
22588
  if (!readAccountCredentials(cand, this.home))
22483
22589
  continue;
22484
22590
  const score = this.softAvoidUtilScore(cand);
@@ -22493,12 +22599,14 @@ class AuthBroker {
22493
22599
  for (const cand of this.config.auth?.fallback_order ?? []) {
22494
22600
  if (cand === account || this.isAccountExhausted(cand))
22495
22601
  continue;
22602
+ if (!this.isServableBy(cand, forAgent))
22603
+ continue;
22496
22604
  if (readAccountCredentials(cand, this.home))
22497
22605
  return cand;
22498
22606
  }
22499
22607
  }
22500
22608
  const active = this.config.auth?.active;
22501
- if (active && active !== account && !this.isAccountExhausted(active) && readAccountCredentials(active, this.home)) {
22609
+ if (active && active !== account && !this.isAccountExhausted(active) && this.isServableBy(active, forAgent) && readAccountCredentials(active, this.home)) {
22502
22610
  return active;
22503
22611
  }
22504
22612
  return account;
@@ -22550,7 +22658,9 @@ class AuthBroker {
22550
22658
  const agents = Object.entries(this.config.agents ?? {}).map(([name, agent]) => {
22551
22659
  const override = agent.auth?.override ?? null;
22552
22660
  const account = override ?? auth.active ?? "";
22553
- return { name, account, override };
22661
+ const strict = Boolean(override && agent.auth?.strict);
22662
+ const exclusive = Boolean(override && agent.auth?.exclusive);
22663
+ return { name, account, override, strict, exclusive };
22554
22664
  });
22555
22665
  const consumers = (auth.consumers ?? []).map((c) => ({
22556
22666
  name: c.name,
@@ -22877,6 +22987,12 @@ class AuthBroker {
22877
22987
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
22878
22988
  return;
22879
22989
  }
22990
+ const exclusiveOwner = this.exclusiveOwnerOf(account);
22991
+ if (exclusiveOwner) {
22992
+ this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: false, error: "exclusive-account" });
22993
+ socket.write(encodeError(id, "FORBIDDEN", `account '${account}' is exclusive to agent '${exclusiveOwner}' ` + `(agents.${exclusiveOwner}.auth.exclusive) — it cannot be the fleet active. ` + `Clear the exclusive flag in switchroom.yaml first if you really mean this.`));
22994
+ return;
22995
+ }
22880
22996
  const cfg = {
22881
22997
  ...this.config,
22882
22998
  auth: { ...this.config.auth ?? {}, active: account }
@@ -22888,6 +23004,15 @@ class AuthBroker {
22888
23004
  this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: true });
22889
23005
  socket.write(encodeSuccess(id, { active: account, fanned }));
22890
23006
  }
23007
+ callerStrictPinned(identity) {
23008
+ if (identity.kind !== "agent")
23009
+ return false;
23010
+ const a = (this.config.agents ?? {})[identity.name]?.auth;
23011
+ return Boolean(a?.override && a?.strict);
23012
+ }
23013
+ callerRolledTo(identity, rolledTo) {
23014
+ return this.callerStrictPinned(identity) ? null : rolledTo;
23015
+ }
22891
23016
  async opMarkExhausted(socket, id, identity, until) {
22892
23017
  const account = this.callerAccount(identity);
22893
23018
  if (!account) {
@@ -22897,7 +23022,12 @@ class AuthBroker {
22897
23022
  }
22898
23023
  const { rolled, rolledTo } = await this.markExhaustedAndRoll(account, until, identity);
22899
23024
  this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true });
22900
- socket.write(encodeSuccess(id, { account, rolled, rolledTo }));
23025
+ socket.write(encodeSuccess(id, {
23026
+ account,
23027
+ rolled,
23028
+ rolledTo: this.callerRolledTo(identity, rolledTo),
23029
+ caller_pinned_strict: this.callerStrictPinned(identity)
23030
+ }));
22901
23031
  }
22902
23032
  async opMarkThrottled(socket, id, identity, until, probeOnly = false) {
22903
23033
  const account = this.callerAccount(identity);
@@ -22914,7 +23044,8 @@ class AuthBroker {
22914
23044
  account,
22915
23045
  throttled_until: this.quota[account]?.throttled_until ?? 0,
22916
23046
  escalated: escalated2,
22917
- rolledTo: escalated2 ? rolledTo2 : null
23047
+ rolledTo: escalated2 ? this.callerRolledTo(identity, rolledTo2) : null,
23048
+ caller_pinned_strict: this.callerStrictPinned(identity)
22918
23049
  }));
22919
23050
  return;
22920
23051
  }
@@ -22957,7 +23088,8 @@ class AuthBroker {
22957
23088
  account,
22958
23089
  throttled_until: throttledUntil,
22959
23090
  escalated,
22960
- rolledTo: escalated ? rolledTo : null
23091
+ rolledTo: escalated ? this.callerRolledTo(identity, rolledTo) : null,
23092
+ caller_pinned_strict: this.callerStrictPinned(identity)
22961
23093
  }));
22962
23094
  }
22963
23095
  async maybeEscalateThrottle(account, identity, now) {
@@ -23529,6 +23661,22 @@ class AuthBroker {
23529
23661
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
23530
23662
  return;
23531
23663
  }
23664
+ if (account !== null) {
23665
+ const exclusiveOwner = this.exclusiveOwnerOf(account);
23666
+ if (exclusiveOwner && exclusiveOwner !== agentName) {
23667
+ this.audit({ op: "set-override", identity, account, accountKind: "claude", ok: false, error: "exclusive-account" });
23668
+ socket.write(encodeError(id, "FORBIDDEN", `account '${account}' is exclusive to agent '${exclusiveOwner}' ` + `(agents.${exclusiveOwner}.auth.exclusive) — agent '${agentName}' cannot pin it.`));
23669
+ return;
23670
+ }
23671
+ }
23672
+ {
23673
+ const curAuth = (this.config.agents ?? {})[agentName]?.auth;
23674
+ if ((curAuth?.strict || curAuth?.exclusive) && account !== (curAuth?.override ?? null)) {
23675
+ this.audit({ op: "set-override", identity, account: account ?? undefined, accountKind: "claude", ok: false, error: "yaml-pinned" });
23676
+ socket.write(encodeError(id, "FORBIDDEN", `agent '${agentName}' has a strict/exclusive pin declared in switchroom.yaml — ` + `edit the yaml (agents.${agentName}.auth) and restart the broker to change it.`));
23677
+ return;
23678
+ }
23679
+ }
23532
23680
  const agents = { ...this.config.agents ?? {} };
23533
23681
  const cur = agents[agentName];
23534
23682
  const auth = { ...cur.auth ?? {} };
@@ -23704,10 +23852,9 @@ class AuthBroker {
23704
23852
  return out;
23705
23853
  }
23706
23854
  fanoutToAffectedAgents(label) {
23707
- const auth = this.config.auth ?? {};
23708
23855
  const fanned = [];
23709
- for (const [name, agent] of Object.entries(this.config.agents ?? {})) {
23710
- const effective = this.accountWithFailover(agent.auth?.override ?? auth.active);
23856
+ for (const name of Object.keys(this.config.agents ?? {})) {
23857
+ const effective = this.servedAccountForAgent(name);
23711
23858
  if (effective === label) {
23712
23859
  if (this.fanoutForAgent(name))
23713
23860
  fanned.push(name);
@@ -23726,10 +23873,9 @@ class AuthBroker {
23726
23873
  const isEffective = effective === label;
23727
23874
  if (!isPinned && !isEffective)
23728
23875
  continue;
23729
- const toMirror = effective ?? bound;
23730
- if (toMirror == null)
23876
+ if (effective == null)
23731
23877
  continue;
23732
- if (this.mirrorAccountToConsumer(toMirror, consumer)) {
23878
+ if (this.mirrorAccountToConsumer(effective, consumer)) {
23733
23879
  fanned.push(consumer.name);
23734
23880
  }
23735
23881
  }
@@ -23830,6 +23976,10 @@ class AuthBroker {
23830
23976
  const effective = agent.auth?.override ?? auth.active;
23831
23977
  if (effective !== label)
23832
23978
  continue;
23979
+ if (agent.auth?.override && agent.auth?.strict)
23980
+ continue;
23981
+ if (!this.isServableBy(next, name))
23982
+ continue;
23833
23983
  if (this.mirrorAccountToAgent(next, name)) {
23834
23984
  rolled.push(name);
23835
23985
  }
@@ -23838,8 +23988,13 @@ class AuthBroker {
23838
23988
  }
23839
23989
  nextHealthyAccount(current, order) {
23840
23990
  const start = order.indexOf(current);
23841
- if (start === -1)
23842
- return order[0] ?? null;
23991
+ if (start === -1) {
23992
+ for (const cand of order) {
23993
+ if (cand && !this.exclusiveOwnerOf(cand))
23994
+ return cand;
23995
+ }
23996
+ return null;
23997
+ }
23843
23998
  let bestSoftAvoided = null;
23844
23999
  let bestScore = Number.POSITIVE_INFINITY;
23845
24000
  for (let i = 1;i <= order.length; i++) {
@@ -23848,6 +24003,8 @@ class AuthBroker {
23848
24003
  continue;
23849
24004
  if (this.isAccountExhausted(cand) || !accountExists(cand, this.home))
23850
24005
  continue;
24006
+ if (this.exclusiveOwnerOf(cand))
24007
+ continue;
23851
24008
  if (!this.isAccountSoftAvoided(cand))
23852
24009
  return cand;
23853
24010
  const score = this.softAvoidUtilScore(cand);
@@ -23859,11 +24016,10 @@ class AuthBroker {
23859
24016
  return bestSoftAvoided;
23860
24017
  }
23861
24018
  fanoutForAgent(name) {
23862
- const auth = this.config.auth ?? {};
23863
24019
  const agent = (this.config.agents ?? {})[name];
23864
24020
  if (!agent)
23865
24021
  return false;
23866
- const effective = this.accountWithFailover(agent.auth?.override ?? auth.active);
24022
+ const effective = this.servedAccountForAgent(name);
23867
24023
  if (!effective)
23868
24024
  return false;
23869
24025
  return this.mirrorAccountToAgent(effective, name);
@@ -23992,6 +24148,15 @@ class AuthBroker {
23992
24148
  `);
23993
24149
  return;
23994
24150
  }
24151
+ const exclusiveOwner = this.exclusiveOwnerOf(ov.active);
24152
+ if (exclusiveOwner) {
24153
+ process.stdout.write(`auth-broker: active-override dropped — account '${ov.active}' is now exclusive ` + `to agent '${exclusiveOwner}' (agents.${exclusiveOwner}.auth.exclusive); yaml wins
24154
+ `);
24155
+ try {
24156
+ unlinkSync(join7(this.stateDir, "active-override.json"));
24157
+ } catch {}
24158
+ return;
24159
+ }
23995
24160
  if (ov.active === this.config.auth?.active)
23996
24161
  return;
23997
24162
  this.config = {