switchroom 0.18.7 → 0.18.8
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.
- package/dist/cli/switchroom.js +905 -758
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +111 -34
- package/skills/switchroom-runtime/SKILL.md +2 -0
- package/telegram-plugin/dist/gateway/gateway.js +1403 -657
- package/telegram-plugin/flood-circuit-breaker.ts +123 -0
- package/telegram-plugin/gateway/activity-card-store.ts +63 -18
- package/telegram-plugin/gateway/boot-card.ts +27 -0
- package/telegram-plugin/gateway/busy-ack.ts +106 -0
- package/telegram-plugin/gateway/gateway.ts +564 -85
- package/telegram-plugin/gateway/mental-model-propose-diff.ts +61 -5
- package/telegram-plugin/gateway/model-command.ts +23 -11
- package/telegram-plugin/gateway/session-model-file.ts +198 -0
- package/telegram-plugin/gateway/status-pin-store.ts +82 -22
- package/telegram-plugin/gateway/worker-pin-reaper.ts +114 -0
- package/telegram-plugin/hooks/hooks.json +10 -10
- package/telegram-plugin/hooks/run-hook.sh +84 -0
- package/telegram-plugin/model-unavailable.ts +26 -0
- package/telegram-plugin/pty-partial-handler.ts +39 -0
- package/telegram-plugin/render/rich-render.ts +79 -1
- package/telegram-plugin/retry-api-call.ts +62 -0
- package/telegram-plugin/shared/bot-runtime.ts +8 -1
- package/telegram-plugin/silence-poke.ts +14 -0
- package/telegram-plugin/stream-controller.ts +156 -38
- package/telegram-plugin/tests/activity-card-store.test.ts +47 -2
- package/telegram-plugin/tests/approval-card-restart-outcome.test.ts +218 -0
- package/telegram-plugin/tests/boot-card-flood-suppress.test.ts +111 -0
- package/telegram-plugin/tests/busy-ack-wiring.test.ts +118 -0
- package/telegram-plugin/tests/busy-ack.test.ts +121 -0
- package/telegram-plugin/tests/flood-circuit-breaker.test.ts +74 -0
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +177 -25
- package/telegram-plugin/tests/mental-model-name-entity-corruption.test.ts +119 -0
- package/telegram-plugin/tests/model-command.test.ts +2 -2
- package/telegram-plugin/tests/model-unavailable.test.ts +41 -0
- package/telegram-plugin/tests/pty-partial-handler.test.ts +56 -0
- package/telegram-plugin/tests/render/render-outbound-chunks.test.ts +98 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +59 -0
- package/telegram-plugin/tests/run-hook-wrapper.test.ts +132 -0
- package/telegram-plugin/tests/session-model-file.test.ts +132 -0
- package/telegram-plugin/tests/slot-banner-boot-recovery.test.ts +3 -3
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +3 -3
- package/telegram-plugin/tests/status-pin-store.test.ts +62 -6
- package/telegram-plugin/tests/stream-controller-chunk-cap.test.ts +122 -0
- package/telegram-plugin/tests/voice-send.test.ts +308 -0
- package/telegram-plugin/tests/worker-pin-reaper.test.ts +132 -0
- package/telegram-plugin/uat/scenarios/jtbd-deliberate-restart-resumes-dm.test.ts +118 -0
- package/telegram-plugin/uat/scenarios/jtbd-midflight-busy-ack-dm.test.ts +201 -0
- package/telegram-plugin/uat/scenarios/jtbd-worker-pin-lifecycle-dm.test.ts +208 -0
- package/telegram-plugin/uat/scenarios/vault-card-survives-gateway-restart-dm.test.ts +140 -0
- package/telegram-plugin/uat/scenarios/vault-deny-resumes-turn-dm.test.ts +84 -0
- package/telegram-plugin/uat/scenarios/vault-timeout-wakes-agent-dm.test.ts +91 -0
- package/telegram-plugin/voice-ondemand.ts +25 -1
- package/telegram-plugin/voice-send.ts +154 -0
|
@@ -28738,7 +28738,7 @@ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:f
|
|
|
28738
28738
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
28739
28739
|
|
|
28740
28740
|
// src/build-info.ts
|
|
28741
|
-
var VERSION = "0.18.
|
|
28741
|
+
var VERSION = "0.18.8";
|
|
28742
28742
|
|
|
28743
28743
|
// src/cli/resolve-version.ts
|
|
28744
28744
|
function readPackageVersion() {
|
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.18.
|
|
4
|
+
"version": "0.18.8",
|
|
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": {
|
|
@@ -980,8 +980,8 @@ fi
|
|
|
980
980
|
# per-session cost/issue tracking is handled out-of-band by correlating
|
|
981
981
|
# LiteLLM's request log against switchroom's turn ledger, never by mutating the
|
|
982
982
|
# claude protocol.
|
|
983
|
-
# _LITELLM_OK is read by the session-model
|
|
984
|
-
# (LiteLLM/OpenRouter) session override must be
|
|
983
|
+
# _LITELLM_OK is read by the session-model resolution block below: an sr-*
|
|
984
|
+
# (LiteLLM/OpenRouter) session override must NOT be applied when the proxy is
|
|
985
985
|
# unreachable at boot, or claude would 4xx an unknown model against Anthropic.
|
|
986
986
|
# Empty by default (proxy not confirmed live); set to "1" only on a successful
|
|
987
987
|
# liveliness probe. Deliberately NOT unset with the sr_ll_* scratch vars.
|
|
@@ -1065,54 +1065,131 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
|
|
|
1065
1065
|
unset sr_ll_key sr_ll_ok sr_ll_unreachable
|
|
1066
1066
|
fi
|
|
1067
1067
|
|
|
1068
|
-
# --- Session
|
|
1068
|
+
# --- Session model resolution (durable .session-model + .relaunch-model-intent) ---
|
|
1069
1069
|
#
|
|
1070
|
-
#
|
|
1071
|
-
#
|
|
1072
|
-
#
|
|
1073
|
-
#
|
|
1074
|
-
#
|
|
1075
|
-
#
|
|
1070
|
+
# Contract: reference/rfcs/session-model-stickiness.md. A positively-
|
|
1071
|
+
# confirmed `/model X` switch persists as `{{agentDir}}/.session-model`
|
|
1072
|
+
# (one-line JSON written by the gateway). Whether THIS boot applies it is
|
|
1073
|
+
# decided by the one-shot `.relaunch-model-intent` file, stamped by the
|
|
1074
|
+
# gateway immediately before every switchroom-managed bounce:
|
|
1075
|
+
#
|
|
1076
|
+
# fresh (<10 min by embedded ts) "keep" intent → apply the override
|
|
1077
|
+
# anything else (revert / absent / stale / corrupt) → delete the override,
|
|
1078
|
+
# boot the configured default, and (when an override was actually
|
|
1079
|
+
# removed) write a `.session-model-alert` notice naming why.
|
|
1080
|
+
#
|
|
1081
|
+
# Default is REVERT (operator decision 2026-07): a crash, OOM, raw
|
|
1082
|
+
# `docker restart`, host reboot, or deploy writes no intent and therefore
|
|
1083
|
+
# reverts to the yaml model. Watchdog/recovery bounces run gateway code and
|
|
1084
|
+
# stamp "keep" before SIGTERM, so a confirmed switch survives them.
|
|
1076
1085
|
#
|
|
1077
|
-
# One-shot + session-only: the carrier is consumed (rm) on this one boot
|
|
1078
|
-
# regardless of validity, so the next restart reverts to the configured
|
|
1079
|
-
# default `{{{modelQ}}}`. Mirrors the `.force-fresh-session` marker's
|
|
1080
|
-
# gateway↔start.sh round-trip. See telegram-plugin/gateway/model-command.ts.
|
|
1081
1086
|
# NB {{{modelQ}}} is already shell-single-quoted by the scaffold (it renders as
|
|
1082
1087
|
# a quoted token, e.g. 'claude-sonnet-5'), so it is assigned BARE here — never
|
|
1083
1088
|
# inside additional double quotes, which would embed the literal quote chars in
|
|
1084
1089
|
# the value and break `claude --model`.
|
|
1085
1090
|
_EFFECTIVE_MODEL={{{modelQ}}}
|
|
1091
|
+
# Record the RESOLVED configured default (raw, unquoted) every boot, before
|
|
1092
|
+
# override resolution. The gateway copies this into the override's
|
|
1093
|
+
# `configuredDefaultAtWrite`, so both sides of the invalidation compare below
|
|
1094
|
+
# come from the same resolver. Overwrite, not consumed.
|
|
1095
|
+
printf '%s\n' "$_EFFECTIVE_MODEL" > "{{agentDir}}/.configured-default-model" 2>/dev/null || true
|
|
1096
|
+
|
|
1097
|
+
# Migration shim (one release, RFC §7): a leftover one-shot
|
|
1098
|
+
# `.session-model-override` carrier means an OLD gateway wrote it immediately
|
|
1099
|
+
# before this very bounce — it is the newest user intent and WINS over any
|
|
1100
|
+
# `.session-model` on disk. Convert (overwrite) + consume, and apply THIS
|
|
1101
|
+
# boot (the carrier's presence is itself the keep signal).
|
|
1102
|
+
_sm_keep=""
|
|
1086
1103
|
if [ -f "{{agentDir}}/.session-model-override" ]; then
|
|
1087
|
-
|
|
1104
|
+
_mig="$(tr -d '[:space:]' < "{{agentDir}}/.session-model-override" 2>/dev/null || true)"
|
|
1088
1105
|
rm -f "{{agentDir}}/.session-model-override"
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1106
|
+
if printf '%s' "$_mig" | grep -Eq '^[A-Za-z0-9][]A-Za-z0-9._[/-]{0,99}$'; then
|
|
1107
|
+
printf '{"model":"%s","configuredDefaultAtWrite":"%s","ts":%s}\n' "$_mig" "$_EFFECTIVE_MODEL" "$(( $(date +%s) * 1000 ))" > "{{agentDir}}/.session-model" 2>/dev/null || true
|
|
1108
|
+
_sm_keep="1"
|
|
1109
|
+
echo "session-model: migrated legacy one-shot carrier '$_mig' to durable .session-model (applying this boot)" >&2
|
|
1110
|
+
else
|
|
1111
|
+
echo "session-model: ignoring malformed legacy .session-model-override (failed shape gate)" >&2
|
|
1112
|
+
fi
|
|
1113
|
+
unset _mig
|
|
1114
|
+
fi
|
|
1115
|
+
|
|
1116
|
+
# One-shot intent: consume unconditionally, honor only a fresh "keep".
|
|
1117
|
+
# Freshness clock is the EMBEDDED ts (ms), never file mtime — same clock the
|
|
1118
|
+
# gateway writes with.
|
|
1119
|
+
_sm_reason=""
|
|
1120
|
+
if [ -f "{{agentDir}}/.relaunch-model-intent" ]; then
|
|
1121
|
+
_int_raw="$(cat "{{agentDir}}/.relaunch-model-intent" 2>/dev/null || true)"
|
|
1122
|
+
rm -f "{{agentDir}}/.relaunch-model-intent"
|
|
1123
|
+
_int="$(printf '%s' "$_int_raw" | sed -n 's/.*"intent"[[:space:]]*:[[:space:]]*"\([a-z]*\)".*/\1/p')"
|
|
1124
|
+
_int_ts="$(printf '%s' "$_int_raw" | sed -n 's/.*"ts"[[:space:]]*:[[:space:]]*\([0-9]\{1,\}\).*/\1/p')"
|
|
1125
|
+
_sm_reason="$(printf '%s' "$_int_raw" | sed -n 's/.*"reason"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1126
|
+
if [ "$_int" = "keep" ] && [ -n "$_int_ts" ] && [ $(( $(date +%s) * 1000 - _int_ts )) -lt 600000 ]; then
|
|
1127
|
+
_sm_keep="1"
|
|
1128
|
+
fi
|
|
1129
|
+
unset _int_raw _int _int_ts
|
|
1130
|
+
fi
|
|
1131
|
+
|
|
1132
|
+
if [ -f "{{agentDir}}/.session-model" ]; then
|
|
1133
|
+
_smf="$(cat "{{agentDir}}/.session-model" 2>/dev/null || true)"
|
|
1134
|
+
_sm_model="$(printf '%s' "$_smf" | sed -n 's/.*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1135
|
+
_sm_cfg="$(printf '%s' "$_smf" | sed -n 's/.*"configuredDefaultAtWrite"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1136
|
+
_sm_ts="$(printf '%s' "$_smf" | sed -n 's/.*"ts"[[:space:]]*:[[:space:]]*\([0-9]\{1,\}\).*/\1/p')"
|
|
1137
|
+
if [ -z "$_sm_keep" ]; then
|
|
1138
|
+
# No fresh keep intent → deliberate restart, crash, external container
|
|
1139
|
+
# restart, or deploy. Revert to the configured default and say so.
|
|
1140
|
+
rm -f "{{agentDir}}/.session-model"
|
|
1141
|
+
_sm_why="${_sm_reason:-no keep intent — crash, external container restart, or deploy}"
|
|
1142
|
+
echo "session-model: reverting to configured default '$_EFFECTIVE_MODEL' — $_sm_why (session override '$_sm_model' cleared)" >&2
|
|
1143
|
+
printf 'Session model override `%s` was cleared — this relaunch reverted to the configured default `%s` (%s). Re-issue /model %s to switch back.\n' "$_sm_model" "$_EFFECTIVE_MODEL" "$_sm_why" "$_sm_model" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1144
|
+
unset _sm_why
|
|
1145
|
+
# Shape gate — kept BYTE-IDENTICAL with MODEL_ARG_RE in
|
|
1146
|
+
# telegram-plugin/gateway/model-command.ts. `/` is allowed for
|
|
1147
|
+
# OpenRouter-style `sr-vendor/model` ids; it is not a shell metachar inside
|
|
1148
|
+
# the double-quoted `claude --model "$_EFFECTIVE_MODEL"` usage below.
|
|
1149
|
+
# The wc -c/-l check rejects EMPTY and MULTILINE values first: the sed
|
|
1150
|
+
# extraction above emits one match per matching LINE, and `grep -Eq`
|
|
1151
|
+
# passes if ANY line matches — a multiline value must never reach
|
|
1152
|
+
# `claude --model` (parity with parseSessionModel's single-string check).
|
|
1153
|
+
elif [ "$(printf '%s' "$_sm_model" | wc -c)" -eq 0 ] || [ "$(printf '%s' "$_sm_model" | wc -l)" -ne 0 ] || ! printf '%s' "$_sm_model" | grep -Eq '^[A-Za-z0-9][]A-Za-z0-9._[/-]{0,99}$'; then
|
|
1154
|
+
rm -f "{{agentDir}}/.session-model"
|
|
1155
|
+
echo "session-model: ignoring malformed .session-model (failed shape gate) — using configured default '$_EFFECTIVE_MODEL'" >&2
|
|
1156
|
+
elif [ "$_sm_cfg" != "$_EFFECTIVE_MODEL" ]; then
|
|
1157
|
+
# switchroom.yaml `model:` changed since the switch → the override is
|
|
1158
|
+
# against a default that no longer exists. Invalidate + announce.
|
|
1159
|
+
rm -f "{{agentDir}}/.session-model"
|
|
1160
|
+
echo "session-model: configured default changed ('$_sm_cfg' → '$_EFFECTIVE_MODEL') — clearing session override '$_sm_model'" >&2
|
|
1161
|
+
printf 'The configured default model changed (`%s` → `%s`), so your session override to `%s` was cleared — the agent booted on the new configured default. Re-issue /model %s if you still want it.\n' "$_sm_cfg" "$_EFFECTIVE_MODEL" "$_sm_model" "$_sm_model" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1162
|
+
elif [ -n "$_sm_ts" ] && [ $(( $(date +%s) * 1000 - _sm_ts )) -gt 604800000 ]; then
|
|
1163
|
+
# 7-day staleness bound (belt-and-braces vs version-rollback resurrection
|
|
1164
|
+
# and long-forgotten overrides).
|
|
1165
|
+
rm -f "{{agentDir}}/.session-model"
|
|
1166
|
+
echo "session-model: session override '$_sm_model' is older than 7 days — expiring it, using configured default '$_EFFECTIVE_MODEL'" >&2
|
|
1167
|
+
printf 'Session model override `%s` expired (older than 7 days) — the agent booted on its configured default `%s`. Re-issue /model %s if you still want it.\n' "$_sm_model" "$_EFFECTIVE_MODEL" "$_sm_model" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1168
|
+
else
|
|
1169
|
+
case "$_sm_model" in
|
|
1102
1170
|
sr-*)
|
|
1103
1171
|
if [ -z "$_LITELLM_OK" ]; then
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1172
|
+
# sr-* + LiteLLM unreachable: booting the override would 4xx against
|
|
1173
|
+
# Anthropic. Boot the configured default but RETAIN the durable file
|
|
1174
|
+
# — it re-applies on the next keep relaunch once the proxy is back.
|
|
1175
|
+
echo "session-model: LiteLLM proxy unreachable at boot — NOT applying sr-* override '$_sm_model' this boot; booting configured default '$_EFFECTIVE_MODEL' (override retained, re-applies next relaunch)" >&2
|
|
1176
|
+
printf 'LiteLLM proxy was unreachable at boot, so the session model override `%s` was not applied — the agent booted on its configured default `%s`. The override is retained and will re-apply on the next switchroom-managed relaunch once LiteLLM is reachable; /model default drops it.\n' "$_sm_model" "$_EFFECTIVE_MODEL" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1177
|
+
else
|
|
1178
|
+
_EFFECTIVE_MODEL="$_sm_model"
|
|
1108
1179
|
fi
|
|
1109
1180
|
;;
|
|
1181
|
+
*)
|
|
1182
|
+
_EFFECTIVE_MODEL="$_sm_model"
|
|
1183
|
+
;;
|
|
1110
1184
|
esac
|
|
1111
|
-
|
|
1112
|
-
|
|
1185
|
+
if [ "$_EFFECTIVE_MODEL" = "$_sm_model" ]; then
|
|
1186
|
+
echo "session-model: keeping session override '$_sm_model' across this switchroom-managed relaunch${_sm_reason:+ ($_sm_reason)}" >&2
|
|
1187
|
+
printf 'Session model override `%s` kept across this relaunch%s. It reverts on /restart, agent restart, crash, or an external container restart; /model default clears it now.\n' "$_sm_model" "${_sm_reason:+ ($_sm_reason)}" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1188
|
+
fi
|
|
1113
1189
|
fi
|
|
1114
|
-
unset
|
|
1190
|
+
unset _smf _sm_model _sm_cfg _sm_ts
|
|
1115
1191
|
fi
|
|
1192
|
+
unset _sm_keep _sm_reason
|
|
1116
1193
|
|
|
1117
1194
|
# sr-* passthrough→router repoint — ONE post-resolution gate covering BOTH the
|
|
1118
1195
|
# /model override path AND the configured-default path (`model: sr-*` in
|
|
@@ -178,4 +178,6 @@ This is **the persistent-shell wedge.** Claude Code keeps a single `bash` subpro
|
|
|
178
178
|
|
|
179
179
|
**Triggering causes to avoid.** The wedge most often follows: (a) a long `npm test` / `bun test` run, (b) any command that was `!`-interrupted mid-flight, (c) heredoc-style commands the shell's stdin couldn't fully consume. Prevention: dispatch heavy test suites to a worker sub-agent (so the wedge dies with the worker) rather than running them in your own session, and use `run_in_background: true` for long jobs.
|
|
180
180
|
|
|
181
|
+
**Mid-flight responsiveness — never block your turn on a long foreground watch.** A blocking foreground command (`gh pr checks --watch`, `sleep`-and-poll loops, a long `docker build`) pins your whole turn: a user message arriving mid-watch waits behind it, sometimes for minutes. The gateway posts a deterministic "⏳ Queued — currently inside `<tool>`" ack on your behalf (#2995), but the ack is a mitigation, not a license. Run long watches as background tasks (`run_in_background: true`, then poll `BashOutput` between other work) so mid-flight questions get real answers in seconds.
|
|
182
|
+
|
|
181
183
|
A sentinel file at `$TELEGRAM_STATE_DIR/wedge-detected.json` records the most recent wedge detection. Operators can `cat` it for forensic timestamps; you don't normally need to read it yourself.
|