switchroom 0.17.6 → 0.17.10

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 (155) hide show
  1. package/dist/agent-scheduler/index.js +38 -4
  2. package/dist/auth-broker/index.js +302 -203
  3. package/dist/cli/notion-write-pretool.mjs +35 -2
  4. package/dist/cli/switchroom.js +1178 -576
  5. package/dist/host-control/main.js +148 -14
  6. package/dist/vault/approvals/kernel-server.js +140 -55
  7. package/dist/vault/broker/server.js +142 -57
  8. package/package.json +1 -1
  9. package/profiles/_base/start.sh.hbs +50 -6
  10. package/profiles/default/CLAUDE.md +116 -0
  11. package/skills/mental-model-curator/SKILL.md +162 -0
  12. package/telegram-plugin/bridge/bridge.ts +80 -1
  13. package/telegram-plugin/bridge/ipc-client.ts +19 -0
  14. package/telegram-plugin/bridge/permission-ledger.ts +61 -0
  15. package/telegram-plugin/consolidation-legibility.ts +279 -0
  16. package/telegram-plugin/dist/bridge/bridge.js +85 -1
  17. package/telegram-plugin/dist/gateway/gateway.js +2565 -610
  18. package/telegram-plugin/dist/server.js +86 -2
  19. package/telegram-plugin/feed-heartbeat-climb.ts +206 -0
  20. package/telegram-plugin/gateway/activity-card-store.ts +293 -0
  21. package/telegram-plugin/gateway/gateway.ts +1376 -82
  22. package/telegram-plugin/gateway/inbound-spool.ts +22 -0
  23. package/telegram-plugin/gateway/mental-model-propose-card.ts +69 -0
  24. package/telegram-plugin/gateway/mental-model-propose-diff.ts +171 -0
  25. package/telegram-plugin/gateway/mental-model-propose-inbound-builders.ts +147 -0
  26. package/telegram-plugin/gateway/mental-model-propose-resolve.ts +201 -0
  27. package/telegram-plugin/gateway/missed-approvals-card.ts +161 -0
  28. package/telegram-plugin/gateway/missed-approvals-store.ts +167 -0
  29. package/telegram-plugin/gateway/permission-rearm.ts +115 -0
  30. package/telegram-plugin/gateway/scoped-grant-store.ts +89 -0
  31. package/telegram-plugin/memory-legibility.ts +217 -0
  32. package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  33. package/telegram-plugin/scoped-approval.ts +59 -0
  34. package/telegram-plugin/silent-end.ts +78 -0
  35. package/telegram-plugin/subagent-watcher.ts +60 -6
  36. package/telegram-plugin/tests/activity-card-store.test.ts +436 -0
  37. package/telegram-plugin/tests/activity-card-wiring.test.ts +88 -0
  38. package/telegram-plugin/tests/consolidation-legibility.test.ts +224 -0
  39. package/telegram-plugin/tests/emission-authority-facade.test.ts +25 -10
  40. package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +33 -9
  41. package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
  42. package/telegram-plugin/tests/inbound-spool.test.ts +105 -0
  43. package/telegram-plugin/tests/memory-legibility.test.ts +216 -0
  44. package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +67 -0
  45. package/telegram-plugin/tests/mental-model-propose-card.test.ts +56 -0
  46. package/telegram-plugin/tests/mental-model-propose-diff.test.ts +201 -0
  47. package/telegram-plugin/tests/mental-model-propose-inbound-builders.test.ts +68 -0
  48. package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +157 -0
  49. package/telegram-plugin/tests/missed-approvals-card.test.ts +145 -0
  50. package/telegram-plugin/tests/missed-approvals-store.test.ts +147 -0
  51. package/telegram-plugin/tests/missed-approvals-wiring.test.ts +89 -0
  52. package/telegram-plugin/tests/permission-ledger.test.ts +166 -0
  53. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +1 -1
  54. package/telegram-plugin/tests/permission-rearm-wiring.test.ts +175 -0
  55. package/telegram-plugin/tests/permission-rearm.test.ts +126 -0
  56. package/telegram-plugin/tests/scoped-grant-persist.test.ts +223 -0
  57. package/telegram-plugin/tests/silent-end-transport.test.ts +290 -0
  58. package/telegram-plugin/tests/silent-turn-climb-transport.test.ts +337 -0
  59. package/telegram-plugin/tests/subagent-watcher.test.ts +139 -0
  60. package/telegram-plugin/tests/worktree-watch-cwds.test.ts +103 -0
  61. package/telegram-plugin/uat/assertions.ts +88 -4
  62. package/telegram-plugin/uat/feed-matcher.test.ts +69 -0
  63. package/telegram-plugin/uat/scenarios/fuzz-liveness-climb-dm.test.ts +155 -0
  64. package/telegram-plugin/uat/scenarios/jtbd-directive-capture-nudge-dm.test.ts +185 -0
  65. package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-channel.test.ts +192 -0
  66. package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-dm.test.ts +220 -0
  67. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +137 -0
  68. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +148 -0
  69. package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-channel.test.ts +66 -0
  70. package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-dm.test.ts +61 -0
  71. package/telegram-plugin/uat/scenarios/silent-end-recovery-channel.test.ts +136 -0
  72. package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +24 -2
  73. package/telegram-plugin/worktree-watch-cwds.ts +60 -0
  74. package/vendor/hindsight-memory/hooks/hooks.json +9 -0
  75. package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
  76. package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
  77. package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
  78. package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
  79. package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
  80. package/vendor/hindsight-memory/scripts/directive_verify.py +445 -0
  81. package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  82. package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
  83. package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
  84. package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
  85. package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
  86. package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
  87. package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
  88. package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
  89. package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
  90. package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
  91. package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
  92. package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
  93. package/vendor/hindsight-memory/scripts/lib/config.py +37 -0
  94. package/vendor/hindsight-memory/scripts/lib/directives.py +88 -0
  95. package/vendor/hindsight-memory/scripts/lib/switchroom_envelope.py +77 -0
  96. package/vendor/hindsight-memory/scripts/recall.py +153 -4
  97. package/vendor/hindsight-memory/scripts/retain.py +17 -0
  98. package/vendor/hindsight-memory/scripts/setup_hooks.py +9 -0
  99. package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  100. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
  101. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
  102. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
  103. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
  104. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
  105. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
  106. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
  107. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
  108. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
  109. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
  110. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
  111. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
  112. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
  113. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
  114. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
  115. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
  116. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
  117. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
  118. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
  119. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
  120. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
  121. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
  122. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
  123. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
  124. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
  125. package/vendor/hindsight-memory/scripts/tests/test_directive_capture_nudge.py +185 -0
  126. package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +516 -0
  127. package/vendor/hindsight-memory/scripts/tests/test_directives.py +49 -0
  128. package/vendor/hindsight-memory/scripts/tests/test_retain_window.py +66 -1
  129. package/vendor/hindsight-memory/scripts/tests/test_switchroom_envelope.py +69 -0
  130. package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  131. package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  132. package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
  133. package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
  134. package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
  135. package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
  136. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
  137. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
  138. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
  139. package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
  140. package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
  141. package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  142. package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
  143. package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
  144. package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
  145. package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
  146. package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
  147. package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  148. package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
  149. package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
  150. package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
  151. package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  152. package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
  153. package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
  154. package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
  155. package/vendor/hindsight-memory/tests/test_recall_exit_codes.py +49 -2
@@ -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."),
@@ -13922,6 +13949,7 @@ var init_schema = __esm(() => {
13922
13949
  auto_recall: exports_external.boolean().optional(),
13923
13950
  file: exports_external.boolean().optional(),
13924
13951
  isolation: exports_external.enum(["default", "strict"]).optional(),
13952
+ directive_capture_nudge: exports_external.boolean().optional(),
13925
13953
  recall: exports_external.object({
13926
13954
  max_memories: exports_external.number().int().min(0).optional(),
13927
13955
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
@@ -14617,6 +14645,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
14617
14645
  continue;
14618
14646
  if (k === "recall" && base.recall && typeof v === "object" && v !== null && !Array.isArray(v)) {
14619
14647
  combined[k] = { ...base.recall, ...v };
14648
+ } else if (k === "disposition" && base.disposition && typeof v === "object" && v !== null && !Array.isArray(v)) {
14649
+ combined[k] = {
14650
+ ...base.disposition,
14651
+ ...v
14652
+ };
14620
14653
  } else {
14621
14654
  combined[k] = v;
14622
14655
  }
@@ -15122,9 +15155,427 @@ var init_users = __esm(() => {
15122
15155
  init_merge();
15123
15156
  });
15124
15157
 
15158
+ // src/util/atomic.ts
15159
+ var exports_atomic = {};
15160
+ __export(exports_atomic, {
15161
+ writeConfigFileSync: () => writeConfigFileSync,
15162
+ atomicWriteJsonSync: () => atomicWriteJsonSync,
15163
+ atomicWriteFileSync: () => atomicWriteFileSync
15164
+ });
15165
+ import { randomBytes } from "node:crypto";
15166
+ import { closeSync as closeSync2, constants, fsyncSync, openSync as openSync2, renameSync, rmSync, writeSync } from "node:fs";
15167
+ function atomicWriteFileSync(destPath, contents, mode = 384) {
15168
+ const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
15169
+ const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
15170
+ let fd = null;
15171
+ try {
15172
+ fd = openSync2(tmp, TMP_OPEN_FLAGS, mode);
15173
+ writeSync(fd, buf, 0, buf.length, 0);
15174
+ fsyncSync(fd);
15175
+ closeSync2(fd);
15176
+ fd = null;
15177
+ renameSync(tmp, destPath);
15178
+ } catch (err) {
15179
+ if (fd !== null) {
15180
+ try {
15181
+ closeSync2(fd);
15182
+ } catch {}
15183
+ }
15184
+ try {
15185
+ rmSync(tmp, { force: true });
15186
+ } catch {}
15187
+ throw err;
15188
+ }
15189
+ }
15190
+ function atomicWriteJsonSync(destPath, value, mode = 384) {
15191
+ atomicWriteFileSync(destPath, JSON.stringify(value, null, 2) + `
15192
+ `, mode);
15193
+ }
15194
+ function writeConfigFileSync(destPath, contents, mode = 420) {
15195
+ try {
15196
+ atomicWriteFileSync(destPath, contents, mode);
15197
+ } catch (err) {
15198
+ const code = err.code;
15199
+ if (code !== "EBUSY" && code !== "EXDEV" && code !== "EINVAL") {
15200
+ throw err;
15201
+ }
15202
+ const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
15203
+ let fd = null;
15204
+ try {
15205
+ fd = openSync2(destPath, constants.O_WRONLY | constants.O_TRUNC, mode);
15206
+ writeSync(fd, buf, 0, buf.length, 0);
15207
+ fsyncSync(fd);
15208
+ closeSync2(fd);
15209
+ fd = null;
15210
+ } catch (fallbackErr) {
15211
+ if (fd !== null) {
15212
+ try {
15213
+ closeSync2(fd);
15214
+ } catch {}
15215
+ }
15216
+ throw fallbackErr;
15217
+ }
15218
+ }
15219
+ }
15220
+ var TMP_OPEN_FLAGS;
15221
+ var init_atomic = __esm(() => {
15222
+ TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
15223
+ });
15224
+
15225
+ // src/setup/hindsight.ts
15226
+ import { execFileSync } from "node:child_process";
15227
+ import { createServer } from "node:net";
15228
+ function hindsightImageRef(tag) {
15229
+ const t = tag?.trim();
15230
+ if (!t)
15231
+ return HINDSIGHT_IMAGE;
15232
+ const v = t.replace(/^v/, "");
15233
+ return `${HINDSIGHT_IMAGE_REPO}:v${v}`;
15234
+ }
15235
+ function isPortFree(port) {
15236
+ return new Promise((resolve4) => {
15237
+ const server = createServer();
15238
+ server.once("error", () => resolve4(false));
15239
+ server.once("listening", () => {
15240
+ server.close(() => resolve4(true));
15241
+ });
15242
+ server.listen(port, "127.0.0.1");
15243
+ });
15244
+ }
15245
+ function describePortHolder(port) {
15246
+ const probes = [
15247
+ ["ss", ["-ltnpH", `sport = :${port}`]],
15248
+ ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"]]
15249
+ ];
15250
+ for (const [cmd, args] of probes) {
15251
+ try {
15252
+ const out = execFileSync(cmd, args, {
15253
+ stdio: "pipe",
15254
+ encoding: "utf-8"
15255
+ }).trim();
15256
+ if (!out)
15257
+ continue;
15258
+ const ssMatch = out.match(/users:\(\("([^"]+)",pid=(\d+)/);
15259
+ if (ssMatch)
15260
+ return `pid ${ssMatch[2]} (${ssMatch[1]})`;
15261
+ const firstDataLine = out.split(`
15262
+ `).map((l) => l.trim()).find((l) => l && !/^COMMAND\b/.test(l));
15263
+ if (firstDataLine) {
15264
+ const parts = firstDataLine.split(/\s+/);
15265
+ if (cmd === "lsof" && parts.length >= 2) {
15266
+ return `pid ${parts[1]} (${parts[0]})`;
15267
+ }
15268
+ return firstDataLine.slice(0, 80);
15269
+ }
15270
+ } catch {}
15271
+ }
15272
+ return null;
15273
+ }
15274
+ async function preflightHindsightPorts(ports) {
15275
+ for (const port of [ports.apiPort, ports.uiPort]) {
15276
+ if (!await isPortFree(port)) {
15277
+ return { port, holder: describePortHolder(port) };
15278
+ }
15279
+ }
15280
+ return null;
15281
+ }
15282
+ async function findFreePort(start, maxAttempts = 50) {
15283
+ for (let i = 0;i < maxAttempts; i++) {
15284
+ const port = start + i;
15285
+ if (port < 1024)
15286
+ continue;
15287
+ if (await isPortFree(port)) {
15288
+ return port;
15289
+ }
15290
+ }
15291
+ return null;
15292
+ }
15293
+ async function pickHindsightPorts() {
15294
+ if (await isPortFree(HINDSIGHT_DEFAULT_API_PORT) && await isPortFree(HINDSIGHT_DEFAULT_UI_PORT)) {
15295
+ return {
15296
+ apiPort: HINDSIGHT_DEFAULT_API_PORT,
15297
+ uiPort: HINDSIGHT_DEFAULT_UI_PORT
15298
+ };
15299
+ }
15300
+ const apiPort = await findFreePort(18888);
15301
+ const uiPort = await findFreePort(19999);
15302
+ if (apiPort === null || uiPort === null) {
15303
+ throw new Error("Could not find a free port for Hindsight. " + "Stop whatever is using 18888 / 19999 and retry.");
15304
+ }
15305
+ return { apiPort, uiPort };
15306
+ }
15307
+ function isDockerAvailable() {
15308
+ try {
15309
+ execFileSync("docker", ["--version"], { stdio: "pipe" });
15310
+ return true;
15311
+ } catch {
15312
+ return false;
15313
+ }
15314
+ }
15315
+ function isHindsightRunning() {
15316
+ try {
15317
+ const output = execFileSync("docker", ["ps", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
15318
+ return output.trim().length > 0;
15319
+ } catch {
15320
+ return false;
15321
+ }
15322
+ }
15323
+ function isHindsightContainerExists() {
15324
+ try {
15325
+ const output = execFileSync("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf-8" });
15326
+ return output.trim().length > 0;
15327
+ } catch {
15328
+ return false;
15329
+ }
15330
+ }
15331
+ function startHindsight(ports, litellm, imageTag) {
15332
+ const apiPort = ports?.apiPort ?? HINDSIGHT_DEFAULT_API_PORT;
15333
+ const uiPort = ports?.uiPort ?? HINDSIGHT_DEFAULT_UI_PORT;
15334
+ const envArgs = [
15335
+ "-e",
15336
+ `HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
15337
+ "-e",
15338
+ "HINDSIGHT_API_LLM_PROVIDER=claude-code",
15339
+ "-e",
15340
+ `HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
15341
+ "-e",
15342
+ `ANTHROPIC_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
15343
+ "-e",
15344
+ `HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
15345
+ "-e",
15346
+ `HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=${HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING}`,
15347
+ "-e",
15348
+ `HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
15349
+ "-e",
15350
+ `HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
15351
+ "-e",
15352
+ `HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
15353
+ "-e",
15354
+ `HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
15355
+ "-e",
15356
+ `HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
15357
+ "-e",
15358
+ `HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
15359
+ "-e",
15360
+ `HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
15361
+ "-e",
15362
+ `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`
15363
+ ];
15364
+ if (litellm) {
15365
+ 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}
15366
+ x-litellm-customer-id: hindsight
15367
+ x-litellm-tags: service:hindsight`);
15368
+ }
15369
+ const args = [
15370
+ "run",
15371
+ "-d",
15372
+ "--name",
15373
+ "switchroom-hindsight",
15374
+ "--restart",
15375
+ "always",
15376
+ `--memory=${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
15377
+ `--memory-reservation=${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
15378
+ `--pids-limit=${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
15379
+ `--shm-size=${HINDSIGHT_DEFAULT_SHM_SIZE}`,
15380
+ "--health-cmd",
15381
+ 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,
15382
+ "--health-interval",
15383
+ "30s",
15384
+ "--health-timeout",
15385
+ "5s",
15386
+ "--health-retries",
15387
+ "3",
15388
+ "--health-start-period",
15389
+ "60s"
15390
+ ];
15391
+ if (litellm) {
15392
+ args.push("--network", "host");
15393
+ } else {
15394
+ args.push("-p", `127.0.0.1:${apiPort}:8888`, "-p", `127.0.0.1:${uiPort}:9999`);
15395
+ }
15396
+ 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));
15397
+ execFileSync("docker", args, { stdio: "pipe" });
15398
+ }
15399
+ function stopHindsight() {
15400
+ try {
15401
+ execFileSync("docker", ["stop", "switchroom-hindsight"], { stdio: "pipe" });
15402
+ } catch {}
15403
+ try {
15404
+ execFileSync("docker", ["rm", "switchroom-hindsight"], { stdio: "pipe" });
15405
+ } catch {}
15406
+ }
15407
+ function pullHindsightImage(imageTag) {
15408
+ execFileSync("docker", ["pull", hindsightImageRef(imageTag)], { stdio: "inherit" });
15409
+ }
15410
+ function getRunningHindsightPorts() {
15411
+ const CONTAINER_API_PORT = 8888;
15412
+ const CONTAINER_UI_PORT = 9999;
15413
+ const readPort = (containerPort) => {
15414
+ try {
15415
+ const out = execFileSync("docker", ["port", "switchroom-hindsight", `${containerPort}/tcp`], { stdio: "pipe", encoding: "utf-8" });
15416
+ const m = out.split(`
15417
+ `).map((l) => l.trim()).find((l) => l.length > 0);
15418
+ if (!m)
15419
+ return null;
15420
+ const port = Number(m.slice(m.lastIndexOf(":") + 1));
15421
+ return Number.isInteger(port) && port > 0 ? port : null;
15422
+ } catch {
15423
+ return null;
15424
+ }
15425
+ };
15426
+ const apiPort = readPort(CONTAINER_API_PORT);
15427
+ if (apiPort !== null) {
15428
+ const uiPort = readPort(CONTAINER_UI_PORT) ?? (apiPort === HINDSIGHT_DEFAULT_API_PORT ? HINDSIGHT_DEFAULT_UI_PORT : 19999);
15429
+ return { apiPort, uiPort };
15430
+ }
15431
+ return getHindsightPortsFromEnv();
15432
+ }
15433
+ function getHindsightPortsFromEnv() {
15434
+ let env2;
15435
+ try {
15436
+ env2 = execFileSync("docker", ["inspect", "--format", "{{.Config.Env}}", "switchroom-hindsight"], { stdio: "pipe", encoding: "utf-8" });
15437
+ } catch {
15438
+ return null;
15439
+ }
15440
+ const readVar = (name) => {
15441
+ const re = new RegExp(`(?:^|[\\s\\[])${name}=([^\\s\\]]+)`);
15442
+ const m = env2.match(re);
15443
+ if (!m)
15444
+ return null;
15445
+ const n = Number(m[1]);
15446
+ return Number.isInteger(n) && n > 0 ? n : null;
15447
+ };
15448
+ const apiPort = readVar("HINDSIGHT_API_PORT");
15449
+ if (apiPort === null)
15450
+ return null;
15451
+ const uiPort = readVar("HINDSIGHT_UI_PORT") ?? (apiPort === HINDSIGHT_DEFAULT_API_PORT ? HINDSIGHT_DEFAULT_UI_PORT : 19999);
15452
+ return { apiPort, uiPort };
15453
+ }
15454
+ function getHindsightStatus() {
15455
+ try {
15456
+ const output = execFileSync("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
15457
+ const status = output.trim();
15458
+ return status.length > 0 ? status : null;
15459
+ } catch {
15460
+ return null;
15461
+ }
15462
+ }
15463
+ function generateHindsightComposeSnippet() {
15464
+ return [
15465
+ "services:",
15466
+ " switchroom-hindsight:",
15467
+ ` image: ${HINDSIGHT_IMAGE}`,
15468
+ " container_name: switchroom-hindsight",
15469
+ " ports:",
15470
+ ` - "127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}:8888"`,
15471
+ ' - "127.0.0.1:19999:9999"',
15472
+ " environment:",
15473
+ ` - HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
15474
+ " - HINDSIGHT_API_LLM_PROVIDER=claude-code",
15475
+ ` - HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
15476
+ ` - ANTHROPIC_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
15477
+ ` - HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
15478
+ ` - HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=${HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING}`,
15479
+ ` - HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
15480
+ ` - HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
15481
+ ` - HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
15482
+ ` - HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
15483
+ ` - HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
15484
+ ` - HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
15485
+ ` - HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
15486
+ ` - HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`,
15487
+ ` mem_limit: ${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
15488
+ ` mem_reservation: ${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
15489
+ ` pids_limit: ${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
15490
+ ` shm_size: ${HINDSIGHT_DEFAULT_SHM_SIZE}`,
15491
+ " healthcheck:",
15492
+ ` test: ${JSON.stringify(["CMD", "python3", "-c", HINDSIGHT_HEALTHCHECK_PY])}`,
15493
+ " interval: 30s",
15494
+ " timeout: 5s",
15495
+ " retries: 3",
15496
+ " start_period: 60s",
15497
+ " volumes:",
15498
+ " - switchroom-hindsight-data:/home/hindsight/.pg0",
15499
+ " - switchroom-hindsight-backups:/backups",
15500
+ ` - ${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
15501
+ " tmpfs:",
15502
+ ` - /run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`,
15503
+ " restart: always",
15504
+ "",
15505
+ "volumes:",
15506
+ " switchroom-hindsight-data:",
15507
+ " switchroom-hindsight-backups:",
15508
+ ` ${HINDSIGHT_BROKER_SOCK_VOLUME}:`,
15509
+ " external: true",
15510
+ " # Bound by the switchroom-auth-broker singleton in the main",
15511
+ " # switchroom compose project. Declare an `auth.consumers[]`",
15512
+ ` # entry named "${HINDSIGHT_CONSUMER_NAME}" in switchroom.yaml.`
15513
+ ].join(`
15514
+ `);
15515
+ }
15516
+ async function ensureHindsightConsumer(configPath, account, uid = HINDSIGHT_DEFAULT_UID) {
15517
+ const fs = await import("node:fs");
15518
+ const { parseDocument, isMap, isSeq, YAMLMap, YAMLSeq } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
15519
+ const { atomicWriteFileSync: atomicWriteFileSync2 } = await Promise.resolve().then(() => (init_atomic(), exports_atomic));
15520
+ const raw = fs.readFileSync(configPath, "utf-8");
15521
+ const doc = parseDocument(raw);
15522
+ const root = doc.contents;
15523
+ if (!isMap(root)) {
15524
+ return { added: false, reason: "switchroom.yaml root is not a map" };
15525
+ }
15526
+ const authRaw = root.get("auth", true);
15527
+ let authNode;
15528
+ if (isMap(authRaw)) {
15529
+ authNode = authRaw;
15530
+ } else {
15531
+ authNode = new YAMLMap;
15532
+ doc.setIn(["auth"], authNode);
15533
+ }
15534
+ const consumersRaw = authNode.get("consumers", true);
15535
+ let consumersNode;
15536
+ if (isSeq(consumersRaw)) {
15537
+ consumersNode = consumersRaw;
15538
+ } else {
15539
+ consumersNode = new YAMLSeq;
15540
+ doc.setIn(["auth", "consumers"], consumersNode);
15541
+ }
15542
+ for (const item of consumersNode.items) {
15543
+ if (isMap(item)) {
15544
+ const name = item.get("name");
15545
+ if (name === HINDSIGHT_CONSUMER_NAME) {
15546
+ return { added: false, reason: "already present" };
15547
+ }
15548
+ }
15549
+ }
15550
+ const entry = new YAMLMap;
15551
+ entry.set("name", HINDSIGHT_CONSUMER_NAME);
15552
+ if (account !== undefined)
15553
+ entry.set("account", account);
15554
+ entry.set("uid", uid);
15555
+ consumersNode.add(entry);
15556
+ const out = String(doc);
15557
+ const tail = out.endsWith(`
15558
+ `) ? out : out + `
15559
+ `;
15560
+ let mode = 420;
15561
+ try {
15562
+ mode = fs.statSync(configPath).mode & 511;
15563
+ } catch {}
15564
+ atomicWriteFileSync2(configPath, tail, mode);
15565
+ return { added: true, reason: "added" };
15566
+ }
15567
+ 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_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;
15568
+ var init_hindsight = __esm(() => {
15569
+ HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
15570
+ HINDSIGHT_DEFAULT_API_BASE_URL = HINDSIGHT_DEFAULT_MCP_URL.replace(/\/mcp\/?$/, "");
15571
+ HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
15572
+ HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
15573
+ HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
15574
+ });
15575
+
15125
15576
  // src/memory/hindsight.ts
15126
15577
  function generateHindsightMcpConfig(collection, memoryConfig) {
15127
- const url = memoryConfig.config?.url ?? "http://localhost:8888/mcp/";
15578
+ const url = memoryConfig.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
15128
15579
  return {
15129
15580
  type: "http",
15130
15581
  url,
@@ -15167,6 +15618,26 @@ function isHindsightEnabled(config) {
15167
15618
  return false;
15168
15619
  return config?.memory?.backend === "hindsight";
15169
15620
  }
15621
+ function mergeDisposition(base, over) {
15622
+ if (!base && !over)
15623
+ return;
15624
+ const merged = { ...base ?? {}, ...over ?? {} };
15625
+ return Object.keys(merged).length > 0 ? merged : undefined;
15626
+ }
15627
+ function resolveBankMissionExtras(memory, profileName) {
15628
+ const pd = PROFILE_MEMORY_DEFAULTS[profileName] ?? {};
15629
+ const out = {};
15630
+ const reflect = memory?.reflect_mission ?? pd.reflect_mission;
15631
+ if (reflect != null)
15632
+ out.reflect_mission = reflect;
15633
+ const observations = memory?.observations_mission ?? pd.observations_mission;
15634
+ if (observations != null)
15635
+ out.observations_mission = observations;
15636
+ const disposition = mergeDisposition(pd.disposition, memory?.disposition);
15637
+ if (disposition)
15638
+ out.disposition = disposition;
15639
+ return out;
15640
+ }
15170
15641
  async function parseSseOrJson(resp) {
15171
15642
  const text = await resp.text();
15172
15643
  const dataLine = text.split(`
@@ -15253,9 +15724,19 @@ async function probeHindsight(apiUrl, opts) {
15253
15724
  clearTimeout(timeout);
15254
15725
  }
15255
15726
  }
15256
- async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
15727
+ async function ensureMentalModel(apiUrl, bankId, spec, opts) {
15257
15728
  const fetchImpl = opts?.fetchImpl ?? fetch;
15258
15729
  const timeoutMs = opts?.timeoutMs ?? 5000;
15730
+ const createArguments = {
15731
+ name: spec.name,
15732
+ source_query: spec.sourceQuery
15733
+ };
15734
+ if (spec.refreshAfterConsolidation !== undefined) {
15735
+ createArguments.trigger_refresh_after_consolidation = spec.refreshAfterConsolidation;
15736
+ }
15737
+ if (spec.maxTokens !== undefined) {
15738
+ createArguments.max_tokens = spec.maxTokens;
15739
+ }
15259
15740
  try {
15260
15741
  const controller = new AbortController;
15261
15742
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -15312,7 +15793,7 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
15312
15793
  if (models && typeof models === "string") {
15313
15794
  const parsed = JSON.parse(models);
15314
15795
  const items = Array.isArray(parsed?.items) ? parsed.items : [];
15315
- if (items.some((m) => m?.name === "user-profile")) {
15796
+ if (items.some((m) => m?.name === spec.name)) {
15316
15797
  return { ok: true };
15317
15798
  }
15318
15799
  }
@@ -15333,10 +15814,7 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
15333
15814
  method: "tools/call",
15334
15815
  params: {
15335
15816
  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
- }
15817
+ arguments: createArguments
15340
15818
  }
15341
15819
  }),
15342
15820
  signal: controller.signal
@@ -15360,6 +15838,33 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
15360
15838
  return { ok: false, reason: String(err) };
15361
15839
  }
15362
15840
  }
15841
+ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
15842
+ return ensureMentalModel(apiUrl, bankId, { name: "user-profile", sourceQuery: USER_PROFILE_SOURCE_QUERY }, opts);
15843
+ }
15844
+ async function ensureDeclaredMentalModels(apiUrl, bankId, models, opts) {
15845
+ if (!models || models.length === 0)
15846
+ return [];
15847
+ const outcomes = [];
15848
+ const seen = new Set;
15849
+ for (const m of models) {
15850
+ if (seen.has(m.name))
15851
+ continue;
15852
+ seen.add(m.name);
15853
+ const spec = {
15854
+ name: m.name,
15855
+ sourceQuery: m.source_query,
15856
+ refreshAfterConsolidation: m.refresh_after_consolidation,
15857
+ maxTokens: m.max_tokens
15858
+ };
15859
+ try {
15860
+ const result = await ensureMentalModel(apiUrl, bankId, spec, opts);
15861
+ outcomes.push(result.ok ? { name: m.name, ok: true } : { name: m.name, ok: false, reason: result.reason });
15862
+ } catch (err) {
15863
+ outcomes.push({ name: m.name, ok: false, reason: String(err) });
15864
+ }
15865
+ }
15866
+ return outcomes;
15867
+ }
15363
15868
  async function createBank(apiUrl, bankId, opts) {
15364
15869
  const fetchImpl = opts?.fetchImpl ?? fetch;
15365
15870
  const timeoutMs = opts?.timeoutMs ?? 5000;
@@ -15487,8 +15992,24 @@ async function updateBankMissions(apiUrl, bankId, missions, opts) {
15487
15992
  name: "update_bank",
15488
15993
  arguments: {
15489
15994
  bank_id: bankId,
15490
- ...missions.bank_mission != null ? { mission: missions.bank_mission } : {},
15491
- ...missions.retain_mission != null ? { config_updates: { retain_mission: missions.retain_mission } } : {}
15995
+ ...missions.reflect_mission == null && missions.bank_mission != null ? { mission: missions.bank_mission } : {},
15996
+ ...(() => {
15997
+ const configUpdates = {};
15998
+ if (missions.retain_mission != null)
15999
+ configUpdates.retain_mission = missions.retain_mission;
16000
+ if (missions.reflect_mission != null)
16001
+ configUpdates.reflect_mission = missions.reflect_mission;
16002
+ if (missions.observations_mission != null)
16003
+ configUpdates.observations_mission = missions.observations_mission;
16004
+ const d = missions.disposition;
16005
+ if (d?.skepticism != null)
16006
+ configUpdates.disposition_skepticism = d.skepticism;
16007
+ if (d?.literalism != null)
16008
+ configUpdates.disposition_literalism = d.literalism;
16009
+ if (d?.empathy != null)
16010
+ configUpdates.disposition_empathy = d.empathy;
16011
+ return Object.keys(configUpdates).length > 0 ? { config_updates: configUpdates } : {};
16012
+ })()
15492
16013
  }
15493
16014
  }
15494
16015
  }),
@@ -15586,10 +16107,23 @@ async function addMemoryTag(apiUrl, bankId, memoryId, tag, opts) {
15586
16107
  return { ok: false, reason: String(err) };
15587
16108
  }
15588
16109
  }
15589
- var DEFAULT_RETAIN_MISSION, DEMOTE_FROM_RECALL_TAG = "[demote-from-recall]";
15590
- var init_hindsight = __esm(() => {
16110
+ 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]";
16111
+ var init_hindsight2 = __esm(() => {
15591
16112
  init_users();
16113
+ init_hindsight();
15592
16114
  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.";
16115
+ PROFILE_MEMORY_DEFAULTS = {
16116
+ "health-coach": {
16117
+ disposition: { skepticism: 2, literalism: 2, empathy: 5 },
16118
+ observations_mission: "Synthesise the person's wellbeing patterns, motivations, and emotional " + "context \u2014 how habits, setbacks, and encouragement connect over time."
16119
+ },
16120
+ "executive-assistant": {
16121
+ disposition: { skepticism: 4, literalism: 4, empathy: 3 }
16122
+ },
16123
+ coding: {
16124
+ disposition: { skepticism: 4, literalism: 5, empathy: 2 }
16125
+ }
16126
+ };
15593
16127
  });
15594
16128
 
15595
16129
  // src/cli/helpers.ts
@@ -22398,7 +22932,8 @@ function getBuiltinDefaultSkillEntries() {
22398
22932
  "switchroom-cli",
22399
22933
  "switchroom-status",
22400
22934
  "switchroom-health",
22401
- "switchroom-runtime"
22935
+ "switchroom-runtime",
22936
+ "mental-model-curator"
22402
22937
  ];
22403
22938
  return [
22404
22939
  ...anthropic.map((key) => ({ key, optOutKey: key, source: "anthropic" })),
@@ -22407,7 +22942,7 @@ function getBuiltinDefaultSkillEntries() {
22407
22942
  }
22408
22943
  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
22944
  var init_scaffold_integration = __esm(() => {
22410
- init_hindsight();
22945
+ init_hindsight2();
22411
22946
  });
22412
22947
 
22413
22948
  // src/config/microsoft-workspace-acl.ts
@@ -22425,7 +22960,7 @@ function shouldEmitMs365Mcp(agentName, agentMicrosoftAccount, microsoftAccounts)
22425
22960
  }
22426
22961
 
22427
22962
  // 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";
22963
+ import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readlinkSync as readlinkSync2, rmSync as rmSync2, symlinkSync } from "node:fs";
22429
22964
  import { homedir as homedir3 } from "node:os";
22430
22965
  import { join as join5, resolve as resolve5 } from "node:path";
22431
22966
  function warnMissingPoolDir(poolDir) {
@@ -22495,7 +23030,7 @@ function reconcileAgentDefaultSkills(agentDir, optOuts = {}, defaults = getBuilt
22495
23030
  }
22496
23031
  if (currentTarget && isOwnedStaleLink(currentTarget, poolDir)) {
22497
23032
  try {
22498
- rmSync(dest, { force: true });
23033
+ rmSync2(dest, { force: true });
22499
23034
  } catch {}
22500
23035
  } else {
22501
23036
  result.conflicts.push(entry.key);
@@ -22560,7 +23095,7 @@ import {
22560
23095
  writeFileSync as writeFileSync2,
22561
23096
  mkdirSync as mkdirSync4,
22562
23097
  existsSync as existsSync8,
22563
- renameSync,
23098
+ renameSync as renameSync2,
22564
23099
  copyFileSync as copyFileSync2,
22565
23100
  unlinkSync
22566
23101
  } from "node:fs";
@@ -22600,7 +23135,7 @@ function saveTopicState(state, statePath) {
22600
23135
  try {
22601
23136
  writeFileSync2(tmp, JSON.stringify(state, null, 2) + `
22602
23137
  `, "utf-8");
22603
- renameSync(tmp, path);
23138
+ renameSync2(tmp, path);
22604
23139
  } catch (err) {
22605
23140
  try {
22606
23141
  if (existsSync8(tmp))
@@ -22616,10 +23151,10 @@ var init_state = __esm(() => {
22616
23151
  // src/vault/flock.ts
22617
23152
  import {
22618
23153
  existsSync as existsSync9,
22619
- openSync as openSync2,
22620
- closeSync as closeSync2,
22621
- writeSync,
22622
- fsyncSync,
23154
+ openSync as openSync3,
23155
+ closeSync as closeSync3,
23156
+ writeSync as writeSync2,
23157
+ fsyncSync as fsyncSync2,
22623
23158
  unlinkSync as unlinkSync2,
22624
23159
  readFileSync as readFileSync8,
22625
23160
  readdirSync as readdirSync4,
@@ -22741,7 +23276,7 @@ function acquireLock(vaultPath, options = {}) {
22741
23276
  while (true) {
22742
23277
  let fd = null;
22743
23278
  try {
22744
- fd = openSync2(lockPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
23279
+ fd = openSync3(lockPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
22745
23280
  } catch (err) {
22746
23281
  const code = err?.code ?? "";
22747
23282
  if (code === "EEXIST") {
@@ -22777,11 +23312,11 @@ function acquireLock(vaultPath, options = {}) {
22777
23312
  ${Date.now()}
22778
23313
  ${process.argv[1] ?? ""}
22779
23314
  `;
22780
- writeSync(fd, meta);
22781
- fsyncSync(fd);
23315
+ writeSync2(fd, meta);
23316
+ fsyncSync2(fd);
22782
23317
  } catch {
22783
23318
  try {
22784
- closeSync2(fd);
23319
+ closeSync3(fd);
22785
23320
  } catch {}
22786
23321
  try {
22787
23322
  unlinkSync2(lockPath);
@@ -22792,7 +23327,7 @@ ${process.argv[1] ?? ""}
22792
23327
  return {
22793
23328
  release: () => {
22794
23329
  try {
22795
- closeSync2(ownedFd);
23330
+ closeSync3(ownedFd);
22796
23331
  } catch {}
22797
23332
  try {
22798
23333
  unlinkSync2(lockPath);
@@ -22849,22 +23384,22 @@ __export(exports_vault, {
22849
23384
  KNOWN_VAULT_ARTIFACT_PATTERNS: () => KNOWN_VAULT_ARTIFACT_PATTERNS,
22850
23385
  KNOWN_VAULT_ARTIFACT_NAMES: () => KNOWN_VAULT_ARTIFACT_NAMES
22851
23386
  });
22852
- import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
23387
+ import { randomBytes as randomBytes2, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
22853
23388
  import {
22854
23389
  readFileSync as readFileSync9,
22855
- writeSync as writeSync2,
23390
+ writeSync as writeSync3,
22856
23391
  existsSync as existsSync10,
22857
- renameSync as renameSync2,
23392
+ renameSync as renameSync3,
22858
23393
  mkdirSync as mkdirSync5,
22859
23394
  unlinkSync as unlinkSync3,
22860
- fsyncSync as fsyncSync2,
22861
- openSync as openSync3,
22862
- closeSync as closeSync3,
23395
+ fsyncSync as fsyncSync3,
23396
+ openSync as openSync4,
23397
+ closeSync as closeSync4,
22863
23398
  lstatSync as lstatSync2,
22864
23399
  realpathSync as realpathSync2
22865
23400
  } from "node:fs";
22866
23401
  import { dirname as dirname3, basename as basename3, resolve as resolve7 } from "node:path";
22867
- function atomicWriteFileSync(path, data, mode) {
23402
+ function atomicWriteFileSync2(path, data, mode) {
22868
23403
  let effectivePath = path;
22869
23404
  try {
22870
23405
  if (existsSync10(path) && lstatSync2(path).isSymbolicLink()) {
@@ -22874,20 +23409,20 @@ function atomicWriteFileSync(path, data, mode) {
22874
23409
  const dir = dirname3(resolve7(effectivePath));
22875
23410
  const tmp = resolve7(dir, `.${basename3(effectivePath)}.${process.pid}.${Date.now()}.tmp`);
22876
23411
  try {
22877
- const fd = openSync3(tmp, "wx", mode);
23412
+ const fd = openSync4(tmp, "wx", mode);
22878
23413
  try {
22879
- writeSync2(fd, data);
22880
- fsyncSync2(fd);
23414
+ writeSync3(fd, data);
23415
+ fsyncSync3(fd);
22881
23416
  } finally {
22882
- closeSync3(fd);
23417
+ closeSync4(fd);
22883
23418
  }
22884
- renameSync2(tmp, effectivePath);
23419
+ renameSync3(tmp, effectivePath);
22885
23420
  try {
22886
- const dirFd = openSync3(dir, "r");
23421
+ const dirFd = openSync4(dir, "r");
22887
23422
  try {
22888
- fsyncSync2(dirFd);
23423
+ fsyncSync3(dirFd);
22889
23424
  } finally {
22890
- closeSync3(dirFd);
23425
+ closeSync4(dirFd);
22891
23426
  }
22892
23427
  } catch {}
22893
23428
  } catch (err) {
@@ -22907,7 +23442,7 @@ function deriveKey(passphrase, salt, params = { N: SCRYPT_N, r: SCRYPT_R, p: SCR
22907
23442
  });
22908
23443
  }
22909
23444
  function encrypt(key, plaintext) {
22910
- const iv = randomBytes(12);
23445
+ const iv = randomBytes2(12);
22911
23446
  const cipher = createCipheriv("aes-256-gcm", key, iv);
22912
23447
  const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
22913
23448
  const tag = cipher.getAuthTag();
@@ -23012,7 +23547,7 @@ function createVault(passphrase, vaultPath) {
23012
23547
  if (!existsSync10(dir)) {
23013
23548
  mkdirSync5(dir, { recursive: true, mode: 448 });
23014
23549
  }
23015
- const salt = randomBytes(16);
23550
+ const salt = randomBytes2(16);
23016
23551
  const key = deriveKey(passphrase, salt);
23017
23552
  const vaultData = { secrets: {} };
23018
23553
  const { iv, data, tag } = encrypt(key, JSON.stringify(vaultData));
@@ -23023,7 +23558,7 @@ function createVault(passphrase, vaultPath) {
23023
23558
  tag,
23024
23559
  kdf: { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }
23025
23560
  };
23026
- atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
23561
+ atomicWriteFileSync2(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
23027
23562
  }
23028
23563
  function openVault(passphrase, vaultPath) {
23029
23564
  if (!existsSync10(vaultPath)) {
@@ -23080,7 +23615,7 @@ function saveVault(passphrase, vaultPath, secrets) {
23080
23615
  vaultFile.iv = iv;
23081
23616
  vaultFile.data = data;
23082
23617
  vaultFile.tag = tag;
23083
- atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
23618
+ atomicWriteFileSync2(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
23084
23619
  } finally {
23085
23620
  if (releaseLock !== null) {
23086
23621
  try {
@@ -23262,7 +23797,7 @@ function parseSocketIdentity(socketPath, spec) {
23262
23797
  }
23263
23798
 
23264
23799
  // src/vault/broker/peercred.ts
23265
- import { execFileSync } from "node:child_process";
23800
+ import { execFileSync as execFileSync2 } from "node:child_process";
23266
23801
  import { readFileSync as readFileSync10, readlinkSync as readlinkSync3, fstatSync } from "node:fs";
23267
23802
  function socketPathToIdentity(socketPath) {
23268
23803
  return parseSocketIdentity(socketPath, {
@@ -23435,7 +23970,7 @@ function identify(socketPath, socket, execFileSyncOverride) {
23435
23970
  if (process.platform !== "linux") {
23436
23971
  return null;
23437
23972
  }
23438
- const runner = execFileSyncOverride ?? execFileSync;
23973
+ const runner = execFileSyncOverride ?? execFileSync2;
23439
23974
  let pid = null;
23440
23975
  if (socket !== undefined) {
23441
23976
  const fd = fdFromSocket(socket);
@@ -24295,13 +24830,13 @@ __export(exports_resolver, {
24295
24830
  });
24296
24831
  import {
24297
24832
  chmodSync,
24298
- closeSync as closeSync4,
24833
+ closeSync as closeSync5,
24299
24834
  mkdirSync as mkdirSync6,
24300
24835
  mkdtempSync,
24301
- openSync as openSync4,
24302
- rmSync as rmSync2,
24836
+ openSync as openSync5,
24837
+ rmSync as rmSync3,
24303
24838
  statSync as statSync6,
24304
- writeSync as writeSync3
24839
+ writeSync as writeSync4
24305
24840
  } from "node:fs";
24306
24841
  import { join as join7 } from "node:path";
24307
24842
  import { tmpdir } from "node:os";
@@ -24329,13 +24864,13 @@ function registerCleanupHook() {
24329
24864
  const cleanup = () => {
24330
24865
  for (const dir of materializedDirs) {
24331
24866
  try {
24332
- rmSync2(dir, { recursive: true, force: true });
24867
+ rmSync3(dir, { recursive: true, force: true });
24333
24868
  } catch {}
24334
24869
  }
24335
24870
  materializedDirs.clear();
24336
24871
  if (cachedRoot) {
24337
24872
  try {
24338
- rmSync2(cachedRoot, { recursive: true, force: true });
24873
+ rmSync3(cachedRoot, { recursive: true, force: true });
24339
24874
  } catch {}
24340
24875
  cachedRoot = null;
24341
24876
  }
@@ -24366,18 +24901,18 @@ function materializationRoot() {
24366
24901
  }
24367
24902
  function writeFileExclusive(filePath, content) {
24368
24903
  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);
24904
+ const fd = openSync5(filePath, fsConstants2.O_WRONLY | fsConstants2.O_CREAT | fsConstants2.O_EXCL, 384);
24370
24905
  try {
24371
- writeSync3(fd, buf);
24906
+ writeSync4(fd, buf);
24372
24907
  } finally {
24373
- closeSync4(fd);
24908
+ closeSync5(fd);
24374
24909
  }
24375
24910
  }
24376
24911
  function materializeFilesEntry(key, files) {
24377
24912
  const dir = join7(materializationRoot(), key);
24378
24913
  if (materializedDirs.has(dir)) {
24379
24914
  try {
24380
- rmSync2(dir, { recursive: true, force: true });
24915
+ rmSync3(dir, { recursive: true, force: true });
24381
24916
  } catch {}
24382
24917
  }
24383
24918
  mkdirSync6(dir, { recursive: true, mode: 448 });
@@ -24401,13 +24936,13 @@ function materializeFilesEntry(key, files) {
24401
24936
  function cleanupMaterializedSecrets() {
24402
24937
  for (const dir of materializedDirs) {
24403
24938
  try {
24404
- rmSync2(dir, { recursive: true, force: true });
24939
+ rmSync3(dir, { recursive: true, force: true });
24405
24940
  } catch {}
24406
24941
  }
24407
24942
  materializedDirs.clear();
24408
24943
  if (cachedRoot) {
24409
24944
  try {
24410
- rmSync2(cachedRoot, { recursive: true, force: true });
24945
+ rmSync3(cachedRoot, { recursive: true, force: true });
24411
24946
  } catch {}
24412
24947
  cachedRoot = null;
24413
24948
  }
@@ -24728,7 +25263,7 @@ var init_onboarding = __esm(() => {
24728
25263
  });
24729
25264
 
24730
25265
  // src/repos/bare-clone.ts
24731
- import { execFileSync as execFileSync2 } from "node:child_process";
25266
+ import { execFileSync as execFileSync3 } from "node:child_process";
24732
25267
  import { existsSync as existsSync13, mkdirSync as mkdirSync8 } from "node:fs";
24733
25268
  import { resolve as resolve9 } from "node:path";
24734
25269
  function bareClonePath(slug) {
@@ -24741,7 +25276,7 @@ function ensureBareClone(slug, url) {
24741
25276
  if (!existsSync13(clonePath)) {
24742
25277
  process.stderr.write(`[switchroom] repo "${slug}": cloning ${url} \u2026
24743
25278
  `);
24744
- execFileSync2("git", ["clone", "--bare", url, clonePath], {
25279
+ execFileSync3("git", ["clone", "--bare", url, clonePath], {
24745
25280
  stdio: ["ignore", "pipe", "pipe"]
24746
25281
  });
24747
25282
  process.stderr.write(`[switchroom] repo "${slug}": bare clone ready at ${clonePath}
@@ -24750,7 +25285,7 @@ function ensureBareClone(slug, url) {
24750
25285
  process.stderr.write(`[switchroom] repo "${slug}": fetching ${clonePath} \u2026
24751
25286
  `);
24752
25287
  try {
24753
- execFileSync2("git", ["fetch", "--all"], {
25288
+ execFileSync3("git", ["fetch", "--all"], {
24754
25289
  cwd: clonePath,
24755
25290
  stdio: ["ignore", "pipe", "pipe"]
24756
25291
  });
@@ -24767,7 +25302,7 @@ var init_bare_clone = __esm(() => {
24767
25302
  });
24768
25303
 
24769
25304
  // src/repos/agent-worktree.ts
24770
- import { execFileSync as execFileSync3 } from "node:child_process";
25305
+ import { execFileSync as execFileSync4 } from "node:child_process";
24771
25306
  import { existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync5 } from "node:fs";
24772
25307
  import { join as join9, resolve as resolve10 } from "node:path";
24773
25308
  function agentBranchName(agentName) {
@@ -24778,7 +25313,7 @@ function agentWorktreePath(agentDir, slug) {
24778
25313
  }
24779
25314
  function isWorktreeDirty(worktreePath) {
24780
25315
  try {
24781
- const out = execFileSync3("git", ["status", "--porcelain"], {
25316
+ const out = execFileSync4("git", ["status", "--porcelain"], {
24782
25317
  cwd: worktreePath,
24783
25318
  stdio: ["ignore", "pipe", "pipe"],
24784
25319
  encoding: "utf-8"
@@ -24790,7 +25325,7 @@ function isWorktreeDirty(worktreePath) {
24790
25325
  }
24791
25326
  function headShortSha(worktreePath) {
24792
25327
  try {
24793
- return execFileSync3("git", ["rev-parse", "--short", "HEAD"], {
25328
+ return execFileSync4("git", ["rev-parse", "--short", "HEAD"], {
24794
25329
  cwd: worktreePath,
24795
25330
  stdio: ["ignore", "pipe", "pipe"],
24796
25331
  encoding: "utf-8"
@@ -24801,7 +25336,7 @@ function headShortSha(worktreePath) {
24801
25336
  }
24802
25337
  function branchExistsInBare(bareClonePath2, branchName) {
24803
25338
  try {
24804
- execFileSync3("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
25339
+ execFileSync4("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
24805
25340
  cwd: bareClonePath2,
24806
25341
  stdio: ["ignore", "pipe", "pipe"]
24807
25342
  });
@@ -24812,7 +25347,7 @@ function branchExistsInBare(bareClonePath2, branchName) {
24812
25347
  }
24813
25348
  function resolveDefaultBranch(bareClonePath2) {
24814
25349
  try {
24815
- const out = execFileSync3("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
25350
+ const out = execFileSync4("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
24816
25351
  cwd: bareClonePath2,
24817
25352
  stdio: ["ignore", "pipe", "pipe"],
24818
25353
  encoding: "utf-8"
@@ -24830,12 +25365,12 @@ function ensureAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
24830
25365
  if (!existsSync14(worktreePath)) {
24831
25366
  mkdirSync9(join9(agentDir, "work"), { recursive: true });
24832
25367
  if (branchExistsInBare(bareClonePath2, branch)) {
24833
- execFileSync3("git", ["worktree", "add", worktreePath, branch], {
25368
+ execFileSync4("git", ["worktree", "add", worktreePath, branch], {
24834
25369
  cwd: bareClonePath2,
24835
25370
  stdio: ["ignore", "pipe", "pipe"]
24836
25371
  });
24837
25372
  } else {
24838
- execFileSync3("git", [
25373
+ execFileSync4("git", [
24839
25374
  "worktree",
24840
25375
  "add",
24841
25376
  worktreePath,
@@ -24863,11 +25398,11 @@ function ensureAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
24863
25398
  };
24864
25399
  }
24865
25400
  try {
24866
- execFileSync3("git", ["fetch", "origin"], {
25401
+ execFileSync4("git", ["fetch", "origin"], {
24867
25402
  cwd: worktreePath,
24868
25403
  stdio: ["ignore", "pipe", "pipe"]
24869
25404
  });
24870
- execFileSync3("git", ["merge", "--ff-only", `origin/${defaultBranch}`], {
25405
+ execFileSync4("git", ["merge", "--ff-only", `origin/${defaultBranch}`], {
24871
25406
  cwd: worktreePath,
24872
25407
  stdio: ["ignore", "pipe", "pipe"]
24873
25408
  });
@@ -24885,7 +25420,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
24885
25420
  const branch = agentBranchName(agentName);
24886
25421
  if (existsSync14(worktreePath)) {
24887
25422
  try {
24888
- execFileSync3("git", ["worktree", "remove", "--force", worktreePath], {
25423
+ execFileSync4("git", ["worktree", "remove", "--force", worktreePath], {
24889
25424
  cwd: bareClonePath2,
24890
25425
  stdio: ["ignore", "pipe", "pipe"]
24891
25426
  });
@@ -24897,7 +25432,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
24897
25432
  }
24898
25433
  if (branchExistsInBare(bareClonePath2, branch)) {
24899
25434
  try {
24900
- execFileSync3("git", ["branch", "-D", branch], {
25435
+ execFileSync4("git", ["branch", "-D", branch], {
24901
25436
  cwd: bareClonePath2,
24902
25437
  stdio: ["ignore", "pipe", "pipe"]
24903
25438
  });
@@ -24908,7 +25443,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
24908
25443
  }
24909
25444
  }
24910
25445
  try {
24911
- execFileSync3("git", ["worktree", "prune"], {
25446
+ execFileSync4("git", ["worktree", "prune"], {
24912
25447
  cwd: bareClonePath2,
24913
25448
  stdio: ["ignore", "pipe", "pipe"]
24914
25449
  });
@@ -24925,19 +25460,19 @@ import {
24925
25460
  writeFileSync as writeFileSync4,
24926
25461
  appendFileSync as appendFileSync2,
24927
25462
  readFileSync as readFileSync13,
24928
- renameSync as renameSync3,
25463
+ renameSync as renameSync4,
24929
25464
  chmodSync as chmodSync2,
24930
25465
  symlinkSync as symlinkSync2,
24931
25466
  copyFileSync as copyFileSync4,
24932
25467
  readdirSync as readdirSync6,
24933
- rmSync as rmSync3,
25468
+ rmSync as rmSync4,
24934
25469
  statSync as statSync7,
24935
25470
  lstatSync as lstatSync3,
24936
25471
  readlinkSync as readlinkSync4,
24937
25472
  unlinkSync as unlinkSync4
24938
25473
  } from "node:fs";
24939
25474
  import { homedir as homedir5 } from "node:os";
24940
- import { execSync, execFileSync as execFileSync4 } from "node:child_process";
25475
+ import { execSync, execFileSync as execFileSync5 } from "node:child_process";
24941
25476
  import { join as join10, resolve as resolve11 } from "node:path";
24942
25477
  import { createHash as createHash2 } from "node:crypto";
24943
25478
  function prependReplyDiscipline(rendered, context) {
@@ -25071,13 +25606,13 @@ function alignAgentUid(name, agentDir, uid, opts = {}) {
25071
25606
  `));
25072
25607
  }
25073
25608
  try {
25074
- execFileSync4("chown", ["-R", `${uid}:${uid}`, ...paths], {
25609
+ execFileSync5("chown", ["-R", `${uid}:${uid}`, ...paths], {
25075
25610
  stdio: ["ignore", "ignore", "pipe"]
25076
25611
  });
25077
25612
  return { chowned: true, paths };
25078
25613
  } catch {}
25079
25614
  try {
25080
- execFileSync4("sudo", ["chown", "-R", `${uid}:${uid}`, ...paths], {
25615
+ execFileSync5("sudo", ["chown", "-R", `${uid}:${uid}`, ...paths], {
25081
25616
  stdio: "inherit"
25082
25617
  });
25083
25618
  return { chowned: true, paths };
@@ -25271,7 +25806,7 @@ function migrateLegacySkillsDir(agentDir, skillsPool) {
25271
25806
  }
25272
25807
  if (target && target.startsWith(skillsPool)) {
25273
25808
  try {
25274
- rmSync3(entryPath, { force: true });
25809
+ rmSync4(entryPath, { force: true });
25275
25810
  } catch {}
25276
25811
  }
25277
25812
  }
@@ -25306,7 +25841,7 @@ function syncGlobalSkills(agentDir, declared, skillsDirOverride) {
25306
25841
  } catch {}
25307
25842
  if (target && target.startsWith(skillsPool)) {
25308
25843
  try {
25309
- rmSync3(dest, { force: true });
25844
+ rmSync4(dest, { force: true });
25310
25845
  } catch {}
25311
25846
  } else {
25312
25847
  continue;
@@ -25336,7 +25871,7 @@ function syncGlobalSkills(agentDir, declared, skillsDirOverride) {
25336
25871
  continue;
25337
25872
  }
25338
25873
  if (linkTarget && linkTarget.startsWith(skillsPool)) {
25339
- rmSync3(entryPath, { force: true });
25874
+ rmSync4(entryPath, { force: true });
25340
25875
  }
25341
25876
  }
25342
25877
  }
@@ -25391,7 +25926,7 @@ function installSwitchroomSkills(agentDir, opts = {}) {
25391
25926
  if (currentTarget !== join10(builtinSkillsDir, name))
25392
25927
  continue;
25393
25928
  try {
25394
- rmSync3(dest, { force: true });
25929
+ rmSync4(dest, { force: true });
25395
25930
  } catch {}
25396
25931
  }
25397
25932
  return;
@@ -25414,7 +25949,7 @@ function installSwitchroomSkills(agentDir, opts = {}) {
25414
25949
  if (currentTarget === src)
25415
25950
  continue;
25416
25951
  try {
25417
- rmSync3(dest, { force: true });
25952
+ rmSync4(dest, { force: true });
25418
25953
  } catch {}
25419
25954
  } else {
25420
25955
  continue;
@@ -25666,7 +26201,7 @@ function migrateLegacyAgentsMdIfPresent(agentWorkspaceDir, created) {
25666
26201
  }
25667
26202
  const content = readFileSync13(agentsMd, "utf-8");
25668
26203
  writeFileSync4(claudeMd, content, "utf-8");
25669
- rmSync3(agentsMd);
26204
+ rmSync4(agentsMd);
25670
26205
  created.push(claudeMd);
25671
26206
  console.log(source_default.dim(` migrated legacy workspace/AGENTS.md \u2192 workspace/CLAUDE.md (content preserved)`));
25672
26207
  }
@@ -25682,9 +26217,9 @@ function ensureClaudeMdSymlinks(agentWorkspaceDir, changes) {
25682
26217
  const target = readlinkSync4(linkPath);
25683
26218
  if (target === "CLAUDE.md")
25684
26219
  continue;
25685
- rmSync3(linkPath);
26220
+ rmSync4(linkPath);
25686
26221
  } else {
25687
- rmSync3(linkPath);
26222
+ rmSync4(linkPath);
25688
26223
  }
25689
26224
  }
25690
26225
  symlinkSync2("CLAUDE.md", linkPath);
@@ -25725,7 +26260,7 @@ Thumbs.db
25725
26260
  execSync("git add -A", { cwd: workspaceDir, stdio: "pipe" });
25726
26261
  const userEmail = process.env.GIT_AUTHOR_EMAIL || "switchroom@localhost";
25727
26262
  const userName = process.env.GIT_AUTHOR_NAME || "Switchroom Agent";
25728
- execFileSync4("git", [
26263
+ execFileSync5("git", [
25729
26264
  "-c",
25730
26265
  `user.email=${userEmail}`,
25731
26266
  "-c",
@@ -25773,13 +26308,13 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig) {
25773
26308
  const upToDate = dirContentEquals(sourcePath, destPath, expectedSettings != null ? { "settings.json": expectedSettings } : undefined);
25774
26309
  if (!upToDate) {
25775
26310
  if (existsSync15(destPath)) {
25776
- rmSync3(destPath, { recursive: true, force: true });
26311
+ rmSync4(destPath, { recursive: true, force: true });
25777
26312
  }
25778
26313
  copyDirRecursive2(sourcePath, destPath);
25779
26314
  }
25780
26315
  applyHindsightSettingsOverrides(destPath, additionalBanks);
25781
26316
  const bankId = agentMemory?.collection ?? agentName;
25782
- const mcpUrl = memory.config?.url ?? "http://127.0.0.1:8888/mcp/";
26317
+ const mcpUrl = memory.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
25783
26318
  const apiBaseUrl = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
25784
26319
  return { pluginDir: destPath, apiBaseUrl, bankId };
25785
26320
  }
@@ -25811,6 +26346,7 @@ function renderHindsightSettingsOverrides(raw, additionalBanks) {
25811
26346
  settings.recallMinOverlap = 0.1;
25812
26347
  settings.recallTypes = ["world", "experience", "observation"];
25813
26348
  settings.recallSkipTrivial = true;
26349
+ settings.directiveCaptureNudge = true;
25814
26350
  if (additionalBanks.length > 0) {
25815
26351
  settings.recallAdditionalBanks = [...additionalBanks];
25816
26352
  }
@@ -25838,6 +26374,7 @@ function buildWorkspaceContext(args) {
25838
26374
  hindsightRecallMinOverlap,
25839
26375
  hindsightRecallTypes,
25840
26376
  hindsightRecallSkipTrivial,
26377
+ hindsightDirectiveCaptureNudge,
25841
26378
  hindsightTopicAliasesJson,
25842
26379
  hindsightSenderBanksJson,
25843
26380
  hindsightTopicFilterMode
@@ -25882,6 +26419,7 @@ function buildWorkspaceContext(args) {
25882
26419
  hindsightRecallMinOverlap,
25883
26420
  hindsightRecallTypes,
25884
26421
  hindsightRecallSkipTrivial,
26422
+ hindsightDirectiveCaptureNudge,
25885
26423
  hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
25886
26424
  hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
25887
26425
  hindsightTopicFilterMode,
@@ -26052,12 +26590,13 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
26052
26590
  ]);
26053
26591
  const hindsightAutoRecallEnabled = hindsightEnabled && agentConfig.memory?.auto_recall !== false;
26054
26592
  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";
26593
+ const hindsightApiBaseUrl = switchroomConfig?.memory?.config?.url ? switchroomConfig.memory.config.url.replace(/\/mcp\/?$/, "").replace(/\/$/, "") : HINDSIGHT_DEFAULT_API_BASE_URL;
26056
26594
  const hindsightRecallMaxMemories = agentConfig.memory?.recall?.max_memories;
26057
26595
  const hindsightRecallCacheTtlSecs = agentConfig.memory?.recall?.cache_ttl_secs;
26058
26596
  const hindsightRecallMinOverlap = agentConfig.memory?.recall?.min_overlap;
26059
26597
  const hindsightRecallTypes = agentConfig.memory?.recall?.types?.length ? agentConfig.memory.recall.types.join(",") : undefined;
26060
26598
  const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
26599
+ const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
26061
26600
  const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
26062
26601
  const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
26063
26602
  const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
@@ -26084,6 +26623,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
26084
26623
  hindsightRecallMinOverlap,
26085
26624
  hindsightRecallTypes,
26086
26625
  hindsightRecallSkipTrivial,
26626
+ hindsightDirectiveCaptureNudge,
26087
26627
  hindsightTopicAliasesJson,
26088
26628
  hindsightSenderBanksJson,
26089
26629
  hindsightTopicFilterMode
@@ -26452,12 +26992,12 @@ ${body}
26452
26992
  if (target === "workspace/SOUL.md") {
26453
26993
  skipped.push(agentSoulPath);
26454
26994
  } else {
26455
- rmSync3(agentSoulPath);
26995
+ rmSync4(agentSoulPath);
26456
26996
  symlinkSync2("workspace/SOUL.md", agentSoulPath);
26457
26997
  created.push(agentSoulPath);
26458
26998
  }
26459
26999
  } else {
26460
- rmSync3(agentSoulPath);
27000
+ rmSync4(agentSoulPath);
26461
27001
  symlinkSync2("workspace/SOUL.md", agentSoulPath);
26462
27002
  created.push(agentSoulPath);
26463
27003
  }
@@ -26490,8 +27030,10 @@ ${body}
26490
27030
  const userBankMission = agentConfig.memory?.bank_mission;
26491
27031
  const userRetainMission = agentConfig.memory?.retain_mission;
26492
27032
  const seededRetainMission = userRetainMission ?? DEFAULT_RETAIN_MISSION;
27033
+ const extras = resolveBankMissionExtras(agentConfig.memory, agentConfig.extends ?? DEFAULT_PROFILE);
26493
27034
  const missions = {
26494
- retain_mission: seededRetainMission
27035
+ retain_mission: seededRetainMission,
27036
+ ...extras
26495
27037
  };
26496
27038
  if (userBankMission) {
26497
27039
  missions.bank_mission = userBankMission;
@@ -26506,6 +27048,17 @@ ${body}
26506
27048
  }).catch((err) => {
26507
27049
  console.warn(` ${source_default.yellow("\u26a0")} Bank mission update error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
26508
27050
  });
27051
+ ensureDeclaredMentalModels(apiUrl, hindsightBankId, agentConfig.memory?.mental_models, { timeoutMs: 5000 }).then((outcomes) => {
27052
+ for (const o of outcomes) {
27053
+ if (o.ok) {
27054
+ console.log(` ${source_default.green("\u2713")} Mental model "${o.name}" ready for ${formatAgentBankLabel(name, hindsightBankId)}`);
27055
+ } else {
27056
+ console.warn(` ${source_default.yellow("\u26a0")} Failed to ensure mental model "${o.name}" for ${formatAgentBankLabel(name, hindsightBankId)}: ${o.reason}`);
27057
+ }
27058
+ }
27059
+ }).catch((err) => {
27060
+ console.warn(` ${source_default.yellow("\u26a0")} Mental model ensure error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
27061
+ });
26509
27062
  });
26510
27063
  }
26511
27064
  for (const f of rewrittenWithBackup) {
@@ -26884,7 +27437,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
26884
27437
  mkdirSync10(workspaceDir, { recursive: true });
26885
27438
  const legacyContent = readFileSync13(legacyCustomPath, "utf-8");
26886
27439
  writeFileSync4(newCustomPath, legacyContent, "utf-8");
26887
- rmSync3(legacyCustomPath);
27440
+ rmSync4(legacyCustomPath);
26888
27441
  console.log(source_default.green(` moved CLAUDE.custom.md \u2192 workspace/CLAUDE.custom.md`));
26889
27442
  }
26890
27443
  const tools = agentConfig.tools ?? { allow: [], deny: [] };
@@ -26923,12 +27476,13 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
26923
27476
  const resolvedBotToken = resolveBotToken(rawBotToken);
26924
27477
  const hindsightAutoRecallEnabled = hindsightEnabled && agentConfig.memory?.auto_recall !== false;
26925
27478
  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";
27479
+ const hindsightApiBaseUrl = switchroomConfig.memory?.config?.url ? switchroomConfig.memory.config.url.replace(/\/mcp\/?$/, "").replace(/\/$/, "") : HINDSIGHT_DEFAULT_API_BASE_URL;
26927
27480
  const hindsightRecallMaxMemories = agentConfig.memory?.recall?.max_memories;
26928
27481
  const hindsightRecallCacheTtlSecs = agentConfig.memory?.recall?.cache_ttl_secs;
26929
27482
  const hindsightRecallMinOverlap = agentConfig.memory?.recall?.min_overlap;
26930
27483
  const hindsightRecallTypes = agentConfig.memory?.recall?.types?.length ? agentConfig.memory.recall.types.join(",") : undefined;
26931
27484
  const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
27485
+ const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
26932
27486
  const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
26933
27487
  const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
26934
27488
  const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
@@ -26968,6 +27522,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
26968
27522
  hindsightRecallMinOverlap,
26969
27523
  hindsightRecallTypes,
26970
27524
  hindsightRecallSkipTrivial,
27525
+ hindsightDirectiveCaptureNudge,
26971
27526
  hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
26972
27527
  hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
26973
27528
  hindsightTopicFilterMode,
@@ -27369,6 +27924,7 @@ ${body}
27369
27924
  hindsightRecallMinOverlap,
27370
27925
  hindsightRecallTypes,
27371
27926
  hindsightRecallSkipTrivial,
27927
+ hindsightDirectiveCaptureNudge,
27372
27928
  hindsightTopicAliasesJson,
27373
27929
  hindsightSenderBanksJson,
27374
27930
  hindsightTopicFilterMode
@@ -27397,12 +27953,12 @@ ${body}
27397
27953
  if (stat.isSymbolicLink()) {
27398
27954
  const target = readlinkSync4(agentSoulPath);
27399
27955
  if (target !== "workspace/SOUL.md") {
27400
- rmSync3(agentSoulPath);
27956
+ rmSync4(agentSoulPath);
27401
27957
  symlinkSync2("workspace/SOUL.md", agentSoulPath);
27402
27958
  changes.push(agentSoulPath);
27403
27959
  }
27404
27960
  } else {
27405
- rmSync3(agentSoulPath);
27961
+ rmSync4(agentSoulPath);
27406
27962
  symlinkSync2("workspace/SOUL.md", agentSoulPath);
27407
27963
  changes.push(agentSoulPath);
27408
27964
  }
@@ -27446,14 +28002,15 @@ ${body}
27446
28002
  bankOpsChain.then((bankReady) => {
27447
28003
  if (!bankReady)
27448
28004
  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
- }
28005
+ const extras = resolveBankMissionExtras(agentConfig.memory, agentConfig.extends ?? DEFAULT_PROFILE);
28006
+ const missions = { ...extras };
28007
+ if (agentConfig.memory?.bank_mission) {
28008
+ missions.bank_mission = agentConfig.memory.bank_mission;
28009
+ }
28010
+ if (agentConfig.memory?.retain_mission) {
28011
+ missions.retain_mission = agentConfig.memory.retain_mission;
28012
+ }
28013
+ if (Object.keys(missions).length > 0) {
27457
28014
  updateBankMissions(apiUrl, hindsightBankId, missions, { timeoutMs: 5000 }).then((result) => {
27458
28015
  if (result.ok) {
27459
28016
  console.log(` ${source_default.green("\u2713")} Bank missions updated for ${formatAgentBankLabel(name, hindsightBankId)}`);
@@ -27464,6 +28021,17 @@ ${body}
27464
28021
  console.warn(` ${source_default.yellow("\u26a0")} Bank mission update error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
27465
28022
  });
27466
28023
  }
28024
+ ensureDeclaredMentalModels(apiUrl, hindsightBankId, agentConfig.memory?.mental_models, { timeoutMs: 5000 }).then((outcomes) => {
28025
+ for (const o of outcomes) {
28026
+ if (o.ok) {
28027
+ console.log(` ${source_default.green("\u2713")} Mental model "${o.name}" ready for ${formatAgentBankLabel(name, hindsightBankId)}`);
28028
+ } else {
28029
+ console.warn(` ${source_default.yellow("\u26a0")} Failed to ensure mental model "${o.name}" for ${formatAgentBankLabel(name, hindsightBankId)}: ${o.reason}`);
28030
+ }
28031
+ }
28032
+ }).catch((err) => {
28033
+ console.warn(` ${source_default.yellow("\u26a0")} Mental model ensure error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
28034
+ });
27467
28035
  });
27468
28036
  }
27469
28037
  return {
@@ -27558,7 +28126,7 @@ function reconcileConfiguredGroup(accessPath, agentConfig, telegramConfig) {
27558
28126
  const tmp = accessPath + ".tmp";
27559
28127
  writeFileSync4(tmp, JSON.stringify(access, null, 2) + `
27560
28128
  `, { mode: 384 });
27561
- renameSync3(tmp, accessPath);
28129
+ renameSync4(tmp, accessPath);
27562
28130
  console.log(source_default.green(` registered supergroup ${forumChatId} in access.json ` + `(responds to all topics; requireMention=false)`));
27563
28131
  }
27564
28132
  function buildAccessJson2(agentConfig, telegramConfig, resolvedTopicId, userId) {
@@ -27916,13 +28484,14 @@ var init_scaffold = __esm(() => {
27916
28484
  init_profiles();
27917
28485
  init_scaffold_integration();
27918
28486
  init_reconcile_default_skills();
27919
- init_hindsight();
28487
+ init_hindsight2();
27920
28488
  init_state();
27921
28489
  init_paths();
27922
28490
  init_loader();
27923
28491
  init_resolver();
27924
28492
  init_vault();
27925
28493
  init_onboarding();
28494
+ init_hindsight();
27926
28495
  init_bare_clone();
27927
28496
  init_agent_worktree();
27928
28497
  REPO_ROOT = resolve11(import.meta.dirname, "../..");
@@ -27940,8 +28509,31 @@ var init_scaffold = __esm(() => {
27940
28509
  "mcp__switchroom-telegram__progress_update"
27941
28510
  ];
27942
28511
  HINDSIGHT_MCP_TOOLS = [
27943
- "mcp__hindsight",
27944
- "mcp__hindsight__*"
28512
+ "mcp__hindsight__recall",
28513
+ "mcp__hindsight__retain",
28514
+ "mcp__hindsight__sync_retain",
28515
+ "mcp__hindsight__reflect",
28516
+ "mcp__hindsight__create_directive",
28517
+ "mcp__hindsight__update_bank",
28518
+ "mcp__hindsight__delete_directive",
28519
+ "mcp__hindsight__list_directives",
28520
+ "mcp__hindsight__create_bank",
28521
+ "mcp__hindsight__get_bank",
28522
+ "mcp__hindsight__get_bank_stats",
28523
+ "mcp__hindsight__list_banks",
28524
+ "mcp__hindsight__delete_bank",
28525
+ "mcp__hindsight__get_memory",
28526
+ "mcp__hindsight__list_memories",
28527
+ "mcp__hindsight__clear_memories",
28528
+ "mcp__hindsight__get_document",
28529
+ "mcp__hindsight__list_documents",
28530
+ "mcp__hindsight__delete_document",
28531
+ "mcp__hindsight__get_mental_model",
28532
+ "mcp__hindsight__list_mental_models",
28533
+ "mcp__hindsight__list_tags",
28534
+ "mcp__hindsight__get_operation",
28535
+ "mcp__hindsight__list_operations",
28536
+ "mcp__hindsight__cancel_operation"
27945
28537
  ];
27946
28538
  AGENT_CONFIG_MCP_TOOLS = [
27947
28539
  "mcp__agent-config",
@@ -29033,7 +29625,7 @@ function resolveHostHomeForCompose() {
29033
29625
  }
29034
29626
  return homedir6();
29035
29627
  }
29036
- async function writeComposeFile(opts) {
29628
+ async function computeComposeContent(opts) {
29037
29629
  const release = resolveRelease({ override: opts.releaseOverride, root: opts.config.release });
29038
29630
  const imageTag = resolveImageTag(release);
29039
29631
  const operatorUid = resolveOperatorUid();
@@ -29057,6 +29649,11 @@ async function writeComposeFile(opts) {
29057
29649
  previous = null;
29058
29650
  }
29059
29651
  const previousImageTag = previous ? AGENT_IMAGE_TAG_RE.exec(previous)?.[1] ?? null : null;
29652
+ return { content, imageTag, previous, previousImageTag };
29653
+ }
29654
+ async function writeComposeFile(opts) {
29655
+ const { content, imageTag, previous, previousImageTag } = await computeComposeContent(opts);
29656
+ const operatorUid = resolveOperatorUid();
29060
29657
  await mkdir(dirname6(opts.composePath), { recursive: true });
29061
29658
  if (previous !== null) {
29062
29659
  try {
@@ -29089,7 +29686,7 @@ var init_write_compose = __esm(() => {
29089
29686
  });
29090
29687
 
29091
29688
  // src/agents/tmux.ts
29092
- import { execFileSync as execFileSync5 } from "node:child_process";
29689
+ import { execFileSync as execFileSync6 } from "node:child_process";
29093
29690
  function sendAgentInterrupt(opts) {
29094
29691
  const { agentName } = opts;
29095
29692
  const attempts = typeof opts.attempts === "number" && opts.attempts > 0 ? opts.attempts : 1;
@@ -29099,7 +29696,7 @@ function sendAgentInterrupt(opts) {
29099
29696
  let lastError = null;
29100
29697
  for (let i = 0;i < attempts; i++) {
29101
29698
  try {
29102
- execFileSync5("tmux", args, {
29699
+ execFileSync6("tmux", args, {
29103
29700
  timeout: 3000,
29104
29701
  stdio: ["ignore", "pipe", "pipe"]
29105
29702
  });
@@ -29140,7 +29737,7 @@ var init_compose_env = () => {};
29140
29737
  import { resolve as resolve13 } from "node:path";
29141
29738
  import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync6 } from "node:fs";
29142
29739
  import { homedir as homedir7 } from "node:os";
29143
- import { execFileSync as execFileSync6 } from "node:child_process";
29740
+ import { execFileSync as execFileSync7 } from "node:child_process";
29144
29741
  function resolveSwitchroomHome(explicit) {
29145
29742
  if (explicit && explicit.length > 0)
29146
29743
  return explicit;
@@ -29171,7 +29768,7 @@ function bringUpAgentService(opts) {
29171
29768
  const dockerBin = opts.dockerBin ?? "docker";
29172
29769
  const stdio = opts.stdio ?? "inherit";
29173
29770
  for (const svc of ["vault-broker", "approval-kernel", "switchroom-auth-broker"]) {
29174
- execFileSync6(dockerBin, [
29771
+ execFileSync7(dockerBin, [
29175
29772
  "compose",
29176
29773
  "-f",
29177
29774
  composePath,
@@ -29183,7 +29780,7 @@ function bringUpAgentService(opts) {
29183
29780
  svc
29184
29781
  ], { stdio });
29185
29782
  }
29186
- execFileSync6(dockerBin, [
29783
+ execFileSync7(dockerBin, [
29187
29784
  "compose",
29188
29785
  "-f",
29189
29786
  composePath,
@@ -29202,7 +29799,7 @@ var init_docker_fleet = __esm(() => {
29202
29799
  });
29203
29800
 
29204
29801
  // src/agents/singleton-reconcile.ts
29205
- import { execFileSync as execFileSync7 } from "node:child_process";
29802
+ import { execFileSync as execFileSync8 } from "node:child_process";
29206
29803
  import { readFileSync as readFileSync16 } from "node:fs";
29207
29804
  function resolveSingletonServices(voiceEngine) {
29208
29805
  const engine = voiceEngine ?? loadHostCapabilities()?.voice.engine ?? "cloud";
@@ -29228,7 +29825,7 @@ function readPinnedSingletonImages(composeText, services = SINGLETON_SERVICES) {
29228
29825
  }
29229
29826
  function defaultInspectImage(dockerBin, container) {
29230
29827
  try {
29231
- const out = execFileSync7(dockerBin, ["inspect", "-f", "{{.Config.Image}}", container], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 }).trim();
29828
+ const out = execFileSync8(dockerBin, ["inspect", "-f", "{{.Config.Image}}", container], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 }).trim();
29232
29829
  return out.length > 0 ? out : null;
29233
29830
  } catch {
29234
29831
  return null;
@@ -29254,7 +29851,7 @@ function detectSingletonDrift(deps) {
29254
29851
  });
29255
29852
  }
29256
29853
  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 });
29854
+ execFileSync8(dockerBin, ["compose", "-p", project, "-f", composeFile, ...composeEnvFileArgs(composeFile), "up", "-d", "--no-deps", service], { stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
29258
29855
  }
29259
29856
  function reconcileSingletons(deps) {
29260
29857
  const dockerBin = deps.dockerBin ?? "docker";
@@ -29297,8 +29894,8 @@ var init_singleton_reconcile = __esm(() => {
29297
29894
  });
29298
29895
 
29299
29896
  // 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";
29897
+ import { execFileSync as execFileSync9, spawn, spawnSync } from "node:child_process";
29898
+ import { existsSync as existsSync20, mkdirSync as mkdirSync14, writeFileSync as writeFileSync7, renameSync as renameSync5, readFileSync as readFileSync17 } from "node:fs";
29302
29899
  import { resolve as resolve14, join as join14 } from "node:path";
29303
29900
  import { connect } from "node:net";
29304
29901
  function cleanShutdownMarkerPathForAgent(name) {
@@ -29320,7 +29917,7 @@ function writeRestartReasonMarker(name, reason, opts = {}) {
29320
29917
  const marker = { ts: Date.now(), signal: "SIGTERM", reason };
29321
29918
  const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
29322
29919
  writeFileSync7(tmp, JSON.stringify(marker), "utf-8");
29323
- renameSync4(tmp, path);
29920
+ renameSync5(tmp, path);
29324
29921
  } catch {}
29325
29922
  }
29326
29923
  function buildCliRestartReason(opts) {
@@ -29328,7 +29925,7 @@ function buildCliRestartReason(opts) {
29328
29925
  if (!buildCommit)
29329
29926
  return "cli: restart";
29330
29927
  try {
29331
- const head = execFileSync8("git", ["rev-parse", "HEAD"], {
29928
+ const head = execFileSync9("git", ["rev-parse", "HEAD"], {
29332
29929
  cwd: cwd ?? process.cwd(),
29333
29930
  encoding: "utf-8",
29334
29931
  stdio: ["ignore", "pipe", "ignore"]
@@ -29339,7 +29936,7 @@ function buildCliRestartReason(opts) {
29339
29936
  return "cli: restart";
29340
29937
  let subject = "";
29341
29938
  try {
29342
- subject = execFileSync8("git", ["log", "-1", "--pretty=%s", head], {
29939
+ subject = execFileSync9("git", ["log", "-1", "--pretty=%s", head], {
29343
29940
  cwd: cwd ?? process.cwd(),
29344
29941
  encoding: "utf-8",
29345
29942
  stdio: ["ignore", "pipe", "ignore"]
@@ -29369,7 +29966,7 @@ function reconcileSingletonImages(log) {
29369
29966
  }
29370
29967
  function dockerSync(args) {
29371
29968
  try {
29372
- return execFileSync8("docker", args, {
29969
+ return execFileSync9("docker", args, {
29373
29970
  encoding: "utf-8",
29374
29971
  stdio: ["ignore", "pipe", "pipe"]
29375
29972
  }).trim();
@@ -29758,12 +30355,12 @@ var init_lifecycle = __esm(() => {
29758
30355
  });
29759
30356
 
29760
30357
  // src/auth/pane-ready-probe.ts
29761
- import { execFileSync as execFileSync10 } from "node:child_process";
30358
+ import { execFileSync as execFileSync11 } from "node:child_process";
29762
30359
  function defaultPaneReadyDeps() {
29763
30360
  return {
29764
30361
  capturePane(sessionName) {
29765
30362
  try {
29766
- return execFileSync10("tmux", ["capture-pane", "-p", "-t", sessionName, "-S", "-200"], {
30363
+ return execFileSync11("tmux", ["capture-pane", "-p", "-t", sessionName, "-S", "-200"], {
29767
30364
  encoding: "utf-8",
29768
30365
  stdio: ["pipe", "pipe", "pipe"]
29769
30366
  }).trim();
@@ -29805,12 +30402,12 @@ import {
29805
30402
  mkdirSync as mkdirSync16,
29806
30403
  readFileSync as readFileSync21,
29807
30404
  readdirSync as readdirSync10,
29808
- renameSync as renameSync6,
29809
- rmSync as rmSync4,
30405
+ renameSync as renameSync7,
30406
+ rmSync as rmSync5,
29810
30407
  statSync as statSync12,
29811
30408
  writeFileSync as writeFileSync9
29812
30409
  } from "node:fs";
29813
- import { randomBytes as randomBytes2 } from "node:crypto";
30410
+ import { randomBytes as randomBytes3 } from "node:crypto";
29814
30411
  import { join as join17 } from "node:path";
29815
30412
  function claudeDir(agentDir) {
29816
30413
  return join17(agentDir, ".claude");
@@ -29944,13 +30541,13 @@ function syncLegacyFromActive(agentDir) {
29944
30541
  }
29945
30542
  function atomicCopy(src, dest, mode) {
29946
30543
  const contents = readFileSync21(src);
29947
- const tmp = `${dest}.tmp-${process.pid}-${randomBytes2(4).toString("hex")}`;
30544
+ const tmp = `${dest}.tmp-${process.pid}-${randomBytes3(4).toString("hex")}`;
29948
30545
  try {
29949
30546
  writeFileSync9(tmp, contents, { mode });
29950
- renameSync6(tmp, dest);
30547
+ renameSync7(tmp, dest);
29951
30548
  } catch (err) {
29952
30549
  try {
29953
- rmSync4(tmp);
30550
+ rmSync5(tmp);
29954
30551
  } catch {}
29955
30552
  throw err;
29956
30553
  }
@@ -30044,7 +30641,7 @@ var init_accounts = __esm(() => {
30044
30641
  });
30045
30642
 
30046
30643
  // src/auth/manager.ts
30047
- import { execFileSync as execFileSync11 } from "node:child_process";
30644
+ import { execFileSync as execFileSync12 } from "node:child_process";
30048
30645
  import {
30049
30646
  readFileSync as readFileSync22,
30050
30647
  readdirSync as readdirSync11,
@@ -30052,7 +30649,7 @@ import {
30052
30649
  writeFileSync as writeFileSync10,
30053
30650
  mkdirSync as mkdirSync17,
30054
30651
  mkdtempSync as mkdtempSync2,
30055
- rmSync as rmSync5,
30652
+ rmSync as rmSync6,
30056
30653
  chmodSync as chmodSync3,
30057
30654
  statSync as statSync13
30058
30655
  } from "node:fs";
@@ -30095,14 +30692,14 @@ function authSessionName(name, slot) {
30095
30692
  return `${base}-${slot.replace(/[^a-zA-Z0-9_.-]/g, "-")}`;
30096
30693
  }
30097
30694
  function tmux(args) {
30098
- return execFileSync11("tmux", args, {
30695
+ return execFileSync12("tmux", args, {
30099
30696
  encoding: "utf-8",
30100
30697
  stdio: ["pipe", "pipe", "pipe"]
30101
30698
  }).trim();
30102
30699
  }
30103
30700
  function tmuxSessionExists(sessionName) {
30104
30701
  try {
30105
- execFileSync11("tmux", ["has-session", "-t", sessionName], {
30702
+ execFileSync12("tmux", ["has-session", "-t", sessionName], {
30106
30703
  stdio: ["pipe", "pipe", "pipe"]
30107
30704
  });
30108
30705
  return true;
@@ -30180,7 +30777,7 @@ function writeAuthSessionMeta(agentDir, meta) {
30180
30777
  });
30181
30778
  }
30182
30779
  function clearAuthSessionMeta(agentDir) {
30183
- rmSync5(authSessionMetaPath(agentDir), { force: true });
30780
+ rmSync6(authSessionMetaPath(agentDir), { force: true });
30184
30781
  }
30185
30782
  function isSessionStale(agentDir, sessionName) {
30186
30783
  const meta = readJsonFile(authSessionMetaPath(agentDir));
@@ -30206,7 +30803,7 @@ function cleanupAuthTempDirs(agentDir) {
30206
30803
  try {
30207
30804
  for (const entry of readdirSync11(dir)) {
30208
30805
  if (entry.startsWith(".setup-token-tmp-")) {
30209
- rmSync5(join18(dir, entry), { recursive: true, force: true });
30806
+ rmSync6(join18(dir, entry), { recursive: true, force: true });
30210
30807
  }
30211
30808
  }
30212
30809
  } catch {}
@@ -30364,7 +30961,7 @@ function startAuthSession(name, agentDir, opts = {}) {
30364
30961
  mkdirSync17(claudeDir2(agentDir), { recursive: true });
30365
30962
  cleanupAuthTempDirs(agentDir);
30366
30963
  const logPath = authLogPath(agentDir);
30367
- rmSync5(logPath, { force: true });
30964
+ rmSync6(logPath, { force: true });
30368
30965
  let configDir;
30369
30966
  if (opts.force) {
30370
30967
  configDir = mkdtempSync2(join18(claudeDir2(agentDir), ".setup-token-tmp-"));
@@ -30520,8 +31117,8 @@ function submitAuthCode(name, agentDir, code, slot, _opts = {}) {
30520
31117
  } catch {}
30521
31118
  clearAuthSessionMeta(agentDir);
30522
31119
  cleanupAuthTempDirs(agentDir);
30523
- rmSync5(authLogPath(agentDir), { force: true });
30524
- rmSync5(credentialsPath(agentDir), { force: true });
31120
+ rmSync6(authLogPath(agentDir), { force: true });
31121
+ rmSync6(credentialsPath(agentDir), { force: true });
30525
31122
  const outcome = { kind: "success" };
30526
31123
  return {
30527
31124
  completed: true,
@@ -30652,7 +31249,7 @@ __export(exports_inject, {
30652
31249
  INJECT_BLOCKED: () => INJECT_BLOCKED,
30653
31250
  INJECT_ALLOWLIST: () => INJECT_ALLOWLIST
30654
31251
  });
30655
- import { execFileSync as execFileSync13 } from "node:child_process";
31252
+ import { execFileSync as execFileSync14 } from "node:child_process";
30656
31253
  function validateInjectCommand(command) {
30657
31254
  if (typeof command !== "string" || command.trim().length === 0) {
30658
31255
  throw new InjectError("invalid", "command is empty");
@@ -30698,7 +31295,7 @@ function makeTmuxRunner(tmuxBin) {
30698
31295
  return {
30699
31296
  capture(socket, session) {
30700
31297
  try {
30701
- return execFileSync13(tmuxBin, ["-L", socket, "capture-pane", "-p", "-t", session, "-S", "-200"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
31298
+ return execFileSync14(tmuxBin, ["-L", socket, "capture-pane", "-p", "-t", session, "-S", "-200"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
30702
31299
  } catch {
30703
31300
  return null;
30704
31301
  }
@@ -30708,11 +31305,11 @@ function makeTmuxRunner(tmuxBin) {
30708
31305
  const flagEnd = rest.findIndex((a) => !a.startsWith("-"));
30709
31306
  const flagsBeforeKeys = flagEnd === -1 ? rest : rest.slice(0, flagEnd);
30710
31307
  const keys = flagEnd === -1 ? [] : rest.slice(flagEnd);
30711
- execFileSync13(tmuxBin, ["-L", socket, subcmd, ...flagsBeforeKeys, "-t", session, ...keys], { stdio: ["pipe", "pipe", "pipe"] });
31308
+ execFileSync14(tmuxBin, ["-L", socket, subcmd, ...flagsBeforeKeys, "-t", session, ...keys], { stdio: ["pipe", "pipe", "pipe"] });
30712
31309
  },
30713
31310
  hasSession(socket, session) {
30714
31311
  try {
30715
- execFileSync13(tmuxBin, ["-L", socket, "has-session", "-t", session], {
31312
+ execFileSync14(tmuxBin, ["-L", socket, "has-session", "-t", session], {
30716
31313
  stdio: ["pipe", "pipe", "pipe"]
30717
31314
  });
30718
31315
  return true;
@@ -31607,8 +32204,8 @@ import {
31607
32204
  mkdirSync as mkdirSync19,
31608
32205
  readFileSync as readFileSync26,
31609
32206
  readdirSync as readdirSync15,
31610
- renameSync as renameSync7,
31611
- rmSync as rmSync10,
32207
+ renameSync as renameSync8,
32208
+ rmSync as rmSync11,
31612
32209
  statSync as statSync16,
31613
32210
  writeFileSync as writeFileSync14
31614
32211
  } from "node:fs";
@@ -31737,73 +32334,6 @@ var init_google_workspace_yaml = __esm(() => {
31737
32334
  import_yaml7 = __toESM(require_dist(), 1);
31738
32335
  });
31739
32336
 
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
32337
  // src/drive/oauth.ts
31808
32338
  var exports_oauth = {};
31809
32339
  __export(exports_oauth, {
@@ -33536,7 +34066,7 @@ __export(exports_via_claude, {
33536
34066
  PRE_PASTE_RULES: () => PRE_PASTE_RULES,
33537
34067
  POST_PASTE_RULES: () => POST_PASTE_RULES
33538
34068
  });
33539
- import { execFileSync as execFileSync14, spawnSync as spawnSync3 } from "node:child_process";
34069
+ import { execFileSync as execFileSync15, spawnSync as spawnSync3 } from "node:child_process";
33540
34070
  import { existsSync as existsSync34, mkdirSync as mkdirSync20, readFileSync as readFileSync29 } from "node:fs";
33541
34071
  import { join as join26, resolve as resolve24 } from "node:path";
33542
34072
  function tmuxHasSession(session) {
@@ -33547,7 +34077,7 @@ function tmuxHasSession(session) {
33547
34077
  }
33548
34078
  function tmuxKillSession(session) {
33549
34079
  try {
33550
- execFileSync14("tmux", ["kill-session", "-t", session], {
34080
+ execFileSync15("tmux", ["kill-session", "-t", session], {
33551
34081
  stdio: "ignore",
33552
34082
  timeout: 3000
33553
34083
  });
@@ -33555,7 +34085,7 @@ function tmuxKillSession(session) {
33555
34085
  }
33556
34086
  function tmuxCapturePane(session) {
33557
34087
  try {
33558
- const out = execFileSync14("tmux", ["capture-pane", "-p", "-t", session, "-S", "-200"], { timeout: 3000, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 4 * 1024 * 1024 });
34088
+ const out = execFileSync15("tmux", ["capture-pane", "-p", "-t", session, "-S", "-200"], { timeout: 3000, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 4 * 1024 * 1024 });
33559
34089
  return out.toString("utf8");
33560
34090
  } catch {
33561
34091
  return "";
@@ -33563,7 +34093,7 @@ function tmuxCapturePane(session) {
33563
34093
  }
33564
34094
  function tmuxSendKeys(session, keys, literal = false) {
33565
34095
  try {
33566
- execFileSync14("tmux", ["send-keys", "-t", session, ...literal ? ["-l"] : [], ...keys], { timeout: 3000, stdio: ["ignore", "pipe", "pipe"] });
34096
+ execFileSync15("tmux", ["send-keys", "-t", session, ...literal ? ["-l"] : [], ...keys], { timeout: 3000, stdio: ["ignore", "pipe", "pipe"] });
33567
34097
  } catch {}
33568
34098
  }
33569
34099
  function extractAuthorizeUrl(pane) {
@@ -33592,7 +34122,7 @@ async function runViaClaude(opts) {
33592
34122
  tmuxKillSession(SESSION);
33593
34123
  }
33594
34124
  const spawn4 = opts.spawnClaude ?? (() => {
33595
- execFileSync14("tmux", [
34125
+ execFileSync15("tmux", [
33596
34126
  "new-session",
33597
34127
  "-d",
33598
34128
  "-s",
@@ -39970,6 +40500,30 @@ function classifyShmSize(bytes) {
39970
40500
  const gib = (bytes / 1024 / 1024 / 1024).toFixed(bytes % 1024 ** 3 === 0 ? 0 : 1);
39971
40501
  return { name: "hindsight shm-size", status: "ok", detail: `${gib}g` };
39972
40502
  }
40503
+ function classifyConsolidationBacklog(queueDepth, oldestAgeSecs = null) {
40504
+ const ageStr = oldestAgeSecs !== null && oldestAgeSecs > 0 ? `, oldest ${Math.round(oldestAgeSecs / 60)}m old` : "";
40505
+ const base = `${queueDepth} pending consolidation op(s)${ageStr}`;
40506
+ if (queueDepth >= CONSOLIDATION_BACKLOG_FAIL) {
40507
+ return {
40508
+ name: "hindsight consolidation backlog",
40509
+ status: "fail",
40510
+ 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)`,
40511
+ 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."
40512
+ };
40513
+ }
40514
+ if (queueDepth >= CONSOLIDATION_BACKLOG_WARN) {
40515
+ return {
40516
+ name: "hindsight consolidation backlog",
40517
+ status: "warn",
40518
+ detail: `${base} \u2014 building faster than it drains (~7 ops/hr under the #2894 ` + `throttle); recall may lag recent turns until it catches up`
40519
+ };
40520
+ }
40521
+ return {
40522
+ name: "hindsight consolidation backlog",
40523
+ status: "ok",
40524
+ detail: queueDepth > 0 ? base : "queue drained (0 pending)"
40525
+ };
40526
+ }
39973
40527
  function classifyExtractionLogs(logs) {
39974
40528
  const noSpace = /No space left on device|could not resize shared memory/i.test(logs);
39975
40529
  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 +40580,49 @@ function checkHindsightContainerHealth(opts) {
40026
40580
  } catch {}
40027
40581
  return results;
40028
40582
  }
40583
+ function classifyHindsightHealthProbe(status, port) {
40584
+ if (status === 200) {
40585
+ return {
40586
+ name: "hindsight /health",
40587
+ status: "ok",
40588
+ detail: `200 at :${port} \u2014 memory backend live`
40589
+ };
40590
+ }
40591
+ if (status === null) {
40592
+ return {
40593
+ name: "hindsight /health",
40594
+ status: "fail",
40595
+ 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)`,
40596
+ 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."
40597
+ };
40598
+ }
40599
+ return {
40600
+ name: "hindsight /health",
40601
+ status: "fail",
40602
+ detail: `HTTP ${status} at :${port} \u2014 memory backend unhealthy`,
40603
+ fix: "Inspect `docker logs switchroom-hindsight`; restart with `switchroom memory --restart`."
40604
+ };
40605
+ }
40606
+ async function checkHindsightHealthEndpoint(mcpUrl, opts = {}) {
40607
+ const doFetch = opts.fetchImpl ?? fetch;
40608
+ const timeoutMs = opts.timeoutMs ?? 4000;
40609
+ const origin = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
40610
+ const healthUrl = `${origin}/health`;
40611
+ const port = (() => {
40612
+ const m = origin.match(/:(\d+)(?:$|\/)/);
40613
+ return m ? parseInt(m[1], 10) : 80;
40614
+ })();
40615
+ const controller = new AbortController;
40616
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
40617
+ try {
40618
+ const res = await doFetch(healthUrl, { signal: controller.signal });
40619
+ return classifyHindsightHealthProbe(res.status, port);
40620
+ } catch {
40621
+ return classifyHindsightHealthProbe(null, port);
40622
+ } finally {
40623
+ clearTimeout(timer);
40624
+ }
40625
+ }
40029
40626
  function classifyToolContract(advertised) {
40030
40627
  const byName = new Map(advertised.map((t) => [t.name, t]));
40031
40628
  const results = [];
@@ -40068,7 +40665,7 @@ function classifyToolContract(advertised) {
40068
40665
  }
40069
40666
  return results;
40070
40667
  }
40071
- var MIN_HINDSIGHT_SHM_BYTES;
40668
+ var MIN_HINDSIGHT_SHM_BYTES, CONSOLIDATION_BACKLOG_WARN = 25, CONSOLIDATION_BACKLOG_FAIL = 200;
40072
40669
  var init_doctor_memory = __esm(() => {
40073
40670
  init_hindsight_tools();
40074
40671
  MIN_HINDSIGHT_SHM_BYTES = 1024 * 1024 * 1024;
@@ -43474,6 +44071,19 @@ async function checkBankIngestHealth(config, url, opts) {
43474
44071
  banks.set(bank, []);
43475
44072
  }
43476
44073
  const inspected = await Promise.all([...banks].map(async ([bankId, agents]) => [bankId, agents, await inspectBankHealth(url, bankId, { fetchImpl: opts?.fetchImpl })]));
44074
+ let backlogRow;
44075
+ if (opts?.includeConsolidationBacklog) {
44076
+ let totalPending = 0;
44077
+ let anyBankReachable = false;
44078
+ for (const [, , h] of inspected) {
44079
+ if (h.ok) {
44080
+ anyBankReachable = true;
44081
+ totalPending += h.pendingOperations;
44082
+ }
44083
+ }
44084
+ if (anyBankReachable)
44085
+ backlogRow = classifyConsolidationBacklog(totalPending);
44086
+ }
43477
44087
  for (const [bankId, agents, h] of inspected) {
43478
44088
  const label = `bank ${bankId}` + (profileBanks.has(bankId) ? " (profile)" : agents[0] !== bankId ? ` (${agents.join(", ")})` : "");
43479
44089
  if (!h.ok) {
@@ -43535,6 +44145,8 @@ async function checkBankIngestHealth(config, url, opts) {
43535
44145
  }
43536
44146
  results.push({ name: label, status: "ok", detail: summary });
43537
44147
  }
44148
+ if (backlogRow)
44149
+ results.push(backlogRow);
43538
44150
  return results;
43539
44151
  }
43540
44152
  function hindsightDocReprocessUrl(mcpUrl, bankId, docId) {
@@ -43545,7 +44157,7 @@ async function checkHindsight(config) {
43545
44157
  if (!isHindsightEnabled(config)) {
43546
44158
  return [];
43547
44159
  }
43548
- const url = config.memory?.config?.url ?? "http://localhost:8888/mcp/";
44160
+ const url = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
43549
44161
  const results = [];
43550
44162
  const match = url.match(/^https?:\/\/([^:/]+):?(\d+)?/);
43551
44163
  if (!match) {
@@ -43591,7 +44203,8 @@ async function checkHindsight(config) {
43591
44203
  }
43592
44204
  results.push(checkHindsightConsumer(config));
43593
44205
  results.push(...checkHindsightContainerHealth());
43594
- results.push(...await checkBankIngestHealth(config, url));
44206
+ results.push(await checkHindsightHealthEndpoint(url));
44207
+ results.push(...await checkBankIngestHealth(config, url, { includeConsolidationBacklog: true }));
43595
44208
  for (const [agentName, agentConfig] of Object.entries(config.agents)) {
43596
44209
  const bankId = agentConfig.memory?.collection ?? agentName;
43597
44210
  const hasBankMission = !!agentConfig.memory?.bank_mission;
@@ -44745,6 +45358,7 @@ var init_doctor = __esm(() => {
44745
45358
  init_manager();
44746
45359
  init_accounts();
44747
45360
  init_manifest();
45361
+ init_hindsight2();
44748
45362
  init_hindsight();
44749
45363
  init_doctor_memory();
44750
45364
  init_doctor_docker();
@@ -48352,12 +48966,15 @@ var exports_apply = {};
48352
48966
  __export(exports_apply, {
48353
48967
  writeInstallTypeCache: () => writeInstallTypeCache,
48354
48968
  runApplyPreflight: () => runApplyPreflight,
48969
+ runApplyDryRun: () => runApplyDryRun,
48355
48970
  runApply: () => runApply,
48356
48971
  resolveVaultBindMountDir: () => resolveVaultBindMountDir,
48357
48972
  resolveOperatorVaultPassphrase: () => resolveOperatorVaultPassphrase,
48358
48973
  registerApplyCommand: () => registerApplyCommand,
48359
48974
  reexecUnderSudo: () => reexecUnderSudo,
48360
48975
  provisionLiteLLMKeys: () => provisionLiteLLMKeys,
48976
+ probeVaultProvisioning: () => probeVaultProvisioning,
48977
+ parseComposeServiceNames: () => parseComposeServiceNames,
48361
48978
  isInAgentContainer: () => isInAgentContainer,
48362
48979
  inspectVaultBindMountDir: () => inspectVaultBindMountDir,
48363
48980
  formatScaffoldFailureResolution: () => formatScaffoldFailureResolution,
@@ -49040,6 +49657,186 @@ Done. Scaffolded ${scaffolded}/${allAgentNames.length} agents.
49040
49657
  failures
49041
49658
  };
49042
49659
  }
49660
+ async function probeVaultProvisioning(config, agentNames, home2) {
49661
+ const optedIn = [];
49662
+ for (const name of agentNames) {
49663
+ const agentConfig = config.agents[name];
49664
+ if (!agentConfig)
49665
+ continue;
49666
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
49667
+ if (effectiveLiteLLMEnabled(config, resolved.litellm))
49668
+ optedIn.push(name);
49669
+ }
49670
+ const needsHindsight = config.litellm?.enabled === true && config.memory?.backend === "hindsight";
49671
+ const passphraseAvailable = await resolveOperatorVaultPassphrase(home2) !== null;
49672
+ const alreadyProvisioned = [];
49673
+ const unprovisionedNew = [];
49674
+ const brokerUnreachable = [];
49675
+ if (optedIn.length > 0) {
49676
+ const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
49677
+ for (const name of optedIn) {
49678
+ const existing = await getViaBrokerStructured2(`litellm/${name}/api-key`);
49679
+ if (existing.kind === "ok")
49680
+ alreadyProvisioned.push(name);
49681
+ else if (existing.kind === "unreachable")
49682
+ brokerUnreachable.push(name);
49683
+ else
49684
+ unprovisionedNew.push(name);
49685
+ }
49686
+ }
49687
+ return {
49688
+ optedInCount: optedIn.length,
49689
+ passphraseAvailable,
49690
+ alreadyProvisioned,
49691
+ unprovisionedNew,
49692
+ brokerUnreachable,
49693
+ needsHindsight
49694
+ };
49695
+ }
49696
+ function parseComposeServiceNames(compose) {
49697
+ const lines = compose.split(`
49698
+ `);
49699
+ const names = [];
49700
+ let inServices = false;
49701
+ for (const line of lines) {
49702
+ if (/^services:\s*$/.test(line)) {
49703
+ inServices = true;
49704
+ continue;
49705
+ }
49706
+ if (inServices && /^\S/.test(line))
49707
+ break;
49708
+ if (!inServices)
49709
+ continue;
49710
+ const m = /^ {2}([A-Za-z0-9_.-]+):\s*$/.exec(line);
49711
+ if (m)
49712
+ names.push(m[1]);
49713
+ }
49714
+ return names;
49715
+ }
49716
+ async function runApplyDryRun(config, options, deps = {}, switchroomConfigPath) {
49717
+ const writeOut = deps.writeOut ?? ((s) => process.stdout.write(s));
49718
+ const writeErr = deps.writeErr ?? ((s) => process.stderr.write(s));
49719
+ const blocking = [];
49720
+ const warnings = [];
49721
+ const info = [];
49722
+ writeOut(source_default.bold(`
49723
+ Dry-run: validating switchroom config (no changes will be written)...
49724
+ `));
49725
+ try {
49726
+ runApplyPreflight(config, { detectComposeV2: deps.detectComposeV2 });
49727
+ } catch (err) {
49728
+ blocking.push(`preflight: ${err.message}`);
49729
+ }
49730
+ const allAgentNames = Object.keys(config.agents ?? {});
49731
+ const agentNames = options.only !== undefined ? [options.only] : allAgentNames;
49732
+ if (options.only !== undefined && !allAgentNames.includes(options.only)) {
49733
+ blocking.push(`apply --only=${options.only}: no such agent in switchroom.yaml (defined: ${allAgentNames.join(", ") || "none"}).`);
49734
+ }
49735
+ for (const name of agentNames) {
49736
+ const agentConfig = config.agents[name];
49737
+ if (!agentConfig)
49738
+ continue;
49739
+ const profileName = agentConfig.extends ?? "default";
49740
+ try {
49741
+ getProfilePath(profileName);
49742
+ resolveAgentConfig(config.defaults, config.profiles, agentConfig);
49743
+ } catch (err) {
49744
+ blocking.push(`profile '${profileName}' for agent '${name}': ${err.message}`);
49745
+ }
49746
+ }
49747
+ try {
49748
+ const probe2 = await probeVaultProvisioning(config, agentNames, homedir47());
49749
+ if (probe2.alreadyProvisioned.length > 0) {
49750
+ info.push(`litellm: ${probe2.alreadyProvisioned.length} agent(s) already provisioned (${probe2.alreadyProvisioned.join(", ")}).`);
49751
+ }
49752
+ if (probe2.brokerUnreachable.length > 0) {
49753
+ 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.`);
49754
+ }
49755
+ const pending = [
49756
+ ...probe2.unprovisionedNew,
49757
+ ...probe2.needsHindsight ? ["hindsight (service)"] : []
49758
+ ];
49759
+ if (pending.length > 0) {
49760
+ if (probe2.passphraseAvailable) {
49761
+ info.push(`litellm: would provision ${pending.length} new virtual key(s) (${pending.join(", ")}) \u2014 vault passphrase is available.`);
49762
+ } else {
49763
+ 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.`);
49764
+ }
49765
+ }
49766
+ } catch (err) {
49767
+ warnings.push(`litellm: provisioning probe failed \u2014 ${err.message}`);
49768
+ }
49769
+ let imageTag = null;
49770
+ let previousImageTag = null;
49771
+ let composeChanged = false;
49772
+ let addedServices = [];
49773
+ let removedServices = [];
49774
+ try {
49775
+ const computed = await computeComposeContent({
49776
+ config,
49777
+ composePath: options.outPath ?? DEFAULT_COMPOSE_PATH2,
49778
+ switchroomConfigPath,
49779
+ releaseOverride: options.releaseOverride,
49780
+ buildMode: options.buildLocal ? "local" : "pull",
49781
+ buildContext: options.buildContext
49782
+ });
49783
+ imageTag = computed.imageTag;
49784
+ previousImageTag = computed.previousImageTag;
49785
+ composeChanged = computed.previous !== computed.content;
49786
+ const newServices = parseComposeServiceNames(computed.content);
49787
+ const oldServices = computed.previous ? parseComposeServiceNames(computed.previous) : [];
49788
+ addedServices = newServices.filter((s) => !oldServices.includes(s));
49789
+ removedServices = oldServices.filter((s) => !newServices.includes(s));
49790
+ if (previousImageTag === null) {
49791
+ info.push(`compose: no existing compose on disk \u2014 a real apply would create it.`);
49792
+ } else if (previousImageTag !== imageTag) {
49793
+ info.push(`compose: agent image would change ${previousImageTag} \u2192 ${imageTag}.`);
49794
+ } else {
49795
+ info.push(`compose: agent image unchanged (${imageTag}).`);
49796
+ }
49797
+ if (addedServices.length > 0) {
49798
+ info.push(`compose: services added: ${addedServices.join(", ")}.`);
49799
+ }
49800
+ if (removedServices.length > 0) {
49801
+ info.push(`compose: services removed: ${removedServices.join(", ")}.`);
49802
+ }
49803
+ if (previousImageTag !== null && !composeChanged) {
49804
+ info.push(`compose: no change (would be byte-identical to the current file).`);
49805
+ }
49806
+ } catch (err) {
49807
+ blocking.push(`compose generation: ${err.message}`);
49808
+ }
49809
+ for (const line of info)
49810
+ writeOut(source_default.gray(` ~ ${line}
49811
+ `));
49812
+ for (const line of warnings)
49813
+ writeErr(source_default.yellow(` ! ${line}
49814
+ `));
49815
+ for (const line of blocking)
49816
+ writeErr(source_default.red(` x ${line}
49817
+ `));
49818
+ const wouldSucceed = blocking.length === 0;
49819
+ writeOut(`
49820
+ `);
49821
+ if (wouldSucceed) {
49822
+ writeOut(source_default.green.bold(`Dry-run: a real apply would SUCCEED. ${allAgentNames.length} agent(s) validated; nothing was written.
49823
+ `));
49824
+ } else {
49825
+ writeErr(source_default.red.bold(`Dry-run: a real apply would FAIL \u2014 ${blocking.length} blocking condition(s) above. Nothing was written.
49826
+ `));
49827
+ }
49828
+ return {
49829
+ blocking,
49830
+ warnings,
49831
+ info,
49832
+ imageTag,
49833
+ previousImageTag,
49834
+ composeChanged,
49835
+ addedServices,
49836
+ removedServices,
49837
+ wouldSucceed
49838
+ };
49839
+ }
49043
49840
  function formatScaffoldFailureResolution(failures, scaffolded, agentsTotal) {
49044
49841
  const fail3 = failures.length;
49045
49842
  const lines = [];
@@ -49157,7 +49954,7 @@ ERROR: failed to spawn sudo: ${result.error.message}
49157
49954
  process.exit(result.status ?? 1);
49158
49955
  }
49159
49956
  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) => {
49957
+ 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
49958
  try {
49162
49959
  if (opts.example) {
49163
49960
  copyExampleConfig2(opts.example);
@@ -49171,6 +49968,23 @@ function registerApplyCommand(program3) {
49171
49968
  `);
49172
49969
  process.exit(0);
49173
49970
  }
49971
+ if (opts.dryRun) {
49972
+ const dry = await runApplyDryRun(config, {
49973
+ buildLocal: !!opts.buildLocal,
49974
+ buildContext: typeof opts.buildLocal === "string" ? opts.buildLocal : process.cwd(),
49975
+ outPath: opts.out,
49976
+ only: opts.only,
49977
+ releaseOverride: opts.channel ? { channel: opts.channel } : opts.pin ? { pin: opts.pin } : undefined,
49978
+ dryRun: true
49979
+ }, {}, switchroomConfigPath);
49980
+ await captureEvent("apply_dry_run", {
49981
+ agents_total: Object.keys(config.agents ?? {}).length,
49982
+ would_succeed: dry.wouldSucceed,
49983
+ blocking: dry.blocking.length,
49984
+ warnings: dry.warnings.length
49985
+ });
49986
+ process.exit(dry.wouldSucceed ? 0 : 1);
49987
+ }
49174
49988
  if (!opts.skipSelfElevate && !opts.composeOnly && process.geteuid?.() !== 0) {
49175
49989
  const unwritable = findUnwritableAgentDirs(config, {
49176
49990
  only: opts.only
@@ -49278,6 +50092,7 @@ var init_apply = __esm(() => {
49278
50092
  init_update_prompt_hook();
49279
50093
  init_compose();
49280
50094
  init_write_compose();
50095
+ init_profiles();
49281
50096
  init_install_detect();
49282
50097
  init_operator_uid();
49283
50098
  init_posthog();
@@ -63737,8 +64552,8 @@ import { existsSync, readFileSync } from "node:fs";
63737
64552
  import { dirname, join } from "node:path";
63738
64553
 
63739
64554
  // src/build-info.ts
63740
- var VERSION = "0.17.6";
63741
- var COMMIT_SHA = "0cc74c23";
64555
+ var VERSION = "0.17.10";
64556
+ var COMMIT_SHA = "d38372bf";
63742
64557
 
63743
64558
  // src/cli/resolve-version.ts
63744
64559
  function readPackageVersion() {
@@ -63769,7 +64584,7 @@ var SWITCHROOM_VERSION = (() => {
63769
64584
  init_esm();
63770
64585
  init_source();
63771
64586
  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";
64587
+ import { rmSync as rmSync10, existsSync as existsSync31, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
63773
64588
  import { homedir as homedir8 } from "node:os";
63774
64589
 
63775
64590
  // src/agents/scheduler-state.ts
@@ -63833,6 +64648,7 @@ function formatSchedulerState(state) {
63833
64648
 
63834
64649
  // src/cli/agent.ts
63835
64650
  init_loader();
64651
+ init_hindsight2();
63836
64652
  init_hindsight();
63837
64653
  init_helpers();
63838
64654
  init_scaffold();
@@ -64067,10 +64883,10 @@ function spinner(message) {
64067
64883
  // src/agents/status.ts
64068
64884
  import { existsSync as existsSync23, readFileSync as readFileSync20, statSync as statSync11 } from "node:fs";
64069
64885
  import { join as join16 } from "node:path";
64070
- import { execFileSync as execFileSync9 } from "node:child_process";
64886
+ import { execFileSync as execFileSync10 } from "node:child_process";
64071
64887
 
64072
64888
  // 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";
64889
+ 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
64890
  import { join as join15 } from "node:path";
64075
64891
  var DEFAULT_MAX_TURNS = 50;
64076
64892
  var TOPIC_MAX_CHARS = 117;
@@ -64212,8 +65028,8 @@ function writeSidecarsAtomic(agentDir, briefing, topic) {
64212
65028
  const topicTmp = topicPath + ".tmp";
64213
65029
  writeFileSync8(handoffTmp, briefing, "utf-8");
64214
65030
  writeFileSync8(topicTmp, topic, "utf-8");
64215
- renameSync5(handoffTmp, handoffPath);
64216
- renameSync5(topicTmp, topicPath);
65031
+ renameSync6(handoffTmp, handoffPath);
65032
+ renameSync6(topicTmp, topicPath);
64217
65033
  }
64218
65034
  async function buildHandoff(opts) {
64219
65035
  const maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
@@ -64744,14 +65560,14 @@ function readLastMessages(historyDbPath) {
64744
65560
  };
64745
65561
  }
64746
65562
  try {
64747
- const out = execFileSync9("bun", [
65563
+ const out = execFileSync10("bun", [
64748
65564
  "-e",
64749
65565
  `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
65566
  ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
64751
65567
  return parseSqliteRoleTsOutput(out);
64752
65568
  } catch {}
64753
65569
  try {
64754
- const out = execFileSync9("sqlite3", [
65570
+ const out = execFileSync10("sqlite3", [
64755
65571
  historyDbPath,
64756
65572
  "SELECT role, MAX(ts) FROM messages GROUP BY role;"
64757
65573
  ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
@@ -64863,7 +65679,7 @@ function escapeRegex(s) {
64863
65679
  }
64864
65680
  function readDockerContainer(containerName2) {
64865
65681
  try {
64866
- const out = execFileSync9("docker", ["inspect", "--format", "{{json .State}}", containerName2], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
65682
+ const out = execFileSync10("docker", ["inspect", "--format", "{{json .State}}", containerName2], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
64867
65683
  const state = JSON.parse(out.trim());
64868
65684
  const active = state.Status === "running" ? "active" : state.Status === "restarting" ? "restarting" : "inactive";
64869
65685
  const pid = typeof state.Pid === "number" && state.Pid > 0 ? state.Pid : null;
@@ -64925,7 +65741,7 @@ init_profiles();
64925
65741
  init_lifecycle();
64926
65742
  init_manager();
64927
65743
  import { resolve as resolve17 } from "node:path";
64928
- import { existsSync as existsSync26, rmSync as rmSync6 } from "node:fs";
65744
+ import { existsSync as existsSync26, rmSync as rmSync7 } from "node:fs";
64929
65745
 
64930
65746
  // src/telegram/redact-token.ts
64931
65747
  var REDACTED_PLACEHOLDER = "<redacted-bot-token>";
@@ -65074,7 +65890,7 @@ async function createAgent(opts) {
65074
65890
  const agentDir = resolve17(agentsDir, name);
65075
65891
  await withRollback(() => {
65076
65892
  scaffoldAgent(name, agentConfig, agentsDir, config.telegram, config, undefined, configPath);
65077
- rollbackStack.push(() => rmSync6(agentDir, { recursive: true, force: true }));
65893
+ rollbackStack.push(() => rmSync7(agentDir, { recursive: true, force: true }));
65078
65894
  });
65079
65895
  await withRollback(() => {
65080
65896
  writeAgentEnv(agentDir, telegramBotToken);
@@ -65141,8 +65957,8 @@ async function completeCreation(name, code, opts = {}) {
65141
65957
 
65142
65958
  // src/agents/add-orchestrator.ts
65143
65959
  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";
65960
+ import { existsSync as existsSync28, rmSync as rmSync8, statSync as statSync15 } from "node:fs";
65961
+ import { execFileSync as execFileSync13 } from "node:child_process";
65146
65962
  init_onboarding();
65147
65963
 
65148
65964
  // src/setup/botfather-walkthrough.ts
@@ -65708,7 +66524,7 @@ function pruneBundledSkills(agentDir, keep, scope) {
65708
66524
  if (!isDir)
65709
66525
  continue;
65710
66526
  try {
65711
- rmSync7(entryPath, { recursive: true, force: true });
66527
+ rmSync8(entryPath, { recursive: true, force: true });
65712
66528
  removed.push(entry);
65713
66529
  } catch {}
65714
66530
  }
@@ -65716,7 +66532,7 @@ function pruneBundledSkills(agentDir, keep, scope) {
65716
66532
  }
65717
66533
  function defaultIsUnitActive(unitName) {
65718
66534
  try {
65719
- const out = execFileSync12("systemctl", ["--user", "is-active", unitName], {
66535
+ const out = execFileSync13("systemctl", ["--user", "is-active", unitName], {
65720
66536
  encoding: "utf-8",
65721
66537
  stdio: ["ignore", "pipe", "pipe"]
65722
66538
  }).trim();
@@ -65743,7 +66559,7 @@ import { resolve as resolve20 } from "node:path";
65743
66559
  import {
65744
66560
  existsSync as existsSync29,
65745
66561
  mkdtempSync as mkdtempSync3,
65746
- rmSync as rmSync8,
66562
+ rmSync as rmSync9,
65747
66563
  cpSync
65748
66564
  } from "node:fs";
65749
66565
  import { tmpdir as tmpdir2 } from "node:os";
@@ -65768,7 +66584,7 @@ function defaultDeps() {
65768
66584
  return tmp;
65769
66585
  },
65770
66586
  copyDir: (src, dst) => cpSync(src, dst, { recursive: true }),
65771
- removeDir: (p) => rmSync8(p, { recursive: true, force: true }),
66587
+ removeDir: (p) => rmSync9(p, { recursive: true, force: true }),
65772
66588
  existsSync: existsSync29,
65773
66589
  readFileSync: (p, enc) => readFileSync23(p, enc)
65774
66590
  };
@@ -66137,16 +66953,16 @@ function parsePorcelainDirty(porcelain) {
66137
66953
  }
66138
66954
  function checkSwitchroomBranch() {
66139
66955
  try {
66140
- const { execFileSync: execFileSync14 } = __require("node:child_process");
66956
+ const { execFileSync: execFileSync15 } = __require("node:child_process");
66141
66957
  const cwd = process.cwd();
66142
- const branch = execFileSync14("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], {
66958
+ const branch = execFileSync15("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], {
66143
66959
  encoding: "utf-8",
66144
66960
  stdio: ["ignore", "pipe", "ignore"]
66145
66961
  }).trim();
66146
66962
  if (branch === "" || branch === "HEAD")
66147
66963
  return null;
66148
66964
  if (branch === "main" || branch === "master") {
66149
- const porcelain = execFileSync14("git", ["-C", cwd, "status", "--porcelain"], {
66965
+ const porcelain = execFileSync15("git", ["-C", cwd, "status", "--porcelain"], {
66150
66966
  encoding: "utf-8",
66151
66967
  stdio: ["ignore", "pipe", "ignore"]
66152
66968
  });
@@ -66209,7 +67025,7 @@ function buildStatusInputs(name, config, agentsDir) {
66209
67025
  let hindsightApiUrl = null;
66210
67026
  let hindsightBankId = name;
66211
67027
  if (isHindsightEnabled(config)) {
66212
- const baseUrl = config.memory?.config?.url ?? "http://localhost:8888/mcp/";
67028
+ const baseUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
66213
67029
  hindsightApiUrl = baseUrl.endsWith("/mcp/") ? baseUrl : baseUrl.replace(/\/$/, "") + "/mcp/";
66214
67030
  hindsightBankId = agentConfig?.memory?.collection ?? name;
66215
67031
  }
@@ -66477,7 +67293,7 @@ function registerAgentCommand(program3) {
66477
67293
  let hindsightApiUrl = null;
66478
67294
  let hindsightBankId = name;
66479
67295
  if (isHindsightEnabled(config)) {
66480
- const baseUrl = config.memory?.config?.url ?? "http://localhost:8888/mcp/";
67296
+ const baseUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
66481
67297
  hindsightApiUrl = baseUrl.endsWith("/mcp/") ? baseUrl : baseUrl.replace(/\/$/, "") + "/mcp/";
66482
67298
  hindsightBankId = agentConfig.memory?.collection ?? name;
66483
67299
  }
@@ -67161,7 +67977,7 @@ Permissions for ${name}
67161
67977
  }
67162
67978
  }
67163
67979
  if (existsSync31(agentDir)) {
67164
- rmSync9(agentDir, { recursive: true, force: true });
67980
+ rmSync10(agentDir, { recursive: true, force: true });
67165
67981
  console.log(source_default.green(` Removed directory: ${agentDir}`));
67166
67982
  } else {
67167
67983
  console.log(source_default.gray(` Directory not found: ${agentDir}`));
@@ -67365,7 +68181,7 @@ switchroom agent add: ${name}
67365
68181
  const agentsDirRollback = resolveAgentsDir(cfg);
67366
68182
  const agentDirRollback = resolve22(agentsDirRollback, name);
67367
68183
  if (existsSync31(agentDirRollback)) {
67368
- rmSync9(agentDirRollback, { recursive: true, force: true });
68184
+ rmSync10(agentDirRollback, { recursive: true, force: true });
67369
68185
  console.log(source_default.gray(` Rolled back: removed ${agentDirRollback}`));
67370
68186
  }
67371
68187
  process.exit(1);
@@ -69996,7 +70812,7 @@ function registerAuthCommand(program3) {
69996
70812
  try {
69997
70813
  mode = statSync17(yamlPath).mode & 511;
69998
70814
  } catch {}
69999
- atomicWriteFileSync2(yamlPath, after, mode);
70815
+ atomicWriteFileSync(yamlPath, after, mode);
70000
70816
  if (!quiet) {
70001
70817
  console.log(source_default.gray(` Pinned auth.active: ${label} in ${yamlPath}`));
70002
70818
  }
@@ -70143,9 +70959,9 @@ import { readFileSync as readFileSync39 } from "node:fs";
70143
70959
  init_source();
70144
70960
  init_loader();
70145
70961
  init_vault();
70146
- init_hindsight();
70962
+ init_hindsight2();
70147
70963
  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";
70964
+ import { execFileSync as execFileSync16 } from "node:child_process";
70149
70965
  import { join as join28 } from "node:path";
70150
70966
  import { homedir as homedir13 } from "node:os";
70151
70967
  function getVaultPath2(configPath) {
@@ -70492,7 +71308,7 @@ function registerVaultSweep(vault, program3) {
70492
71308
  function scrubSqlite(path, values, dryRun) {
70493
71309
  let hasSqlite = true;
70494
71310
  try {
70495
- execFileSync15("sqlite3", ["-version"], { timeout: 2000, stdio: "ignore" });
71311
+ execFileSync16("sqlite3", ["-version"], { timeout: 2000, stdio: "ignore" });
70496
71312
  } catch {
70497
71313
  hasSqlite = false;
70498
71314
  }
@@ -70504,7 +71320,7 @@ function scrubSqlite(path, values, dryRun) {
70504
71320
  for (const { value } of values) {
70505
71321
  const esc = value.replace(/'/g, "''");
70506
71322
  try {
70507
- const out = execFileSync15("sqlite3", [path, `SELECT COUNT(*) FROM messages WHERE text LIKE '%${esc}%'`], { encoding: "utf8", timeout: 1e4 });
71323
+ const out = execFileSync16("sqlite3", [path, `SELECT COUNT(*) FROM messages WHERE text LIKE '%${esc}%'`], { encoding: "utf8", timeout: 1e4 });
70508
71324
  const n = parseInt(out.trim(), 10);
70509
71325
  if (!Number.isNaN(n))
70510
71326
  matches += n;
@@ -70518,7 +71334,7 @@ function scrubSqlite(path, values, dryRun) {
70518
71334
  for (const { key, value } of values) {
70519
71335
  const esc = value.replace(/'/g, "''");
70520
71336
  const replacement = `vault:${key}`.replace(/'/g, "''");
70521
- execFileSync15("sqlite3", [path, `UPDATE messages SET text = replace(text, '${esc}', '${replacement}')`], { timeout: 15000 });
71337
+ execFileSync16("sqlite3", [path, `UPDATE messages SET text = replace(text, '${esc}', '${replacement}')`], { timeout: 15000 });
70522
71338
  }
70523
71339
  return { target: path, matches, modified: true };
70524
71340
  } catch (err) {
@@ -77429,10 +78245,10 @@ function fail2(msg) {
77429
78245
 
77430
78246
  // src/cli/memory.ts
77431
78247
  init_source();
77432
- init_hindsight();
78248
+ init_hindsight2();
77433
78249
 
77434
78250
  // src/memory/search.ts
77435
- init_hindsight();
78251
+ init_hindsight2();
77436
78252
  function shellQuote2(s) {
77437
78253
  return "'" + s.replace(/'/g, `'"'"'`) + "'";
77438
78254
  }
@@ -77459,312 +78275,7 @@ function reflectAcrossAgents(config) {
77459
78275
 
77460
78276
  // src/cli/memory.ts
77461
78277
  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
78278
+ init_hindsight();
77768
78279
  init_client();
77769
78280
  init_loader();
77770
78281
  var import_yaml13 = __toESM(require_dist(), 1);
@@ -77984,6 +78495,21 @@ Cross-agent reflection plan
77984
78495
  let ports;
77985
78496
  if (reusePorts) {
77986
78497
  ports = reusePorts;
78498
+ if (reusePorts.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
78499
+ let explicitUrl;
78500
+ try {
78501
+ explicitUrl = getConfig(program3).memory?.config?.url;
78502
+ } catch {
78503
+ explicitUrl = undefined;
78504
+ }
78505
+ if (!explicitUrl) {
78506
+ console.log(source_default.yellow(`
78507
+ \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.`));
78508
+ 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}.`));
78509
+ 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}.
78510
+ `));
78511
+ }
78512
+ }
77987
78513
  } else {
77988
78514
  try {
77989
78515
  ports = await pickHindsightPorts();
@@ -77997,6 +78523,29 @@ Cross-agent reflection plan
77997
78523
  if (ports.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
77998
78524
  console.log(source_default.yellow(` Port ${HINDSIGHT_DEFAULT_API_PORT} is already in use; ` + `using ${ports.apiPort}/${ports.uiPort} instead.`));
77999
78525
  }
78526
+ let conflict = await preflightHindsightPorts(ports);
78527
+ if (conflict) {
78528
+ const heldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
78529
+ console.log(source_default.yellow(`
78530
+ Chosen Hindsight port ${conflict.port} is occupied${heldBy}; ` + `selecting a free port instead of crash-looping.`));
78531
+ try {
78532
+ ports = await pickHindsightPorts();
78533
+ } catch (err) {
78534
+ console.error(source_default.red(`
78535
+ ${err.message}
78536
+ `));
78537
+ process.exit(1);
78538
+ }
78539
+ conflict = await preflightHindsightPorts(ports);
78540
+ if (conflict) {
78541
+ const stillHeldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
78542
+ console.error(source_default.red(`
78543
+ Refusing to start Hindsight: port ${conflict.port} is still ` + `occupied${stillHeldBy} after reassignment. Free it and retry ` + `\`switchroom memory --start\`.
78544
+ `));
78545
+ process.exit(1);
78546
+ }
78547
+ console.log(source_default.green(` Reassigned Hindsight to port ${ports.apiPort}/${ports.uiPort}.`));
78548
+ }
78000
78549
  if (opts.provider && opts.provider !== "claude-code") {
78001
78550
  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
78551
  }
@@ -78103,7 +78652,7 @@ ${name}:`));
78103
78652
  process.exit(1);
78104
78653
  }
78105
78654
  const collection = getCollectionForAgent(agent, config);
78106
- const apiUrl = config.memory?.config?.url ?? "http://localhost:8888/mcp/";
78655
+ const apiUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
78107
78656
  const timeoutMs = Math.max(500, parseInt(opts.timeout, 10) || 5000);
78108
78657
  console.log(source_default.bold(`
78109
78658
  Demoting memory ${source_default.cyan(memoryId)}`));
@@ -78119,7 +78668,7 @@ Demoting memory ${source_default.cyan(memoryId)}`));
78119
78668
  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
78669
  process.exit(1);
78121
78670
  }));
78122
- const restBase = (url) => (url ?? "http://127.0.0.1:8888/mcp/").replace(/\/mcp\/?$/, "").replace(/\/$/, "");
78671
+ const restBase = (url) => (url ?? HINDSIGHT_DEFAULT_MCP_URL).replace(/\/mcp\/?$/, "").replace(/\/$/, "");
78123
78672
  const VALID_BANK = /^[a-zA-Z0-9_.-]+$/;
78124
78673
  const profile = memory.command("profile").description("Author + inspect operator profile banks (shared/per-user memory; wire via memory.recall.additional_banks)");
78125
78674
  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 +78776,13 @@ import { timingSafeEqual as timingSafeEqual3, randomBytes as randomBytes11 } fro
78227
78776
  // src/web/api.ts
78228
78777
  init_lifecycle();
78229
78778
  init_manager();
78230
- init_hindsight();
78779
+ init_hindsight2();
78231
78780
  import { spawnSync as spawnSync6 } from "node:child_process";
78232
78781
  import { existsSync as existsSync51, readFileSync as readFileSync45, statSync as statSync23 } from "node:fs";
78233
78782
  import { resolve as resolve29, join as join45 } from "node:path";
78234
78783
 
78235
78784
  // src/web/memory-remediation.ts
78236
- init_hindsight();
78785
+ init_hindsight2();
78237
78786
  function restBaseFromMcpUrl(mcpUrl) {
78238
78787
  return hindsightRestBase(mcpUrl);
78239
78788
  }
@@ -78295,6 +78844,7 @@ async function buildUserProfile(mcpUrl, bank, opts) {
78295
78844
  }
78296
78845
 
78297
78846
  // src/web/api.ts
78847
+ init_hindsight();
78298
78848
  init_audit_reader();
78299
78849
 
78300
78850
  // src/scheduler/dispatch.ts
@@ -82307,6 +82857,7 @@ var NVIDIA_TOOLKIT_INSTALL_HINT = "https://docs.nvidia.com/datacenter/cloud-nati
82307
82857
 
82308
82858
  // src/cli/setup.ts
82309
82859
  init_host_capabilities();
82860
+ init_hindsight();
82310
82861
  init_client();
82311
82862
  init_posthog();
82312
82863
 
@@ -82913,17 +83464,45 @@ async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath) {
82913
83464
  console.log(source_default.gray(" Found stopped switchroom-hindsight container, removing..."));
82914
83465
  stopHindsight();
82915
83466
  }
83467
+ let ports;
83468
+ try {
83469
+ ports = await pickHindsightPorts();
83470
+ } catch (err) {
83471
+ console.log(source_default.red(` ${err.message}`));
83472
+ return;
83473
+ }
83474
+ if (ports.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
83475
+ console.log(source_default.yellow(` Port ${HINDSIGHT_DEFAULT_API_PORT} is already in use; using ${ports.apiPort}/${ports.uiPort} instead.`));
83476
+ }
83477
+ let conflict = await preflightHindsightPorts(ports);
83478
+ if (conflict) {
83479
+ const heldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
83480
+ console.log(source_default.yellow(` Chosen Hindsight port ${conflict.port} is occupied${heldBy}; selecting a free port instead of crash-looping.`));
83481
+ try {
83482
+ ports = await pickHindsightPorts();
83483
+ } catch (err) {
83484
+ console.log(source_default.red(` ${err.message}`));
83485
+ return;
83486
+ }
83487
+ conflict = await preflightHindsightPorts(ports);
83488
+ if (conflict) {
83489
+ const stillHeldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
83490
+ 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\`.`));
83491
+ return;
83492
+ }
83493
+ console.log(source_default.green(` Reassigned Hindsight to port ${ports.apiPort}/${ports.uiPort}.`));
83494
+ }
82916
83495
  const spin = spinner("Starting Hindsight Docker container...");
82917
83496
  try {
82918
83497
  const litellmCfg = await resolveLiteLLMForHindsight2(config);
82919
- startHindsight(undefined, litellmCfg);
83498
+ startHindsight(ports, litellmCfg);
82920
83499
  if (litellmCfg) {
82921
83500
  console.log(source_default.gray(" LiteLLM routing enabled (--network host, ANTHROPIC_BASE_URL set)."));
82922
83501
  }
82923
83502
  if (isHindsightRunning()) {
82924
83503
  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"));
83504
+ console.log(source_default.gray(` API: http://localhost:${ports.apiPort}/mcp`));
83505
+ console.log(source_default.gray(` UI: http://localhost:${ports.uiPort}`));
82927
83506
  } else {
82928
83507
  spin.stop(source_default.yellow("Container started but may still be initializing"));
82929
83508
  }
@@ -83450,6 +84029,22 @@ var UPDATE_RESULT_SENTINEL = "SWITCHROOM_UPDATE_RESULT:";
83450
84029
  function encodeUpdateResultLine(payload) {
83451
84030
  return `${UPDATE_RESULT_SENTINEL}${JSON.stringify(payload)}`;
83452
84031
  }
84032
+ function resolveHindsightPinTag(opts) {
84033
+ if (typeof opts.hindsightPinTag === "string") {
84034
+ return opts.hindsightPinTag || undefined;
84035
+ }
84036
+ if (opts.channel)
84037
+ return;
84038
+ let candidate = opts.pin;
84039
+ if (!candidate) {
84040
+ try {
84041
+ candidate = loadConfig().release?.pin ?? undefined;
84042
+ } catch {
84043
+ candidate = undefined;
84044
+ }
84045
+ }
84046
+ return candidate && /^v\d+\.\d+\.\d+$/.test(candidate) ? candidate : undefined;
84047
+ }
83453
84048
  function planUpdate(opts) {
83454
84049
  const composePath = opts.composePath ?? DEFAULT_COMPOSE_PATH;
83455
84050
  const runner = opts.runner ?? defaultRunner2;
@@ -83596,10 +84191,14 @@ function planUpdate(opts) {
83596
84191
  }
83597
84192
  steps.push({
83598
84193
  name: "refresh-hindsight",
83599
- description: "switchroom memory setup --recreate \u2014 pull latest hindsight image + recreate the memory singleton (standalone container, reusing its current port)",
84194
+ 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
84195
  skipReason: !memoryBackendHindsight ? "memory.backend is not hindsight \u2014 no hindsight singleton to refresh" : opts.skipImages ? "--skip-images flag set" : undefined,
83601
84196
  run: () => {
83602
- const r = runner(process.execPath, [scriptPath, "memory", "setup", "--recreate"]);
84197
+ const pinTag = resolveHindsightPinTag(opts);
84198
+ const setupArgs = ["memory", "setup", "--recreate"];
84199
+ if (pinTag)
84200
+ setupArgs.push("--tag", pinTag);
84201
+ const r = runner(process.execPath, [scriptPath, ...setupArgs]);
83603
84202
  if (r.status !== 0)
83604
84203
  throw new Error("switchroom memory setup --recreate failed");
83605
84204
  }
@@ -83963,6 +84562,7 @@ import { homedir as homedir39 } from "node:os";
83963
84562
  init_operator_uid();
83964
84563
  init_atomic();
83965
84564
  init_audit_reader();
84565
+ init_hindsight();
83966
84566
  function normalizeVersion(v) {
83967
84567
  return v.trim().replace(/^v/, "");
83968
84568
  }
@@ -86902,7 +87502,7 @@ import { existsSync as existsSync72, readFileSync as readFileSync63, readdirSync
86902
87502
  import { resolve as resolve45, join as join71 } from "node:path";
86903
87503
  import { createHash as createHash14 } from "node:crypto";
86904
87504
  init_merge();
86905
- init_hindsight();
87505
+ init_hindsight2();
86906
87506
  function formatBytes(bytes) {
86907
87507
  return `${bytes.toLocaleString()} bytes`;
86908
87508
  }
@@ -87323,6 +87923,8 @@ async function claimWorktree(input, codeRepos) {
87323
87923
  const baseDir = worktreesBaseDir();
87324
87924
  mkdirSync38(baseDir, { recursive: true });
87325
87925
  worktreePath = join73(baseDir, `${id}-${taskSuffix}`);
87926
+ const ambientOwner = process.env.SWITCHROOM_AGENT_NAME;
87927
+ const ownerAgent = input.ownerAgent ?? (ambientOwner != null && ambientOwner !== "" ? ambientOwner : undefined);
87326
87928
  const now = new Date().toISOString();
87327
87929
  const record2 = {
87328
87930
  id,
@@ -87332,7 +87934,7 @@ async function claimWorktree(input, codeRepos) {
87332
87934
  path: worktreePath,
87333
87935
  createdAt: now,
87334
87936
  heartbeatAt: now,
87335
- ownerAgent: input.ownerAgent
87937
+ ownerAgent
87336
87938
  };
87337
87939
  writeRecord(record2);
87338
87940
  } finally {