switchroom 0.18.28 → 0.18.30
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/bin/handoff-briefing.sh +15 -2
- package/dist/agent-scheduler/index.js +111 -7
- package/dist/auth-broker/index.js +154 -73
- package/dist/cli/autoaccept-poll.js +8 -3
- package/dist/cli/drive-write-pretool.mjs +8 -3
- package/dist/cli/ms-365-write-pretool.mjs +158 -11
- package/dist/cli/notion-write-pretool.mjs +103 -4
- package/dist/cli/switchroom.js +2712 -2219
- package/dist/host-control/main.js +110 -70
- package/dist/vault/approvals/kernel-server.js +116 -70
- package/dist/vault/broker/server.js +314 -202
- package/package.json +3 -3
- package/profiles/_base/start.sh.hbs +105 -34
- package/telegram-plugin/dist/bridge/bridge.js +71 -47
- package/telegram-plugin/dist/gateway/gateway.js +1128 -666
- package/telegram-plugin/dist/server.js +89 -64
- package/telegram-plugin/gateway/backstop-delivery.ts +272 -0
- package/telegram-plugin/gateway/forward-origin.ts +9 -1
- package/telegram-plugin/gateway/gateway.ts +656 -388
- package/telegram-plugin/gateway/model-command.ts +331 -602
- package/telegram-plugin/gateway/session-model-file.ts +40 -0
- package/telegram-plugin/gateway/turn-record-status.ts +45 -0
- package/telegram-plugin/gateway/unhandled-message.ts +177 -0
- package/telegram-plugin/history.ts +153 -23
- package/telegram-plugin/llm-error-present.ts +24 -0
- package/telegram-plugin/model-unavailable.ts +55 -0
- package/telegram-plugin/operator-events.ts +113 -0
- package/telegram-plugin/pending-user-notice.ts +88 -0
- package/telegram-plugin/shared/local-time.ts +99 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +250 -0
- package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +103 -0
- package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +264 -0
- package/telegram-plugin/tests/forward-origin.test.ts +30 -3
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +111 -60
- package/telegram-plugin/tests/history.test.ts +88 -0
- package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +278 -0
- package/telegram-plugin/tests/local-time.test.ts +135 -0
- package/telegram-plugin/tests/model-command.test.ts +427 -1512
- package/telegram-plugin/tests/session-model-file.test.ts +23 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +34 -0
- package/telegram-plugin/tier-downgrade.ts +4 -3
- package/telegram-plugin/turn-flush-safety.ts +25 -1
- package/vendor/hindsight-memory/scripts/backfill_transcripts.py +399 -2
- package/vendor/hindsight-memory/scripts/lib/client.py +47 -0
- package/vendor/hindsight-memory/scripts/lib/content.py +93 -7
- package/vendor/hindsight-memory/scripts/lib/turnlog.py +450 -0
- package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +467 -0
- package/vendor/hindsight-memory/tests/test_content.py +63 -7
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.30",
|
|
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/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.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",
|
|
27
|
+
"test": "vitest run && bun test telegram-plugin/tests/catch-all-forwarded-history.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/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.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/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/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.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",
|
|
29
|
+
"test:bun": "bun test 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-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/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.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 && node scripts/check-no-unpinned-npx-playwright.mjs",
|
|
32
32
|
"lint:tsc": "tsc --noEmit",
|
|
@@ -1148,18 +1148,19 @@ fi
|
|
|
1148
1148
|
|
|
1149
1149
|
# --- Session model resolution (consume-once .session-model carrier) ---
|
|
1150
1150
|
#
|
|
1151
|
-
# Contract: reference/rfcs/session-model-stickiness.md §0.
|
|
1152
|
-
# decision 2026-07-
|
|
1153
|
-
# `/model` override lasts only for the
|
|
1151
|
+
# Contract: reference/rfcs/session-model-stickiness.md §0.05 (rev 5, operator
|
|
1152
|
+
# decision 2026-07-16 — DETERMINISTIC SWITCH, superseding rev 4 §0.1's "live
|
|
1153
|
+
# Claude switches write no carrier"). A `/model` override lasts only for the
|
|
1154
|
+
# current session:
|
|
1154
1155
|
#
|
|
1155
|
-
# -
|
|
1156
|
-
#
|
|
1157
|
-
#
|
|
1158
|
-
#
|
|
1159
|
-
#
|
|
1160
|
-
#
|
|
1161
|
-
#
|
|
1162
|
-
#
|
|
1156
|
+
# - EVERY `/model` switch — Claude→Claude, Claude→sr-*, sr-*→Claude, the
|
|
1157
|
+
# Fable/alias button, and the picker SELECT — writes `{{agentDir}}/.session-model`
|
|
1158
|
+
# (one-line JSON) IMMEDIATELY before the relaunch that applies it. The
|
|
1159
|
+
# inject-into-tmux + terminal-scrape path is RETIRED: it silently no-op'd
|
|
1160
|
+
# then optimistically lied to /status. This block APPLIES the carrier on the
|
|
1161
|
+
# single boot that reads it and then DELETES it (consume-once), and the
|
|
1162
|
+
# `exec claude --model <token>` below cannot silently no-op.
|
|
1163
|
+
# - Any SUBSEQUENT restart — deploy, `/restart`, `/new`, watchdog recovery,
|
|
1163
1164
|
# crash, raw `docker restart`, host reboot — finds no carrier and boots the
|
|
1164
1165
|
# configured default. That deletion is the marker distinguishing the
|
|
1165
1166
|
# model-apply relaunch from every later restart.
|
|
@@ -1181,10 +1182,17 @@ _EFFECTIVE_MODEL={{{modelQ}}}
|
|
|
1181
1182
|
printf '%s\n' "$_EFFECTIVE_MODEL" > "{{agentDir}}/.configured-default-model" 2>/dev/null || true
|
|
1182
1183
|
|
|
1183
1184
|
# Rev-4 hygiene (#3184 review LOW-2): remove the files retired with the
|
|
1184
|
-
# keep/revert intent subsystem
|
|
1185
|
-
#
|
|
1186
|
-
#
|
|
1187
|
-
rm -f "{{agentDir}}/.relaunch-model-intent" "{{agentDir}}/.session-model-
|
|
1185
|
+
# keep/revert intent subsystem. Nothing reads them anymore; without this rm they
|
|
1186
|
+
# would linger in the bind-mounted state dir forever after rollout. Idempotent —
|
|
1187
|
+
# a cheap no-op once clean.
|
|
1188
|
+
rm -f "{{agentDir}}/.relaunch-model-intent" "{{agentDir}}/.session-model-kept-notified"
|
|
1189
|
+
# `.session-model-boot-attempts` is REVIVED as LIVE state (#3284 bounded-retry
|
|
1190
|
+
# counter, see the resolution block below): the gateway clears it on a healthy
|
|
1191
|
+
# boot, and start.sh gives up + reverts once it exceeds the attempt bound. It is
|
|
1192
|
+
# meaningless without a carrier to belong to, so sweep it as stale ONLY when no
|
|
1193
|
+
# `.session-model` carrier is present (rollout leftover / completed apply / a
|
|
1194
|
+
# prior giveup that didn't get to delete it).
|
|
1195
|
+
[ -f "{{agentDir}}/.session-model" ] || rm -f "{{agentDir}}/.session-model-boot-attempts"
|
|
1188
1196
|
|
|
1189
1197
|
# Migration shim (one release): a leftover one-shot `.session-model-override`
|
|
1190
1198
|
# carrier from a pre-consume-once gateway means an OLD gateway wrote it
|
|
@@ -1206,11 +1214,34 @@ if [ -f "{{agentDir}}/.session-model" ]; then
|
|
|
1206
1214
|
_smf="$(cat "{{agentDir}}/.session-model" 2>/dev/null || true)"
|
|
1207
1215
|
_sm_model="$(printf '%s' "$_smf" | sed -n 's/.*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1208
1216
|
_sm_cfg="$(printf '%s' "$_smf" | sed -n 's/.*"configuredDefaultAtWrite"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1209
|
-
#
|
|
1210
|
-
#
|
|
1211
|
-
#
|
|
1212
|
-
#
|
|
1213
|
-
|
|
1217
|
+
# BOUNDED-RETRY CONSUME (#3284) — the carrier is NO LONGER deleted here before
|
|
1218
|
+
# the apply/exec. It used to be (pure consume-once), but a boot that reads the
|
|
1219
|
+
# carrier may WEDGE before its gateway acquires the boot lock (the
|
|
1220
|
+
# boot.lock_stale_recovered_boot_mismatch race, e.g. a proxy-only `fable`
|
|
1221
|
+
# apply-boot) and never become the surviving healthy session. Deleting before
|
|
1222
|
+
# apply handed the requested model to that loser: the retry boot found no
|
|
1223
|
+
# carrier and silently reverted to the configured default. Instead:
|
|
1224
|
+
#
|
|
1225
|
+
# - the GATEWAY deletes the carrier + the attempt counter once it ACQUIRES
|
|
1226
|
+
# the boot lock (boot.lock_acquired in gateway.ts — the healthy-boot signal
|
|
1227
|
+
# a wedged boot cannot fake), so a GOOD token survives a transient wedge
|
|
1228
|
+
# and RE-APPLIES on the retry boot instead of reverting;
|
|
1229
|
+
# - a persisted attempt counter (`.session-model-boot-attempts`) BOUNDS the
|
|
1230
|
+
# retries: a token that never reaches a healthy gateway (a genuinely BAD
|
|
1231
|
+
# token that crashes claude before lock-acquire) is given up after
|
|
1232
|
+
# _SM_MAX_ATTEMPTS boots — deleted, reverted, and alerted — so a bad token
|
|
1233
|
+
# can NEVER produce an unbounded wedge→retry→wedge crashloop. The original
|
|
1234
|
+
# "a bad token crashes at most one boot then reverts" guarantee becomes "at
|
|
1235
|
+
# most _SM_MAX_ATTEMPTS boots then reverts", still a hard finite bound.
|
|
1236
|
+
#
|
|
1237
|
+
# Every INVALIDATION / GIVEUP branch below deletes BOTH the carrier and the
|
|
1238
|
+
# counter; only the APPLY branch keeps the carrier (for the gateway to clear)
|
|
1239
|
+
# and persists the incremented counter. The delete-before-apply that used to
|
|
1240
|
+
# sit here is GONE by design.
|
|
1241
|
+
_SM_MAX_ATTEMPTS=3
|
|
1242
|
+
_sm_attempts="$(cat "{{agentDir}}/.session-model-boot-attempts" 2>/dev/null | tr -dc '0-9' || true)"
|
|
1243
|
+
[ -z "$_sm_attempts" ] && _sm_attempts=0
|
|
1244
|
+
_sm_attempts=$(( _sm_attempts + 1 ))
|
|
1214
1245
|
# Shape gate — kept BYTE-IDENTICAL with MODEL_ARG_RE in
|
|
1215
1246
|
# telegram-plugin/gateway/model-command.ts. `/` is allowed for
|
|
1216
1247
|
# OpenRouter-style `sr-vendor/model` ids; it is not a shell metachar inside
|
|
@@ -1221,29 +1252,56 @@ if [ -f "{{agentDir}}/.session-model" ]; then
|
|
|
1221
1252
|
# `claude --model` (parity with parseSessionModel's single-string check).
|
|
1222
1253
|
if [ "$(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
|
|
1223
1254
|
# Invalid carrier: fall back to the configured default AND tell the
|
|
1224
|
-
# operator once — never a silent stderr-only drop.
|
|
1255
|
+
# operator once — never a silent stderr-only drop. Terminal → drop the
|
|
1256
|
+
# carrier + counter (a corrupt token is never worth retrying).
|
|
1257
|
+
rm -f "{{agentDir}}/.session-model" "{{agentDir}}/.session-model-boot-attempts"
|
|
1225
1258
|
echo "session-model: ignoring malformed .session-model (failed shape gate) — using configured default '$_EFFECTIVE_MODEL'" >&2
|
|
1226
1259
|
printf 'Your saved session model override could not be read (invalid or corrupt), so the agent booted on its configured default `%s`. Re-issue /model <name> if you want a different model.\n' "$_EFFECTIVE_MODEL" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1227
1260
|
elif [ "$_sm_cfg" != "$_EFFECTIVE_MODEL" ]; then
|
|
1228
1261
|
# switchroom.yaml `model:` changed between the switch and this apply-boot →
|
|
1229
1262
|
# the carrier is against a default that no longer exists. Invalidate +
|
|
1230
|
-
# announce once.
|
|
1263
|
+
# announce once. Terminal → drop the carrier + counter.
|
|
1264
|
+
rm -f "{{agentDir}}/.session-model" "{{agentDir}}/.session-model-boot-attempts"
|
|
1231
1265
|
echo "session-model: configured default changed ('$_sm_cfg' → '$_EFFECTIVE_MODEL') — dropping session override '$_sm_model'" >&2
|
|
1232
1266
|
printf 'The configured default model changed (`%s` → `%s`), so your session override to `%s` was not applied — 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
|
|
1233
|
-
elif [ "${_sm_model#sr-}" != "$_sm_model" ] && [ -z "$_LITELLM_OK" ]; then
|
|
1234
|
-
#
|
|
1235
|
-
# 4xx against Anthropic
|
|
1236
|
-
#
|
|
1237
|
-
#
|
|
1238
|
-
|
|
1267
|
+
elif { [ "${_sm_model#sr-}" != "$_sm_model" ] || [ "$_sm_model" = fable ] || [ "$_sm_model" = claude-fable-5 ]; } && [ -z "$_LITELLM_OK" ]; then
|
|
1268
|
+
# Proxy-only override + LiteLLM unreachable at the apply-boot: booting the
|
|
1269
|
+
# override would 4xx (sr-* against Anthropic; `fable`/`claude-fable-5` is a
|
|
1270
|
+
# retired codename direct-to-Anthropic and ONLY resolves via the LiteLLM
|
|
1271
|
+
# router — see the repoint case below). The litellm probe already waited out
|
|
1272
|
+
# its full 120s window before we got here, so "still unreachable" is a real
|
|
1273
|
+
# down-state, not the transient boot-lock wedge the bounded-retry counter
|
|
1274
|
+
# covers — drop the carrier + counter (terminal) and tell the operator to
|
|
1275
|
+
# re-issue once the proxy is back, rather than looping against a dead proxy.
|
|
1276
|
+
# (Rev 5: Fable is now reachable via the carrier relaunch — the old
|
|
1277
|
+
# live-inject path never launched it — so it needs the same down-guard as sr-*.)
|
|
1278
|
+
rm -f "{{agentDir}}/.session-model" "{{agentDir}}/.session-model-boot-attempts"
|
|
1279
|
+
echo "session-model: LiteLLM proxy unreachable at boot — NOT applying proxy-only override '$_sm_model'; booting configured default '$_EFFECTIVE_MODEL' (re-issue /model when the proxy is back)" >&2
|
|
1239
1280
|
printf 'LiteLLM proxy was unreachable at boot, so the session model override `%s` was not applied — the agent booted on its configured default `%s`. Re-issue /model %s once the proxy is reachable.\n' "$_sm_model" "$_EFFECTIVE_MODEL" "$_sm_model" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1281
|
+
elif [ "$_sm_attempts" -gt "$_SM_MAX_ATTEMPTS" ]; then
|
|
1282
|
+
# CRASHLOOP BOUND (#3284). This carrier has now been read on more than
|
|
1283
|
+
# _SM_MAX_ATTEMPTS boots WITHOUT the gateway ever reaching boot.lock_acquired
|
|
1284
|
+
# to clear it — i.e. every apply-boot wedged/crashed before a healthy
|
|
1285
|
+
# gateway. That is the signature of a genuinely bad token (one that will
|
|
1286
|
+
# never boot), NOT the transient boot-lock race we retry for. Give up:
|
|
1287
|
+
# drop the carrier + counter, revert to the configured default, and alert.
|
|
1288
|
+
# This is the hard finite bound that guarantees no unbounded crashloop.
|
|
1289
|
+
rm -f "{{agentDir}}/.session-model" "{{agentDir}}/.session-model-boot-attempts"
|
|
1290
|
+
echo "session-model: override '$_sm_model' did not reach a healthy boot after $_SM_MAX_ATTEMPTS attempts — reverting to configured default '$_EFFECTIVE_MODEL'" >&2
|
|
1291
|
+
printf 'Your session model override `%s` could not boot successfully after %s attempts, so the agent reverted to its configured default `%s`. Re-issue /model %s to try again.\n' "$_sm_model" "$_SM_MAX_ATTEMPTS" "$_EFFECTIVE_MODEL" "$_sm_model" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1240
1292
|
else
|
|
1241
|
-
# Apply the carrier for THIS session
|
|
1242
|
-
#
|
|
1293
|
+
# Apply the carrier for THIS session and KEEP it: the gateway deletes the
|
|
1294
|
+
# carrier + counter once it acquires the boot lock (healthy boot), so a
|
|
1295
|
+
# transient wedge before that RE-APPLIES on the retry boot instead of
|
|
1296
|
+
# reverting. Persist the incremented attempt counter so the crashloop bound
|
|
1297
|
+
# above can fire if this token keeps wedging. No boot alert — the gateway
|
|
1298
|
+
# already acked the /model switch in chat, and the override reverts on the
|
|
1299
|
+
# next restart (once the gateway has cleared the carrier).
|
|
1300
|
+
printf '%s\n' "$_sm_attempts" > "{{agentDir}}/.session-model-boot-attempts" 2>/dev/null || true
|
|
1243
1301
|
_EFFECTIVE_MODEL="$_sm_model"
|
|
1244
|
-
echo "session-model: applying session override '$_sm_model' this boot (
|
|
1302
|
+
echo "session-model: applying session override '$_sm_model' this boot (attempt $_sm_attempts/$_SM_MAX_ATTEMPTS — cleared by the gateway on a healthy boot, reverts on next restart)" >&2
|
|
1245
1303
|
fi
|
|
1246
|
-
unset _smf _sm_model _sm_cfg
|
|
1304
|
+
unset _smf _sm_model _sm_cfg _sm_attempts _SM_MAX_ATTEMPTS
|
|
1247
1305
|
fi
|
|
1248
1306
|
|
|
1249
1307
|
# --- Session effort resolution (consume-once .session-effort carrier, #3186) ---
|
|
@@ -1312,11 +1370,24 @@ printf '%s\n' "$_EFFECTIVE_EFFORT" > "{{agentDir}}/.active-session-effort" 2>/de
|
|
|
1312
1370
|
# non-Claude default at the router root) and run degraded until litellm returns —
|
|
1313
1371
|
# there is NO Claude default to fall back to for a persistent sr-* agent, and
|
|
1314
1372
|
# that (loud-warned, non-fatal) degraded state is the correct behavior.
|
|
1373
|
+
#
|
|
1374
|
+
# Rev 5 (F2): `fable`/`claude-fable-5` ALSO needs the router root, not the
|
|
1375
|
+
# passthrough. The `fable` alias resolves in the CLI to `claude-fable-5`
|
|
1376
|
+
# (ANTHROPIC_DEFAULT_FABLE_MODEL above), a RETIRED codename that 4xxs
|
|
1377
|
+
# direct-to-Anthropic; it ONLY resolves via the LiteLLM router (maps
|
|
1378
|
+
# fable/claude-fable-5 -> anthropic/claude-fable-5, forwarding the OAuth). The
|
|
1379
|
+
# old Fable path was a live inject that silently no-op'd, so Fable never actually
|
|
1380
|
+
# launched via `--model`; routing the Fable button through the carrier relaunch
|
|
1381
|
+
# newly exposes this, so the repoint case must include it or a fable carrier
|
|
1382
|
+
# 4xxs even with LiteLLM UP. Verified against the live litellm config + router
|
|
1383
|
+
# /v1/models (both `fable` and `claude-fable-5` served). Other Claude sessions
|
|
1384
|
+
# (opus/sonnet/haiku/default) keep the passthrough — it dodges the Opus SSE
|
|
1385
|
+
# re-chunk stall; Fable trades that for being routable at all.
|
|
1315
1386
|
case "$_EFFECTIVE_MODEL" in
|
|
1316
|
-
sr
|
|
1387
|
+
sr-*|fable|claude-fable-5)
|
|
1317
1388
|
if [ -n "$_LITELLM_OK" ] && [ -n "$SWITCHROOM_LITELLM_BASE" ]; then
|
|
1318
1389
|
export ANTHROPIC_BASE_URL="$SWITCHROOM_LITELLM_BASE"
|
|
1319
|
-
echo "session-model: effective model '$_EFFECTIVE_MODEL' is sr
|
|
1390
|
+
echo "session-model: effective model '$_EFFECTIVE_MODEL' is proxy-only (sr-*/fable) — routing via LiteLLM model router '$SWITCHROOM_LITELLM_BASE' (off /anthropic passthrough)" >&2
|
|
1320
1391
|
fi
|
|
1321
1392
|
;;
|
|
1322
1393
|
esac
|
|
@@ -22970,6 +22970,65 @@ import {
|
|
|
22970
22970
|
import { homedir as homedir2 } from "os";
|
|
22971
22971
|
import { basename, join as join3 } from "path";
|
|
22972
22972
|
|
|
22973
|
+
// text-voice-scrub.ts
|
|
22974
|
+
var NULL = "\x00";
|
|
22975
|
+
var FENCE_PH = `${NULL}VS_FENCE`;
|
|
22976
|
+
var INLINE_PH = `${NULL}VS_INLINE`;
|
|
22977
|
+
var HTML_CODE_PH = `${NULL}VS_HTMLCODE`;
|
|
22978
|
+
var HTML_PRE_PH = `${NULL}VS_HTMLPRE`;
|
|
22979
|
+
var URL_PH = `${NULL}VS_URL`;
|
|
22980
|
+
|
|
22981
|
+
// model-unavailable.ts
|
|
22982
|
+
var transientUpstreamSignals = [
|
|
22983
|
+
"not your usage limit",
|
|
22984
|
+
"not your account",
|
|
22985
|
+
"not your account's",
|
|
22986
|
+
"temporarily limiting requests",
|
|
22987
|
+
"temporarily rate",
|
|
22988
|
+
"server is temporarily",
|
|
22989
|
+
"would exceed your account\u2019s rate limit",
|
|
22990
|
+
"would exceed your account's rate limit"
|
|
22991
|
+
];
|
|
22992
|
+
function isTransientUpstreamSignal(text) {
|
|
22993
|
+
if (typeof text !== "string" || text.length === 0)
|
|
22994
|
+
return false;
|
|
22995
|
+
const sample = text.length > 16384 ? text.slice(0, 16384) : text;
|
|
22996
|
+
const lower = sample.toLowerCase();
|
|
22997
|
+
return transientUpstreamSignals.some((s) => lower.includes(s));
|
|
22998
|
+
}
|
|
22999
|
+
var litellmProxyLocal429Signals = [
|
|
23000
|
+
"deployment over user-defined ratelimit",
|
|
23001
|
+
"model rate limit exceeded. tpm limit",
|
|
23002
|
+
"model rate limit exceeded. rpm limit",
|
|
23003
|
+
"deployment over defined rpm limit",
|
|
23004
|
+
"no deployments available for selected model",
|
|
23005
|
+
"litellm rate limit handler",
|
|
23006
|
+
"crossed tpm / rpm",
|
|
23007
|
+
"max parallel request limit reached"
|
|
23008
|
+
];
|
|
23009
|
+
var litellmV3LimiterSignalPair = ["rate limit exceeded for ", "limit type:"];
|
|
23010
|
+
function isLitellmProxyLocal429(text) {
|
|
23011
|
+
if (typeof text !== "string" || text.length === 0)
|
|
23012
|
+
return false;
|
|
23013
|
+
const sample = text.length > 16384 ? text.slice(0, 16384) : text;
|
|
23014
|
+
const lower = sample.toLowerCase();
|
|
23015
|
+
if (litellmProxyLocal429Signals.some((s) => lower.includes(s)))
|
|
23016
|
+
return true;
|
|
23017
|
+
return litellmV3LimiterSignalPair.every((s) => lower.includes(s));
|
|
23018
|
+
}
|
|
23019
|
+
function isLitellmProxyAuthMisconfig(text) {
|
|
23020
|
+
if (typeof text !== "string" || text.length === 0)
|
|
23021
|
+
return false;
|
|
23022
|
+
const sample = text.length > 16384 ? text.slice(0, 16384) : text;
|
|
23023
|
+
const lower = sample.toLowerCase();
|
|
23024
|
+
if (lower.includes("x-api-key header is required"))
|
|
23025
|
+
return true;
|
|
23026
|
+
const isAuthErr = lower.includes("authentication_error") || lower.includes("authenticationerror");
|
|
23027
|
+
if (!isAuthErr)
|
|
23028
|
+
return false;
|
|
23029
|
+
return lower.includes("fallback") && lower.includes("x-api-key");
|
|
23030
|
+
}
|
|
23031
|
+
|
|
22973
23032
|
// operator-events.ts
|
|
22974
23033
|
function classifyClaudeError(raw) {
|
|
22975
23034
|
try {
|
|
@@ -22987,6 +23046,12 @@ function classifyInner(raw) {
|
|
|
22987
23046
|
const message = extractString(obj, "message") ?? extractString(getNestedObj(obj, "error"), "message") ?? (typeof raw === "string" ? raw : "") ?? "";
|
|
22988
23047
|
const status = extractNumber(obj, "status") ?? extractNumber(obj, "statusCode") ?? extractNumber(obj, "status_code") ?? null;
|
|
22989
23048
|
const sdkCode = extractString(obj, "error_code") ?? "";
|
|
23049
|
+
if (isLitellmProxyAuthMisconfig(`${errorType}
|
|
23050
|
+
${errorCode}
|
|
23051
|
+
${sdkCode}
|
|
23052
|
+
${message}`)) {
|
|
23053
|
+
return "proxy-misconfig";
|
|
23054
|
+
}
|
|
22990
23055
|
if (errorType === "authentication_error" || errorCode === "authentication_error" || sdkCode === "authentication_error" || message.toLowerCase().includes("authentication_error")) {
|
|
22991
23056
|
const msg = message.toLowerCase();
|
|
22992
23057
|
if (msg.includes("expired") || msg.includes("refresh")) {
|
|
@@ -23034,53 +23099,12 @@ function getNestedObj(obj, key) {
|
|
|
23034
23099
|
}
|
|
23035
23100
|
var DEFAULT_OPERATOR_EVENT_COOLDOWN_MS = 5 * 60000;
|
|
23036
23101
|
var cooldownMap = new Map;
|
|
23037
|
-
|
|
23038
|
-
|
|
23039
|
-
|
|
23040
|
-
|
|
23041
|
-
|
|
23042
|
-
|
|
23043
|
-
var HTML_PRE_PH = `${NULL}VS_HTMLPRE`;
|
|
23044
|
-
var URL_PH = `${NULL}VS_URL`;
|
|
23045
|
-
|
|
23046
|
-
// model-unavailable.ts
|
|
23047
|
-
var transientUpstreamSignals = [
|
|
23048
|
-
"not your usage limit",
|
|
23049
|
-
"not your account",
|
|
23050
|
-
"not your account's",
|
|
23051
|
-
"temporarily limiting requests",
|
|
23052
|
-
"temporarily rate",
|
|
23053
|
-
"server is temporarily",
|
|
23054
|
-
"would exceed your account\u2019s rate limit",
|
|
23055
|
-
"would exceed your account's rate limit"
|
|
23056
|
-
];
|
|
23057
|
-
function isTransientUpstreamSignal(text) {
|
|
23058
|
-
if (typeof text !== "string" || text.length === 0)
|
|
23059
|
-
return false;
|
|
23060
|
-
const sample = text.length > 16384 ? text.slice(0, 16384) : text;
|
|
23061
|
-
const lower = sample.toLowerCase();
|
|
23062
|
-
return transientUpstreamSignals.some((s) => lower.includes(s));
|
|
23063
|
-
}
|
|
23064
|
-
var litellmProxyLocal429Signals = [
|
|
23065
|
-
"deployment over user-defined ratelimit",
|
|
23066
|
-
"model rate limit exceeded. tpm limit",
|
|
23067
|
-
"model rate limit exceeded. rpm limit",
|
|
23068
|
-
"deployment over defined rpm limit",
|
|
23069
|
-
"no deployments available for selected model",
|
|
23070
|
-
"litellm rate limit handler",
|
|
23071
|
-
"crossed tpm / rpm",
|
|
23072
|
-
"max parallel request limit reached"
|
|
23073
|
-
];
|
|
23074
|
-
var litellmV3LimiterSignalPair = ["rate limit exceeded for ", "limit type:"];
|
|
23075
|
-
function isLitellmProxyLocal429(text) {
|
|
23076
|
-
if (typeof text !== "string" || text.length === 0)
|
|
23077
|
-
return false;
|
|
23078
|
-
const sample = text.length > 16384 ? text.slice(0, 16384) : text;
|
|
23079
|
-
const lower = sample.toLowerCase();
|
|
23080
|
-
if (litellmProxyLocal429Signals.some((s) => lower.includes(s)))
|
|
23081
|
-
return true;
|
|
23082
|
-
return litellmV3LimiterSignalPair.every((s) => lower.includes(s));
|
|
23083
|
-
}
|
|
23102
|
+
var OPERATOR_ACTIONABLE_KINDS = new Set([
|
|
23103
|
+
"credentials-expired",
|
|
23104
|
+
"credentials-invalid",
|
|
23105
|
+
"credit-exhausted",
|
|
23106
|
+
"proxy-misconfig"
|
|
23107
|
+
]);
|
|
23084
23108
|
|
|
23085
23109
|
// tool-label-sidecar.ts
|
|
23086
23110
|
import { existsSync as existsSync2, readFileSync, statSync as statSync2 } from "node:fs";
|