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.
- package/dist/agent-scheduler/index.js +12 -1
- package/dist/auth-broker/index.js +14 -2
- package/dist/cli/notion-write-pretool.mjs +14 -1
- package/dist/cli/switchroom.js +71 -11
- package/dist/host-control/main.js +17 -3
- package/dist/vault/approvals/kernel-server.js +19 -3
- package/dist/vault/broker/server.js +19 -3
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +9 -0
- package/telegram-plugin/dist/gateway/gateway.js +23 -7
- package/vendor/hindsight-memory/CHANGELOG.md +50 -0
- package/vendor/hindsight-memory/scripts/backfill_transcripts.py +1 -0
- package/vendor/hindsight-memory/scripts/drain_pending.py +4 -0
- package/vendor/hindsight-memory/scripts/lib/client.py +14 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +91 -0
- package/vendor/hindsight-memory/scripts/lib/pending.py +6 -0
- package/vendor/hindsight-memory/scripts/reconcile_tail.py +1 -0
- package/vendor/hindsight-memory/scripts/retain.py +41 -1
- package/vendor/hindsight-memory/scripts/subagent_retain.py +1 -0
- package/vendor/hindsight-memory/scripts/tests/test_backfill.py +23 -1
- package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +5 -1
- package/vendor/hindsight-memory/scripts/tests/test_observation_scopes.py +325 -0
- package/vendor/hindsight-memory/scripts/tests/test_reconcile_durability.py +160 -1
- package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +40 -2
|
@@ -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.
|
|
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.
|
|
98173
|
-
var COMMIT_SHA = "
|
|
98174
|
-
var COMMIT_DATE = "2026-07-
|
|
98175
|
-
var LATEST_PR =
|
|
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}")
|