switchroom 0.19.17 → 0.19.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/bin/run-hook.sh +148 -0
  2. package/bin/workspace-dynamic-hook.sh +147 -38
  3. package/dist/agent-scheduler/index.js +13 -4
  4. package/dist/auth-broker/index.js +32 -5
  5. package/dist/cli/drive-write-pretool.mjs +48 -5
  6. package/dist/cli/ms-365-write-pretool.mjs +40 -2
  7. package/dist/cli/notion-write-pretool.mjs +13 -4
  8. package/dist/cli/switchroom.js +10614 -8104
  9. package/dist/host-control/main.js +12849 -11446
  10. package/dist/vault/approvals/kernel-server.js +90 -12
  11. package/dist/vault/broker/server.js +277 -94
  12. package/package.json +5 -3
  13. package/profiles/_base/start.sh.hbs +69 -5
  14. package/profiles/coding/CLAUDE.md.hbs +1 -1
  15. package/profiles/default/CLAUDE.md.hbs +3 -3
  16. package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
  17. package/profiles/health-coach/CLAUDE.md.hbs +1 -1
  18. package/skills/mental-model-curator/SKILL.md +8 -6
  19. package/telegram-plugin/bridge/bridge.ts +25 -19
  20. package/telegram-plugin/bridge/mcp-instructions.ts +87 -0
  21. package/telegram-plugin/dist/bridge/bridge.js +28 -20
  22. package/telegram-plugin/dist/gateway/gateway.js +2077 -1087
  23. package/telegram-plugin/dist/server.js +32 -20
  24. package/telegram-plugin/gateway/always-allow-persist-queue.ts +97 -11
  25. package/telegram-plugin/gateway/boot-card.ts +5 -1
  26. package/telegram-plugin/gateway/boot-probes.ts +113 -0
  27. package/telegram-plugin/gateway/config-approval-handler.test.ts +54 -0
  28. package/telegram-plugin/gateway/config-approval-handler.ts +16 -1
  29. package/telegram-plugin/gateway/disconnect-flush.ts +17 -0
  30. package/telegram-plugin/gateway/gateway.ts +43 -1
  31. package/telegram-plugin/gateway/handback-preturn-signal.ts +61 -7
  32. package/telegram-plugin/gateway/ipc-protocol.ts +5 -0
  33. package/telegram-plugin/gateway/ipc-server.ts +13 -0
  34. package/telegram-plugin/gateway/liveness-wiring.ts +125 -5
  35. package/telegram-plugin/gateway/missed-approvals-store.ts +66 -17
  36. package/telegram-plugin/gateway/obligation-ledger.ts +84 -4
  37. package/telegram-plugin/gateway/pending-card-store.ts +46 -16
  38. package/telegram-plugin/gateway/resume-inbound-builder.ts +13 -4
  39. package/telegram-plugin/gateway/scoped-grant-store.ts +39 -14
  40. package/telegram-plugin/gateway/store-file.ts +244 -0
  41. package/telegram-plugin/gateway/stream-render.ts +24 -5
  42. package/telegram-plugin/hooks/secret-guard-pretool.mjs +249 -76
  43. package/telegram-plugin/hooks/tool-label-pretool.mjs +88 -2
  44. package/telegram-plugin/registry/turns-schema.test.ts +8 -3
  45. package/telegram-plugin/registry/turns-schema.ts +40 -12
  46. package/telegram-plugin/runtime-metrics.ts +14 -0
  47. package/telegram-plugin/silence-poke.ts +138 -0
  48. package/telegram-plugin/tests/boot-probe-drift.test.ts +152 -0
  49. package/telegram-plugin/tests/bridge-tool-parity.test.ts +95 -0
  50. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +32 -0
  51. package/telegram-plugin/tests/handback-preturn-signal.test.ts +62 -0
  52. package/telegram-plugin/tests/helpers/liveness-wiring-fixture.ts +178 -0
  53. package/telegram-plugin/tests/ipc-server-validate-config-approval.test.ts +95 -0
  54. package/telegram-plugin/tests/mcp-instructions-budget.test.ts +184 -0
  55. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +22 -2
  56. package/telegram-plugin/tests/obligation-determinism.test.ts +114 -3
  57. package/telegram-plugin/tests/obligation-ledger.test.ts +310 -0
  58. package/telegram-plugin/tests/registry-turns.test.ts +13 -0
  59. package/telegram-plugin/tests/resume-inbound-builder.test.ts +15 -0
  60. package/telegram-plugin/tests/secret-guard-pretool.test.ts +347 -16
  61. package/telegram-plugin/tests/silence-poke-orphan-reap.test.ts +392 -0
  62. package/telegram-plugin/tests/silence-poke-teardown-notice.test.ts +301 -0
  63. package/telegram-plugin/tests/store-atomic-write.test.ts +411 -0
  64. package/telegram-plugin/tests/stream-render-golden.test.ts +103 -1
  65. package/telegram-plugin/tests/tool-activity-summary.test.ts +9 -2
  66. package/telegram-plugin/tests/tool-label-pretool.test.ts +94 -0
  67. package/telegram-plugin/tests/tts-normalize.test.ts +43 -0
  68. package/telegram-plugin/tests/voice-normalize-text.test.ts +212 -3
  69. package/telegram-plugin/tests/worker-feed-repeat-steps.test.ts +147 -0
  70. package/telegram-plugin/tts-normalize.ts +6 -4
  71. package/telegram-plugin/voice-normalize-text.ts +168 -11
  72. package/telegram-plugin/worker-activity-feed.ts +51 -1
  73. package/vendor/hindsight-memory/CHANGELOG.md +73 -0
  74. package/vendor/hindsight-memory/scripts/drain_pending.py +668 -56
  75. package/vendor/hindsight-memory/scripts/lib/client.py +124 -0
  76. package/vendor/hindsight-memory/scripts/lib/config.py +8 -3
  77. package/vendor/hindsight-memory/scripts/lib/directives.py +62 -4
  78. package/vendor/hindsight-memory/scripts/lib/pending.py +865 -33
  79. package/vendor/hindsight-memory/scripts/lib/retain_split.py +449 -0
  80. package/vendor/hindsight-memory/scripts/recall.py +257 -12
  81. package/vendor/hindsight-memory/scripts/retain.py +12 -6
  82. package/vendor/hindsight-memory/scripts/session_start.py +48 -0
  83. package/vendor/hindsight-memory/scripts/tests/test_client_document_exists.py +470 -0
  84. package/vendor/hindsight-memory/scripts/tests/test_directives.py +80 -9
  85. package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +2121 -0
  86. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +362 -18
  87. package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +430 -0
  88. package/vendor/hindsight-memory/scripts/tests/test_session_start_version_skew.py +204 -0
  89. package/vendor/hindsight-memory/settings.json +1 -1
  90. package/vendor/hindsight-memory/tests/test_drain_pending.py +102 -6
  91. package/vendor/hindsight-memory/tests/test_pending.py +32 -7
@@ -5300,6 +5300,34 @@ function truncateLine2(s) {
5300
5300
  return cleaned.length <= 60 ? cleaned : `${cleaned.slice(0, 59)}\u2026`;
5301
5301
  }
5302
5302
 
5303
+ // src/vault/approvals/gated-write-policy.ts
5304
+ var REQUIRE_OPERATOR_WRITE_ENV = "SWITCHROOM_REQUIRE_OPERATOR_APPROVAL_WRITE";
5305
+ function requireOperatorApprovalForWrites(env = process.env) {
5306
+ return env[REQUIRE_OPERATOR_WRITE_ENV] === "1";
5307
+ }
5308
+ function isGatedWriteApproved(lookup, requireOperator) {
5309
+ if (lookup.state !== "granted") {
5310
+ return { allow: false, reason: `approval state is '${lookup.state ?? "unknown"}', not 'granted'` };
5311
+ }
5312
+ if (!requireOperator)
5313
+ return { allow: true };
5314
+ if (lookup.origin !== "operator") {
5315
+ return {
5316
+ allow: false,
5317
+ reason: `approval was recorded with origin='${lookup.origin ?? "unknown"}', not 'operator' \u2014 ` + `an agent-origin decision is not proof an operator tapped ` + `(${REQUIRE_OPERATOR_WRITE_ENV}=1)`
5318
+ };
5319
+ }
5320
+ return { allow: true };
5321
+ }
5322
+ function evaluateGatedWrite(lookup, requireOperator) {
5323
+ const verdict = isGatedWriteApproved(lookup, requireOperator);
5324
+ if (verdict.allow)
5325
+ return { kind: "allow" };
5326
+ if (lookup.state === "granted")
5327
+ return { kind: "block", reason: verdict.reason };
5328
+ return { kind: "wait" };
5329
+ }
5330
+
5303
5331
  // src/cli/drive-write-pretool.ts
5304
5332
  var HOOK_TIMEOUT_MS = 5 * 60 * 1000;
5305
5333
  var KERNEL_POLL_INTERVAL_MS = 2000;
@@ -5383,9 +5411,15 @@ async function approvalLookup(scope, approverSet) {
5383
5411
  current_approver_set: approverSet
5384
5412
  });
5385
5413
  if (r === null || r.ok !== true)
5386
- return null;
5387
- return typeof r.state === "string" ? r.state : null;
5414
+ return { state: null };
5415
+ const dec = r.decision;
5416
+ const origin = dec != null && (dec.origin === "operator" || dec.origin === "agent") ? dec.origin : undefined;
5417
+ return {
5418
+ state: typeof r.state === "string" ? r.state : null,
5419
+ origin
5420
+ };
5388
5421
  }
5422
+ var REQUIRE_OPERATOR_ORIGIN = requireOperatorApprovalForWrites();
5389
5423
  function loadAllowFrom() {
5390
5424
  const stateDir = process.env.TELEGRAM_STATE_DIR ?? join2(homedir2(), ".claude", "channels", "telegram");
5391
5425
  const accessPath = join2(stateDir, "access.json");
@@ -5500,9 +5534,13 @@ async function main() {
5500
5534
  block("no operator paired \u2014 cannot post approval card");
5501
5535
  }
5502
5536
  const existing = await approvalLookup(scope, allowFrom);
5503
- if (existing === "granted") {
5537
+ const existingAction = evaluateGatedWrite(existing, REQUIRE_OPERATOR_ORIGIN);
5538
+ if (existingAction.kind === "allow") {
5504
5539
  allow();
5505
5540
  }
5541
+ if (existingAction.kind === "block") {
5542
+ block(`Drive write refused: ${existingAction.reason}`);
5543
+ }
5506
5544
  let handle;
5507
5545
  try {
5508
5546
  handle = await loadFromAuthBroker();
@@ -5546,10 +5584,15 @@ async function main() {
5546
5584
  if (!first) {
5547
5585
  await new Promise((r) => setTimeout(r, KERNEL_POLL_INTERVAL_MS));
5548
5586
  }
5549
- const state = await approvalLookup(scope, allowFrom);
5550
- if (state === "granted") {
5587
+ const lookup = await approvalLookup(scope, allowFrom);
5588
+ const state = lookup.state;
5589
+ const action = evaluateGatedWrite(lookup, REQUIRE_OPERATOR_ORIGIN);
5590
+ if (action.kind === "allow") {
5551
5591
  allow();
5552
5592
  }
5593
+ if (action.kind === "block") {
5594
+ block(`Drive write refused: ${action.reason}`);
5595
+ }
5553
5596
  if (state === "denied" || state === "drift_revoked") {
5554
5597
  block(`user denied Drive write (kernel verdict: ${state})`);
5555
5598
  }
@@ -4944,6 +4944,34 @@ function buildBatchAbortReason(appliedInBatch) {
4944
4944
  return `grant lapsed mid-batch \u2014 ${n} MS-365 write${n === 1 ? "" : "s"} already ` + `applied in this batch; remaining ops aborted. STOP: do not retry blindly \u2014 ` + `reconcile what landed before re-issuing.`;
4945
4945
  }
4946
4946
 
4947
+ // src/vault/approvals/gated-write-policy.ts
4948
+ var REQUIRE_OPERATOR_WRITE_ENV = "SWITCHROOM_REQUIRE_OPERATOR_APPROVAL_WRITE";
4949
+ function requireOperatorApprovalForWrites(env = process.env) {
4950
+ return env[REQUIRE_OPERATOR_WRITE_ENV] === "1";
4951
+ }
4952
+ function isGatedWriteApproved(lookup, requireOperator) {
4953
+ if (lookup.state !== "granted") {
4954
+ return { allow: false, reason: `approval state is '${lookup.state ?? "unknown"}', not 'granted'` };
4955
+ }
4956
+ if (!requireOperator)
4957
+ return { allow: true };
4958
+ if (lookup.origin !== "operator") {
4959
+ return {
4960
+ allow: false,
4961
+ reason: `approval was recorded with origin='${lookup.origin ?? "unknown"}', not 'operator' \u2014 ` + `an agent-origin decision is not proof an operator tapped ` + `(${REQUIRE_OPERATOR_WRITE_ENV}=1)`
4962
+ };
4963
+ }
4964
+ return { allow: true };
4965
+ }
4966
+ function evaluateGatedWrite(lookup, requireOperator) {
4967
+ const verdict = isGatedWriteApproved(lookup, requireOperator);
4968
+ if (verdict.allow)
4969
+ return { kind: "allow" };
4970
+ if (lookup.state === "granted")
4971
+ return { kind: "block", reason: verdict.reason };
4972
+ return { kind: "wait" };
4973
+ }
4974
+
4947
4975
  // src/cli/ms-365-write-pretool.ts
4948
4976
  var HOOK_TIMEOUT_MS = 5 * 60 * 1000;
4949
4977
  var KERNEL_POLL_INTERVAL_MS = 2000;
@@ -5350,8 +5378,14 @@ async function approvalLookupByRequest(agentUnit, requestId, approverSet) {
5350
5378
  });
5351
5379
  if (!res.ok)
5352
5380
  return null;
5353
- return res.value;
5381
+ const value = res.value;
5382
+ const o = value.decision?.origin;
5383
+ return {
5384
+ state: value.state,
5385
+ origin: o === "operator" || o === "agent" ? o : undefined
5386
+ };
5354
5387
  }
5388
+ var REQUIRE_OPERATOR_ORIGIN = requireOperatorApprovalForWrites();
5355
5389
  function fail(reason) {
5356
5390
  process.stdout.write(JSON.stringify({
5357
5391
  decision: "block",
@@ -5426,10 +5460,14 @@ async function main() {
5426
5460
  if (!lookup)
5427
5461
  continue;
5428
5462
  const state = lookup.state;
5429
- if (state === "granted") {
5463
+ const action = evaluateGatedWrite({ state: state ?? null, origin: lookup.origin }, REQUIRE_OPERATOR_ORIGIN);
5464
+ if (action.kind === "allow") {
5430
5465
  recordOutcome(Date.now(), { ...ledgerEntry, outcome: "applied" });
5431
5466
  allow();
5432
5467
  }
5468
+ if (action.kind === "block") {
5469
+ fail(action.reason);
5470
+ }
5433
5471
  if (state === "expired" || state === "drift_revoked") {
5434
5472
  const t = Date.now();
5435
5473
  const applied = countCurrentBatchApplied(readLedger(t).entries, t);
@@ -11190,7 +11190,8 @@ var init_protocol = __esm(() => {
11190
11190
  ttl_expires_at: exports_external.number().nullable(),
11191
11191
  last_used_at: exports_external.number().nullable(),
11192
11192
  revoked_at: exports_external.number().nullable(),
11193
- revoke_reason: exports_external.string().nullable()
11193
+ revoke_reason: exports_external.string().nullable(),
11194
+ origin: exports_external.enum(["agent", "operator"]).optional()
11194
11195
  });
11195
11196
  OkApprovalLookupResponseSchema = exports_external.object({
11196
11197
  ok: exports_external.literal(true),
@@ -11865,7 +11866,7 @@ var AgentMemorySchema = exports_external.object({
11865
11866
  recall: exports_external.object({
11866
11867
  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)."),
11867
11868
  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."),
11868
- min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0\u20131.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default \u2014 " + "current behaviour). Try 0.10\u20130.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
11869
+ min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum containment token overlap [0.0\u20131.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "A cheap FLOOR that removes candidates with (near-)zero lexical " + "relationship to the prompt \u2014 NOT a precision control: at the " + "recommended 0.10 the gate is near-passthrough, and the " + "effective precision control is the engine rerank plus the " + "max_memories head-slice. 0.0 disables (default \u2014 current " + "behaviour). Use 0.10; values at or above 0.20 measurably " + "starve recall \u2014 on production replay 0.20 leaves ~41.9% of " + "turns with NO memories at all, re-creating the bug #3541 " + "fixed. Observe the `overlap_dropped` field via " + "`switchroom memory recall-log`."),
11869
11870
  types: exports_external.array(exports_external.string()).optional().describe("Hindsight fact types to recall. Switchroom default is " + '["world", "experience", "observation"] \u2014 the synthesized ' + "`observation` tier is on by default. Set to " + '["world", "experience"] to opt out of observation-backed ' + "recall for this agent (or fleet-wide under defaults)."),
11870
11871
  additional_banks: exports_external.array(exports_external.string()).optional().describe("Extra Hindsight banks to recall from on every turn, merged into " + "the agent's own bank results \u2014 e.g. a shared operator/household " + "profile bank authored via `switchroom memory profile`. Each is " + "recalled with an 8s timeout and is non-fatal on failure. Stays " + "within the single tenant: all banks are the operator's data, in " + "the operator's Hindsight instance (see the `single-tenant` " + "invariant). Defaults to [] (no extra banks)."),
11871
11872
  sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Per-speaker recall routing: a map of Telegram sender \u2192 extra " + "recall bank. When a message arrives, the agent also recalls the " + "speaker's bank (matched by Telegram username \u2014 a leading @ is " + "optional \u2014 or numeric user_id), merged " + "into its own results \u2014 so each trusted user gets their own " + "profile context. Additive recall scoping within the single " + "tenant: never an access boundary (who may drive an agent stays " + "the per-agent user assignment in `access.allowFrom`). Author the " + "banks via `switchroom memory profile`."),
@@ -12177,9 +12178,16 @@ var ReactionDispatchSchema = exports_external.object({
12177
12178
  enabled: exports_external.boolean().optional().describe("Master switch for the reaction-dispatch path. Default false \u2014 " + "with no reaction_dispatch block, reactions are persisted (and may " + "feed the `reactions` feedback path) but are NEVER dispatched as " + "event-driven inbound turns."),
12178
12179
  emojis: exports_external.array(exports_external.string()).optional().describe('Emoji allowlist that triggers a `<channel event="reaction">` ' + "inbound turn when reacted to any message. Default [] (nothing " + "fires). Cascade mode: REPLACE (not union) \u2014 a layer's list " + "replaces lower layers entirely so an operator can narrow per-agent.")
12179
12180
  }).optional();
12180
- var ReleaseBlock = exports_external.object({
12181
+ var releaseBlockFields = {
12181
12182
  channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
12182
12183
  pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
12184
+ };
12185
+ var ReleaseBlock = exports_external.object(releaseBlockFields).strict().refine((r) => !(r.channel && r.pin), {
12186
+ message: "release.channel and release.pin are mutually exclusive"
12187
+ });
12188
+ var RootReleaseBlock = exports_external.object({
12189
+ ...releaseBlockFields,
12190
+ auto_update: exports_external.boolean().optional().describe("Opt-in unattended fleet auto-update (KEN-131). When true, hostd's " + "release watcher polls the published release version and, on a new " + "release, drives the EXISTING staggered canary rollout " + "(`switchroom rollout --pin vX.Y.Z`) unattended: canary-first, " + "per-agent version assert, durable pin persisted only after the " + "canary is green, abort + operator alert card on canary failure, " + "and compose rollback on a failed apply. Default false \u2014 with it " + "unset/false, behaviour is unchanged.")
12183
12191
  }).strict().refine((r) => !(r.channel && r.pin), {
12184
12192
  message: "release.channel and release.pin are mutually exclusive"
12185
12193
  });
@@ -12423,6 +12431,7 @@ var AutoReleaseCheckSchema = exports_external.object({
12423
12431
  enabled: exports_external.boolean().default(false).describe("When true, hostd polls the remote release tag every " + "`interval_minutes` and applies + restarts the fleet when a new " + "release is detected. Default false \u2014 opt-in."),
12424
12432
  interval_minutes: exports_external.number().int().min(5).max(1440).default(5).describe("Poll interval in minutes. Floor of 5m matches the agent-config " + "cron rate limit; ceiling of 1440m (24h) is a sanity cap."),
12425
12433
  apply_on_detect: exports_external.boolean().default(true).describe("When false, hostd logs `release_detected` but does NOT call " + "update_apply / restart all. Useful for dogfooding the detector " + "without rolling the fleet."),
12434
+ notify_on_detect: exports_external.boolean().default(false).describe("KEN-129 \u2014 operator-in-the-loop update prompt. Only consulted " + "when apply_on_detect is false (auto-apply supersedes notify): " + "a newly detected release posts ONE operator approval card " + "('fleet is behind \u2014 tap to apply') via an admin agent's " + "gateway; Approve runs hostd's update_apply path (fleet-" + "mutation-locked, durable status rows, get_status-pollable). " + "Dedup on release id: the last-notified id persists in " + "~/.switchroom/release-notify-state.json, so a card that " + "reached the operator is never re-posted for the same release."),
12426
12435
  image_ref: exports_external.string().default("ghcr.io/switchroom/switchroom-agent:latest").describe("Image reference whose remote digest is compared to the local " + "image digest. Defaults to the agent image's :latest tag, which " + "is the canonical signal that a release has been promoted.")
12427
12436
  });
12428
12437
  var HostControlConfigSchema = exports_external.object({
@@ -12465,7 +12474,7 @@ var SwitchroomConfigSchema = exports_external.object({
12465
12474
  timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC'").optional().describe("Global default IANA timezone applied to every agent unless the " + "agent (or its profile) declares its own. See the per-agent " + "timezone field for the full cascade and auto-detection fallback.")
12466
12475
  }),
12467
12476
  telegram: TelegramConfigSchema,
12468
- release: ReleaseBlock.optional().describe("Fleet-wide default release-channel pin / pointer for the update " + "flow. Either `channel` (dev|rc|latest) or `pin` (sha-<hex>|v<semver>) " + "\u2014 mutually exclusive. Per-agent `release` REPLACES this entirely."),
12477
+ release: RootReleaseBlock.optional().describe("Fleet-wide default release-channel pin / pointer for the update " + "flow. Either `channel` (dev|rc|latest) or `pin` (sha-<hex>|v<semver>) " + "\u2014 mutually exclusive. Per-agent `release` REPLACES this entirely " + "(except `auto_update`, which is root-only \u2014 a fleet property read by " + "hostd's release watcher, never per-agent)."),
12469
12478
  memory: MemoryBackendConfigSchema.optional(),
12470
12479
  hindsight: HindsightConfigSchema.optional().describe("Fleet-singleton Hindsight (memory backend) configuration. Currently " + "just the LLM knob (provider + model) used for retain/reflect/" + "consolidation. Both fields optional; when unset the container falls " + "back to provider=claude-code + the hard-coded HINDSIGHT_DEFAULT_MODEL " + "so nothing changes for operators who don't set it. Read at container " + "launch by startHindsight() (src/setup/hindsight.ts) \u2014 takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
12471
12480
  vault: VaultConfigSchema.optional(),