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
@@ -10977,7 +10977,7 @@ var init_zod = __esm(() => {
10977
10977
  });
10978
10978
 
10979
10979
  // src/memory/observation-scopes.ts
10980
- var OBSERVATION_SCOPES;
10980
+ var OBSERVATION_SCOPES, OBSERVATION_SCOPE_STRATEGIES;
10981
10981
  var init_observation_scopes = __esm(() => {
10982
10982
  OBSERVATION_SCOPES = [
10983
10983
  "per_tag",
@@ -10985,10 +10985,16 @@ var init_observation_scopes = __esm(() => {
10985
10985
  "all_combinations",
10986
10986
  "shared"
10987
10987
  ];
10988
+ OBSERVATION_SCOPE_STRATEGIES = [
10989
+ "curated",
10990
+ "shared",
10991
+ "combined",
10992
+ "off"
10993
+ ];
10988
10994
  });
10989
10995
 
10990
10996
  // src/config/schema.ts
10991
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
10997
+ 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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
10992
10998
  var init_schema = __esm(() => {
10993
10999
  init_zod();
10994
11000
  init_observation_scopes();
@@ -11103,7 +11109,8 @@ var init_schema = __esm(() => {
11103
11109
  allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
11104
11110
  deny: exports_external.array(exports_external.string()).default([]).describe("Denied tools (overrides allow)")
11105
11111
  }).optional();
11106
- ObservationScopesSchema = exports_external.enum(OBSERVATION_SCOPES).optional().describe("Per-row observation scope stamped on every memory this agent " + `retains. "shared" makes Hindsight's consolidation write the ` + "resulting observations into ONE global untagged scope instead of a " + "scope per tag — what several agents pooling one bank need so their " + "observations actually merge rather than sitting in parallel " + "per-tag silos. OMITTED BY DEFAULT: unset means the field never goes " + "on the wire and the engine's own default stands, which is the " + "shipped behaviour. Applies to every retain path (Stop hook, " + "sidechain, boot reconcile, queue drain, backfill, session-handoff " + "mirror) and is carried on the queued payload, so a retain that fails " + "now and drains later still lands in this scope. Accepted values: " + `${OBSERVATION_SCOPES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
11112
+ 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 — 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).");
11113
+ 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 — keeping the stable " + "semantic ones on the source fact — 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).");
11107
11114
  AgentMemorySchema = exports_external.object({
11108
11115
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
11109
11116
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
@@ -11139,6 +11146,7 @@ var init_schema = __esm(() => {
11139
11146
  }).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis — a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
11140
11147
  directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true — Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never …", "from now on …", ' + `"stop doing …", a stated preference, "that's wrong, it's …") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex — the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
11141
11148
  observation_scopes: ObservationScopesSchema,
11149
+ observation_scope_strategy: ObservationScopeStrategySchema,
11142
11150
  recall: exports_external.object({
11143
11151
  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)."),
11144
11152
  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."),
@@ -11539,6 +11547,7 @@ var init_schema = __esm(() => {
11539
11547
  isolation: exports_external.enum(["default", "strict"]).optional(),
11540
11548
  directive_capture_nudge: exports_external.boolean().optional(),
11541
11549
  observation_scopes: ObservationScopesSchema,
11550
+ observation_scope_strategy: ObservationScopeStrategySchema,
11542
11551
  recall: exports_external.object({
11543
11552
  max_memories: exports_external.number().int().min(0).optional(),
11544
11553
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
@@ -21001,6 +21010,7 @@ function parsePendingRolloutMarker(raw) {
21001
21010
  if (o.allow_downgrade !== undefined && typeof o.allow_downgrade !== "boolean") {
21002
21011
  return null;
21003
21012
  }
21013
+ const narrationIdValid = typeof o.narration_message_id === "number" && Number.isInteger(o.narration_message_id) && o.narration_message_id > 0;
21004
21014
  return {
21005
21015
  v: 1,
21006
21016
  request_id: o.request_id,
@@ -21008,6 +21018,7 @@ function parsePendingRolloutMarker(raw) {
21008
21018
  ...o.agents !== undefined ? { agents: o.agents } : {},
21009
21019
  ...o.skip_web !== undefined ? { skip_web: o.skip_web } : {},
21010
21020
  ...o.allow_downgrade !== undefined ? { allow_downgrade: o.allow_downgrade } : {},
21021
+ ...narrationIdValid ? { narration_message_id: o.narration_message_id } : {},
21011
21022
  caller: caller.kind === "agent" ? { kind: "agent", name: caller.name } : { kind: "operator" },
21012
21023
  created_at: o.created_at,
21013
21024
  prior_hostd_version: o.prior_hostd_version
@@ -21311,7 +21322,7 @@ function allocateAgentUid(name) {
21311
21322
  }
21312
21323
 
21313
21324
  // src/build-info.ts
21314
- var VERSION = "0.19.39";
21325
+ var VERSION = "0.19.41";
21315
21326
 
21316
21327
  // src/setup/hindsight-recall-tunables.ts
21317
21328
  var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
@@ -28405,6 +28416,10 @@ function parseRolloutPhaseLine(line) {
28405
28416
  "agent-done",
28406
28417
  "persist-pin",
28407
28418
  "web-refresh",
28419
+ "web-refresh-done",
28420
+ "hindsight-refresh",
28421
+ "hindsight-refresh-done",
28422
+ "hindsight-skipped",
28408
28423
  "hostd-web-deferred",
28409
28424
  "self-bump",
28410
28425
  "self-bump-done"
@@ -29385,6 +29400,14 @@ function phaseLine(s) {
29385
29400
  return "persisting pin";
29386
29401
  case "web-refresh":
29387
29402
  return "refreshing web dashboard (webd install)";
29403
+ case "web-refresh-done":
29404
+ return "web dashboard refreshed";
29405
+ case "hindsight-refresh":
29406
+ return "refreshing hindsight (memory backend recreate)";
29407
+ case "hindsight-refresh-done":
29408
+ return "hindsight refreshed";
29409
+ case "hindsight-skipped":
29410
+ return "hindsight — no container on this host; skipped";
29388
29411
  case "hostd-web-deferred":
29389
29412
  return "hostd refresh deferred (run host-side)";
29390
29413
  case "self-bump":
@@ -29452,6 +29475,33 @@ function checklistLines(s, compact) {
29452
29475
  }
29453
29476
  return lines;
29454
29477
  }
29478
+ function singletonGlyph(status) {
29479
+ switch (status) {
29480
+ case "updated":
29481
+ return "✓";
29482
+ case "updating":
29483
+ return "⏳";
29484
+ case "deferred":
29485
+ return "⧗";
29486
+ case "skipped":
29487
+ return "○";
29488
+ case "failed":
29489
+ return "✗";
29490
+ default:
29491
+ return "·";
29492
+ }
29493
+ }
29494
+ function singletonLines(s) {
29495
+ const singletons = s.singletons ?? [];
29496
+ if (singletons.length === 0)
29497
+ return [];
29498
+ const lines = ["**Singletons / shared:**"];
29499
+ for (const g of singletons) {
29500
+ const detail = g.detail ? ` — ${g.detail}` : "";
29501
+ lines.push(`- ${singletonGlyph(g.status)} \`${g.name}\`${detail}`);
29502
+ }
29503
+ return lines;
29504
+ }
29455
29505
  function etaLine(s) {
29456
29506
  if (s.m === undefined)
29457
29507
  return null;
@@ -29485,6 +29535,7 @@ function renderWith(s, compact) {
29485
29535
  const rolled = s.rolled ?? [];
29486
29536
  const rolledCount = rolled.length;
29487
29537
  const checklist = checklistLines(s, compact);
29538
+ const singletons = singletonLines(s);
29488
29539
  const elapsedMs = s.elapsedMs ?? (s.startedAtMs !== undefined && s.nowMs !== undefined ? s.nowMs - s.startedAtMs : undefined);
29489
29540
  if (s.terminal === "completed") {
29490
29541
  const parts2 = [headerLine(s, "✅")];
@@ -29495,6 +29546,8 @@ function renderWith(s, compact) {
29495
29546
  parts2.push(summary);
29496
29547
  if (checklist.length > 0)
29497
29548
  parts2.push("", ...checklist);
29549
+ if (singletons.length > 0)
29550
+ parts2.push("", ...singletons);
29498
29551
  if (s.deferred !== false)
29499
29552
  parts2.push("", ...deferredLines(s.target));
29500
29553
  return parts2.join(`
@@ -29512,6 +29565,8 @@ function renderWith(s, compact) {
29512
29565
  parts2.push("", `Re-running the roll will NOT fix this — the agents are already on ` + `target. Finish the stale component(s), then \`switchroom update --check\`.`);
29513
29566
  if (checklist.length > 0)
29514
29567
  parts2.push("", ...checklist);
29568
+ if (singletons.length > 0)
29569
+ parts2.push("", ...singletons);
29515
29570
  return parts2.join(`
29516
29571
  `);
29517
29572
  }
@@ -29525,12 +29580,16 @@ function renderWith(s, compact) {
29525
29580
  parts2.push(summary);
29526
29581
  if (checklist.length > 0)
29527
29582
  parts2.push("", ...checklist);
29583
+ if (singletons.length > 0)
29584
+ parts2.push("", ...singletons);
29528
29585
  return parts2.join(`
29529
29586
  `);
29530
29587
  }
29531
29588
  const parts = [headerLine(s, "⏳"), `**Phase:** ${phaseLine(s)}`];
29532
29589
  if (checklist.length > 0)
29533
29590
  parts.push("", ...checklist);
29591
+ if (singletons.length > 0)
29592
+ parts.push("", ...singletons);
29534
29593
  const footer = [];
29535
29594
  if (s.m !== undefined)
29536
29595
  footer.push(`${rolledCount}/${s.m} rolled`);
@@ -30729,6 +30788,27 @@ class HostdServer {
30729
30788
  })
30730
30789
  };
30731
30790
  }
30791
+ persistNarrationMessageId(requestId, messageId) {
30792
+ try {
30793
+ if (!Number.isInteger(messageId) || messageId <= 0)
30794
+ return;
30795
+ const markerPath = join13(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
30796
+ if (!existsSync15(markerPath))
30797
+ return;
30798
+ const marker = parsePendingRolloutMarker(readFileSync11(markerPath, "utf8"));
30799
+ if (!marker)
30800
+ return;
30801
+ if (marker.request_id !== requestId)
30802
+ return;
30803
+ const updated = {
30804
+ ...marker,
30805
+ narration_message_id: messageId
30806
+ };
30807
+ const tmp = `${markerPath}.tmp`;
30808
+ writeFileSync6(tmp, encodePendingRolloutMarker(updated), { mode: 384 });
30809
+ renameSync5(tmp, markerPath);
30810
+ } catch {}
30811
+ }
30732
30812
  async resumePendingSelfBumpRollout() {
30733
30813
  const markerPath = join13(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
30734
30814
  if (!existsSync15(markerPath))
@@ -30797,6 +30877,9 @@ class HostdServer {
30797
30877
  ...marker.skip_web ? { skip_web: true } : {},
30798
30878
  ...marker.allow_downgrade ? { allow_downgrade: true } : {}
30799
30879
  }, marker.request_id, caller, Date.now());
30880
+ if (marker.narration_message_id !== undefined && caller.kind === "agent") {
30881
+ this.rolloutNarrator?.seedPostedMessage?.(marker.request_id, caller.name, marker.narration_message_id);
30882
+ }
30800
30883
  this.onRolloutPhase(entry, { phase: "self-bump-done", target: marker.pin });
30801
30884
  }
30802
30885
  async handleAgentStart(req, started) {
@@ -31446,6 +31529,8 @@ ${pinNote}`.trim();
31446
31529
  const drifted = entry.drifted ?? [];
31447
31530
  const notes = entry.failed_step === "verify-components" ? [
31448
31531
  `Unattended auto-update roll to ${entry.pin ?? "?"} rolled ` + `${(entry.rolled ?? []).length} agent(s) but did NOT converge the ` + `whole host: ${drifted.join(", ") || "one or more components"} ` + `still behind. Re-running the roll will not fix this — run the ` + `per-component install named in the rollout warnings, then ` + `\`switchroom update --check\` to confirm.`
31532
+ ] : entry.failed_step === "ensure-banks" ? [
31533
+ `Unattended auto-update roll to ${entry.pin ?? "?"} rolled ` + `${(entry.rolled ?? []).length} agent(s) but could NOT create the ` + `Hindsight bank for: ${drifted.join(", ") || "one or more agents"}. ` + `Re-running the roll will not fix this — restart the named agent(s) ` + `to re-run bank creation now that Hindsight is back up.`
31449
31534
  ] : [
31450
31535
  `Unattended auto-update roll to ${entry.pin ?? "?"} FAILED at ` + `${entry.failed_step ?? "unknown step"}` + `${entry.failed_agent ? ` (agent ${entry.failed_agent})` : ""}. ` + `Unattended retries of this version are disabled; fix the cause, ` + `then roll manually (\`switchroom rollout --pin ${entry.pin ?? "vX.Y.Z"}\`).`
31451
31536
  ];
@@ -31473,6 +31558,10 @@ ${pinNote}`.trim();
31473
31558
  notes.push(`No automatic rollback: the agent fleet reached ` + `${entry.pin ?? "the target"} and the pin is committed — the gap is ` + `${(entry.drifted ?? []).join(", ") || "a component"}, not the fleet. ` + `Rolling back would move working agents backwards; converge the ` + `named component(s) forward instead.`);
31474
31559
  return notes;
31475
31560
  }
31561
+ if (entry.failed_step === "ensure-banks") {
31562
+ notes.push(`No automatic rollback: the agent fleet reached ` + `${entry.pin ?? "the target"} and the pin is committed — the gap is a ` + `missing Hindsight bank for ${(entry.drifted ?? []).join(", ") || "an agent"}, ` + `not the fleet. Restart the named agent(s) to re-create the bank; do ` + `not roll back.`);
31563
+ return notes;
31564
+ }
31476
31565
  const beforeAnyAgent = entry.failed_step === "apply" || entry.failed_step === "restart-agent" && rolledCount === 0;
31477
31566
  if (!beforeAnyAgent) {
31478
31567
  notes.push(rolledCount > 0 ? `No automatic rollback: ${rolledCount} agent(s) already confirmed on ` + `${entry.pin ?? "the target"} past a green canary ` + `(${(entry.rolled ?? []).join(", ")}). The fleet is mixed — ` + `finish the roll or roll back manually.` : `No automatic rollback: the roll stopped at ` + `${entry.failed_step ?? "an unknown step"} before any change ` + `landed — nothing to revert. Verify host-side if unsure.`);
@@ -32314,6 +32403,26 @@ class SocketRolloutNarrationRelay {
32314
32403
  }
32315
32404
 
32316
32405
  // src/host-control/rollout-narrator.ts
32406
+ var SINGLETON_WEB = "switchroom-web";
32407
+ var SINGLETON_HINDSIGHT = "hindsight";
32408
+ var SINGLETON_HOSTD = "hostd";
32409
+ var SINGLETON_SHARED = "shared services (approval-kernel · auth-broker · vault-broker · voice)";
32410
+ function initialSingletons() {
32411
+ return [
32412
+ { name: SINGLETON_WEB, status: "pending" },
32413
+ { name: SINGLETON_HINDSIGHT, status: "pending" },
32414
+ {
32415
+ name: SINGLETON_HOSTD,
32416
+ status: "deferred",
32417
+ detail: "host-side (agent-invoked roll cannot recreate its own hostd)"
32418
+ },
32419
+ {
32420
+ name: SINGLETON_SHARED,
32421
+ status: "pending",
32422
+ detail: "self-heal with the first agent restart"
32423
+ }
32424
+ ];
32425
+ }
32317
32426
  var DEFAULT_DEBOUNCE_MS = 1500;
32318
32427
  var MAX_POST_ATTEMPTS = 2;
32319
32428
 
@@ -32366,6 +32475,68 @@ class LogTailRolloutNarrator {
32366
32475
  break;
32367
32476
  }
32368
32477
  }
32478
+ static patchSingleton(st, name, patch) {
32479
+ const row = st.singletons.find((g) => g.name === name);
32480
+ if (row)
32481
+ Object.assign(row, patch);
32482
+ }
32483
+ trackSingletonProgress(st, phase) {
32484
+ const patch = LogTailRolloutNarrator.patchSingleton;
32485
+ switch (phase.phase) {
32486
+ case "canary-pass":
32487
+ case "agent-done": {
32488
+ const shared = st.singletons.find((g) => g.name === SINGLETON_SHARED);
32489
+ if (shared && shared.status === "pending") {
32490
+ shared.status = "updating";
32491
+ shared.detail = "recreated with the first agent restart — verified at roll end";
32492
+ }
32493
+ break;
32494
+ }
32495
+ case "web-refresh":
32496
+ patch(st, SINGLETON_WEB, { status: "updating", detail: "webd install…" });
32497
+ break;
32498
+ case "web-refresh-done":
32499
+ patch(st, SINGLETON_WEB, { status: "updated", detail: undefined });
32500
+ break;
32501
+ case "hindsight-refresh":
32502
+ patch(st, SINGLETON_HINDSIGHT, {
32503
+ status: "updating",
32504
+ detail: "memory setup --recreate…"
32505
+ });
32506
+ break;
32507
+ case "hindsight-refresh-done":
32508
+ patch(st, SINGLETON_HINDSIGHT, { status: "updated", detail: undefined });
32509
+ break;
32510
+ case "hindsight-skipped":
32511
+ patch(st, SINGLETON_HINDSIGHT, {
32512
+ status: "skipped",
32513
+ detail: "no hindsight container on this host"
32514
+ });
32515
+ break;
32516
+ case "self-bump":
32517
+ patch(st, SINGLETON_HOSTD, {
32518
+ status: "updating",
32519
+ detail: "self-bump to the target…"
32520
+ });
32521
+ break;
32522
+ case "self-bump-done":
32523
+ patch(st, SINGLETON_HOSTD, {
32524
+ status: "updated",
32525
+ detail: "self-bump; template regen stays host-side"
32526
+ });
32527
+ break;
32528
+ case "hostd-web-deferred": {
32529
+ const hostd = st.singletons.find((g) => g.name === SINGLETON_HOSTD);
32530
+ if (hostd && hostd.status !== "updated") {
32531
+ hostd.status = "deferred";
32532
+ hostd.detail = "host-side";
32533
+ }
32534
+ break;
32535
+ }
32536
+ default:
32537
+ break;
32538
+ }
32539
+ }
32369
32540
  static seqFor(phase) {
32370
32541
  switch (phase.phase) {
32371
32542
  case "self-bump":
@@ -32385,7 +32556,12 @@ class LogTailRolloutNarrator {
32385
32556
  case "agent-done":
32386
32557
  return 2 * (phase.n ?? 1) + 1;
32387
32558
  case "web-refresh":
32388
- return Number.MAX_SAFE_INTEGER - 2;
32559
+ case "web-refresh-done":
32560
+ return Number.MAX_SAFE_INTEGER - 4;
32561
+ case "hindsight-refresh":
32562
+ case "hindsight-refresh-done":
32563
+ case "hindsight-skipped":
32564
+ return Number.MAX_SAFE_INTEGER - 3;
32389
32565
  case "hostd-web-deferred":
32390
32566
  return Number.MAX_SAFE_INTEGER - 1;
32391
32567
  default:
@@ -32402,6 +32578,7 @@ class LogTailRolloutNarrator {
32402
32578
  return;
32403
32579
  st.lastAppliedSeq = seq;
32404
32580
  this.trackAgentProgress(st, phase);
32581
+ this.trackSingletonProgress(st, phase);
32405
32582
  const doneNames = st.agents.filter((a) => a.status === "done").map((a) => a.name);
32406
32583
  const rolledSoFar = entry.rolled && entry.rolled.length > 0 ? entry.rolled : doneNames;
32407
32584
  st.render = {
@@ -32413,6 +32590,7 @@ class LogTailRolloutNarrator {
32413
32590
  ...phase.agent !== undefined ? { agent: phase.agent } : {},
32414
32591
  rolled: rolledSoFar,
32415
32592
  agents: st.agents,
32593
+ singletons: st.singletons,
32416
32594
  requestId: entry.request_id,
32417
32595
  ...entry.prior_pin ? { fromVersion: entry.prior_pin } : {},
32418
32596
  startedAtMs: entry.started_at
@@ -32435,6 +32613,7 @@ class LogTailRolloutNarrator {
32435
32613
  a.status = "pending";
32436
32614
  }
32437
32615
  }
32616
+ this.reconcileSingletonsAtTerminal(st, entry);
32438
32617
  st.render = {
32439
32618
  target: entry.pin ?? st.render.target,
32440
32619
  rolled: entry.rolled ?? st.render.rolled,
@@ -32444,6 +32623,7 @@ class LogTailRolloutNarrator {
32444
32623
  ...entry.got !== undefined ? { got: entry.got } : {},
32445
32624
  ...entry.drifted && entry.drifted.length > 0 ? { drifted: entry.drifted } : {},
32446
32625
  ...st.agents.length > 0 ? { agents: st.agents } : {},
32626
+ ...st.singletons.length > 0 ? { singletons: st.singletons } : {},
32447
32627
  ...st.render.m !== undefined ? { m: st.render.m } : {},
32448
32628
  requestId: entry.request_id,
32449
32629
  ...entry.prior_pin ?? st.render.fromVersion ? { fromVersion: entry.prior_pin ?? st.render.fromVersion } : {},
@@ -32461,27 +32641,106 @@ class LogTailRolloutNarrator {
32461
32641
  this.opts.log?.(`onTerminal threw (non-fatal): ${e.message}`);
32462
32642
  }
32463
32643
  }
32644
+ reconcileSingletonsAtTerminal(st, entry) {
32645
+ const completed = entry.result === "completed";
32646
+ const drifted = new Set(entry.drifted ?? []);
32647
+ const driftedHits = (row) => {
32648
+ switch (row) {
32649
+ case SINGLETON_WEB:
32650
+ return [...drifted].filter((d) => d === "switchroom-web");
32651
+ case SINGLETON_HINDSIGHT:
32652
+ return [...drifted].filter((d) => d === "switchroom-hindsight");
32653
+ case SINGLETON_HOSTD:
32654
+ return [...drifted].filter((d) => d === "switchroom-hostd" || d === "switchroom-hindsight-autoheal");
32655
+ case SINGLETON_SHARED:
32656
+ return [...drifted].filter((d) => d.includes("broker") || d.includes("kernel") || d.includes("voice"));
32657
+ default:
32658
+ return [];
32659
+ }
32660
+ };
32661
+ for (const g of st.singletons) {
32662
+ const hits = driftedHits(g.name);
32663
+ if (hits.length > 0) {
32664
+ g.status = "failed";
32665
+ g.detail = `still behind (${hits.join(", ")}) — see the roll's warnings`;
32666
+ continue;
32667
+ }
32668
+ if (entry.failed_step === "refresh-hindsight" && g.name === SINGLETON_HINDSIGHT) {
32669
+ g.status = "failed";
32670
+ g.detail = "recreate FAILED — memory backend down; run `switchroom memory setup`";
32671
+ continue;
32672
+ }
32673
+ if (!completed) {
32674
+ if (g.status === "updating" && g.name !== SINGLETON_SHARED) {
32675
+ g.detail = "outcome unknown — the roll stopped; see warnings";
32676
+ }
32677
+ continue;
32678
+ }
32679
+ switch (g.name) {
32680
+ case SINGLETON_WEB:
32681
+ case SINGLETON_HINDSIGHT:
32682
+ if (g.status === "updated")
32683
+ break;
32684
+ if (g.status === "updating") {
32685
+ g.status = "updated";
32686
+ g.detail = "verified by the end-of-roll component check";
32687
+ } else if (g.status === "pending") {
32688
+ g.status = "skipped";
32689
+ g.detail = "no refresh observed in this roll — see warnings";
32690
+ }
32691
+ break;
32692
+ case SINGLETON_SHARED:
32693
+ g.status = "updated";
32694
+ g.detail = "self-healed with the first agent restart; end-of-roll check passed";
32695
+ break;
32696
+ default:
32697
+ break;
32698
+ }
32699
+ }
32700
+ }
32701
+ freshState(agentName, target) {
32702
+ return {
32703
+ agentName,
32704
+ lastAppliedSeq: -1,
32705
+ messageId: null,
32706
+ posting: false,
32707
+ postAttempts: 0,
32708
+ postFailed: false,
32709
+ render: { target },
32710
+ agents: [],
32711
+ singletons: initialSingletons(),
32712
+ frozen: false,
32713
+ timer: null,
32714
+ pendingEditAfterPost: false
32715
+ };
32716
+ }
32464
32717
  ensureState(entry) {
32465
32718
  let st = this.states.get(entry.request_id);
32466
32719
  if (!st) {
32467
32720
  const agentName = entry.caller.kind === "agent" ? entry.caller.name : "";
32468
- st = {
32469
- agentName,
32470
- lastAppliedSeq: -1,
32471
- messageId: null,
32472
- posting: false,
32473
- postAttempts: 0,
32474
- postFailed: false,
32475
- render: { target: entry.pin ?? "" },
32476
- agents: [],
32477
- frozen: false,
32478
- timer: null,
32479
- pendingEditAfterPost: false
32480
- };
32721
+ st = this.freshState(agentName, entry.pin ?? "");
32481
32722
  this.states.set(entry.request_id, st);
32482
32723
  }
32483
32724
  return st;
32484
32725
  }
32726
+ seedPostedMessage(requestId, agentName, messageId) {
32727
+ const existing = this.states.get(requestId);
32728
+ if (existing) {
32729
+ if (existing.messageId === null) {
32730
+ existing.messageId = messageId;
32731
+ existing.posting = false;
32732
+ existing.postFailed = false;
32733
+ if (existing.postAttempts < 1)
32734
+ existing.postAttempts = 1;
32735
+ existing.pendingEditAfterPost = false;
32736
+ }
32737
+ return;
32738
+ }
32739
+ const st = this.freshState(agentName, "");
32740
+ st.messageId = messageId;
32741
+ st.postAttempts = 1;
32742
+ this.states.set(requestId, st);
32743
+ }
32485
32744
  scheduleRenderOrPost(requestId) {
32486
32745
  const st = this.states.get(requestId);
32487
32746
  if (!st)
@@ -32522,6 +32781,11 @@ class LogTailRolloutNarrator {
32522
32781
  }
32523
32782
  st.messageId = mid;
32524
32783
  st.postFailed = false;
32784
+ try {
32785
+ this.opts.onMessageId?.(requestId, mid);
32786
+ } catch (e) {
32787
+ this.opts.log?.(`onMessageId sink threw (non-fatal): ${e.message}`);
32788
+ }
32525
32789
  if (st.pendingEditAfterPost) {
32526
32790
  st.pendingEditAfterPost = false;
32527
32791
  this.flush(requestId, false);
@@ -33166,8 +33430,11 @@ async function main() {
33166
33430
  resolveGatewaySocket,
33167
33431
  log: (m) => process.stderr.write(`hostd: rollout-narration — ${m}
33168
33432
  `)
33169
- }), { log: (m) => process.stderr.write(`hostd: rollout-narration — ${m}
33170
- `) });
33433
+ }), {
33434
+ log: (m) => process.stderr.write(`hostd: rollout-narration — ${m}
33435
+ `),
33436
+ onMessageId: (rid, mid) => server?.persistNarrationMessageId(rid, mid)
33437
+ });
33171
33438
  const server = new HostdServer({
33172
33439
  homeDir: homedir8(),
33173
33440
  agentUids,
@@ -4417,7 +4417,7 @@ var init_protocol = __esm(() => {
4417
4417
  });
4418
4418
 
4419
4419
  // src/memory/observation-scopes.ts
4420
- var OBSERVATION_SCOPES;
4420
+ var OBSERVATION_SCOPES, OBSERVATION_SCOPE_STRATEGIES;
4421
4421
  var init_observation_scopes = __esm(() => {
4422
4422
  OBSERVATION_SCOPES = [
4423
4423
  "per_tag",
@@ -4425,10 +4425,16 @@ var init_observation_scopes = __esm(() => {
4425
4425
  "all_combinations",
4426
4426
  "shared"
4427
4427
  ];
4428
+ OBSERVATION_SCOPE_STRATEGIES = [
4429
+ "curated",
4430
+ "shared",
4431
+ "combined",
4432
+ "off"
4433
+ ];
4428
4434
  });
4429
4435
 
4430
4436
  // src/config/schema.ts
4431
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
4437
+ 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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
4432
4438
  var init_schema = __esm(() => {
4433
4439
  init_zod();
4434
4440
  init_observation_scopes();
@@ -4543,7 +4549,8 @@ var init_schema = __esm(() => {
4543
4549
  allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
4544
4550
  deny: exports_external.array(exports_external.string()).default([]).describe("Denied tools (overrides allow)")
4545
4551
  }).optional();
4546
- ObservationScopesSchema = exports_external.enum(OBSERVATION_SCOPES).optional().describe("Per-row observation scope stamped on every memory this agent " + `retains. "shared" makes Hindsight's consolidation write the ` + "resulting observations into ONE global untagged scope instead of a " + "scope per tag — what several agents pooling one bank need so their " + "observations actually merge rather than sitting in parallel " + "per-tag silos. OMITTED BY DEFAULT: unset means the field never goes " + "on the wire and the engine's own default stands, which is the " + "shipped behaviour. Applies to every retain path (Stop hook, " + "sidechain, boot reconcile, queue drain, backfill, session-handoff " + "mirror) and is carried on the queued payload, so a retain that fails " + "now and drains later still lands in this scope. Accepted values: " + `${OBSERVATION_SCOPES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
4552
+ 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 — 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).");
4553
+ 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 — keeping the stable " + "semantic ones on the source fact — 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).");
4547
4554
  AgentMemorySchema = exports_external.object({
4548
4555
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
4549
4556
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
@@ -4579,6 +4586,7 @@ var init_schema = __esm(() => {
4579
4586
  }).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis — a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
4580
4587
  directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true — Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never …", "from now on …", ' + `"stop doing …", a stated preference, "that's wrong, it's …") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex — the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
4581
4588
  observation_scopes: ObservationScopesSchema,
4589
+ observation_scope_strategy: ObservationScopeStrategySchema,
4582
4590
  recall: exports_external.object({
4583
4591
  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)."),
4584
4592
  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."),
@@ -4979,6 +4987,7 @@ var init_schema = __esm(() => {
4979
4987
  isolation: exports_external.enum(["default", "strict"]).optional(),
4980
4988
  directive_capture_nudge: exports_external.boolean().optional(),
4981
4989
  observation_scopes: ObservationScopesSchema,
4990
+ observation_scope_strategy: ObservationScopeStrategySchema,
4982
4991
  recall: exports_external.object({
4983
4992
  max_memories: exports_external.number().int().min(0).optional(),
4984
4993
  cache_ttl_secs: exports_external.number().int().min(0).optional(),