switchroom 0.16.46 → 0.17.0

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 (109) hide show
  1. package/dist/agent-scheduler/index.js +83 -81
  2. package/dist/auth-broker/index.js +104 -88
  3. package/dist/cli/autoaccept-poll.js +8 -8
  4. package/dist/cli/drive-write-pretool.mjs +10 -15
  5. package/dist/cli/notion-write-pretool.mjs +85 -83
  6. package/dist/cli/skill-validate-pretool.mjs +91 -91
  7. package/dist/cli/switchroom.js +1720 -1392
  8. package/dist/cli/ui/index.html +84 -12
  9. package/dist/host-control/main.js +209 -173
  10. package/dist/vault/approvals/kernel-server.js +86 -83
  11. package/dist/vault/broker/server.js +284 -139
  12. package/package.json +3 -3
  13. package/profiles/_base/cron-session.sh.hbs +1 -1
  14. package/profiles/_base/start.sh.hbs +54 -3
  15. package/skills/switchroom-architecture/telegram.md +8 -15
  16. package/skills/switchroom-cli/SKILL.md +4 -5
  17. package/skills/telegram-test-harness/SKILL.md +1 -1
  18. package/telegram-plugin/README.md +18 -29
  19. package/telegram-plugin/bridge/bridge.ts +1 -41
  20. package/telegram-plugin/bridge/tool-filter.ts +3 -4
  21. package/telegram-plugin/dist/bridge/bridge.js +120 -155
  22. package/telegram-plugin/dist/gateway/gateway.js +1127 -1029
  23. package/telegram-plugin/dist/server.js +168 -203
  24. package/telegram-plugin/gateway/busy-key-reaper.ts +113 -0
  25. package/telegram-plugin/gateway/disconnect-flush.ts +11 -0
  26. package/telegram-plugin/gateway/escalation-bridge-gate.ts +46 -0
  27. package/telegram-plugin/gateway/gate-parity-probe.ts +102 -0
  28. package/telegram-plugin/gateway/gateway.ts +566 -631
  29. package/telegram-plugin/gateway/inbound-delivery-confirm.ts +89 -7
  30. package/telegram-plugin/gateway/inbound-spool.ts +108 -10
  31. package/telegram-plugin/gateway/model-command.ts +51 -3
  32. package/telegram-plugin/gateway/pending-inbound-buffer.ts +26 -0
  33. package/telegram-plugin/gateway/represent-guard.ts +28 -11
  34. package/telegram-plugin/gateway/status-pin-store.ts +124 -45
  35. package/telegram-plugin/gateway/worker-feed-dispatch.ts +19 -0
  36. package/telegram-plugin/history.ts +5 -0
  37. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +1 -2
  38. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +9 -1
  39. package/telegram-plugin/registry/subagents-schema.ts +126 -1
  40. package/telegram-plugin/registry/turns-schema.ts +65 -1
  41. package/telegram-plugin/session-tail.ts +26 -4
  42. package/telegram-plugin/slot-banner-driver.ts +42 -2
  43. package/telegram-plugin/status-query-telemetry.ts +100 -0
  44. package/telegram-plugin/stream-reply-handler.ts +15 -16
  45. package/telegram-plugin/subagent-watcher.ts +182 -30
  46. package/telegram-plugin/tests/buffer-gate-broadened.test.ts +4 -10
  47. package/telegram-plugin/tests/busy-key-reaper.test.ts +191 -0
  48. package/telegram-plugin/tests/emission-authority-facade.test.ts +11 -17
  49. package/telegram-plugin/tests/emission-determinism-wiring.test.ts +5 -26
  50. package/telegram-plugin/tests/escalation-bridge-gate.test.ts +38 -0
  51. package/telegram-plugin/tests/gate-parity-probe.test.ts +171 -0
  52. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +13 -0
  53. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +14 -11
  54. package/telegram-plugin/tests/inbound-delivery-confirm.test.ts +146 -0
  55. package/telegram-plugin/tests/inbound-spool.test.ts +143 -0
  56. package/telegram-plugin/tests/model-command.test.ts +54 -1
  57. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +5 -11
  58. package/telegram-plugin/tests/nested-worker-visibility-harness.test.ts +329 -0
  59. package/telegram-plugin/tests/pending-inbound-buffer.test.ts +53 -0
  60. package/telegram-plugin/tests/progress-update-redact.test.ts +99 -0
  61. package/telegram-plugin/tests/registry-turns.test.ts +67 -0
  62. package/telegram-plugin/tests/represent-guard.test.ts +42 -6
  63. package/telegram-plugin/tests/resume-inbound-builder.test.ts +1 -0
  64. package/telegram-plugin/tests/session-tail.test.ts +10 -1
  65. package/telegram-plugin/tests/slot-banner-boot-recovery.test.ts +246 -0
  66. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +0 -14
  67. package/telegram-plugin/tests/status-pin-store.test.ts +220 -5
  68. package/telegram-plugin/tests/status-query-telemetry.test.ts +115 -0
  69. package/telegram-plugin/tests/subagent-nested-dispatch.test.ts +209 -0
  70. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +37 -0
  71. package/telegram-plugin/tests/subagent-watcher-boot-promotion-replay.test.ts +167 -0
  72. package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +46 -3
  73. package/telegram-plugin/tests/subagent-watcher-stall-notification.test.ts +70 -0
  74. package/telegram-plugin/tests/tool-activity-summary.test.ts +16 -0
  75. package/telegram-plugin/tests/tool-filter.test.ts +1 -3
  76. package/telegram-plugin/tests/tool-label-pretool.test.ts +1 -4
  77. package/telegram-plugin/tests/turn-flush-safety.test.ts +222 -1
  78. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +46 -0
  79. package/telegram-plugin/tests/worker-activity-feed.test.ts +202 -9
  80. package/telegram-plugin/tests/worker-feed-dispatch.test.ts +25 -0
  81. package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +295 -0
  82. package/telegram-plugin/tool-activity-summary.ts +19 -0
  83. package/telegram-plugin/turn-flush-safety.ts +16 -1
  84. package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +8 -9
  85. package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +1 -1
  86. package/telegram-plugin/uat/scenarios/jtbd-narration-intent-dm.test.ts +1 -1
  87. package/telegram-plugin/worker-activity-feed.ts +75 -15
  88. package/vendor/hindsight-memory/CHANGELOG.md +24 -0
  89. package/vendor/hindsight-memory/README.md +5 -0
  90. package/vendor/hindsight-memory/scripts/lib/client.py +31 -1
  91. package/vendor/hindsight-memory/scripts/lib/config.py +41 -2
  92. package/vendor/hindsight-memory/scripts/lib/content.py +4 -1
  93. package/vendor/hindsight-memory/scripts/lib/daemon.py +11 -2
  94. package/vendor/hindsight-memory/scripts/recall.py +74 -1
  95. package/vendor/hindsight-memory/scripts/retain.py +8 -1
  96. package/vendor/hindsight-memory/scripts/tests/test_config_client_casts.py +111 -0
  97. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +85 -1
  98. package/vendor/hindsight-memory/scripts/tests/test_recall_tag_filters.py +107 -0
  99. package/vendor/hindsight-memory/settings.json +4 -0
  100. package/vendor/hindsight-memory/tests/test_client.py +130 -0
  101. package/vendor/hindsight-memory/tests/test_config.py +47 -0
  102. package/vendor/hindsight-memory/tests/test_content.py +18 -0
  103. package/vendor/hindsight-memory/tests/test_hooks.py +62 -0
  104. package/telegram-plugin/gateway/error-envelope-card.ts +0 -64
  105. package/telegram-plugin/gateway/resolve-calling-subagent.ts +0 -78
  106. package/telegram-plugin/silent-reply.ts +0 -58
  107. package/telegram-plugin/tests/error-envelope-unlock-card.test.ts +0 -79
  108. package/telegram-plugin/tests/resolve-calling-subagent.test.ts +0 -269
  109. package/telegram-plugin/tests/silent-reply-guard.test.ts +0 -122
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.16.46",
4
+ "version": "0.17.0",
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": {
@@ -24,9 +24,9 @@
24
24
  "build": "node scripts/build.mjs",
25
25
  "build:cli": "node scripts/build.mjs && bun build --compile --target=bun-linux-x64 --minify bin/switchroom.ts --outfile switchroom-linux-amd64",
26
26
  "pretest": "npm run build",
27
- "test": "vitest run && bun test telegram-plugin/tests/history.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/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/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.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/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
27
+ "test": "vitest run && bun test telegram-plugin/tests/history.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/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/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/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.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/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
28
28
  "test:vitest": "vitest run",
29
- "test:bun": "bun test src/vault/grants.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-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/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/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/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/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/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",
29
+ "test:bun": "bun test 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-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/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/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/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
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-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs",
32
32
  "lint:tsc": "tsc --noEmit",
@@ -95,7 +95,7 @@ cd "{{agentDir}}" || exit 1
95
95
  # can distinguish cron spend from main-session spend per agent.
96
96
  # FAIL-OPEN: missing key OR unreachable proxy → strip routing env, fall back to
97
97
  # direct OAuth. An outage must never take the cron session dark.
98
- if [ -n "$SWITCHROOM_LITELLM" ] && command -v switchroom >/dev/null 2>&1; then
98
+ if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; then
99
99
  sr_ll_key="$(switchroom vault get "litellm/{{name}}/api-key" 2>/dev/null || true)"
100
100
  sr_ll_ok=""
101
101
  if [ -z "$sr_ll_key" ]; then
@@ -64,7 +64,7 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
64
64
  #
65
65
  # SWITCHROOM_AGENT_NAME is injected by compose env (compose.ts:1816)
66
66
  # so it is available here, before the inner-pass export at line ~320.
67
- if [ -n "$SWITCHROOM_LITELLM" ] && [ -z "$ANTHROPIC_CUSTOM_HEADERS" ] && command -v switchroom >/dev/null 2>&1; then
67
+ if [ -n "${SWITCHROOM_LITELLM:-}" ] && [ -z "$ANTHROPIC_CUSTOM_HEADERS" ] && command -v switchroom >/dev/null 2>&1; then
68
68
  sr_ll_key="$(switchroom vault get "litellm/$SWITCHROOM_AGENT_NAME/api-key" 2>/dev/null || true)"
69
69
  sr_ll_ok=""
70
70
  if [ -z "$sr_ll_key" ]; then
@@ -657,8 +657,24 @@ mkdir -p "$TELEGRAM_STATE_DIR" 2>/dev/null || true
657
657
  # .handoff-briefing.md and merged alongside (or instead of) .handoff.md.
658
658
  HANDOFF_FILE="{{agentDir}}/.handoff.md"
659
659
  HANDOFF_BRIEFING_FILE="{{agentDir}}/.handoff-briefing.md"
660
+ # Decide whether the on-disk briefing is stale and must be rebuilt (#2790).
661
+ # A clean shutdown leaves a non-empty .handoff.md; a *later* hard crash never
662
+ # fires the Stop hook, so that stale briefing would otherwise be re-injected
663
+ # — hiding the crashed session's turns and reorienting the agent from old
664
+ # context. Rebuild when .handoff.md is empty OR when the newest session JSONL
665
+ # is newer than .handoff.md (i.e. a session ran after the briefing was written).
666
+ _LATEST_JSONL=$(_find_latest_jsonl "$CLAUDE_CONFIG_DIR/projects")
667
+ _HANDOFF_STALE=0
660
668
  if [ ! -s "$HANDOFF_FILE" ]; then
661
- _LATEST_JSONL=$(_find_latest_jsonl "$CLAUDE_CONFIG_DIR/projects")
669
+ _HANDOFF_STALE=1
670
+ elif [ -n "$_LATEST_JSONL" ]; then
671
+ _JSONL_MOD=$(_stat_mtime "$_LATEST_JSONL")
672
+ _HANDOFF_MOD=$(_stat_mtime "$HANDOFF_FILE")
673
+ if [ "$_JSONL_MOD" -gt "$_HANDOFF_MOD" ]; then
674
+ _HANDOFF_STALE=1
675
+ fi
676
+ fi
677
+ if [ "$_HANDOFF_STALE" = "1" ]; then
662
678
  if [ -n "$_LATEST_JSONL" ]; then
663
679
  _LAST_MOD=$(_stat_mtime "$_LATEST_JSONL")
664
680
  _NOW=$(date +%s)
@@ -704,6 +720,11 @@ $_HANDOFF_CONTENT"
704
720
  APPEND_PROMPT="$_HANDOFF_CONTENT"
705
721
  fi
706
722
  fi
723
+ # Consume the briefing sidecars once injected (#2790). Leaving them on disk
724
+ # lets a later hard crash (which never fires the Stop hook to refresh them)
725
+ # re-serve this now-stale briefing on the next boot. Removing them means the
726
+ # staleness guard above starts from a clean slate every boot.
727
+ rm -f "$HANDOFF_BRIEFING_FILE" "$HANDOFF_FILE" 2>/dev/null || true
707
728
  {{else}}
708
729
  APPEND_PROMPT={{#if systemPromptAppendShellQuoted}}{{{systemPromptAppendShellQuoted}}}{{else}}""{{/if}}
709
730
  {{/if}}
@@ -807,6 +828,36 @@ if command -v switchroom >/dev/null 2>&1; then
807
828
  unset sr_wk_pair sr_wk_env sr_wk_key sr_wk_val
808
829
  fi
809
830
 
831
+ # Fleet-wide webkite render config (#2805). Seed the image-baked default
832
+ # (/opt/switchroom/webkite/config.toml) into the XDG location webkite reads
833
+ # ($HOME/.config/webkite/config.toml) so JS-heavy SPA/booking pages render
834
+ # via the baked-in cloakbrowser instead of returning a static shell.
835
+ #
836
+ # REFRESH-from-image, not copy-if-absent. $HOME (/state/agent/home) is a
837
+ # host-PERSISTENT bind mount (see src/agents/compose.ts), so a copy-if-absent
838
+ # seed would write once and then the stale home copy would shadow every future
839
+ # image-shipped config update forever — the config would be un-updatable
840
+ # fleet-wide after first boot. Instead re-sync the target from the baked
841
+ # default whenever they differ, so an image bump always propagates.
842
+ #
843
+ # Operator override still wins: an operator-authored
844
+ # ~/.switchroom/webkite/config.toml is bind-mounted RO onto this exact target
845
+ # (compose.ts). We detect that mount and leave it untouched; even absent the
846
+ # mountpoint check, a cp onto the RO mount fails harmlessly and the override
847
+ # is preserved.
848
+ if [ -f /opt/switchroom/webkite/config.toml ]; then
849
+ sr_wk_target="$HOME/.config/webkite/config.toml"
850
+ if mountpoint -q "$sr_wk_target" 2>/dev/null; then
851
+ : # operator override bind-mounted RO — never clobber it
852
+ elif ! cmp -s /opt/switchroom/webkite/config.toml "$sr_wk_target" 2>/dev/null; then
853
+ mkdir -p "$HOME/.config/webkite" 2>/dev/null || true
854
+ cp /opt/switchroom/webkite/config.toml "$sr_wk_target" 2>/dev/null \
855
+ && echo "webkite: synced fleet render config to \$HOME/.config/webkite/config.toml" >&2 \
856
+ || echo "webkite: could not sync render config (non-fatal)" >&2
857
+ fi
858
+ unset sr_wk_target
859
+ fi
860
+
810
861
  # LiteLLM routing (opt-in, #litellm). When SWITCHROOM_LITELLM is set (compose
811
862
  # env, gated on litellm.enabled && keyConfirmed), route the unmodified `claude`
812
863
  # CLI through the operator's LiteLLM proxy at ANTHROPIC_BASE_URL: fetch the
@@ -833,7 +884,7 @@ fi
833
884
  # per-session cost/issue tracking is handled out-of-band by correlating
834
885
  # LiteLLM's request log against switchroom's turn ledger, never by mutating the
835
886
  # claude protocol.
836
- if [ -n "$SWITCHROOM_LITELLM" ] && command -v switchroom >/dev/null 2>&1; then
887
+ if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; then
837
888
  sr_ll_key="$(switchroom vault get "litellm/$SWITCHROOM_AGENT_NAME/api-key" 2>/dev/null || true)"
838
889
  sr_ll_ok=""
839
890
  if [ -z "$sr_ll_key" ]; then
@@ -2,12 +2,11 @@
2
2
 
3
3
  Switchroom ships an enhanced `switchroom-telegram` MCP plugin that replaces the official marketplace plugin. It is the default — no configuration needed.
4
4
 
5
- ## 10 MCP tools
5
+ ## 9 MCP tools
6
6
 
7
7
  | Tool | What it does |
8
8
  |------|-------------|
9
- | `reply` | Send text, photos, or documents. Supports threading, topic routing, file attachments. |
10
- | `stream_reply` | Edit a single message in place as work progresses (~1/sec throttle). Use for long tasks to avoid chat spam. |
9
+ | `reply` | Send text, photos, or documents — the single final-answer tool. Chunks anything over Telegram's 4096-char limit. Supports threading, topic routing, file attachments. |
11
10
  | `react` | Add emoji reactions to messages (Telegram whitelist: 👍 👎 ❤️ 🔥 👀 🎉 etc). |
12
11
  | `edit_message` | Update a previously sent message. Edits are silent (no push notification). |
13
12
  | `delete_message` | Remove a bot-sent message (48h Telegram API limit). |
@@ -37,19 +36,13 @@ A local SQLite database (`telegram/history.db`) records every message. After a C
37
36
 
38
37
  History survives process restarts and session resets.
39
38
 
40
- ## stream_reply pattern
39
+ ## Progress while working
41
40
 
42
- For tasks taking more than ~5 seconds:
43
-
44
- ```
45
- 1. stream_reply(chat_id, "Reading the file...", done=false) ← creates message
46
- 2. stream_reply(chat_id, "Reading the file...\nParsing...", done=false) ← edits in place
47
- 3. stream_reply(chat_id, "Done! Here's the result: ...", done=true) ← locks
48
- ```
49
-
50
- Pass the **full current text** on each call (not a delta). The plugin throttles to ~1/sec.
51
-
52
- After `done=true`, send a separate `reply` if you want a push notification to the user's device (edits are silent).
41
+ For long tasks you do not need to narrate progress by editing a message. The
42
+ plugin renders an event-driven progress card (Plan → Run → Done with live tool
43
+ bullets, elapsed time, and status emoji) for free while the turn is in-flight.
44
+ Send the final answer once, with `reply` — it chunks anything over Telegram's
45
+ 4096-char limit. For an explicit mid-turn check-in use `progress_update`.
53
46
 
54
47
  ## Formatting
55
48
 
@@ -10,7 +10,7 @@ This skill is the reference for running `switchroom` CLI commands against existi
10
10
 
11
11
  **Four commands to know:**
12
12
  - `switchroom update` — full operator path: pulls images + applies config + recreates containers + runs doctor (since v0.7.8 / #918). What you want 95% of the time.
13
- - `switchroom apply` — config-only reconcile: refresh per-agent scaffolds and (re)write `~/.switchroom/compose/docker-compose.yml` without touching running containers. Use when you want to inspect the generated compose before bringing the fleet up yourself.
13
+ - `switchroom apply` — **host/operator-only**, for structural changes: refresh per-agent scaffolds and (re)write `~/.switchroom/compose/docker-compose.yml`. Its full per-agent scaffold **cannot run from inside an agent container** (no vault at the container HOME, no `docker compose` v2 plugin) — that's correct by construction, not a bug. Version rolls do NOT need it: drive the **hostd rollout** (`mcp__hostd__rollout`), which runs a `--compose-only` apply plus a per-agent restart-reconcile that refreshes each agent's templates automatically. Reserve a host-side `sudo switchroom apply` for compose regeneration / new-agent scaffolding.
14
14
  - `switchroom restart [agent]` — bounces a stuck or wedged agent
15
15
  - `switchroom version` — shows what's running (versions + health summary)
16
16
 
@@ -59,7 +59,7 @@ switchroom update --rebuild # source-checkout users: also git pull + npm bu
59
59
 
60
60
  `switchroom update` is the operator path. The CLI self-elevates via sudo internally for the per-agent scaffold dirs that need root — no need for `sudo HOME=… PATH=…` incantations.
61
61
 
62
- If you only need the config-reconcile half without restarting agents, `switchroom apply` writes `~/.switchroom/compose/docker-compose.yml` and refreshes per-agent scaffolds without touching running containers. The operator runs the docker bring-up themselves.
62
+ If you only need the config-reconcile half without restarting agents, `switchroom apply` (run **on the host by the operator**) writes `~/.switchroom/compose/docker-compose.yml` and refreshes per-agent scaffolds without touching running containers. The operator runs the docker bring-up themselves. Note this full apply cannot run from inside an agent container by construction — to roll the fleet to a new version from an agent, use the hostd rollout (`mcp__hostd__rollout`) instead of a full apply.
63
63
 
64
64
  From inside an agent's Telegram DM, the same flow is available as `/upgradestatus` (read-only) and `/update apply` (admin-gated).
65
65
 
@@ -265,12 +265,11 @@ If the user asks whether scheduled runs were missed during downtime: the schedul
265
265
 
266
266
  ## Telegram plugin reference — "what MCP tools", "how does reply work"
267
267
 
268
- The `switchroom-telegram` plugin is an enhanced fork of the official Telegram MCP plugin and is the default for all switchroom agents. It exposes **10 MCP tools** (all prefixed `mcp__switchroom-telegram__`):
268
+ The `switchroom-telegram` plugin is an enhanced fork of the official Telegram MCP plugin and is the default for all switchroom agents. It exposes **9 MCP tools** (all prefixed `mcp__switchroom-telegram__`):
269
269
 
270
270
  | Tool | Purpose |
271
271
  |---|---|
272
- | `reply` | Send a text/photo message, with optional `reply_to` for threaded quotes |
273
- | `stream_reply` | Incrementally stream a long reply (edits the same message as tokens arrive) |
272
+ | `reply` | Send a text/photo message the single final-answer tool; chunks anything over Telegram's 4096-char limit. Optional `reply_to` for threaded quotes |
274
273
  | `react` | Emoji reaction on an inbound or outbound message |
275
274
  | `edit_message` | Modify an earlier bot message's text |
276
275
  | `delete_message` | Remove an earlier bot message |
@@ -191,4 +191,4 @@ Before writing the test, ask:
191
191
  - `telegram-plugin/tests/fake-bot-api.test.ts` — meta-test of the fake;
192
192
  read first when adding new fake-bot capabilities
193
193
  - `telegram-plugin/tests/streaming-e2e.test.ts` — worked example of a
194
- larger end-to-end test (PTY → stream_reply → done)
194
+ larger end-to-end test (PTY → internal stream-reply handler → done)
@@ -9,10 +9,12 @@ and substantially extended for long-running, multi-agent deployments.
9
9
  The upstream plugin handles the basic send/receive wire protocol. This fork
10
10
  adds the ergonomics and reliability that an always-on agent fleet needs:
11
11
 
12
- - **Streaming replies** — `stream_reply` edits a single message in place as
13
- work progresses (~1/sec throttle), so users see live progress instead of
14
- silent gaps followed by a wall-of-text. Optional `lane` parameter lets each
15
- lane (e.g. `thinking` vs default `answer`) get its own message per chat+thread.
12
+ - **Live progress card** — an event-driven progress card (Plan Run → Done
13
+ with live tool bullets, elapsed time, and status emoji) edits a single
14
+ message in place as work progresses, so users see live progress instead of
15
+ silent gaps followed by a wall-of-text. This is rendered by the plugin for
16
+ free while the turn is in-flight — the model just sends the final answer once
17
+ via `reply`.
16
18
  - **Status reactions** — emoji lifecycle (👀 queued → 🤔 thinking → 👨‍💻 tool
17
19
  use → 🔥 streaming → 👍 done) on the user's own message gives "I'm working"
18
20
  feedback for free, plus stall watchdogs (🥱 30s idle, 😨 90s).
@@ -36,7 +38,7 @@ adds the ergonomics and reliability that an always-on agent fleet needs:
36
38
  - **Switchroom slash-commands** — `/agents`, `/restart`, `/logs`, `/memory`,
37
39
  `/grant`, `/dangerous`, `/permissions`, `/reconcile` etc., handled by the
38
40
  plugin without consuming Claude Code tokens.
39
- - **12 MCP tools** — `reply`, `stream_reply`, `react`, `edit_message`,
41
+ - **11 MCP tools** — `reply`, `react`, `edit_message`,
40
42
  `delete_message`, `forward_message`, `pin_message`, `send_typing`,
41
43
  `download_attachment`, `get_recent_messages`, `send_checklist`,
42
44
  `update_checklist` (the latter two ship native Telegram checklists,
@@ -196,40 +198,27 @@ When an inbound message is received, the plugin immediately reacts with an emoji
196
198
 
197
199
  Set to an empty string `""` to disable. Only Telegram's fixed emoji whitelist is accepted (👍 👎 ❤ 🔥 👀 🎉 etc). A typing indicator is also sent automatically.
198
200
 
199
- ### `stream_reply` tool (preferred for multi-step work)
201
+ ### Progress while working
200
202
 
201
- Sends or updates a streaming reply that edits one message in-place rather
202
- than sending many. Call repeatedly during long tasks with full snapshots of
203
- the current message; the plugin throttles edits to ~1/sec to respect
204
- Telegram's rate limit. Set `done=true` on the final call to lock the
205
- message.
206
-
207
- | Parameter | Required | Description |
208
- |-----------|----------|-------------|
209
- | `chat_id` | yes | Target chat ID |
210
- | `text` | yes | Full text snapshot (NOT a delta — pass the complete current content each call) |
211
- | `done` | no | `true` on final call. After `done=true` the stream is locked and further calls are no-ops. Default `false`. |
212
- | `format` | no | `"html"` (default), `"markdownv2"`, or `"text"` |
213
- | `lane` | no | Optional lane name. Each lane gets its own Telegram message per chat+thread. Use `lane: "thinking"` to surface reasoning progress alongside the main answer stream. Omit for the default answer lane. |
214
- | `message_thread_id` | no | Forum topic thread ID (auto-applied from the last inbound message if not specified) |
215
-
216
- Hard-stops at 4096 chars (Telegram message limit). On edit-404 (the message
217
- we're editing was deleted), the plugin sends a fresh message and continues
218
- the stream against the new id. A short `idleMs` pre-send debounce coalesces
219
- back-to-back snapshots before the first wire send, avoiding a redundant
220
- edit when several updates arrive in the same tick.
203
+ For long tasks the model does **not** need to narrate progress or drive a
204
+ streaming message itself. The plugin renders an event-driven progress card
205
+ (Plan Run Done with live tool bullets, elapsed time, and status emoji)
206
+ for free while the turn is in-flight. Send the final answer once, with
207
+ `reply` — it chunks anything over Telegram's 4096-char limit. (The retired
208
+ `stream_reply` tool was a redundant, worse alias of `reply`; the internal
209
+ progress-card streaming that drove it is preserved and is what renders the
210
+ live card.)
221
211
 
222
212
  ### Manual streaming progress via `edit_message`
223
213
 
224
- If you need finer control than `stream_reply` offers, you can drive the
225
- edit loop yourself:
214
+ If you want to drive an in-place edit loop yourself:
226
215
 
227
216
  1. Send an initial "thinking..." message with `reply` — note the returned `message_id`
228
217
  2. Call `edit_message` with updated text as work progresses (edits are silent — no push notification)
229
218
  3. Call `send_typing` between steps to keep the typing indicator alive (it expires after ~5s)
230
219
  4. When done, send a **new** `reply` so the user's device pings with a push notification
231
220
 
232
- In most cases `stream_reply` is simpler and is the recommended path.
221
+ In most cases the automatic progress card is simpler and is the recommended path.
233
222
 
234
223
  ### `send_typing` tool
235
224
 
@@ -75,7 +75,7 @@ const mcp = new Server(
75
75
  instructions: [
76
76
  'The sender reads Telegram, not this session. Anything you want them to see must go through the reply tool — your transcript output never reaches their chat.',
77
77
  '',
78
- 'Messages from Telegram arrive as <channel source="telegram" chat_id="..." message_id="..." user="..." ts="...">. If the tag has an image_path attribute, Read that file — it is a photo the sender attached. If the tag has attachment_file_id, call download_attachment with that file_id to fetch the file, then Read the returned path. A single message may carry SEVERAL attachments (a forwarded album or a text+multi-image burst): when attachment_count is set (>1), also handle the numbered siblings — image_path_2, image_path_3, … (Read each) and attachment_file_id_2, attachment_file_id_3, … (download_attachment each). Process every one, not just the first. Reply with the reply tool — pass chat_id back. The reply and stream_reply tools quote-reply to the latest inbound user message by default, so you do NOT need to pass reply_to for normal responses. Pass reply_to (a message_id) only when quoting a specific earlier message, or pass quote:false to send a bare (non-quoted) message.',
78
+ 'Messages from Telegram arrive as <channel source="telegram" chat_id="..." message_id="..." user="..." ts="...">. If the tag has an image_path attribute, Read that file — it is a photo the sender attached. If the tag has attachment_file_id, call download_attachment with that file_id to fetch the file, then Read the returned path. A single message may carry SEVERAL attachments (a forwarded album or a text+multi-image burst): when attachment_count is set (>1), also handle the numbered siblings — image_path_2, image_path_3, … (Read each) and attachment_file_id_2, attachment_file_id_3, … (download_attachment each). Process every one, not just the first. Reply with the reply tool — pass chat_id back. The reply tool quote-replies to the latest inbound user message by default, so you do NOT need to pass reply_to for normal responses. Pass reply_to (a message_id) only when quoting a specific earlier message, or pass quote:false to send a bare (non-quoted) message.',
79
79
  '',
80
80
  'reply accepts file paths (files: ["/abs/path.png"]) for attachments. Use react to add emoji reactions, edit_message for interim progress updates, and delete_message when you need to truly remove a message (prefer edit_message if you just want to change text — delete is for retraction). Edits don\'t trigger push notifications — when a long task completes, send a new reply so the user\'s device pings. Use send_typing to show a typing indicator during long operations. Use pin_message to pin important outputs. Use forward_message to quote/resurface earlier messages.',
81
81
  '',
@@ -134,46 +134,6 @@ const TOOL_SCHEMAS = [
134
134
  required: ['chat_id', 'text'],
135
135
  },
136
136
  },
137
- {
138
- name: 'stream_reply',
139
- description:
140
- 'Post the final answer for this turn. The plugin renders an event-driven progress card (Plan → Run → Done with live tool bullets, elapsed time, and status emoji) for free while the turn is in-flight, so you do not need to narrate intermediate progress. Call `stream_reply` exactly once per turn with done=true and the complete answer text. Hard cap is 32768 chars (the rich-message wire limit) — longer text is dropped by a defensive guard, so use `reply` for anything that long (it chunks). Calling with done=false is an error in this environment (the progress card already owns the mid-turn surface). inline_keyboard adds tappable buttons under the final message — see `reply` for shape and constraints.',
141
- inputSchema: {
142
- type: 'object',
143
- properties: {
144
- chat_id: { type: 'string' },
145
- text: { type: 'string', description: 'Full text snapshot. NOT a delta — pass the complete current content each call.' },
146
- done: { type: 'boolean', description: 'Must be true. Posts this text as the final answer for the turn and locks the message.' },
147
- message_thread_id: { type: 'string', description: 'Forum topic thread ID. Auto-applied from the last inbound message if not specified.' },
148
- origin_turn_id: { type: 'string', description: 'In a forum supergroup, pass back the origin_turn_id attribute from the <channel> message you are answering. It pins the reply to that message\'s topic even if another topic\'s turn started meanwhile. Omit in DMs / single-topic chats.' },
149
- format: { type: 'string', enum: ['html', 'markdownv2', 'text'], description: "Rendering mode. 'html' (default) converts markdown to Telegram HTML." },
150
- reply_to: { type: 'string', description: 'Message ID to quote-reply to. Overrides the default (latest inbound).' },
151
- quote: { type: 'boolean', description: 'Opt out of the default quote-reply behavior. Default: true. Ignored when reply_to is explicitly set.' },
152
- protect_content: { type: 'boolean', description: 'When true, Telegram prevents the message from being forwarded or saved.' },
153
- quote_text: { type: 'string', description: 'Surgical quote: specific text to highlight from the reply_to message. Requires reply_to.' },
154
- disable_notification: { type: 'boolean', description: 'When true, the INITIAL message send is silent (no device ping). Has no effect on subsequent edits — Telegram never pings on editMessageText. Default false. Use for mid-turn stream starts you do not want to ping; omit on the final answer.' },
155
- inline_keyboard: {
156
- type: 'array',
157
- description: '2D array of tappable buttons under the final message. Same shape and constraints as `reply.inline_keyboard` — each button has `text` and EXACTLY ONE of `url` or `callback_data`, plus optional `ack_text` (custom tap-toast; default "✓ received") and `single_use` (default true; set false to keep the keyboard tappable after a tap). Tap on a callback_data button is delivered to this agent as an inbound channel event with meta.button_callback_data set.',
158
- items: {
159
- type: 'array',
160
- items: {
161
- type: 'object',
162
- properties: {
163
- text: { type: 'string' },
164
- url: { type: 'string' },
165
- callback_data: { type: 'string' },
166
- ack_text: { type: 'string', description: 'Toast text shown on tap. Default "✓ received".' },
167
- single_use: { type: 'boolean', description: 'Default true. Set false to keep the keyboard tappable after this button is tapped.' },
168
- },
169
- required: ['text'],
170
- },
171
- },
172
- },
173
- },
174
- required: ['chat_id', 'text'],
175
- },
176
- },
177
137
  {
178
138
  name: 'react',
179
139
  description: 'Add an emoji reaction to a Telegram message. Telegram only accepts a fixed whitelist (👍 👎 ❤ 🔥 👀 🎉 etc) — non-whitelisted emoji will be rejected.',
@@ -30,13 +30,12 @@ export interface NamedTool {
30
30
  }
31
31
 
32
32
  /**
33
- * Hot tools pinned loaded — must never defer. The reply path
34
- * (reply/stream_reply) plus the frequently-used early-turn ops. Everything
35
- * NOT in this set defers under tool-search.
33
+ * Hot tools pinned loaded — must never defer. The reply path plus the
34
+ * frequently-used early-turn ops. Everything NOT in this set defers under
35
+ * tool-search.
36
36
  */
37
37
  export const ALWAYS_LOAD_TOOLS: ReadonlySet<string> = new Set([
38
38
  'reply',
39
- 'stream_reply',
40
39
  'get_recent_messages',
41
40
  'react',
42
41
  'edit_message',