switchroom 0.19.28 → 0.19.29

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.
@@ -4012,10 +4012,22 @@ var init_zod = __esm(() => {
4012
4012
  init_external();
4013
4013
  });
4014
4014
 
4015
+ // src/memory/observation-scopes.ts
4016
+ var OBSERVATION_SCOPES;
4017
+ var init_observation_scopes = __esm(() => {
4018
+ OBSERVATION_SCOPES = [
4019
+ "per_tag",
4020
+ "combined",
4021
+ "all_combinations",
4022
+ "shared"
4023
+ ];
4024
+ });
4025
+
4015
4026
  // src/config/schema.ts
4016
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, 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;
4027
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, 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;
4017
4028
  var init_schema = __esm(() => {
4018
4029
  init_zod();
4030
+ init_observation_scopes();
4019
4031
  CodeRepoEntrySchema = exports_external.object({
4020
4032
  name: exports_external.string().describe("Short alias used when claiming (e.g. 'switchroom')"),
4021
4033
  source: exports_external.string().describe("Absolute or home-relative path to the repo (e.g. ~/code/switchroom)"),
@@ -4127,6 +4139,7 @@ var init_schema = __esm(() => {
4127
4139
  allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
4128
4140
  deny: exports_external.array(exports_external.string()).default([]).describe("Denied tools (overrides allow)")
4129
4141
  }).optional();
4142
+ ObservationScopesSchema = exports_external.enum(OBSERVATION_SCOPES).optional().describe("Per-row observation scope stamped on every memory this agent " + `retains. "shared" makes Hindsight's consolidation write the ` + "resulting observations into ONE global untagged scope instead of a " + "scope per tag — what several agents pooling one bank need so their " + "observations actually merge rather than sitting in parallel " + "per-tag silos. OMITTED BY DEFAULT: unset means the field never goes " + "on the wire and the engine's own default stands, which is the " + "shipped behaviour. Applies to every retain path (Stop hook, " + "sidechain, boot reconcile, queue drain, backfill, session-handoff " + "mirror) and is carried on the queued payload, so a retain that fails " + "now and drains later still lands in this scope. Accepted values: " + `${OBSERVATION_SCOPES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
4130
4143
  AgentMemorySchema = exports_external.object({
4131
4144
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
4132
4145
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
@@ -4161,6 +4174,7 @@ var init_schema = __esm(() => {
4161
4174
  empathy: exports_external.number().int().min(1).max(5).optional().describe("How much the bank weights emotional/relational context (1-5; engine default 3).")
4162
4175
  }).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis — a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
4163
4176
  directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true — Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never …", "from now on …", ' + `"stop doing …", a stated preference, "that's wrong, it's …") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex — the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
4177
+ observation_scopes: ObservationScopesSchema,
4164
4178
  recall: exports_external.object({
4165
4179
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
4166
4180
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
@@ -4426,7 +4440,7 @@ var init_schema = __esm(() => {
4426
4440
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
4427
4441
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
4428
4442
  }).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."),
4429
- 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_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, 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_MAX_SLOTS reserves out of; " + "unset means upstream's own default), 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).")
4443
+ 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_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, 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_MAX_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_MAX_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), 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).")
4430
4444
  });
4431
4445
  MicrosoftWorkspaceConfigSchema = exports_external.object({
4432
4446
  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)."),
@@ -4559,6 +4573,7 @@ var init_schema = __esm(() => {
4559
4573
  file: exports_external.boolean().optional(),
4560
4574
  isolation: exports_external.enum(["default", "strict"]).optional(),
4561
4575
  directive_capture_nudge: exports_external.boolean().optional(),
4576
+ observation_scopes: ObservationScopesSchema,
4562
4577
  recall: exports_external.object({
4563
4578
  max_memories: exports_external.number().int().min(0).optional(),
4564
4579
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
@@ -19410,7 +19425,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
19410
19425
  "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY",
19411
19426
  "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP",
19412
19427
  "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS",
19413
- "HINDSIGHT_API_WORKER_MAX_SLOTS"
19428
+ "HINDSIGHT_API_WORKER_MAX_SLOTS",
19429
+ "HINDSIGHT_API_WORKER_RETAIN_MAX_SLOTS"
19414
19430
  ]);
19415
19431
  var HINDSIGHT_PERF_ENV_KEYS = new Set([
19416
19432
  ...[
@@ -4012,10 +4012,22 @@ var init_zod = __esm(() => {
4012
4012
  init_external();
4013
4013
  });
4014
4014
 
4015
+ // src/memory/observation-scopes.ts
4016
+ var OBSERVATION_SCOPES;
4017
+ var init_observation_scopes = __esm(() => {
4018
+ OBSERVATION_SCOPES = [
4019
+ "per_tag",
4020
+ "combined",
4021
+ "all_combinations",
4022
+ "shared"
4023
+ ];
4024
+ });
4025
+
4015
4026
  // src/config/schema.ts
4016
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, 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;
4027
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, 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;
4017
4028
  var init_schema = __esm(() => {
4018
4029
  init_zod();
4030
+ init_observation_scopes();
4019
4031
  CodeRepoEntrySchema = exports_external.object({
4020
4032
  name: exports_external.string().describe("Short alias used when claiming (e.g. 'switchroom')"),
4021
4033
  source: exports_external.string().describe("Absolute or home-relative path to the repo (e.g. ~/code/switchroom)"),
@@ -4127,6 +4139,7 @@ var init_schema = __esm(() => {
4127
4139
  allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
4128
4140
  deny: exports_external.array(exports_external.string()).default([]).describe("Denied tools (overrides allow)")
4129
4141
  }).optional();
4142
+ ObservationScopesSchema = exports_external.enum(OBSERVATION_SCOPES).optional().describe("Per-row observation scope stamped on every memory this agent " + `retains. "shared" makes Hindsight's consolidation write the ` + "resulting observations into ONE global untagged scope instead of a " + "scope per tag — what several agents pooling one bank need so their " + "observations actually merge rather than sitting in parallel " + "per-tag silos. OMITTED BY DEFAULT: unset means the field never goes " + "on the wire and the engine's own default stands, which is the " + "shipped behaviour. Applies to every retain path (Stop hook, " + "sidechain, boot reconcile, queue drain, backfill, session-handoff " + "mirror) and is carried on the queued payload, so a retain that fails " + "now and drains later still lands in this scope. Accepted values: " + `${OBSERVATION_SCOPES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
4130
4143
  AgentMemorySchema = exports_external.object({
4131
4144
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
4132
4145
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
@@ -4161,6 +4174,7 @@ var init_schema = __esm(() => {
4161
4174
  empathy: exports_external.number().int().min(1).max(5).optional().describe("How much the bank weights emotional/relational context (1-5; engine default 3).")
4162
4175
  }).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis — a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
4163
4176
  directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true — Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never …", "from now on …", ' + `"stop doing …", a stated preference, "that's wrong, it's …") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex — the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
4177
+ observation_scopes: ObservationScopesSchema,
4164
4178
  recall: exports_external.object({
4165
4179
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
4166
4180
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
@@ -4426,7 +4440,7 @@ var init_schema = __esm(() => {
4426
4440
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
4427
4441
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
4428
4442
  }).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."),
4429
- 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_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, 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_MAX_SLOTS reserves out of; " + "unset means upstream's own default), 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).")
4443
+ 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_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, 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_MAX_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_MAX_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), 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).")
4430
4444
  });
4431
4445
  MicrosoftWorkspaceConfigSchema = exports_external.object({
4432
4446
  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)."),
@@ -4559,6 +4573,7 @@ var init_schema = __esm(() => {
4559
4573
  file: exports_external.boolean().optional(),
4560
4574
  isolation: exports_external.enum(["default", "strict"]).optional(),
4561
4575
  directive_capture_nudge: exports_external.boolean().optional(),
4576
+ observation_scopes: ObservationScopesSchema,
4562
4577
  recall: exports_external.object({
4563
4578
  max_memories: exports_external.number().int().min(0).optional(),
4564
4579
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
@@ -19083,7 +19098,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
19083
19098
  "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY",
19084
19099
  "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP",
19085
19100
  "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS",
19086
- "HINDSIGHT_API_WORKER_MAX_SLOTS"
19101
+ "HINDSIGHT_API_WORKER_MAX_SLOTS",
19102
+ "HINDSIGHT_API_WORKER_RETAIN_MAX_SLOTS"
19087
19103
  ]);
19088
19104
  var HINDSIGHT_PERF_ENV_KEYS = new Set([
19089
19105
  ...[
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.28",
4
+ "version": "0.19.29",
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": {
@@ -999,6 +999,15 @@ export HINDSIGHT_RECALL_SKIP_TRIVIAL={{hindsightRecallSkipTrivial}}
999
999
  {{#if hindsightDirectiveCaptureNudge}}
1000
1000
  export HINDSIGHT_DIRECTIVE_CAPTURE_NUDGE={{hindsightDirectiveCaptureNudge}}
1001
1001
  {{/if}}
1002
+ # Per-row observation scope (memory.observation_scopes cascade). Stamped on
1003
+ # every retain — Stop hook, sidechain, boot reconcile, queue drain, backfill.
1004
+ # "shared" makes consolidation pool this agent's observations into ONE global
1005
+ # untagged scope instead of a scope per tag. Export ONLY when the operator set
1006
+ # it: unset means the field never goes on the wire and the engine default
1007
+ # stands, which is the shipped behaviour.
1008
+ {{#if hindsightObservationScopesQ}}
1009
+ export HINDSIGHT_OBSERVATION_SCOPES={{{hindsightObservationScopesQ}}}
1010
+ {{/if}}
1002
1011
  # PR6 — supergroup-mode topic tagging. JSON map of {alias: thread_id}
1003
1012
  # parsed by retain.py + recall.py to (a) stamp chat_id/thread_id/topic_alias
1004
1013
  # into retained memory metadata and (b) emit a "Current topic: …" preamble
@@ -21552,10 +21552,22 @@ var require_dist = __commonJS((exports) => {
21552
21552
  exports.visitAsync = visit.visitAsync;
21553
21553
  });
21554
21554
 
21555
+ // ../src/memory/observation-scopes.ts
21556
+ var OBSERVATION_SCOPES;
21557
+ var init_observation_scopes = __esm(() => {
21558
+ OBSERVATION_SCOPES = [
21559
+ "per_tag",
21560
+ "combined",
21561
+ "all_combinations",
21562
+ "shared"
21563
+ ];
21564
+ });
21565
+
21555
21566
  // ../src/config/schema.ts
21556
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, 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;
21567
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, 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;
21557
21568
  var init_schema = __esm(() => {
21558
21569
  init_zod();
21570
+ init_observation_scopes();
21559
21571
  CodeRepoEntrySchema = exports_external.object({
21560
21572
  name: exports_external.string().describe("Short alias used when claiming (e.g. 'switchroom')"),
21561
21573
  source: exports_external.string().describe("Absolute or home-relative path to the repo (e.g. ~/code/switchroom)"),
@@ -21667,6 +21679,7 @@ var init_schema = __esm(() => {
21667
21679
  allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
21668
21680
  deny: exports_external.array(exports_external.string()).default([]).describe("Denied tools (overrides allow)")
21669
21681
  }).optional();
21682
+ ObservationScopesSchema = exports_external.enum(OBSERVATION_SCOPES).optional().describe("Per-row observation scope stamped on every memory this agent " + `retains. "shared" makes Hindsight's consolidation write the ` + "resulting observations into ONE global untagged scope instead of a " + "scope per tag \u2014 what several agents pooling one bank need so their " + "observations actually merge rather than sitting in parallel " + "per-tag silos. OMITTED BY DEFAULT: unset means the field never goes " + "on the wire and the engine's own default stands, which is the " + "shipped behaviour. Applies to every retain path (Stop hook, " + "sidechain, boot reconcile, queue drain, backfill, session-handoff " + "mirror) and is carried on the queued payload, so a retain that fails " + "now and drains later still lands in this scope. Accepted values: " + `${OBSERVATION_SCOPES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
21670
21683
  AgentMemorySchema = exports_external.object({
21671
21684
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
21672
21685
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
@@ -21701,6 +21714,7 @@ var init_schema = __esm(() => {
21701
21714
  empathy: exports_external.number().int().min(1).max(5).optional().describe("How much the bank weights emotional/relational context (1-5; engine default 3).")
21702
21715
  }).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis \u2014 a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
21703
21716
  directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true \u2014 Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never \u2026", "from now on \u2026", ' + `"stop doing \u2026", a stated preference, "that's wrong, it's \u2026") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex \u2014 the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
21717
+ observation_scopes: ObservationScopesSchema,
21704
21718
  recall: exports_external.object({
21705
21719
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
21706
21720
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
@@ -21966,7 +21980,7 @@ var init_schema = __esm(() => {
21966
21980
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent \u2192 uses global."),
21967
21981
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
21968
21982
  }).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."),
21969
- 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_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 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 \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 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 \u2014 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 \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_MAX_SLOTS reserves out of; " + "unset means upstream's own default), 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 \u2014 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 \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
21983
+ 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_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 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 \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 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 \u2014 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 \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_MAX_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_MAX_SLOTS \u2014 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), 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 \u2014 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 \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
21970
21984
  });
21971
21985
  MicrosoftWorkspaceConfigSchema = exports_external.object({
21972
21986
  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 \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
@@ -22099,6 +22113,7 @@ var init_schema = __esm(() => {
22099
22113
  file: exports_external.boolean().optional(),
22100
22114
  isolation: exports_external.enum(["default", "strict"]).optional(),
22101
22115
  directive_capture_nudge: exports_external.boolean().optional(),
22116
+ observation_scopes: ObservationScopesSchema,
22102
22117
  recall: exports_external.object({
22103
22118
  max_memories: exports_external.number().int().min(0).optional(),
22104
22119
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
@@ -84524,7 +84539,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
84524
84539
  "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY",
84525
84540
  "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP",
84526
84541
  "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS",
84527
- "HINDSIGHT_API_WORKER_MAX_SLOTS"
84542
+ "HINDSIGHT_API_WORKER_MAX_SLOTS",
84543
+ "HINDSIGHT_API_WORKER_RETAIN_MAX_SLOTS"
84528
84544
  ]);
84529
84545
  var HINDSIGHT_PERF_ENV_KEYS = new Set([
84530
84546
  ...[
@@ -98169,10 +98185,10 @@ function startOutboxSweep(deps) {
98169
98185
  }
98170
98186
 
98171
98187
  // ../src/build-info.ts
98172
- var VERSION2 = "0.19.28";
98173
- var COMMIT_SHA = "d9a5f4ae";
98174
- var COMMIT_DATE = "2026-07-28T03:46:02Z";
98175
- var LATEST_PR = 3906;
98188
+ var VERSION2 = "0.19.29";
98189
+ var COMMIT_SHA = "c618451c";
98190
+ var COMMIT_DATE = "2026-07-28T17:39:27Z";
98191
+ var LATEST_PR = 3918;
98176
98192
  var COMMITS_AHEAD_OF_TAG = 0;
98177
98193
 
98178
98194
  // gateway/boot-version.ts
@@ -2,6 +2,56 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Added (switchroom divergence)
6
+
7
+ - **Per-row `observation_scopes` on every retain.** Hindsight accepts and
8
+ stores an `observation_scopes` field per retained item; `"shared"` makes
9
+ consolidation write that item's observations into ONE global untagged scope
10
+ instead of a scope per tag. The plugin had no way to send it, so a bank
11
+ several agents write into could not pool their observations. Added as an
12
+ explicit `observation_scopes` kwarg on `HindsightClient.retain()` /
13
+ `_retain_one()` (`scripts/lib/client.py`), sourced from the new
14
+ `observationScopes` config key (`scripts/lib/config.py`, default `None`,
15
+ overridable by `HINDSIGHT_OBSERVATION_SCOPES`, which switchroom's
16
+ `start.sh` exports only when the operator set
17
+ `memory.observation_scopes`).
18
+
19
+ `build_retain_payload` (`scripts/retain.py`) puts the resolved value on the
20
+ payload, and all five hand-enumerated `client.retain()` callsites forward it:
21
+ `retain.py` (Stop hook), `subagent_retain.py` (sidechain),
22
+ `reconcile_tail.py` (boot reconcile), `drain_pending.py` (queue drain) and
23
+ `backfill_transcripts.py`. Each enumerates kwargs rather than splatting the
24
+ payload, so a missed one silently keeps writing per-tag scopes — every path
25
+ is pinned by a test.
26
+
27
+ Two durability properties are load-bearing. The scope rides ON the queued
28
+ payload (`lib/pending` copies the payload wholesale), so a retain that fails
29
+ now and drains hours later lands in the scope it was written for rather than
30
+ whatever config says at drain time. And `drain_pending._retry_one` reads it
31
+ with `entry.get(...)`: entries queued by a build that predates this field are
32
+ on disk right now, and a `KeyError` there would strand the last on-disk copy
33
+ of a turn (the #3244 silent-loss shape).
34
+
35
+ Default behaviour is unchanged and asserted as such: with the config unset
36
+ the kwarg is `None` and `_retain_one` omits the key from the request body
37
+ entirely rather than sending a null, so the body is byte-identical to a
38
+ pre-change client. Split retains carry the same scope on every part.
39
+ Tests: `scripts/tests/test_observation_scopes.py`, plus per-path pins in
40
+ `test_reconcile_durability.py`, `test_subagent_retain.py` and
41
+ `test_backfill.py`.
42
+
43
+ An off-list value RAISES rather than reaching the wire
44
+ (`resolve_observation_scopes` in `scripts/lib/config.py`, called from
45
+ `build_retain_payload`). Switchroom's config schema is the primary gate, but
46
+ it cannot see a hand-edited `settings.json` or a raw
47
+ `HINDSIGHT_OBSERVATION_SCOPES` export — and a scope is invisible after the
48
+ write, so a value quietly ignored today reads as a bank whose observations
49
+ never merged months later. Empty/whitespace stays UNSET, matching the
50
+ existing "an empty export hands authority back to the config file" idiom.
51
+ The accepted tuple (`OBSERVATION_SCOPES_VALUES`) is paired with
52
+ `src/memory/observation-scopes.ts` on the switchroom side; widening the set
53
+ means widening both.
54
+
5
55
  ### Changed (switchroom divergence)
6
56
 
7
57
  - **`MAX_DIRECTIVES` 15 → 30, and truncation is no longer SILENT**
@@ -474,6 +474,7 @@ class Backfill:
474
474
  tags=payload["tags"],
475
475
  timeout=15,
476
476
  async_processing=False, # commit-before-ack (daemon contract 2)
477
+ observation_scopes=payload.get("observation_scopes"),
477
478
  )
478
479
  except Exception as e:
479
480
  debug_log(self.config, f"backfill: POST failed for {built['document_id']}: {e}")
@@ -600,6 +600,10 @@ def _retry_one(entry: dict, timeout: int) -> None:
600
600
  tags=entry.get("tags"),
601
601
  timeout=timeout,
602
602
  async_processing=False,
603
+ # `.get` (not `[...]`): entries queued by a pre-#observation_scopes build
604
+ # are on disk RIGHT NOW and carry no such key. They must drain, not
605
+ # KeyError — a crash here loses the queue entry's only copy of a turn.
606
+ observation_scopes=entry.get("observation_scopes"),
603
607
  )
604
608
 
605
609
 
@@ -177,9 +177,17 @@ class HindsightClient:
177
177
  tags: Optional[list] = None,
178
178
  timeout: int = 15,
179
179
  async_processing: bool = True,
180
+ observation_scopes: Optional[str] = None,
180
181
  ) -> dict:
181
182
  """Retain content into a bank's memory.
182
183
 
184
+ ``observation_scopes`` is a per-row Hindsight field controlling which
185
+ observation scope consolidation writes this item's observations into.
186
+ ``"shared"`` puts them in ONE global untagged scope instead of a scope
187
+ per tag. ``None`` (the default) omits the field entirely, so the
188
+ engine's own default stands and the wire body is byte-identical to a
189
+ pre-``observation_scopes`` client.
190
+
183
191
  By default posts with ``async=true`` so the server processes extraction
184
192
  in the background (a 200 is an ack-of-receipt, not proof of durable
185
193
  persistence). The context field helps Hindsight cluster memories by
@@ -253,6 +261,7 @@ class HindsightClient:
253
261
  tags=tags,
254
262
  timeout=part_timeout,
255
263
  async_processing=async_processing,
264
+ observation_scopes=observation_scopes,
256
265
  )
257
266
  if total > 1 and isinstance(response, dict):
258
267
  response = dict(response)
@@ -269,6 +278,7 @@ class HindsightClient:
269
278
  tags: Optional[list],
270
279
  timeout: int,
271
280
  async_processing: bool,
281
+ observation_scopes: Optional[str] = None,
272
282
  ) -> dict:
273
283
  """POST exactly one retain item. Raises on any HTTP/transport error."""
274
284
  path = f"/v1/default/banks/{urllib.parse.quote(bank_id, safe='')}/memories"
@@ -281,6 +291,10 @@ class HindsightClient:
281
291
  item["context"] = context
282
292
  if tags:
283
293
  item["tags"] = tags
294
+ # Omitted entirely when unset — the engine default must stay in force
295
+ # and the body must match a pre-observation_scopes client byte for byte.
296
+ if observation_scopes:
297
+ item["observation_scopes"] = observation_scopes
284
298
  body = {
285
299
  "items": [item],
286
300
  "async": bool(async_processing),
@@ -172,6 +172,15 @@ DEFAULTS = {
172
172
  "retainContext": "claude-code",
173
173
  "retainTags": [],
174
174
  "retainMetadata": {},
175
+ # Switchroom-local: per-row Hindsight `observation_scopes` on every retain.
176
+ # `"shared"` makes consolidation write this item's observations into ONE
177
+ # global untagged scope instead of a scope per tag — what a set of agents
178
+ # pooling one bank needs. `None` (the default) omits the field from the
179
+ # wire body entirely, leaving the engine's own default in force. Set by
180
+ # start.sh from `agents.<name>.memory.observation_scopes` (cascading
181
+ # through `defaults.memory.observation_scopes`) via
182
+ # HINDSIGHT_OBSERVATION_SCOPES, exported ONLY when the operator opted in.
183
+ "observationScopes": None,
175
184
  # Switchroom hindsight-leverage E2 / PR9 (#398) — lesson & anti-pattern
176
185
  # tagging at retain time. When on (default), build_retain_payload scans the
177
186
  # formatted transcript slice for explicit lesson / anti-pattern markers and
@@ -319,6 +328,11 @@ ENV_OVERRIDES = {
319
328
  "HINDSIGHT_AUTO_RECALL": ("autoRecall", bool),
320
329
  "HINDSIGHT_AUTO_RETAIN": ("autoRetain", bool),
321
330
  "HINDSIGHT_RETAIN_MODE": ("retainMode", str),
331
+ # Switchroom-local: per-row observation scope on retains. Set by start.sh
332
+ # from agents.<name>.memory.observation_scopes (cascading through
333
+ # defaults.memory.observation_scopes) ONLY when the operator set it; unset
334
+ # leaves `observationScopes` None and the field off the wire entirely.
335
+ "HINDSIGHT_OBSERVATION_SCOPES": ("observationScopes", str),
322
336
  # Switchroom hindsight-leverage E2 / PR9 (#398) — lesson/anti-pattern tagging
323
337
  # + recall demotion toggles and overrides.
324
338
  "HINDSIGHT_LESSON_TAGGING": ("lessonTagging", bool),
@@ -422,6 +436,83 @@ ENV_OVERRIDES = {
422
436
  }
423
437
 
424
438
 
439
+ #: Switchroom-local: the `observation_scopes` values Hindsight accepts as a
440
+ #: bare string (`MemoryItem.observation_scopes`, typed
441
+ #: `Literal["per_tag","combined","all_combinations","shared"] | list[list[str]]
442
+ #: | None` server-side). The explicit list-of-lists tag matrix is deliberately
443
+ #: NOT exposed through switchroom config: unbounded, no safe fleet-wide
444
+ #: default, no caller needs it. Paired with `OBSERVATION_SCOPES` in
445
+ #: src/memory/observation-scopes.ts, which the zod enum reads — widening the
446
+ #: set means widening BOTH.
447
+ OBSERVATION_SCOPES_VALUES = ("per_tag", "combined", "all_combinations", "shared")
448
+
449
+
450
+ def classify_observation_scopes(config: dict):
451
+ """Classify ``observationScopes`` WITHOUT raising: ``(value, error)``.
452
+
453
+ Exactly one of the two is non-``None``:
454
+
455
+ * ``(None, None)`` — unset. Do not put the field on the wire at all;
456
+ the shipped default, byte-for-byte the pre-plumbing request body.
457
+ * ``(value, None)`` — a valid member of :data:`OBSERVATION_SCOPES_VALUES`.
458
+ * ``(None, reason)`` — an off-list or non-string value, with a
459
+ human-readable reason naming the accepted set.
460
+
461
+ THIS FUNCTION MUST NEVER RAISE, and callers on the retain path must never
462
+ turn its ``error`` into one. A bad scope is a misconfiguration; losing the
463
+ turn is data loss. Those are not the same severity and must not share a
464
+ failure mode — see ``retain.build_retain_payload`` for the consequence
465
+ chain (a raise there propagated out of ``run_retain`` and past
466
+ ``retain.main``'s ``pending_enqueue``, so the turn was never queued,
467
+ the watermark never advanced, and the boot reconciler swallowed the same
468
+ raise into ``debug_log`` — the memory was gone, permanently and silently).
469
+ That is switchroom #3244's shape, which this very feature cites.
470
+
471
+ An empty/whitespace-only value is treated as UNSET, matching the plugin's
472
+ existing "an empty export hands authority back to the config file" idiom
473
+ (see ``_cast_env``): an absent knob, not a typo'd one.
474
+ """
475
+ raw = config.get("observationScopes")
476
+ if raw is None:
477
+ return None, None
478
+ if not isinstance(raw, str):
479
+ return None, (
480
+ "observationScopes must be a string, one of "
481
+ f"{', '.join(OBSERVATION_SCOPES_VALUES)}; got {type(raw).__name__} ({raw!r}). "
482
+ "Set it via `memory.observation_scopes` in switchroom.yaml."
483
+ )
484
+ value = raw.strip()
485
+ if not value:
486
+ return None, None
487
+ if value not in OBSERVATION_SCOPES_VALUES:
488
+ return None, (
489
+ f"observationScopes={raw!r} is not a valid Hindsight observation scope. "
490
+ f"Accepted values: {', '.join(OBSERVATION_SCOPES_VALUES)}. "
491
+ "Set it via `memory.observation_scopes` in switchroom.yaml "
492
+ "(a typo there is rejected at `switchroom apply`)."
493
+ )
494
+ return value, None
495
+
496
+
497
+ def resolve_observation_scopes(config: dict):
498
+ """Strict form of :func:`classify_observation_scopes` — raises on a bad value.
499
+
500
+ ``None`` means "do not put the field on the wire at all".
501
+
502
+ Raises ``ValueError`` on any value outside
503
+ :data:`OBSERVATION_SCOPES_VALUES`. This is the VALIDATOR, for callers that
504
+ genuinely want to fail — a config check, a test, a hand-run script that
505
+ should stop before it writes anything. **It is deliberately NOT what the
506
+ retain path calls**: a retain must never be destroyed by a config typo, so
507
+ ``retain.build_retain_payload`` uses the non-raising classifier and shouts
508
+ instead. See ``classify_observation_scopes``.
509
+ """
510
+ value, error = classify_observation_scopes(config)
511
+ if error:
512
+ raise ValueError(error)
513
+ return value
514
+
515
+
425
516
  def _cast_env(value: str, typ):
426
517
  """Cast environment variable string to target type. Returns None on failure."""
427
518
  try:
@@ -41,12 +41,18 @@ Each entry is a JSON file ``<unix-ms>-<short-uuid>.json`` containing::
41
41
  "context": "<retainContext>",
42
42
  "metadata": {...},
43
43
  "tags": [...] or null,
44
+ "observation_scopes": "<scope>" or null,
44
45
  "failed_at": "<ISO-8601 UTC>",
45
46
  "error_class": "<exception class name>",
46
47
  "error_message": "<str(e)>",
47
48
  "attempt_count": 1
48
49
  }
49
50
 
51
+ ``observation_scopes`` is ABSENT from every entry queued by a build that
52
+ predates it, and those entries are on disk right now. Readers must use
53
+ ``entry.get("observation_scopes")``, never ``entry[...]`` — a KeyError in
54
+ ``drain_pending._retry_one`` would strand the last on-disk copy of a turn.
55
+
50
56
  The file is written via ``write tmp + rename`` so concurrent agents
51
57
  sharing ``$HOME`` (legacy installs) never observe a half-written entry.
52
58
 
@@ -312,6 +312,7 @@ def _post_inline(
312
312
  tags=payload["tags"],
313
313
  timeout=15,
314
314
  async_processing=False,
315
+ observation_scopes=payload.get("observation_scopes"),
315
316
  )
316
317
  except Exception as e:
317
318
  debug_log(config, f"reconcile_tail: inline POST failed, enqueuing: {e}")