switchroom 0.20.0 → 0.20.1

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 (29) hide show
  1. package/bin/handoff-briefing.sh +213 -74
  2. package/dist/agent-scheduler/index.js +1 -1
  3. package/dist/auth-broker/index.js +1 -1
  4. package/dist/buzz-gateway/index.js +166 -6
  5. package/dist/cli/notion-write-pretool.mjs +1 -1
  6. package/dist/cli/switchroom.js +24701 -16397
  7. package/dist/host-control/main.js +41 -8
  8. package/dist/vault/approvals/kernel-server.js +1 -1
  9. package/dist/vault/broker/server.js +1 -1
  10. package/package.json +3 -2
  11. package/profiles/_base/start.sh.hbs +79 -10
  12. package/telegram-plugin/dist/gateway/gateway.js +1397 -962
  13. package/telegram-plugin/gateway/access-store.test.ts +234 -0
  14. package/telegram-plugin/gateway/access-store.ts +194 -0
  15. package/telegram-plugin/gateway/boot-briefing-builder.ts +135 -7
  16. package/telegram-plugin/gateway/boot-briefing-capability.ts +31 -0
  17. package/telegram-plugin/gateway/boot-briefing-wiring.ts +166 -4
  18. package/telegram-plugin/gateway/buzz-mirror-correlation-store.ts +285 -0
  19. package/telegram-plugin/gateway/buzz-mirror.ts +177 -12
  20. package/telegram-plugin/gateway/gateway.ts +43 -123
  21. package/telegram-plugin/gateway/inbound-router.ts +93 -3
  22. package/telegram-plugin/gateway/outbound-send-path.ts +48 -1
  23. package/telegram-plugin/gateway/pending-turn-env.ts +10 -1
  24. package/telegram-plugin/tests/boot-briefing-builder.test.ts +422 -31
  25. package/telegram-plugin/tests/buzz-mirror-correlation-store.test.ts +173 -0
  26. package/telegram-plugin/tests/buzz-mirror.test.ts +297 -1
  27. package/telegram-plugin/tests/outbound-send-path.test.ts +24 -0
  28. package/telegram-plugin/tests/reply-to-buffer-fallback.test.ts +273 -0
  29. package/telegram-plugin/tests/reply-to-buffer-history.test.ts +134 -0
@@ -11387,7 +11387,7 @@ var init_schema = __esm(() => {
11387
11387
  default_channel_id: exports_external.string().describe("Relay-minted group UUID (the NIP-29 `h` tag) the sidecar subscribes " + "to and stamps on injected turns."),
11388
11388
  channel_map: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional map of extra group UUIDs → friendly labels."),
11389
11389
  pubkey_names: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional petnames: hex/npub pubkey → display name, used to label " + "the sender on injected turns."),
11390
- pinned_relay_digest: exports_external.string().optional().describe("Pinned relay image digest (M4). The compat-check warns on mismatch; " + "advisory in Phase 1.")
11390
+ pinned_relay_digest: exports_external.string().optional().describe("Pinned relay image digest (M4). RESERVED — no consumer of this field " + "exists; the existing compat-check (compat-check.ts) validates only " + "the wire contract (AUTH kind, message kind, tag names) and does not " + "read this field. Kept in the schema so the intended digest-pin can " + "be wired without a config shape change.")
11391
11391
  }).strict();
11392
11392
  ChannelsSchema = exports_external.object({
11393
11393
  telegram: TelegramChannelSchema,
@@ -21432,7 +21432,7 @@ function allocateAgentUid(name) {
21432
21432
  }
21433
21433
 
21434
21434
  // src/build-info.ts
21435
- var VERSION = "0.20.0";
21435
+ var VERSION = "0.20.1";
21436
21436
 
21437
21437
  // src/setup/hindsight-recall-tunables.ts
21438
21438
  var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
@@ -28372,6 +28372,20 @@ function resolveOperatorUid(ownershipDeps = {}) {
28372
28372
  // src/cli/rollout.ts
28373
28373
  init_atomic();
28374
28374
 
28375
+ // src/config/hostd-template-version.ts
28376
+ var HOSTD_TEMPLATE_LAST_CHANGED = "v0.19.43";
28377
+ function hostdTemplateRegenVerdict(target, fromVersion) {
28378
+ const cmpTarget = compareReleaseTags(HOSTD_TEMPLATE_LAST_CHANGED, target);
28379
+ if (cmpTarget === null)
28380
+ return "unknown";
28381
+ const cmpFrom = compareReleaseTags(HOSTD_TEMPLATE_LAST_CHANGED, fromVersion);
28382
+ if (cmpFrom === null)
28383
+ return "unknown";
28384
+ const targetHas = cmpTarget <= 0;
28385
+ const fromHas = cmpFrom <= 0;
28386
+ return targetHas === fromHas ? "not-needed" : "required";
28387
+ }
28388
+
28375
28389
  // src/cli/resolve-version.ts
28376
28390
  import { existsSync as existsSync14, readFileSync as readFileSync10 } from "node:fs";
28377
28391
  import { dirname as dirname7, join as join9 } from "node:path";
@@ -29733,12 +29747,31 @@ function etaLine(s) {
29733
29747
  const meanMs = done.reduce((t, a) => t + (a.durationMs ?? 0), 0) / done.length;
29734
29748
  return `~${formatDurationMs(meanMs * remaining)} left (rough est.)`;
29735
29749
  }
29736
- function deferredLines(target) {
29750
+ function deferredLines(target, fromVersion) {
29737
29751
  const bare = target.trim().replace(/^v/, "");
29738
- return [
29752
+ const head = [
29739
29753
  `**Verified on ${target}** — every component this roll owned passed \`verify-components\`, so this is host convergence, not just the agents. Anything the roll was told to skip is named in its warnings.`,
29740
- `**Still host-side (nothing in a roll can do these):**`,
29741
- `- host operator CLI — \`sudo npm i -g switchroom@${bare}\``,
29754
+ `**Still host-side (nothing in a roll can do these):**`
29755
+ ];
29756
+ const verdict = hostdTemplateRegenVerdict(target, fromVersion);
29757
+ if (verdict === "required") {
29758
+ return [
29759
+ ...head,
29760
+ `- host operator CLI + hostd template regen — regen is **REQUIRED** for this roll (hostd mounts/env changed in ${HOSTD_TEMPLATE_LAST_CHANGED}). One copy-paste:`,
29761
+ ` \`sudo npm i -g switchroom@${bare} && switchroom hostd install --tag ${target}\``
29762
+ ];
29763
+ }
29764
+ const cliLine = `- host operator CLI — \`sudo npm i -g switchroom@${bare}\``;
29765
+ if (verdict === "not-needed") {
29766
+ return [
29767
+ ...head,
29768
+ cliLine,
29769
+ `- hostd template regen: **not needed** for this release — hostd mounts/env unchanged since ${HOSTD_TEMPLATE_LAST_CHANGED}.`
29770
+ ];
29771
+ }
29772
+ return [
29773
+ ...head,
29774
+ cliLine,
29742
29775
  `- hostd template regen (only if the release changed hostd mounts/env) — \`switchroom hostd install --tag ${target}\``
29743
29776
  ];
29744
29777
  }
@@ -29782,7 +29815,7 @@ function renderWith(s, compact) {
29782
29815
  if (warnings.length > 0)
29783
29816
  parts2.push("", ...warnings);
29784
29817
  if (s.deferred !== false)
29785
- parts2.push("", ...deferredLines(s.target));
29818
+ parts2.push("", ...deferredLines(s.target, s.fromVersion));
29786
29819
  return parts2.join(`
29787
29820
  `);
29788
29821
  }
@@ -32851,7 +32884,7 @@ class LogTailRolloutNarrator {
32851
32884
  case "hindsight-refresh":
32852
32885
  case "hindsight-refresh-done":
32853
32886
  case "hindsight-skipped":
32854
- return Number.MAX_SAFE_INTEGER - 3;
32887
+ return 1;
32855
32888
  case "hostd-web-deferred":
32856
32889
  return Number.MAX_SAFE_INTEGER - 1;
32857
32890
  default:
@@ -4827,7 +4827,7 @@ var init_schema = __esm(() => {
4827
4827
  default_channel_id: exports_external.string().describe("Relay-minted group UUID (the NIP-29 `h` tag) the sidecar subscribes " + "to and stamps on injected turns."),
4828
4828
  channel_map: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional map of extra group UUIDs → friendly labels."),
4829
4829
  pubkey_names: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional petnames: hex/npub pubkey → display name, used to label " + "the sender on injected turns."),
4830
- pinned_relay_digest: exports_external.string().optional().describe("Pinned relay image digest (M4). The compat-check warns on mismatch; " + "advisory in Phase 1.")
4830
+ pinned_relay_digest: exports_external.string().optional().describe("Pinned relay image digest (M4). RESERVED — no consumer of this field " + "exists; the existing compat-check (compat-check.ts) validates only " + "the wire contract (AUTH kind, message kind, tag names) and does not " + "read this field. Kept in the schema so the intended digest-pin can " + "be wired without a config shape change.")
4831
4831
  }).strict();
4832
4832
  ChannelsSchema = exports_external.object({
4833
4833
  telegram: TelegramChannelSchema,
@@ -4423,7 +4423,7 @@ var init_schema = __esm(() => {
4423
4423
  default_channel_id: exports_external.string().describe("Relay-minted group UUID (the NIP-29 `h` tag) the sidecar subscribes " + "to and stamps on injected turns."),
4424
4424
  channel_map: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional map of extra group UUIDs → friendly labels."),
4425
4425
  pubkey_names: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional petnames: hex/npub pubkey → display name, used to label " + "the sender on injected turns."),
4426
- pinned_relay_digest: exports_external.string().optional().describe("Pinned relay image digest (M4). The compat-check warns on mismatch; " + "advisory in Phase 1.")
4426
+ pinned_relay_digest: exports_external.string().optional().describe("Pinned relay image digest (M4). RESERVED — no consumer of this field " + "exists; the existing compat-check (compat-check.ts) validates only " + "the wire contract (AUTH kind, message kind, tag names) and does not " + "read this field. Kept in the schema so the intended digest-pin can " + "be wired without a config shape change.")
4427
4427
  }).strict();
4428
4428
  ChannelsSchema = exports_external.object({
4429
4429
  telegram: TelegramChannelSchema,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.20.0",
4
+ "version": "0.20.1",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -28,7 +28,7 @@
28
28
  "test:vitest": "vitest run",
29
29
  "test:bun": "bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-scope-persist.test.ts src/vault/broker/server-tokenless-scope.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/self-approval-bypass.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/boot-briefing-builder.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-buzz-dedup.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
30
30
  "test:watch": "vitest",
31
- "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts",
31
+ "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts && bun scripts/check-hostd-template-guard.ts",
32
32
  "lint:tsc": "tsc --noEmit",
33
33
  "lint:hindsight-write-redaction": "node scripts/check-hindsight-write-redaction.mjs",
34
34
  "lint:secret-pattern-parity": "bun scripts/check-secret-pattern-parity.ts",
@@ -50,6 +50,7 @@
50
50
  "lint:litellm-config-guard": "node scripts/check-litellm-config-guard.mjs",
51
51
  "lint:release-asset-contract": "node scripts/check-release-asset-names.mjs",
52
52
  "lint:agent-attribution-trailers": "node scripts/check-agent-attribution-trailers.mjs",
53
+ "lint:hostd-template-guard": "bun scripts/check-hostd-template-guard.ts",
53
54
  "prepublishOnly": "npm run build && npm run lint && npm test"
54
55
  },
55
56
  "dependencies": {
@@ -70,6 +70,21 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
70
70
  export SWITCHROOM_SESSION_BRIEFING="{{#if sessionBriefingMode}}{{{sessionBriefingMode}}}{{else}}legacy{{/if}}"
71
71
  export SWITCHROOM_RESUME_MODE="{{#if resumeMode}}{{{resumeMode}}}{{else}}handoff{{/if}}"
72
72
 
73
+ {{#if hindsightEnabled}}
74
+ # Hindsight endpoint for the gateway boot briefing (session_continuity.briefing:
75
+ # gateway). When assembling the fresh-session reorientation briefing the gateway
76
+ # daemon does its OWN Hindsight recall — mirroring bin/handoff-briefing.sh — to
77
+ # fold recent memory into the briefing alongside the durable Telegram history.
78
+ # The authoritative exports live in the inner pass (~line 893), AFTER the
79
+ # gateway fork below, so without this hoist the forked daemon never sees the
80
+ # endpoint and the Hindsight section silently no-ops for every boot (same
81
+ # start.sh env-fork landmine as SWITCHROOM_SESSION_BRIEFING above). Hoist both
82
+ # here, before the fork; idempotent with the inner-pass exports (same rendered
83
+ # template values). Pinned by tests/scaffold.gateway-env-order.test.ts.
84
+ export HINDSIGHT_API_URL={{{hindsightApiBaseUrlQ}}}
85
+ export HINDSIGHT_BANK_ID={{{hindsightBankIdQ}}}
86
+ {{/if}}
87
+
73
88
  # Force-fresh suppression signal for the gateway boot briefing. The gateway
74
89
  # suppresses the briefing when this boot is a /reset·/new fresh start —
75
90
  # re-feeding the just-reset conversation into the fresh session would defeat
@@ -450,6 +465,32 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
450
465
 
451
466
  _gateway_bundle=/opt/switchroom/telegram-plugin/dist/gateway/gateway.js
452
467
  _telegram_enabled={{#if telegramEnabledFlag}}{{telegramEnabledFlag}}{{else}}true{{/if}}
468
+
469
+ # Version-skew handshake for the gateway boot briefing (#4245). This start.sh
470
+ # is fresh; the deployed /opt/switchroom gateway bundle may NOT be. When it
471
+ # sets briefing=gateway it also skips the legacy shell handoff-briefing
472
+ # assembler (inner pass, below) — so if the bundle PREDATES the boot-briefing
473
+ # builder, BOTH paths are dead and the agent gets a SILENT no-briefing until
474
+ # the image is updated. The stale bundle cannot self-report a feature it lacks,
475
+ # so this fresh start.sh does the handshake: grep the bundle for the capability
476
+ # sentinel the builder exports (GATEWAY_BOOT_BRIEFING_CAPABILITY in
477
+ # telegram-plugin/gateway/boot-briefing-capability.ts; the bundle is built
478
+ # un-minified so the literal survives verbatim). On a miss, warn LOUDLY and
479
+ # drop a marker so the inner pass runs the legacy handoff assembler as a
480
+ # fallback instead of leaving this boot with nothing. Resolve the marker afresh
481
+ # every boot (rm, then re-touch only on a confirmed miss) so a later image
482
+ # update self-heals with no leftover state.
483
+ _briefing_skew_marker="{{agentDir}}/.gateway-briefing-unavailable"
484
+ rm -f "$_briefing_skew_marker"
485
+ if [ "$SWITCHROOM_SESSION_BRIEFING" = "gateway" ]; then
486
+ if [ -f "$_gateway_bundle" ] && grep -q '{{gatewayBriefingCapability}}' "$_gateway_bundle" 2>/dev/null; then
487
+ : # bundle carries the boot-briefing builder — gateway path is live
488
+ else
489
+ echo "[start.sh] WARNING: session_continuity.briefing=gateway is configured, but the deployed gateway bundle ($_gateway_bundle) PREDATES the boot-briefing builder (capability sentinel '{{gatewayBriefingCapability}}' absent from the bundle). The gateway cannot assemble the reorientation briefing, and the legacy shell handoff path is skipped for 'gateway' mode — so this boot would otherwise get NO briefing at all. FALLING BACK to the legacy handoff-briefing assembler for this boot. Update the agent image (/opt/switchroom) to a build that includes the boot-briefing feature to use the gateway path." >&2
490
+ touch "$_briefing_skew_marker" 2>/dev/null || true
491
+ fi
492
+ fi
493
+
453
494
  if [ "$_telegram_enabled" = "true" ] && [ -f "$_gateway_bundle" ] && command -v bun >/dev/null 2>&1; then
454
495
  _switchroom_supervise gateway /var/log/switchroom/gateway-supervisor.log \
455
496
  bun "$_gateway_bundle" &
@@ -506,14 +547,13 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
506
547
  #
507
548
  # Default OFF and deterministic: the block is present for every agent
508
549
  # (start.sh stays byte-identical fleet-wide) but forks the sidecar ONLY
509
- # when BUZZ_ENABLED=1 is set in the container env. NOTE: nothing in this
510
- # branch sets BUZZ_ENABLED (or the other BUZZ_* vars) — projecting an
511
- # ENABLED channels.buzz block into container env is DEFERRED to the
512
- # deploy-wiring phase. Until then the env is unset for every agent and
513
- # this block is a no-op, so the channel is dark by construction. The
514
- # sidecar itself also fails closed if its env is incomplete, and fetches
515
- # the nsec in-process from the vault broker (never an env var, never
516
- # logged).
550
+ # when BUZZ_ENABLED=1 is set in the container env. That var is projected
551
+ # by src/agents/compose.ts from the cascade-resolved channels.buzz block,
552
+ # and BUZZ_ENABLED=1 lands ONLY when that block sets enabled:true. An
553
+ # enabled:false/absent block leaves it unset, so for those agents this
554
+ # block is a no-op and the channel is dark by construction. The sidecar
555
+ # itself also fails closed if its env is incomplete, and fetches the nsec
556
+ # in-process from the vault broker (never an env var, never logged).
517
557
  if [ "$BUZZ_ENABLED" = "1" ] \
518
558
  && [ -f /opt/switchroom/buzz-gateway/index.js ] \
519
559
  && command -v bun >/dev/null 2>&1; then
@@ -1430,9 +1470,38 @@ if [ "$_HANDOFF_STALE" = "1" ]; then
1430
1470
  # running the shell assembler here would double-inject the same recent
1431
1471
  # conversation. The Stop-hook .handoff.md path below is left untouched in
1432
1472
  # this stage of the migration.
1433
- if [ "$SWITCHROOM_RESUME_MODE" = "handoff" ] && [ "{{#if sessionBriefingMode}}{{{sessionBriefingMode}}}{{else}}legacy{{/if}}" != "gateway" ] && command -v handoff-briefing.sh >/dev/null 2>&1; then
1473
+ #
1474
+ # EXCEPTION (#4245): when the outer pass detected a bundle version skew
1475
+ # (briefing=gateway requested but the deployed gateway bundle predates the
1476
+ # boot-briefing builder), it dropped the .gateway-briefing-unavailable marker.
1477
+ # In that case the gateway path is dead, so run the legacy assembler here as a
1478
+ # fallback even in 'gateway' mode — the double-inject risk does not apply
1479
+ # because the stale gateway never queues a briefing. This is a file-based
1480
+ # handshake on purpose: the marker survives the outer→inner tmux re-exec
1481
+ # deterministically (written before the exec, read after), unlike env.
1482
+ if [ "$SWITCHROOM_RESUME_MODE" = "handoff" ] \
1483
+ && { [ "{{#if sessionBriefingMode}}{{{sessionBriefingMode}}}{{else}}legacy{{/if}}" != "gateway" ] || [ -f "{{agentDir}}/.gateway-briefing-unavailable" ]; } \
1484
+ && command -v handoff-briefing.sh >/dev/null 2>&1; then
1434
1485
  export AGENT_DIR="{{agentDir}}"
1435
- timeout 5 handoff-briefing.sh 2>/dev/null || true
1486
+ # Clear any prior-boot briefing BEFORE rebuilding (#continuity). The
1487
+ # sidecar rm below (post-injection) is skipped if a boot crashes between
1488
+ # the rebuild and injection, leaving last boot's .handoff-briefing.md on
1489
+ # disk. If this boot's rebuild is then killed by the outer `timeout`
1490
+ # (SIGTERM — `timeout 10` sends SIGTERM by default, not SIGKILL) before
1491
+ # handoff-briefing.sh's atomic write, the injection would re-serve that
1492
+ # STALE prior-boot briefing. Deleting the target up front makes a killed
1493
+ # rebuild yield a MISSING file (injection skips) rather than a stale one.
1494
+ # handoff-briefing.sh recreates it via tmp+mv on success.
1495
+ rm -f "$HANDOFF_BRIEFING_FILE" 2>/dev/null || true
1496
+ # Outer kill budget (10s) must strictly exceed the inner Hindsight cap
1497
+ # (3s, HANDOFF_BRIEFING_HINDSIGHT_TIMEOUT) with margin, so the SIGTERM
1498
+ # (`timeout 10`'s default signal) lands only on a genuinely wedged run —
1499
+ # never mid-write of a recall that was about to return. The local SQLite
1500
+ # (Telegram) and daily-memory reads are sub-second; Hindsight is the only
1501
+ # network hop and is self-capped, so 10s leaves ~7s of headroom over the
1502
+ # worst-case source.
1503
+ export HANDOFF_BRIEFING_HINDSIGHT_TIMEOUT="${HANDOFF_BRIEFING_HINDSIGHT_TIMEOUT:-3}"
1504
+ timeout 10 handoff-briefing.sh 2>/dev/null || true
1436
1505
  fi
1437
1506
  fi
1438
1507
  APPEND_PROMPT={{#if systemPromptAppendShellQuoted}}{{{systemPromptAppendShellQuoted}}}{{else}}""{{/if}}