switchroom 0.19.45 → 0.19.47

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.
@@ -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, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, 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, 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;
10998
10998
  var init_schema = __esm(() => {
10999
10999
  init_zod();
11000
11000
  init_observation_scopes();
@@ -11381,11 +11381,18 @@ var init_schema = __esm(() => {
11381
11381
  "extended",
11382
11382
  "complete"
11383
11383
  ]);
11384
+ GoogleServiceTokenSchema = exports_external.enum([
11385
+ "cal",
11386
+ "drive",
11387
+ "docs",
11388
+ "sheets",
11389
+ "slides"
11390
+ ]);
11384
11391
  GoogleWorkspaceConfigSchema = exports_external.object({
11385
11392
  google_client_id: exports_external.string().min(1).describe("Google OAuth client ID (literal string or vault reference e.g. 'vault:google-oauth-client-id')"),
11386
11393
  google_client_secret: exports_external.string().min(1).describe("Google OAuth client secret (literal string or vault reference e.g. 'vault:google-oauth-client-secret')"),
11387
11394
  approvers: exports_external.array(ApproverIdSchema).min(1).describe("Array of numeric Telegram user IDs authorized to approve drive onboarding. " + "At least one must be specified."),
11388
- tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
11395
+ tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar tools; the " + "Calendar tools only authenticate when the account holds the opt-in " + "calendar scope — no tier mints it). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
11389
11396
  }).optional();
11390
11397
  LiteLLMConfigSchema = exports_external.object({
11391
11398
  enabled: exports_external.boolean().optional().describe("Opt-in toggle. When true, `switchroom apply` provisions a per-agent " + "LiteLLM virtual key and injects routing env into the container. " + "Default OFF."),
@@ -11418,7 +11425,7 @@ var init_schema = __esm(() => {
11418
11425
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
11419
11426
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
11420
11427
  }).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."),
11421
- 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), 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).")
11428
+ 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), 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).")
11422
11429
  });
11423
11430
  MicrosoftWorkspaceConfigSchema = exports_external.object({
11424
11431
  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)."),
@@ -11892,7 +11899,9 @@ var init_schema = __esm(() => {
11892
11899
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
11893
11900
  enabled_for: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
11894
11901
  message: "Agent name must match the standard agent-name pattern"
11895
- })).describe("Agent slugs that may read this account's vault slots " + "(`google:<account>:refresh_token` etc). Per-agent ACL is " + "enforced at the broker, not at the agent identity layer — " + "the agent still authenticates via socket-path-as-identity " + "per RFC D §4.1, broker just gates the cross-agent token share.")
11902
+ })).describe("Agent slugs that may read this account's vault slots " + "(`google:<account>:refresh_token` etc). Per-agent ACL is " + "enforced at the broker, not at the agent identity layer — " + "the agent still authenticates via socket-path-as-identity " + "per RFC D §4.1, broker just gates the cross-agent token share."),
11903
+ readonly: exports_external.boolean().optional().describe("Per-account read-only selection (v1 scope model). true → the " + "token minted by `auth google account add` carries ONLY " + ".readonly scope variants (zero write scopes), and the gdrive " + "MCP launcher passes upstream `--read-only` so write tools are " + "not exposed. Written by `account add --readonly`; re-read on " + "`--replace` so a re-consent cannot silently re-widen. Omitted " + "= legacy behaviour (tier-tied read-write document scopes)."),
11904
+ services: exports_external.array(GoogleServiceTokenSchema).min(1).optional().describe("Per-account service selection (v1 scope model). Which Google " + "services the minted token covers AND the gdrive MCP exposes " + "(upstream `--tools`): cal, drive, docs, sheets, slides. " + "Written by `account add --services`; re-read on `--replace`. " + "Omitted = the tier's default services (drive,docs,sheets; " + "+slides at extended/complete).")
11896
11905
  })).optional().describe("RFC G Phase 2: per-Google-account ACL for vault slots holding " + "OAuth refresh tokens. Maps account email → list of agents " + "permitted to read that account's slots. Written by `switchroom " + "auth google enable|disable` (Phase 3); read by the broker on " + "every Google slot access. Replaces RFC D's per-agent vault slot " + "scope (which can't express 'two agents share one Google account')."),
11897
11906
  microsoft_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11898
11907
  message: "Account key must be a Microsoft account email like 'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
@@ -21402,7 +21411,7 @@ function allocateAgentUid(name) {
21402
21411
  }
21403
21412
 
21404
21413
  // src/build-info.ts
21405
- var VERSION = "0.19.45";
21414
+ var VERSION = "0.19.47";
21406
21415
 
21407
21416
  // src/setup/hindsight-recall-tunables.ts
21408
21417
  var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
@@ -21673,7 +21682,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
21673
21682
  "HINDSIGHT_MCP_RECALL_BUDGET_MODE",
21674
21683
  "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT",
21675
21684
  "HINDSIGHT_API_RETAIN_WALL_TIMEOUT",
21676
- "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT"
21685
+ "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
21686
+ "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S"
21677
21687
  ]);
21678
21688
  var HINDSIGHT_WORKER_SLOT_TYPES = [
21679
21689
  "consolidation",
@@ -4434,7 +4434,7 @@ var init_observation_scopes = __esm(() => {
4434
4434
  });
4435
4435
 
4436
4436
  // src/config/schema.ts
4437
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, 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;
4437
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, 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;
4438
4438
  var init_schema = __esm(() => {
4439
4439
  init_zod();
4440
4440
  init_observation_scopes();
@@ -4821,11 +4821,18 @@ var init_schema = __esm(() => {
4821
4821
  "extended",
4822
4822
  "complete"
4823
4823
  ]);
4824
+ GoogleServiceTokenSchema = exports_external.enum([
4825
+ "cal",
4826
+ "drive",
4827
+ "docs",
4828
+ "sheets",
4829
+ "slides"
4830
+ ]);
4824
4831
  GoogleWorkspaceConfigSchema = exports_external.object({
4825
4832
  google_client_id: exports_external.string().min(1).describe("Google OAuth client ID (literal string or vault reference e.g. 'vault:google-oauth-client-id')"),
4826
4833
  google_client_secret: exports_external.string().min(1).describe("Google OAuth client secret (literal string or vault reference e.g. 'vault:google-oauth-client-secret')"),
4827
4834
  approvers: exports_external.array(ApproverIdSchema).min(1).describe("Array of numeric Telegram user IDs authorized to approve drive onboarding. " + "At least one must be specified."),
4828
- tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
4835
+ tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar tools; the " + "Calendar tools only authenticate when the account holds the opt-in " + "calendar scope — no tier mints it). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
4829
4836
  }).optional();
4830
4837
  LiteLLMConfigSchema = exports_external.object({
4831
4838
  enabled: exports_external.boolean().optional().describe("Opt-in toggle. When true, `switchroom apply` provisions a per-agent " + "LiteLLM virtual key and injects routing env into the container. " + "Default OFF."),
@@ -4858,7 +4865,7 @@ var init_schema = __esm(() => {
4858
4865
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
4859
4866
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
4860
4867
  }).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."),
4861
- 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), 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).")
4868
+ 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), 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).")
4862
4869
  });
4863
4870
  MicrosoftWorkspaceConfigSchema = exports_external.object({
4864
4871
  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)."),
@@ -5332,7 +5339,9 @@ var init_schema = __esm(() => {
5332
5339
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
5333
5340
  enabled_for: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
5334
5341
  message: "Agent name must match the standard agent-name pattern"
5335
- })).describe("Agent slugs that may read this account's vault slots " + "(`google:<account>:refresh_token` etc). Per-agent ACL is " + "enforced at the broker, not at the agent identity layer — " + "the agent still authenticates via socket-path-as-identity " + "per RFC D §4.1, broker just gates the cross-agent token share.")
5342
+ })).describe("Agent slugs that may read this account's vault slots " + "(`google:<account>:refresh_token` etc). Per-agent ACL is " + "enforced at the broker, not at the agent identity layer — " + "the agent still authenticates via socket-path-as-identity " + "per RFC D §4.1, broker just gates the cross-agent token share."),
5343
+ readonly: exports_external.boolean().optional().describe("Per-account read-only selection (v1 scope model). true → the " + "token minted by `auth google account add` carries ONLY " + ".readonly scope variants (zero write scopes), and the gdrive " + "MCP launcher passes upstream `--read-only` so write tools are " + "not exposed. Written by `account add --readonly`; re-read on " + "`--replace` so a re-consent cannot silently re-widen. Omitted " + "= legacy behaviour (tier-tied read-write document scopes)."),
5344
+ services: exports_external.array(GoogleServiceTokenSchema).min(1).optional().describe("Per-account service selection (v1 scope model). Which Google " + "services the minted token covers AND the gdrive MCP exposes " + "(upstream `--tools`): cal, drive, docs, sheets, slides. " + "Written by `account add --services`; re-read on `--replace`. " + "Omitted = the tier's default services (drive,docs,sheets; " + "+slides at extended/complete).")
5336
5345
  })).optional().describe("RFC G Phase 2: per-Google-account ACL for vault slots holding " + "OAuth refresh tokens. Maps account email → list of agents " + "permitted to read that account's slots. Written by `switchroom " + "auth google enable|disable` (Phase 3); read by the broker on " + "every Google slot access. Replaces RFC D's per-agent vault slot " + "scope (which can't express 'two agents share one Google account')."),
5337
5346
  microsoft_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
5338
5347
  message: "Account key must be a Microsoft account email like 'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
@@ -19568,7 +19577,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
19568
19577
  "HINDSIGHT_MCP_RECALL_BUDGET_MODE",
19569
19578
  "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT",
19570
19579
  "HINDSIGHT_API_RETAIN_WALL_TIMEOUT",
19571
- "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT"
19580
+ "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
19581
+ "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S"
19572
19582
  ]);
19573
19583
  var HINDSIGHT_WORKER_SLOT_TYPES = [
19574
19584
  "consolidation",
@@ -4030,7 +4030,7 @@ var init_observation_scopes = __esm(() => {
4030
4030
  });
4031
4031
 
4032
4032
  // src/config/schema.ts
4033
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, 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;
4033
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, 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;
4034
4034
  var init_schema = __esm(() => {
4035
4035
  init_zod();
4036
4036
  init_observation_scopes();
@@ -4417,11 +4417,18 @@ var init_schema = __esm(() => {
4417
4417
  "extended",
4418
4418
  "complete"
4419
4419
  ]);
4420
+ GoogleServiceTokenSchema = exports_external.enum([
4421
+ "cal",
4422
+ "drive",
4423
+ "docs",
4424
+ "sheets",
4425
+ "slides"
4426
+ ]);
4420
4427
  GoogleWorkspaceConfigSchema = exports_external.object({
4421
4428
  google_client_id: exports_external.string().min(1).describe("Google OAuth client ID (literal string or vault reference e.g. 'vault:google-oauth-client-id')"),
4422
4429
  google_client_secret: exports_external.string().min(1).describe("Google OAuth client secret (literal string or vault reference e.g. 'vault:google-oauth-client-secret')"),
4423
4430
  approvers: exports_external.array(ApproverIdSchema).min(1).describe("Array of numeric Telegram user IDs authorized to approve drive onboarding. " + "At least one must be specified."),
4424
- tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
4431
+ tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar tools; the " + "Calendar tools only authenticate when the account holds the opt-in " + "calendar scope — no tier mints it). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
4425
4432
  }).optional();
4426
4433
  LiteLLMConfigSchema = exports_external.object({
4427
4434
  enabled: exports_external.boolean().optional().describe("Opt-in toggle. When true, `switchroom apply` provisions a per-agent " + "LiteLLM virtual key and injects routing env into the container. " + "Default OFF."),
@@ -4454,7 +4461,7 @@ var init_schema = __esm(() => {
4454
4461
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
4455
4462
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
4456
4463
  }).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."),
4457
- 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), 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).")
4464
+ 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), 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).")
4458
4465
  });
4459
4466
  MicrosoftWorkspaceConfigSchema = exports_external.object({
4460
4467
  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)."),
@@ -4928,7 +4935,9 @@ var init_schema = __esm(() => {
4928
4935
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
4929
4936
  enabled_for: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
4930
4937
  message: "Agent name must match the standard agent-name pattern"
4931
- })).describe("Agent slugs that may read this account's vault slots " + "(`google:<account>:refresh_token` etc). Per-agent ACL is " + "enforced at the broker, not at the agent identity layer — " + "the agent still authenticates via socket-path-as-identity " + "per RFC D §4.1, broker just gates the cross-agent token share.")
4938
+ })).describe("Agent slugs that may read this account's vault slots " + "(`google:<account>:refresh_token` etc). Per-agent ACL is " + "enforced at the broker, not at the agent identity layer — " + "the agent still authenticates via socket-path-as-identity " + "per RFC D §4.1, broker just gates the cross-agent token share."),
4939
+ readonly: exports_external.boolean().optional().describe("Per-account read-only selection (v1 scope model). true → the " + "token minted by `auth google account add` carries ONLY " + ".readonly scope variants (zero write scopes), and the gdrive " + "MCP launcher passes upstream `--read-only` so write tools are " + "not exposed. Written by `account add --readonly`; re-read on " + "`--replace` so a re-consent cannot silently re-widen. Omitted " + "= legacy behaviour (tier-tied read-write document scopes)."),
4940
+ services: exports_external.array(GoogleServiceTokenSchema).min(1).optional().describe("Per-account service selection (v1 scope model). Which Google " + "services the minted token covers AND the gdrive MCP exposes " + "(upstream `--tools`): cal, drive, docs, sheets, slides. " + "Written by `account add --services`; re-read on `--replace`. " + "Omitted = the tier's default services (drive,docs,sheets; " + "+slides at extended/complete).")
4932
4941
  })).optional().describe("RFC G Phase 2: per-Google-account ACL for vault slots holding " + "OAuth refresh tokens. Maps account email → list of agents " + "permitted to read that account's slots. Written by `switchroom " + "auth google enable|disable` (Phase 3); read by the broker on " + "every Google slot access. Replaces RFC D's per-agent vault slot " + "scope (which can't express 'two agents share one Google account')."),
4933
4942
  microsoft_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
4934
4943
  message: "Account key must be a Microsoft account email like 'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
@@ -19981,7 +19990,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
19981
19990
  "HINDSIGHT_MCP_RECALL_BUDGET_MODE",
19982
19991
  "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT",
19983
19992
  "HINDSIGHT_API_RETAIN_WALL_TIMEOUT",
19984
- "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT"
19993
+ "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
19994
+ "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S"
19985
19995
  ]);
19986
19996
  var HINDSIGHT_WORKER_SLOT_TYPES = [
19987
19997
  "consolidation",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.19.45",
4
+ "version": "0.19.47",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -805,7 +805,26 @@ export SWITCHROOM_AGENT_START_CWD="{{agentDir}}"
805
805
  # already gets this via .mcp.json env, but the Claude Code process
806
806
  # doesn't inherit that — so without this export, `switchroom auth status`
807
807
  # from Bash inside the agent fails with "No switchroom.yaml found".
808
- export SWITCHROOM_CONFIG={{{switchroomConfigPathQ}}}
808
+ #
809
+ # DEFER to a pre-set value: in Docker, compose.ts already sets
810
+ # SWITCHROOM_CONFIG to the in-container mount (/state/config/
811
+ # switchroom.yaml). The value baked here is the HOST path — resolve()d on
812
+ # the machine that ran the scaffold — which does not exist inside the
813
+ # container. Unconditionally exporting it clobbered the correct compose
814
+ # value and silently killed the live model/effort resolvers below (their
815
+ # [ -f "$SWITCHROOM_CONFIG" ] guard failed and fell into the quiet
816
+ # no-config-mount branch), so `model:` edits + bare restart did nothing.
817
+ # The bake stays as the fallback for host-run (non-Docker) agents, where
818
+ # nothing else sets the var. ${VAR:-} also treats a set-but-EMPTY var as
819
+ # unset, so an empty compose env can never blank the fallback.
820
+ #
821
+ # NB the baked path is pre-single-quoted by the scaffold, so it is
822
+ # assigned BARE to a temp var first — embedding it directly inside
823
+ # "${SWITCHROOM_CONFIG:-...}" would keep the literal quote characters in
824
+ # the value.
825
+ _SR_CONFIG_BAKED={{{switchroomConfigPathQ}}}
826
+ export SWITCHROOM_CONFIG="${SWITCHROOM_CONFIG:-$_SR_CONFIG_BAKED}"
827
+ unset _SR_CONFIG_BAKED
809
828
  {{/if}}
810
829
  {{#if hindsightEnabled}}
811
830
  # Hindsight memory plugin (vendored at .claude/plugins/hindsight-memory/).
@@ -1722,9 +1741,11 @@ fi
1722
1741
  # launcher recorded on every prior boot (read BEFORE it is overwritten);
1723
1742
  # 3. the apply-time bake.
1724
1743
  # Steps 2/3 append to `.session-model-alert` so the gateway tells the
1725
- # operator once at boot. No SWITCHROOM_CONFIG mount at all means there is
1726
- # nothing to read live — the bake IS the truth there (stderr note only; an
1727
- # operator alert every boot forever would be noise with no action).
1744
+ # operator once at boot. No SWITCHROOM_CONFIG at all means there is
1745
+ # nothing to read live — the bake IS the truth there and the fallback is
1746
+ # silent. But SWITCHROOM_CONFIG set with the FILE MISSING is a
1747
+ # misconfiguration that kills live resolution outright, so that case is
1748
+ # loud (stderr + operator alert) — see the else branch below.
1728
1749
  #
1729
1750
  # The yaml file bind mount re-resolves by PATH at container start, so a
1730
1751
  # host-side atomic-rename write is always picked up by the NEXT boot — the
@@ -1785,6 +1806,23 @@ if [ -n "$SWITCHROOM_CONFIG" ] && [ -f "$SWITCHROOM_CONFIG" ] && command -v swit
1785
1806
  unset _lm_live _lm_try _lm_baked_class _lm_live_class
1786
1807
  else
1787
1808
  _EFFECTIVE_MODEL="$_BAKED_MODEL"
1809
+ # No live read possible. Distinguish the legitimately quiet case — no
1810
+ # SWITCHROOM_CONFIG at all (host-run agent scaffolded without a config
1811
+ # path; the bake IS the truth) — from a broken resolver hiding behind
1812
+ # it. SWITCHROOM_CONFIG set but pointing at a file that does not exist
1813
+ # is a misconfiguration (e.g. a host path leaking into a container env)
1814
+ # that silently disables live `model:`/`thinking_effort:` resolution:
1815
+ # exactly the defect that let the start.sh host-path clobber of the
1816
+ # compose-set env go unnoticed. That case is LOUD — stderr + a one-boot
1817
+ # operator alert. A set config whose file exists but with no
1818
+ # `switchroom` CLI on PATH is equally dead, stderr-only (nothing the
1819
+ # operator can act on from chat).
1820
+ if [ -n "$SWITCHROOM_CONFIG" ] && [ ! -f "$SWITCHROOM_CONFIG" ]; then
1821
+ echo "session-model: SWITCHROOM_CONFIG='$SWITCHROOM_CONFIG' is set but the file does not exist — live config resolution is DISABLED; using apply-time baked model '$_BAKED_MODEL'" >&2
1822
+ printf '`SWITCHROOM_CONFIG` points at `%s`, which does not exist in this environment, so live `model:`/`thinking_effort:` resolution is disabled and the agent booted on the apply-time default `%s`. Edits to switchroom.yaml will NOT take effect on a bare restart until this is fixed.\n' "$SWITCHROOM_CONFIG" "$_BAKED_MODEL" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
1823
+ elif [ -n "$SWITCHROOM_CONFIG" ]; then
1824
+ echo "session-model: SWITCHROOM_CONFIG is set but no 'switchroom' CLI is on PATH — live config resolution is DISABLED; using apply-time baked model '$_BAKED_MODEL'" >&2
1825
+ fi
1788
1826
  fi
1789
1827
  unset _BAKED_MODEL _LKG_MODEL
1790
1828
  # Record the RESOLVED configured default (raw, unquoted) every boot, before
@@ -2078,6 +2116,12 @@ if [ -n "$SWITCHROOM_CONFIG" ] && [ -f "$SWITCHROOM_CONFIG" ] && command -v swit
2078
2116
  unset _le_live _le_try
2079
2117
  else
2080
2118
  _EFFECTIVE_EFFORT="$_BAKED_EFFORT"
2119
+ # Misconfig visibility, parity with the model sibling above — which
2120
+ # owns the operator alert for this shared root cause; a second alert
2121
+ # here would double-post for the same broken env. stderr only.
2122
+ if [ -n "$SWITCHROOM_CONFIG" ] && [ ! -f "$SWITCHROOM_CONFIG" ]; then
2123
+ echo "session-effort: SWITCHROOM_CONFIG='$SWITCHROOM_CONFIG' is set but the file does not exist — live config resolution is DISABLED; using apply-time baked thinking_effort '${_BAKED_EFFORT:-<unset>}'" >&2
2124
+ fi
2081
2125
  fi
2082
2126
  unset _BAKED_EFFORT _LKG_EFFORT
2083
2127
  # Record the LIVE-RESOLVED configured default, BEFORE the carrier below can
@@ -33,6 +33,61 @@ import { decideTurnFlush, type FlushDecisionInput } from './turn-flush-safety.js
33
33
  * matching the repo's env-flag convention. */
34
34
  export const ANSWER_READY_FLUSH_MS = 1000
35
35
 
36
+ /**
37
+ * Default STAGE window (ms) — the stage-don't-send fix for the live
38
+ * flush-then-late-reply duplicate (2026-08-02, klanker DM, msgs 25843/25844).
39
+ *
40
+ * The quiescence flush used to SEND the composed terminal answer the moment the
41
+ * ~1 s debounce fired. But quiescence is a heuristic, not a completion signal:
42
+ * the model routinely goes quiet for a few seconds BETWEEN its terminal
43
+ * narration and its `reply` tool call (observed gap: 6 s). The flush then posts
44
+ * a PROVISIONAL message that the supersede machinery must claw back — and the
45
+ * claw-back is deliberately conservative (#3429 content gate, #4176 sub-agent
46
+ * liveness hold), so a REWORDED own-answer with a background sub-agent live
47
+ * ships as a visible duplicate even on current main (the accepted residual
48
+ * documented in gateway/subagent-reply-authority.ts).
49
+ *
50
+ * Stage-don't-send removes the provisional send instead of reconciling it:
51
+ * when the quiescence debounce fires, the composed answer is STAGED (held on
52
+ * the still-live turn — the turn is NOT ended, so the typing indicator keeps
53
+ * running) and promoted to a real send only when the completion window closes
54
+ * with no reply:
55
+ *
56
+ * - the model's `reply` lands → the staged text is DISCARDED (fire-time
57
+ * re-verify declines on `replyCalled`) — exactly one message, the canonical
58
+ * reply, on its normal path;
59
+ * - the REAL turn_end lands first → the turn-flush branch at turn_end
60
+ * delivers the captured answer (the pre-PR-A path) — promotion at the true
61
+ * completion signal, usually seconds;
62
+ * - neither arrives within this window (the KNOWN-UNRELIABLE turn_end case
63
+ * PR A was built for) → the stage timer promotes through the same
64
+ * synthetic-turn_end flush path as before.
65
+ *
66
+ * Worst-case added latency for a silent no-op turn whose turn_end never lands
67
+ * is this window (30 s) — still ~5× faster than the ~150 s orphaned-reply
68
+ * backstop PR A replaced, and it buys the hard guarantee that a composing
69
+ * model can no longer race the flush into a duplicate. Env-tunable via
70
+ * SWITCHROOM_ANSWER_STAGE_MS; 0 (or any non-positive value) disables staging
71
+ * and restores the legacy immediate flush.
72
+ */
73
+ export const ANSWER_STAGE_MS = 30_000
74
+
75
+ /**
76
+ * Resolve the stage window from the environment. Returns a positive integer
77
+ * ms, or 0 when explicitly disabled (non-positive value = legacy immediate
78
+ * flush). Unparseable values fail safe to the default.
79
+ */
80
+ export function resolveAnswerStageMs(
81
+ env: Record<string, string | undefined>,
82
+ ): number {
83
+ const raw = env.SWITCHROOM_ANSWER_STAGE_MS
84
+ if (raw == null || raw.trim() === '') return ANSWER_STAGE_MS
85
+ const n = Number(raw)
86
+ if (!Number.isFinite(n)) return ANSWER_STAGE_MS
87
+ if (n <= 0) return 0
88
+ return Math.floor(n)
89
+ }
90
+
36
91
  /**
37
92
  * Resolve the debounce window from the environment. Returns a positive integer
38
93
  * ms, or 0 when disabled (kill-switch) / unparseable / non-positive. The
@@ -117,6 +172,17 @@ export interface AnswerReadyFlushDeps<Turn> {
117
172
  * short-circuits — the exactly-once guarantee.
118
173
  */
119
174
  onFlush(turn: Turn): void
175
+ /**
176
+ * Stage-don't-send window (ms) — see {@link ANSWER_STAGE_MS}. When > 0 the
177
+ * quiescence expiry STAGES the answer (turn stays live, typing hold) and
178
+ * arms a promotion timer for this many ms instead of flushing immediately;
179
+ * the promotion re-verifies before delivering, so a `reply` that landed in
180
+ * the window discards the staged text, and a real turn_end that landed first
181
+ * delivered it through the turn-flush branch already (its
182
+ * `endCurrentTurnAtomic` → `clear` cancels the promotion). 0 / undefined =
183
+ * legacy immediate flush at quiescence.
184
+ */
185
+ stageWindowMs?: number
120
186
  /** Injectable timer primitives (default to the globals). */
121
187
  setTimeoutFn?: (fn: () => void, ms: number) => FlushTimerHandle
122
188
  clearTimeoutFn?: (handle: FlushTimerHandle) => void
@@ -139,9 +205,14 @@ export interface AnswerReadyFlushDeps<Turn> {
139
205
  * landed since the arm cancels the flush deterministically), then dispatches
140
206
  * exactly one `onFlush`.
141
207
  */
142
- export class AnswerReadyFlushController<Turn> {
208
+ export class AnswerReadyFlushController<Turn extends object> {
143
209
  constructor(private readonly deps: AnswerReadyFlushDeps<Turn>) {}
144
210
 
211
+ /** Turns whose composed answer is currently STAGED (stage-don't-send): the
212
+ * quiescence debounce fired, but delivery is held for the promotion window.
213
+ * WeakSet so a superseded/ended turn atom can never leak an entry. */
214
+ private readonly staged = new WeakSet<Turn>()
215
+
145
216
  private get setTimeoutFn(): (fn: () => void, ms: number) => FlushTimerHandle {
146
217
  return this.deps.setTimeoutFn ?? ((fn, ms) => setTimeout(fn, ms))
147
218
  }
@@ -150,9 +221,11 @@ export class AnswerReadyFlushController<Turn> {
150
221
  return this.deps.clearTimeoutFn ?? ((h) => clearTimeout(h))
151
222
  }
152
223
 
153
- /** Disarm the flush timer for a turn (idempotent). */
224
+ /** Disarm the flush timer for a turn (idempotent). Clears BOTH phases: a
225
+ * pending quiescence debounce or a pending staged promotion. */
154
226
  clear(turn: Turn | null): void {
155
227
  if (turn == null) return
228
+ this.staged.delete(turn)
156
229
  const handle = this.deps.getTimerHandle(turn)
157
230
  if (handle != null) {
158
231
  this.clearTimeoutFn(handle)
@@ -160,6 +233,11 @@ export class AnswerReadyFlushController<Turn> {
160
233
  }
161
234
  }
162
235
 
236
+ /** True IFF `turn`'s composed answer is currently staged (test/diagnostics). */
237
+ isStaged(turn: Turn): boolean {
238
+ return this.staged.has(turn)
239
+ }
240
+
163
241
  /** (Re)arm the flush timer for the current turn — the debounce. */
164
242
  reset(): void {
165
243
  const turn = this.deps.getCurrentTurn()
@@ -171,7 +249,10 @@ export class AnswerReadyFlushController<Turn> {
171
249
  this.deps.setTimerHandle(turn, handle)
172
250
  }
173
251
 
174
- /** Timer-expiry callback. Re-pins the turn, re-verifies quiescence, fires once. */
252
+ /** Timer-expiry callback — BOTH phases share it. Re-pins the turn,
253
+ * re-verifies quiescence, then either STAGES (first fire, window > 0) or
254
+ * delivers (legacy immediate mode, or the staged promotion firing after the
255
+ * completion window closed with no reply). */
175
256
  private onExpiry(armedTurn: Turn): void {
176
257
  const live = this.deps.getCurrentTurn()
177
258
  // Rollover guard: a superseded turn's timer must not fire against a fresh atom.
@@ -179,9 +260,40 @@ export class AnswerReadyFlushController<Turn> {
179
260
  this.deps.setTimerHandle(live, null)
180
261
  // Fire-time re-verification: a tool that started (or a reply that landed)
181
262
  // since the arm deterministically cancels the flush even if the explicit
182
- // disarm was somehow missed.
183
- if (!shouldArmAnswerReadyFlush(this.deps.getArmInput(live))) return
184
- this.deps.log?.('answer-ready quiescence flush delivering composed terminal answer')
263
+ // disarm was somehow missed. On a staged promotion this is the DISCARD
264
+ // path: the reply landed inside the window, so the staged provisional text
265
+ // is dropped and the canonical reply is the one message the user sees.
266
+ if (!shouldArmAnswerReadyFlush(this.deps.getArmInput(live))) {
267
+ if (this.staged.delete(live)) {
268
+ this.deps.log?.(
269
+ 'answer-ready stage discarded — reply/tool activity landed inside the completion window',
270
+ )
271
+ }
272
+ return
273
+ }
274
+ const stageMs = this.deps.stageWindowMs ?? 0
275
+ if (stageMs > 0 && !this.staged.has(live)) {
276
+ // STAGE-DON'T-SEND (see ANSWER_STAGE_MS): hold the composed answer on
277
+ // the still-live turn instead of posting a provisional message. The turn
278
+ // is NOT ended, so the typing indicator keeps running (the "typing
279
+ // hold") and a real turn_end landing first delivers through the normal
280
+ // turn-flush branch (endCurrentTurnAtomic's `clear` cancels this timer).
281
+ this.staged.add(live)
282
+ this.deps.log?.(
283
+ `answer-ready quiescence — staging composed terminal answer ` +
284
+ `(promotes in ${stageMs}ms unless the reply lands or the turn completes)`,
285
+ )
286
+ const handle = this.setTimeoutFn(() => this.onExpiry(live), stageMs)
287
+ this.deps.setTimerHandle(live, handle)
288
+ return
289
+ }
290
+ if (this.staged.delete(live)) {
291
+ this.deps.log?.(
292
+ 'answer-ready stage promoted — completion window closed with no reply; delivering composed terminal answer',
293
+ )
294
+ } else {
295
+ this.deps.log?.('answer-ready quiescence flush — delivering composed terminal answer')
296
+ }
185
297
  this.deps.onFlush(live)
186
298
  }
187
299
  }