switchroom 0.18.29 → 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.
Files changed (38) hide show
  1. package/bin/handoff-briefing.sh +8 -2
  2. package/dist/agent-scheduler/index.js +111 -7
  3. package/dist/auth-broker/index.js +154 -16
  4. package/dist/cli/autoaccept-poll.js +8 -3
  5. package/dist/cli/drive-write-pretool.mjs +8 -3
  6. package/dist/cli/ms-365-write-pretool.mjs +158 -11
  7. package/dist/cli/notion-write-pretool.mjs +103 -4
  8. package/dist/cli/switchroom.js +2074 -1585
  9. package/dist/host-control/main.js +110 -13
  10. package/dist/vault/approvals/kernel-server.js +116 -13
  11. package/dist/vault/broker/server.js +314 -145
  12. package/package.json +3 -3
  13. package/profiles/_base/start.sh.hbs +73 -20
  14. package/telegram-plugin/dist/bridge/bridge.js +71 -47
  15. package/telegram-plugin/dist/gateway/gateway.js +560 -96
  16. package/telegram-plugin/dist/server.js +89 -64
  17. package/telegram-plugin/gateway/gateway.ts +212 -17
  18. package/telegram-plugin/gateway/model-command.ts +104 -0
  19. package/telegram-plugin/gateway/session-model-file.ts +40 -0
  20. package/telegram-plugin/gateway/unhandled-message.ts +177 -0
  21. package/telegram-plugin/llm-error-present.ts +24 -0
  22. package/telegram-plugin/model-unavailable.ts +55 -0
  23. package/telegram-plugin/operator-events.ts +113 -0
  24. package/telegram-plugin/pending-user-notice.ts +88 -0
  25. package/telegram-plugin/shared/local-time.ts +43 -0
  26. package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +103 -0
  27. package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +264 -0
  28. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +26 -2
  29. package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +278 -0
  30. package/telegram-plugin/tests/local-time.test.ts +68 -1
  31. package/telegram-plugin/tests/model-command.test.ts +133 -0
  32. package/telegram-plugin/tests/session-model-file.test.ts +23 -0
  33. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +399 -2
  34. package/vendor/hindsight-memory/scripts/lib/client.py +47 -0
  35. package/vendor/hindsight-memory/scripts/lib/content.py +53 -1
  36. package/vendor/hindsight-memory/scripts/lib/turnlog.py +450 -0
  37. package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +467 -0
  38. package/vendor/hindsight-memory/tests/test_content.py +35 -0
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.29",
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",
@@ -1182,10 +1182,17 @@ _EFFECTIVE_MODEL={{{modelQ}}}
1182
1182
  printf '%s\n' "$_EFFECTIVE_MODEL" > "{{agentDir}}/.configured-default-model" 2>/dev/null || true
1183
1183
 
1184
1184
  # Rev-4 hygiene (#3184 review LOW-2): remove the files retired with the
1185
- # keep/revert intent subsystem and the crashloop self-heal. Nothing reads them
1186
- # anymore; without this rm they would linger in the bind-mounted state dir
1187
- # forever after rollout. Idempotent — a cheap no-op once clean.
1188
- rm -f "{{agentDir}}/.relaunch-model-intent" "{{agentDir}}/.session-model-boot-attempts" "{{agentDir}}/.session-model-kept-notified"
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"
1189
1196
 
1190
1197
  # Migration shim (one release): a leftover one-shot `.session-model-override`
1191
1198
  # carrier from a pre-consume-once gateway means an OLD gateway wrote it
@@ -1207,11 +1214,34 @@ if [ -f "{{agentDir}}/.session-model" ]; then
1207
1214
  _smf="$(cat "{{agentDir}}/.session-model" 2>/dev/null || true)"
1208
1215
  _sm_model="$(printf '%s' "$_smf" | sed -n 's/.*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
1209
1216
  _sm_cfg="$(printf '%s' "$_smf" | sed -n 's/.*"configuredDefaultAtWrite"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
1210
- # CONSUME-ONCE: delete the carrier now, before any apply/exec. The carrier
1211
- # governs exactly this one boot; a bad token can therefore crash at most one
1212
- # boot (the next boot has no carrier and reverts), so no crashloop self-heal
1213
- # is needed. Every branch below has already removed the file.
1214
- rm -f "{{agentDir}}/.session-model"
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 ))
1215
1245
  # Shape gate — kept BYTE-IDENTICAL with MODEL_ARG_RE in
1216
1246
  # telegram-plugin/gateway/model-command.ts. `/` is allowed for
1217
1247
  # OpenRouter-style `sr-vendor/model` ids; it is not a shell metachar inside
@@ -1222,33 +1252,56 @@ if [ -f "{{agentDir}}/.session-model" ]; then
1222
1252
  # `claude --model` (parity with parseSessionModel's single-string check).
1223
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
1224
1254
  # Invalid carrier: fall back to the configured default AND tell the
1225
- # 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"
1226
1258
  echo "session-model: ignoring malformed .session-model (failed shape gate) — using configured default '$_EFFECTIVE_MODEL'" >&2
1227
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
1228
1260
  elif [ "$_sm_cfg" != "$_EFFECTIVE_MODEL" ]; then
1229
1261
  # switchroom.yaml `model:` changed between the switch and this apply-boot →
1230
1262
  # the carrier is against a default that no longer exists. Invalidate +
1231
- # announce once.
1263
+ # announce once. Terminal → drop the carrier + counter.
1264
+ rm -f "{{agentDir}}/.session-model" "{{agentDir}}/.session-model-boot-attempts"
1232
1265
  echo "session-model: configured default changed ('$_sm_cfg' → '$_EFFECTIVE_MODEL') — dropping session override '$_sm_model'" >&2
1233
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
1234
1267
  elif { [ "${_sm_model#sr-}" != "$_sm_model" ] || [ "$_sm_model" = fable ] || [ "$_sm_model" = claude-fable-5 ]; } && [ -z "$_LITELLM_OK" ]; then
1235
1268
  # Proxy-only override + LiteLLM unreachable at the apply-boot: booting the
1236
1269
  # override would 4xx (sr-* against Anthropic; `fable`/`claude-fable-5` is a
1237
1270
  # retired codename direct-to-Anthropic and ONLY resolves via the LiteLLM
1238
- # router — see the repoint case below). Consume-once means we can't retain it
1239
- # for a later relaunch, so boot the configured default and tell the operator
1240
- # to re-issue once the proxy is back. (Rev 5: Fable is now reachable via the
1241
- # carrier relaunch — the old live-inject path never launched it so it needs
1242
- # the same down-guard as sr-*.)
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
+ # coversdrop 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"
1243
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
1244
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
1245
1292
  else
1246
- # Apply the carrier for THIS session. No boot alert the gateway already
1247
- # acked the /model switch in chat, and the override reverts on next restart.
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
1248
1301
  _EFFECTIVE_MODEL="$_sm_model"
1249
- echo "session-model: applying session override '$_sm_model' this boot (consume-once — reverts on next restart)" >&2
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
1250
1303
  fi
1251
- unset _smf _sm_model _sm_cfg
1304
+ unset _smf _sm_model _sm_cfg _sm_attempts _SM_MAX_ATTEMPTS
1252
1305
  fi
1253
1306
 
1254
1307
  # --- Session effort resolution (consume-once .session-effort carrier, #3186) ---
@@ -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
- // text-voice-scrub.ts
23039
- var NULL = "\x00";
23040
- var FENCE_PH = `${NULL}VS_FENCE`;
23041
- var INLINE_PH = `${NULL}VS_INLINE`;
23042
- var HTML_CODE_PH = `${NULL}VS_HTMLCODE`;
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";