switchroom 0.19.39 → 0.19.41

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 (37) hide show
  1. package/dist/agent-scheduler/index.js +10 -1
  2. package/dist/auth-broker/index.js +12 -3
  3. package/dist/cli/notion-write-pretool.mjs +10 -1
  4. package/dist/cli/switchroom.js +562 -196
  5. package/dist/host-control/main.js +287 -20
  6. package/dist/vault/approvals/kernel-server.js +12 -3
  7. package/dist/vault/broker/server.js +12 -3
  8. package/package.json +1 -1
  9. package/profiles/_base/start.sh.hbs +10 -0
  10. package/telegram-plugin/bridge/bridge.ts +2 -2
  11. package/telegram-plugin/dist/bridge/bridge.js +10 -2
  12. package/telegram-plugin/dist/gateway/gateway.js +549 -232
  13. package/telegram-plugin/dist/server.js +10 -2
  14. package/telegram-plugin/gateway/backstop-delivery.ts +48 -0
  15. package/telegram-plugin/gateway/checklist-fallback.ts +370 -0
  16. package/telegram-plugin/gateway/compaction-marker.ts +84 -0
  17. package/telegram-plugin/gateway/gateway.ts +72 -72
  18. package/telegram-plugin/gateway/liveness-wiring.ts +15 -0
  19. package/telegram-plugin/gateway/outbound-send-path.ts +20 -0
  20. package/telegram-plugin/gateway/outbox-sweep.ts +116 -18
  21. package/telegram-plugin/gateway/silence-poke-session-event.ts +13 -0
  22. package/telegram-plugin/gateway/stream-render.ts +39 -1
  23. package/telegram-plugin/gateway/turn-record-status.ts +80 -0
  24. package/telegram-plugin/hooks/compaction-marker-precompact.mjs +70 -0
  25. package/telegram-plugin/hooks/hooks.json +11 -0
  26. package/telegram-plugin/session-tail.ts +20 -0
  27. package/telegram-plugin/silence-poke.ts +28 -0
  28. package/telegram-plugin/tests/checklist-fallback.test.ts +317 -0
  29. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +10 -6
  30. package/telegram-plugin/tests/outbox-delivery.test.ts +38 -1
  31. package/telegram-plugin/tests/outbox-flush-ack-claim-race.test.ts +213 -0
  32. package/telegram-plugin/tests/outbox-reply-then-recap-e2e.test.ts +1 -1
  33. package/telegram-plugin/tests/outbox-sweep-flood-breaker.test.ts +4 -4
  34. package/telegram-plugin/tests/outbox-sweep-listen-button.test.ts +71 -8
  35. package/telegram-plugin/tests/send-reply-golden.test.ts +47 -0
  36. package/telegram-plugin/tests/silence-poke-compaction.test.ts +222 -0
  37. package/telegram-plugin/tests/turn-record-status.test.ts +62 -0
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.19.39", COMMIT_SHA = "4aff2bdd";
2123
+ var VERSION = "0.19.41", COMMIT_SHA = "e77ee9c0";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -13593,7 +13593,7 @@ function resolveObservationScope(configured, env2 = process.env) {
13593
13593
  return classifyObservationScope(configured);
13594
13594
  return classifyObservationScope(env2[OBSERVATION_SCOPES_ENV]);
13595
13595
  }
13596
- var OBSERVATION_SCOPES, OBSERVATION_SCOPES_ENV = "HINDSIGHT_OBSERVATION_SCOPES";
13596
+ var OBSERVATION_SCOPES, OBSERVATION_SCOPES_ENV = "HINDSIGHT_OBSERVATION_SCOPES", OBSERVATION_SCOPE_STRATEGIES;
13597
13597
  var init_observation_scopes = __esm(() => {
13598
13598
  OBSERVATION_SCOPES = [
13599
13599
  "per_tag",
@@ -13601,13 +13601,19 @@ var init_observation_scopes = __esm(() => {
13601
13601
  "all_combinations",
13602
13602
  "shared"
13603
13603
  ];
13604
+ OBSERVATION_SCOPE_STRATEGIES = [
13605
+ "curated",
13606
+ "shared",
13607
+ "combined",
13608
+ "off"
13609
+ ];
13604
13610
  });
13605
13611
 
13606
13612
  // src/config/schema.ts
13607
13613
  function isValidTimezone(value) {
13608
13614
  return TIMEZONE_REGEX.test(value);
13609
13615
  }
13610
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
13616
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
13611
13617
  var init_schema = __esm(() => {
13612
13618
  init_zod();
13613
13619
  init_observation_scopes();
@@ -13722,7 +13728,8 @@ var init_schema = __esm(() => {
13722
13728
  allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
13723
13729
  deny: exports_external.array(exports_external.string()).default([]).describe("Denied tools (overrides allow)")
13724
13730
  }).optional();
13725
- ObservationScopesSchema = exports_external.enum(OBSERVATION_SCOPES).optional().describe("Per-row observation scope stamped on every memory this agent " + `retains. "shared" makes Hindsight's consolidation write the ` + "resulting observations into ONE global untagged scope instead of a " + "scope per tag \u2014 what several agents pooling one bank need so their " + "observations actually merge rather than sitting in parallel " + "per-tag silos. OMITTED BY DEFAULT: unset means the field never goes " + "on the wire and the engine's own default stands, which is the " + "shipped behaviour. Applies to every retain path (Stop hook, " + "sidechain, boot reconcile, queue drain, backfill, session-handoff " + "mirror) and is carried on the queued payload, so a retain that fails " + "now and drains later still lands in this scope. Accepted values: " + `${OBSERVATION_SCOPES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
13731
+ ObservationScopesSchema = exports_external.enum(OBSERVATION_SCOPES).optional().describe("Operator PIN for the per-row observation scope, overriding the " + 'default `curated` strategy (see docs/configuration.md). "shared" ' + "forces Hindsight's consolidation to write EVERY observation into ONE " + "global untagged scope \u2014 what several agents pooling one bank want so " + "their observations merge rather than sitting in parallel silos. " + '"combined" is the pin form of opting out (restores the pre-#4035 ' + "engine default). UNSET does NOT mean 'nothing on the wire': the " + "`curated` strategy still computes a scope per retain. Applies to " + "every retain path (Stop hook, " + "sidechain, boot reconcile, queue drain, backfill, session-handoff " + "mirror) and is carried on the queued payload, so a retain that fails " + "now and drains later still lands in this scope. Accepted values: " + `${OBSERVATION_SCOPES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
13732
+ ObservationScopeStrategySchema = exports_external.enum(OBSERVATION_SCOPE_STRATEGIES).optional().describe("Per-retain strategy that decides how the observation scope is " + 'computed when no `observation_scopes` pin is set. "curated" ' + "(the default since #4035) strips volatile per-session provenance " + "tags from each retain's consolidation scope \u2014 keeping the stable " + "semantic ones on the source fact \u2014 so an agent's observations dedup " + "and merge across sessions instead of pocketing one-per-session. " + '"shared" sends every retain to the one global untagged scope. ' + '"combined" / "off" opt OUT: no per-row scope goes on the wire, ' + "restoring the pre-feature engine default (byte-identical to an " + "unconfigured client). OMITTED BY DEFAULT: unset leaves the plugin's " + "own default (`curated`) in force. A manual `observation_scopes` pin " + "wins over this strategy. Accepted values: " + `${OBSERVATION_SCOPE_STRATEGIES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
13726
13733
  AgentMemorySchema = exports_external.object({
13727
13734
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
13728
13735
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
@@ -13758,6 +13765,7 @@ var init_schema = __esm(() => {
13758
13765
  }).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`)."),
13759
13766
  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)."),
13760
13767
  observation_scopes: ObservationScopesSchema,
13768
+ observation_scope_strategy: ObservationScopeStrategySchema,
13761
13769
  recall: exports_external.object({
13762
13770
  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)."),
13763
13771
  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."),
@@ -14158,6 +14166,7 @@ var init_schema = __esm(() => {
14158
14166
  isolation: exports_external.enum(["default", "strict"]).optional(),
14159
14167
  directive_capture_nudge: exports_external.boolean().optional(),
14160
14168
  observation_scopes: ObservationScopesSchema,
14169
+ observation_scope_strategy: ObservationScopeStrategySchema,
14161
14170
  recall: exports_external.object({
14162
14171
  max_memories: exports_external.number().int().min(0).optional(),
14163
14172
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
@@ -29102,6 +29111,7 @@ function buildWorkspaceContext(args) {
29102
29111
  hindsightRecallSkipTrivial,
29103
29112
  hindsightDirectiveCaptureNudge,
29104
29113
  hindsightObservationScopes,
29114
+ hindsightObservationScopeStrategy,
29105
29115
  hindsightTopicAliasesJson,
29106
29116
  hindsightSenderBanksJson,
29107
29117
  hindsightTopicFilterMode
@@ -29156,6 +29166,7 @@ function buildWorkspaceContext(args) {
29156
29166
  hindsightRecallSkipTrivial,
29157
29167
  hindsightDirectiveCaptureNudge,
29158
29168
  hindsightObservationScopesQ: hindsightObservationScopes ? shellSingleQuote(hindsightObservationScopes) : undefined,
29169
+ hindsightObservationScopeStrategyQ: hindsightObservationScopeStrategy ? shellSingleQuote(hindsightObservationScopeStrategy) : undefined,
29159
29170
  hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
29160
29171
  hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
29161
29172
  hindsightTopicFilterMode,
@@ -29421,6 +29432,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
29421
29432
  const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
29422
29433
  const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
29423
29434
  const hindsightObservationScopes = agentConfig.memory?.observation_scopes;
29435
+ const hindsightObservationScopeStrategy = agentConfig.memory?.observation_scope_strategy;
29424
29436
  const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
29425
29437
  const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
29426
29438
  const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
@@ -29457,6 +29469,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
29457
29469
  hindsightRecallSkipTrivial,
29458
29470
  hindsightDirectiveCaptureNudge,
29459
29471
  hindsightObservationScopes,
29472
+ hindsightObservationScopeStrategy,
29460
29473
  hindsightTopicAliasesJson,
29461
29474
  hindsightSenderBanksJson,
29462
29475
  hindsightTopicFilterMode
@@ -30402,6 +30415,7 @@ function reconcileAgentInner(name, agentConfigRaw, agentsDir, telegramConfig, sw
30402
30415
  const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
30403
30416
  const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
30404
30417
  const hindsightObservationScopes = agentConfig.memory?.observation_scopes;
30418
+ const hindsightObservationScopeStrategy = agentConfig.memory?.observation_scope_strategy;
30405
30419
  const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
30406
30420
  const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
30407
30421
  const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
@@ -30451,6 +30465,7 @@ function reconcileAgentInner(name, agentConfigRaw, agentsDir, telegramConfig, sw
30451
30465
  hindsightRecallSkipTrivial,
30452
30466
  hindsightDirectiveCaptureNudge,
30453
30467
  hindsightObservationScopesQ: hindsightObservationScopes ? shellSingleQuote(hindsightObservationScopes) : undefined,
30468
+ hindsightObservationScopeStrategyQ: hindsightObservationScopeStrategy ? shellSingleQuote(hindsightObservationScopeStrategy) : undefined,
30454
30469
  hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
30455
30470
  hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
30456
30471
  hindsightTopicFilterMode,
@@ -30874,6 +30889,7 @@ ${body}
30874
30889
  hindsightRecallSkipTrivial,
30875
30890
  hindsightDirectiveCaptureNudge,
30876
30891
  hindsightObservationScopes,
30892
+ hindsightObservationScopeStrategy,
30877
30893
  hindsightTopicAliasesJson,
30878
30894
  hindsightSenderBanksJson,
30879
30895
  hindsightTopicFilterMode
@@ -39115,168 +39131,6 @@ var init_telegram_yaml = __esm(() => {
39115
39131
  import_yaml12 = __toESM(require_dist(), 1);
39116
39132
  });
39117
39133
 
39118
- // src/memory/hindsight-tools.ts
39119
- var HINDSIGHT_MIN_API_VERSION = "0.8.5", HINDSIGHT_REPAIR_MIN_API_VERSION = "0.8.5", EXPECTED_HINDSIGHT_TOOLS;
39120
- var init_hindsight_tools = __esm(() => {
39121
- EXPECTED_HINDSIGHT_TOOLS = {
39122
- recall: { required: ["query"] },
39123
- reflect: { required: ["query"] },
39124
- retain: { required: ["content"] },
39125
- sync_retain: { required: ["content"] },
39126
- delete_document: { required: ["document_id"] },
39127
- create_directive: { required: ["content", "name"] },
39128
- list_directives: { required: [] },
39129
- delete_directive: { required: ["directive_id"] },
39130
- create_bank: { required: ["bank_id"] },
39131
- update_bank: { required: [] },
39132
- list_banks: { required: [] },
39133
- create_mental_model: { required: ["name", "source_query"] },
39134
- list_mental_models: { required: [] },
39135
- update_mental_model: { required: ["mental_model_id"] },
39136
- refresh_mental_model: { required: ["mental_model_id"] },
39137
- list_memories: { required: [] },
39138
- get_memory: { required: ["memory_id"] },
39139
- update_memory: { required: ["memory_id"] },
39140
- invalidate_memory: { required: ["memory_id"] }
39141
- };
39142
- });
39143
-
39144
- // src/memory/hindsight-repair.ts
39145
- function buildRepairBankArgv(opts) {
39146
- const hasBank = typeof opts.bank === "string" && opts.bank.length > 0;
39147
- if (hasBank === Boolean(opts.all)) {
39148
- throw new Error("pass exactly one of --bank <id> or --all");
39149
- }
39150
- const cmdArgs = ["repair-bank"];
39151
- if (hasBank)
39152
- cmdArgs.push("--bank", opts.bank);
39153
- if (opts.all)
39154
- cmdArgs.push("--all");
39155
- if (opts.schema)
39156
- cmdArgs.push("--schema", opts.schema);
39157
- if (opts.dryRun)
39158
- cmdArgs.push("--dry-run");
39159
- const script = `if command -v hindsight-admin >/dev/null 2>&1; then exec hindsight-admin "$@"; fi; ` + `exec ${HINDSIGHT_ADMIN_BIN} "$@"`;
39160
- return [
39161
- "exec",
39162
- opts.container ?? HINDSIGHT_CONTAINER_NAME,
39163
- "sh",
39164
- "-c",
39165
- script,
39166
- "--",
39167
- ...cmdArgs
39168
- ];
39169
- }
39170
- function parseRepairSummary(stdout) {
39171
- const m = stdout.match(/Done:\s*(\d+)\s+schema\(s\),\s*(\d+)\s+bank\(s\) scanned,\s*(\d+)\s+already present,\s*(\d+)\s+created,\s*(\d+)\s+to-create \(dry-run\),\s*(\d+)\s+failed/);
39172
- if (!m)
39173
- return null;
39174
- return {
39175
- schemas: Number(m[1]),
39176
- banksScanned: Number(m[2]),
39177
- alreadyPresent: Number(m[3]),
39178
- created: Number(m[4]),
39179
- toCreate: Number(m[5]),
39180
- failed: Number(m[6])
39181
- };
39182
- }
39183
- function compareApiVersion(a, b) {
39184
- const part = (v, i) => {
39185
- const raw = v.split(".")[i];
39186
- if (raw === undefined)
39187
- return 0;
39188
- const n = parseInt(raw, 10);
39189
- return Number.isFinite(n) ? n : 0;
39190
- };
39191
- const len = Math.max(a.split(".").length, b.split(".").length);
39192
- for (let i = 0;i < len; i++) {
39193
- const d = part(a, i) - part(b, i);
39194
- if (d !== 0)
39195
- return d;
39196
- }
39197
- return 0;
39198
- }
39199
- async function fetchHindsightApiVersion(mcpUrl, opts = {}) {
39200
- const doFetch = opts.fetchImpl ?? fetch;
39201
- const origin = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
39202
- const controller = new AbortController;
39203
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 4000);
39204
- try {
39205
- const res = await doFetch(`${origin}/version`, { signal: controller.signal });
39206
- if (!res.ok)
39207
- return null;
39208
- const body = await res.json();
39209
- return typeof body?.api_version === "string" ? body.api_version : null;
39210
- } catch {
39211
- return null;
39212
- } finally {
39213
- clearTimeout(timer);
39214
- }
39215
- }
39216
- function classifyRepairPreflight(liveVersion) {
39217
- if (liveVersion === null)
39218
- return { ok: true };
39219
- if (compareApiVersion(liveVersion, HINDSIGHT_REPAIR_MIN_API_VERSION) < 0) {
39220
- return {
39221
- ok: false,
39222
- reason: `hindsight ${liveVersion} has no \`repair-bank\` command \u2014 it lands in ` + `${HINDSIGHT_REPAIR_MIN_API_VERSION} (upstream #2645). Without it there is no ` + `supported way to rebuild a bank's vector-index coverage. Update the ` + `pinned image in docker/Dockerfile.hindsight and recreate the container ` + `with \`switchroom memory --update\`, then re-run this command.`
39223
- };
39224
- }
39225
- return { ok: true };
39226
- }
39227
- function classifyRepairOutcome(run) {
39228
- if (run.childExit !== 0) {
39229
- return {
39230
- exit: REPAIR_FAILED_EXIT,
39231
- level: "fail",
39232
- headline: `repair-bank exited ${run.childExit}.`,
39233
- detail: run.childExit === 2 ? "Exit 2 from the admin CLI is a usage error \u2014 check --bank / --schema spelling. " + "Nothing was repaired and coverage is unknown." : "Failed indexes are dropped, so a re-run is safe and is the documented retry."
39234
- };
39235
- }
39236
- if (run.summary === null) {
39237
- return {
39238
- exit: REPAIR_UNVERIFIED_EXIT,
39239
- level: "fail",
39240
- headline: "repair-bank exited 0 but printed no summary \u2014 coverage was NOT confirmed.",
39241
- detail: "Read its output above. This is not a pass: a backend with a single global " + "index, an aborted scan, or an output-format change upstream all look like " + "this, and none of them means coverage is complete."
39242
- };
39243
- }
39244
- if (run.summary.banksScanned === 0) {
39245
- return {
39246
- exit: REPAIR_UNVERIFIED_EXIT,
39247
- level: "fail",
39248
- headline: "repair-bank scanned 0 bank(s) \u2014 nothing was checked.",
39249
- detail: "A scan that examined no bank proves nothing about coverage. Usual cause is a " + "--bank id or --schema name that matches nothing; `switchroom memory banks` " + "lists the real ids."
39250
- };
39251
- }
39252
- if (run.summary.failed > 0) {
39253
- return {
39254
- exit: REPAIR_FAILED_EXIT,
39255
- level: "fail",
39256
- headline: `${run.summary.failed} index(es) failed to build.`,
39257
- detail: "Those banks still under-return on recall. Failed indexes are dropped, so " + "re-running is safe and is the documented retry; if it repeats, the hindsight " + "container logs carry the postgres error."
39258
- };
39259
- }
39260
- if (run.dryRun && run.summary.toCreate > 0) {
39261
- return {
39262
- exit: REPAIR_COVERAGE_MISSING_EXIT,
39263
- level: "fail",
39264
- headline: `${run.summary.toCreate} vector index(es) missing across ${run.summary.banksScanned} bank(s).`,
39265
- detail: "Recall on those banks silently under-returns. Re-run without --dry-run to fix."
39266
- };
39267
- }
39268
- return {
39269
- exit: 0,
39270
- level: "ok",
39271
- headline: "repair-bank finished.",
39272
- detail: run.dryRun ? `Coverage complete: ${run.summary.alreadyPresent} index(es) present across ` + `${run.summary.banksScanned} bank(s), nothing to create.` : `${run.summary.created} created, ${run.summary.alreadyPresent} already present ` + `across ${run.summary.banksScanned} bank(s). Re-run \`switchroom doctor\` to confirm.`
39273
- };
39274
- }
39275
- var HINDSIGHT_CONTAINER_NAME = "switchroom-hindsight", HINDSIGHT_ADMIN_BIN = "/app/api/.venv/bin/hindsight-admin", REPAIR_FAILED_EXIT = 1, REPAIR_COVERAGE_MISSING_EXIT = 3, REPAIR_UNVERIFIED_EXIT = 4;
39276
- var init_hindsight_repair = __esm(() => {
39277
- init_hindsight_tools();
39278
- });
39279
-
39280
39134
  // src/memory/bank-health.ts
39281
39135
  function hindsightRestBase(mcpUrl) {
39282
39136
  return mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
@@ -39446,6 +39300,168 @@ function recentUnextracted(docs, withinDays = 30, now = new Date, minTextLength
39446
39300
  }
39447
39301
  var DOCUMENTS_PAGE_LIMIT = 500;
39448
39302
 
39303
+ // src/memory/hindsight-tools.ts
39304
+ var HINDSIGHT_MIN_API_VERSION = "0.8.5", HINDSIGHT_REPAIR_MIN_API_VERSION = "0.8.5", EXPECTED_HINDSIGHT_TOOLS;
39305
+ var init_hindsight_tools = __esm(() => {
39306
+ EXPECTED_HINDSIGHT_TOOLS = {
39307
+ recall: { required: ["query"] },
39308
+ reflect: { required: ["query"] },
39309
+ retain: { required: ["content"] },
39310
+ sync_retain: { required: ["content"] },
39311
+ delete_document: { required: ["document_id"] },
39312
+ create_directive: { required: ["content", "name"] },
39313
+ list_directives: { required: [] },
39314
+ delete_directive: { required: ["directive_id"] },
39315
+ create_bank: { required: ["bank_id"] },
39316
+ update_bank: { required: [] },
39317
+ list_banks: { required: [] },
39318
+ create_mental_model: { required: ["name", "source_query"] },
39319
+ list_mental_models: { required: [] },
39320
+ update_mental_model: { required: ["mental_model_id"] },
39321
+ refresh_mental_model: { required: ["mental_model_id"] },
39322
+ list_memories: { required: [] },
39323
+ get_memory: { required: ["memory_id"] },
39324
+ update_memory: { required: ["memory_id"] },
39325
+ invalidate_memory: { required: ["memory_id"] }
39326
+ };
39327
+ });
39328
+
39329
+ // src/memory/hindsight-repair.ts
39330
+ function buildRepairBankArgv(opts) {
39331
+ const hasBank = typeof opts.bank === "string" && opts.bank.length > 0;
39332
+ if (hasBank === Boolean(opts.all)) {
39333
+ throw new Error("pass exactly one of --bank <id> or --all");
39334
+ }
39335
+ const cmdArgs = ["repair-bank"];
39336
+ if (hasBank)
39337
+ cmdArgs.push("--bank", opts.bank);
39338
+ if (opts.all)
39339
+ cmdArgs.push("--all");
39340
+ if (opts.schema)
39341
+ cmdArgs.push("--schema", opts.schema);
39342
+ if (opts.dryRun)
39343
+ cmdArgs.push("--dry-run");
39344
+ const script = `if command -v hindsight-admin >/dev/null 2>&1; then exec hindsight-admin "$@"; fi; ` + `exec ${HINDSIGHT_ADMIN_BIN} "$@"`;
39345
+ return [
39346
+ "exec",
39347
+ opts.container ?? HINDSIGHT_CONTAINER_NAME,
39348
+ "sh",
39349
+ "-c",
39350
+ script,
39351
+ "--",
39352
+ ...cmdArgs
39353
+ ];
39354
+ }
39355
+ function parseRepairSummary(stdout) {
39356
+ const m = stdout.match(/Done:\s*(\d+)\s+schema\(s\),\s*(\d+)\s+bank\(s\) scanned,\s*(\d+)\s+already present,\s*(\d+)\s+created,\s*(\d+)\s+to-create \(dry-run\),\s*(\d+)\s+failed/);
39357
+ if (!m)
39358
+ return null;
39359
+ return {
39360
+ schemas: Number(m[1]),
39361
+ banksScanned: Number(m[2]),
39362
+ alreadyPresent: Number(m[3]),
39363
+ created: Number(m[4]),
39364
+ toCreate: Number(m[5]),
39365
+ failed: Number(m[6])
39366
+ };
39367
+ }
39368
+ function compareApiVersion(a, b) {
39369
+ const part = (v, i) => {
39370
+ const raw = v.split(".")[i];
39371
+ if (raw === undefined)
39372
+ return 0;
39373
+ const n = parseInt(raw, 10);
39374
+ return Number.isFinite(n) ? n : 0;
39375
+ };
39376
+ const len = Math.max(a.split(".").length, b.split(".").length);
39377
+ for (let i = 0;i < len; i++) {
39378
+ const d = part(a, i) - part(b, i);
39379
+ if (d !== 0)
39380
+ return d;
39381
+ }
39382
+ return 0;
39383
+ }
39384
+ async function fetchHindsightApiVersion(mcpUrl, opts = {}) {
39385
+ const doFetch = opts.fetchImpl ?? fetch;
39386
+ const origin = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
39387
+ const controller = new AbortController;
39388
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 4000);
39389
+ try {
39390
+ const res = await doFetch(`${origin}/version`, { signal: controller.signal });
39391
+ if (!res.ok)
39392
+ return null;
39393
+ const body = await res.json();
39394
+ return typeof body?.api_version === "string" ? body.api_version : null;
39395
+ } catch {
39396
+ return null;
39397
+ } finally {
39398
+ clearTimeout(timer);
39399
+ }
39400
+ }
39401
+ function classifyRepairPreflight(liveVersion) {
39402
+ if (liveVersion === null)
39403
+ return { ok: true };
39404
+ if (compareApiVersion(liveVersion, HINDSIGHT_REPAIR_MIN_API_VERSION) < 0) {
39405
+ return {
39406
+ ok: false,
39407
+ reason: `hindsight ${liveVersion} has no \`repair-bank\` command \u2014 it lands in ` + `${HINDSIGHT_REPAIR_MIN_API_VERSION} (upstream #2645). Without it there is no ` + `supported way to rebuild a bank's vector-index coverage. Update the ` + `pinned image in docker/Dockerfile.hindsight and recreate the container ` + `with \`switchroom memory --update\`, then re-run this command.`
39408
+ };
39409
+ }
39410
+ return { ok: true };
39411
+ }
39412
+ function classifyRepairOutcome(run) {
39413
+ if (run.childExit !== 0) {
39414
+ return {
39415
+ exit: REPAIR_FAILED_EXIT,
39416
+ level: "fail",
39417
+ headline: `repair-bank exited ${run.childExit}.`,
39418
+ detail: run.childExit === 2 ? "Exit 2 from the admin CLI is a usage error \u2014 check --bank / --schema spelling. " + "Nothing was repaired and coverage is unknown." : "Failed indexes are dropped, so a re-run is safe and is the documented retry."
39419
+ };
39420
+ }
39421
+ if (run.summary === null) {
39422
+ return {
39423
+ exit: REPAIR_UNVERIFIED_EXIT,
39424
+ level: "fail",
39425
+ headline: "repair-bank exited 0 but printed no summary \u2014 coverage was NOT confirmed.",
39426
+ detail: "Read its output above. This is not a pass: a backend with a single global " + "index, an aborted scan, or an output-format change upstream all look like " + "this, and none of them means coverage is complete."
39427
+ };
39428
+ }
39429
+ if (run.summary.banksScanned === 0) {
39430
+ return {
39431
+ exit: REPAIR_UNVERIFIED_EXIT,
39432
+ level: "fail",
39433
+ headline: "repair-bank scanned 0 bank(s) \u2014 nothing was checked.",
39434
+ detail: "A scan that examined no bank proves nothing about coverage. Usual cause is a " + "--bank id or --schema name that matches nothing; `switchroom memory banks` " + "lists the real ids."
39435
+ };
39436
+ }
39437
+ if (run.summary.failed > 0) {
39438
+ return {
39439
+ exit: REPAIR_FAILED_EXIT,
39440
+ level: "fail",
39441
+ headline: `${run.summary.failed} index(es) failed to build.`,
39442
+ detail: "Those banks still under-return on recall. Failed indexes are dropped, so " + "re-running is safe and is the documented retry; if it repeats, the hindsight " + "container logs carry the postgres error."
39443
+ };
39444
+ }
39445
+ if (run.dryRun && run.summary.toCreate > 0) {
39446
+ return {
39447
+ exit: REPAIR_COVERAGE_MISSING_EXIT,
39448
+ level: "fail",
39449
+ headline: `${run.summary.toCreate} vector index(es) missing across ${run.summary.banksScanned} bank(s).`,
39450
+ detail: "Recall on those banks silently under-returns. Re-run without --dry-run to fix."
39451
+ };
39452
+ }
39453
+ return {
39454
+ exit: 0,
39455
+ level: "ok",
39456
+ headline: "repair-bank finished.",
39457
+ detail: run.dryRun ? `Coverage complete: ${run.summary.alreadyPresent} index(es) present across ` + `${run.summary.banksScanned} bank(s), nothing to create.` : `${run.summary.created} created, ${run.summary.alreadyPresent} already present ` + `across ${run.summary.banksScanned} bank(s). Re-run \`switchroom doctor\` to confirm.`
39458
+ };
39459
+ }
39460
+ var HINDSIGHT_CONTAINER_NAME = "switchroom-hindsight", HINDSIGHT_ADMIN_BIN = "/app/api/.venv/bin/hindsight-admin", REPAIR_FAILED_EXIT = 1, REPAIR_COVERAGE_MISSING_EXIT = 3, REPAIR_UNVERIFIED_EXIT = 4;
39461
+ var init_hindsight_repair = __esm(() => {
39462
+ init_hindsight_tools();
39463
+ });
39464
+
39449
39465
  // src/host-control/audit-rotation-config.ts
39450
39466
  var DEFAULT_HOSTD_AUDIT_MAX_BYTES, DEFAULT_HOSTD_AUDIT_MAX_FILES = 3;
39451
39467
  var init_audit_rotation_config = __esm(() => {
@@ -45781,6 +45797,8 @@ function classifyObservationScopeSaturation(reports) {
45781
45797
  }
45782
45798
  const unreadable = reports.filter((r) => !r.ok);
45783
45799
  const readable = reports.filter((r) => r.ok);
45800
+ const growing = readable.filter((r) => r.observationsEnabled && r.untaggedCount >= OBSERVATION_SCOPE_UNTAGGED_WARN_COUNT).sort((a, b) => b.untaggedCount - a.untaggedCount);
45801
+ const growthNote = growing.length > 0 ? `${growing.length} bank(s) with an untagged global scope over ` + `${OBSERVATION_SCOPE_UNTAGGED_WARN_COUNT.toLocaleString()} observations ` + `(uncapped curated sink, growth watch): ` + growing.slice(0, OBSERVATION_SCOPE_MAX_LISTED).map((r) => `${r.bankId} ${r.untaggedCount.toLocaleString()}`).join(", ") + (growing.length > OBSERVATION_SCOPE_MAX_LISTED ? `; +${growing.length - OBSERVATION_SCOPE_MAX_LISTED} more` : "") : "";
45784
45802
  if (readable.length === 0) {
45785
45803
  return {
45786
45804
  name,
@@ -45803,7 +45821,11 @@ function classifyObservationScopeSaturation(reports) {
45803
45821
  notes.push(`${disabled.length} bank(s) have observations disabled: ` + disabled.map((r) => r.bankId).join(", "));
45804
45822
  }
45805
45823
  const suffix = notes.length > 0 ? ` \u00b7 ${notes.join(" \u00b7 ")}` : "";
45824
+ const growthSuffix = growthNote ? ` \u00b7 ${growthNote}` : "";
45806
45825
  if (judged.length === 0) {
45826
+ if (growing.length > 0) {
45827
+ return { name, status: "warn", detail: `${growthNote}${suffix}` };
45828
+ }
45807
45829
  return { name, status: "skip", detail: `no bank could be judged${suffix}` };
45808
45830
  }
45809
45831
  const closed = judged.filter((r) => r.cap === 0);
@@ -45835,17 +45857,14 @@ function classifyObservationScopeSaturation(reports) {
45835
45857
  return {
45836
45858
  name,
45837
45859
  status: "fail",
45838
- detail: `${failing.length} observation scope(s) at or over the cap \u2014 consolidation ` + `output is being discarded on them right now: ${list(failing)}` + (warning.length > 0 ? ` \u00b7 ${warning.length} more above ${Math.round(OBSERVATION_SCOPE_WARN_RATIO * 100)}%` : "") + backlog([...failing, ...warning]) + suffix,
45860
+ detail: `${failing.length} observation scope(s) at or over the cap \u2014 consolidation ` + `output is being discarded on them right now: ${list(failing)}` + (warning.length > 0 ? ` \u00b7 ${warning.length} more above ${Math.round(OBSERVATION_SCOPE_WARN_RATIO * 100)}%` : "") + backlog([...failing, ...warning]) + growthSuffix + suffix,
45839
45861
  fix
45840
45862
  };
45841
45863
  }
45842
- if (warning.length > 0) {
45843
- return {
45844
- name,
45845
- status: "warn",
45846
- detail: `${warning.length} observation scope(s) above ` + `${Math.round(OBSERVATION_SCOPE_WARN_RATIO * 100)}% of the cap and still writable: ` + `${list(warning)}` + backlog(warning) + suffix,
45847
- fix
45848
- };
45864
+ if (warning.length > 0 || growing.length > 0) {
45865
+ const scopeClause = warning.length > 0 ? `${warning.length} observation scope(s) above ` + `${Math.round(OBSERVATION_SCOPE_WARN_RATIO * 100)}% of the cap and still writable: ` + `${list(warning)}` + backlog(warning) : "";
45866
+ const detail = warning.length > 0 ? scopeClause + growthSuffix + suffix : `${growthNote}${suffix}`;
45867
+ return { name, status: "warn", detail, ...warning.length > 0 ? { fix } : {} };
45849
45868
  }
45850
45869
  const totalScopes = capped.reduce((n, r) => n + r.scopeCount, 0);
45851
45870
  const detailBase = capped.length === 0 ? `no bank has a positive observation cap` : `${totalScopes} scope(s) across ${capped.length} bank(s) below ` + `${Math.round(OBSERVATION_SCOPE_WARN_RATIO * 100)}% of the cap`;
@@ -45862,7 +45881,7 @@ function classifyObservationScopeSaturation(reports) {
45862
45881
  detail: detailBase + (extra.length > 0 ? ` \u00b7 ${extra.join(" \u00b7 ")}` : "") + suffix
45863
45882
  };
45864
45883
  }
45865
- async function getJson2(url, opts) {
45884
+ async function getJson3(url, opts) {
45866
45885
  const fetchImpl = opts?.fetchImpl ?? fetch;
45867
45886
  const timeoutMs = opts?.timeoutMs ?? 15000;
45868
45887
  const controller = new AbortController;
@@ -45891,10 +45910,11 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
45891
45910
  observationsEnabled: false,
45892
45911
  hasScopeLimitRules: false,
45893
45912
  scopeCount: 0,
45913
+ untaggedCount: 0,
45894
45914
  saturated: [],
45895
45915
  pendingConsolidation: null
45896
45916
  });
45897
- const cfg = await getJson2(`${base}/v1/default/banks/${bank}/config`, opts);
45917
+ const cfg = await getJson3(`${base}/v1/default/banks/${bank}/config`, opts);
45898
45918
  if (!cfg.ok)
45899
45919
  return fail4(cfg.reason);
45900
45920
  const config = cfg.data?.config;
@@ -45907,7 +45927,7 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
45907
45927
  const rules = config["observation_scope_limits"];
45908
45928
  const hasScopeLimitRules = Array.isArray(rules) && rules.length > 0;
45909
45929
  const observationsEnabled = config["enable_observations"] !== false;
45910
- const scopesResp = await getJson2(`${base}/v1/default/banks/${bank}/observations/scopes`, opts);
45930
+ const scopesResp = await getJson3(`${base}/v1/default/banks/${bank}/observations/scopes`, opts);
45911
45931
  if (!scopesResp.ok)
45912
45932
  return fail4(scopesResp.reason);
45913
45933
  const rawScopes = scopesResp.data?.scopes;
@@ -45924,9 +45944,10 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
45924
45944
  scopes.push({ tags, count });
45925
45945
  }
45926
45946
  const saturated = computeScopeSaturation(scopes, rawCap).filter((s) => s.status !== "ok");
45947
+ const untaggedCount = scopes.filter((s) => s.tags.length === 0).reduce((n, s) => n + s.count, 0);
45927
45948
  let pendingConsolidation = null;
45928
45949
  if (saturated.length > 0 && !hasScopeLimitRules && observationsEnabled) {
45929
- const stats = await getJson2(`${base}/v1/default/banks/${bank}/stats`, opts);
45950
+ const stats = await getJson3(`${base}/v1/default/banks/${bank}/stats`, opts);
45930
45951
  if (stats.ok && typeof stats.data?.pending_consolidation === "number") {
45931
45952
  pendingConsolidation = stats.data.pending_consolidation;
45932
45953
  }
@@ -45938,6 +45959,7 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
45938
45959
  observationsEnabled,
45939
45960
  hasScopeLimitRules,
45940
45961
  scopeCount: scopes.length,
45962
+ untaggedCount,
45941
45963
  saturated,
45942
45964
  pendingConsolidation
45943
45965
  };
@@ -45953,7 +45975,7 @@ async function checkObservationScopeSaturation(config, apiUrl, opts) {
45953
45975
  const reports = await Promise.all([...banks].map((bankId) => inspectBankScopes(apiUrl, bankId, opts)));
45954
45976
  return classifyObservationScopeSaturation(reports);
45955
45977
  }
45956
- var OBSERVATION_SCOPE_CHECK_NAME = "hindsight observation scopes", OBSERVATION_SCOPE_WARN_RATIO = 0.8, OBSERVATION_SCOPE_MAX_LISTED = 6;
45978
+ var OBSERVATION_SCOPE_CHECK_NAME = "hindsight observation scopes", OBSERVATION_SCOPE_WARN_RATIO = 0.8, OBSERVATION_SCOPE_MAX_LISTED = 6, OBSERVATION_SCOPE_UNTAGGED_WARN_COUNT = 1e4;
45957
45979
  var init_doctor_observation_scopes = __esm(() => {
45958
45980
  init_merge();
45959
45981
  init_hindsight2();
@@ -70103,6 +70125,9 @@ var init_server4 = __esm(() => {
70103
70125
  ];
70104
70126
  });
70105
70127
 
70128
+ // telegram-plugin/gateway/turn-record-status.ts
70129
+ var ROUTE_FIELD_SHIP_TS = 1785456000;
70130
+
70106
70131
  // src/fleet-health/detect.ts
70107
70132
  function parseTurns(text) {
70108
70133
  const out = [];
@@ -70138,12 +70163,14 @@ function isoFromTs(ts) {
70138
70163
  function detectTurnFindings(agent, turns, opts = {}) {
70139
70164
  const findings = [];
70140
70165
  const silentNoopFloorTs = opts.silentNoopFloorTs ?? SILENT_NOOP_FLOOR_TS;
70166
+ const routeFieldShipTs = opts.routeFieldShipTs ?? ROUTE_FIELD_SHIP_TS;
70141
70167
  for (const t of ownedTurns(agent, turns)) {
70142
70168
  const tid = typeof t.turn_id === "string" ? t.turn_id : "?";
70143
70169
  const st = t.status;
70144
70170
  const tl = typeof t.tools === "number" ? t.tools : 0;
70145
70171
  const dur = typeof t.duration_ms === "number" ? t.duration_ms : 0;
70146
70172
  const synthetic = tid.includes("synthetic-");
70173
+ const route = typeof t.route === "string" ? t.route : undefined;
70147
70174
  const ts = isoFromTs(t.ts);
70148
70175
  if (st === "send_failed") {
70149
70176
  findings.push({
@@ -70172,13 +70199,33 @@ function detectTurnFindings(agent, turns, opts = {}) {
70172
70199
  });
70173
70200
  }
70174
70201
  if (st === "complete" && tl === 0 && !synthetic && t.ts != null && t.ts >= silentNoopFloorTs) {
70175
- findings.push({
70176
- signal: "silent-no-op-candidate",
70177
- agent,
70178
- turn_id: tid,
70179
- log_pointer: `turns.jsonl:${tid} tools=0`,
70180
- ts
70181
- });
70202
+ if (route === "flush") {
70203
+ findings.push({
70204
+ signal: "flush-recovered-turn",
70205
+ agent,
70206
+ turn_id: tid,
70207
+ log_pointer: `turns.jsonl:${tid} tools=0 route=flush`,
70208
+ ts
70209
+ });
70210
+ } else if (route === "reply" || route === "stream") {} else if (route === "none") {
70211
+ findings.push({
70212
+ signal: "silent-no-op-candidate",
70213
+ agent,
70214
+ turn_id: tid,
70215
+ log_pointer: `turns.jsonl:${tid} tools=0 route=none`,
70216
+ ts
70217
+ });
70218
+ } else {
70219
+ if (t.ts >= routeFieldShipTs) {
70220
+ findings.push({
70221
+ signal: "silent-no-op-candidate",
70222
+ agent,
70223
+ turn_id: tid,
70224
+ log_pointer: `turns.jsonl:${tid} tools=0`,
70225
+ ts
70226
+ });
70227
+ }
70228
+ }
70182
70229
  }
70183
70230
  }
70184
70231
  return findings;
@@ -70292,6 +70339,12 @@ var init_mapping = __esm(() => {
70292
70339
  job_spec: "know-what-my-agent-is-doing",
70293
70340
  signature: "silent-no-op:completed-zero-tools"
70294
70341
  },
70342
+ "flush-recovered-turn": {
70343
+ failure_mode: "drift",
70344
+ severity: 1,
70345
+ job_spec: "know-what-my-agent-is-doing",
70346
+ signature: "flush-recovered:completed-zero-tools-route-flush"
70347
+ },
70295
70348
  "duplicate-delivery-represent": {
70296
70349
  failure_mode: "duplicate",
70297
70350
  severity: 2,
@@ -85474,6 +85527,174 @@ function reflectAcrossAgents(config) {
85474
85527
  return { eligible, excluded, commands };
85475
85528
  }
85476
85529
 
85530
+ // src/memory/observation-scope-cleanup.ts
85531
+ var CLEANUP_VOLATILE_PATTERNS = [
85532
+ /^parent_session:/,
85533
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(-sub-.+)?$/
85534
+ ];
85535
+ var CLEANUP_STALE_PATTERNS = [/^agent-[0-9a-f]{8,}$/];
85536
+ function matchesAny(tag, patterns) {
85537
+ return patterns.some((p) => p.test(tag));
85538
+ }
85539
+ function scopeKey(tags) {
85540
+ return tags.length === 0 ? "shared" : [...tags].sort().join(",");
85541
+ }
85542
+ function planScopeCleanup(bankId, scopes, options = {}) {
85543
+ const volatilePatterns = options.volatilePatterns ?? CLEANUP_VOLATILE_PATTERNS;
85544
+ const stalePatterns = options.stalePatterns ?? CLEANUP_STALE_PATTERNS;
85545
+ const retireStale = options.retireStale ?? false;
85546
+ const totalScopes = scopes.length;
85547
+ const totalObservations = scopes.reduce((n, s) => n + s.count, 0);
85548
+ const reclassified = [];
85549
+ const breakdown = {
85550
+ allVolatileScopes: 0,
85551
+ allVolatileObservations: 0,
85552
+ mixedScopes: 0,
85553
+ mixedObservations: 0,
85554
+ staleScopes: 0,
85555
+ staleObservations: 0
85556
+ };
85557
+ const targets = new Map;
85558
+ for (const s of scopes) {
85559
+ const volatileTags = s.tags.filter((t) => matchesAny(t, volatilePatterns));
85560
+ const staleTags = s.tags.filter((t) => matchesAny(t, stalePatterns));
85561
+ const stripped = s.tags.filter((t) => {
85562
+ if (matchesAny(t, volatilePatterns))
85563
+ return false;
85564
+ if (retireStale && matchesAny(t, stalePatterns))
85565
+ return false;
85566
+ return true;
85567
+ });
85568
+ const toKey = scopeKey(stripped);
85569
+ const changed = scopeKey(s.tags) !== toKey;
85570
+ const hasStable = s.tags.some((t) => !matchesAny(t, volatilePatterns) && !matchesAny(t, stalePatterns));
85571
+ if (staleTags.length > 0) {
85572
+ breakdown.staleScopes += 1;
85573
+ breakdown.staleObservations += s.count;
85574
+ }
85575
+ if (volatileTags.length > 0 && !hasStable && staleTags.length === 0) {
85576
+ breakdown.allVolatileScopes += 1;
85577
+ breakdown.allVolatileObservations += s.count;
85578
+ } else if (volatileTags.length > 0 && hasStable) {
85579
+ breakdown.mixedScopes += 1;
85580
+ breakdown.mixedObservations += s.count;
85581
+ }
85582
+ const entry = targets.get(toKey) ?? { sources: 0, observations: 0, changed: false };
85583
+ entry.sources += 1;
85584
+ entry.observations += s.count;
85585
+ entry.changed = entry.changed || changed;
85586
+ targets.set(toKey, entry);
85587
+ if (changed) {
85588
+ reclassified.push({
85589
+ from: s.tags,
85590
+ to: toKey,
85591
+ count: s.count,
85592
+ strippedVolatile: volatileTags,
85593
+ strippedStale: retireStale ? staleTags : []
85594
+ });
85595
+ }
85596
+ }
85597
+ reclassified.sort((a, b) => b.count - a.count);
85598
+ const merges = [...targets.entries()].filter(([, v]) => v.sources > 1 && v.changed).map(([target, v]) => ({ target, sourceScopes: v.sources, observations: v.observations })).sort((a, b) => b.observations - a.observations);
85599
+ const affectedObservations = reclassified.reduce((n, r) => n + r.count, 0);
85600
+ const resultingScopeCount = targets.size;
85601
+ return {
85602
+ bankId,
85603
+ ok: true,
85604
+ totalScopes,
85605
+ totalObservations,
85606
+ reclassified,
85607
+ merges,
85608
+ affectedScopes: reclassified.length,
85609
+ affectedObservations,
85610
+ resultingScopeCount,
85611
+ scopeReduction: totalScopes - resultingScopeCount,
85612
+ breakdown
85613
+ };
85614
+ }
85615
+ async function getJson2(url, opts) {
85616
+ const fetchImpl = opts?.fetchImpl ?? fetch;
85617
+ const timeoutMs = opts?.timeoutMs ?? 15000;
85618
+ const controller = new AbortController;
85619
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
85620
+ try {
85621
+ const resp = await fetchImpl(url, { signal: controller.signal });
85622
+ clearTimeout(timeout);
85623
+ if (!resp.ok)
85624
+ return { ok: false, reason: `HTTP ${resp.status}` };
85625
+ return { ok: true, data: await resp.json() };
85626
+ } catch (err) {
85627
+ clearTimeout(timeout);
85628
+ if (err.name === "AbortError")
85629
+ return { ok: false, reason: "Timeout" };
85630
+ return { ok: false, reason: String(err.message ?? err) };
85631
+ }
85632
+ }
85633
+ async function planBankCleanup(apiUrl, bankId, options = {}, fetchOpts) {
85634
+ const base = hindsightRestBase(apiUrl);
85635
+ const bank = encodeURIComponent(bankId);
85636
+ const resp = await getJson2(`${base}/v1/default/banks/${bank}/observations/scopes`, fetchOpts);
85637
+ if (!resp.ok) {
85638
+ return {
85639
+ bankId,
85640
+ ok: false,
85641
+ reason: resp.reason,
85642
+ totalScopes: 0,
85643
+ totalObservations: 0,
85644
+ reclassified: [],
85645
+ merges: [],
85646
+ affectedScopes: 0,
85647
+ affectedObservations: 0,
85648
+ resultingScopeCount: 0,
85649
+ scopeReduction: 0,
85650
+ breakdown: {
85651
+ allVolatileScopes: 0,
85652
+ allVolatileObservations: 0,
85653
+ mixedScopes: 0,
85654
+ mixedObservations: 0,
85655
+ staleScopes: 0,
85656
+ staleObservations: 0
85657
+ }
85658
+ };
85659
+ }
85660
+ const raw = resp.data?.scopes;
85661
+ const scopes = [];
85662
+ if (Array.isArray(raw)) {
85663
+ for (const entry of raw) {
85664
+ const tags = entry?.tags;
85665
+ const count = entry?.count;
85666
+ if (!Array.isArray(tags) || typeof count !== "number")
85667
+ continue;
85668
+ if (!tags.every((t) => typeof t === "string"))
85669
+ continue;
85670
+ scopes.push({ tags, count });
85671
+ }
85672
+ }
85673
+ return planScopeCleanup(bankId, scopes, options);
85674
+ }
85675
+ function formatCleanupPlan(plan, maxMerges = 10) {
85676
+ if (!plan.ok) {
85677
+ return `bank ${plan.bankId}: UNREADABLE (${plan.reason ?? "unknown"})`;
85678
+ }
85679
+ const b = plan.breakdown;
85680
+ const lines = [];
85681
+ lines.push(`bank ${plan.bankId}`);
85682
+ lines.push(` today: ${plan.totalScopes} scopes, ${plan.totalObservations.toLocaleString()} observations`);
85683
+ lines.push(` legacy fragmentation:` + ` all-volatile ${b.allVolatileScopes} scopes / ${b.allVolatileObservations.toLocaleString()} obs;` + ` mixed ${b.mixedScopes} scopes / ${b.mixedObservations.toLocaleString()} obs;` + ` stale agent-<hex> ${b.staleScopes} scopes / ${b.staleObservations.toLocaleString()} obs`);
85684
+ lines.push(` WOULD change: ${plan.affectedScopes} scopes` + ` (${plan.affectedObservations.toLocaleString()} obs re-homed),` + ` ${plan.totalScopes} \u2192 ${plan.resultingScopeCount} scopes` + ` (\u2212${plan.scopeReduction})`);
85685
+ if (plan.merges.length > 0) {
85686
+ lines.push(` top merges:`);
85687
+ for (const m of plan.merges.slice(0, maxMerges)) {
85688
+ lines.push(` ${m.sourceScopes} scopes \u2192 ${m.target} (${m.observations.toLocaleString()} obs)`);
85689
+ }
85690
+ if (plan.merges.length > maxMerges) {
85691
+ lines.push(` +${plan.merges.length - maxMerges} more merge targets`);
85692
+ }
85693
+ }
85694
+ return lines.join(`
85695
+ `);
85696
+ }
85697
+
85477
85698
  // src/cli/memory.ts
85478
85699
  init_helpers();
85479
85700
  init_hindsight();
@@ -86200,6 +86421,55 @@ Demoting memory ${source_default.cyan(memoryId)}`));
86200
86421
  ` + " If the ID itself is suspect: `switchroom memory recall-log " + agent + " --json`."));
86201
86422
  process.exit(1);
86202
86423
  }));
86424
+ memory.command("cleanup-scopes [agent]").description("Dry-run audit of legacy pre-#4035 observation-scope fragmentation " + "(per-session UUID islands, stale agent-<hex> scopes). Reads only; " + "prints the blast radius of a curated re-home. Pass an agent, or --all.").option("--all", "Audit every agent bank and profile bank").option("--retire-stale", "Also fold stale agent-<hex> producer scopes into shared (opt-in; not part of the #4035 heal)").option("--execute", "(refused) live mutation is not supported \u2014 see the printed reason").option("--json", "Emit the plans as JSON").action(withConfigError(async (agent, opts) => {
86425
+ const config = getConfig(program3);
86426
+ const apiUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
86427
+ if (opts.execute) {
86428
+ console.error(source_default.red("\u2717 --execute is not supported: legacy-scope healing is not mechanizable"));
86429
+ console.error(source_default.gray(` Hindsight exposes no per-memory tag-write (update_memory / PATCH memories/{id}
86430
+ ` + " silently drop a `tags` argument, #3772) and no per-scope delete (DELETE\n" + ` .../observations clears the WHOLE bank). So an observation's scope cannot be
86431
+ ` + ` edited in place, and this tool stays a read-only audit. A live heal needs an
86432
+ ` + ` upstream tag-write path or a deliberate, operator-approved enumerate-and-
86433
+ ` + " invalidate pass \u2014 neither is run from here."));
86434
+ process.exit(1);
86435
+ }
86436
+ const banks = new Set;
86437
+ if (agent) {
86438
+ if (!config.agents[agent]) {
86439
+ console.error(source_default.red(`Agent "${agent}" is not defined in switchroom.yaml`));
86440
+ process.exit(1);
86441
+ }
86442
+ banks.add(getCollectionForAgent(agent, config));
86443
+ } else if (opts.all) {
86444
+ for (const name of Object.keys(config.agents)) {
86445
+ banks.add(getCollectionForAgent(name, config));
86446
+ }
86447
+ for (const bank of collectProfileBanks(config))
86448
+ banks.add(bank);
86449
+ } else {
86450
+ console.error(source_default.red("Pass an agent name, or --all to audit every bank."));
86451
+ process.exit(1);
86452
+ }
86453
+ const plans = await Promise.all([...banks].map((bankId) => planBankCleanup(apiUrl, bankId, { retireStale: opts.retireStale })));
86454
+ plans.sort((a, b) => b.affectedObservations - a.affectedObservations);
86455
+ if (opts.json) {
86456
+ console.log(JSON.stringify(plans, null, 2));
86457
+ return;
86458
+ }
86459
+ console.log(source_default.bold(`
86460
+ Observation-scope cleanup \u2014 DRY RUN (no writes)
86461
+ `));
86462
+ console.log(source_default.gray(` api=${apiUrl} retireStale=${Boolean(opts.retireStale)}
86463
+ `));
86464
+ for (const plan of plans) {
86465
+ console.log(formatCleanupPlan(plan));
86466
+ console.log();
86467
+ }
86468
+ const totalObs = plans.reduce((n, p) => n + p.affectedObservations, 0);
86469
+ const totalScopes = plans.reduce((n, p) => n + p.scopeReduction, 0);
86470
+ console.log(source_default.cyan(`Fleet total (would change): ${totalObs.toLocaleString()} observations re-homed, ` + `${totalScopes.toLocaleString()} scopes removed.`));
86471
+ console.log(source_default.gray("Nothing was written. Live execution is not supported from this tool " + "(--execute prints why)."));
86472
+ }));
86203
86473
  const restBase = (url) => (url ?? HINDSIGHT_DEFAULT_MCP_URL).replace(/\/mcp\/?$/, "").replace(/\/$/, "");
86204
86474
  const VALID_BANK = /^[a-zA-Z0-9_.-]+$/;
86205
86475
  const profile = memory.command("profile").description("Author + inspect operator profile banks (shared/per-user memory; wire via memory.recall.additional_banks)");
@@ -93801,6 +94071,7 @@ init_atomic();
93801
94071
  init_resolve_version();
93802
94072
  init_audit_reader();
93803
94073
  init_hindsight();
94074
+ init_hindsight2();
93804
94075
 
93805
94076
  // src/cli/deploy-version-guard.ts
93806
94077
  import { spawnSync as spawnSync18 } from "node:child_process";
@@ -93848,22 +94119,31 @@ function checkDowngrade(opts) {
93848
94119
  init_agent_owned_tree();
93849
94120
  init_component_versions();
93850
94121
  var VERIFY_COMPONENTS_STEP = "verify-components";
94122
+ var ENSURE_BANKS_STEP = "ensure-banks";
93851
94123
  function normalizeVersion(v) {
93852
94124
  return v.trim().replace(/^v/, "");
93853
94125
  }
93854
94126
  function isVersionAssertable(target) {
93855
94127
  return /^v?\d+\.\d+\.\d+$/.test(target.trim());
93856
94128
  }
93857
- function orderAgentsCanaryFirst(agents) {
94129
+ var ROLLOUT_PRIORITY_AGENTS = [
94130
+ "overlord",
94131
+ "carrie",
94132
+ "finn"
94133
+ ];
94134
+ function orderAgentsForRoll(agents) {
93858
94135
  const canary = agents.filter((a) => a === "test-harness");
93859
- const rest = agents.filter((a) => a !== "test-harness");
93860
- return [...canary, ...rest];
94136
+ const priority = ROLLOUT_PRIORITY_AGENTS.filter((p) => p !== "test-harness" && agents.includes(p));
94137
+ const prioritySet = new Set(priority);
94138
+ const rest = agents.filter((a) => a !== "test-harness" && !prioritySet.has(a));
94139
+ return [...canary, ...priority, ...rest];
93861
94140
  }
93862
94141
  function planRollout(agents, opts = {}) {
93863
94142
  const steps = [];
93864
- const ordered = orderAgentsCanaryFirst(agents);
94143
+ const ordered = orderAgentsForRoll(agents);
93865
94144
  if (opts.hostdContext) {
93866
94145
  steps.push({ kind: "apply" });
94146
+ steps.push({ kind: "refresh-hindsight" });
93867
94147
  const [canary, ...rest] = ordered;
93868
94148
  if (canary !== undefined) {
93869
94149
  steps.push({ kind: "restart-agent", agent: canary });
@@ -93879,23 +94159,26 @@ function planRollout(agents, opts = {}) {
93879
94159
  if (!opts.skipWeb) {
93880
94160
  steps.push({ kind: "refresh-web" });
93881
94161
  }
93882
- steps.push({ kind: "refresh-hindsight" });
93883
94162
  steps.push({ kind: "sweep" });
94163
+ steps.push({ kind: "ensure-banks" });
93884
94164
  steps.push({ kind: "verify-components" });
93885
94165
  return steps;
93886
94166
  }
93887
94167
  if (opts.pinToPersist)
93888
94168
  steps.push({ kind: "persist-pin", pin: opts.pinToPersist });
93889
94169
  steps.push({ kind: "apply" });
94170
+ if (!opts.skipWeb) {
94171
+ steps.push({ kind: "refresh-hindsight" });
94172
+ }
93890
94173
  for (const agent of ordered) {
93891
94174
  steps.push({ kind: "restart-agent", agent });
93892
94175
  }
93893
94176
  if (!opts.skipWeb) {
93894
94177
  steps.push({ kind: "refresh-web" });
93895
94178
  steps.push({ kind: "refresh-hostd" });
93896
- steps.push({ kind: "refresh-hindsight" });
93897
94179
  }
93898
94180
  steps.push({ kind: "sweep" });
94181
+ steps.push({ kind: "ensure-banks" });
93899
94182
  steps.push({ kind: "verify-components" });
93900
94183
  return steps;
93901
94184
  }
@@ -93926,6 +94209,9 @@ function formatRolloutPlan(steps, target) {
93926
94209
  case "sweep":
93927
94210
  lines.push(` ${n}. sweep \u2014 print per-agent version table`);
93928
94211
  break;
94212
+ case "ensure-banks":
94213
+ lines.push(` ${n}. ensure-banks \u2014 create any missing agent Hindsight bank ` + `(idempotent backstop; structured failure if one stays missing)`);
94214
+ break;
93929
94215
  case "verify-components":
93930
94216
  lines.push(` ${n}. verify-components \u2014 assert EVERY in-scope component is on ${target} ` + `(same inventory \`switchroom update --check\` uses); non-zero exit if any is behind`);
93931
94217
  break;
@@ -93987,7 +94273,7 @@ function evaluateRolloutDrift(components, target, steps, hostdContext) {
93987
94273
  const { gated, exempt } = partitionDrift(report.behind, { owners, rolledAgents });
93988
94274
  return { gated, exempt, target: report.target };
93989
94275
  }
93990
- function executeRollout(steps, target, deps, execOpts = {}) {
94276
+ async function executeRollout(steps, target, deps, execOpts = {}) {
93991
94277
  const targetNorm = normalizeVersion(target);
93992
94278
  const rolled = [];
93993
94279
  const warnings = [];
@@ -93996,6 +94282,7 @@ function executeRollout(steps, target, deps, execOpts = {}) {
93996
94282
  let agentIndex = 0;
93997
94283
  let pinPersisted = false;
93998
94284
  let driftVerdict = null;
94285
+ const bankEnsureFailed = [];
93999
94286
  const fail4 = (partial) => {
94000
94287
  let pinReverted = false;
94001
94288
  if (pinPersisted && deps.revertPin) {
@@ -94100,6 +94387,10 @@ function executeRollout(steps, target, deps, execOpts = {}) {
94100
94387
  const webTag = deps.webImageTag?.();
94101
94388
  if (webTag !== undefined && webTag !== null && isVersionAssertable(webTag) && normalizeVersion(webTag) !== targetNorm) {
94102
94389
  warnings.push(`web refresh completed but switchroom-web is running ${webTag}, ` + `NOT the roll target ${target} (most likely its downgrade guard ` + `skipped the install). Finish it host-side: \`switchroom webd ` + `install --tag ${target} --allow-downgrade\`.`);
94390
+ break;
94391
+ }
94392
+ if (webTag !== undefined && webTag !== null && isVersionAssertable(webTag) && normalizeVersion(webTag) === targetNorm) {
94393
+ emit({ phase: "web-refresh-done", target });
94103
94394
  }
94104
94395
  break;
94105
94396
  }
@@ -94115,9 +94406,11 @@ function executeRollout(steps, target, deps, execOpts = {}) {
94115
94406
  const hindsightExists = deps.hindsightExists ?? isHindsightContainerExists;
94116
94407
  if (!hindsightExists()) {
94117
94408
  deps.log(`ROLL_STEP refresh-hindsight \u2014 no hindsight container on this host; skipping`);
94409
+ emit({ phase: "hindsight-skipped", target });
94118
94410
  break;
94119
94411
  }
94120
94412
  deps.log(`ROLL_STEP refresh-hindsight \u2014 memory setup --recreate --tag ${target} (pull ${target} + recreate)`);
94413
+ emit({ phase: "hindsight-refresh", target });
94121
94414
  const r = deps.run(["memory", "setup", "--recreate", "--tag", target]);
94122
94415
  if (r.status !== 0) {
94123
94416
  deps.log(` \u2717 hindsight recreate FAILED \u2014 the memory backend is DOWN (the ` + `recreate stopped + removed the old container before the failed ` + `start). Run \`switchroom memory setup\` to bring it back. \u2014 STOPPING`);
@@ -94126,6 +94419,7 @@ function executeRollout(steps, target, deps, execOpts = {}) {
94126
94419
  ...r.timedOut ? { timedOut: true } : {}
94127
94420
  });
94128
94421
  }
94422
+ emit({ phase: "hindsight-refresh-done", target });
94129
94423
  break;
94130
94424
  }
94131
94425
  case "sweep": {
@@ -94136,6 +94430,38 @@ function executeRollout(steps, target, deps, execOpts = {}) {
94136
94430
  }
94137
94431
  break;
94138
94432
  }
94433
+ case "ensure-banks": {
94434
+ const hindsightExists = deps.hindsightExists ?? isHindsightContainerExists;
94435
+ if (!hindsightExists()) {
94436
+ deps.log(`ROLL_STEP ensure-banks \u2014 no hindsight container on this host; skipping`);
94437
+ break;
94438
+ }
94439
+ if (!deps.ensureBank) {
94440
+ warnings.push(`ensure-banks ran with NO bank-ensure hook wired, so brand-new ` + `agents' Hindsight banks were NOT verified. If an agent's first ` + `\`retain\` fails with a foreign-key error, its bank was never ` + `created \u2014 recreate it by restarting that agent.`);
94441
+ break;
94442
+ }
94443
+ deps.log(`ROLL_STEP ensure-banks \u2014 creating any missing agent bank (idempotent) for ${rolled.length} agent(s)`);
94444
+ for (const a of rolled) {
94445
+ let res;
94446
+ try {
94447
+ res = await deps.ensureBank(a);
94448
+ } catch (e) {
94449
+ res = { ok: false, reason: e.message };
94450
+ }
94451
+ if (res.ok) {
94452
+ deps.log(` ${a}: bank present`);
94453
+ continue;
94454
+ }
94455
+ if (/unreachable|timeout/i.test(res.reason)) {
94456
+ deps.log(` ${a}: bank check ${res.reason.toLowerCase()} (degraded to warning)`);
94457
+ warnings.push(`ensure-banks could not reach Hindsight to verify ${a}'s bank ` + `(${res.reason}). If ${a} is brand-new and its first \`retain\` ` + `fails with a foreign-key error, restart it to recreate the bank.`);
94458
+ continue;
94459
+ }
94460
+ deps.log(` \u2717 ${a}: bank could NOT be created \u2014 ${res.reason}`);
94461
+ bankEnsureFailed.push(a);
94462
+ }
94463
+ break;
94464
+ }
94139
94465
  case "verify-components": {
94140
94466
  deps.log(`ROLL_STEP verify-components \u2014 asserting every in-scope component is on ${target}`);
94141
94467
  if (!deps.collectComponents) {
@@ -94174,6 +94500,19 @@ function executeRollout(steps, target, deps, execOpts = {}) {
94174
94500
  warnings.push(`roll succeeded but committing the durable release.pin threw: ` + `${e.message}. The pin IS written; a stale rollout ` + `journal may cause a later boot to revert it \u2014 verify host-side.`);
94175
94501
  }
94176
94502
  }
94503
+ if (bankEnsureFailed.length > 0) {
94504
+ for (const a of bankEnsureFailed) {
94505
+ warnings.push(`${a}'s Hindsight bank could NOT be created during the roll \u2014 its ` + `first \`retain\` will fail with a foreign-key error until the bank ` + `exists. Restart ${a} to re-run its bank creation now that Hindsight ` + `is back up.`);
94506
+ }
94507
+ deps.log(` \u2717 ensure-banks FAILED \u2014 ${bankEnsureFailed.length} agent bank(s) ` + `could not be created: ${bankEnsureFailed.join(", ")}`);
94508
+ return {
94509
+ ok: false,
94510
+ rolled,
94511
+ warnings,
94512
+ failedStep: ENSURE_BANKS_STEP,
94513
+ drifted: bankEnsureFailed
94514
+ };
94515
+ }
94177
94516
  if (driftVerdict && driftVerdict.gated.length > 0) {
94178
94517
  const normalizedTarget = `v${normalizeVersion(target)}`;
94179
94518
  for (const c of driftVerdict.gated) {
@@ -94340,7 +94679,12 @@ function createRolloutDeps(params) {
94340
94679
  `);
94341
94680
  }
94342
94681
  return outcome.reverted;
94343
- }
94682
+ },
94683
+ ...params.hindsightApiUrl && params.resolveBankId ? {
94684
+ ensureBank: (agent) => createBank(params.hindsightApiUrl, params.resolveBankId(agent), {
94685
+ timeoutMs: 5000
94686
+ })
94687
+ } : {}
94344
94688
  };
94345
94689
  }
94346
94690
  function registerRolloutCommand(program3) {
@@ -94433,10 +94777,23 @@ function registerRolloutCommand(program3) {
94433
94777
  }
94434
94778
  const configPath = getConfigPath(program3);
94435
94779
  const scriptPath = process.argv[1] ?? "";
94436
- const deps = createRolloutDeps({ configPath, scriptPath, hostdCtx });
94780
+ let hindsightApiUrl;
94781
+ let resolveBankId;
94782
+ if (isHindsightEnabled(config)) {
94783
+ const rawUrl = config.memory?.config?.url;
94784
+ hindsightApiUrl = rawUrl ? rawUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "") : HINDSIGHT_DEFAULT_API_BASE_URL;
94785
+ resolveBankId = (agent) => config.agents?.[agent]?.memory?.collection ?? agent;
94786
+ }
94787
+ const deps = createRolloutDeps({
94788
+ configPath,
94789
+ scriptPath,
94790
+ hostdCtx,
94791
+ hindsightApiUrl,
94792
+ resolveBankId
94793
+ });
94437
94794
  process.stdout.write(`${opts.allowDowngrade ? "Rolling back" : "Rolling"} ${requested.length} agent(s) to ${target}\u2026
94438
94795
  `);
94439
- const result = executeRollout(steps, target, deps, {
94796
+ const result = await executeRollout(steps, target, deps, {
94440
94797
  hostdContext: hostdCtx,
94441
94798
  allowDowngrade: opts.allowDowngrade
94442
94799
  });
@@ -94457,6 +94814,15 @@ function registerRolloutCommand(program3) {
94457
94814
  \u2717 Rollout INCOMPLETE \u2014 ${(result.drifted ?? []).length} component(s) ` + `still behind ${target}: ${(result.drifted ?? []).join(", ") || "unknown"}.
94458
94815
  ` + ` ${result.rolled.length} agent(s) DID reach ${target} and the pin is ` + `committed \u2014 re-running the agent roll will not fix this.
94459
94816
  ` + ` Run the per-component command named in the warning(s) above, then ` + `\`switchroom update --check\` to confirm the host is converged.
94817
+ `);
94818
+ process.exitCode = 1;
94819
+ return;
94820
+ }
94821
+ if (!result.ok && result.failedStep === ENSURE_BANKS_STEP) {
94822
+ process.stderr.write(`
94823
+ \u2717 Rollout INCOMPLETE \u2014 ${(result.drifted ?? []).length} agent(s) ` + `reached ${target} but their Hindsight bank could NOT be created: ` + `${(result.drifted ?? []).join(", ") || "unknown"}.
94824
+ ` + ` The pin is committed and the agents ARE on the target \u2014 re-running ` + `the agent roll will not fix this.
94825
+ ` + ` Restart the named agent(s) to re-run bank creation now that ` + `Hindsight is back up (their first \`retain\` fails with a foreign-key ` + `error until the bank exists).
94460
94826
  `);
94461
94827
  process.exitCode = 1;
94462
94828
  return;