switchroom 0.17.6 → 0.18.3

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 (197) hide show
  1. package/bin/workspace-dynamic-hook.sh +12 -13
  2. package/dist/agent-scheduler/index.js +65 -5
  3. package/dist/auth-broker/index.js +6623 -514
  4. package/dist/cli/notion-write-pretool.mjs +64 -4
  5. package/dist/cli/switchroom.js +1888 -1162
  6. package/dist/host-control/main.js +6306 -162
  7. package/dist/vault/approvals/kernel-server.js +6014 -202
  8. package/dist/vault/broker/server.js +6741 -940
  9. package/package.json +1 -1
  10. package/profiles/_base/settings.json.hbs +2 -2
  11. package/profiles/_base/start.sh.hbs +218 -25
  12. package/profiles/coding/CLAUDE.md.hbs +1 -1
  13. package/profiles/default/CLAUDE.md +116 -0
  14. package/profiles/default/CLAUDE.md.hbs +2 -2
  15. package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
  16. package/profiles/health-coach/CLAUDE.md.hbs +1 -1
  17. package/skills/mental-model-curator/SKILL.md +162 -0
  18. package/telegram-plugin/auth-snapshot-format.ts +22 -24
  19. package/telegram-plugin/bridge/bridge.ts +80 -1
  20. package/telegram-plugin/bridge/ipc-client.ts +19 -0
  21. package/telegram-plugin/bridge/permission-ledger.ts +61 -0
  22. package/telegram-plugin/consolidation-legibility.ts +279 -0
  23. package/telegram-plugin/context-exhaustion.ts +124 -0
  24. package/telegram-plugin/dist/bridge/bridge.js +85 -1
  25. package/telegram-plugin/dist/gateway/gateway.js +25802 -8488
  26. package/telegram-plugin/dist/server.js +86 -2
  27. package/telegram-plugin/feed-heartbeat-climb.ts +206 -0
  28. package/telegram-plugin/gateway/activity-card-store.ts +369 -0
  29. package/telegram-plugin/gateway/gateway.ts +1861 -172
  30. package/telegram-plugin/gateway/inbound-delivery-gate.ts +26 -0
  31. package/telegram-plugin/gateway/inbound-spool.ts +22 -0
  32. package/telegram-plugin/gateway/mental-model-propose-card.ts +69 -0
  33. package/telegram-plugin/gateway/mental-model-propose-diff.ts +171 -0
  34. package/telegram-plugin/gateway/mental-model-propose-inbound-builders.ts +147 -0
  35. package/telegram-plugin/gateway/mental-model-propose-resolve.ts +201 -0
  36. package/telegram-plugin/gateway/missed-approvals-card.ts +161 -0
  37. package/telegram-plugin/gateway/missed-approvals-store.ts +167 -0
  38. package/telegram-plugin/gateway/model-command.ts +70 -10
  39. package/telegram-plugin/gateway/permission-rearm.ts +115 -0
  40. package/telegram-plugin/gateway/scoped-grant-store.ts +89 -0
  41. package/telegram-plugin/memory-legibility.ts +217 -0
  42. package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  43. package/telegram-plugin/package.json +6 -0
  44. package/telegram-plugin/quota-watch.ts +4 -6
  45. package/telegram-plugin/registry/turns-schema.test.ts +97 -0
  46. package/telegram-plugin/registry/turns-schema.ts +78 -0
  47. package/telegram-plugin/render/ir.ts +209 -0
  48. package/telegram-plugin/render/parse.ts +363 -0
  49. package/telegram-plugin/render/render.ts +440 -0
  50. package/telegram-plugin/render/rich-render.ts +72 -0
  51. package/telegram-plugin/scoped-approval.ts +59 -0
  52. package/telegram-plugin/silent-end.ts +78 -0
  53. package/telegram-plugin/stream-controller.ts +14 -3
  54. package/telegram-plugin/subagent-watcher.ts +60 -6
  55. package/telegram-plugin/tests/activity-card-store.test.ts +530 -0
  56. package/telegram-plugin/tests/activity-card-wiring.test.ts +88 -0
  57. package/telegram-plugin/tests/auth-command-format2.test.ts +1 -1
  58. package/telegram-plugin/tests/auth-snapshot-format.test.ts +30 -16
  59. package/telegram-plugin/tests/claude-code-event-contract.test.ts +48 -0
  60. package/telegram-plugin/tests/consolidation-legibility.test.ts +224 -0
  61. package/telegram-plugin/tests/emission-authority-facade.test.ts +25 -10
  62. package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +44 -9
  63. package/telegram-plugin/tests/feed-survival.test.ts +39 -0
  64. package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
  65. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +81 -0
  66. package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +82 -0
  67. package/telegram-plugin/tests/inbound-spool.test.ts +105 -0
  68. package/telegram-plugin/tests/liveness-tracker.test.ts +228 -0
  69. package/telegram-plugin/tests/memory-legibility.test.ts +216 -0
  70. package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +67 -0
  71. package/telegram-plugin/tests/mental-model-propose-card.test.ts +56 -0
  72. package/telegram-plugin/tests/mental-model-propose-diff.test.ts +201 -0
  73. package/telegram-plugin/tests/mental-model-propose-inbound-builders.test.ts +68 -0
  74. package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +157 -0
  75. package/telegram-plugin/tests/missed-approvals-card.test.ts +145 -0
  76. package/telegram-plugin/tests/missed-approvals-store.test.ts +147 -0
  77. package/telegram-plugin/tests/missed-approvals-wiring.test.ts +89 -0
  78. package/telegram-plugin/tests/model-command.test.ts +193 -16
  79. package/telegram-plugin/tests/narrative-render.test.ts +125 -0
  80. package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +123 -163
  81. package/telegram-plugin/tests/permission-ledger.test.ts +166 -0
  82. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +1 -1
  83. package/telegram-plugin/tests/permission-rearm-wiring.test.ts +175 -0
  84. package/telegram-plugin/tests/permission-rearm.test.ts +126 -0
  85. package/telegram-plugin/tests/quota-watch.test.ts +1 -4
  86. package/telegram-plugin/tests/rapid-fire-delivery-ordering.test.ts +149 -0
  87. package/telegram-plugin/tests/render/parse-torture.test.ts +136 -0
  88. package/telegram-plugin/tests/render/parse.test.ts +393 -0
  89. package/telegram-plugin/tests/render/render.test.ts +436 -0
  90. package/telegram-plugin/tests/render/rich-render.test.ts +85 -0
  91. package/telegram-plugin/tests/scoped-grant-persist.test.ts +223 -0
  92. package/telegram-plugin/tests/silent-end-transport.test.ts +290 -0
  93. package/telegram-plugin/tests/silent-turn-climb-transport.test.ts +337 -0
  94. package/telegram-plugin/tests/subagent-watcher.test.ts +139 -0
  95. package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +155 -1
  96. package/telegram-plugin/tests/worktree-watch-cwds.test.ts +198 -0
  97. package/telegram-plugin/turn-liveness-floor.ts +35 -1
  98. package/telegram-plugin/uat/assertions.ts +88 -4
  99. package/telegram-plugin/uat/feed-matcher.test.ts +69 -0
  100. package/telegram-plugin/uat/scenarios/fuzz-liveness-climb-dm.test.ts +155 -0
  101. package/telegram-plugin/uat/scenarios/jtbd-directive-capture-nudge-dm.test.ts +185 -0
  102. package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-channel.test.ts +192 -0
  103. package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-dm.test.ts +220 -0
  104. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +137 -0
  105. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +148 -0
  106. package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-channel.test.ts +66 -0
  107. package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-dm.test.ts +61 -0
  108. package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +99 -7
  109. package/telegram-plugin/uat/scenarios/silent-end-recovery-channel.test.ts +136 -0
  110. package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +24 -2
  111. package/telegram-plugin/worktree-watch-cwds.ts +135 -0
  112. package/vendor/hindsight-memory/hooks/hooks.json +9 -0
  113. package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
  114. package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
  115. package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
  116. package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
  117. package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
  118. package/vendor/hindsight-memory/scripts/directive_verify.py +445 -0
  119. package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  120. package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
  121. package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
  122. package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
  123. package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
  124. package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
  125. package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
  126. package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
  127. package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
  128. package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
  129. package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
  130. package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
  131. package/vendor/hindsight-memory/scripts/lib/client.py +11 -1
  132. package/vendor/hindsight-memory/scripts/lib/config.py +46 -2
  133. package/vendor/hindsight-memory/scripts/lib/directives.py +88 -0
  134. package/vendor/hindsight-memory/scripts/lib/switchroom_envelope.py +77 -0
  135. package/vendor/hindsight-memory/scripts/recall.py +217 -10
  136. package/vendor/hindsight-memory/scripts/retain.py +17 -0
  137. package/vendor/hindsight-memory/scripts/setup_hooks.py +9 -0
  138. package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  139. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
  140. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
  141. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
  142. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
  143. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
  144. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
  145. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
  146. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
  147. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
  148. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
  149. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
  150. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
  151. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
  152. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
  153. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
  154. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
  155. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
  156. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
  157. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
  158. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
  159. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
  160. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
  161. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
  162. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
  163. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
  164. package/vendor/hindsight-memory/scripts/tests/test_directive_capture_nudge.py +185 -0
  165. package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +516 -0
  166. package/vendor/hindsight-memory/scripts/tests/test_directives.py +49 -0
  167. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +1 -0
  168. package/vendor/hindsight-memory/scripts/tests/test_retain_window.py +66 -1
  169. package/vendor/hindsight-memory/scripts/tests/test_switchroom_envelope.py +69 -0
  170. package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  171. package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  172. package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
  173. package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
  174. package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
  175. package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
  176. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
  177. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
  178. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
  179. package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
  180. package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
  181. package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  182. package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
  183. package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
  184. package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
  185. package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
  186. package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
  187. package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  188. package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
  189. package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
  190. package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
  191. package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  192. package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
  193. package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
  194. package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
  195. package/vendor/hindsight-memory/tests/test_client.py +43 -0
  196. package/vendor/hindsight-memory/tests/test_recall_exit_codes.py +49 -2
  197. package/vendor/hindsight-memory/tests/test_recall_precision.py +114 -0
@@ -13542,7 +13542,7 @@ var init_zod = __esm(() => {
13542
13542
  function isValidTimezone(value) {
13543
13543
  return TIMEZONE_REGEX.test(value);
13544
13544
  }
13545
- 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, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
13545
+ 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, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
13546
13546
  var init_schema = __esm(() => {
13547
13547
  init_zod();
13548
13548
  CodeRepoEntrySchema = exports_external.object({
@@ -13656,8 +13656,35 @@ var init_schema = __esm(() => {
13656
13656
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
13657
13657
  file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
13658
13658
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
13659
- bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
13660
- retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
13659
+ bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize " + "results. NOTE: this is an alias for the Hindsight engine's " + "`reflect_mission` field (verified live: switchroom's bank_mission " + "lands in `config.reflect_mission`). Prefer `reflect_mission` going " + "forward; `bank_mission` is retained for back-compat. If both are " + "set, `reflect_mission` wins. Cascade: override."),
13660
+ reflect_mission: exports_external.string().optional().describe("Mission/context steering Hindsight Reflect operations (the bank's " + "'who am I / what matters' framing applied during recall). The " + "engine-accurate name for what `bank_mission` sets. Cascade: override."),
13661
+ retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain. Cascade: override."),
13662
+ mental_models: exports_external.array(exports_external.object({
13663
+ name: exports_external.string().min(1).describe("Stable model name (identity key for idempotent ensure). Two " + "declarations with the same name in one agent are rejected."),
13664
+ source_query: exports_external.string().min(1).max(2000).describe("The reflection query the model answers, semantically " + "refreshed from the bank's content. Capped at 2000 chars \u2014 a " + "standing reflection query, not a document; the ceiling also " + "bounds what an agent-proposed model can smuggle past the " + "operator approval card."),
13665
+ refresh_after_consolidation: exports_external.boolean().optional().describe("Refresh this model after each consolidation. Defaults OFF \u2014 " + "refresh adds bounded background model-spend + timeout risk " + "(RFC Phase 5), so it is opt-in per model."),
13666
+ max_tokens: exports_external.number().int().positive().max(8192).optional().describe("Cap on the synthesized model's token size. Upper-bounded at " + "8192 \u2014 a mental model is a standing summary, not a corpus; the " + "ceiling also caps what an agent-proposed model can request.")
13667
+ })).superRefine((models, ctx) => {
13668
+ const seen = new Set;
13669
+ for (let i = 0;i < models.length; i++) {
13670
+ const key = models[i].name;
13671
+ if (seen.has(key)) {
13672
+ ctx.addIssue({
13673
+ code: exports_external.ZodIssueCode.custom,
13674
+ path: [i, "name"],
13675
+ message: `duplicate mental_models name "${key}" \u2014 model names must be ` + `unique within an agent (they are the idempotent-ensure key)`
13676
+ });
13677
+ }
13678
+ seen.add(key);
13679
+ }
13680
+ }).optional().describe("Operator-declared, per-specialist Hindsight mental models (RFC " + "Phase 5). Named, opt-in curated reflections this agent's bank should " + "carry \u2014 e.g. a coach's 'training-plan-state' or a lawyer's " + "'open-matters'. Ensured idempotently at scaffold/reconcile: NOTHING " + "is created unless declared here (zero declarations = zero models, " + "matching post-#2447 behaviour), and no fixed identity model is " + "reintroduced \u2014 'who the user is' stays owned by dedicated profile " + "banks (users.*.profile_bank), never a per-agent model. Per-agent " + "ONLY: intentionally not accepted at the defaults/profile tier, so a " + "model can never be fleet-seeded \u2014 each specialist opts in on its own " + "(the invariant-clean inverse of the retired blind auto-seeding)."),
13681
+ observations_mission: exports_external.string().optional().describe("Steers what the observation-consolidation LLM synthesises from raw " + "facts (the higher-order 'what patterns matter' lens). Cascade: override."),
13682
+ disposition: exports_external.object({
13683
+ skepticism: exports_external.number().int().min(1).max(5).optional().describe("How much the bank doubts unverified claims (1-5; engine default 3)."),
13684
+ literalism: exports_external.number().int().min(1).max(5).optional().describe("How literally the bank reads statements vs inferring intent (1-5; engine default 3)."),
13685
+ empathy: exports_external.number().int().min(1).max(5).optional().describe("How much the bank weights emotional/relational context (1-5; engine default 3).")
13686
+ }).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`)."),
13687
+ 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)."),
13661
13688
  recall: exports_external.object({
13662
13689
  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)."),
13663
13690
  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."),
@@ -13667,7 +13694,11 @@ var init_schema = __esm(() => {
13667
13694
  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`."),
13668
13695
  skip_trivial: exports_external.boolean().optional().describe("Skip recall on plausibly-stateless trivial turns (time/date/" + "greeting). Switchroom default true \u2014 saves the recall arm + " + "injected tokens on turns that never need memory, guarded so it " + "never skips a turn that references user/project/session state. " + "Set false to always run recall."),
13669
13696
  topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) \u2192 soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: \u2026' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
13670
- }).optional().describe("Auto-recall tuning knobs")
13697
+ }).optional().describe("Auto-recall tuning knobs"),
13698
+ retain: exports_external.object({
13699
+ every_n_turns: exports_external.number().int().min(1).optional().describe("How often the Stop hook fires auto-retention, in turns. The " + "vendor plugin default is 10 (a short session can end before " + "retention ever fires); switchroom's scaffold default is 3. " + "Lower = more frequent, smaller retains + tighter crash " + "durability (at 1, every turn is retained before a restart can " + "lose it); higher = fewer, larger retains + less LLM churn. " + "Raised from the historical 1 to 3 because the local reasoning " + "consolidation model (Ollama gpt-oss-20b) ran away on the large " + "overlapping every-turn payloads. Set to 1 for the old " + "every-turn crash-durability guarantee. Min 1. Cascade: " + "per-field merge (agent wins over default)."),
13700
+ overlap_turns: exports_external.number().int().min(0).optional().describe("Extra recent turns included in each chunked retain window on " + "top of `every_n_turns`, so window = overlap_turns + " + "every_n_turns recent turns. Vendor default is 2; switchroom's " + "scaffold default is 1 (smaller payloads for the local " + "reasoning consolidation model). Higher = more redundant " + "context re-sent per fire. Min 0. Cascade: per-field merge.")
13701
+ }).optional().describe("Auto-retain (Stop-hook consolidation) cadence knobs")
13671
13702
  }).optional();
13672
13703
  HookEntrySchema = exports_external.object({
13673
13704
  command: exports_external.string().describe("Shell command to run. Supports ${CLAUDE_CONFIG_DIR} and ${CLAUDE_PLUGIN_ROOT} substitution."),
@@ -13845,6 +13876,21 @@ var init_schema = __esm(() => {
13845
13876
  small_fast_model: exports_external.string().optional().describe("Model id exported as ANTHROPIC_SMALL_FAST_MODEL for the claude CLI's " + "background/fast lane, e.g. 'claude-haiku-4-5-20251001'."),
13846
13877
  tags: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra key/value metadata tags attached to the provisioned LiteLLM " + "virtual key. Merged per-key across cascade layers (agent wins).")
13847
13878
  }).optional().describe("LiteLLM routing config \u2014 opt-in per-agent virtual-key auto-provisioning " + "+ routing env. Default OFF. See LiteLLMConfigSchema doc for the full flow.");
13879
+ HindsightPerOpLlmSchema = exports_external.object({
13880
+ model: exports_external.string().min(1).optional().describe("Per-op model (upstream `HINDSIGHT_API_<OP>_LLM_MODEL`). Absent \u2192 " + "inherit the global `hindsight.llm.model`."),
13881
+ provider: exports_external.string().min(1).optional().describe("Per-op provider (upstream `HINDSIGHT_API_<OP>_LLM_PROVIDER`). " + "Absent \u2192 inherit the global `hindsight.llm.provider`."),
13882
+ base_url: exports_external.string().min(1).optional().describe("Per-op base URL (upstream `HINDSIGHT_API_<OP>_LLM_BASE_URL`). " + "Optional passthrough; absent \u2192 inherit the global."),
13883
+ api_key: exports_external.string().min(1).optional().describe("Per-op API key (upstream `HINDSIGHT_API_<OP>_LLM_API_KEY`). Literal " + "or `vault:` reference. Optional passthrough; absent \u2192 inherit global.")
13884
+ }).describe("Per-operation LLM override. Every field optional; an unset field (or " + "an omitted op block) inherits the global `hindsight.llm.*`, which is " + "already the engine's fallback \u2014 switchroom emits only the vars set.");
13885
+ HindsightConfigSchema = exports_external.object({
13886
+ llm: exports_external.object({
13887
+ provider: exports_external.string().min(1).optional().describe("Hindsight LLM provider (upstream `HINDSIGHT_API_LLM_PROVIDER`). " + "Defaults to `claude-code` (subscription-honest, broker-fed OAuth). " + "Any litellm-routable provider the upstream image supports is valid. " + "Serves as the GLOBAL default for every op absent a per-op override."),
13888
+ model: exports_external.string().min(1).optional().describe("Hindsight LLM model (upstream `HINDSIGHT_API_LLM_MODEL`). Defaults " + "to HINDSIGHT_DEFAULT_MODEL. Any model your LiteLLM proxy can route " + "is valid, e.g. `openrouter/z-ai/glm-5.2` when routing through the " + "fleet proxy. With provider=claude-code this value is ALSO exported " + "as `ANTHROPIC_MODEL` to the claude subprocess. Serves as the GLOBAL " + "default for every op absent a per-op override."),
13889
+ retain: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `retain` LLM op (memory ingestion). Emits " + "`HINDSIGHT_API_RETAIN_LLM_*`. Absent \u2192 uses the global model/provider."),
13890
+ 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."),
13891
+ consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
13892
+ }).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.")
13893
+ });
13848
13894
  MicrosoftWorkspaceConfigSchema = exports_external.object({
13849
13895
  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)."),
13850
13896
  microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional \u2014 public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
@@ -13922,6 +13968,7 @@ var init_schema = __esm(() => {
13922
13968
  auto_recall: exports_external.boolean().optional(),
13923
13969
  file: exports_external.boolean().optional(),
13924
13970
  isolation: exports_external.enum(["default", "strict"]).optional(),
13971
+ directive_capture_nudge: exports_external.boolean().optional(),
13925
13972
  recall: exports_external.object({
13926
13973
  max_memories: exports_external.number().int().min(0).optional(),
13927
13974
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
@@ -14092,6 +14139,7 @@ var init_schema = __esm(() => {
14092
14139
  config: exports_external.object({
14093
14140
  provider: exports_external.string().default("ollama").describe("Embedding provider (ollama, openai, anthropic)"),
14094
14141
  model: exports_external.string().optional().describe("Embedding model (e.g., 'nomic-embed-text')"),
14142
+ llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
14095
14143
  api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
14096
14144
  docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
14097
14145
  url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
@@ -14169,6 +14217,7 @@ var init_schema = __esm(() => {
14169
14217
  telegram: TelegramConfigSchema,
14170
14218
  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."),
14171
14219
  memory: MemoryBackendConfigSchema.optional(),
14220
+ 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`."),
14172
14221
  vault: VaultConfigSchema.optional(),
14173
14222
  auth: exports_external.object({
14174
14223
  active: exports_external.string().min(1).optional().describe("Fleet-wide active Anthropic account label. Every agent without " + "an explicit `agent.auth.override` uses this account. See " + "docs/auth.md for the full model. Set by `switchroom auth use <label>`."),
@@ -14617,6 +14666,16 @@ function mergeAgentConfig(defaultsIn, agentIn) {
14617
14666
  continue;
14618
14667
  if (k === "recall" && base.recall && typeof v === "object" && v !== null && !Array.isArray(v)) {
14619
14668
  combined[k] = { ...base.recall, ...v };
14669
+ } else if (k === "retain" && base.retain && typeof v === "object" && v !== null && !Array.isArray(v)) {
14670
+ combined[k] = {
14671
+ ...base.retain,
14672
+ ...v
14673
+ };
14674
+ } else if (k === "disposition" && base.disposition && typeof v === "object" && v !== null && !Array.isArray(v)) {
14675
+ combined[k] = {
14676
+ ...base.disposition,
14677
+ ...v
14678
+ };
14620
14679
  } else {
14621
14680
  combined[k] = v;
14622
14681
  }
@@ -15065,7 +15124,7 @@ function resolveAgentsDir(config) {
15065
15124
  if (override && override.length > 0 && override.startsWith("/")) {
15066
15125
  return override;
15067
15126
  }
15068
- return resolveDualPath(config.switchroom.agents_dir);
15127
+ return resolveDualPath(config.switchroom.agents_dir || "~/.switchroom/agents");
15069
15128
  }
15070
15129
  function resolvePath(pathStr) {
15071
15130
  return resolveDualPath(pathStr);
@@ -15122,9 +15181,839 @@ var init_users = __esm(() => {
15122
15181
  init_merge();
15123
15182
  });
15124
15183
 
15184
+ // src/agents/pane-lock.ts
15185
+ function withPaneLock(key, fn) {
15186
+ const tail = tails.get(key) ?? Promise.resolve();
15187
+ const run = tail.then(() => fn());
15188
+ const next = run.then(() => {}, () => {});
15189
+ tails.set(key, next);
15190
+ next.then(() => {
15191
+ if (tails.get(key) === next)
15192
+ tails.delete(key);
15193
+ });
15194
+ return run;
15195
+ }
15196
+ var tails;
15197
+ var init_pane_lock = __esm(() => {
15198
+ tails = new Map;
15199
+ });
15200
+
15201
+ // src/agents/inject.ts
15202
+ var exports_inject = {};
15203
+ __export(exports_inject, {
15204
+ validateInjectCommand: () => validateInjectCommand,
15205
+ normalizeInjectCommand: () => normalizeInjectCommand,
15206
+ makeTmuxRunner: () => makeTmuxRunner,
15207
+ isTuiChromeLine: () => isTuiChromeLine,
15208
+ injectSlashCommandWith: () => injectSlashCommandWith,
15209
+ injectSlashCommand: () => injectSlashCommand,
15210
+ diffPane: () => diffPane,
15211
+ dedupeInjectQueue: () => dedupeInjectQueue,
15212
+ InjectError: () => InjectError,
15213
+ INJECT_COMMANDS: () => INJECT_COMMANDS,
15214
+ INJECT_BLOCKLIST: () => INJECT_BLOCKLIST,
15215
+ INJECT_BLOCKED: () => INJECT_BLOCKED,
15216
+ INJECT_ALLOWLIST: () => INJECT_ALLOWLIST
15217
+ });
15218
+ import { execFileSync } from "node:child_process";
15219
+ function validateInjectCommand(command) {
15220
+ if (typeof command !== "string" || command.trim().length === 0) {
15221
+ throw new InjectError("invalid", "command is empty");
15222
+ }
15223
+ const trimmed = command.trim();
15224
+ if (!trimmed.startsWith("/")) {
15225
+ throw new InjectError("invalid", `command must start with "/": got ${trimmed}`);
15226
+ }
15227
+ const bare = trimmed.split(/\s+/, 1)[0].toLowerCase();
15228
+ const blockedMeta = INJECT_BLOCKED.get(bare);
15229
+ if (blockedMeta) {
15230
+ throw new InjectError("blocked", `${bare} is explicitly blocked from inject (${blockedMeta.reason}).`);
15231
+ }
15232
+ if (!INJECT_COMMANDS.has(bare)) {
15233
+ const allowed = [...INJECT_COMMANDS.keys()].sort().join(", ");
15234
+ throw new InjectError("not_allowed", `${bare} is not in the inject allowlist. Allowed: ${allowed}`);
15235
+ }
15236
+ return bare;
15237
+ }
15238
+ function normalizeInjectCommand(command) {
15239
+ return command.trim().replace(/\s+/g, " ").toLowerCase();
15240
+ }
15241
+ function dedupeInjectQueue(commands) {
15242
+ const seen = new Set;
15243
+ const out = [];
15244
+ for (const raw of commands) {
15245
+ if (typeof raw !== "string")
15246
+ continue;
15247
+ const key = normalizeInjectCommand(raw);
15248
+ if (key.length === 0)
15249
+ continue;
15250
+ if (seen.has(key))
15251
+ continue;
15252
+ seen.add(key);
15253
+ out.push(raw.trim());
15254
+ }
15255
+ return out;
15256
+ }
15257
+ function defaultSocketName(agentName) {
15258
+ return `switchroom-${agentName}`;
15259
+ }
15260
+ function makeTmuxRunner(tmuxBin) {
15261
+ return {
15262
+ capture(socket, session) {
15263
+ try {
15264
+ return execFileSync(tmuxBin, ["-L", socket, "capture-pane", "-p", "-t", session, "-S", "-200"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
15265
+ } catch {
15266
+ return null;
15267
+ }
15268
+ },
15269
+ send(socket, session, args) {
15270
+ const [subcmd, ...rest] = args;
15271
+ const flagEnd = rest.findIndex((a) => !a.startsWith("-"));
15272
+ const flagsBeforeKeys = flagEnd === -1 ? rest : rest.slice(0, flagEnd);
15273
+ const keys = flagEnd === -1 ? [] : rest.slice(flagEnd);
15274
+ execFileSync(tmuxBin, ["-L", socket, subcmd, ...flagsBeforeKeys, "-t", session, ...keys], { stdio: ["pipe", "pipe", "pipe"] });
15275
+ },
15276
+ hasSession(socket, session) {
15277
+ try {
15278
+ execFileSync(tmuxBin, ["-L", socket, "has-session", "-t", session], {
15279
+ stdio: ["pipe", "pipe", "pipe"]
15280
+ });
15281
+ return true;
15282
+ } catch {
15283
+ return false;
15284
+ }
15285
+ }
15286
+ };
15287
+ }
15288
+ function sleep(ms) {
15289
+ return new Promise((res) => setTimeout(res, ms));
15290
+ }
15291
+ function isTuiChromeLine(line) {
15292
+ const t = line.trim();
15293
+ if (t.length === 0)
15294
+ return false;
15295
+ if (/^[\s\-_\u2502\u2500\u256d\u256e\u2570\u256f\u250c\u2510\u2514\u2518|]+$/.test(t))
15296
+ return true;
15297
+ if (/^[\u256d\u256e\u2570\u256f\u2502\s]*[>)\u276f][\u256d\u256e\u2570\u256f\u2502\s]*$/.test(t))
15298
+ return true;
15299
+ if (/accept edits on/i.test(t))
15300
+ return true;
15301
+ if (/shift\+tab to cycle/i.test(t))
15302
+ return true;
15303
+ if (/for agents/i.test(t))
15304
+ return true;
15305
+ if (/\? for shortcuts/i.test(t))
15306
+ return true;
15307
+ if (/bypassing permissions/i.test(t))
15308
+ return true;
15309
+ if (t === "copy")
15310
+ return true;
15311
+ return false;
15312
+ }
15313
+ function diffPane(before, after, command) {
15314
+ if (command) {
15315
+ const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15316
+ const lines = after.split(`
15317
+ `);
15318
+ let anchorIdx = -1;
15319
+ for (let i = lines.length - 1;i >= 0; i--) {
15320
+ if (/[\u276f>]\s+/.test(lines[i]) && lines[i].includes(escaped)) {
15321
+ anchorIdx = i;
15322
+ break;
15323
+ }
15324
+ }
15325
+ if (anchorIdx >= 0) {
15326
+ const tail = lines.slice(anchorIdx + 1);
15327
+ const trimmed = [];
15328
+ for (const raw of tail) {
15329
+ const line = raw.trimEnd();
15330
+ if (line.length === 0 && trimmed.length === 0)
15331
+ continue;
15332
+ if (isTuiChromeLine(line))
15333
+ continue;
15334
+ trimmed.push(line);
15335
+ }
15336
+ while (trimmed.length > 0 && trimmed[trimmed.length - 1].length === 0) {
15337
+ trimmed.pop();
15338
+ }
15339
+ const affordances = /^(esc to cancel|press any key|\u21b5 select)/i;
15340
+ while (trimmed.length > 0 && affordances.test(trimmed[trimmed.length - 1].trim())) {
15341
+ trimmed.pop();
15342
+ }
15343
+ if (trimmed.length > 0) {
15344
+ return { output: trimmed.join(`
15345
+ `), anchored: true };
15346
+ }
15347
+ }
15348
+ }
15349
+ const beforeSet = new Set(before.split(`
15350
+ `).map((l) => l.trimEnd()));
15351
+ const newLines = [];
15352
+ for (const raw of after.split(`
15353
+ `)) {
15354
+ const line = raw.trimEnd();
15355
+ if (line.length === 0)
15356
+ continue;
15357
+ if (beforeSet.has(line))
15358
+ continue;
15359
+ if (isTuiChromeLine(line))
15360
+ continue;
15361
+ newLines.push(line);
15362
+ }
15363
+ return { output: newLines.join(`
15364
+ `), anchored: false };
15365
+ }
15366
+ async function injectSlashCommand(agentName, command, opts = {}) {
15367
+ validateInjectCommand(command);
15368
+ const tmuxBin = opts.tmuxBin ?? "tmux";
15369
+ const socket = opts.socketName ?? defaultSocketName(agentName);
15370
+ const session = opts.sessionName ?? agentName;
15371
+ const settleMs = opts.settleMs ?? 2000;
15372
+ const timeoutMs = opts.timeoutMs ?? 5000;
15373
+ return withPaneLock(`${socket}:${session}`, () => injectSlashCommandWith(makeTmuxRunner(tmuxBin), {
15374
+ socket,
15375
+ session,
15376
+ command: command.trim(),
15377
+ settleMs,
15378
+ timeoutMs
15379
+ }));
15380
+ }
15381
+ async function injectSlashCommandWith(runner, args) {
15382
+ const { socket, session, command, settleMs, timeoutMs } = args;
15383
+ let bareVerb;
15384
+ try {
15385
+ bareVerb = validateInjectCommand(command);
15386
+ } catch (err) {
15387
+ if (err instanceof InjectError) {
15388
+ return {
15389
+ outcome: "failed",
15390
+ output: "",
15391
+ truncated: false,
15392
+ command: command.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? "",
15393
+ meta: null,
15394
+ errorCode: err.code,
15395
+ errorMessage: err.message
15396
+ };
15397
+ }
15398
+ throw err;
15399
+ }
15400
+ const meta = INJECT_COMMANDS.get(bareVerb) ?? null;
15401
+ if (!runner.hasSession(socket, session)) {
15402
+ return {
15403
+ outcome: "failed",
15404
+ output: "",
15405
+ truncated: false,
15406
+ command: bareVerb,
15407
+ meta,
15408
+ errorCode: "session_missing",
15409
+ errorMessage: `tmux session "${session}" on socket "${socket}" not found. ` + `Is the agent running under the tmux supervisor (the default)? ` + `If experimental.legacy_pty=true is set, inject is unsupported.`
15410
+ };
15411
+ }
15412
+ const before = runner.capture(socket, session) ?? "";
15413
+ try {
15414
+ runner.send(socket, session, ["send-keys", "-l", command]);
15415
+ runner.send(socket, session, ["send-keys", "Enter"]);
15416
+ } catch (err) {
15417
+ return {
15418
+ outcome: "failed",
15419
+ output: "",
15420
+ truncated: false,
15421
+ command: bareVerb,
15422
+ meta,
15423
+ errorCode: "tmux_failed",
15424
+ errorMessage: `tmux send-keys failed: ${err instanceof Error ? err.message : String(err)}`
15425
+ };
15426
+ }
15427
+ const start = Date.now();
15428
+ let last = before;
15429
+ let stableSince = null;
15430
+ while (Date.now() - start < timeoutMs) {
15431
+ await sleep(POLL_INTERVAL_MS);
15432
+ const cur = runner.capture(socket, session) ?? "";
15433
+ if (cur === last && cur !== before) {
15434
+ if (stableSince === null) {
15435
+ stableSince = Date.now();
15436
+ } else if (Date.now() - stableSince >= POLL_INTERVAL_MS) {
15437
+ last = cur;
15438
+ break;
15439
+ }
15440
+ } else {
15441
+ stableSince = null;
15442
+ }
15443
+ last = cur;
15444
+ if (Date.now() - start >= settleMs && cur !== before) {
15445
+ break;
15446
+ }
15447
+ }
15448
+ const { output: rawOutput, anchored } = diffPane(before, last, command);
15449
+ let output = rawOutput;
15450
+ let truncated = false;
15451
+ const bytes = Buffer.byteLength(output, "utf-8");
15452
+ if (bytes > OUTPUT_BYTE_CAP) {
15453
+ while (Buffer.byteLength(output, "utf-8") > OUTPUT_BYTE_CAP) {
15454
+ output = output.slice(Math.floor(output.length * 0.1) + 1);
15455
+ }
15456
+ truncated = true;
15457
+ }
15458
+ if (meta?.dialog) {
15459
+ try {
15460
+ runner.send(socket, session, ["send-keys", "Escape"]);
15461
+ } catch {}
15462
+ }
15463
+ if (output.trim().length === 0) {
15464
+ return {
15465
+ outcome: "ok_no_output",
15466
+ output: "",
15467
+ truncated: false,
15468
+ command: bareVerb,
15469
+ meta,
15470
+ ...anchored ? {} : { diagnostic: "anchor_missing" }
15471
+ };
15472
+ }
15473
+ return {
15474
+ outcome: "ok",
15475
+ output,
15476
+ truncated,
15477
+ command: bareVerb,
15478
+ meta,
15479
+ ...truncated ? { diagnostic: "truncated_output" } : {}
15480
+ };
15481
+ }
15482
+ var INJECT_COMMANDS, INJECT_ALLOWLIST, INJECT_BLOCKED, INJECT_BLOCKLIST, InjectError, POLL_INTERVAL_MS = 150, OUTPUT_BYTE_CAP = 3000;
15483
+ var init_inject = __esm(() => {
15484
+ init_pane_lock();
15485
+ INJECT_COMMANDS = new Map([
15486
+ ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true }],
15487
+ ["/status", { description: "Show session status", expectsOutput: true, dialog: true }],
15488
+ ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true }],
15489
+ ["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true }],
15490
+ ["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true }],
15491
+ ["/model", { description: "Open model picker", expectsOutput: true }],
15492
+ [
15493
+ "/clear",
15494
+ {
15495
+ description: "Clear session screen",
15496
+ expectsOutput: false,
15497
+ silentNote: "context cleared \u2014 fresh slate"
15498
+ }
15499
+ ],
15500
+ [
15501
+ "/compact",
15502
+ {
15503
+ description: "Compact conversation history",
15504
+ expectsOutput: false,
15505
+ silentNote: "compaction runs silently"
15506
+ }
15507
+ ]
15508
+ ]);
15509
+ INJECT_ALLOWLIST = new Set(INJECT_COMMANDS.keys());
15510
+ INJECT_BLOCKED = new Map([
15511
+ ["/login", { reason: "would mutate auth state" }],
15512
+ ["/logout", { reason: "would terminate the agent's auth session" }],
15513
+ ["/exit", { reason: "would kill the agent process" }],
15514
+ ["/quit", { reason: "would kill the agent process" }],
15515
+ [
15516
+ "/effort",
15517
+ {
15518
+ reason: "leaves a blocking confirmation modal open and wedges the pane; use the /effort command (it drives the modal), not raw inject"
15519
+ }
15520
+ ]
15521
+ ]);
15522
+ INJECT_BLOCKLIST = new Set(INJECT_BLOCKED.keys());
15523
+ InjectError = class InjectError extends Error {
15524
+ code;
15525
+ constructor(code, message) {
15526
+ super(message);
15527
+ this.name = "InjectError";
15528
+ this.code = code;
15529
+ }
15530
+ };
15531
+ });
15532
+
15533
+ // src/agents/model-picker.ts
15534
+ var init_model_picker = __esm(() => {
15535
+ init_inject();
15536
+ init_pane_lock();
15537
+ });
15538
+
15539
+ // telegram-plugin/gateway/model-command.ts
15540
+ function isClaudeModel(name) {
15541
+ const lower = name.toLowerCase();
15542
+ if (MODEL_ALIASES.includes(lower))
15543
+ return true;
15544
+ return lower.startsWith("claude-");
15545
+ }
15546
+ var MODEL_ALIASES;
15547
+ var init_model_command = __esm(() => {
15548
+ init_model_picker();
15549
+ MODEL_ALIASES = ["opus", "sonnet", "haiku", "fable", "default"];
15550
+ });
15551
+
15552
+ // src/util/atomic.ts
15553
+ var exports_atomic = {};
15554
+ __export(exports_atomic, {
15555
+ writeConfigFileSync: () => writeConfigFileSync,
15556
+ atomicWriteJsonSync: () => atomicWriteJsonSync,
15557
+ atomicWriteFileSync: () => atomicWriteFileSync
15558
+ });
15559
+ import { randomBytes } from "node:crypto";
15560
+ import { closeSync as closeSync2, constants, fsyncSync, openSync as openSync2, renameSync, rmSync, writeSync } from "node:fs";
15561
+ function atomicWriteFileSync(destPath, contents, mode = 384) {
15562
+ const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
15563
+ const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
15564
+ let fd = null;
15565
+ try {
15566
+ fd = openSync2(tmp, TMP_OPEN_FLAGS, mode);
15567
+ writeSync(fd, buf, 0, buf.length, 0);
15568
+ fsyncSync(fd);
15569
+ closeSync2(fd);
15570
+ fd = null;
15571
+ renameSync(tmp, destPath);
15572
+ } catch (err) {
15573
+ if (fd !== null) {
15574
+ try {
15575
+ closeSync2(fd);
15576
+ } catch {}
15577
+ }
15578
+ try {
15579
+ rmSync(tmp, { force: true });
15580
+ } catch {}
15581
+ throw err;
15582
+ }
15583
+ }
15584
+ function atomicWriteJsonSync(destPath, value, mode = 384) {
15585
+ atomicWriteFileSync(destPath, JSON.stringify(value, null, 2) + `
15586
+ `, mode);
15587
+ }
15588
+ function writeConfigFileSync(destPath, contents, mode = 420) {
15589
+ try {
15590
+ atomicWriteFileSync(destPath, contents, mode);
15591
+ } catch (err) {
15592
+ const code = err.code;
15593
+ if (code !== "EBUSY" && code !== "EXDEV" && code !== "EINVAL") {
15594
+ throw err;
15595
+ }
15596
+ const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
15597
+ let fd = null;
15598
+ try {
15599
+ fd = openSync2(destPath, constants.O_WRONLY | constants.O_TRUNC, mode);
15600
+ writeSync(fd, buf, 0, buf.length, 0);
15601
+ fsyncSync(fd);
15602
+ closeSync2(fd);
15603
+ fd = null;
15604
+ } catch (fallbackErr) {
15605
+ if (fd !== null) {
15606
+ try {
15607
+ closeSync2(fd);
15608
+ } catch {}
15609
+ }
15610
+ throw fallbackErr;
15611
+ }
15612
+ }
15613
+ }
15614
+ var TMP_OPEN_FLAGS;
15615
+ var init_atomic = __esm(() => {
15616
+ TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
15617
+ });
15618
+
15619
+ // src/setup/hindsight.ts
15620
+ import { execFileSync as execFileSync2 } from "node:child_process";
15621
+ import { createServer } from "node:net";
15622
+ function hindsightImageRef(tag) {
15623
+ const t = tag?.trim();
15624
+ if (!t)
15625
+ return HINDSIGHT_IMAGE;
15626
+ const v = t.replace(/^v/, "");
15627
+ return `${HINDSIGHT_IMAGE_REPO}:v${v}`;
15628
+ }
15629
+ function isPortFree(port) {
15630
+ return new Promise((resolve4) => {
15631
+ const server = createServer();
15632
+ server.once("error", () => resolve4(false));
15633
+ server.once("listening", () => {
15634
+ server.close(() => resolve4(true));
15635
+ });
15636
+ server.listen(port, "127.0.0.1");
15637
+ });
15638
+ }
15639
+ function describePortHolder(port) {
15640
+ const probes = [
15641
+ ["ss", ["-ltnpH", `sport = :${port}`]],
15642
+ ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"]]
15643
+ ];
15644
+ for (const [cmd, args] of probes) {
15645
+ try {
15646
+ const out = execFileSync2(cmd, args, {
15647
+ stdio: "pipe",
15648
+ encoding: "utf-8"
15649
+ }).trim();
15650
+ if (!out)
15651
+ continue;
15652
+ const ssMatch = out.match(/users:\(\("([^"]+)",pid=(\d+)/);
15653
+ if (ssMatch)
15654
+ return `pid ${ssMatch[2]} (${ssMatch[1]})`;
15655
+ const firstDataLine = out.split(`
15656
+ `).map((l) => l.trim()).find((l) => l && !/^COMMAND\b/.test(l));
15657
+ if (firstDataLine) {
15658
+ const parts = firstDataLine.split(/\s+/);
15659
+ if (cmd === "lsof" && parts.length >= 2) {
15660
+ return `pid ${parts[1]} (${parts[0]})`;
15661
+ }
15662
+ return firstDataLine.slice(0, 80);
15663
+ }
15664
+ } catch {}
15665
+ }
15666
+ return null;
15667
+ }
15668
+ async function preflightHindsightPorts(ports) {
15669
+ for (const port of [ports.apiPort, ports.uiPort]) {
15670
+ if (!await isPortFree(port)) {
15671
+ return { port, holder: describePortHolder(port) };
15672
+ }
15673
+ }
15674
+ return null;
15675
+ }
15676
+ async function findFreePort(start, maxAttempts = 50) {
15677
+ for (let i = 0;i < maxAttempts; i++) {
15678
+ const port = start + i;
15679
+ if (port < 1024)
15680
+ continue;
15681
+ if (await isPortFree(port)) {
15682
+ return port;
15683
+ }
15684
+ }
15685
+ return null;
15686
+ }
15687
+ async function pickHindsightPorts() {
15688
+ if (await isPortFree(HINDSIGHT_DEFAULT_API_PORT) && await isPortFree(HINDSIGHT_DEFAULT_UI_PORT)) {
15689
+ return {
15690
+ apiPort: HINDSIGHT_DEFAULT_API_PORT,
15691
+ uiPort: HINDSIGHT_DEFAULT_UI_PORT
15692
+ };
15693
+ }
15694
+ const apiPort = await findFreePort(18888);
15695
+ const uiPort = await findFreePort(19999);
15696
+ if (apiPort === null || uiPort === null) {
15697
+ throw new Error("Could not find a free port for Hindsight. " + "Stop whatever is using 18888 / 19999 and retry.");
15698
+ }
15699
+ return { apiPort, uiPort };
15700
+ }
15701
+ function isDockerAvailable() {
15702
+ try {
15703
+ execFileSync2("docker", ["--version"], { stdio: "pipe" });
15704
+ return true;
15705
+ } catch {
15706
+ return false;
15707
+ }
15708
+ }
15709
+ function isHindsightRunning() {
15710
+ try {
15711
+ const output = execFileSync2("docker", ["ps", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
15712
+ return output.trim().length > 0;
15713
+ } catch {
15714
+ return false;
15715
+ }
15716
+ }
15717
+ function isHindsightContainerExists() {
15718
+ try {
15719
+ const output = execFileSync2("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf-8" });
15720
+ return output.trim().length > 0;
15721
+ } catch {
15722
+ return false;
15723
+ }
15724
+ }
15725
+ function resolveHindsightPerOpLlm(llm) {
15726
+ const out = [];
15727
+ if (!llm)
15728
+ return out;
15729
+ for (const op of HINDSIGHT_LLM_OPS) {
15730
+ const cfg = llm[op];
15731
+ if (!cfg)
15732
+ continue;
15733
+ const prefix = `HINDSIGHT_API_${op.toUpperCase()}_LLM`;
15734
+ const model = cfg.model?.trim();
15735
+ const provider = cfg.provider?.trim();
15736
+ const baseUrl = cfg.base_url?.trim();
15737
+ const apiKey = cfg.api_key?.trim();
15738
+ if (model)
15739
+ out.push([`${prefix}_MODEL`, model]);
15740
+ if (provider)
15741
+ out.push([`${prefix}_PROVIDER`, provider]);
15742
+ if (baseUrl)
15743
+ out.push([`${prefix}_BASE_URL`, baseUrl]);
15744
+ if (apiKey)
15745
+ out.push([`${prefix}_API_KEY`, apiKey]);
15746
+ }
15747
+ return out;
15748
+ }
15749
+ function resolveHindsightLlm(llm, litellm) {
15750
+ const provider = llm?.provider?.trim() || "claude-code";
15751
+ const model = llm?.model?.trim() || (litellm ? litellm.model ?? HINDSIGHT_DEFAULT_LITELLM_MODEL : HINDSIGHT_DEFAULT_MODEL);
15752
+ return { provider, model };
15753
+ }
15754
+ function startHindsight(ports, litellm, imageTag, llm) {
15755
+ const apiPort = ports?.apiPort ?? HINDSIGHT_DEFAULT_API_PORT;
15756
+ const uiPort = ports?.uiPort ?? HINDSIGHT_DEFAULT_UI_PORT;
15757
+ const { provider: llmProvider, model: llmModel } = resolveHindsightLlm(llm, litellm);
15758
+ const perOpLlm = resolveHindsightPerOpLlm(llm);
15759
+ const envArgs = [
15760
+ "-e",
15761
+ `HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
15762
+ "-e",
15763
+ `HINDSIGHT_API_LLM_PROVIDER=${llmProvider}`,
15764
+ "-e",
15765
+ `HINDSIGHT_API_LLM_MODEL=${llmModel}`,
15766
+ ...perOpLlm.flatMap(([k, v]) => ["-e", `${k}=${v}`]),
15767
+ "-e",
15768
+ `HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
15769
+ "-e",
15770
+ `HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=${HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING}`,
15771
+ "-e",
15772
+ `HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
15773
+ "-e",
15774
+ `HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
15775
+ "-e",
15776
+ `HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
15777
+ "-e",
15778
+ `HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
15779
+ "-e",
15780
+ `HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
15781
+ "-e",
15782
+ `HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
15783
+ "-e",
15784
+ `HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
15785
+ "-e",
15786
+ `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`
15787
+ ];
15788
+ if (llmProvider === "claude-code") {
15789
+ envArgs.push("-e", `ANTHROPIC_MODEL=${llmModel}`);
15790
+ }
15791
+ if (litellm) {
15792
+ const litellmRoot = litellm.baseUrl.replace(/\/+$/, "");
15793
+ const anthropicBaseUrl = isClaudeModel(llmModel) ? `${litellmRoot}/anthropic` : litellmRoot;
15794
+ envArgs.push("-e", `HINDSIGHT_API_PORT=${apiPort}`, "-e", `HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:${apiPort}`, "-e", `ANTHROPIC_BASE_URL=${anthropicBaseUrl}`, "-e", `ANTHROPIC_CUSTOM_HEADERS=x-litellm-api-key: Bearer ${litellm.apiKey}
15795
+ x-litellm-customer-id: hindsight
15796
+ x-litellm-tags: service:hindsight`);
15797
+ }
15798
+ const args = [
15799
+ "run",
15800
+ "-d",
15801
+ "--name",
15802
+ "switchroom-hindsight",
15803
+ "--restart",
15804
+ "always",
15805
+ `--memory=${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
15806
+ `--memory-reservation=${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
15807
+ `--pids-limit=${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
15808
+ `--shm-size=${HINDSIGHT_DEFAULT_SHM_SIZE}`,
15809
+ "--health-cmd",
15810
+ litellm ? `python3 -c 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:${apiPort}/health",timeout=4).getcode()==200 else 1)'` : HINDSIGHT_HEALTHCHECK_CMD,
15811
+ "--health-interval",
15812
+ "30s",
15813
+ "--health-timeout",
15814
+ "5s",
15815
+ "--health-retries",
15816
+ "3",
15817
+ "--health-start-period",
15818
+ "60s"
15819
+ ];
15820
+ if (litellm) {
15821
+ args.push("--network", "host");
15822
+ } else {
15823
+ args.push("-p", `127.0.0.1:${apiPort}:8888`, "-p", `127.0.0.1:${uiPort}:9999`);
15824
+ }
15825
+ args.push("-v", "switchroom-hindsight-data:/home/hindsight/.pg0", "-v", "switchroom-hindsight-backups:/backups", "-v", `${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`, "--tmpfs", `/run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`, ...envArgs, hindsightImageRef(imageTag));
15826
+ execFileSync2("docker", args, { stdio: "pipe" });
15827
+ }
15828
+ function stopHindsight() {
15829
+ try {
15830
+ execFileSync2("docker", ["stop", "switchroom-hindsight"], { stdio: "pipe" });
15831
+ } catch {}
15832
+ try {
15833
+ execFileSync2("docker", ["rm", "switchroom-hindsight"], { stdio: "pipe" });
15834
+ } catch {}
15835
+ }
15836
+ function pullHindsightImage(imageTag) {
15837
+ execFileSync2("docker", ["pull", hindsightImageRef(imageTag)], { stdio: "inherit" });
15838
+ }
15839
+ function getRunningHindsightPorts() {
15840
+ const CONTAINER_API_PORT = 8888;
15841
+ const CONTAINER_UI_PORT = 9999;
15842
+ const readPort = (containerPort) => {
15843
+ try {
15844
+ const out = execFileSync2("docker", ["port", "switchroom-hindsight", `${containerPort}/tcp`], { stdio: "pipe", encoding: "utf-8" });
15845
+ const m = out.split(`
15846
+ `).map((l) => l.trim()).find((l) => l.length > 0);
15847
+ if (!m)
15848
+ return null;
15849
+ const port = Number(m.slice(m.lastIndexOf(":") + 1));
15850
+ return Number.isInteger(port) && port > 0 ? port : null;
15851
+ } catch {
15852
+ return null;
15853
+ }
15854
+ };
15855
+ const apiPort = readPort(CONTAINER_API_PORT);
15856
+ if (apiPort !== null) {
15857
+ const uiPort = readPort(CONTAINER_UI_PORT) ?? (apiPort === HINDSIGHT_DEFAULT_API_PORT ? HINDSIGHT_DEFAULT_UI_PORT : 19999);
15858
+ return { apiPort, uiPort };
15859
+ }
15860
+ return getHindsightPortsFromEnv();
15861
+ }
15862
+ function getHindsightPortsFromEnv() {
15863
+ let env2;
15864
+ try {
15865
+ env2 = execFileSync2("docker", ["inspect", "--format", "{{.Config.Env}}", "switchroom-hindsight"], { stdio: "pipe", encoding: "utf-8" });
15866
+ } catch {
15867
+ return null;
15868
+ }
15869
+ const readVar = (name) => {
15870
+ const re = new RegExp(`(?:^|[\\s\\[])${name}=([^\\s\\]]+)`);
15871
+ const m = env2.match(re);
15872
+ if (!m)
15873
+ return null;
15874
+ const n = Number(m[1]);
15875
+ return Number.isInteger(n) && n > 0 ? n : null;
15876
+ };
15877
+ const apiPort = readVar("HINDSIGHT_API_PORT");
15878
+ if (apiPort === null)
15879
+ return null;
15880
+ const uiPort = readVar("HINDSIGHT_UI_PORT") ?? (apiPort === HINDSIGHT_DEFAULT_API_PORT ? HINDSIGHT_DEFAULT_UI_PORT : 19999);
15881
+ return { apiPort, uiPort };
15882
+ }
15883
+ function getHindsightStatus() {
15884
+ try {
15885
+ const output = execFileSync2("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
15886
+ const status = output.trim();
15887
+ return status.length > 0 ? status : null;
15888
+ } catch {
15889
+ return null;
15890
+ }
15891
+ }
15892
+ function generateHindsightComposeSnippet(llm) {
15893
+ const { provider: llmProvider, model: llmModel } = resolveHindsightLlm(llm);
15894
+ const perOpLlm = resolveHindsightPerOpLlm(llm);
15895
+ const environment = [
15896
+ ` - HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
15897
+ ` - HINDSIGHT_API_LLM_PROVIDER=${llmProvider}`,
15898
+ ` - HINDSIGHT_API_LLM_MODEL=${llmModel}`,
15899
+ ...perOpLlm.map(([k, v]) => ` - ${k}=${v}`),
15900
+ ...llmProvider === "claude-code" ? [` - ANTHROPIC_MODEL=${llmModel}`] : [],
15901
+ ` - HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888`
15902
+ ];
15903
+ return [
15904
+ "services:",
15905
+ " switchroom-hindsight:",
15906
+ ` image: ${HINDSIGHT_IMAGE}`,
15907
+ " container_name: switchroom-hindsight",
15908
+ " ports:",
15909
+ ` - "127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}:8888"`,
15910
+ ' - "127.0.0.1:19999:9999"',
15911
+ " environment:",
15912
+ ...environment,
15913
+ ` - HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
15914
+ ` - HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=${HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING}`,
15915
+ ` - HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
15916
+ ` - HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
15917
+ ` - HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
15918
+ ` - HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
15919
+ ` - HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
15920
+ ` - HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
15921
+ ` - HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
15922
+ ` - HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`,
15923
+ ` mem_limit: ${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
15924
+ ` mem_reservation: ${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
15925
+ ` pids_limit: ${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
15926
+ ` shm_size: ${HINDSIGHT_DEFAULT_SHM_SIZE}`,
15927
+ " healthcheck:",
15928
+ ` test: ${JSON.stringify(["CMD", "python3", "-c", HINDSIGHT_HEALTHCHECK_PY])}`,
15929
+ " interval: 30s",
15930
+ " timeout: 5s",
15931
+ " retries: 3",
15932
+ " start_period: 60s",
15933
+ " volumes:",
15934
+ " - switchroom-hindsight-data:/home/hindsight/.pg0",
15935
+ " - switchroom-hindsight-backups:/backups",
15936
+ ` - ${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
15937
+ " tmpfs:",
15938
+ ` - /run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`,
15939
+ " restart: always",
15940
+ "",
15941
+ "volumes:",
15942
+ " switchroom-hindsight-data:",
15943
+ " switchroom-hindsight-backups:",
15944
+ ` ${HINDSIGHT_BROKER_SOCK_VOLUME}:`,
15945
+ " external: true",
15946
+ " # Bound by the switchroom-auth-broker singleton in the main",
15947
+ " # switchroom compose project. Declare an `auth.consumers[]`",
15948
+ ` # entry named "${HINDSIGHT_CONSUMER_NAME}" in switchroom.yaml.`
15949
+ ].join(`
15950
+ `);
15951
+ }
15952
+ async function ensureHindsightConsumer(configPath, account, uid = HINDSIGHT_DEFAULT_UID) {
15953
+ const fs = await import("node:fs");
15954
+ const { parseDocument, isMap, isSeq, YAMLMap, YAMLSeq } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
15955
+ const { atomicWriteFileSync: atomicWriteFileSync2 } = await Promise.resolve().then(() => (init_atomic(), exports_atomic));
15956
+ const raw = fs.readFileSync(configPath, "utf-8");
15957
+ const doc = parseDocument(raw);
15958
+ const root = doc.contents;
15959
+ if (!isMap(root)) {
15960
+ return { added: false, reason: "switchroom.yaml root is not a map" };
15961
+ }
15962
+ const authRaw = root.get("auth", true);
15963
+ let authNode;
15964
+ if (isMap(authRaw)) {
15965
+ authNode = authRaw;
15966
+ } else {
15967
+ authNode = new YAMLMap;
15968
+ doc.setIn(["auth"], authNode);
15969
+ }
15970
+ const consumersRaw = authNode.get("consumers", true);
15971
+ let consumersNode;
15972
+ if (isSeq(consumersRaw)) {
15973
+ consumersNode = consumersRaw;
15974
+ } else {
15975
+ consumersNode = new YAMLSeq;
15976
+ doc.setIn(["auth", "consumers"], consumersNode);
15977
+ }
15978
+ for (const item of consumersNode.items) {
15979
+ if (isMap(item)) {
15980
+ const name = item.get("name");
15981
+ if (name === HINDSIGHT_CONSUMER_NAME) {
15982
+ return { added: false, reason: "already present" };
15983
+ }
15984
+ }
15985
+ }
15986
+ const entry = new YAMLMap;
15987
+ entry.set("name", HINDSIGHT_CONSUMER_NAME);
15988
+ if (account !== undefined)
15989
+ entry.set("account", account);
15990
+ entry.set("uid", uid);
15991
+ consumersNode.add(entry);
15992
+ const out = String(doc);
15993
+ const tail = out.endsWith(`
15994
+ `) ? out : out + `
15995
+ `;
15996
+ let mode = 420;
15997
+ try {
15998
+ mode = fs.statSync(configPath).mode & 511;
15999
+ } catch {}
16000
+ atomicWriteFileSync2(configPath, tail, mode);
16001
+ return { added: true, reason: "added" };
16002
+ }
16003
+ var HINDSIGHT_DEFAULT_API_PORT = 18888, HINDSIGHT_DEFAULT_UI_PORT = 9999, HINDSIGHT_DEFAULT_MCP_URL, HINDSIGHT_DEFAULT_API_BASE_URL, HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000, HINDSIGHT_CONSUMER_NAME = "hindsight", HINDSIGHT_DEFAULT_UID = 11000, HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight", HINDSIGHT_IMAGE, HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5", HINDSIGHT_DEFAULT_LITELLM_MODEL = "openrouter/google/gemini-3.1-flash-lite", HINDSIGHT_DEFAULT_MCP_STATELESS = true, HINDSIGHT_BROKER_SOCK_VOLUME, HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true", HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150, HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8, HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 12, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 100, HINDSIGHT_DEFAULT_MEM_LIMIT = "8g", HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g", HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000, HINDSIGHT_DEFAULT_SHM_SIZE = "2g", HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)', HINDSIGHT_HEALTHCHECK_CMD, HINDSIGHT_LLM_OPS;
16004
+ var init_hindsight = __esm(() => {
16005
+ init_model_command();
16006
+ HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
16007
+ HINDSIGHT_DEFAULT_API_BASE_URL = HINDSIGHT_DEFAULT_MCP_URL.replace(/\/mcp\/?$/, "");
16008
+ HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
16009
+ HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
16010
+ HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
16011
+ HINDSIGHT_LLM_OPS = ["retain", "reflect", "consolidation"];
16012
+ });
16013
+
15125
16014
  // src/memory/hindsight.ts
15126
16015
  function generateHindsightMcpConfig(collection, memoryConfig) {
15127
- const url = memoryConfig.config?.url ?? "http://localhost:8888/mcp/";
16016
+ const url = memoryConfig.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
15128
16017
  return {
15129
16018
  type: "http",
15130
16019
  url,
@@ -15167,6 +16056,26 @@ function isHindsightEnabled(config) {
15167
16056
  return false;
15168
16057
  return config?.memory?.backend === "hindsight";
15169
16058
  }
16059
+ function mergeDisposition(base, over) {
16060
+ if (!base && !over)
16061
+ return;
16062
+ const merged = { ...base ?? {}, ...over ?? {} };
16063
+ return Object.keys(merged).length > 0 ? merged : undefined;
16064
+ }
16065
+ function resolveBankMissionExtras(memory, profileName) {
16066
+ const pd = PROFILE_MEMORY_DEFAULTS[profileName] ?? {};
16067
+ const out = {};
16068
+ const reflect = memory?.reflect_mission ?? pd.reflect_mission;
16069
+ if (reflect != null)
16070
+ out.reflect_mission = reflect;
16071
+ const observations = memory?.observations_mission ?? pd.observations_mission;
16072
+ if (observations != null)
16073
+ out.observations_mission = observations;
16074
+ const disposition = mergeDisposition(pd.disposition, memory?.disposition);
16075
+ if (disposition)
16076
+ out.disposition = disposition;
16077
+ return out;
16078
+ }
15170
16079
  async function parseSseOrJson(resp) {
15171
16080
  const text = await resp.text();
15172
16081
  const dataLine = text.split(`
@@ -15253,9 +16162,19 @@ async function probeHindsight(apiUrl, opts) {
15253
16162
  clearTimeout(timeout);
15254
16163
  }
15255
16164
  }
15256
- async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
16165
+ async function ensureMentalModel(apiUrl, bankId, spec, opts) {
15257
16166
  const fetchImpl = opts?.fetchImpl ?? fetch;
15258
16167
  const timeoutMs = opts?.timeoutMs ?? 5000;
16168
+ const createArguments = {
16169
+ name: spec.name,
16170
+ source_query: spec.sourceQuery
16171
+ };
16172
+ if (spec.refreshAfterConsolidation !== undefined) {
16173
+ createArguments.trigger_refresh_after_consolidation = spec.refreshAfterConsolidation;
16174
+ }
16175
+ if (spec.maxTokens !== undefined) {
16176
+ createArguments.max_tokens = spec.maxTokens;
16177
+ }
15259
16178
  try {
15260
16179
  const controller = new AbortController;
15261
16180
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -15312,7 +16231,7 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
15312
16231
  if (models && typeof models === "string") {
15313
16232
  const parsed = JSON.parse(models);
15314
16233
  const items = Array.isArray(parsed?.items) ? parsed.items : [];
15315
- if (items.some((m) => m?.name === "user-profile")) {
16234
+ if (items.some((m) => m?.name === spec.name)) {
15316
16235
  return { ok: true };
15317
16236
  }
15318
16237
  }
@@ -15333,10 +16252,7 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
15333
16252
  method: "tools/call",
15334
16253
  params: {
15335
16254
  name: "create_mental_model",
15336
- arguments: {
15337
- name: "user-profile",
15338
- source_query: "What are the key facts, preferences, context, and communication style about the user I talk to? Summarize what matters for making the agent feel like it knows them."
15339
- }
16255
+ arguments: createArguments
15340
16256
  }
15341
16257
  }),
15342
16258
  signal: controller.signal
@@ -15360,6 +16276,33 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
15360
16276
  return { ok: false, reason: String(err) };
15361
16277
  }
15362
16278
  }
16279
+ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
16280
+ return ensureMentalModel(apiUrl, bankId, { name: "user-profile", sourceQuery: USER_PROFILE_SOURCE_QUERY }, opts);
16281
+ }
16282
+ async function ensureDeclaredMentalModels(apiUrl, bankId, models, opts) {
16283
+ if (!models || models.length === 0)
16284
+ return [];
16285
+ const outcomes = [];
16286
+ const seen = new Set;
16287
+ for (const m of models) {
16288
+ if (seen.has(m.name))
16289
+ continue;
16290
+ seen.add(m.name);
16291
+ const spec = {
16292
+ name: m.name,
16293
+ sourceQuery: m.source_query,
16294
+ refreshAfterConsolidation: m.refresh_after_consolidation,
16295
+ maxTokens: m.max_tokens
16296
+ };
16297
+ try {
16298
+ const result = await ensureMentalModel(apiUrl, bankId, spec, opts);
16299
+ outcomes.push(result.ok ? { name: m.name, ok: true } : { name: m.name, ok: false, reason: result.reason });
16300
+ } catch (err) {
16301
+ outcomes.push({ name: m.name, ok: false, reason: String(err) });
16302
+ }
16303
+ }
16304
+ return outcomes;
16305
+ }
15363
16306
  async function createBank(apiUrl, bankId, opts) {
15364
16307
  const fetchImpl = opts?.fetchImpl ?? fetch;
15365
16308
  const timeoutMs = opts?.timeoutMs ?? 5000;
@@ -15487,8 +16430,24 @@ async function updateBankMissions(apiUrl, bankId, missions, opts) {
15487
16430
  name: "update_bank",
15488
16431
  arguments: {
15489
16432
  bank_id: bankId,
15490
- ...missions.bank_mission != null ? { mission: missions.bank_mission } : {},
15491
- ...missions.retain_mission != null ? { config_updates: { retain_mission: missions.retain_mission } } : {}
16433
+ ...missions.reflect_mission == null && missions.bank_mission != null ? { mission: missions.bank_mission } : {},
16434
+ ...(() => {
16435
+ const configUpdates = {};
16436
+ if (missions.retain_mission != null)
16437
+ configUpdates.retain_mission = missions.retain_mission;
16438
+ if (missions.reflect_mission != null)
16439
+ configUpdates.reflect_mission = missions.reflect_mission;
16440
+ if (missions.observations_mission != null)
16441
+ configUpdates.observations_mission = missions.observations_mission;
16442
+ const d = missions.disposition;
16443
+ if (d?.skepticism != null)
16444
+ configUpdates.disposition_skepticism = d.skepticism;
16445
+ if (d?.literalism != null)
16446
+ configUpdates.disposition_literalism = d.literalism;
16447
+ if (d?.empathy != null)
16448
+ configUpdates.disposition_empathy = d.empathy;
16449
+ return Object.keys(configUpdates).length > 0 ? { config_updates: configUpdates } : {};
16450
+ })()
15492
16451
  }
15493
16452
  }
15494
16453
  }),
@@ -15586,10 +16545,23 @@ async function addMemoryTag(apiUrl, bankId, memoryId, tag, opts) {
15586
16545
  return { ok: false, reason: String(err) };
15587
16546
  }
15588
16547
  }
15589
- var DEFAULT_RETAIN_MISSION, DEMOTE_FROM_RECALL_TAG = "[demote-from-recall]";
15590
- var init_hindsight = __esm(() => {
16548
+ var DEFAULT_RETAIN_MISSION, PROFILE_MEMORY_DEFAULTS, USER_PROFILE_SOURCE_QUERY = "What are the key facts, preferences, context, and communication style about the user I talk to? Summarize what matters for making the agent feel like it knows them.", DEMOTE_FROM_RECALL_TAG = "[demote-from-recall]";
16549
+ var init_hindsight2 = __esm(() => {
15591
16550
  init_users();
16551
+ init_hindsight();
15592
16552
  DEFAULT_RETAIN_MISSION = "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.";
16553
+ PROFILE_MEMORY_DEFAULTS = {
16554
+ "health-coach": {
16555
+ disposition: { skepticism: 2, literalism: 2, empathy: 5 },
16556
+ observations_mission: "Synthesise the person's wellbeing patterns, motivations, and emotional " + "context \u2014 how habits, setbacks, and encouragement connect over time."
16557
+ },
16558
+ "executive-assistant": {
16559
+ disposition: { skepticism: 4, literalism: 4, empathy: 3 }
16560
+ },
16561
+ coding: {
16562
+ disposition: { skepticism: 4, literalism: 5, empathy: 2 }
16563
+ }
16564
+ };
15593
16565
  });
15594
16566
 
15595
16567
  // src/cli/helpers.ts
@@ -22398,7 +23370,8 @@ function getBuiltinDefaultSkillEntries() {
22398
23370
  "switchroom-cli",
22399
23371
  "switchroom-status",
22400
23372
  "switchroom-health",
22401
- "switchroom-runtime"
23373
+ "switchroom-runtime",
23374
+ "mental-model-curator"
22402
23375
  ];
22403
23376
  return [
22404
23377
  ...anthropic.map((key) => ({ key, optOutKey: key, source: "anthropic" })),
@@ -22407,7 +23380,7 @@ function getBuiltinDefaultSkillEntries() {
22407
23380
  }
22408
23381
  var GOOGLE_WORKSPACE_MCP_PINNED_SHA = "9d69115b63e6bc2ef0d4b5d7a3b962396382b44c", MICROSOFT_WORKSPACE_MCP_PINNED_VERSION = "0.113.0", MICROSOFT_WORKSPACE_MCP_PACKAGE = "@softeria/ms-365-mcp-server", NOTION_MCP_PINNED_VERSION = "1.8.1", NOTION_MCP_PACKAGE = "@notionhq/notion-mcp-server", NOTION_TOKEN_ENV = "NOTION_TOKEN";
22409
23382
  var init_scaffold_integration = __esm(() => {
22410
- init_hindsight();
23383
+ init_hindsight2();
22411
23384
  });
22412
23385
 
22413
23386
  // src/config/microsoft-workspace-acl.ts
@@ -22425,7 +23398,7 @@ function shouldEmitMs365Mcp(agentName, agentMicrosoftAccount, microsoftAccounts)
22425
23398
  }
22426
23399
 
22427
23400
  // src/agents/reconcile-default-skills.ts
22428
- import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readlinkSync as readlinkSync2, rmSync, symlinkSync } from "node:fs";
23401
+ import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readlinkSync as readlinkSync2, rmSync as rmSync2, symlinkSync } from "node:fs";
22429
23402
  import { homedir as homedir3 } from "node:os";
22430
23403
  import { join as join5, resolve as resolve5 } from "node:path";
22431
23404
  function warnMissingPoolDir(poolDir) {
@@ -22495,7 +23468,7 @@ function reconcileAgentDefaultSkills(agentDir, optOuts = {}, defaults = getBuilt
22495
23468
  }
22496
23469
  if (currentTarget && isOwnedStaleLink(currentTarget, poolDir)) {
22497
23470
  try {
22498
- rmSync(dest, { force: true });
23471
+ rmSync2(dest, { force: true });
22499
23472
  } catch {}
22500
23473
  } else {
22501
23474
  result.conflicts.push(entry.key);
@@ -22560,7 +23533,7 @@ import {
22560
23533
  writeFileSync as writeFileSync2,
22561
23534
  mkdirSync as mkdirSync4,
22562
23535
  existsSync as existsSync8,
22563
- renameSync,
23536
+ renameSync as renameSync2,
22564
23537
  copyFileSync as copyFileSync2,
22565
23538
  unlinkSync
22566
23539
  } from "node:fs";
@@ -22600,7 +23573,7 @@ function saveTopicState(state, statePath) {
22600
23573
  try {
22601
23574
  writeFileSync2(tmp, JSON.stringify(state, null, 2) + `
22602
23575
  `, "utf-8");
22603
- renameSync(tmp, path);
23576
+ renameSync2(tmp, path);
22604
23577
  } catch (err) {
22605
23578
  try {
22606
23579
  if (existsSync8(tmp))
@@ -22616,10 +23589,10 @@ var init_state = __esm(() => {
22616
23589
  // src/vault/flock.ts
22617
23590
  import {
22618
23591
  existsSync as existsSync9,
22619
- openSync as openSync2,
22620
- closeSync as closeSync2,
22621
- writeSync,
22622
- fsyncSync,
23592
+ openSync as openSync3,
23593
+ closeSync as closeSync3,
23594
+ writeSync as writeSync2,
23595
+ fsyncSync as fsyncSync2,
22623
23596
  unlinkSync as unlinkSync2,
22624
23597
  readFileSync as readFileSync8,
22625
23598
  readdirSync as readdirSync4,
@@ -22741,7 +23714,7 @@ function acquireLock(vaultPath, options = {}) {
22741
23714
  while (true) {
22742
23715
  let fd = null;
22743
23716
  try {
22744
- fd = openSync2(lockPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
23717
+ fd = openSync3(lockPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
22745
23718
  } catch (err) {
22746
23719
  const code = err?.code ?? "";
22747
23720
  if (code === "EEXIST") {
@@ -22777,11 +23750,11 @@ function acquireLock(vaultPath, options = {}) {
22777
23750
  ${Date.now()}
22778
23751
  ${process.argv[1] ?? ""}
22779
23752
  `;
22780
- writeSync(fd, meta);
22781
- fsyncSync(fd);
23753
+ writeSync2(fd, meta);
23754
+ fsyncSync2(fd);
22782
23755
  } catch {
22783
23756
  try {
22784
- closeSync2(fd);
23757
+ closeSync3(fd);
22785
23758
  } catch {}
22786
23759
  try {
22787
23760
  unlinkSync2(lockPath);
@@ -22792,7 +23765,7 @@ ${process.argv[1] ?? ""}
22792
23765
  return {
22793
23766
  release: () => {
22794
23767
  try {
22795
- closeSync2(ownedFd);
23768
+ closeSync3(ownedFd);
22796
23769
  } catch {}
22797
23770
  try {
22798
23771
  unlinkSync2(lockPath);
@@ -22849,22 +23822,22 @@ __export(exports_vault, {
22849
23822
  KNOWN_VAULT_ARTIFACT_PATTERNS: () => KNOWN_VAULT_ARTIFACT_PATTERNS,
22850
23823
  KNOWN_VAULT_ARTIFACT_NAMES: () => KNOWN_VAULT_ARTIFACT_NAMES
22851
23824
  });
22852
- import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
23825
+ import { randomBytes as randomBytes2, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
22853
23826
  import {
22854
23827
  readFileSync as readFileSync9,
22855
- writeSync as writeSync2,
23828
+ writeSync as writeSync3,
22856
23829
  existsSync as existsSync10,
22857
- renameSync as renameSync2,
23830
+ renameSync as renameSync3,
22858
23831
  mkdirSync as mkdirSync5,
22859
23832
  unlinkSync as unlinkSync3,
22860
- fsyncSync as fsyncSync2,
22861
- openSync as openSync3,
22862
- closeSync as closeSync3,
23833
+ fsyncSync as fsyncSync3,
23834
+ openSync as openSync4,
23835
+ closeSync as closeSync4,
22863
23836
  lstatSync as lstatSync2,
22864
23837
  realpathSync as realpathSync2
22865
23838
  } from "node:fs";
22866
23839
  import { dirname as dirname3, basename as basename3, resolve as resolve7 } from "node:path";
22867
- function atomicWriteFileSync(path, data, mode) {
23840
+ function atomicWriteFileSync2(path, data, mode) {
22868
23841
  let effectivePath = path;
22869
23842
  try {
22870
23843
  if (existsSync10(path) && lstatSync2(path).isSymbolicLink()) {
@@ -22874,20 +23847,20 @@ function atomicWriteFileSync(path, data, mode) {
22874
23847
  const dir = dirname3(resolve7(effectivePath));
22875
23848
  const tmp = resolve7(dir, `.${basename3(effectivePath)}.${process.pid}.${Date.now()}.tmp`);
22876
23849
  try {
22877
- const fd = openSync3(tmp, "wx", mode);
23850
+ const fd = openSync4(tmp, "wx", mode);
22878
23851
  try {
22879
- writeSync2(fd, data);
22880
- fsyncSync2(fd);
23852
+ writeSync3(fd, data);
23853
+ fsyncSync3(fd);
22881
23854
  } finally {
22882
- closeSync3(fd);
23855
+ closeSync4(fd);
22883
23856
  }
22884
- renameSync2(tmp, effectivePath);
23857
+ renameSync3(tmp, effectivePath);
22885
23858
  try {
22886
- const dirFd = openSync3(dir, "r");
23859
+ const dirFd = openSync4(dir, "r");
22887
23860
  try {
22888
- fsyncSync2(dirFd);
23861
+ fsyncSync3(dirFd);
22889
23862
  } finally {
22890
- closeSync3(dirFd);
23863
+ closeSync4(dirFd);
22891
23864
  }
22892
23865
  } catch {}
22893
23866
  } catch (err) {
@@ -22907,7 +23880,7 @@ function deriveKey(passphrase, salt, params = { N: SCRYPT_N, r: SCRYPT_R, p: SCR
22907
23880
  });
22908
23881
  }
22909
23882
  function encrypt(key, plaintext) {
22910
- const iv = randomBytes(12);
23883
+ const iv = randomBytes2(12);
22911
23884
  const cipher = createCipheriv("aes-256-gcm", key, iv);
22912
23885
  const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
22913
23886
  const tag = cipher.getAuthTag();
@@ -23012,7 +23985,7 @@ function createVault(passphrase, vaultPath) {
23012
23985
  if (!existsSync10(dir)) {
23013
23986
  mkdirSync5(dir, { recursive: true, mode: 448 });
23014
23987
  }
23015
- const salt = randomBytes(16);
23988
+ const salt = randomBytes2(16);
23016
23989
  const key = deriveKey(passphrase, salt);
23017
23990
  const vaultData = { secrets: {} };
23018
23991
  const { iv, data, tag } = encrypt(key, JSON.stringify(vaultData));
@@ -23023,7 +23996,7 @@ function createVault(passphrase, vaultPath) {
23023
23996
  tag,
23024
23997
  kdf: { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }
23025
23998
  };
23026
- atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
23999
+ atomicWriteFileSync2(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
23027
24000
  }
23028
24001
  function openVault(passphrase, vaultPath) {
23029
24002
  if (!existsSync10(vaultPath)) {
@@ -23080,7 +24053,7 @@ function saveVault(passphrase, vaultPath, secrets) {
23080
24053
  vaultFile.iv = iv;
23081
24054
  vaultFile.data = data;
23082
24055
  vaultFile.tag = tag;
23083
- atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
24056
+ atomicWriteFileSync2(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
23084
24057
  } finally {
23085
24058
  if (releaseLock !== null) {
23086
24059
  try {
@@ -23262,7 +24235,7 @@ function parseSocketIdentity(socketPath, spec) {
23262
24235
  }
23263
24236
 
23264
24237
  // src/vault/broker/peercred.ts
23265
- import { execFileSync } from "node:child_process";
24238
+ import { execFileSync as execFileSync3 } from "node:child_process";
23266
24239
  import { readFileSync as readFileSync10, readlinkSync as readlinkSync3, fstatSync } from "node:fs";
23267
24240
  function socketPathToIdentity(socketPath) {
23268
24241
  return parseSocketIdentity(socketPath, {
@@ -23435,7 +24408,7 @@ function identify(socketPath, socket, execFileSyncOverride) {
23435
24408
  if (process.platform !== "linux") {
23436
24409
  return null;
23437
24410
  }
23438
- const runner = execFileSyncOverride ?? execFileSync;
24411
+ const runner = execFileSyncOverride ?? execFileSync3;
23439
24412
  let pid = null;
23440
24413
  if (socket !== undefined) {
23441
24414
  const fd = fdFromSocket(socket);
@@ -23919,7 +24892,8 @@ function brokerIsComposeManaged() {
23919
24892
  return fs.existsSync(OPERATOR_SOCKET_PATH);
23920
24893
  }
23921
24894
  function vaultTokenFilePath(agentSlug) {
23922
- return join6(homedir4(), ".switchroom", "agents", agentSlug, ".vault-token");
24895
+ const base = process.env.SWITCHROOM_AGENTS_DIR || join6(homedir4(), ".switchroom", "agents");
24896
+ return join6(base, agentSlug, ".vault-token");
23923
24897
  }
23924
24898
  function readVaultTokenFile(agentSlug) {
23925
24899
  const filePath = vaultTokenFilePath(agentSlug);
@@ -24295,13 +25269,13 @@ __export(exports_resolver, {
24295
25269
  });
24296
25270
  import {
24297
25271
  chmodSync,
24298
- closeSync as closeSync4,
25272
+ closeSync as closeSync5,
24299
25273
  mkdirSync as mkdirSync6,
24300
25274
  mkdtempSync,
24301
- openSync as openSync4,
24302
- rmSync as rmSync2,
25275
+ openSync as openSync5,
25276
+ rmSync as rmSync3,
24303
25277
  statSync as statSync6,
24304
- writeSync as writeSync3
25278
+ writeSync as writeSync4
24305
25279
  } from "node:fs";
24306
25280
  import { join as join7 } from "node:path";
24307
25281
  import { tmpdir } from "node:os";
@@ -24329,13 +25303,13 @@ function registerCleanupHook() {
24329
25303
  const cleanup = () => {
24330
25304
  for (const dir of materializedDirs) {
24331
25305
  try {
24332
- rmSync2(dir, { recursive: true, force: true });
25306
+ rmSync3(dir, { recursive: true, force: true });
24333
25307
  } catch {}
24334
25308
  }
24335
25309
  materializedDirs.clear();
24336
25310
  if (cachedRoot) {
24337
25311
  try {
24338
- rmSync2(cachedRoot, { recursive: true, force: true });
25312
+ rmSync3(cachedRoot, { recursive: true, force: true });
24339
25313
  } catch {}
24340
25314
  cachedRoot = null;
24341
25315
  }
@@ -24366,18 +25340,18 @@ function materializationRoot() {
24366
25340
  }
24367
25341
  function writeFileExclusive(filePath, content) {
24368
25342
  const buf = typeof content === "string" ? Buffer.from(content, "utf8") : content;
24369
- const fd = openSync4(filePath, fsConstants2.O_WRONLY | fsConstants2.O_CREAT | fsConstants2.O_EXCL, 384);
25343
+ const fd = openSync5(filePath, fsConstants2.O_WRONLY | fsConstants2.O_CREAT | fsConstants2.O_EXCL, 384);
24370
25344
  try {
24371
- writeSync3(fd, buf);
25345
+ writeSync4(fd, buf);
24372
25346
  } finally {
24373
- closeSync4(fd);
25347
+ closeSync5(fd);
24374
25348
  }
24375
25349
  }
24376
25350
  function materializeFilesEntry(key, files) {
24377
25351
  const dir = join7(materializationRoot(), key);
24378
25352
  if (materializedDirs.has(dir)) {
24379
25353
  try {
24380
- rmSync2(dir, { recursive: true, force: true });
25354
+ rmSync3(dir, { recursive: true, force: true });
24381
25355
  } catch {}
24382
25356
  }
24383
25357
  mkdirSync6(dir, { recursive: true, mode: 448 });
@@ -24401,13 +25375,13 @@ function materializeFilesEntry(key, files) {
24401
25375
  function cleanupMaterializedSecrets() {
24402
25376
  for (const dir of materializedDirs) {
24403
25377
  try {
24404
- rmSync2(dir, { recursive: true, force: true });
25378
+ rmSync3(dir, { recursive: true, force: true });
24405
25379
  } catch {}
24406
25380
  }
24407
25381
  materializedDirs.clear();
24408
25382
  if (cachedRoot) {
24409
25383
  try {
24410
- rmSync2(cachedRoot, { recursive: true, force: true });
25384
+ rmSync3(cachedRoot, { recursive: true, force: true });
24411
25385
  } catch {}
24412
25386
  cachedRoot = null;
24413
25387
  }
@@ -24728,7 +25702,7 @@ var init_onboarding = __esm(() => {
24728
25702
  });
24729
25703
 
24730
25704
  // src/repos/bare-clone.ts
24731
- import { execFileSync as execFileSync2 } from "node:child_process";
25705
+ import { execFileSync as execFileSync4 } from "node:child_process";
24732
25706
  import { existsSync as existsSync13, mkdirSync as mkdirSync8 } from "node:fs";
24733
25707
  import { resolve as resolve9 } from "node:path";
24734
25708
  function bareClonePath(slug) {
@@ -24741,7 +25715,7 @@ function ensureBareClone(slug, url) {
24741
25715
  if (!existsSync13(clonePath)) {
24742
25716
  process.stderr.write(`[switchroom] repo "${slug}": cloning ${url} \u2026
24743
25717
  `);
24744
- execFileSync2("git", ["clone", "--bare", url, clonePath], {
25718
+ execFileSync4("git", ["clone", "--bare", url, clonePath], {
24745
25719
  stdio: ["ignore", "pipe", "pipe"]
24746
25720
  });
24747
25721
  process.stderr.write(`[switchroom] repo "${slug}": bare clone ready at ${clonePath}
@@ -24750,7 +25724,7 @@ function ensureBareClone(slug, url) {
24750
25724
  process.stderr.write(`[switchroom] repo "${slug}": fetching ${clonePath} \u2026
24751
25725
  `);
24752
25726
  try {
24753
- execFileSync2("git", ["fetch", "--all"], {
25727
+ execFileSync4("git", ["fetch", "--all"], {
24754
25728
  cwd: clonePath,
24755
25729
  stdio: ["ignore", "pipe", "pipe"]
24756
25730
  });
@@ -24767,7 +25741,7 @@ var init_bare_clone = __esm(() => {
24767
25741
  });
24768
25742
 
24769
25743
  // src/repos/agent-worktree.ts
24770
- import { execFileSync as execFileSync3 } from "node:child_process";
25744
+ import { execFileSync as execFileSync5 } from "node:child_process";
24771
25745
  import { existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync5 } from "node:fs";
24772
25746
  import { join as join9, resolve as resolve10 } from "node:path";
24773
25747
  function agentBranchName(agentName) {
@@ -24778,7 +25752,7 @@ function agentWorktreePath(agentDir, slug) {
24778
25752
  }
24779
25753
  function isWorktreeDirty(worktreePath) {
24780
25754
  try {
24781
- const out = execFileSync3("git", ["status", "--porcelain"], {
25755
+ const out = execFileSync5("git", ["status", "--porcelain"], {
24782
25756
  cwd: worktreePath,
24783
25757
  stdio: ["ignore", "pipe", "pipe"],
24784
25758
  encoding: "utf-8"
@@ -24790,7 +25764,7 @@ function isWorktreeDirty(worktreePath) {
24790
25764
  }
24791
25765
  function headShortSha(worktreePath) {
24792
25766
  try {
24793
- return execFileSync3("git", ["rev-parse", "--short", "HEAD"], {
25767
+ return execFileSync5("git", ["rev-parse", "--short", "HEAD"], {
24794
25768
  cwd: worktreePath,
24795
25769
  stdio: ["ignore", "pipe", "pipe"],
24796
25770
  encoding: "utf-8"
@@ -24801,7 +25775,7 @@ function headShortSha(worktreePath) {
24801
25775
  }
24802
25776
  function branchExistsInBare(bareClonePath2, branchName) {
24803
25777
  try {
24804
- execFileSync3("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
25778
+ execFileSync5("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
24805
25779
  cwd: bareClonePath2,
24806
25780
  stdio: ["ignore", "pipe", "pipe"]
24807
25781
  });
@@ -24812,7 +25786,7 @@ function branchExistsInBare(bareClonePath2, branchName) {
24812
25786
  }
24813
25787
  function resolveDefaultBranch(bareClonePath2) {
24814
25788
  try {
24815
- const out = execFileSync3("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
25789
+ const out = execFileSync5("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
24816
25790
  cwd: bareClonePath2,
24817
25791
  stdio: ["ignore", "pipe", "pipe"],
24818
25792
  encoding: "utf-8"
@@ -24830,12 +25804,12 @@ function ensureAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
24830
25804
  if (!existsSync14(worktreePath)) {
24831
25805
  mkdirSync9(join9(agentDir, "work"), { recursive: true });
24832
25806
  if (branchExistsInBare(bareClonePath2, branch)) {
24833
- execFileSync3("git", ["worktree", "add", worktreePath, branch], {
25807
+ execFileSync5("git", ["worktree", "add", worktreePath, branch], {
24834
25808
  cwd: bareClonePath2,
24835
25809
  stdio: ["ignore", "pipe", "pipe"]
24836
25810
  });
24837
25811
  } else {
24838
- execFileSync3("git", [
25812
+ execFileSync5("git", [
24839
25813
  "worktree",
24840
25814
  "add",
24841
25815
  worktreePath,
@@ -24863,11 +25837,11 @@ function ensureAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
24863
25837
  };
24864
25838
  }
24865
25839
  try {
24866
- execFileSync3("git", ["fetch", "origin"], {
25840
+ execFileSync5("git", ["fetch", "origin"], {
24867
25841
  cwd: worktreePath,
24868
25842
  stdio: ["ignore", "pipe", "pipe"]
24869
25843
  });
24870
- execFileSync3("git", ["merge", "--ff-only", `origin/${defaultBranch}`], {
25844
+ execFileSync5("git", ["merge", "--ff-only", `origin/${defaultBranch}`], {
24871
25845
  cwd: worktreePath,
24872
25846
  stdio: ["ignore", "pipe", "pipe"]
24873
25847
  });
@@ -24885,7 +25859,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
24885
25859
  const branch = agentBranchName(agentName);
24886
25860
  if (existsSync14(worktreePath)) {
24887
25861
  try {
24888
- execFileSync3("git", ["worktree", "remove", "--force", worktreePath], {
25862
+ execFileSync5("git", ["worktree", "remove", "--force", worktreePath], {
24889
25863
  cwd: bareClonePath2,
24890
25864
  stdio: ["ignore", "pipe", "pipe"]
24891
25865
  });
@@ -24897,7 +25871,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
24897
25871
  }
24898
25872
  if (branchExistsInBare(bareClonePath2, branch)) {
24899
25873
  try {
24900
- execFileSync3("git", ["branch", "-D", branch], {
25874
+ execFileSync5("git", ["branch", "-D", branch], {
24901
25875
  cwd: bareClonePath2,
24902
25876
  stdio: ["ignore", "pipe", "pipe"]
24903
25877
  });
@@ -24908,7 +25882,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
24908
25882
  }
24909
25883
  }
24910
25884
  try {
24911
- execFileSync3("git", ["worktree", "prune"], {
25885
+ execFileSync5("git", ["worktree", "prune"], {
24912
25886
  cwd: bareClonePath2,
24913
25887
  stdio: ["ignore", "pipe", "pipe"]
24914
25888
  });
@@ -24925,19 +25899,19 @@ import {
24925
25899
  writeFileSync as writeFileSync4,
24926
25900
  appendFileSync as appendFileSync2,
24927
25901
  readFileSync as readFileSync13,
24928
- renameSync as renameSync3,
25902
+ renameSync as renameSync4,
24929
25903
  chmodSync as chmodSync2,
24930
25904
  symlinkSync as symlinkSync2,
24931
25905
  copyFileSync as copyFileSync4,
24932
25906
  readdirSync as readdirSync6,
24933
- rmSync as rmSync3,
25907
+ rmSync as rmSync4,
24934
25908
  statSync as statSync7,
24935
25909
  lstatSync as lstatSync3,
24936
25910
  readlinkSync as readlinkSync4,
24937
25911
  unlinkSync as unlinkSync4
24938
25912
  } from "node:fs";
24939
25913
  import { homedir as homedir5 } from "node:os";
24940
- import { execSync, execFileSync as execFileSync4 } from "node:child_process";
25914
+ import { execSync, execFileSync as execFileSync6 } from "node:child_process";
24941
25915
  import { join as join10, resolve as resolve11 } from "node:path";
24942
25916
  import { createHash as createHash2 } from "node:crypto";
24943
25917
  function prependReplyDiscipline(rendered, context) {
@@ -25071,13 +26045,13 @@ function alignAgentUid(name, agentDir, uid, opts = {}) {
25071
26045
  `));
25072
26046
  }
25073
26047
  try {
25074
- execFileSync4("chown", ["-R", `${uid}:${uid}`, ...paths], {
26048
+ execFileSync6("chown", ["-R", `${uid}:${uid}`, ...paths], {
25075
26049
  stdio: ["ignore", "ignore", "pipe"]
25076
26050
  });
25077
26051
  return { chowned: true, paths };
25078
26052
  } catch {}
25079
26053
  try {
25080
- execFileSync4("sudo", ["chown", "-R", `${uid}:${uid}`, ...paths], {
26054
+ execFileSync6("sudo", ["chown", "-R", `${uid}:${uid}`, ...paths], {
25081
26055
  stdio: "inherit"
25082
26056
  });
25083
26057
  return { chowned: true, paths };
@@ -25271,7 +26245,7 @@ function migrateLegacySkillsDir(agentDir, skillsPool) {
25271
26245
  }
25272
26246
  if (target && target.startsWith(skillsPool)) {
25273
26247
  try {
25274
- rmSync3(entryPath, { force: true });
26248
+ rmSync4(entryPath, { force: true });
25275
26249
  } catch {}
25276
26250
  }
25277
26251
  }
@@ -25306,7 +26280,7 @@ function syncGlobalSkills(agentDir, declared, skillsDirOverride) {
25306
26280
  } catch {}
25307
26281
  if (target && target.startsWith(skillsPool)) {
25308
26282
  try {
25309
- rmSync3(dest, { force: true });
26283
+ rmSync4(dest, { force: true });
25310
26284
  } catch {}
25311
26285
  } else {
25312
26286
  continue;
@@ -25336,7 +26310,7 @@ function syncGlobalSkills(agentDir, declared, skillsDirOverride) {
25336
26310
  continue;
25337
26311
  }
25338
26312
  if (linkTarget && linkTarget.startsWith(skillsPool)) {
25339
- rmSync3(entryPath, { force: true });
26313
+ rmSync4(entryPath, { force: true });
25340
26314
  }
25341
26315
  }
25342
26316
  }
@@ -25391,7 +26365,7 @@ function installSwitchroomSkills(agentDir, opts = {}) {
25391
26365
  if (currentTarget !== join10(builtinSkillsDir, name))
25392
26366
  continue;
25393
26367
  try {
25394
- rmSync3(dest, { force: true });
26368
+ rmSync4(dest, { force: true });
25395
26369
  } catch {}
25396
26370
  }
25397
26371
  return;
@@ -25414,7 +26388,7 @@ function installSwitchroomSkills(agentDir, opts = {}) {
25414
26388
  if (currentTarget === src)
25415
26389
  continue;
25416
26390
  try {
25417
- rmSync3(dest, { force: true });
26391
+ rmSync4(dest, { force: true });
25418
26392
  } catch {}
25419
26393
  } else {
25420
26394
  continue;
@@ -25666,7 +26640,7 @@ function migrateLegacyAgentsMdIfPresent(agentWorkspaceDir, created) {
25666
26640
  }
25667
26641
  const content = readFileSync13(agentsMd, "utf-8");
25668
26642
  writeFileSync4(claudeMd, content, "utf-8");
25669
- rmSync3(agentsMd);
26643
+ rmSync4(agentsMd);
25670
26644
  created.push(claudeMd);
25671
26645
  console.log(source_default.dim(` migrated legacy workspace/AGENTS.md \u2192 workspace/CLAUDE.md (content preserved)`));
25672
26646
  }
@@ -25682,9 +26656,9 @@ function ensureClaudeMdSymlinks(agentWorkspaceDir, changes) {
25682
26656
  const target = readlinkSync4(linkPath);
25683
26657
  if (target === "CLAUDE.md")
25684
26658
  continue;
25685
- rmSync3(linkPath);
26659
+ rmSync4(linkPath);
25686
26660
  } else {
25687
- rmSync3(linkPath);
26661
+ rmSync4(linkPath);
25688
26662
  }
25689
26663
  }
25690
26664
  symlinkSync2("CLAUDE.md", linkPath);
@@ -25725,7 +26699,7 @@ Thumbs.db
25725
26699
  execSync("git add -A", { cwd: workspaceDir, stdio: "pipe" });
25726
26700
  const userEmail = process.env.GIT_AUTHOR_EMAIL || "switchroom@localhost";
25727
26701
  const userName = process.env.GIT_AUTHOR_NAME || "Switchroom Agent";
25728
- execFileSync4("git", [
26702
+ execFileSync6("git", [
25729
26703
  "-c",
25730
26704
  `user.email=${userEmail}`,
25731
26705
  "-c",
@@ -25763,27 +26737,36 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig) {
25763
26737
  }
25764
26738
  const destPath = join10(agentDir, ".claude", "plugins", "hindsight-memory");
25765
26739
  const additionalBanks = resolveUsers(switchroomConfig, agentName).additionalBanks;
26740
+ const retainConfig = resolveHindsightRetainConfig(switchroomConfig, agentName);
25766
26741
  let expectedSettings = null;
25767
26742
  try {
25768
26743
  const vendorSettingsPath = join10(sourcePath, "settings.json");
25769
26744
  if (existsSync15(vendorSettingsPath)) {
25770
- expectedSettings = renderHindsightSettingsOverrides(readFileSync13(vendorSettingsPath, "utf-8"), additionalBanks);
26745
+ expectedSettings = renderHindsightSettingsOverrides(readFileSync13(vendorSettingsPath, "utf-8"), additionalBanks, retainConfig);
25771
26746
  }
25772
26747
  } catch {}
25773
26748
  const upToDate = dirContentEquals(sourcePath, destPath, expectedSettings != null ? { "settings.json": expectedSettings } : undefined);
25774
26749
  if (!upToDate) {
25775
26750
  if (existsSync15(destPath)) {
25776
- rmSync3(destPath, { recursive: true, force: true });
26751
+ rmSync4(destPath, { recursive: true, force: true });
25777
26752
  }
25778
26753
  copyDirRecursive2(sourcePath, destPath);
25779
26754
  }
25780
- applyHindsightSettingsOverrides(destPath, additionalBanks);
26755
+ applyHindsightSettingsOverrides(destPath, additionalBanks, retainConfig);
25781
26756
  const bankId = agentMemory?.collection ?? agentName;
25782
- const mcpUrl = memory.config?.url ?? "http://127.0.0.1:8888/mcp/";
26757
+ const mcpUrl = memory.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
25783
26758
  const apiBaseUrl = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
25784
26759
  return { pluginDir: destPath, apiBaseUrl, bankId };
25785
26760
  }
25786
- function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks) {
26761
+ function resolveHindsightRetainConfig(switchroomConfig, agentName) {
26762
+ const resolved = switchroomConfig ? resolveAgentConfig(switchroomConfig.defaults, switchroomConfig.profiles, switchroomConfig.agents[agentName] ?? {}) : undefined;
26763
+ const retain = resolved?.memory?.retain;
26764
+ return {
26765
+ everyNTurns: retain?.every_n_turns ?? HINDSIGHT_DEFAULT_RETAIN_EVERY_N_TURNS,
26766
+ overlapTurns: retain?.overlap_turns ?? HINDSIGHT_DEFAULT_RETAIN_OVERLAP_TURNS
26767
+ };
26768
+ }
26769
+ function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks, retainConfig) {
25787
26770
  const settingsPath = join10(pluginDestPath, "settings.json");
25788
26771
  if (!existsSync15(settingsPath))
25789
26772
  return;
@@ -25793,41 +26776,51 @@ function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks) {
25793
26776
  } catch {
25794
26777
  return;
25795
26778
  }
25796
- const next = renderHindsightSettingsOverrides(raw, additionalBanks);
26779
+ const next = renderHindsightSettingsOverrides(raw, additionalBanks, retainConfig);
25797
26780
  if (next == null)
25798
26781
  return;
25799
26782
  writeFileSyncIfChanged(settingsPath, next);
25800
26783
  }
25801
- function renderHindsightSettingsOverrides(raw, additionalBanks) {
26784
+ function renderHindsightSettingsOverrides(raw, additionalBanks, retainConfig) {
25802
26785
  let settings;
25803
26786
  try {
25804
26787
  settings = JSON.parse(raw);
25805
26788
  } catch {
25806
26789
  return null;
25807
26790
  }
25808
- settings.retainEveryNTurns = 1;
26791
+ settings.retainEveryNTurns = retainConfig.everyNTurns;
25809
26792
  settings.retainMode = "chunked";
26793
+ settings.retainOverlapTurns = retainConfig.overlapTurns;
25810
26794
  settings.recallMaxMemories = 8;
25811
26795
  settings.recallMinOverlap = 0.1;
25812
26796
  settings.recallTypes = ["world", "experience", "observation"];
25813
26797
  settings.recallSkipTrivial = true;
26798
+ settings.directiveCaptureNudge = true;
25814
26799
  if (additionalBanks.length > 0) {
25815
26800
  settings.recallAdditionalBanks = [...additionalBanks];
25816
26801
  }
25817
26802
  return JSON.stringify(settings, null, 2) + `
25818
26803
  `;
25819
26804
  }
26805
+ function resolvePermissionDefaultMode(agentConfig, switchroomConfig) {
26806
+ const raw = agentConfig.permission_mode ?? switchroomConfig?.defaults?.permission_mode ?? SWITCHROOM_DEFAULT_PERMISSION_MODE;
26807
+ if (VALID_SETTINGS_DEFAULT_MODES.includes(raw)) {
26808
+ return raw;
26809
+ }
26810
+ console.warn(source_default.yellow(`[switchroom] permission_mode "${raw}" has no settings.json defaultMode ` + `equivalent \u2014 using "${SWITCHROOM_DEFAULT_PERMISSION_MODE}" for ` + `permissions.defaultMode (the --permission-mode CLI flag still carries "${raw}").`));
26811
+ return SWITCHROOM_DEFAULT_PERMISSION_MODE;
26812
+ }
25820
26813
  function buildWorkspaceContext(args) {
25821
26814
  const {
25822
26815
  name,
25823
26816
  agentDir,
25824
26817
  agentConfig,
25825
26818
  telegramConfig,
26819
+ switchroomConfig,
25826
26820
  switchroomConfigPath,
25827
26821
  topicId,
25828
26822
  tools,
25829
26823
  permissionAllow,
25830
- hasAllWildcard,
25831
26824
  resolvedBotToken,
25832
26825
  rawBotToken,
25833
26826
  hindsightAutoRecallEnabled,
@@ -25838,6 +26831,7 @@ function buildWorkspaceContext(args) {
25838
26831
  hindsightRecallMinOverlap,
25839
26832
  hindsightRecallTypes,
25840
26833
  hindsightRecallSkipTrivial,
26834
+ hindsightDirectiveCaptureNudge,
25841
26835
  hindsightTopicAliasesJson,
25842
26836
  hindsightSenderBanksJson,
25843
26837
  hindsightTopicFilterMode
@@ -25859,7 +26853,7 @@ function buildWorkspaceContext(args) {
25859
26853
  ...INTERACTIVE_TUI_FLEET_DENY_TOOLS
25860
26854
  ]),
25861
26855
  permissionAllow,
25862
- defaultModeAcceptEdits: hasAllWildcard,
26856
+ defaultMode: resolvePermissionDefaultMode(agentConfig, switchroomConfig),
25863
26857
  memory: agentConfig.memory,
25864
26858
  model: agentConfig.model,
25865
26859
  mcpServers: agentConfig.mcp_servers ? filterMcpServers(agentConfig.mcp_servers) : agentConfig.mcp_servers,
@@ -25882,6 +26876,7 @@ function buildWorkspaceContext(args) {
25882
26876
  hindsightRecallMinOverlap,
25883
26877
  hindsightRecallTypes,
25884
26878
  hindsightRecallSkipTrivial,
26879
+ hindsightDirectiveCaptureNudge,
25885
26880
  hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
25886
26881
  hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
25887
26882
  hindsightTopicFilterMode,
@@ -26052,12 +27047,13 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
26052
27047
  ]);
26053
27048
  const hindsightAutoRecallEnabled = hindsightEnabled && agentConfig.memory?.auto_recall !== false;
26054
27049
  const hindsightBankId = agentConfig.memory?.collection ?? name;
26055
- const hindsightApiBaseUrl = switchroomConfig?.memory?.config?.url ? switchroomConfig.memory.config.url.replace(/\/mcp\/?$/, "").replace(/\/$/, "") : "http://127.0.0.1:8888";
27050
+ const hindsightApiBaseUrl = switchroomConfig?.memory?.config?.url ? switchroomConfig.memory.config.url.replace(/\/mcp\/?$/, "").replace(/\/$/, "") : HINDSIGHT_DEFAULT_API_BASE_URL;
26056
27051
  const hindsightRecallMaxMemories = agentConfig.memory?.recall?.max_memories;
26057
27052
  const hindsightRecallCacheTtlSecs = agentConfig.memory?.recall?.cache_ttl_secs;
26058
27053
  const hindsightRecallMinOverlap = agentConfig.memory?.recall?.min_overlap;
26059
27054
  const hindsightRecallTypes = agentConfig.memory?.recall?.types?.length ? agentConfig.memory.recall.types.join(",") : undefined;
26060
27055
  const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
27056
+ const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
26061
27057
  const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
26062
27058
  const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
26063
27059
  const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
@@ -26084,6 +27080,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
26084
27080
  hindsightRecallMinOverlap,
26085
27081
  hindsightRecallTypes,
26086
27082
  hindsightRecallSkipTrivial,
27083
+ hindsightDirectiveCaptureNudge,
26087
27084
  hindsightTopicAliasesJson,
26088
27085
  hindsightSenderBanksJson,
26089
27086
  hindsightTopicFilterMode
@@ -26452,12 +27449,12 @@ ${body}
26452
27449
  if (target === "workspace/SOUL.md") {
26453
27450
  skipped.push(agentSoulPath);
26454
27451
  } else {
26455
- rmSync3(agentSoulPath);
27452
+ rmSync4(agentSoulPath);
26456
27453
  symlinkSync2("workspace/SOUL.md", agentSoulPath);
26457
27454
  created.push(agentSoulPath);
26458
27455
  }
26459
27456
  } else {
26460
- rmSync3(agentSoulPath);
27457
+ rmSync4(agentSoulPath);
26461
27458
  symlinkSync2("workspace/SOUL.md", agentSoulPath);
26462
27459
  created.push(agentSoulPath);
26463
27460
  }
@@ -26490,8 +27487,10 @@ ${body}
26490
27487
  const userBankMission = agentConfig.memory?.bank_mission;
26491
27488
  const userRetainMission = agentConfig.memory?.retain_mission;
26492
27489
  const seededRetainMission = userRetainMission ?? DEFAULT_RETAIN_MISSION;
27490
+ const extras = resolveBankMissionExtras(agentConfig.memory, agentConfig.extends ?? DEFAULT_PROFILE);
26493
27491
  const missions = {
26494
- retain_mission: seededRetainMission
27492
+ retain_mission: seededRetainMission,
27493
+ ...extras
26495
27494
  };
26496
27495
  if (userBankMission) {
26497
27496
  missions.bank_mission = userBankMission;
@@ -26506,6 +27505,17 @@ ${body}
26506
27505
  }).catch((err) => {
26507
27506
  console.warn(` ${source_default.yellow("\u26a0")} Bank mission update error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
26508
27507
  });
27508
+ ensureDeclaredMentalModels(apiUrl, hindsightBankId, agentConfig.memory?.mental_models, { timeoutMs: 5000 }).then((outcomes) => {
27509
+ for (const o of outcomes) {
27510
+ if (o.ok) {
27511
+ console.log(` ${source_default.green("\u2713")} Mental model "${o.name}" ready for ${formatAgentBankLabel(name, hindsightBankId)}`);
27512
+ } else {
27513
+ console.warn(` ${source_default.yellow("\u26a0")} Failed to ensure mental model "${o.name}" for ${formatAgentBankLabel(name, hindsightBankId)}: ${o.reason}`);
27514
+ }
27515
+ }
27516
+ }).catch((err) => {
27517
+ console.warn(` ${source_default.yellow("\u26a0")} Mental model ensure error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
27518
+ });
26509
27519
  });
26510
27520
  }
26511
27521
  for (const f of rewrittenWithBackup) {
@@ -26759,7 +27769,7 @@ function buildSettingsHooksBlock(p) {
26759
27769
  hooks: [
26760
27770
  {
26761
27771
  type: "command",
26762
- command: wrap("hook:workspace-dynamic", `${useInjectOnChange ? `env SWITCHROOM_INJECT_ON_CHANGE=1 ` : ""}bash "${join10(DOCKER_BIN_PATH, "workspace-dynamic-hook.sh")}"`),
27772
+ command: wrap("hook:workspace-dynamic", `${useInjectOnChange ? "" : `env SWITCHROOM_INJECT_ON_CHANGE=0 `}bash "${join10(DOCKER_BIN_PATH, "workspace-dynamic-hook.sh")}"`),
26763
27773
  timeout: 5
26764
27774
  }
26765
27775
  ]
@@ -26884,7 +27894,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
26884
27894
  mkdirSync10(workspaceDir, { recursive: true });
26885
27895
  const legacyContent = readFileSync13(legacyCustomPath, "utf-8");
26886
27896
  writeFileSync4(newCustomPath, legacyContent, "utf-8");
26887
- rmSync3(legacyCustomPath);
27897
+ rmSync4(legacyCustomPath);
26888
27898
  console.log(source_default.green(` moved CLAUDE.custom.md \u2192 workspace/CLAUDE.custom.md`));
26889
27899
  }
26890
27900
  const tools = agentConfig.tools ?? { allow: [], deny: [] };
@@ -26923,12 +27933,13 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
26923
27933
  const resolvedBotToken = resolveBotToken(rawBotToken);
26924
27934
  const hindsightAutoRecallEnabled = hindsightEnabled && agentConfig.memory?.auto_recall !== false;
26925
27935
  const hindsightBankId = agentConfig.memory?.collection ?? name;
26926
- const hindsightApiBaseUrl = switchroomConfig.memory?.config?.url ? switchroomConfig.memory.config.url.replace(/\/mcp\/?$/, "").replace(/\/$/, "") : "http://127.0.0.1:8888";
27936
+ const hindsightApiBaseUrl = switchroomConfig.memory?.config?.url ? switchroomConfig.memory.config.url.replace(/\/mcp\/?$/, "").replace(/\/$/, "") : HINDSIGHT_DEFAULT_API_BASE_URL;
26927
27937
  const hindsightRecallMaxMemories = agentConfig.memory?.recall?.max_memories;
26928
27938
  const hindsightRecallCacheTtlSecs = agentConfig.memory?.recall?.cache_ttl_secs;
26929
27939
  const hindsightRecallMinOverlap = agentConfig.memory?.recall?.min_overlap;
26930
27940
  const hindsightRecallTypes = agentConfig.memory?.recall?.types?.length ? agentConfig.memory.recall.types.join(",") : undefined;
26931
27941
  const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
27942
+ const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
26932
27943
  const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
26933
27944
  const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
26934
27945
  const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
@@ -26968,6 +27979,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
26968
27979
  hindsightRecallMinOverlap,
26969
27980
  hindsightRecallTypes,
26970
27981
  hindsightRecallSkipTrivial,
27982
+ hindsightDirectiveCaptureNudge,
26971
27983
  hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
26972
27984
  hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
26973
27985
  hindsightTopicFilterMode,
@@ -27107,11 +28119,7 @@ ${baseAppend}` : TELEGRAM_FORMATTING_FLOOR_CARD;
27107
28119
  settings.permissions = settings.permissions ?? {};
27108
28120
  settings.permissions.allow = desiredAllow;
27109
28121
  settings.permissions.deny = desiredDeny;
27110
- if (hasAllWildcard) {
27111
- settings.permissions.defaultMode = "acceptEdits";
27112
- } else {
27113
- delete settings.permissions.defaultMode;
27114
- }
28122
+ settings.permissions.defaultMode = resolvePermissionDefaultMode(agentConfig, switchroomConfig);
27115
28123
  const mcpServers = {};
27116
28124
  const hindsightEntry = getHindsightSettingsEntry(name, switchroomConfig);
27117
28125
  if (hindsightEntry) {
@@ -27369,6 +28377,7 @@ ${body}
27369
28377
  hindsightRecallMinOverlap,
27370
28378
  hindsightRecallTypes,
27371
28379
  hindsightRecallSkipTrivial,
28380
+ hindsightDirectiveCaptureNudge,
27372
28381
  hindsightTopicAliasesJson,
27373
28382
  hindsightSenderBanksJson,
27374
28383
  hindsightTopicFilterMode
@@ -27397,12 +28406,12 @@ ${body}
27397
28406
  if (stat.isSymbolicLink()) {
27398
28407
  const target = readlinkSync4(agentSoulPath);
27399
28408
  if (target !== "workspace/SOUL.md") {
27400
- rmSync3(agentSoulPath);
28409
+ rmSync4(agentSoulPath);
27401
28410
  symlinkSync2("workspace/SOUL.md", agentSoulPath);
27402
28411
  changes.push(agentSoulPath);
27403
28412
  }
27404
28413
  } else {
27405
- rmSync3(agentSoulPath);
28414
+ rmSync4(agentSoulPath);
27406
28415
  symlinkSync2("workspace/SOUL.md", agentSoulPath);
27407
28416
  changes.push(agentSoulPath);
27408
28417
  }
@@ -27446,14 +28455,15 @@ ${body}
27446
28455
  bankOpsChain.then((bankReady) => {
27447
28456
  if (!bankReady)
27448
28457
  return;
27449
- if (agentConfig.memory?.bank_mission || agentConfig.memory?.retain_mission) {
27450
- const missions = {};
27451
- if (agentConfig.memory?.bank_mission) {
27452
- missions.bank_mission = agentConfig.memory.bank_mission;
27453
- }
27454
- if (agentConfig.memory?.retain_mission) {
27455
- missions.retain_mission = agentConfig.memory.retain_mission;
27456
- }
28458
+ const extras = resolveBankMissionExtras(agentConfig.memory, agentConfig.extends ?? DEFAULT_PROFILE);
28459
+ const missions = { ...extras };
28460
+ if (agentConfig.memory?.bank_mission) {
28461
+ missions.bank_mission = agentConfig.memory.bank_mission;
28462
+ }
28463
+ if (agentConfig.memory?.retain_mission) {
28464
+ missions.retain_mission = agentConfig.memory.retain_mission;
28465
+ }
28466
+ if (Object.keys(missions).length > 0) {
27457
28467
  updateBankMissions(apiUrl, hindsightBankId, missions, { timeoutMs: 5000 }).then((result) => {
27458
28468
  if (result.ok) {
27459
28469
  console.log(` ${source_default.green("\u2713")} Bank missions updated for ${formatAgentBankLabel(name, hindsightBankId)}`);
@@ -27464,6 +28474,17 @@ ${body}
27464
28474
  console.warn(` ${source_default.yellow("\u26a0")} Bank mission update error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
27465
28475
  });
27466
28476
  }
28477
+ ensureDeclaredMentalModels(apiUrl, hindsightBankId, agentConfig.memory?.mental_models, { timeoutMs: 5000 }).then((outcomes) => {
28478
+ for (const o of outcomes) {
28479
+ if (o.ok) {
28480
+ console.log(` ${source_default.green("\u2713")} Mental model "${o.name}" ready for ${formatAgentBankLabel(name, hindsightBankId)}`);
28481
+ } else {
28482
+ console.warn(` ${source_default.yellow("\u26a0")} Failed to ensure mental model "${o.name}" for ${formatAgentBankLabel(name, hindsightBankId)}: ${o.reason}`);
28483
+ }
28484
+ }
28485
+ }).catch((err) => {
28486
+ console.warn(` ${source_default.yellow("\u26a0")} Mental model ensure error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
28487
+ });
27467
28488
  });
27468
28489
  }
27469
28490
  return {
@@ -27558,7 +28579,7 @@ function reconcileConfiguredGroup(accessPath, agentConfig, telegramConfig) {
27558
28579
  const tmp = accessPath + ".tmp";
27559
28580
  writeFileSync4(tmp, JSON.stringify(access, null, 2) + `
27560
28581
  `, { mode: 384 });
27561
- renameSync3(tmp, accessPath);
28582
+ renameSync4(tmp, accessPath);
27562
28583
  console.log(source_default.green(` registered supergroup ${forumChatId} in access.json ` + `(responds to all topics; requireMention=false)`));
27563
28584
  }
27564
28585
  function buildAccessJson2(agentConfig, telegramConfig, resolvedTopicId, userId) {
@@ -27902,7 +28923,7 @@ in doubt, shorter and plainer wins.
27902
28923
 
27903
28924
  Every turn that answers a user message ends with a user-visible \`reply\`
27904
28925
  \u2014 Telegram is all the user sees; your terminal output
27905
- never reaches them.`, SWITCHROOM_TELEGRAM_MCP_TOOLS, HINDSIGHT_MCP_TOOLS, AGENT_CONFIG_MCP_TOOLS, HOSTD_MCP_TOOLS, WEBKITE_MCP_TOOLS, LEGACY_SWITCHROOM_MCP_TOKENS, LEGACY_HOSTD_BLANKET_TOKENS, DEFAULT_READ_ONLY_PREAPPROVED_TOOLS, WEBKITE_FLEET_DENY_TOOLS, INTERACTIVE_TUI_FLEET_DENY_TOOLS, WEBKITE_BINARY_CONTAINER_PATH = "/usr/local/bin/webkite", SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-5", SWITCHROOM_DEFAULT_THINKING_EFFORT = "low", SWITCHROOM_OWNED_SETTINGS_KEYS, ALL_BUILTIN_TOOLS, DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin", DOCKER_HOOKS_PATH, DOCKER_BUNDLED_HOOKS_PATH = "/opt/switchroom/hooks", DOCKER_BIN_PATH = "/opt/switchroom/bin", DOCKER_SECURITY_PLUGIN_PATH = "/opt/switchroom/security-plugin", DOCKER_CONFIG_PATH = "/state/config/switchroom.yaml", DOCKER_SWITCHROOM_CLI_PATH = "/usr/local/bin/switchroom", DOCKER_AUTH_BROKER_SOCKET = "/run/switchroom/auth-broker/sock", DOCKER_VAULT_BROKER_SOCKET = "/run/switchroom/broker/sock", DOCKER_AGENT_HOME = "/state/agent/home", INTEGRATION_MCP_RESOLVERS;
28926
+ never reaches them.`, SWITCHROOM_TELEGRAM_MCP_TOOLS, HINDSIGHT_MCP_TOOLS, AGENT_CONFIG_MCP_TOOLS, HOSTD_MCP_TOOLS, WEBKITE_MCP_TOOLS, LEGACY_SWITCHROOM_MCP_TOKENS, LEGACY_HOSTD_BLANKET_TOKENS, DEFAULT_READ_ONLY_PREAPPROVED_TOOLS, WEBKITE_FLEET_DENY_TOOLS, INTERACTIVE_TUI_FLEET_DENY_TOOLS, WEBKITE_BINARY_CONTAINER_PATH = "/usr/local/bin/webkite", SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-5", SWITCHROOM_DEFAULT_THINKING_EFFORT = "low", SWITCHROOM_OWNED_SETTINGS_KEYS, ALL_BUILTIN_TOOLS, HINDSIGHT_DEFAULT_RETAIN_EVERY_N_TURNS = 3, HINDSIGHT_DEFAULT_RETAIN_OVERLAP_TURNS = 1, VALID_SETTINGS_DEFAULT_MODES, SWITCHROOM_DEFAULT_PERMISSION_MODE = "acceptEdits", DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin", DOCKER_HOOKS_PATH, DOCKER_BUNDLED_HOOKS_PATH = "/opt/switchroom/hooks", DOCKER_BIN_PATH = "/opt/switchroom/bin", DOCKER_SECURITY_PLUGIN_PATH = "/opt/switchroom/security-plugin", DOCKER_CONFIG_PATH = "/state/config/switchroom.yaml", DOCKER_SWITCHROOM_CLI_PATH = "/usr/local/bin/switchroom", DOCKER_AUTH_BROKER_SOCKET = "/run/switchroom/auth-broker/sock", DOCKER_VAULT_BROKER_SOCKET = "/run/switchroom/broker/sock", DOCKER_AGENT_HOME = "/state/agent/home", INTEGRATION_MCP_RESOLVERS;
27906
28927
  var init_scaffold = __esm(() => {
27907
28928
  init_source();
27908
28929
  init_cron_unit_name();
@@ -27916,13 +28937,14 @@ var init_scaffold = __esm(() => {
27916
28937
  init_profiles();
27917
28938
  init_scaffold_integration();
27918
28939
  init_reconcile_default_skills();
27919
- init_hindsight();
28940
+ init_hindsight2();
27920
28941
  init_state();
27921
28942
  init_paths();
27922
28943
  init_loader();
27923
28944
  init_resolver();
27924
28945
  init_vault();
27925
28946
  init_onboarding();
28947
+ init_hindsight();
27926
28948
  init_bare_clone();
27927
28949
  init_agent_worktree();
27928
28950
  REPO_ROOT = resolve11(import.meta.dirname, "../..");
@@ -27940,8 +28962,31 @@ var init_scaffold = __esm(() => {
27940
28962
  "mcp__switchroom-telegram__progress_update"
27941
28963
  ];
27942
28964
  HINDSIGHT_MCP_TOOLS = [
27943
- "mcp__hindsight",
27944
- "mcp__hindsight__*"
28965
+ "mcp__hindsight__recall",
28966
+ "mcp__hindsight__retain",
28967
+ "mcp__hindsight__sync_retain",
28968
+ "mcp__hindsight__reflect",
28969
+ "mcp__hindsight__create_directive",
28970
+ "mcp__hindsight__update_bank",
28971
+ "mcp__hindsight__delete_directive",
28972
+ "mcp__hindsight__list_directives",
28973
+ "mcp__hindsight__create_bank",
28974
+ "mcp__hindsight__get_bank",
28975
+ "mcp__hindsight__get_bank_stats",
28976
+ "mcp__hindsight__list_banks",
28977
+ "mcp__hindsight__delete_bank",
28978
+ "mcp__hindsight__get_memory",
28979
+ "mcp__hindsight__list_memories",
28980
+ "mcp__hindsight__clear_memories",
28981
+ "mcp__hindsight__get_document",
28982
+ "mcp__hindsight__list_documents",
28983
+ "mcp__hindsight__delete_document",
28984
+ "mcp__hindsight__get_mental_model",
28985
+ "mcp__hindsight__list_mental_models",
28986
+ "mcp__hindsight__list_tags",
28987
+ "mcp__hindsight__get_operation",
28988
+ "mcp__hindsight__list_operations",
28989
+ "mcp__hindsight__cancel_operation"
27945
28990
  ];
27946
28991
  AGENT_CONFIG_MCP_TOOLS = [
27947
28992
  "mcp__agent-config",
@@ -27993,6 +29038,12 @@ var init_scaffold = __esm(() => {
27993
29038
  "Agent",
27994
29039
  "ExitPlanMode"
27995
29040
  ];
29041
+ VALID_SETTINGS_DEFAULT_MODES = [
29042
+ "default",
29043
+ "acceptEdits",
29044
+ "plan",
29045
+ "bypassPermissions"
29046
+ ];
27996
29047
  DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
27997
29048
  INTEGRATION_MCP_RESOLVERS = [
27998
29049
  {
@@ -28237,6 +29288,7 @@ function describeAgents(config, litellmConfirmedAgents) {
28237
29288
  baseUrl: resolved.litellm?.base_url ?? config.litellm?.base_url,
28238
29289
  smallFastModel: resolved.litellm?.small_fast_model ?? config.litellm?.small_fast_model ?? "claude-haiku-4-5-20251001"
28239
29290
  },
29291
+ model: resolveMainModel(resolved.model),
28240
29292
  timezone: resolveTimezone(config, resolved, {
28241
29293
  onUtcFallback: () => {
28242
29294
  console.warn(` \u26a0 timezone: no explicit timezone set and server detection resolved to UTC ` + `for agent "${name}" \u2014 cron schedules and the per-turn time hint will ` + `run in UTC. Add \`switchroom.timezone: "Region/City"\` (e.g. ` + `"Australia/Melbourne") to switchroom.yaml to silence this warning.`);
@@ -28669,7 +29721,7 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
28669
29721
  const root = a.litellm.baseUrl.replace(/\/+$/, "");
28670
29722
  env2.SWITCHROOM_LITELLM = "1";
28671
29723
  env2.SWITCHROOM_LITELLM_BASE = root;
28672
- env2.ANTHROPIC_BASE_URL = `${root}/anthropic`;
29724
+ env2.ANTHROPIC_BASE_URL = isClaudeModel(a.model) ? `${root}/anthropic` : root;
28673
29725
  env2.ANTHROPIC_SMALL_FAST_MODEL = a.litellm.smallFastModel;
28674
29726
  }
28675
29727
  env2.SWITCHROOM_VOICE_ENGINE = voiceEngine;
@@ -28767,6 +29819,8 @@ var init_compose = __esm(() => {
28767
29819
  init_schema();
28768
29820
  init_cron_routing();
28769
29821
  init_tier_selector();
29822
+ init_scaffold();
29823
+ init_model_command();
28770
29824
  init_merge();
28771
29825
  init_timezone();
28772
29826
  init_peercred();
@@ -29033,7 +30087,7 @@ function resolveHostHomeForCompose() {
29033
30087
  }
29034
30088
  return homedir6();
29035
30089
  }
29036
- async function writeComposeFile(opts) {
30090
+ async function computeComposeContent(opts) {
29037
30091
  const release = resolveRelease({ override: opts.releaseOverride, root: opts.config.release });
29038
30092
  const imageTag = resolveImageTag(release);
29039
30093
  const operatorUid = resolveOperatorUid();
@@ -29057,6 +30111,11 @@ async function writeComposeFile(opts) {
29057
30111
  previous = null;
29058
30112
  }
29059
30113
  const previousImageTag = previous ? AGENT_IMAGE_TAG_RE.exec(previous)?.[1] ?? null : null;
30114
+ return { content, imageTag, previous, previousImageTag };
30115
+ }
30116
+ async function writeComposeFile(opts) {
30117
+ const { content, imageTag, previous, previousImageTag } = await computeComposeContent(opts);
30118
+ const operatorUid = resolveOperatorUid();
29060
30119
  await mkdir(dirname6(opts.composePath), { recursive: true });
29061
30120
  if (previous !== null) {
29062
30121
  try {
@@ -29089,7 +30148,7 @@ var init_write_compose = __esm(() => {
29089
30148
  });
29090
30149
 
29091
30150
  // src/agents/tmux.ts
29092
- import { execFileSync as execFileSync5 } from "node:child_process";
30151
+ import { execFileSync as execFileSync7 } from "node:child_process";
29093
30152
  function sendAgentInterrupt(opts) {
29094
30153
  const { agentName } = opts;
29095
30154
  const attempts = typeof opts.attempts === "number" && opts.attempts > 0 ? opts.attempts : 1;
@@ -29099,7 +30158,7 @@ function sendAgentInterrupt(opts) {
29099
30158
  let lastError = null;
29100
30159
  for (let i = 0;i < attempts; i++) {
29101
30160
  try {
29102
- execFileSync5("tmux", args, {
30161
+ execFileSync7("tmux", args, {
29103
30162
  timeout: 3000,
29104
30163
  stdio: ["ignore", "pipe", "pipe"]
29105
30164
  });
@@ -29140,7 +30199,7 @@ var init_compose_env = () => {};
29140
30199
  import { resolve as resolve13 } from "node:path";
29141
30200
  import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync6 } from "node:fs";
29142
30201
  import { homedir as homedir7 } from "node:os";
29143
- import { execFileSync as execFileSync6 } from "node:child_process";
30202
+ import { execFileSync as execFileSync8 } from "node:child_process";
29144
30203
  function resolveSwitchroomHome(explicit) {
29145
30204
  if (explicit && explicit.length > 0)
29146
30205
  return explicit;
@@ -29171,7 +30230,7 @@ function bringUpAgentService(opts) {
29171
30230
  const dockerBin = opts.dockerBin ?? "docker";
29172
30231
  const stdio = opts.stdio ?? "inherit";
29173
30232
  for (const svc of ["vault-broker", "approval-kernel", "switchroom-auth-broker"]) {
29174
- execFileSync6(dockerBin, [
30233
+ execFileSync8(dockerBin, [
29175
30234
  "compose",
29176
30235
  "-f",
29177
30236
  composePath,
@@ -29183,7 +30242,7 @@ function bringUpAgentService(opts) {
29183
30242
  svc
29184
30243
  ], { stdio });
29185
30244
  }
29186
- execFileSync6(dockerBin, [
30245
+ execFileSync8(dockerBin, [
29187
30246
  "compose",
29188
30247
  "-f",
29189
30248
  composePath,
@@ -29202,7 +30261,7 @@ var init_docker_fleet = __esm(() => {
29202
30261
  });
29203
30262
 
29204
30263
  // src/agents/singleton-reconcile.ts
29205
- import { execFileSync as execFileSync7 } from "node:child_process";
30264
+ import { execFileSync as execFileSync9 } from "node:child_process";
29206
30265
  import { readFileSync as readFileSync16 } from "node:fs";
29207
30266
  function resolveSingletonServices(voiceEngine) {
29208
30267
  const engine = voiceEngine ?? loadHostCapabilities()?.voice.engine ?? "cloud";
@@ -29228,7 +30287,7 @@ function readPinnedSingletonImages(composeText, services = SINGLETON_SERVICES) {
29228
30287
  }
29229
30288
  function defaultInspectImage(dockerBin, container) {
29230
30289
  try {
29231
- const out = execFileSync7(dockerBin, ["inspect", "-f", "{{.Config.Image}}", container], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 }).trim();
30290
+ const out = execFileSync9(dockerBin, ["inspect", "-f", "{{.Config.Image}}", container], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 }).trim();
29232
30291
  return out.length > 0 ? out : null;
29233
30292
  } catch {
29234
30293
  return null;
@@ -29254,7 +30313,7 @@ function detectSingletonDrift(deps) {
29254
30313
  });
29255
30314
  }
29256
30315
  function defaultRecreate(dockerBin, project, composeFile, service) {
29257
- execFileSync7(dockerBin, ["compose", "-p", project, "-f", composeFile, ...composeEnvFileArgs(composeFile), "up", "-d", "--no-deps", service], { stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
30316
+ execFileSync9(dockerBin, ["compose", "-p", project, "-f", composeFile, ...composeEnvFileArgs(composeFile), "up", "-d", "--no-deps", service], { stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
29258
30317
  }
29259
30318
  function reconcileSingletons(deps) {
29260
30319
  const dockerBin = deps.dockerBin ?? "docker";
@@ -29297,8 +30356,8 @@ var init_singleton_reconcile = __esm(() => {
29297
30356
  });
29298
30357
 
29299
30358
  // src/agents/lifecycle.ts
29300
- import { execFileSync as execFileSync8, spawn, spawnSync } from "node:child_process";
29301
- import { existsSync as existsSync20, mkdirSync as mkdirSync14, writeFileSync as writeFileSync7, renameSync as renameSync4, readFileSync as readFileSync17 } from "node:fs";
30359
+ import { execFileSync as execFileSync10, spawn, spawnSync } from "node:child_process";
30360
+ import { existsSync as existsSync20, mkdirSync as mkdirSync14, writeFileSync as writeFileSync7, renameSync as renameSync5, readFileSync as readFileSync17 } from "node:fs";
29302
30361
  import { resolve as resolve14, join as join14 } from "node:path";
29303
30362
  import { connect } from "node:net";
29304
30363
  function cleanShutdownMarkerPathForAgent(name) {
@@ -29320,7 +30379,7 @@ function writeRestartReasonMarker(name, reason, opts = {}) {
29320
30379
  const marker = { ts: Date.now(), signal: "SIGTERM", reason };
29321
30380
  const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
29322
30381
  writeFileSync7(tmp, JSON.stringify(marker), "utf-8");
29323
- renameSync4(tmp, path);
30382
+ renameSync5(tmp, path);
29324
30383
  } catch {}
29325
30384
  }
29326
30385
  function buildCliRestartReason(opts) {
@@ -29328,7 +30387,7 @@ function buildCliRestartReason(opts) {
29328
30387
  if (!buildCommit)
29329
30388
  return "cli: restart";
29330
30389
  try {
29331
- const head = execFileSync8("git", ["rev-parse", "HEAD"], {
30390
+ const head = execFileSync10("git", ["rev-parse", "HEAD"], {
29332
30391
  cwd: cwd ?? process.cwd(),
29333
30392
  encoding: "utf-8",
29334
30393
  stdio: ["ignore", "pipe", "ignore"]
@@ -29339,7 +30398,7 @@ function buildCliRestartReason(opts) {
29339
30398
  return "cli: restart";
29340
30399
  let subject = "";
29341
30400
  try {
29342
- subject = execFileSync8("git", ["log", "-1", "--pretty=%s", head], {
30401
+ subject = execFileSync10("git", ["log", "-1", "--pretty=%s", head], {
29343
30402
  cwd: cwd ?? process.cwd(),
29344
30403
  encoding: "utf-8",
29345
30404
  stdio: ["ignore", "pipe", "ignore"]
@@ -29369,7 +30428,7 @@ function reconcileSingletonImages(log) {
29369
30428
  }
29370
30429
  function dockerSync(args) {
29371
30430
  try {
29372
- return execFileSync8("docker", args, {
30431
+ return execFileSync10("docker", args, {
29373
30432
  encoding: "utf-8",
29374
30433
  stdio: ["ignore", "pipe", "pipe"]
29375
30434
  }).trim();
@@ -29758,12 +30817,12 @@ var init_lifecycle = __esm(() => {
29758
30817
  });
29759
30818
 
29760
30819
  // src/auth/pane-ready-probe.ts
29761
- import { execFileSync as execFileSync10 } from "node:child_process";
30820
+ import { execFileSync as execFileSync12 } from "node:child_process";
29762
30821
  function defaultPaneReadyDeps() {
29763
30822
  return {
29764
30823
  capturePane(sessionName) {
29765
30824
  try {
29766
- return execFileSync10("tmux", ["capture-pane", "-p", "-t", sessionName, "-S", "-200"], {
30825
+ return execFileSync12("tmux", ["capture-pane", "-p", "-t", sessionName, "-S", "-200"], {
29767
30826
  encoding: "utf-8",
29768
30827
  stdio: ["pipe", "pipe", "pipe"]
29769
30828
  }).trim();
@@ -29805,12 +30864,12 @@ import {
29805
30864
  mkdirSync as mkdirSync16,
29806
30865
  readFileSync as readFileSync21,
29807
30866
  readdirSync as readdirSync10,
29808
- renameSync as renameSync6,
29809
- rmSync as rmSync4,
30867
+ renameSync as renameSync7,
30868
+ rmSync as rmSync5,
29810
30869
  statSync as statSync12,
29811
30870
  writeFileSync as writeFileSync9
29812
30871
  } from "node:fs";
29813
- import { randomBytes as randomBytes2 } from "node:crypto";
30872
+ import { randomBytes as randomBytes3 } from "node:crypto";
29814
30873
  import { join as join17 } from "node:path";
29815
30874
  function claudeDir(agentDir) {
29816
30875
  return join17(agentDir, ".claude");
@@ -29944,13 +31003,13 @@ function syncLegacyFromActive(agentDir) {
29944
31003
  }
29945
31004
  function atomicCopy(src, dest, mode) {
29946
31005
  const contents = readFileSync21(src);
29947
- const tmp = `${dest}.tmp-${process.pid}-${randomBytes2(4).toString("hex")}`;
31006
+ const tmp = `${dest}.tmp-${process.pid}-${randomBytes3(4).toString("hex")}`;
29948
31007
  try {
29949
31008
  writeFileSync9(tmp, contents, { mode });
29950
- renameSync6(tmp, dest);
31009
+ renameSync7(tmp, dest);
29951
31010
  } catch (err) {
29952
31011
  try {
29953
- rmSync4(tmp);
31012
+ rmSync5(tmp);
29954
31013
  } catch {}
29955
31014
  throw err;
29956
31015
  }
@@ -30044,7 +31103,7 @@ var init_accounts = __esm(() => {
30044
31103
  });
30045
31104
 
30046
31105
  // src/auth/manager.ts
30047
- import { execFileSync as execFileSync11 } from "node:child_process";
31106
+ import { execFileSync as execFileSync13 } from "node:child_process";
30048
31107
  import {
30049
31108
  readFileSync as readFileSync22,
30050
31109
  readdirSync as readdirSync11,
@@ -30052,7 +31111,7 @@ import {
30052
31111
  writeFileSync as writeFileSync10,
30053
31112
  mkdirSync as mkdirSync17,
30054
31113
  mkdtempSync as mkdtempSync2,
30055
- rmSync as rmSync5,
31114
+ rmSync as rmSync6,
30056
31115
  chmodSync as chmodSync3,
30057
31116
  statSync as statSync13
30058
31117
  } from "node:fs";
@@ -30095,14 +31154,14 @@ function authSessionName(name, slot) {
30095
31154
  return `${base}-${slot.replace(/[^a-zA-Z0-9_.-]/g, "-")}`;
30096
31155
  }
30097
31156
  function tmux(args) {
30098
- return execFileSync11("tmux", args, {
31157
+ return execFileSync13("tmux", args, {
30099
31158
  encoding: "utf-8",
30100
31159
  stdio: ["pipe", "pipe", "pipe"]
30101
31160
  }).trim();
30102
31161
  }
30103
31162
  function tmuxSessionExists(sessionName) {
30104
31163
  try {
30105
- execFileSync11("tmux", ["has-session", "-t", sessionName], {
31164
+ execFileSync13("tmux", ["has-session", "-t", sessionName], {
30106
31165
  stdio: ["pipe", "pipe", "pipe"]
30107
31166
  });
30108
31167
  return true;
@@ -30180,7 +31239,7 @@ function writeAuthSessionMeta(agentDir, meta) {
30180
31239
  });
30181
31240
  }
30182
31241
  function clearAuthSessionMeta(agentDir) {
30183
- rmSync5(authSessionMetaPath(agentDir), { force: true });
31242
+ rmSync6(authSessionMetaPath(agentDir), { force: true });
30184
31243
  }
30185
31244
  function isSessionStale(agentDir, sessionName) {
30186
31245
  const meta = readJsonFile(authSessionMetaPath(agentDir));
@@ -30206,7 +31265,7 @@ function cleanupAuthTempDirs(agentDir) {
30206
31265
  try {
30207
31266
  for (const entry of readdirSync11(dir)) {
30208
31267
  if (entry.startsWith(".setup-token-tmp-")) {
30209
- rmSync5(join18(dir, entry), { recursive: true, force: true });
31268
+ rmSync6(join18(dir, entry), { recursive: true, force: true });
30210
31269
  }
30211
31270
  }
30212
31271
  } catch {}
@@ -30364,7 +31423,7 @@ function startAuthSession(name, agentDir, opts = {}) {
30364
31423
  mkdirSync17(claudeDir2(agentDir), { recursive: true });
30365
31424
  cleanupAuthTempDirs(agentDir);
30366
31425
  const logPath = authLogPath(agentDir);
30367
- rmSync5(logPath, { force: true });
31426
+ rmSync6(logPath, { force: true });
30368
31427
  let configDir;
30369
31428
  if (opts.force) {
30370
31429
  configDir = mkdtempSync2(join18(claudeDir2(agentDir), ".setup-token-tmp-"));
@@ -30384,589 +31443,240 @@ function startAuthSession(name, agentDir, opts = {}) {
30384
31443
  }
30385
31444
  commandParts.push(`export CLAUDE_CONFIG_DIR BROWSER`);
30386
31445
  commandParts.push(`claude setup-token | tee -- "$LOG_PATH"`);
30387
- const command = commandParts.join(" && ");
30388
- tmux(["new-session", "-d", "-s", sessionName, "-c", agentDir, `bash -lc ${shellQuote(command)}`]);
30389
- sleepMs(8000);
30390
- const output = captureTmuxPane(sessionName);
30391
- const loginUrl = parseSetupTokenUrl(output) ?? undefined;
30392
- const initialCodeChallenge = loginUrl ? extractCodeChallenge(loginUrl) ?? undefined : undefined;
30393
- writeAuthSessionMeta(agentDir, {
30394
- sessionName,
30395
- logPath,
30396
- startedAt: Date.now(),
30397
- slot: slotArg,
30398
- initialCodeChallenge,
30399
- configDir,
30400
- credentialsMtimeAtStart
30401
- });
30402
- return {
30403
- sessionName,
30404
- loginUrl,
30405
- instructions: [
30406
- `Started Claude auth for agent "${name}" in tmux session ${sessionName}.`,
30407
- ...loginUrl ? [`Open this URL in your browser:`, loginUrl, ""] : [
30408
- `Use 'tmux attach -t ${sessionName}' if you need to inspect the auth prompt.`,
30409
- ""
30410
- ],
30411
- `After Claude shows you a browser code, finish with:`,
30412
- ` switchroom auth code ${name} <browser-code>`,
30413
- `Cancel with: switchroom auth cancel ${name}`
30414
- ]
30415
- };
30416
- }
30417
- function readTokenFromLogFile(logPath) {
30418
- if (!existsSync25(logPath))
30419
- return null;
30420
- try {
30421
- const content = readFileSync22(logPath, "utf-8");
30422
- return parseSetupTokenValue(content);
30423
- } catch {
30424
- return null;
30425
- }
30426
- }
30427
- function submitAuthCode(name, agentDir, code, slot, _opts = {}) {
30428
- const pollIntervalMs = _opts.pollIntervalMs ?? 500;
30429
- const envTimeout = process.env.SWITCHROOM_AUTH_CODE_TIMEOUT_MS ? parseInt(process.env.SWITCHROOM_AUTH_CODE_TIMEOUT_MS, 10) : NaN;
30430
- const pollTimeoutMs = _opts.pollTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : 120000);
30431
- let targetSlot = slot;
30432
- if (!targetSlot) {
30433
- const pending = readJsonFile(authSessionMetaPath(agentDir));
30434
- if (pending?.slot)
30435
- targetSlot = pending.slot;
30436
- }
30437
- const sessionName = authSessionName(name, targetSlot);
30438
- if (!tmuxSessionExists(sessionName)) {
30439
- return {
30440
- completed: false,
30441
- tokenSaved: false,
30442
- instructions: [
30443
- `No pending auth session for agent "${name}".`,
30444
- `Start one with: switchroom auth add ${name}`
30445
- ]
30446
- };
30447
- }
30448
- const paneReadyResult = probeForCodePrompt(sessionName);
30449
- if (!paneReadyResult.ready) {
30450
- clearAuthSessionMeta(agentDir);
30451
- const outcome2 = { kind: "pane-not-ready" };
30452
- return {
30453
- completed: false,
30454
- tokenSaved: false,
30455
- outcome: outcome2,
30456
- instructions: [
30457
- `Auth pane for agent "${name}" is not ready (prompt not visible yet).`,
30458
- paneReadyResult.reason === "session-gone" ? `The tmux session disappeared \u2014 start a new auth with: switchroom auth reauth ${name}` : `Retry in a moment with: switchroom auth code ${name} <browser-code>`
30459
- ]
30460
- };
30461
- }
30462
- tmux(["send-keys", "-l", "-t", sessionName, code.trim()]);
30463
- tmux(["send-keys", "-t", sessionName, "Enter"]);
30464
- const meta = readJsonFile(authSessionMetaPath(agentDir));
30465
- const credFileToWatch = meta?.configDir ? join18(meta.configDir, ".credentials.json") : credentialsPath(agentDir);
30466
- const credsMtimeSnapshot = meta?.credentialsMtimeAtStart ?? 0;
30467
- const logPath = authLogPath(agentDir);
30468
- let token = null;
30469
- const deadline = Date.now() + pollTimeoutMs;
30470
- while (Date.now() < deadline) {
30471
- sleepMs(pollIntervalMs);
30472
- const credsMtime = fileMtimeMs(credFileToWatch);
30473
- if (credsMtime > credsMtimeSnapshot) {
30474
- token = readTokenFromCredentialsFile(credFileToWatch);
30475
- if (token)
30476
- break;
30477
- }
30478
- token = readTokenFromLogFile(logPath);
30479
- if (token)
30480
- break;
30481
- }
30482
- if (!token) {
30483
- const paneOutput = captureTmuxPane(sessionName);
30484
- token = parseSetupTokenValue(paneOutput);
30485
- }
30486
- if (!token) {
30487
- let paneTailText;
30488
- try {
30489
- const paneText = captureTmuxPane(sessionName);
30490
- const lastNonEmpty = paneText.split(`
30491
- `).map((l) => l.trim()).filter(Boolean).at(-1);
30492
- if (lastNonEmpty)
30493
- paneTailText = lastNonEmpty;
30494
- } catch {}
30495
- const tailLower = paneTailText?.toLowerCase() ?? "";
30496
- let outcomeKind = "timeout";
30497
- if (/invalid.*code|code.*invalid/i.test(tailLower)) {
30498
- outcomeKind = "invalid-code";
30499
- } else if (/expired.*code|code.*expired/i.test(tailLower)) {
30500
- outcomeKind = "expired-code";
30501
- }
30502
- clearAuthSessionMeta(agentDir);
30503
- const outcome2 = { kind: outcomeKind, paneTailText };
30504
- return {
30505
- completed: false,
30506
- tokenSaved: false,
30507
- outcome: outcome2,
30508
- instructions: [
30509
- `Submitted code to Claude for agent "${name}", but no token was found after ${Math.round(pollTimeoutMs / 1000)}s.`,
30510
- ...paneTailText ? [`Pane output: ${paneTailText}`] : [],
30511
- `If the code was invalid, Claude will say so in the auth session.`,
30512
- `Inspect with: tmux attach -t ${sessionName}`,
30513
- `Retry with: switchroom auth code ${name} <browser-code>`
30514
- ]
30515
- };
30516
- }
30517
- const tokenPath = writeOAuthToken(agentDir, token, targetSlot);
30518
- try {
30519
- tmux(["kill-session", "-t", sessionName]);
30520
- } catch {}
30521
- clearAuthSessionMeta(agentDir);
30522
- cleanupAuthTempDirs(agentDir);
30523
- rmSync5(authLogPath(agentDir), { force: true });
30524
- rmSync5(credentialsPath(agentDir), { force: true });
30525
- const outcome = { kind: "success" };
30526
- return {
30527
- completed: true,
30528
- tokenSaved: true,
30529
- tokenPath,
30530
- outcome,
30531
- instructions: [
30532
- `Saved Claude OAuth token for agent "${name}".`,
30533
- ` Token file: ${tokenPath}`,
30534
- `Restart the agent to pick up the new account, or let switchroom do it now.`
30535
- ]
30536
- };
30537
- }
30538
- function cancelAuthSession(name, agentDir, slot) {
30539
- let targetSlot = slot;
30540
- if (!targetSlot) {
30541
- const pending = readJsonFile(authSessionMetaPath(agentDir));
30542
- if (pending?.slot)
30543
- targetSlot = pending.slot;
30544
- }
30545
- const sessionName = authSessionName(name, targetSlot);
30546
- if (tmuxSessionExists(sessionName)) {
30547
- tmux(["kill-session", "-t", sessionName]);
30548
- }
30549
- clearAuthSessionMeta(agentDir);
30550
- cleanupAuthTempDirs(agentDir);
30551
- return {
30552
- instructions: [`Cancelled pending auth session for agent "${name}".`]
30553
- };
30554
- }
30555
- var TOKEN_VALIDITY_MS;
30556
- var init_manager = __esm(() => {
30557
- init_loader();
30558
- init_pane_ready_probe();
30559
- init_accounts();
30560
- TOKEN_VALIDITY_MS = 365 * 24 * 60 * 60000;
30561
- });
30562
-
30563
- // src/agents/quarantine.ts
30564
- import {
30565
- existsSync as existsSync30,
30566
- mkdirSync as mkdirSync18,
30567
- readFileSync as readFileSync24,
30568
- unlinkSync as unlinkSync6,
30569
- writeFileSync as writeFileSync12
30570
- } from "node:fs";
30571
- import { join as join20 } from "node:path";
30572
- function quarantineMarkerPath(telegramStateDir) {
30573
- return join20(telegramStateDir, QUARANTINE_FILENAME);
30574
- }
30575
- function readQuarantineMarker(telegramStateDir) {
30576
- const path = quarantineMarkerPath(telegramStateDir);
30577
- if (!existsSync30(path))
30578
- return null;
30579
- try {
30580
- const raw = readFileSync24(path, "utf-8");
30581
- const parsed = JSON.parse(raw);
30582
- if (!parsed || typeof parsed !== "object")
30583
- return null;
30584
- const m = parsed;
30585
- if (m.v !== 1)
30586
- return null;
30587
- if (typeof m.reason !== "string")
30588
- return null;
30589
- if (typeof m.ts !== "number")
30590
- return null;
30591
- return {
30592
- v: 1,
30593
- reason: m.reason,
30594
- ts: m.ts,
30595
- detail: typeof m.detail === "string" ? m.detail : undefined
30596
- };
30597
- } catch {
30598
- return null;
30599
- }
30600
- }
30601
- function clearQuarantineMarker(telegramStateDir) {
30602
- const path = quarantineMarkerPath(telegramStateDir);
30603
- if (!existsSync30(path))
30604
- return false;
30605
- try {
30606
- unlinkSync6(path);
30607
- return true;
30608
- } catch {
30609
- return false;
30610
- }
30611
- }
30612
- function hostTelegramStateDir(agentsDir, name) {
30613
- return join20(agentsDir, name, "telegram");
30614
- }
30615
- function readQuarantineMarkerForAgent(agentsDir, name) {
30616
- return readQuarantineMarker(hostTelegramStateDir(agentsDir, name));
30617
- }
30618
- var QUARANTINE_FILENAME = "quarantine.json";
30619
- var init_quarantine = () => {};
30620
-
30621
- // src/agents/pane-lock.ts
30622
- function withPaneLock(key, fn) {
30623
- const tail = tails.get(key) ?? Promise.resolve();
30624
- const run = tail.then(() => fn());
30625
- const next = run.then(() => {}, () => {});
30626
- tails.set(key, next);
30627
- next.then(() => {
30628
- if (tails.get(key) === next)
30629
- tails.delete(key);
30630
- });
30631
- return run;
30632
- }
30633
- var tails;
30634
- var init_pane_lock = __esm(() => {
30635
- tails = new Map;
30636
- });
30637
-
30638
- // src/agents/inject.ts
30639
- var exports_inject = {};
30640
- __export(exports_inject, {
30641
- validateInjectCommand: () => validateInjectCommand,
30642
- normalizeInjectCommand: () => normalizeInjectCommand,
30643
- makeTmuxRunner: () => makeTmuxRunner,
30644
- isTuiChromeLine: () => isTuiChromeLine,
30645
- injectSlashCommandWith: () => injectSlashCommandWith,
30646
- injectSlashCommand: () => injectSlashCommand,
30647
- diffPane: () => diffPane,
30648
- dedupeInjectQueue: () => dedupeInjectQueue,
30649
- InjectError: () => InjectError,
30650
- INJECT_COMMANDS: () => INJECT_COMMANDS,
30651
- INJECT_BLOCKLIST: () => INJECT_BLOCKLIST,
30652
- INJECT_BLOCKED: () => INJECT_BLOCKED,
30653
- INJECT_ALLOWLIST: () => INJECT_ALLOWLIST
30654
- });
30655
- import { execFileSync as execFileSync13 } from "node:child_process";
30656
- function validateInjectCommand(command) {
30657
- if (typeof command !== "string" || command.trim().length === 0) {
30658
- throw new InjectError("invalid", "command is empty");
30659
- }
30660
- const trimmed = command.trim();
30661
- if (!trimmed.startsWith("/")) {
30662
- throw new InjectError("invalid", `command must start with "/": got ${trimmed}`);
30663
- }
30664
- const bare = trimmed.split(/\s+/, 1)[0].toLowerCase();
30665
- const blockedMeta = INJECT_BLOCKED.get(bare);
30666
- if (blockedMeta) {
30667
- throw new InjectError("blocked", `${bare} is explicitly blocked from inject (${blockedMeta.reason}).`);
30668
- }
30669
- if (!INJECT_COMMANDS.has(bare)) {
30670
- const allowed = [...INJECT_COMMANDS.keys()].sort().join(", ");
30671
- throw new InjectError("not_allowed", `${bare} is not in the inject allowlist. Allowed: ${allowed}`);
30672
- }
30673
- return bare;
30674
- }
30675
- function normalizeInjectCommand(command) {
30676
- return command.trim().replace(/\s+/g, " ").toLowerCase();
30677
- }
30678
- function dedupeInjectQueue(commands) {
30679
- const seen = new Set;
30680
- const out = [];
30681
- for (const raw of commands) {
30682
- if (typeof raw !== "string")
30683
- continue;
30684
- const key = normalizeInjectCommand(raw);
30685
- if (key.length === 0)
30686
- continue;
30687
- if (seen.has(key))
30688
- continue;
30689
- seen.add(key);
30690
- out.push(raw.trim());
30691
- }
30692
- return out;
30693
- }
30694
- function defaultSocketName(agentName) {
30695
- return `switchroom-${agentName}`;
30696
- }
30697
- function makeTmuxRunner(tmuxBin) {
30698
- return {
30699
- capture(socket, session) {
30700
- try {
30701
- return execFileSync13(tmuxBin, ["-L", socket, "capture-pane", "-p", "-t", session, "-S", "-200"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
30702
- } catch {
30703
- return null;
30704
- }
30705
- },
30706
- send(socket, session, args) {
30707
- const [subcmd, ...rest] = args;
30708
- const flagEnd = rest.findIndex((a) => !a.startsWith("-"));
30709
- const flagsBeforeKeys = flagEnd === -1 ? rest : rest.slice(0, flagEnd);
30710
- const keys = flagEnd === -1 ? [] : rest.slice(flagEnd);
30711
- execFileSync13(tmuxBin, ["-L", socket, subcmd, ...flagsBeforeKeys, "-t", session, ...keys], { stdio: ["pipe", "pipe", "pipe"] });
30712
- },
30713
- hasSession(socket, session) {
30714
- try {
30715
- execFileSync13(tmuxBin, ["-L", socket, "has-session", "-t", session], {
30716
- stdio: ["pipe", "pipe", "pipe"]
30717
- });
30718
- return true;
30719
- } catch {
30720
- return false;
30721
- }
30722
- }
30723
- };
30724
- }
30725
- function sleep2(ms) {
30726
- return new Promise((res) => setTimeout(res, ms));
30727
- }
30728
- function isTuiChromeLine(line) {
30729
- const t = line.trim();
30730
- if (t.length === 0)
30731
- return false;
30732
- if (/^[\s\-_\u2502\u2500\u256d\u256e\u2570\u256f\u250c\u2510\u2514\u2518|]+$/.test(t))
30733
- return true;
30734
- if (/^[\u256d\u256e\u2570\u256f\u2502\s]*[>)\u276f][\u256d\u256e\u2570\u256f\u2502\s]*$/.test(t))
30735
- return true;
30736
- if (/accept edits on/i.test(t))
30737
- return true;
30738
- if (/shift\+tab to cycle/i.test(t))
30739
- return true;
30740
- if (/for agents/i.test(t))
30741
- return true;
30742
- if (/\? for shortcuts/i.test(t))
30743
- return true;
30744
- if (/bypassing permissions/i.test(t))
30745
- return true;
30746
- if (t === "copy")
30747
- return true;
30748
- return false;
30749
- }
30750
- function diffPane(before, after, command) {
30751
- if (command) {
30752
- const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30753
- const lines = after.split(`
30754
- `);
30755
- let anchorIdx = -1;
30756
- for (let i = lines.length - 1;i >= 0; i--) {
30757
- if (/[\u276f>]\s+/.test(lines[i]) && lines[i].includes(escaped)) {
30758
- anchorIdx = i;
30759
- break;
30760
- }
30761
- }
30762
- if (anchorIdx >= 0) {
30763
- const tail = lines.slice(anchorIdx + 1);
30764
- const trimmed = [];
30765
- for (const raw of tail) {
30766
- const line = raw.trimEnd();
30767
- if (line.length === 0 && trimmed.length === 0)
30768
- continue;
30769
- if (isTuiChromeLine(line))
30770
- continue;
30771
- trimmed.push(line);
30772
- }
30773
- while (trimmed.length > 0 && trimmed[trimmed.length - 1].length === 0) {
30774
- trimmed.pop();
30775
- }
30776
- const affordances = /^(esc to cancel|press any key|\u21b5 select)/i;
30777
- while (trimmed.length > 0 && affordances.test(trimmed[trimmed.length - 1].trim())) {
30778
- trimmed.pop();
30779
- }
30780
- if (trimmed.length > 0) {
30781
- return { output: trimmed.join(`
30782
- `), anchored: true };
30783
- }
30784
- }
30785
- }
30786
- const beforeSet = new Set(before.split(`
30787
- `).map((l) => l.trimEnd()));
30788
- const newLines = [];
30789
- for (const raw of after.split(`
30790
- `)) {
30791
- const line = raw.trimEnd();
30792
- if (line.length === 0)
30793
- continue;
30794
- if (beforeSet.has(line))
30795
- continue;
30796
- if (isTuiChromeLine(line))
30797
- continue;
30798
- newLines.push(line);
30799
- }
30800
- return { output: newLines.join(`
30801
- `), anchored: false };
30802
- }
30803
- async function injectSlashCommand(agentName, command, opts = {}) {
30804
- validateInjectCommand(command);
30805
- const tmuxBin = opts.tmuxBin ?? "tmux";
30806
- const socket = opts.socketName ?? defaultSocketName(agentName);
30807
- const session = opts.sessionName ?? agentName;
30808
- const settleMs = opts.settleMs ?? 2000;
30809
- const timeoutMs = opts.timeoutMs ?? 5000;
30810
- return withPaneLock(`${socket}:${session}`, () => injectSlashCommandWith(makeTmuxRunner(tmuxBin), {
30811
- socket,
30812
- session,
30813
- command: command.trim(),
30814
- settleMs,
30815
- timeoutMs
30816
- }));
31446
+ const command = commandParts.join(" && ");
31447
+ tmux(["new-session", "-d", "-s", sessionName, "-c", agentDir, `bash -lc ${shellQuote(command)}`]);
31448
+ sleepMs(8000);
31449
+ const output = captureTmuxPane(sessionName);
31450
+ const loginUrl = parseSetupTokenUrl(output) ?? undefined;
31451
+ const initialCodeChallenge = loginUrl ? extractCodeChallenge(loginUrl) ?? undefined : undefined;
31452
+ writeAuthSessionMeta(agentDir, {
31453
+ sessionName,
31454
+ logPath,
31455
+ startedAt: Date.now(),
31456
+ slot: slotArg,
31457
+ initialCodeChallenge,
31458
+ configDir,
31459
+ credentialsMtimeAtStart
31460
+ });
31461
+ return {
31462
+ sessionName,
31463
+ loginUrl,
31464
+ instructions: [
31465
+ `Started Claude auth for agent "${name}" in tmux session ${sessionName}.`,
31466
+ ...loginUrl ? [`Open this URL in your browser:`, loginUrl, ""] : [
31467
+ `Use 'tmux attach -t ${sessionName}' if you need to inspect the auth prompt.`,
31468
+ ""
31469
+ ],
31470
+ `After Claude shows you a browser code, finish with:`,
31471
+ ` switchroom auth code ${name} <browser-code>`,
31472
+ `Cancel with: switchroom auth cancel ${name}`
31473
+ ]
31474
+ };
30817
31475
  }
30818
- async function injectSlashCommandWith(runner, args) {
30819
- const { socket, session, command, settleMs, timeoutMs } = args;
30820
- let bareVerb;
31476
+ function readTokenFromLogFile(logPath) {
31477
+ if (!existsSync25(logPath))
31478
+ return null;
30821
31479
  try {
30822
- bareVerb = validateInjectCommand(command);
30823
- } catch (err) {
30824
- if (err instanceof InjectError) {
30825
- return {
30826
- outcome: "failed",
30827
- output: "",
30828
- truncated: false,
30829
- command: command.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? "",
30830
- meta: null,
30831
- errorCode: err.code,
30832
- errorMessage: err.message
30833
- };
30834
- }
30835
- throw err;
31480
+ const content = readFileSync22(logPath, "utf-8");
31481
+ return parseSetupTokenValue(content);
31482
+ } catch {
31483
+ return null;
30836
31484
  }
30837
- const meta = INJECT_COMMANDS.get(bareVerb) ?? null;
30838
- if (!runner.hasSession(socket, session)) {
31485
+ }
31486
+ function submitAuthCode(name, agentDir, code, slot, _opts = {}) {
31487
+ const pollIntervalMs = _opts.pollIntervalMs ?? 500;
31488
+ const envTimeout = process.env.SWITCHROOM_AUTH_CODE_TIMEOUT_MS ? parseInt(process.env.SWITCHROOM_AUTH_CODE_TIMEOUT_MS, 10) : NaN;
31489
+ const pollTimeoutMs = _opts.pollTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : 120000);
31490
+ let targetSlot = slot;
31491
+ if (!targetSlot) {
31492
+ const pending = readJsonFile(authSessionMetaPath(agentDir));
31493
+ if (pending?.slot)
31494
+ targetSlot = pending.slot;
31495
+ }
31496
+ const sessionName = authSessionName(name, targetSlot);
31497
+ if (!tmuxSessionExists(sessionName)) {
30839
31498
  return {
30840
- outcome: "failed",
30841
- output: "",
30842
- truncated: false,
30843
- command: bareVerb,
30844
- meta,
30845
- errorCode: "session_missing",
30846
- errorMessage: `tmux session "${session}" on socket "${socket}" not found. ` + `Is the agent running under the tmux supervisor (the default)? ` + `If experimental.legacy_pty=true is set, inject is unsupported.`
31499
+ completed: false,
31500
+ tokenSaved: false,
31501
+ instructions: [
31502
+ `No pending auth session for agent "${name}".`,
31503
+ `Start one with: switchroom auth add ${name}`
31504
+ ]
30847
31505
  };
30848
31506
  }
30849
- const before = runner.capture(socket, session) ?? "";
30850
- try {
30851
- runner.send(socket, session, ["send-keys", "-l", command]);
30852
- runner.send(socket, session, ["send-keys", "Enter"]);
30853
- } catch (err) {
31507
+ const paneReadyResult = probeForCodePrompt(sessionName);
31508
+ if (!paneReadyResult.ready) {
31509
+ clearAuthSessionMeta(agentDir);
31510
+ const outcome2 = { kind: "pane-not-ready" };
30854
31511
  return {
30855
- outcome: "failed",
30856
- output: "",
30857
- truncated: false,
30858
- command: bareVerb,
30859
- meta,
30860
- errorCode: "tmux_failed",
30861
- errorMessage: `tmux send-keys failed: ${err instanceof Error ? err.message : String(err)}`
31512
+ completed: false,
31513
+ tokenSaved: false,
31514
+ outcome: outcome2,
31515
+ instructions: [
31516
+ `Auth pane for agent "${name}" is not ready (prompt not visible yet).`,
31517
+ paneReadyResult.reason === "session-gone" ? `The tmux session disappeared \u2014 start a new auth with: switchroom auth reauth ${name}` : `Retry in a moment with: switchroom auth code ${name} <browser-code>`
31518
+ ]
30862
31519
  };
30863
31520
  }
30864
- const start = Date.now();
30865
- let last = before;
30866
- let stableSince = null;
30867
- while (Date.now() - start < timeoutMs) {
30868
- await sleep2(POLL_INTERVAL_MS);
30869
- const cur = runner.capture(socket, session) ?? "";
30870
- if (cur === last && cur !== before) {
30871
- if (stableSince === null) {
30872
- stableSince = Date.now();
30873
- } else if (Date.now() - stableSince >= POLL_INTERVAL_MS) {
30874
- last = cur;
31521
+ tmux(["send-keys", "-l", "-t", sessionName, code.trim()]);
31522
+ tmux(["send-keys", "-t", sessionName, "Enter"]);
31523
+ const meta = readJsonFile(authSessionMetaPath(agentDir));
31524
+ const credFileToWatch = meta?.configDir ? join18(meta.configDir, ".credentials.json") : credentialsPath(agentDir);
31525
+ const credsMtimeSnapshot = meta?.credentialsMtimeAtStart ?? 0;
31526
+ const logPath = authLogPath(agentDir);
31527
+ let token = null;
31528
+ const deadline = Date.now() + pollTimeoutMs;
31529
+ while (Date.now() < deadline) {
31530
+ sleepMs(pollIntervalMs);
31531
+ const credsMtime = fileMtimeMs(credFileToWatch);
31532
+ if (credsMtime > credsMtimeSnapshot) {
31533
+ token = readTokenFromCredentialsFile(credFileToWatch);
31534
+ if (token)
30875
31535
  break;
30876
- }
30877
- } else {
30878
- stableSince = null;
30879
31536
  }
30880
- last = cur;
30881
- if (Date.now() - start >= settleMs && cur !== before) {
31537
+ token = readTokenFromLogFile(logPath);
31538
+ if (token)
30882
31539
  break;
30883
- }
30884
31540
  }
30885
- const { output: rawOutput, anchored } = diffPane(before, last, command);
30886
- let output = rawOutput;
30887
- let truncated = false;
30888
- const bytes = Buffer.byteLength(output, "utf-8");
30889
- if (bytes > OUTPUT_BYTE_CAP) {
30890
- while (Buffer.byteLength(output, "utf-8") > OUTPUT_BYTE_CAP) {
30891
- output = output.slice(Math.floor(output.length * 0.1) + 1);
30892
- }
30893
- truncated = true;
31541
+ if (!token) {
31542
+ const paneOutput = captureTmuxPane(sessionName);
31543
+ token = parseSetupTokenValue(paneOutput);
30894
31544
  }
30895
- if (meta?.dialog) {
31545
+ if (!token) {
31546
+ let paneTailText;
30896
31547
  try {
30897
- runner.send(socket, session, ["send-keys", "Escape"]);
31548
+ const paneText = captureTmuxPane(sessionName);
31549
+ const lastNonEmpty = paneText.split(`
31550
+ `).map((l) => l.trim()).filter(Boolean).at(-1);
31551
+ if (lastNonEmpty)
31552
+ paneTailText = lastNonEmpty;
30898
31553
  } catch {}
30899
- }
30900
- if (output.trim().length === 0) {
31554
+ const tailLower = paneTailText?.toLowerCase() ?? "";
31555
+ let outcomeKind = "timeout";
31556
+ if (/invalid.*code|code.*invalid/i.test(tailLower)) {
31557
+ outcomeKind = "invalid-code";
31558
+ } else if (/expired.*code|code.*expired/i.test(tailLower)) {
31559
+ outcomeKind = "expired-code";
31560
+ }
31561
+ clearAuthSessionMeta(agentDir);
31562
+ const outcome2 = { kind: outcomeKind, paneTailText };
30901
31563
  return {
30902
- outcome: "ok_no_output",
30903
- output: "",
30904
- truncated: false,
30905
- command: bareVerb,
30906
- meta,
30907
- ...anchored ? {} : { diagnostic: "anchor_missing" }
31564
+ completed: false,
31565
+ tokenSaved: false,
31566
+ outcome: outcome2,
31567
+ instructions: [
31568
+ `Submitted code to Claude for agent "${name}", but no token was found after ${Math.round(pollTimeoutMs / 1000)}s.`,
31569
+ ...paneTailText ? [`Pane output: ${paneTailText}`] : [],
31570
+ `If the code was invalid, Claude will say so in the auth session.`,
31571
+ `Inspect with: tmux attach -t ${sessionName}`,
31572
+ `Retry with: switchroom auth code ${name} <browser-code>`
31573
+ ]
30908
31574
  };
30909
31575
  }
31576
+ const tokenPath = writeOAuthToken(agentDir, token, targetSlot);
31577
+ try {
31578
+ tmux(["kill-session", "-t", sessionName]);
31579
+ } catch {}
31580
+ clearAuthSessionMeta(agentDir);
31581
+ cleanupAuthTempDirs(agentDir);
31582
+ rmSync6(authLogPath(agentDir), { force: true });
31583
+ rmSync6(credentialsPath(agentDir), { force: true });
31584
+ const outcome = { kind: "success" };
30910
31585
  return {
30911
- outcome: "ok",
30912
- output,
30913
- truncated,
30914
- command: bareVerb,
30915
- meta,
30916
- ...truncated ? { diagnostic: "truncated_output" } : {}
31586
+ completed: true,
31587
+ tokenSaved: true,
31588
+ tokenPath,
31589
+ outcome,
31590
+ instructions: [
31591
+ `Saved Claude OAuth token for agent "${name}".`,
31592
+ ` Token file: ${tokenPath}`,
31593
+ `Restart the agent to pick up the new account, or let switchroom do it now.`
31594
+ ]
30917
31595
  };
30918
31596
  }
30919
- var INJECT_COMMANDS, INJECT_ALLOWLIST, INJECT_BLOCKED, INJECT_BLOCKLIST, InjectError, POLL_INTERVAL_MS = 150, OUTPUT_BYTE_CAP = 3000;
30920
- var init_inject = __esm(() => {
30921
- init_pane_lock();
30922
- INJECT_COMMANDS = new Map([
30923
- ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true }],
30924
- ["/status", { description: "Show session status", expectsOutput: true, dialog: true }],
30925
- ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true }],
30926
- ["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true }],
30927
- ["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true }],
30928
- ["/model", { description: "Open model picker", expectsOutput: true }],
30929
- [
30930
- "/clear",
30931
- {
30932
- description: "Clear session screen",
30933
- expectsOutput: false,
30934
- silentNote: "context cleared \u2014 fresh slate"
30935
- }
30936
- ],
30937
- [
30938
- "/compact",
30939
- {
30940
- description: "Compact conversation history",
30941
- expectsOutput: false,
30942
- silentNote: "compaction runs silently"
30943
- }
30944
- ]
30945
- ]);
30946
- INJECT_ALLOWLIST = new Set(INJECT_COMMANDS.keys());
30947
- INJECT_BLOCKED = new Map([
30948
- ["/login", { reason: "would mutate auth state" }],
30949
- ["/logout", { reason: "would terminate the agent's auth session" }],
30950
- ["/exit", { reason: "would kill the agent process" }],
30951
- ["/quit", { reason: "would kill the agent process" }],
30952
- [
30953
- "/effort",
30954
- {
30955
- reason: "leaves a blocking confirmation modal open and wedges the pane; use the /effort command (it drives the modal), not raw inject"
30956
- }
30957
- ]
30958
- ]);
30959
- INJECT_BLOCKLIST = new Set(INJECT_BLOCKED.keys());
30960
- InjectError = class InjectError extends Error {
30961
- code;
30962
- constructor(code, message) {
30963
- super(message);
30964
- this.name = "InjectError";
30965
- this.code = code;
30966
- }
31597
+ function cancelAuthSession(name, agentDir, slot) {
31598
+ let targetSlot = slot;
31599
+ if (!targetSlot) {
31600
+ const pending = readJsonFile(authSessionMetaPath(agentDir));
31601
+ if (pending?.slot)
31602
+ targetSlot = pending.slot;
31603
+ }
31604
+ const sessionName = authSessionName(name, targetSlot);
31605
+ if (tmuxSessionExists(sessionName)) {
31606
+ tmux(["kill-session", "-t", sessionName]);
31607
+ }
31608
+ clearAuthSessionMeta(agentDir);
31609
+ cleanupAuthTempDirs(agentDir);
31610
+ return {
31611
+ instructions: [`Cancelled pending auth session for agent "${name}".`]
30967
31612
  };
31613
+ }
31614
+ var TOKEN_VALIDITY_MS;
31615
+ var init_manager = __esm(() => {
31616
+ init_loader();
31617
+ init_pane_ready_probe();
31618
+ init_accounts();
31619
+ TOKEN_VALIDITY_MS = 365 * 24 * 60 * 60000;
30968
31620
  });
30969
31621
 
31622
+ // src/agents/quarantine.ts
31623
+ import {
31624
+ existsSync as existsSync30,
31625
+ mkdirSync as mkdirSync18,
31626
+ readFileSync as readFileSync24,
31627
+ unlinkSync as unlinkSync6,
31628
+ writeFileSync as writeFileSync12
31629
+ } from "node:fs";
31630
+ import { join as join20 } from "node:path";
31631
+ function quarantineMarkerPath(telegramStateDir) {
31632
+ return join20(telegramStateDir, QUARANTINE_FILENAME);
31633
+ }
31634
+ function readQuarantineMarker(telegramStateDir) {
31635
+ const path = quarantineMarkerPath(telegramStateDir);
31636
+ if (!existsSync30(path))
31637
+ return null;
31638
+ try {
31639
+ const raw = readFileSync24(path, "utf-8");
31640
+ const parsed = JSON.parse(raw);
31641
+ if (!parsed || typeof parsed !== "object")
31642
+ return null;
31643
+ const m = parsed;
31644
+ if (m.v !== 1)
31645
+ return null;
31646
+ if (typeof m.reason !== "string")
31647
+ return null;
31648
+ if (typeof m.ts !== "number")
31649
+ return null;
31650
+ return {
31651
+ v: 1,
31652
+ reason: m.reason,
31653
+ ts: m.ts,
31654
+ detail: typeof m.detail === "string" ? m.detail : undefined
31655
+ };
31656
+ } catch {
31657
+ return null;
31658
+ }
31659
+ }
31660
+ function clearQuarantineMarker(telegramStateDir) {
31661
+ const path = quarantineMarkerPath(telegramStateDir);
31662
+ if (!existsSync30(path))
31663
+ return false;
31664
+ try {
31665
+ unlinkSync6(path);
31666
+ return true;
31667
+ } catch {
31668
+ return false;
31669
+ }
31670
+ }
31671
+ function hostTelegramStateDir(agentsDir, name) {
31672
+ return join20(agentsDir, name, "telegram");
31673
+ }
31674
+ function readQuarantineMarkerForAgent(agentsDir, name) {
31675
+ return readQuarantineMarker(hostTelegramStateDir(agentsDir, name));
31676
+ }
31677
+ var QUARANTINE_FILENAME = "quarantine.json";
31678
+ var init_quarantine = () => {};
31679
+
30970
31680
  // src/auth/broker/protocol.ts
30971
31681
  function encodeRequest2(req) {
30972
31682
  const line = JSON.stringify(RequestSchema2.parse(req)) + `
@@ -31607,8 +32317,8 @@ import {
31607
32317
  mkdirSync as mkdirSync19,
31608
32318
  readFileSync as readFileSync26,
31609
32319
  readdirSync as readdirSync15,
31610
- renameSync as renameSync7,
31611
- rmSync as rmSync10,
32320
+ renameSync as renameSync8,
32321
+ rmSync as rmSync11,
31612
32322
  statSync as statSync16,
31613
32323
  writeFileSync as writeFileSync14
31614
32324
  } from "node:fs";
@@ -31737,73 +32447,6 @@ var init_google_workspace_yaml = __esm(() => {
31737
32447
  import_yaml7 = __toESM(require_dist(), 1);
31738
32448
  });
31739
32449
 
31740
- // src/util/atomic.ts
31741
- var exports_atomic = {};
31742
- __export(exports_atomic, {
31743
- writeConfigFileSync: () => writeConfigFileSync,
31744
- atomicWriteJsonSync: () => atomicWriteJsonSync,
31745
- atomicWriteFileSync: () => atomicWriteFileSync2
31746
- });
31747
- import { randomBytes as randomBytes3 } from "node:crypto";
31748
- import { closeSync as closeSync5, constants, fsyncSync as fsyncSync3, openSync as openSync5, renameSync as renameSync8, rmSync as rmSync11, writeSync as writeSync4 } from "node:fs";
31749
- function atomicWriteFileSync2(destPath, contents, mode = 384) {
31750
- const tmp = `${destPath}.tmp-${process.pid}-${randomBytes3(4).toString("hex")}`;
31751
- const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
31752
- let fd = null;
31753
- try {
31754
- fd = openSync5(tmp, TMP_OPEN_FLAGS, mode);
31755
- writeSync4(fd, buf, 0, buf.length, 0);
31756
- fsyncSync3(fd);
31757
- closeSync5(fd);
31758
- fd = null;
31759
- renameSync8(tmp, destPath);
31760
- } catch (err) {
31761
- if (fd !== null) {
31762
- try {
31763
- closeSync5(fd);
31764
- } catch {}
31765
- }
31766
- try {
31767
- rmSync11(tmp, { force: true });
31768
- } catch {}
31769
- throw err;
31770
- }
31771
- }
31772
- function atomicWriteJsonSync(destPath, value, mode = 384) {
31773
- atomicWriteFileSync2(destPath, JSON.stringify(value, null, 2) + `
31774
- `, mode);
31775
- }
31776
- function writeConfigFileSync(destPath, contents, mode = 420) {
31777
- try {
31778
- atomicWriteFileSync2(destPath, contents, mode);
31779
- } catch (err) {
31780
- const code = err.code;
31781
- if (code !== "EBUSY" && code !== "EXDEV" && code !== "EINVAL") {
31782
- throw err;
31783
- }
31784
- const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
31785
- let fd = null;
31786
- try {
31787
- fd = openSync5(destPath, constants.O_WRONLY | constants.O_TRUNC, mode);
31788
- writeSync4(fd, buf, 0, buf.length, 0);
31789
- fsyncSync3(fd);
31790
- closeSync5(fd);
31791
- fd = null;
31792
- } catch (fallbackErr) {
31793
- if (fd !== null) {
31794
- try {
31795
- closeSync5(fd);
31796
- } catch {}
31797
- }
31798
- throw fallbackErr;
31799
- }
31800
- }
31801
- }
31802
- var TMP_OPEN_FLAGS;
31803
- var init_atomic = __esm(() => {
31804
- TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
31805
- });
31806
-
31807
32450
  // src/drive/oauth.ts
31808
32451
  var exports_oauth = {};
31809
32452
  __export(exports_oauth, {
@@ -33536,7 +34179,7 @@ __export(exports_via_claude, {
33536
34179
  PRE_PASTE_RULES: () => PRE_PASTE_RULES,
33537
34180
  POST_PASTE_RULES: () => POST_PASTE_RULES
33538
34181
  });
33539
- import { execFileSync as execFileSync14, spawnSync as spawnSync3 } from "node:child_process";
34182
+ import { execFileSync as execFileSync15, spawnSync as spawnSync3 } from "node:child_process";
33540
34183
  import { existsSync as existsSync34, mkdirSync as mkdirSync20, readFileSync as readFileSync29 } from "node:fs";
33541
34184
  import { join as join26, resolve as resolve24 } from "node:path";
33542
34185
  function tmuxHasSession(session) {
@@ -33547,7 +34190,7 @@ function tmuxHasSession(session) {
33547
34190
  }
33548
34191
  function tmuxKillSession(session) {
33549
34192
  try {
33550
- execFileSync14("tmux", ["kill-session", "-t", session], {
34193
+ execFileSync15("tmux", ["kill-session", "-t", session], {
33551
34194
  stdio: "ignore",
33552
34195
  timeout: 3000
33553
34196
  });
@@ -33555,7 +34198,7 @@ function tmuxKillSession(session) {
33555
34198
  }
33556
34199
  function tmuxCapturePane(session) {
33557
34200
  try {
33558
- const out = execFileSync14("tmux", ["capture-pane", "-p", "-t", session, "-S", "-200"], { timeout: 3000, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 4 * 1024 * 1024 });
34201
+ const out = execFileSync15("tmux", ["capture-pane", "-p", "-t", session, "-S", "-200"], { timeout: 3000, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 4 * 1024 * 1024 });
33559
34202
  return out.toString("utf8");
33560
34203
  } catch {
33561
34204
  return "";
@@ -33563,7 +34206,7 @@ function tmuxCapturePane(session) {
33563
34206
  }
33564
34207
  function tmuxSendKeys(session, keys, literal = false) {
33565
34208
  try {
33566
- execFileSync14("tmux", ["send-keys", "-t", session, ...literal ? ["-l"] : [], ...keys], { timeout: 3000, stdio: ["ignore", "pipe", "pipe"] });
34209
+ execFileSync15("tmux", ["send-keys", "-t", session, ...literal ? ["-l"] : [], ...keys], { timeout: 3000, stdio: ["ignore", "pipe", "pipe"] });
33567
34210
  } catch {}
33568
34211
  }
33569
34212
  function extractAuthorizeUrl(pane) {
@@ -33592,7 +34235,7 @@ async function runViaClaude(opts) {
33592
34235
  tmuxKillSession(SESSION);
33593
34236
  }
33594
34237
  const spawn4 = opts.spawnClaude ?? (() => {
33595
- execFileSync14("tmux", [
34238
+ execFileSync15("tmux", [
33596
34239
  "new-session",
33597
34240
  "-d",
33598
34241
  "-s",
@@ -39970,6 +40613,30 @@ function classifyShmSize(bytes) {
39970
40613
  const gib = (bytes / 1024 / 1024 / 1024).toFixed(bytes % 1024 ** 3 === 0 ? 0 : 1);
39971
40614
  return { name: "hindsight shm-size", status: "ok", detail: `${gib}g` };
39972
40615
  }
40616
+ function classifyConsolidationBacklog(queueDepth, oldestAgeSecs = null) {
40617
+ const ageStr = oldestAgeSecs !== null && oldestAgeSecs > 0 ? `, oldest ${Math.round(oldestAgeSecs / 60)}m old` : "";
40618
+ const base = `${queueDepth} pending consolidation op(s)${ageStr}`;
40619
+ if (queueDepth >= CONSOLIDATION_BACKLOG_FAIL) {
40620
+ return {
40621
+ name: "hindsight consolidation backlog",
40622
+ status: "fail",
40623
+ detail: `${base} \u2014 queue is deep enough to be wedged; retains are landing but ` + `not consolidating into recall (drains ~7 ops/hr under the #2894 throttle)`,
40624
+ fix: "Check `docker logs switchroom-hindsight` for the queue-lag WARNING and " + "consolidation errors (quota/429/shm); restart with `switchroom memory " + "--restart` if the worker is stuck."
40625
+ };
40626
+ }
40627
+ if (queueDepth >= CONSOLIDATION_BACKLOG_WARN) {
40628
+ return {
40629
+ name: "hindsight consolidation backlog",
40630
+ status: "warn",
40631
+ detail: `${base} \u2014 building faster than it drains (~7 ops/hr under the #2894 ` + `throttle); recall may lag recent turns until it catches up`
40632
+ };
40633
+ }
40634
+ return {
40635
+ name: "hindsight consolidation backlog",
40636
+ status: "ok",
40637
+ detail: queueDepth > 0 ? base : "queue drained (0 pending)"
40638
+ };
40639
+ }
39973
40640
  function classifyExtractionLogs(logs) {
39974
40641
  const noSpace = /No space left on device|could not resize shared memory/i.test(logs);
39975
40642
  const llmError = /Claude Code returned an error result|claude_code_llm[\s\S]{0,80}error|Fact extraction failed|Content extraction failed/i.test(logs);
@@ -40026,6 +40693,49 @@ function checkHindsightContainerHealth(opts) {
40026
40693
  } catch {}
40027
40694
  return results;
40028
40695
  }
40696
+ function classifyHindsightHealthProbe(status, port) {
40697
+ if (status === 200) {
40698
+ return {
40699
+ name: "hindsight /health",
40700
+ status: "ok",
40701
+ detail: `200 at :${port} \u2014 memory backend live`
40702
+ };
40703
+ }
40704
+ if (status === null) {
40705
+ return {
40706
+ name: "hindsight /health",
40707
+ status: "fail",
40708
+ detail: `no response on :${port} \u2014 the container is down or crash-looping ` + `(fleet memory is silently OFF: auto-recall/retain fail with no user error)`,
40709
+ fix: "Check `docker logs switchroom-hindsight` for `[Errno 98] address " + "already in use` (a foreign process on the port) and run " + "`switchroom memory --start` \u2014 the launcher now preflights the port " + "and reassigns instead of crash-looping."
40710
+ };
40711
+ }
40712
+ return {
40713
+ name: "hindsight /health",
40714
+ status: "fail",
40715
+ detail: `HTTP ${status} at :${port} \u2014 memory backend unhealthy`,
40716
+ fix: "Inspect `docker logs switchroom-hindsight`; restart with `switchroom memory --restart`."
40717
+ };
40718
+ }
40719
+ async function checkHindsightHealthEndpoint(mcpUrl, opts = {}) {
40720
+ const doFetch = opts.fetchImpl ?? fetch;
40721
+ const timeoutMs = opts.timeoutMs ?? 4000;
40722
+ const origin = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
40723
+ const healthUrl = `${origin}/health`;
40724
+ const port = (() => {
40725
+ const m = origin.match(/:(\d+)(?:$|\/)/);
40726
+ return m ? parseInt(m[1], 10) : 80;
40727
+ })();
40728
+ const controller = new AbortController;
40729
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
40730
+ try {
40731
+ const res = await doFetch(healthUrl, { signal: controller.signal });
40732
+ return classifyHindsightHealthProbe(res.status, port);
40733
+ } catch {
40734
+ return classifyHindsightHealthProbe(null, port);
40735
+ } finally {
40736
+ clearTimeout(timer);
40737
+ }
40738
+ }
40029
40739
  function classifyToolContract(advertised) {
40030
40740
  const byName = new Map(advertised.map((t) => [t.name, t]));
40031
40741
  const results = [];
@@ -40068,7 +40778,7 @@ function classifyToolContract(advertised) {
40068
40778
  }
40069
40779
  return results;
40070
40780
  }
40071
- var MIN_HINDSIGHT_SHM_BYTES;
40781
+ var MIN_HINDSIGHT_SHM_BYTES, CONSOLIDATION_BACKLOG_WARN = 25, CONSOLIDATION_BACKLOG_FAIL = 200;
40072
40782
  var init_doctor_memory = __esm(() => {
40073
40783
  init_hindsight_tools();
40074
40784
  MIN_HINDSIGHT_SHM_BYTES = 1024 * 1024 * 1024;
@@ -43474,6 +44184,19 @@ async function checkBankIngestHealth(config, url, opts) {
43474
44184
  banks.set(bank, []);
43475
44185
  }
43476
44186
  const inspected = await Promise.all([...banks].map(async ([bankId, agents]) => [bankId, agents, await inspectBankHealth(url, bankId, { fetchImpl: opts?.fetchImpl })]));
44187
+ let backlogRow;
44188
+ if (opts?.includeConsolidationBacklog) {
44189
+ let totalPending = 0;
44190
+ let anyBankReachable = false;
44191
+ for (const [, , h] of inspected) {
44192
+ if (h.ok) {
44193
+ anyBankReachable = true;
44194
+ totalPending += h.pendingOperations;
44195
+ }
44196
+ }
44197
+ if (anyBankReachable)
44198
+ backlogRow = classifyConsolidationBacklog(totalPending);
44199
+ }
43477
44200
  for (const [bankId, agents, h] of inspected) {
43478
44201
  const label = `bank ${bankId}` + (profileBanks.has(bankId) ? " (profile)" : agents[0] !== bankId ? ` (${agents.join(", ")})` : "");
43479
44202
  if (!h.ok) {
@@ -43535,6 +44258,8 @@ async function checkBankIngestHealth(config, url, opts) {
43535
44258
  }
43536
44259
  results.push({ name: label, status: "ok", detail: summary });
43537
44260
  }
44261
+ if (backlogRow)
44262
+ results.push(backlogRow);
43538
44263
  return results;
43539
44264
  }
43540
44265
  function hindsightDocReprocessUrl(mcpUrl, bankId, docId) {
@@ -43545,7 +44270,7 @@ async function checkHindsight(config) {
43545
44270
  if (!isHindsightEnabled(config)) {
43546
44271
  return [];
43547
44272
  }
43548
- const url = config.memory?.config?.url ?? "http://localhost:8888/mcp/";
44273
+ const url = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
43549
44274
  const results = [];
43550
44275
  const match = url.match(/^https?:\/\/([^:/]+):?(\d+)?/);
43551
44276
  if (!match) {
@@ -43591,7 +44316,8 @@ async function checkHindsight(config) {
43591
44316
  }
43592
44317
  results.push(checkHindsightConsumer(config));
43593
44318
  results.push(...checkHindsightContainerHealth());
43594
- results.push(...await checkBankIngestHealth(config, url));
44319
+ results.push(await checkHindsightHealthEndpoint(url));
44320
+ results.push(...await checkBankIngestHealth(config, url, { includeConsolidationBacklog: true }));
43595
44321
  for (const [agentName, agentConfig] of Object.entries(config.agents)) {
43596
44322
  const bankId = agentConfig.memory?.collection ?? agentName;
43597
44323
  const hasBankMission = !!agentConfig.memory?.bank_mission;
@@ -44745,6 +45471,7 @@ var init_doctor = __esm(() => {
44745
45471
  init_manager();
44746
45472
  init_accounts();
44747
45473
  init_manifest();
45474
+ init_hindsight2();
44748
45475
  init_hindsight();
44749
45476
  init_doctor_memory();
44750
45477
  init_doctor_docker();
@@ -48352,12 +49079,15 @@ var exports_apply = {};
48352
49079
  __export(exports_apply, {
48353
49080
  writeInstallTypeCache: () => writeInstallTypeCache,
48354
49081
  runApplyPreflight: () => runApplyPreflight,
49082
+ runApplyDryRun: () => runApplyDryRun,
48355
49083
  runApply: () => runApply,
48356
49084
  resolveVaultBindMountDir: () => resolveVaultBindMountDir,
48357
49085
  resolveOperatorVaultPassphrase: () => resolveOperatorVaultPassphrase,
48358
49086
  registerApplyCommand: () => registerApplyCommand,
48359
49087
  reexecUnderSudo: () => reexecUnderSudo,
48360
49088
  provisionLiteLLMKeys: () => provisionLiteLLMKeys,
49089
+ probeVaultProvisioning: () => probeVaultProvisioning,
49090
+ parseComposeServiceNames: () => parseComposeServiceNames,
48361
49091
  isInAgentContainer: () => isInAgentContainer,
48362
49092
  inspectVaultBindMountDir: () => inspectVaultBindMountDir,
48363
49093
  formatScaffoldFailureResolution: () => formatScaffoldFailureResolution,
@@ -49040,6 +49770,186 @@ Done. Scaffolded ${scaffolded}/${allAgentNames.length} agents.
49040
49770
  failures
49041
49771
  };
49042
49772
  }
49773
+ async function probeVaultProvisioning(config, agentNames, home2) {
49774
+ const optedIn = [];
49775
+ for (const name of agentNames) {
49776
+ const agentConfig = config.agents[name];
49777
+ if (!agentConfig)
49778
+ continue;
49779
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
49780
+ if (effectiveLiteLLMEnabled(config, resolved.litellm))
49781
+ optedIn.push(name);
49782
+ }
49783
+ const needsHindsight = config.litellm?.enabled === true && config.memory?.backend === "hindsight";
49784
+ const passphraseAvailable = await resolveOperatorVaultPassphrase(home2) !== null;
49785
+ const alreadyProvisioned = [];
49786
+ const unprovisionedNew = [];
49787
+ const brokerUnreachable = [];
49788
+ if (optedIn.length > 0) {
49789
+ const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
49790
+ for (const name of optedIn) {
49791
+ const existing = await getViaBrokerStructured2(`litellm/${name}/api-key`);
49792
+ if (existing.kind === "ok")
49793
+ alreadyProvisioned.push(name);
49794
+ else if (existing.kind === "unreachable")
49795
+ brokerUnreachable.push(name);
49796
+ else
49797
+ unprovisionedNew.push(name);
49798
+ }
49799
+ }
49800
+ return {
49801
+ optedInCount: optedIn.length,
49802
+ passphraseAvailable,
49803
+ alreadyProvisioned,
49804
+ unprovisionedNew,
49805
+ brokerUnreachable,
49806
+ needsHindsight
49807
+ };
49808
+ }
49809
+ function parseComposeServiceNames(compose) {
49810
+ const lines = compose.split(`
49811
+ `);
49812
+ const names = [];
49813
+ let inServices = false;
49814
+ for (const line of lines) {
49815
+ if (/^services:\s*$/.test(line)) {
49816
+ inServices = true;
49817
+ continue;
49818
+ }
49819
+ if (inServices && /^\S/.test(line))
49820
+ break;
49821
+ if (!inServices)
49822
+ continue;
49823
+ const m = /^ {2}([A-Za-z0-9_.-]+):\s*$/.exec(line);
49824
+ if (m)
49825
+ names.push(m[1]);
49826
+ }
49827
+ return names;
49828
+ }
49829
+ async function runApplyDryRun(config, options, deps = {}, switchroomConfigPath) {
49830
+ const writeOut = deps.writeOut ?? ((s) => process.stdout.write(s));
49831
+ const writeErr = deps.writeErr ?? ((s) => process.stderr.write(s));
49832
+ const blocking = [];
49833
+ const warnings = [];
49834
+ const info = [];
49835
+ writeOut(source_default.bold(`
49836
+ Dry-run: validating switchroom config (no changes will be written)...
49837
+ `));
49838
+ try {
49839
+ runApplyPreflight(config, { detectComposeV2: deps.detectComposeV2 });
49840
+ } catch (err) {
49841
+ blocking.push(`preflight: ${err.message}`);
49842
+ }
49843
+ const allAgentNames = Object.keys(config.agents ?? {});
49844
+ const agentNames = options.only !== undefined ? [options.only] : allAgentNames;
49845
+ if (options.only !== undefined && !allAgentNames.includes(options.only)) {
49846
+ blocking.push(`apply --only=${options.only}: no such agent in switchroom.yaml (defined: ${allAgentNames.join(", ") || "none"}).`);
49847
+ }
49848
+ for (const name of agentNames) {
49849
+ const agentConfig = config.agents[name];
49850
+ if (!agentConfig)
49851
+ continue;
49852
+ const profileName = agentConfig.extends ?? "default";
49853
+ try {
49854
+ getProfilePath(profileName);
49855
+ resolveAgentConfig(config.defaults, config.profiles, agentConfig);
49856
+ } catch (err) {
49857
+ blocking.push(`profile '${profileName}' for agent '${name}': ${err.message}`);
49858
+ }
49859
+ }
49860
+ try {
49861
+ const probe2 = await probeVaultProvisioning(config, agentNames, homedir47());
49862
+ if (probe2.alreadyProvisioned.length > 0) {
49863
+ info.push(`litellm: ${probe2.alreadyProvisioned.length} agent(s) already provisioned (${probe2.alreadyProvisioned.join(", ")}).`);
49864
+ }
49865
+ if (probe2.brokerUnreachable.length > 0) {
49866
+ warnings.push(`litellm: vault-broker unreachable \u2014 could not verify/provision ` + `${probe2.brokerUnreachable.length} agent(s) (${probe2.brokerUnreachable.join(", ")}); they keep their previous routing env.`);
49867
+ }
49868
+ const pending = [
49869
+ ...probe2.unprovisionedNew,
49870
+ ...probe2.needsHindsight ? ["hindsight (service)"] : []
49871
+ ];
49872
+ if (pending.length > 0) {
49873
+ if (probe2.passphraseAvailable) {
49874
+ info.push(`litellm: would provision ${pending.length} new virtual key(s) (${pending.join(", ")}) \u2014 vault passphrase is available.`);
49875
+ } else {
49876
+ blocking.push(`litellm: ${pending.length} agent(s)/service(s) need a NEW vault key (${pending.join(", ")}) but the vault passphrase is unavailable (no SWITCHROOM_VAULT_PASSPHRASE env and machine-id auto-unlock blob ` + `missing/undecryptable) \u2014 a real apply could not provision them.`);
49877
+ }
49878
+ }
49879
+ } catch (err) {
49880
+ warnings.push(`litellm: provisioning probe failed \u2014 ${err.message}`);
49881
+ }
49882
+ let imageTag = null;
49883
+ let previousImageTag = null;
49884
+ let composeChanged = false;
49885
+ let addedServices = [];
49886
+ let removedServices = [];
49887
+ try {
49888
+ const computed = await computeComposeContent({
49889
+ config,
49890
+ composePath: options.outPath ?? DEFAULT_COMPOSE_PATH2,
49891
+ switchroomConfigPath,
49892
+ releaseOverride: options.releaseOverride,
49893
+ buildMode: options.buildLocal ? "local" : "pull",
49894
+ buildContext: options.buildContext
49895
+ });
49896
+ imageTag = computed.imageTag;
49897
+ previousImageTag = computed.previousImageTag;
49898
+ composeChanged = computed.previous !== computed.content;
49899
+ const newServices = parseComposeServiceNames(computed.content);
49900
+ const oldServices = computed.previous ? parseComposeServiceNames(computed.previous) : [];
49901
+ addedServices = newServices.filter((s) => !oldServices.includes(s));
49902
+ removedServices = oldServices.filter((s) => !newServices.includes(s));
49903
+ if (previousImageTag === null) {
49904
+ info.push(`compose: no existing compose on disk \u2014 a real apply would create it.`);
49905
+ } else if (previousImageTag !== imageTag) {
49906
+ info.push(`compose: agent image would change ${previousImageTag} \u2192 ${imageTag}.`);
49907
+ } else {
49908
+ info.push(`compose: agent image unchanged (${imageTag}).`);
49909
+ }
49910
+ if (addedServices.length > 0) {
49911
+ info.push(`compose: services added: ${addedServices.join(", ")}.`);
49912
+ }
49913
+ if (removedServices.length > 0) {
49914
+ info.push(`compose: services removed: ${removedServices.join(", ")}.`);
49915
+ }
49916
+ if (previousImageTag !== null && !composeChanged) {
49917
+ info.push(`compose: no change (would be byte-identical to the current file).`);
49918
+ }
49919
+ } catch (err) {
49920
+ blocking.push(`compose generation: ${err.message}`);
49921
+ }
49922
+ for (const line of info)
49923
+ writeOut(source_default.gray(` ~ ${line}
49924
+ `));
49925
+ for (const line of warnings)
49926
+ writeErr(source_default.yellow(` ! ${line}
49927
+ `));
49928
+ for (const line of blocking)
49929
+ writeErr(source_default.red(` x ${line}
49930
+ `));
49931
+ const wouldSucceed = blocking.length === 0;
49932
+ writeOut(`
49933
+ `);
49934
+ if (wouldSucceed) {
49935
+ writeOut(source_default.green.bold(`Dry-run: a real apply would SUCCEED. ${allAgentNames.length} agent(s) validated; nothing was written.
49936
+ `));
49937
+ } else {
49938
+ writeErr(source_default.red.bold(`Dry-run: a real apply would FAIL \u2014 ${blocking.length} blocking condition(s) above. Nothing was written.
49939
+ `));
49940
+ }
49941
+ return {
49942
+ blocking,
49943
+ warnings,
49944
+ info,
49945
+ imageTag,
49946
+ previousImageTag,
49947
+ composeChanged,
49948
+ addedServices,
49949
+ removedServices,
49950
+ wouldSucceed
49951
+ };
49952
+ }
49043
49953
  function formatScaffoldFailureResolution(failures, scaffolded, agentsTotal) {
49044
49954
  const fail3 = failures.length;
49045
49955
  const lines = [];
@@ -49157,7 +50067,7 @@ ERROR: failed to spawn sudo: ${result.error.message}
49157
50067
  process.exit(result.status ?? 1);
49158
50068
  }
49159
50069
  function registerApplyCommand(program3) {
49160
- program3.command("apply").description("Apply switchroom.yaml: scaffold every agent and (re)generate the compose file. Run `docker compose -f <path> up -d` afterwards to bring the fleet up.").option("--build-local [context]", "Dev-only: emit `build:` blocks instead of GHCR `image:` refs so `docker compose up --build` rebuilds from in-tree Dockerfiles. Optional context path (defaults to cwd).").option("-o, --out <path>", `Override compose output path (default: ${DEFAULT_COMPOSE_PATH2}).`).option("--example <name>", "Copy an example config into cwd before applying (e.g., 'switchroom' or 'minimal').").option("--non-interactive", "Skip prompts (e.g. sudo-chown explainer for UID alignment). Use in CI / scripts.").option("--allow-unaligned", "Treat UID-alignment chown failures as warnings instead of hard errors. Unsafe: an unaligned state dir will break the agent on first write. Use only if you know you'll fix ownership out-of-band.").option("--only <agent>", "Restrict scaffold + UID-alignment to a single agent (compose still covers the full fleet). Use during a v0.6 \u2192 v0.7 cutover to migrate agents one at a time without breaking the systemd-managed siblings.").option("--compose-only", "Skip the per-agent scaffold loop entirely; only (re)generate the compose file. Use in CI / scripts that can't chown into per-agent state dirs (mode 0700, owned by per-agent UIDs in v0.7+ docker mode). The full apply still runs preflight + emits compose; only the start.sh / .mcp.json / settings.json refresh is skipped.").option("--no-doctor", "Skip the post-apply doctor sweep that surfaces stale start.sh / unhealthy agents (#929). Default: doctor runs after a successful scaffold so the operator sees whether the v0.7+ post-Phase-4 supervisor block is now in place. `switchroom update` passes this internally to avoid running doctor twice (it has its own doctor step).").addOption(new Option("--channel <c>", "Override the resolved `release` block for this apply run: follow the named channel pointer (dev|rc|latest). Mutually exclusive with --pin.").choices(["dev", "rc", "latest"]).conflicts("pin")).addOption(new Option("--pin <p>", "Override the resolved `release` block for this apply run: pin to a specific build (sha-<7-40 hex> or v<semver>). Mutually exclusive with --channel.").conflicts("channel")).option("--print-sudo-cmd", "Print the sudo invocation that `apply` would re-exec itself with when escalation is needed, then exit. Operators who want to script the escalation themselves (CI, custom orchestration) can capture this. Note: tokens are space-separated and not shell-quoted; re-quote arguments if pasting into a shell.").addOption(new Option("--skip-self-elevate").default(false).hideHelp()).action(async (opts) => {
50070
+ program3.command("apply").description("Apply switchroom.yaml: scaffold every agent and (re)generate the compose file. Run `docker compose -f <path> up -d` afterwards to bring the fleet up.").option("--build-local [context]", "Dev-only: emit `build:` blocks instead of GHCR `image:` refs so `docker compose up --build` rebuilds from in-tree Dockerfiles. Optional context path (defaults to cwd).").option("-o, --out <path>", `Override compose output path (default: ${DEFAULT_COMPOSE_PATH2}).`).option("--example <name>", "Copy an example config into cwd before applying (e.g., 'switchroom' or 'minimal').").option("--non-interactive", "Skip prompts (e.g. sudo-chown explainer for UID alignment). Use in CI / scripts.").option("--allow-unaligned", "Treat UID-alignment chown failures as warnings instead of hard errors. Unsafe: an unaligned state dir will break the agent on first write. Use only if you know you'll fix ownership out-of-band.").option("--only <agent>", "Restrict scaffold + UID-alignment to a single agent (compose still covers the full fleet). Use during a v0.6 \u2192 v0.7 cutover to migrate agents one at a time without breaking the systemd-managed siblings.").option("--compose-only", "Skip the per-agent scaffold loop entirely; only (re)generate the compose file. Use in CI / scripts that can't chown into per-agent state dirs (mode 0700, owned by per-agent UIDs in v0.7+ docker mode). The full apply still runs preflight + emits compose; only the start.sh / .mcp.json / settings.json refresh is skipped.").option("--dry-run", "Validate only: run the full compose-regeneration + validation pipeline IN MEMORY (resolve every agent's profile, probe the vault-broker for key-provisioning gaps, compute the would-be compose) and print a report \u2014 without writing the compose file, persisting anything, or touching a container. Exits non-zero if a real apply would fail (e.g. a profile can't resolve). Use as a pre-flight before a `rollout`/`update` (which only print the step plan, not the config-regeneration that actually fails).").option("--no-doctor", "Skip the post-apply doctor sweep that surfaces stale start.sh / unhealthy agents (#929). Default: doctor runs after a successful scaffold so the operator sees whether the v0.7+ post-Phase-4 supervisor block is now in place. `switchroom update` passes this internally to avoid running doctor twice (it has its own doctor step).").addOption(new Option("--channel <c>", "Override the resolved `release` block for this apply run: follow the named channel pointer (dev|rc|latest). Mutually exclusive with --pin.").choices(["dev", "rc", "latest"]).conflicts("pin")).addOption(new Option("--pin <p>", "Override the resolved `release` block for this apply run: pin to a specific build (sha-<7-40 hex> or v<semver>). Mutually exclusive with --channel.").conflicts("channel")).option("--print-sudo-cmd", "Print the sudo invocation that `apply` would re-exec itself with when escalation is needed, then exit. Operators who want to script the escalation themselves (CI, custom orchestration) can capture this. Note: tokens are space-separated and not shell-quoted; re-quote arguments if pasting into a shell.").addOption(new Option("--skip-self-elevate").default(false).hideHelp()).action(async (opts) => {
49161
50071
  try {
49162
50072
  if (opts.example) {
49163
50073
  copyExampleConfig2(opts.example);
@@ -49171,6 +50081,23 @@ function registerApplyCommand(program3) {
49171
50081
  `);
49172
50082
  process.exit(0);
49173
50083
  }
50084
+ if (opts.dryRun) {
50085
+ const dry = await runApplyDryRun(config, {
50086
+ buildLocal: !!opts.buildLocal,
50087
+ buildContext: typeof opts.buildLocal === "string" ? opts.buildLocal : process.cwd(),
50088
+ outPath: opts.out,
50089
+ only: opts.only,
50090
+ releaseOverride: opts.channel ? { channel: opts.channel } : opts.pin ? { pin: opts.pin } : undefined,
50091
+ dryRun: true
50092
+ }, {}, switchroomConfigPath);
50093
+ await captureEvent("apply_dry_run", {
50094
+ agents_total: Object.keys(config.agents ?? {}).length,
50095
+ would_succeed: dry.wouldSucceed,
50096
+ blocking: dry.blocking.length,
50097
+ warnings: dry.warnings.length
50098
+ });
50099
+ process.exit(dry.wouldSucceed ? 0 : 1);
50100
+ }
49174
50101
  if (!opts.skipSelfElevate && !opts.composeOnly && process.geteuid?.() !== 0) {
49175
50102
  const unwritable = findUnwritableAgentDirs(config, {
49176
50103
  only: opts.only
@@ -49278,6 +50205,7 @@ var init_apply = __esm(() => {
49278
50205
  init_update_prompt_hook();
49279
50206
  init_compose();
49280
50207
  init_write_compose();
50208
+ init_profiles();
49281
50209
  init_install_detect();
49282
50210
  init_operator_uid();
49283
50211
  init_posthog();
@@ -63737,8 +64665,8 @@ import { existsSync, readFileSync } from "node:fs";
63737
64665
  import { dirname, join } from "node:path";
63738
64666
 
63739
64667
  // src/build-info.ts
63740
- var VERSION = "0.17.6";
63741
- var COMMIT_SHA = "0cc74c23";
64668
+ var VERSION = "0.18.3";
64669
+ var COMMIT_SHA = "dbd8b112";
63742
64670
 
63743
64671
  // src/cli/resolve-version.ts
63744
64672
  function readPackageVersion() {
@@ -63769,7 +64697,7 @@ var SWITCHROOM_VERSION = (() => {
63769
64697
  init_esm();
63770
64698
  init_source();
63771
64699
  import { join as join21, resolve as resolve22 } from "node:path";
63772
- import { rmSync as rmSync9, existsSync as existsSync31, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
64700
+ import { rmSync as rmSync10, existsSync as existsSync31, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
63773
64701
  import { homedir as homedir8 } from "node:os";
63774
64702
 
63775
64703
  // src/agents/scheduler-state.ts
@@ -63833,6 +64761,7 @@ function formatSchedulerState(state) {
63833
64761
 
63834
64762
  // src/cli/agent.ts
63835
64763
  init_loader();
64764
+ init_hindsight2();
63836
64765
  init_hindsight();
63837
64766
  init_helpers();
63838
64767
  init_scaffold();
@@ -63951,11 +64880,11 @@ function detectInFlight(opts) {
63951
64880
  async function waitUntilIdle(opts) {
63952
64881
  const pollMs = opts.pollMs ?? 2000;
63953
64882
  const clock = opts.now ?? Date.now;
63954
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
64883
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
63955
64884
  const deadline = clock() + opts.timeoutMs;
63956
64885
  let last = detectInFlight(opts);
63957
64886
  while (last.busy && clock() < deadline) {
63958
- await sleep(pollMs);
64887
+ await sleep2(pollMs);
63959
64888
  last = detectInFlight(opts);
63960
64889
  }
63961
64890
  return last;
@@ -64067,10 +64996,10 @@ function spinner(message) {
64067
64996
  // src/agents/status.ts
64068
64997
  import { existsSync as existsSync23, readFileSync as readFileSync20, statSync as statSync11 } from "node:fs";
64069
64998
  import { join as join16 } from "node:path";
64070
- import { execFileSync as execFileSync9 } from "node:child_process";
64999
+ import { execFileSync as execFileSync11 } from "node:child_process";
64071
65000
 
64072
65001
  // src/agents/handoff-summarizer.ts
64073
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync8, renameSync as renameSync5, mkdirSync as mkdirSync15, existsSync as existsSync21, statSync as statSync10, readdirSync as readdirSync9 } from "node:fs";
65002
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync8, renameSync as renameSync6, mkdirSync as mkdirSync15, existsSync as existsSync21, statSync as statSync10, readdirSync as readdirSync9 } from "node:fs";
64074
65003
  import { join as join15 } from "node:path";
64075
65004
  var DEFAULT_MAX_TURNS = 50;
64076
65005
  var TOPIC_MAX_CHARS = 117;
@@ -64212,8 +65141,8 @@ function writeSidecarsAtomic(agentDir, briefing, topic) {
64212
65141
  const topicTmp = topicPath + ".tmp";
64213
65142
  writeFileSync8(handoffTmp, briefing, "utf-8");
64214
65143
  writeFileSync8(topicTmp, topic, "utf-8");
64215
- renameSync5(handoffTmp, handoffPath);
64216
- renameSync5(topicTmp, topicPath);
65144
+ renameSync6(handoffTmp, handoffPath);
65145
+ renameSync6(topicTmp, topicPath);
64217
65146
  }
64218
65147
  async function buildHandoff(opts) {
64219
65148
  const maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
@@ -64652,7 +65581,7 @@ async function buildAgentStatusReport(inputs) {
64652
65581
  async function waitForAgentReady(inputs, options = {}) {
64653
65582
  const timeoutMs = options.timeoutMs ?? 30000;
64654
65583
  const pollIntervalMs = options.pollIntervalMs ?? 750;
64655
- const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
65584
+ const sleep2 = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
64656
65585
  const now = options.now ?? Date.now;
64657
65586
  const startedAt = now();
64658
65587
  const deadline = startedAt + timeoutMs;
@@ -64667,7 +65596,7 @@ async function waitForAgentReady(inputs, options = {}) {
64667
65596
  notReady
64668
65597
  };
64669
65598
  }
64670
- await sleep(pollIntervalMs);
65599
+ await sleep2(pollIntervalMs);
64671
65600
  report = await buildAgentStatusReport(inputs);
64672
65601
  notReady = readinessGaps(report);
64673
65602
  }
@@ -64744,14 +65673,14 @@ function readLastMessages(historyDbPath) {
64744
65673
  };
64745
65674
  }
64746
65675
  try {
64747
- const out = execFileSync9("bun", [
65676
+ const out = execFileSync11("bun", [
64748
65677
  "-e",
64749
65678
  `const { Database } = require("bun:sqlite"); ` + `const db = new Database(${JSON.stringify(historyDbPath)}, { readonly: true }); ` + `const rows = db.prepare("SELECT role, MAX(ts) as ts FROM messages GROUP BY role").all(); ` + `for (const r of rows) console.log(r.role + "|" + r.ts); ` + `db.close();`
64750
65679
  ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
64751
65680
  return parseSqliteRoleTsOutput(out);
64752
65681
  } catch {}
64753
65682
  try {
64754
- const out = execFileSync9("sqlite3", [
65683
+ const out = execFileSync11("sqlite3", [
64755
65684
  historyDbPath,
64756
65685
  "SELECT role, MAX(ts) FROM messages GROUP BY role;"
64757
65686
  ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
@@ -64863,7 +65792,7 @@ function escapeRegex(s) {
64863
65792
  }
64864
65793
  function readDockerContainer(containerName2) {
64865
65794
  try {
64866
- const out = execFileSync9("docker", ["inspect", "--format", "{{json .State}}", containerName2], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
65795
+ const out = execFileSync11("docker", ["inspect", "--format", "{{json .State}}", containerName2], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
64867
65796
  const state = JSON.parse(out.trim());
64868
65797
  const active = state.Status === "running" ? "active" : state.Status === "restarting" ? "restarting" : "inactive";
64869
65798
  const pid = typeof state.Pid === "number" && state.Pid > 0 ? state.Pid : null;
@@ -64925,7 +65854,7 @@ init_profiles();
64925
65854
  init_lifecycle();
64926
65855
  init_manager();
64927
65856
  import { resolve as resolve17 } from "node:path";
64928
- import { existsSync as existsSync26, rmSync as rmSync6 } from "node:fs";
65857
+ import { existsSync as existsSync26, rmSync as rmSync7 } from "node:fs";
64929
65858
 
64930
65859
  // src/telegram/redact-token.ts
64931
65860
  var REDACTED_PLACEHOLDER = "<redacted-bot-token>";
@@ -64978,7 +65907,7 @@ async function pollForDmStart(token, timeoutMs = 120000) {
64978
65907
  const response = await fetch(url);
64979
65908
  data = await response.json();
64980
65909
  } catch {
64981
- await sleep(2000);
65910
+ await sleep2(2000);
64982
65911
  continue;
64983
65912
  }
64984
65913
  if (data.ok && Array.isArray(data.result)) {
@@ -64994,11 +65923,11 @@ async function pollForDmStart(token, timeoutMs = 120000) {
64994
65923
  }
64995
65924
  }
64996
65925
  }
64997
- await sleep(2000);
65926
+ await sleep2(2000);
64998
65927
  }
64999
65928
  throw new Error("Timed out waiting for /start DM");
65000
65929
  }
65001
- function sleep(ms) {
65930
+ function sleep2(ms) {
65002
65931
  return new Promise((resolve17) => setTimeout(resolve17, ms));
65003
65932
  }
65004
65933
 
@@ -65074,7 +66003,7 @@ async function createAgent(opts) {
65074
66003
  const agentDir = resolve17(agentsDir, name);
65075
66004
  await withRollback(() => {
65076
66005
  scaffoldAgent(name, agentConfig, agentsDir, config.telegram, config, undefined, configPath);
65077
- rollbackStack.push(() => rmSync6(agentDir, { recursive: true, force: true }));
66006
+ rollbackStack.push(() => rmSync7(agentDir, { recursive: true, force: true }));
65078
66007
  });
65079
66008
  await withRollback(() => {
65080
66009
  writeAgentEnv(agentDir, telegramBotToken);
@@ -65141,8 +66070,8 @@ async function completeCreation(name, code, opts = {}) {
65141
66070
 
65142
66071
  // src/agents/add-orchestrator.ts
65143
66072
  import { resolve as resolve19, join as join19 } from "node:path";
65144
- import { existsSync as existsSync28, rmSync as rmSync7, statSync as statSync15 } from "node:fs";
65145
- import { execFileSync as execFileSync12 } from "node:child_process";
66073
+ import { existsSync as existsSync28, rmSync as rmSync8, statSync as statSync15 } from "node:fs";
66074
+ import { execFileSync as execFileSync14 } from "node:child_process";
65146
66075
  init_onboarding();
65147
66076
 
65148
66077
  // src/setup/botfather-walkthrough.ts
@@ -65708,7 +66637,7 @@ function pruneBundledSkills(agentDir, keep, scope) {
65708
66637
  if (!isDir)
65709
66638
  continue;
65710
66639
  try {
65711
- rmSync7(entryPath, { recursive: true, force: true });
66640
+ rmSync8(entryPath, { recursive: true, force: true });
65712
66641
  removed.push(entry);
65713
66642
  } catch {}
65714
66643
  }
@@ -65716,7 +66645,7 @@ function pruneBundledSkills(agentDir, keep, scope) {
65716
66645
  }
65717
66646
  function defaultIsUnitActive(unitName) {
65718
66647
  try {
65719
- const out = execFileSync12("systemctl", ["--user", "is-active", unitName], {
66648
+ const out = execFileSync14("systemctl", ["--user", "is-active", unitName], {
65720
66649
  encoding: "utf-8",
65721
66650
  stdio: ["ignore", "pipe", "pipe"]
65722
66651
  }).trim();
@@ -65743,7 +66672,7 @@ import { resolve as resolve20 } from "node:path";
65743
66672
  import {
65744
66673
  existsSync as existsSync29,
65745
66674
  mkdtempSync as mkdtempSync3,
65746
- rmSync as rmSync8,
66675
+ rmSync as rmSync9,
65747
66676
  cpSync
65748
66677
  } from "node:fs";
65749
66678
  import { tmpdir as tmpdir2 } from "node:os";
@@ -65768,7 +66697,7 @@ function defaultDeps() {
65768
66697
  return tmp;
65769
66698
  },
65770
66699
  copyDir: (src, dst) => cpSync(src, dst, { recursive: true }),
65771
- removeDir: (p) => rmSync8(p, { recursive: true, force: true }),
66700
+ removeDir: (p) => rmSync9(p, { recursive: true, force: true }),
65772
66701
  existsSync: existsSync29,
65773
66702
  readFileSync: (p, enc) => readFileSync23(p, enc)
65774
66703
  };
@@ -66137,16 +67066,16 @@ function parsePorcelainDirty(porcelain) {
66137
67066
  }
66138
67067
  function checkSwitchroomBranch() {
66139
67068
  try {
66140
- const { execFileSync: execFileSync14 } = __require("node:child_process");
67069
+ const { execFileSync: execFileSync15 } = __require("node:child_process");
66141
67070
  const cwd = process.cwd();
66142
- const branch = execFileSync14("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], {
67071
+ const branch = execFileSync15("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], {
66143
67072
  encoding: "utf-8",
66144
67073
  stdio: ["ignore", "pipe", "ignore"]
66145
67074
  }).trim();
66146
67075
  if (branch === "" || branch === "HEAD")
66147
67076
  return null;
66148
67077
  if (branch === "main" || branch === "master") {
66149
- const porcelain = execFileSync14("git", ["-C", cwd, "status", "--porcelain"], {
67078
+ const porcelain = execFileSync15("git", ["-C", cwd, "status", "--porcelain"], {
66150
67079
  encoding: "utf-8",
66151
67080
  stdio: ["ignore", "pipe", "ignore"]
66152
67081
  });
@@ -66209,7 +67138,7 @@ function buildStatusInputs(name, config, agentsDir) {
66209
67138
  let hindsightApiUrl = null;
66210
67139
  let hindsightBankId = name;
66211
67140
  if (isHindsightEnabled(config)) {
66212
- const baseUrl = config.memory?.config?.url ?? "http://localhost:8888/mcp/";
67141
+ const baseUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
66213
67142
  hindsightApiUrl = baseUrl.endsWith("/mcp/") ? baseUrl : baseUrl.replace(/\/$/, "") + "/mcp/";
66214
67143
  hindsightBankId = agentConfig?.memory?.collection ?? name;
66215
67144
  }
@@ -66477,7 +67406,7 @@ function registerAgentCommand(program3) {
66477
67406
  let hindsightApiUrl = null;
66478
67407
  let hindsightBankId = name;
66479
67408
  if (isHindsightEnabled(config)) {
66480
- const baseUrl = config.memory?.config?.url ?? "http://localhost:8888/mcp/";
67409
+ const baseUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
66481
67410
  hindsightApiUrl = baseUrl.endsWith("/mcp/") ? baseUrl : baseUrl.replace(/\/$/, "") + "/mcp/";
66482
67411
  hindsightBankId = agentConfig.memory?.collection ?? name;
66483
67412
  }
@@ -67161,7 +68090,7 @@ Permissions for ${name}
67161
68090
  }
67162
68091
  }
67163
68092
  if (existsSync31(agentDir)) {
67164
- rmSync9(agentDir, { recursive: true, force: true });
68093
+ rmSync10(agentDir, { recursive: true, force: true });
67165
68094
  console.log(source_default.green(` Removed directory: ${agentDir}`));
67166
68095
  } else {
67167
68096
  console.log(source_default.gray(` Directory not found: ${agentDir}`));
@@ -67365,7 +68294,7 @@ switchroom agent add: ${name}
67365
68294
  const agentsDirRollback = resolveAgentsDir(cfg);
67366
68295
  const agentDirRollback = resolve22(agentsDirRollback, name);
67367
68296
  if (existsSync31(agentDirRollback)) {
67368
- rmSync9(agentDirRollback, { recursive: true, force: true });
68297
+ rmSync10(agentDirRollback, { recursive: true, force: true });
67369
68298
  console.log(source_default.gray(` Rolled back: removed ${agentDirRollback}`));
67370
68299
  }
67371
68300
  process.exit(1);
@@ -69996,7 +70925,7 @@ function registerAuthCommand(program3) {
69996
70925
  try {
69997
70926
  mode = statSync17(yamlPath).mode & 511;
69998
70927
  } catch {}
69999
- atomicWriteFileSync2(yamlPath, after, mode);
70928
+ atomicWriteFileSync(yamlPath, after, mode);
70000
70929
  if (!quiet) {
70001
70930
  console.log(source_default.gray(` Pinned auth.active: ${label} in ${yamlPath}`));
70002
70931
  }
@@ -70143,9 +71072,9 @@ import { readFileSync as readFileSync39 } from "node:fs";
70143
71072
  init_source();
70144
71073
  init_loader();
70145
71074
  init_vault();
70146
- init_hindsight();
71075
+ init_hindsight2();
70147
71076
  import { readFileSync as readFileSync31, writeFileSync as writeFileSync17, copyFileSync as copyFileSync6, existsSync as existsSync36, readdirSync as readdirSync16, statSync as statSync18 } from "node:fs";
70148
- import { execFileSync as execFileSync15 } from "node:child_process";
71077
+ import { execFileSync as execFileSync16 } from "node:child_process";
70149
71078
  import { join as join28 } from "node:path";
70150
71079
  import { homedir as homedir13 } from "node:os";
70151
71080
  function getVaultPath2(configPath) {
@@ -70492,7 +71421,7 @@ function registerVaultSweep(vault, program3) {
70492
71421
  function scrubSqlite(path, values, dryRun) {
70493
71422
  let hasSqlite = true;
70494
71423
  try {
70495
- execFileSync15("sqlite3", ["-version"], { timeout: 2000, stdio: "ignore" });
71424
+ execFileSync16("sqlite3", ["-version"], { timeout: 2000, stdio: "ignore" });
70496
71425
  } catch {
70497
71426
  hasSqlite = false;
70498
71427
  }
@@ -70504,7 +71433,7 @@ function scrubSqlite(path, values, dryRun) {
70504
71433
  for (const { value } of values) {
70505
71434
  const esc = value.replace(/'/g, "''");
70506
71435
  try {
70507
- const out = execFileSync15("sqlite3", [path, `SELECT COUNT(*) FROM messages WHERE text LIKE '%${esc}%'`], { encoding: "utf8", timeout: 1e4 });
71436
+ const out = execFileSync16("sqlite3", [path, `SELECT COUNT(*) FROM messages WHERE text LIKE '%${esc}%'`], { encoding: "utf8", timeout: 1e4 });
70508
71437
  const n = parseInt(out.trim(), 10);
70509
71438
  if (!Number.isNaN(n))
70510
71439
  matches += n;
@@ -70518,7 +71447,7 @@ function scrubSqlite(path, values, dryRun) {
70518
71447
  for (const { key, value } of values) {
70519
71448
  const esc = value.replace(/'/g, "''");
70520
71449
  const replacement = `vault:${key}`.replace(/'/g, "''");
70521
- execFileSync15("sqlite3", [path, `UPDATE messages SET text = replace(text, '${esc}', '${replacement}')`], { timeout: 15000 });
71450
+ execFileSync16("sqlite3", [path, `UPDATE messages SET text = replace(text, '${esc}', '${replacement}')`], { timeout: 15000 });
70522
71451
  }
70523
71452
  return { target: path, matches, modified: true };
70524
71453
  } catch (err) {
@@ -74170,7 +75099,8 @@ class VaultBroker {
74170
75099
  return;
74171
75100
  }
74172
75101
  try {
74173
- const tokenDir = path3.join(os4.homedir(), ".switchroom", "agents", agent);
75102
+ const agentsDir = this.config ? resolveAgentsDir(this.config) : path3.join(os4.homedir(), ".switchroom", "agents");
75103
+ const tokenDir = path3.join(agentsDir, agent);
74174
75104
  mkdirSync24(tokenDir, { recursive: true });
74175
75105
  const tokenPath = path3.join(tokenDir, ".vault-token");
74176
75106
  const tmpPath = `${tokenPath}.tmp.${process.pid}`;
@@ -74234,7 +75164,8 @@ class VaultBroker {
74234
75164
  try {
74235
75165
  const row = this.grantsDb.query("SELECT agent_slug FROM vault_grants WHERE id = ?").get(id);
74236
75166
  if (row && AgentNameSchema.safeParse(row.agent_slug).success) {
74237
- const tokenPath = path3.join(os4.homedir(), ".switchroom", "agents", row.agent_slug, ".vault-token");
75167
+ const agentsDir = this.config ? resolveAgentsDir(this.config) : path3.join(os4.homedir(), ".switchroom", "agents");
75168
+ const tokenPath = path3.join(agentsDir, row.agent_slug, ".vault-token");
74238
75169
  if (existsSync41(tokenPath)) {
74239
75170
  try {
74240
75171
  unlinkSync10(tokenPath);
@@ -77429,10 +78360,10 @@ function fail2(msg) {
77429
78360
 
77430
78361
  // src/cli/memory.ts
77431
78362
  init_source();
77432
- init_hindsight();
78363
+ init_hindsight2();
77433
78364
 
77434
78365
  // src/memory/search.ts
77435
- init_hindsight();
78366
+ init_hindsight2();
77436
78367
  function shellQuote2(s) {
77437
78368
  return "'" + s.replace(/'/g, `'"'"'`) + "'";
77438
78369
  }
@@ -77459,312 +78390,7 @@ function reflectAcrossAgents(config) {
77459
78390
 
77460
78391
  // src/cli/memory.ts
77461
78392
  init_helpers();
77462
-
77463
- // src/setup/hindsight.ts
77464
- import { execFileSync as execFileSync16 } from "node:child_process";
77465
- import { createServer as createServer4 } from "node:net";
77466
- var HINDSIGHT_DEFAULT_API_PORT = 8888;
77467
- var HINDSIGHT_DEFAULT_UI_PORT = 9999;
77468
- var HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000;
77469
- var HINDSIGHT_CONSUMER_NAME = "hindsight";
77470
- var HINDSIGHT_DEFAULT_UID = 11000;
77471
- var HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight";
77472
- var HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
77473
- function hindsightImageRef(tag) {
77474
- const t = tag?.trim();
77475
- if (!t)
77476
- return HINDSIGHT_IMAGE;
77477
- const v = t.replace(/^v/, "");
77478
- return `${HINDSIGHT_IMAGE_REPO}:v${v}`;
77479
- }
77480
- var HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5";
77481
- var HINDSIGHT_DEFAULT_MCP_STATELESS = true;
77482
- var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
77483
- var HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true";
77484
- var HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150;
77485
- var HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4;
77486
- var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
77487
- var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
77488
- var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 12;
77489
- var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 3;
77490
- var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 6;
77491
- var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 300;
77492
- var HINDSIGHT_DEFAULT_MEM_LIMIT = "8g";
77493
- var HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g";
77494
- var HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000;
77495
- var HINDSIGHT_DEFAULT_SHM_SIZE = "2g";
77496
- var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
77497
- var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
77498
- function isPortFree(port) {
77499
- return new Promise((resolve29) => {
77500
- const server = createServer4();
77501
- server.once("error", () => resolve29(false));
77502
- server.once("listening", () => {
77503
- server.close(() => resolve29(true));
77504
- });
77505
- server.listen(port, "127.0.0.1");
77506
- });
77507
- }
77508
- async function findFreePort(start, maxAttempts = 50) {
77509
- for (let i = 0;i < maxAttempts; i++) {
77510
- const port = start + i;
77511
- if (port < 1024)
77512
- continue;
77513
- if (await isPortFree(port)) {
77514
- return port;
77515
- }
77516
- }
77517
- return null;
77518
- }
77519
- async function pickHindsightPorts() {
77520
- if (await isPortFree(HINDSIGHT_DEFAULT_API_PORT) && await isPortFree(HINDSIGHT_DEFAULT_UI_PORT)) {
77521
- return {
77522
- apiPort: HINDSIGHT_DEFAULT_API_PORT,
77523
- uiPort: HINDSIGHT_DEFAULT_UI_PORT
77524
- };
77525
- }
77526
- const apiPort = await findFreePort(18888);
77527
- const uiPort = await findFreePort(19999);
77528
- if (apiPort === null || uiPort === null) {
77529
- throw new Error("Could not find a free port for Hindsight. " + "Stop whatever is using 8888 / 9999 / 18888 / 19999 and retry.");
77530
- }
77531
- return { apiPort, uiPort };
77532
- }
77533
- function isDockerAvailable() {
77534
- try {
77535
- execFileSync16("docker", ["--version"], { stdio: "pipe" });
77536
- return true;
77537
- } catch {
77538
- return false;
77539
- }
77540
- }
77541
- function isHindsightRunning() {
77542
- try {
77543
- const output = execFileSync16("docker", ["ps", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
77544
- return output.trim().length > 0;
77545
- } catch {
77546
- return false;
77547
- }
77548
- }
77549
- function isHindsightContainerExists() {
77550
- try {
77551
- const output = execFileSync16("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf-8" });
77552
- return output.trim().length > 0;
77553
- } catch {
77554
- return false;
77555
- }
77556
- }
77557
- function startHindsight(ports, litellm, imageTag) {
77558
- const apiPort = ports?.apiPort ?? HINDSIGHT_DEFAULT_API_PORT;
77559
- const uiPort = ports?.uiPort ?? HINDSIGHT_DEFAULT_UI_PORT;
77560
- const envArgs = [
77561
- "-e",
77562
- `HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
77563
- "-e",
77564
- "HINDSIGHT_API_LLM_PROVIDER=claude-code",
77565
- "-e",
77566
- `HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
77567
- "-e",
77568
- `HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
77569
- "-e",
77570
- `HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=${HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING}`,
77571
- "-e",
77572
- `HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
77573
- "-e",
77574
- `HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
77575
- "-e",
77576
- `HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
77577
- "-e",
77578
- `HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
77579
- "-e",
77580
- `HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
77581
- "-e",
77582
- `HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
77583
- "-e",
77584
- `HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
77585
- "-e",
77586
- `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`
77587
- ];
77588
- if (litellm) {
77589
- envArgs.push("-e", `HINDSIGHT_API_PORT=${apiPort}`, "-e", `ANTHROPIC_BASE_URL=${litellm.baseUrl.replace(/\/+$/, "")}/anthropic`, "-e", `ANTHROPIC_CUSTOM_HEADERS=x-litellm-api-key: Bearer ${litellm.apiKey}
77590
- x-litellm-customer-id: hindsight
77591
- x-litellm-tags: service:hindsight`);
77592
- }
77593
- const args = [
77594
- "run",
77595
- "-d",
77596
- "--name",
77597
- "switchroom-hindsight",
77598
- "--restart",
77599
- "always",
77600
- `--memory=${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
77601
- `--memory-reservation=${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
77602
- `--pids-limit=${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
77603
- `--shm-size=${HINDSIGHT_DEFAULT_SHM_SIZE}`,
77604
- "--health-cmd",
77605
- litellm ? `python3 -c 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:${apiPort}/health",timeout=4).getcode()==200 else 1)'` : HINDSIGHT_HEALTHCHECK_CMD,
77606
- "--health-interval",
77607
- "30s",
77608
- "--health-timeout",
77609
- "5s",
77610
- "--health-retries",
77611
- "3",
77612
- "--health-start-period",
77613
- "60s"
77614
- ];
77615
- if (litellm) {
77616
- args.push("--network", "host");
77617
- } else {
77618
- args.push("-p", `127.0.0.1:${apiPort}:8888`, "-p", `127.0.0.1:${uiPort}:9999`);
77619
- }
77620
- args.push("-v", "switchroom-hindsight-data:/home/hindsight/.pg0", "-v", "switchroom-hindsight-backups:/backups", "-v", `${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`, "--tmpfs", `/run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`, ...envArgs, hindsightImageRef(imageTag));
77621
- execFileSync16("docker", args, { stdio: "pipe" });
77622
- }
77623
- function stopHindsight() {
77624
- try {
77625
- execFileSync16("docker", ["stop", "switchroom-hindsight"], { stdio: "pipe" });
77626
- } catch {}
77627
- try {
77628
- execFileSync16("docker", ["rm", "switchroom-hindsight"], { stdio: "pipe" });
77629
- } catch {}
77630
- }
77631
- function pullHindsightImage(imageTag) {
77632
- execFileSync16("docker", ["pull", hindsightImageRef(imageTag)], { stdio: "inherit" });
77633
- }
77634
- function getRunningHindsightPorts() {
77635
- const readPort = (containerPort) => {
77636
- try {
77637
- const out = execFileSync16("docker", ["port", "switchroom-hindsight", `${containerPort}/tcp`], { stdio: "pipe", encoding: "utf-8" });
77638
- const m = out.split(`
77639
- `).map((l) => l.trim()).find((l) => l.length > 0);
77640
- if (!m)
77641
- return null;
77642
- const port = Number(m.slice(m.lastIndexOf(":") + 1));
77643
- return Number.isInteger(port) && port > 0 ? port : null;
77644
- } catch {
77645
- return null;
77646
- }
77647
- };
77648
- const apiPort = readPort(HINDSIGHT_DEFAULT_API_PORT);
77649
- if (apiPort === null)
77650
- return null;
77651
- const uiPort = readPort(HINDSIGHT_DEFAULT_UI_PORT) ?? (apiPort === HINDSIGHT_DEFAULT_API_PORT ? HINDSIGHT_DEFAULT_UI_PORT : 19999);
77652
- return { apiPort, uiPort };
77653
- }
77654
- function getHindsightStatus() {
77655
- try {
77656
- const output = execFileSync16("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
77657
- const status = output.trim();
77658
- return status.length > 0 ? status : null;
77659
- } catch {
77660
- return null;
77661
- }
77662
- }
77663
- function generateHindsightComposeSnippet() {
77664
- return [
77665
- "services:",
77666
- " switchroom-hindsight:",
77667
- ` image: ${HINDSIGHT_IMAGE}`,
77668
- " container_name: switchroom-hindsight",
77669
- " ports:",
77670
- ' - "8888:8888"',
77671
- ' - "9999:9999"',
77672
- " environment:",
77673
- ` - HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
77674
- " - HINDSIGHT_API_LLM_PROVIDER=claude-code",
77675
- ` - HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
77676
- ` - HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
77677
- ` - HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=${HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING}`,
77678
- ` - HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
77679
- ` - HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
77680
- ` - HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
77681
- ` - HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
77682
- ` - HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
77683
- ` - HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
77684
- ` - HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
77685
- ` - HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`,
77686
- ` mem_limit: ${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
77687
- ` mem_reservation: ${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
77688
- ` pids_limit: ${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
77689
- ` shm_size: ${HINDSIGHT_DEFAULT_SHM_SIZE}`,
77690
- " healthcheck:",
77691
- ` test: ${JSON.stringify(["CMD", "python3", "-c", HINDSIGHT_HEALTHCHECK_PY])}`,
77692
- " interval: 30s",
77693
- " timeout: 5s",
77694
- " retries: 3",
77695
- " start_period: 60s",
77696
- " volumes:",
77697
- " - switchroom-hindsight-data:/home/hindsight/.pg0",
77698
- " - switchroom-hindsight-backups:/backups",
77699
- ` - ${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
77700
- " tmpfs:",
77701
- ` - /run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`,
77702
- " restart: always",
77703
- "",
77704
- "volumes:",
77705
- " switchroom-hindsight-data:",
77706
- " switchroom-hindsight-backups:",
77707
- ` ${HINDSIGHT_BROKER_SOCK_VOLUME}:`,
77708
- " external: true",
77709
- " # Bound by the switchroom-auth-broker singleton in the main",
77710
- " # switchroom compose project. Declare an `auth.consumers[]`",
77711
- ` # entry named "${HINDSIGHT_CONSUMER_NAME}" in switchroom.yaml.`
77712
- ].join(`
77713
- `);
77714
- }
77715
- async function ensureHindsightConsumer(configPath, account, uid = HINDSIGHT_DEFAULT_UID) {
77716
- const fs4 = await import("node:fs");
77717
- const { parseDocument: parseDocument7, isMap: isMap7, isSeq: isSeq4, YAMLMap, YAMLSeq } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
77718
- const { atomicWriteFileSync: atomicWriteFileSync3 } = await Promise.resolve().then(() => (init_atomic(), exports_atomic));
77719
- const raw = fs4.readFileSync(configPath, "utf-8");
77720
- const doc = parseDocument7(raw);
77721
- const root = doc.contents;
77722
- if (!isMap7(root)) {
77723
- return { added: false, reason: "switchroom.yaml root is not a map" };
77724
- }
77725
- const authRaw = root.get("auth", true);
77726
- let authNode;
77727
- if (isMap7(authRaw)) {
77728
- authNode = authRaw;
77729
- } else {
77730
- authNode = new YAMLMap;
77731
- doc.setIn(["auth"], authNode);
77732
- }
77733
- const consumersRaw = authNode.get("consumers", true);
77734
- let consumersNode;
77735
- if (isSeq4(consumersRaw)) {
77736
- consumersNode = consumersRaw;
77737
- } else {
77738
- consumersNode = new YAMLSeq;
77739
- doc.setIn(["auth", "consumers"], consumersNode);
77740
- }
77741
- for (const item of consumersNode.items) {
77742
- if (isMap7(item)) {
77743
- const name = item.get("name");
77744
- if (name === HINDSIGHT_CONSUMER_NAME) {
77745
- return { added: false, reason: "already present" };
77746
- }
77747
- }
77748
- }
77749
- const entry = new YAMLMap;
77750
- entry.set("name", HINDSIGHT_CONSUMER_NAME);
77751
- if (account !== undefined)
77752
- entry.set("account", account);
77753
- entry.set("uid", uid);
77754
- consumersNode.add(entry);
77755
- const out = String(doc);
77756
- const tail = out.endsWith(`
77757
- `) ? out : out + `
77758
- `;
77759
- let mode = 420;
77760
- try {
77761
- mode = fs4.statSync(configPath).mode & 511;
77762
- } catch {}
77763
- atomicWriteFileSync3(configPath, tail, mode);
77764
- return { added: true, reason: "added" };
77765
- }
77766
-
77767
- // src/cli/memory.ts
78393
+ init_hindsight();
77768
78394
  init_client();
77769
78395
  init_loader();
77770
78396
  var import_yaml13 = __toESM(require_dist(), 1);
@@ -77777,7 +78403,11 @@ async function resolveLiteLLMForHindsight(config) {
77777
78403
  const result = await getViaBrokerStructured("litellm/hindsight/api-key").catch(() => null);
77778
78404
  if (!result || result.kind !== "ok" || result.entry.kind !== "string")
77779
78405
  return;
77780
- return { baseUrl: topLiteLLM.base_url, apiKey: result.entry.value };
78406
+ return {
78407
+ baseUrl: topLiteLLM.base_url,
78408
+ apiKey: result.entry.value,
78409
+ model: config.memory?.config?.llm_model
78410
+ };
77781
78411
  }
77782
78412
  function readRecallLog(agentDir, limit) {
77783
78413
  const path4 = join40(agentDir, ".claude", "plugins", "data", "hindsight-memory-inline", "state", "recall_log.jsonl");
@@ -77984,6 +78614,21 @@ Cross-agent reflection plan
77984
78614
  let ports;
77985
78615
  if (reusePorts) {
77986
78616
  ports = reusePorts;
78617
+ if (reusePorts.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
78618
+ let explicitUrl;
78619
+ try {
78620
+ explicitUrl = getConfig(program3).memory?.config?.url;
78621
+ } catch {
78622
+ explicitUrl = undefined;
78623
+ }
78624
+ if (!explicitUrl) {
78625
+ console.log(source_default.yellow(`
78626
+ \u26a0 MIGRATION HAZARD: reusing stale host port ${reusePorts.apiPort} ` + `for switchroom-hindsight, but scaffolding now defaults to ` + `${HINDSIGHT_DEFAULT_API_PORT} and no explicit memory.config.url is set.`));
78627
+ console.log(source_default.yellow(` Agents may be repointed to a dead URL (http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/) ` + `while the container listens on ${reusePorts.apiPort}.`));
78628
+ console.log(source_default.yellow(` Fix: set memory.config.url: http://127.0.0.1:${reusePorts.apiPort}/mcp/ explicitly, ` + `or migrate the container to port ${HINDSIGHT_DEFAULT_API_PORT}.
78629
+ `));
78630
+ }
78631
+ }
77987
78632
  } else {
77988
78633
  try {
77989
78634
  ports = await pickHindsightPorts();
@@ -77997,14 +78642,38 @@ Cross-agent reflection plan
77997
78642
  if (ports.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
77998
78643
  console.log(source_default.yellow(` Port ${HINDSIGHT_DEFAULT_API_PORT} is already in use; ` + `using ${ports.apiPort}/${ports.uiPort} instead.`));
77999
78644
  }
78645
+ let conflict = await preflightHindsightPorts(ports);
78646
+ if (conflict) {
78647
+ const heldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
78648
+ console.log(source_default.yellow(`
78649
+ Chosen Hindsight port ${conflict.port} is occupied${heldBy}; ` + `selecting a free port instead of crash-looping.`));
78650
+ try {
78651
+ ports = await pickHindsightPorts();
78652
+ } catch (err) {
78653
+ console.error(source_default.red(`
78654
+ ${err.message}
78655
+ `));
78656
+ process.exit(1);
78657
+ }
78658
+ conflict = await preflightHindsightPorts(ports);
78659
+ if (conflict) {
78660
+ const stillHeldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
78661
+ console.error(source_default.red(`
78662
+ Refusing to start Hindsight: port ${conflict.port} is still ` + `occupied${stillHeldBy} after reassignment. Free it and retry ` + `\`switchroom memory --start\`.
78663
+ `));
78664
+ process.exit(1);
78665
+ }
78666
+ console.log(source_default.green(` Reassigned Hindsight to port ${ports.apiPort}/${ports.uiPort}.`));
78667
+ }
78000
78668
  if (opts.provider && opts.provider !== "claude-code") {
78001
78669
  console.log(source_default.gray(` Note: --provider=${opts.provider} ignored. switchroom-hindsight is pinned to the ` + "`claude-code` provider (subscription-honest). The flag is kept for back-compat."));
78002
78670
  }
78003
78671
  const provider = "claude-code";
78004
78672
  console.log(source_default.gray(" Starting Hindsight Docker container..."));
78005
78673
  try {
78006
- const litellmCfg = await resolveLiteLLMForHindsight(getConfig(program3));
78007
- startHindsight(ports, litellmCfg, opts.tag);
78674
+ const hindsightConfig = getConfig(program3);
78675
+ const litellmCfg = await resolveLiteLLMForHindsight(hindsightConfig);
78676
+ startHindsight(ports, litellmCfg, opts.tag, hindsightConfig.hindsight?.llm);
78008
78677
  if (litellmCfg) {
78009
78678
  console.log(source_default.gray(" LiteLLM routing enabled for hindsight (--network host)."));
78010
78679
  }
@@ -78050,7 +78719,7 @@ Cross-agent reflection plan
78050
78719
  console.log(source_default.bold(`
78051
78720
  # Add this to your docker-compose.yml:
78052
78721
  `));
78053
- console.log(generateHindsightComposeSnippet());
78722
+ console.log(generateHindsightComposeSnippet(getConfig(program3).hindsight?.llm));
78054
78723
  console.log();
78055
78724
  });
78056
78725
  memory.command("recall-log [agent]").description("Show recent auto-recall events (per-turn JSONL log) \u2014 see what was injected, when the cap fired, hit rate hints").option("-n, --limit <n>", "Tail the last N events per agent (default 20)", "20").option("--json", "Emit raw JSONL (one entry per line)").action(withConfigError(async (agent, opts) => {
@@ -78103,7 +78772,7 @@ ${name}:`));
78103
78772
  process.exit(1);
78104
78773
  }
78105
78774
  const collection = getCollectionForAgent(agent, config);
78106
- const apiUrl = config.memory?.config?.url ?? "http://localhost:8888/mcp/";
78775
+ const apiUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
78107
78776
  const timeoutMs = Math.max(500, parseInt(opts.timeout, 10) || 5000);
78108
78777
  console.log(source_default.bold(`
78109
78778
  Demoting memory ${source_default.cyan(memoryId)}`));
@@ -78119,7 +78788,7 @@ Demoting memory ${source_default.cyan(memoryId)}`));
78119
78788
  console.error(source_default.gray(" The Hindsight MCP `update_memory` tool may not be exposed by your deployment, or the memory ID may be wrong. Try `switchroom memory recall-log " + agent + " --json` to confirm the ID surfaced recently."));
78120
78789
  process.exit(1);
78121
78790
  }));
78122
- const restBase = (url) => (url ?? "http://127.0.0.1:8888/mcp/").replace(/\/mcp\/?$/, "").replace(/\/$/, "");
78791
+ const restBase = (url) => (url ?? HINDSIGHT_DEFAULT_MCP_URL).replace(/\/mcp\/?$/, "").replace(/\/$/, "");
78123
78792
  const VALID_BANK = /^[a-zA-Z0-9_.-]+$/;
78124
78793
  const profile = memory.command("profile").description("Author + inspect operator profile banks (shared/per-user memory; wire via memory.recall.additional_banks)");
78125
78794
  profile.command("add <bank> <fact...>").description("Add an operator-authored fact to a profile bank (creates the bank on first write)").option("--timeout <ms>", "HTTP timeout in milliseconds (retain runs synchronously; default: 60000)", "60000").action(withConfigError(async (bank, factWords, opts) => {
@@ -78227,13 +78896,13 @@ import { timingSafeEqual as timingSafeEqual3, randomBytes as randomBytes11 } fro
78227
78896
  // src/web/api.ts
78228
78897
  init_lifecycle();
78229
78898
  init_manager();
78230
- init_hindsight();
78899
+ init_hindsight2();
78231
78900
  import { spawnSync as spawnSync6 } from "node:child_process";
78232
78901
  import { existsSync as existsSync51, readFileSync as readFileSync45, statSync as statSync23 } from "node:fs";
78233
78902
  import { resolve as resolve29, join as join45 } from "node:path";
78234
78903
 
78235
78904
  // src/web/memory-remediation.ts
78236
- init_hindsight();
78905
+ init_hindsight2();
78237
78906
  function restBaseFromMcpUrl(mcpUrl) {
78238
78907
  return hindsightRestBase(mcpUrl);
78239
78908
  }
@@ -78295,6 +78964,7 @@ async function buildUserProfile(mcpUrl, bank, opts) {
78295
78964
  }
78296
78965
 
78297
78966
  // src/web/api.ts
78967
+ init_hindsight();
78298
78968
  init_audit_reader();
78299
78969
 
78300
78970
  // src/scheduler/dispatch.ts
@@ -82307,6 +82977,7 @@ var NVIDIA_TOOLKIT_INSTALL_HINT = "https://docs.nvidia.com/datacenter/cloud-nati
82307
82977
 
82308
82978
  // src/cli/setup.ts
82309
82979
  init_host_capabilities();
82980
+ init_hindsight();
82310
82981
  init_client();
82311
82982
  init_posthog();
82312
82983
 
@@ -82852,7 +83523,11 @@ async function resolveLiteLLMForHindsight2(config) {
82852
83523
  const result = await getViaBrokerStructured("litellm/hindsight/api-key").catch(() => null);
82853
83524
  if (!result || result.kind !== "ok" || result.entry.kind !== "string")
82854
83525
  return;
82855
- return { baseUrl: topLiteLLM.base_url, apiKey: result.entry.value };
83526
+ return {
83527
+ baseUrl: topLiteLLM.base_url,
83528
+ apiKey: result.entry.value,
83529
+ model: config.memory?.config?.llm_model
83530
+ };
82856
83531
  }
82857
83532
  async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath) {
82858
83533
  stepHeader(6, "Memory backend", STEP_ACTIVE);
@@ -82913,17 +83588,45 @@ async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath) {
82913
83588
  console.log(source_default.gray(" Found stopped switchroom-hindsight container, removing..."));
82914
83589
  stopHindsight();
82915
83590
  }
83591
+ let ports;
83592
+ try {
83593
+ ports = await pickHindsightPorts();
83594
+ } catch (err) {
83595
+ console.log(source_default.red(` ${err.message}`));
83596
+ return;
83597
+ }
83598
+ if (ports.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
83599
+ console.log(source_default.yellow(` Port ${HINDSIGHT_DEFAULT_API_PORT} is already in use; using ${ports.apiPort}/${ports.uiPort} instead.`));
83600
+ }
83601
+ let conflict = await preflightHindsightPorts(ports);
83602
+ if (conflict) {
83603
+ const heldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
83604
+ console.log(source_default.yellow(` Chosen Hindsight port ${conflict.port} is occupied${heldBy}; selecting a free port instead of crash-looping.`));
83605
+ try {
83606
+ ports = await pickHindsightPorts();
83607
+ } catch (err) {
83608
+ console.log(source_default.red(` ${err.message}`));
83609
+ return;
83610
+ }
83611
+ conflict = await preflightHindsightPorts(ports);
83612
+ if (conflict) {
83613
+ const stillHeldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
83614
+ console.log(source_default.red(` Refusing to start Hindsight: port ${conflict.port} is still occupied${stillHeldBy} after reassignment. Free it and re-run \`switchroom setup\`.`));
83615
+ return;
83616
+ }
83617
+ console.log(source_default.green(` Reassigned Hindsight to port ${ports.apiPort}/${ports.uiPort}.`));
83618
+ }
82916
83619
  const spin = spinner("Starting Hindsight Docker container...");
82917
83620
  try {
82918
83621
  const litellmCfg = await resolveLiteLLMForHindsight2(config);
82919
- startHindsight(undefined, litellmCfg);
83622
+ startHindsight(ports, litellmCfg, undefined, config.hindsight?.llm);
82920
83623
  if (litellmCfg) {
82921
83624
  console.log(source_default.gray(" LiteLLM routing enabled (--network host, ANTHROPIC_BASE_URL set)."));
82922
83625
  }
82923
83626
  if (isHindsightRunning()) {
82924
83627
  spin.stop(source_default.green(`${STEP_DONE} Hindsight container started (switchroom-hindsight)`));
82925
- console.log(source_default.gray(" API: http://localhost:8888/mcp"));
82926
- console.log(source_default.gray(" UI: http://localhost:9999"));
83628
+ console.log(source_default.gray(` API: http://localhost:${ports.apiPort}/mcp`));
83629
+ console.log(source_default.gray(` UI: http://localhost:${ports.uiPort}`));
82927
83630
  } else {
82928
83631
  spin.stop(source_default.yellow("Container started but may still be initializing"));
82929
83632
  }
@@ -83450,6 +84153,22 @@ var UPDATE_RESULT_SENTINEL = "SWITCHROOM_UPDATE_RESULT:";
83450
84153
  function encodeUpdateResultLine(payload) {
83451
84154
  return `${UPDATE_RESULT_SENTINEL}${JSON.stringify(payload)}`;
83452
84155
  }
84156
+ function resolveHindsightPinTag(opts) {
84157
+ if (typeof opts.hindsightPinTag === "string") {
84158
+ return opts.hindsightPinTag || undefined;
84159
+ }
84160
+ if (opts.channel)
84161
+ return;
84162
+ let candidate = opts.pin;
84163
+ if (!candidate) {
84164
+ try {
84165
+ candidate = loadConfig().release?.pin ?? undefined;
84166
+ } catch {
84167
+ candidate = undefined;
84168
+ }
84169
+ }
84170
+ return candidate && /^v\d+\.\d+\.\d+$/.test(candidate) ? candidate : undefined;
84171
+ }
83453
84172
  function planUpdate(opts) {
83454
84173
  const composePath = opts.composePath ?? DEFAULT_COMPOSE_PATH;
83455
84174
  const runner = opts.runner ?? defaultRunner2;
@@ -83596,10 +84315,14 @@ function planUpdate(opts) {
83596
84315
  }
83597
84316
  steps.push({
83598
84317
  name: "refresh-hindsight",
83599
- description: "switchroom memory setup --recreate \u2014 pull latest hindsight image + recreate the memory singleton (standalone container, reusing its current port)",
84318
+ description: "switchroom memory setup --recreate \u2014 pull the hindsight image (pinned to the release target when set, else :latest) + recreate the memory singleton (reusing its current port)",
83600
84319
  skipReason: !memoryBackendHindsight ? "memory.backend is not hindsight \u2014 no hindsight singleton to refresh" : opts.skipImages ? "--skip-images flag set" : undefined,
83601
84320
  run: () => {
83602
- const r = runner(process.execPath, [scriptPath, "memory", "setup", "--recreate"]);
84321
+ const pinTag = resolveHindsightPinTag(opts);
84322
+ const setupArgs = ["memory", "setup", "--recreate"];
84323
+ if (pinTag)
84324
+ setupArgs.push("--tag", pinTag);
84325
+ const r = runner(process.execPath, [scriptPath, ...setupArgs]);
83603
84326
  if (r.status !== 0)
83604
84327
  throw new Error("switchroom memory setup --recreate failed");
83605
84328
  }
@@ -83963,6 +84686,7 @@ import { homedir as homedir39 } from "node:os";
83963
84686
  init_operator_uid();
83964
84687
  init_atomic();
83965
84688
  init_audit_reader();
84689
+ init_hindsight();
83966
84690
  function normalizeVersion(v) {
83967
84691
  return v.trim().replace(/^v/, "");
83968
84692
  }
@@ -86902,7 +87626,7 @@ import { existsSync as existsSync72, readFileSync as readFileSync63, readdirSync
86902
87626
  import { resolve as resolve45, join as join71 } from "node:path";
86903
87627
  import { createHash as createHash14 } from "node:crypto";
86904
87628
  init_merge();
86905
- init_hindsight();
87629
+ init_hindsight2();
86906
87630
  function formatBytes(bytes) {
86907
87631
  return `${bytes.toLocaleString()} bytes`;
86908
87632
  }
@@ -87323,6 +88047,8 @@ async function claimWorktree(input, codeRepos) {
87323
88047
  const baseDir = worktreesBaseDir();
87324
88048
  mkdirSync38(baseDir, { recursive: true });
87325
88049
  worktreePath = join73(baseDir, `${id}-${taskSuffix}`);
88050
+ const ambientOwner = process.env.SWITCHROOM_AGENT_NAME;
88051
+ const ownerAgent = input.ownerAgent ?? (ambientOwner != null && ambientOwner !== "" ? ambientOwner : undefined);
87326
88052
  const now = new Date().toISOString();
87327
88053
  const record2 = {
87328
88054
  id,
@@ -87332,7 +88058,7 @@ async function claimWorktree(input, codeRepos) {
87332
88058
  path: worktreePath,
87333
88059
  createdAt: now,
87334
88060
  heartbeatAt: now,
87335
- ownerAgent: input.ownerAgent
88061
+ ownerAgent
87336
88062
  };
87337
88063
  writeRecord(record2);
87338
88064
  } finally {