switchroom 0.18.15 → 0.18.18
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/agent-scheduler/index.js +16 -0
- package/dist/auth-broker/index.js +445 -10
- package/dist/cli/notion-write-pretool.mjs +16 -0
- package/dist/cli/switchroom.js +654 -479
- package/dist/host-control/main.js +20 -1
- package/dist/vault/approvals/kernel-server.js +16 -0
- package/dist/vault/broker/server.js +16 -0
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +81 -139
- package/telegram-plugin/bridge/bridge.ts +7 -1
- package/telegram-plugin/dist/bridge/bridge.js +26 -1
- package/telegram-plugin/dist/gateway/gateway.js +1758 -661
- package/telegram-plugin/dist/server.js +26 -1
- package/telegram-plugin/draft-stream.ts +78 -3
- package/telegram-plugin/fleet-fallback-resume.ts +26 -3
- package/telegram-plugin/gateway/approval-hold.ts +49 -0
- package/telegram-plugin/gateway/bridge-dead-watchdog.ts +64 -22
- package/telegram-plugin/gateway/effort-command.ts +9 -7
- package/telegram-plugin/gateway/gateway.ts +627 -291
- package/telegram-plugin/gateway/linear-activity.ts +20 -4
- package/telegram-plugin/gateway/litellm-local-notice-wiring.ts +200 -0
- package/telegram-plugin/gateway/model-command.ts +96 -18
- package/telegram-plugin/gateway/pending-session-command.ts +10 -8
- package/telegram-plugin/gateway/premium-recovery-wiring.ts +122 -0
- package/telegram-plugin/gateway/session-model-file.ts +141 -172
- package/telegram-plugin/gateway/tier-downgrade-wiring.ts +121 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +14 -1
- package/telegram-plugin/litellm-local-notice.ts +189 -0
- package/telegram-plugin/llm-error-present.ts +436 -0
- package/telegram-plugin/operator-events.ts +7 -1
- package/telegram-plugin/permission-title.ts +172 -10
- package/telegram-plugin/premium-recovery.ts +101 -0
- package/telegram-plugin/quota-watch.ts +16 -4
- package/telegram-plugin/raw-error-scrub.ts +73 -0
- package/telegram-plugin/retry-api-call.ts +8 -2
- package/telegram-plugin/runtime-metrics.ts +16 -0
- package/telegram-plugin/send-gate-degraded.test.ts +161 -8
- package/telegram-plugin/send-gate-observability.test.ts +140 -0
- package/telegram-plugin/send-gate-observability.ts +65 -20
- package/telegram-plugin/send-gate.test.ts +143 -1
- package/telegram-plugin/send-gate.ts +246 -23
- package/telegram-plugin/session-tail.ts +16 -0
- package/telegram-plugin/shared/local-time.ts +69 -0
- package/telegram-plugin/stream-controller.ts +143 -20
- package/telegram-plugin/stream-reply-handler.ts +12 -2
- package/telegram-plugin/tests/approval-hold-harness.ts +6 -6
- package/telegram-plugin/tests/approval-hold-outcome.test.ts +10 -2
- package/telegram-plugin/tests/bot-api.harness.ts +7 -2
- package/telegram-plugin/tests/bridge-dead-watchdog.test.ts +61 -0
- package/telegram-plugin/tests/draft-stream.test.ts +110 -1
- package/telegram-plugin/tests/effort-command.test.ts +4 -4
- package/telegram-plugin/tests/fleet-fallback-resume.test.ts +39 -0
- package/telegram-plugin/tests/flood-windows-persistence.test.ts +5 -4
- package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +33 -19
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +47 -127
- package/telegram-plugin/tests/linear-create-issue.test.ts +30 -2
- package/telegram-plugin/tests/litellm-local-notice.test.ts +417 -0
- package/telegram-plugin/tests/llm-error-present.test.ts +380 -0
- package/telegram-plugin/tests/model-command.test.ts +84 -1
- package/telegram-plugin/tests/permission-title.test.ts +167 -4
- package/telegram-plugin/tests/premium-recovery-wiring.test.ts +150 -0
- package/telegram-plugin/tests/premium-recovery.test.ts +165 -0
- package/telegram-plugin/tests/quota-watch.test.ts +21 -0
- package/telegram-plugin/tests/reaction-gate-routing.test.ts +8 -3
- package/telegram-plugin/tests/retry-api-call.test.ts +21 -0
- package/telegram-plugin/tests/session-model-file.test.ts +7 -155
- package/telegram-plugin/tests/stream-controller-send-gate.test.ts +521 -0
- package/telegram-plugin/tests/stream-reply-handler.test.ts +44 -0
- package/telegram-plugin/tests/tier-downgrade-wiring.test.ts +165 -0
- package/telegram-plugin/tests/tier-downgrade.test.ts +141 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +27 -1
- package/telegram-plugin/tests/worker-activity-feed.test.ts +212 -2
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +492 -0
- package/telegram-plugin/tier-downgrade.ts +198 -0
- package/telegram-plugin/tool-activity-summary.ts +99 -0
- package/telegram-plugin/worker-activity-feed.ts +543 -368
|
@@ -18836,6 +18836,19 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
18836
18836
|
approval_timeout_minutes: exports_external.number().int().nonnegative().optional().describe("Operator approval-card lifetime (minutes) for the tool-use 'Allow " + "once' card and the vault grant decision wait. After this long with " + "no operator tap, the card auto-denies (a TIMEOUT, not a denial — the " + "agent is told not to retry). Default 60. hostd-gated verbs " + "(mcp__hostd__*) keep their own longer window; the hostd " + "config-propose card is not governed by this key."),
|
|
18837
18837
|
sub_agent_tick_interval_ms: exports_external.number().int().nonnegative().optional().describe("Heartbeat tick interval (ms) for sub-agent rendering. Forces a " + "re-render of the elapsed-time counter while sub-agents are running, " + "even during silent stretches between tool calls. Default 10000 (10 s). " + "Set to 0 to disable the elapsed-ticker path."),
|
|
18838
18838
|
edit_budget_threshold: exports_external.number().int().nonnegative().optional().describe("Telegram API edit budget per minute before the progress-card driver " + "falls back to a slower coalesce window. When a chat accumulates more " + "than this many card edits in the trailing 60 s, the driver switches " + "to a wider coalesce interval until the rate drops back. Default 18. " + "Increase if your gateway frequently bumps the Telegram edit-rate ceiling " + "with many parallel sub-agents; decrease for a more conservative buffer."),
|
|
18839
|
+
send_gate: exports_external.object({
|
|
18840
|
+
enabled: exports_external.boolean().optional().describe("Master switch for the deterministic outbound send gate " + "(telegram-plugin/send-gate.ts) — the token-bucket scheduler every " + "Bot API call transits so per-surface throttles can't add up past a " + "flood ceiling. ON by default. Precedence: the operator break-glass " + "env var SWITCHROOM_TELEGRAM_SEND_GATE (0/false/off/no) ALWAYS wins " + "when explicitly set; this key only decides when that env var is " + "unset. Omit to keep the gate on."),
|
|
18841
|
+
global_per_sec: exports_external.number().positive().optional().describe("Global bucket sustained rate (Bot API calls/sec across ALL chats). " + "Default 25 (headroom under Telegram's ~30/s). Must be > 0 — a zero " + "or negative rate would wedge all outbound sends. Omit to keep 25."),
|
|
18842
|
+
global_burst: exports_external.number().int().positive().describe("Global bucket burst capacity. Default 4; worst-case 1s window " + "admits global_burst + global_per_sec = 29 < 30, a real margin under " + "the ceiling. Must be an integer >= 1 (a 0 capacity never admits a " + "token and wedges sends). Omit to keep 4.").optional(),
|
|
18843
|
+
per_chat_per_sec: exports_external.number().positive().optional().describe("Per-chat sustained rate (calls/sec to a single chat). Default 1. " + "Must be > 0 (zero/negative wedges that chat). Omit to keep 1."),
|
|
18844
|
+
per_chat_burst: exports_external.number().int().positive().optional().describe("Per-chat burst capacity. Default 3. Must be an integer >= 1 " + "(0 wedges the chat's bucket). Omit to keep 3."),
|
|
18845
|
+
per_group_per_min: exports_external.number().positive().optional().describe("Per-group sustained rate (calls/min to a single group/supergroup). " + "Default 18 (headroom under Telegram's ~20/min group ceiling). Must " + "be > 0. Omit to keep 18."),
|
|
18846
|
+
per_group_burst: exports_external.number().int().positive().optional().describe("Per-group burst capacity. Default 2. Must be an integer >= 1 " + "(0 wedges the group's bucket). Omit to keep 2."),
|
|
18847
|
+
edit_floor_ms: exports_external.number().int().nonnegative().optional().describe("Minimum ms between successive edits of the SAME message_id " + "(last-write-wins coalescing enforces this floor). Default 1500 " + "(Telegram's ~1 edit/sec/message practical ceiling). 0 disables the " + "floor. Must be an integer >= 0. Omit to keep 1500.")
|
|
18848
|
+
}).optional().describe("Tunable rate limits for the deterministic outbound send gate " + "(telegram-plugin/send-gate.ts). Every key is optional and defaults to " + "the send gate's built-in value, so omitting the whole block reproduces " + "today's exact behaviour — this is pure operator tuning, no default is " + "changed. Cascades from defaults.channels.telegram.send_gate."),
|
|
18849
|
+
worker_feed: exports_external.object({
|
|
18850
|
+
max_rows: exports_external.number().int().positive().optional().describe("Max live-worker rows rendered in the COMBINED worker-activity feed " + "(2+ background workers in one chat/thread coalesce into ONE message; " + "telegram-plugin/worker-activity-feed.ts) before a compact " + "'+M more working…' spill line — keeps the coalesced body compact and " + "legible (and under the rich-message wire ceiling). Default 8. A " + "single-worker chat renders the full \uD83D\uDEE0 Worker card and ignores this. " + "Must be an integer >= 1. Omit to keep 8.")
|
|
18851
|
+
}).optional().describe("Tuning for the coalesced worker-activity feed " + "(telegram-plugin/worker-activity-feed.ts). Cascades from " + "defaults.channels.telegram.worker_feed."),
|
|
18839
18852
|
stickers: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Sticker aliases for the `send_sticker` MCP tool (#576). Maps a " + "short alias name (e.g. 'happy', 'thinking') to a Telegram file_id. " + "Operator-curated — capture file_ids from inbound stickers the user " + "sends and add them here. The agent calls send_sticker(chat_id, " + "alias='happy') and the gateway resolves to the file_id at send " + "time. Aliases enable persona-flavored expressiveness without " + "exposing raw file_ids in the agent prompt. Personal-assistant / " + "health-coach personas benefit; coding agents typically don't " + "configure any."),
|
|
18840
18853
|
voice_in: exports_external.object({
|
|
18841
18854
|
enabled: exports_external.boolean().optional().describe("Master switch for voice-message transcription."),
|
|
@@ -18861,6 +18874,9 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
18861
18874
|
window_ms: exports_external.number().int().nonnegative().optional().describe("Sliding-window (ms) for merging consecutive inbound messages from " + "the same sender+topic into ONE Claude turn. Each new message resets " + "the timer; the turn starts once the sender pauses for this long. " + "Catches forwarded bursts, pasted text the Telegram client split " + "into several messages, and mixed text+media forwards. Default 500. " + "Set 0 to disable (every message becomes its own turn). Raise for " + "users who think in multiple short messages; the trade-off is the " + "single-message turn start is delayed by this much (the \uD83D\uDC40 ack still " + "fires immediately, so perceived latency is unchanged)."),
|
|
18862
18875
|
max_attachments: exports_external.number().int().positive().optional().describe("Maximum number of media attachments carried into ONE coalesced " + "Claude turn. Default 10 — a full Telegram album (media_group caps " + "at 10) or a text+multi-image forwarded burst arrives as a single " + "turn; the agent sees numbered attachment fields (image_path, " + "image_path_2, …). Set 1 to restore the historical " + "single-attachment-per-turn behaviour. Excess attachments beyond " + "the cap spill into the next turn. Each attachment is downloaded, " + "so a high cap on a slow link delays turn start.")
|
|
18863
18876
|
}).optional().describe("Inbound coalescing — how the gateway groups rapid consecutive messages " + "into a single turn so a forwarded album or split paste doesn't fan out " + "into N separate turns. Cascades from defaults.channels.telegram.coalesce."),
|
|
18877
|
+
litellm_notice: exports_external.object({
|
|
18878
|
+
window_ms: exports_external.number().int().positive().optional().describe("Per-agent cooldown window (ms) for the litellm-local 429 notice. " + "When the agent trips the LiteLLM proxy's OWN tpm_limit/rpm_limit " + "cap (a `litellm-local` classified 429 — see docs/auth.md § " + "LiteLLM-proxy-local 429s), the gateway posts ONE calm notice " + "naming the fleet token limiter, then counts further hits silently " + "for this long; the first notice after the window expires says " + "how many were absorbed. Default 900000 (15 min). Invalid values " + "fall back to the default.")
|
|
18879
|
+
}).optional().describe("Debounce tuning for the litellm-local throttle notice — the calm " + "'fleet token limiter engaged' message posted when the LiteLLM " + "proxy's own rate cap trips (never an Anthropic account limit). " + "Cascades from defaults.channels.telegram.litellm_notice."),
|
|
18864
18880
|
interrupt: exports_external.object({
|
|
18865
18881
|
safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
|
|
18866
18882
|
max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short — the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
|
|
@@ -26589,7 +26605,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
|
|
|
26589
26605
|
import { dirname as dirname4, join as join7 } from "node:path";
|
|
26590
26606
|
|
|
26591
26607
|
// src/build-info.ts
|
|
26592
|
-
var VERSION = "0.18.
|
|
26608
|
+
var VERSION = "0.18.18";
|
|
26593
26609
|
|
|
26594
26610
|
// src/cli/resolve-version.ts
|
|
26595
26611
|
function readPackageVersion() {
|
|
@@ -29102,6 +29118,9 @@ ${output.recovery.stderr}` : "";
|
|
|
29102
29118
|
return release;
|
|
29103
29119
|
}
|
|
29104
29120
|
async handleAgentSmoke(req, started) {
|
|
29121
|
+
const rejected = this.rejectUnconfiguredTarget(req.args.name, req.request_id, "agent_smoke", started);
|
|
29122
|
+
if (rejected)
|
|
29123
|
+
return rejected;
|
|
29105
29124
|
const container = `switchroom-${req.args.name}`;
|
|
29106
29125
|
const respond = (containerState, probes2) => ({
|
|
29107
29126
|
v: 1,
|
|
@@ -4256,6 +4256,19 @@ var init_schema = __esm(() => {
|
|
|
4256
4256
|
approval_timeout_minutes: exports_external.number().int().nonnegative().optional().describe("Operator approval-card lifetime (minutes) for the tool-use 'Allow " + "once' card and the vault grant decision wait. After this long with " + "no operator tap, the card auto-denies (a TIMEOUT, not a denial — the " + "agent is told not to retry). Default 60. hostd-gated verbs " + "(mcp__hostd__*) keep their own longer window; the hostd " + "config-propose card is not governed by this key."),
|
|
4257
4257
|
sub_agent_tick_interval_ms: exports_external.number().int().nonnegative().optional().describe("Heartbeat tick interval (ms) for sub-agent rendering. Forces a " + "re-render of the elapsed-time counter while sub-agents are running, " + "even during silent stretches between tool calls. Default 10000 (10 s). " + "Set to 0 to disable the elapsed-ticker path."),
|
|
4258
4258
|
edit_budget_threshold: exports_external.number().int().nonnegative().optional().describe("Telegram API edit budget per minute before the progress-card driver " + "falls back to a slower coalesce window. When a chat accumulates more " + "than this many card edits in the trailing 60 s, the driver switches " + "to a wider coalesce interval until the rate drops back. Default 18. " + "Increase if your gateway frequently bumps the Telegram edit-rate ceiling " + "with many parallel sub-agents; decrease for a more conservative buffer."),
|
|
4259
|
+
send_gate: exports_external.object({
|
|
4260
|
+
enabled: exports_external.boolean().optional().describe("Master switch for the deterministic outbound send gate " + "(telegram-plugin/send-gate.ts) — the token-bucket scheduler every " + "Bot API call transits so per-surface throttles can't add up past a " + "flood ceiling. ON by default. Precedence: the operator break-glass " + "env var SWITCHROOM_TELEGRAM_SEND_GATE (0/false/off/no) ALWAYS wins " + "when explicitly set; this key only decides when that env var is " + "unset. Omit to keep the gate on."),
|
|
4261
|
+
global_per_sec: exports_external.number().positive().optional().describe("Global bucket sustained rate (Bot API calls/sec across ALL chats). " + "Default 25 (headroom under Telegram's ~30/s). Must be > 0 — a zero " + "or negative rate would wedge all outbound sends. Omit to keep 25."),
|
|
4262
|
+
global_burst: exports_external.number().int().positive().describe("Global bucket burst capacity. Default 4; worst-case 1s window " + "admits global_burst + global_per_sec = 29 < 30, a real margin under " + "the ceiling. Must be an integer >= 1 (a 0 capacity never admits a " + "token and wedges sends). Omit to keep 4.").optional(),
|
|
4263
|
+
per_chat_per_sec: exports_external.number().positive().optional().describe("Per-chat sustained rate (calls/sec to a single chat). Default 1. " + "Must be > 0 (zero/negative wedges that chat). Omit to keep 1."),
|
|
4264
|
+
per_chat_burst: exports_external.number().int().positive().optional().describe("Per-chat burst capacity. Default 3. Must be an integer >= 1 " + "(0 wedges the chat's bucket). Omit to keep 3."),
|
|
4265
|
+
per_group_per_min: exports_external.number().positive().optional().describe("Per-group sustained rate (calls/min to a single group/supergroup). " + "Default 18 (headroom under Telegram's ~20/min group ceiling). Must " + "be > 0. Omit to keep 18."),
|
|
4266
|
+
per_group_burst: exports_external.number().int().positive().optional().describe("Per-group burst capacity. Default 2. Must be an integer >= 1 " + "(0 wedges the group's bucket). Omit to keep 2."),
|
|
4267
|
+
edit_floor_ms: exports_external.number().int().nonnegative().optional().describe("Minimum ms between successive edits of the SAME message_id " + "(last-write-wins coalescing enforces this floor). Default 1500 " + "(Telegram's ~1 edit/sec/message practical ceiling). 0 disables the " + "floor. Must be an integer >= 0. Omit to keep 1500.")
|
|
4268
|
+
}).optional().describe("Tunable rate limits for the deterministic outbound send gate " + "(telegram-plugin/send-gate.ts). Every key is optional and defaults to " + "the send gate's built-in value, so omitting the whole block reproduces " + "today's exact behaviour — this is pure operator tuning, no default is " + "changed. Cascades from defaults.channels.telegram.send_gate."),
|
|
4269
|
+
worker_feed: exports_external.object({
|
|
4270
|
+
max_rows: exports_external.number().int().positive().optional().describe("Max live-worker rows rendered in the COMBINED worker-activity feed " + "(2+ background workers in one chat/thread coalesce into ONE message; " + "telegram-plugin/worker-activity-feed.ts) before a compact " + "'+M more working…' spill line — keeps the coalesced body compact and " + "legible (and under the rich-message wire ceiling). Default 8. A " + "single-worker chat renders the full \uD83D\uDEE0 Worker card and ignores this. " + "Must be an integer >= 1. Omit to keep 8.")
|
|
4271
|
+
}).optional().describe("Tuning for the coalesced worker-activity feed " + "(telegram-plugin/worker-activity-feed.ts). Cascades from " + "defaults.channels.telegram.worker_feed."),
|
|
4259
4272
|
stickers: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Sticker aliases for the `send_sticker` MCP tool (#576). Maps a " + "short alias name (e.g. 'happy', 'thinking') to a Telegram file_id. " + "Operator-curated — capture file_ids from inbound stickers the user " + "sends and add them here. The agent calls send_sticker(chat_id, " + "alias='happy') and the gateway resolves to the file_id at send " + "time. Aliases enable persona-flavored expressiveness without " + "exposing raw file_ids in the agent prompt. Personal-assistant / " + "health-coach personas benefit; coding agents typically don't " + "configure any."),
|
|
4260
4273
|
voice_in: exports_external.object({
|
|
4261
4274
|
enabled: exports_external.boolean().optional().describe("Master switch for voice-message transcription."),
|
|
@@ -4281,6 +4294,9 @@ var init_schema = __esm(() => {
|
|
|
4281
4294
|
window_ms: exports_external.number().int().nonnegative().optional().describe("Sliding-window (ms) for merging consecutive inbound messages from " + "the same sender+topic into ONE Claude turn. Each new message resets " + "the timer; the turn starts once the sender pauses for this long. " + "Catches forwarded bursts, pasted text the Telegram client split " + "into several messages, and mixed text+media forwards. Default 500. " + "Set 0 to disable (every message becomes its own turn). Raise for " + "users who think in multiple short messages; the trade-off is the " + "single-message turn start is delayed by this much (the \uD83D\uDC40 ack still " + "fires immediately, so perceived latency is unchanged)."),
|
|
4282
4295
|
max_attachments: exports_external.number().int().positive().optional().describe("Maximum number of media attachments carried into ONE coalesced " + "Claude turn. Default 10 — a full Telegram album (media_group caps " + "at 10) or a text+multi-image forwarded burst arrives as a single " + "turn; the agent sees numbered attachment fields (image_path, " + "image_path_2, …). Set 1 to restore the historical " + "single-attachment-per-turn behaviour. Excess attachments beyond " + "the cap spill into the next turn. Each attachment is downloaded, " + "so a high cap on a slow link delays turn start.")
|
|
4283
4296
|
}).optional().describe("Inbound coalescing — how the gateway groups rapid consecutive messages " + "into a single turn so a forwarded album or split paste doesn't fan out " + "into N separate turns. Cascades from defaults.channels.telegram.coalesce."),
|
|
4297
|
+
litellm_notice: exports_external.object({
|
|
4298
|
+
window_ms: exports_external.number().int().positive().optional().describe("Per-agent cooldown window (ms) for the litellm-local 429 notice. " + "When the agent trips the LiteLLM proxy's OWN tpm_limit/rpm_limit " + "cap (a `litellm-local` classified 429 — see docs/auth.md § " + "LiteLLM-proxy-local 429s), the gateway posts ONE calm notice " + "naming the fleet token limiter, then counts further hits silently " + "for this long; the first notice after the window expires says " + "how many were absorbed. Default 900000 (15 min). Invalid values " + "fall back to the default.")
|
|
4299
|
+
}).optional().describe("Debounce tuning for the litellm-local throttle notice — the calm " + "'fleet token limiter engaged' message posted when the LiteLLM " + "proxy's own rate cap trips (never an Anthropic account limit). " + "Cascades from defaults.channels.telegram.litellm_notice."),
|
|
4284
4300
|
interrupt: exports_external.object({
|
|
4285
4301
|
safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
|
|
4286
4302
|
max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short — the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
|
|
@@ -4256,6 +4256,19 @@ var init_schema = __esm(() => {
|
|
|
4256
4256
|
approval_timeout_minutes: exports_external.number().int().nonnegative().optional().describe("Operator approval-card lifetime (minutes) for the tool-use 'Allow " + "once' card and the vault grant decision wait. After this long with " + "no operator tap, the card auto-denies (a TIMEOUT, not a denial — the " + "agent is told not to retry). Default 60. hostd-gated verbs " + "(mcp__hostd__*) keep their own longer window; the hostd " + "config-propose card is not governed by this key."),
|
|
4257
4257
|
sub_agent_tick_interval_ms: exports_external.number().int().nonnegative().optional().describe("Heartbeat tick interval (ms) for sub-agent rendering. Forces a " + "re-render of the elapsed-time counter while sub-agents are running, " + "even during silent stretches between tool calls. Default 10000 (10 s). " + "Set to 0 to disable the elapsed-ticker path."),
|
|
4258
4258
|
edit_budget_threshold: exports_external.number().int().nonnegative().optional().describe("Telegram API edit budget per minute before the progress-card driver " + "falls back to a slower coalesce window. When a chat accumulates more " + "than this many card edits in the trailing 60 s, the driver switches " + "to a wider coalesce interval until the rate drops back. Default 18. " + "Increase if your gateway frequently bumps the Telegram edit-rate ceiling " + "with many parallel sub-agents; decrease for a more conservative buffer."),
|
|
4259
|
+
send_gate: exports_external.object({
|
|
4260
|
+
enabled: exports_external.boolean().optional().describe("Master switch for the deterministic outbound send gate " + "(telegram-plugin/send-gate.ts) — the token-bucket scheduler every " + "Bot API call transits so per-surface throttles can't add up past a " + "flood ceiling. ON by default. Precedence: the operator break-glass " + "env var SWITCHROOM_TELEGRAM_SEND_GATE (0/false/off/no) ALWAYS wins " + "when explicitly set; this key only decides when that env var is " + "unset. Omit to keep the gate on."),
|
|
4261
|
+
global_per_sec: exports_external.number().positive().optional().describe("Global bucket sustained rate (Bot API calls/sec across ALL chats). " + "Default 25 (headroom under Telegram's ~30/s). Must be > 0 — a zero " + "or negative rate would wedge all outbound sends. Omit to keep 25."),
|
|
4262
|
+
global_burst: exports_external.number().int().positive().describe("Global bucket burst capacity. Default 4; worst-case 1s window " + "admits global_burst + global_per_sec = 29 < 30, a real margin under " + "the ceiling. Must be an integer >= 1 (a 0 capacity never admits a " + "token and wedges sends). Omit to keep 4.").optional(),
|
|
4263
|
+
per_chat_per_sec: exports_external.number().positive().optional().describe("Per-chat sustained rate (calls/sec to a single chat). Default 1. " + "Must be > 0 (zero/negative wedges that chat). Omit to keep 1."),
|
|
4264
|
+
per_chat_burst: exports_external.number().int().positive().optional().describe("Per-chat burst capacity. Default 3. Must be an integer >= 1 " + "(0 wedges the chat's bucket). Omit to keep 3."),
|
|
4265
|
+
per_group_per_min: exports_external.number().positive().optional().describe("Per-group sustained rate (calls/min to a single group/supergroup). " + "Default 18 (headroom under Telegram's ~20/min group ceiling). Must " + "be > 0. Omit to keep 18."),
|
|
4266
|
+
per_group_burst: exports_external.number().int().positive().optional().describe("Per-group burst capacity. Default 2. Must be an integer >= 1 " + "(0 wedges the group's bucket). Omit to keep 2."),
|
|
4267
|
+
edit_floor_ms: exports_external.number().int().nonnegative().optional().describe("Minimum ms between successive edits of the SAME message_id " + "(last-write-wins coalescing enforces this floor). Default 1500 " + "(Telegram's ~1 edit/sec/message practical ceiling). 0 disables the " + "floor. Must be an integer >= 0. Omit to keep 1500.")
|
|
4268
|
+
}).optional().describe("Tunable rate limits for the deterministic outbound send gate " + "(telegram-plugin/send-gate.ts). Every key is optional and defaults to " + "the send gate's built-in value, so omitting the whole block reproduces " + "today's exact behaviour — this is pure operator tuning, no default is " + "changed. Cascades from defaults.channels.telegram.send_gate."),
|
|
4269
|
+
worker_feed: exports_external.object({
|
|
4270
|
+
max_rows: exports_external.number().int().positive().optional().describe("Max live-worker rows rendered in the COMBINED worker-activity feed " + "(2+ background workers in one chat/thread coalesce into ONE message; " + "telegram-plugin/worker-activity-feed.ts) before a compact " + "'+M more working…' spill line — keeps the coalesced body compact and " + "legible (and under the rich-message wire ceiling). Default 8. A " + "single-worker chat renders the full \uD83D\uDEE0 Worker card and ignores this. " + "Must be an integer >= 1. Omit to keep 8.")
|
|
4271
|
+
}).optional().describe("Tuning for the coalesced worker-activity feed " + "(telegram-plugin/worker-activity-feed.ts). Cascades from " + "defaults.channels.telegram.worker_feed."),
|
|
4259
4272
|
stickers: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Sticker aliases for the `send_sticker` MCP tool (#576). Maps a " + "short alias name (e.g. 'happy', 'thinking') to a Telegram file_id. " + "Operator-curated — capture file_ids from inbound stickers the user " + "sends and add them here. The agent calls send_sticker(chat_id, " + "alias='happy') and the gateway resolves to the file_id at send " + "time. Aliases enable persona-flavored expressiveness without " + "exposing raw file_ids in the agent prompt. Personal-assistant / " + "health-coach personas benefit; coding agents typically don't " + "configure any."),
|
|
4260
4273
|
voice_in: exports_external.object({
|
|
4261
4274
|
enabled: exports_external.boolean().optional().describe("Master switch for voice-message transcription."),
|
|
@@ -4281,6 +4294,9 @@ var init_schema = __esm(() => {
|
|
|
4281
4294
|
window_ms: exports_external.number().int().nonnegative().optional().describe("Sliding-window (ms) for merging consecutive inbound messages from " + "the same sender+topic into ONE Claude turn. Each new message resets " + "the timer; the turn starts once the sender pauses for this long. " + "Catches forwarded bursts, pasted text the Telegram client split " + "into several messages, and mixed text+media forwards. Default 500. " + "Set 0 to disable (every message becomes its own turn). Raise for " + "users who think in multiple short messages; the trade-off is the " + "single-message turn start is delayed by this much (the \uD83D\uDC40 ack still " + "fires immediately, so perceived latency is unchanged)."),
|
|
4282
4295
|
max_attachments: exports_external.number().int().positive().optional().describe("Maximum number of media attachments carried into ONE coalesced " + "Claude turn. Default 10 — a full Telegram album (media_group caps " + "at 10) or a text+multi-image forwarded burst arrives as a single " + "turn; the agent sees numbered attachment fields (image_path, " + "image_path_2, …). Set 1 to restore the historical " + "single-attachment-per-turn behaviour. Excess attachments beyond " + "the cap spill into the next turn. Each attachment is downloaded, " + "so a high cap on a slow link delays turn start.")
|
|
4283
4296
|
}).optional().describe("Inbound coalescing — how the gateway groups rapid consecutive messages " + "into a single turn so a forwarded album or split paste doesn't fan out " + "into N separate turns. Cascades from defaults.channels.telegram.coalesce."),
|
|
4297
|
+
litellm_notice: exports_external.object({
|
|
4298
|
+
window_ms: exports_external.number().int().positive().optional().describe("Per-agent cooldown window (ms) for the litellm-local 429 notice. " + "When the agent trips the LiteLLM proxy's OWN tpm_limit/rpm_limit " + "cap (a `litellm-local` classified 429 — see docs/auth.md § " + "LiteLLM-proxy-local 429s), the gateway posts ONE calm notice " + "naming the fleet token limiter, then counts further hits silently " + "for this long; the first notice after the window expires says " + "how many were absorbed. Default 900000 (15 min). Invalid values " + "fall back to the default.")
|
|
4299
|
+
}).optional().describe("Debounce tuning for the litellm-local throttle notice — the calm " + "'fleet token limiter engaged' message posted when the LiteLLM " + "proxy's own rate cap trips (never an Anthropic account limit). " + "Cascades from defaults.channels.telegram.litellm_notice."),
|
|
4284
4300
|
interrupt: exports_external.object({
|
|
4285
4301
|
safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
|
|
4286
4302
|
max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short — the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
|
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.18",
|
|
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": {
|
|
@@ -1130,23 +1130,28 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
|
|
|
1130
1130
|
unset sr_ll_key sr_ll_ok sr_ll_unreachable
|
|
1131
1131
|
fi
|
|
1132
1132
|
|
|
1133
|
-
# --- Session model resolution (
|
|
1133
|
+
# --- Session model resolution (consume-once .session-model carrier) ---
|
|
1134
1134
|
#
|
|
1135
|
-
# Contract: reference/rfcs/session-model-stickiness.md (
|
|
1136
|
-
#
|
|
1137
|
-
#
|
|
1138
|
-
# honored on EVERY boot — deploy, watchdog bounce, raw `docker restart`,
|
|
1139
|
-
# host reboot, crash. It is cleared only by:
|
|
1135
|
+
# Contract: reference/rfcs/session-model-stickiness.md §0.1 (rev 4, operator
|
|
1136
|
+
# decision 2026-07-12 — SESSION-SCOPED, superseding rev 3 keep-by-default). A
|
|
1137
|
+
# `/model` override lasts only for the current session:
|
|
1140
1138
|
#
|
|
1141
|
-
# -
|
|
1142
|
-
#
|
|
1143
|
-
#
|
|
1144
|
-
#
|
|
1139
|
+
# - Claude switches apply LIVE in-session (claude's native picker) and write
|
|
1140
|
+
# NO carrier — the explicit `claude --model {{{modelQ}}}` this script execs
|
|
1141
|
+
# reverts them for free on the next boot.
|
|
1142
|
+
# - sr-* / sr→Claude switches and a queued /model persisted at shutdown need
|
|
1143
|
+
# a relaunch to take effect, so the gateway writes `{{agentDir}}/.session-model`
|
|
1144
|
+
# (one-line JSON) IMMEDIATELY before that relaunch. This block APPLIES the
|
|
1145
|
+
# carrier on the single boot that reads it and then DELETES it (consume-once).
|
|
1146
|
+
# Any SUBSEQUENT restart — deploy, `/restart`, `/new`, watchdog recovery,
|
|
1147
|
+
# crash, raw `docker restart`, host reboot — finds no carrier and boots the
|
|
1148
|
+
# configured default. That deletion is the marker distinguishing the
|
|
1149
|
+
# model-apply relaunch from every later restart.
|
|
1145
1150
|
#
|
|
1146
|
-
#
|
|
1147
|
-
#
|
|
1148
|
-
#
|
|
1149
|
-
#
|
|
1151
|
+
# `/model default` deletes the carrier live. Invalidation paths (corrupt file,
|
|
1152
|
+
# configured yaml `model:` changed) also delete + write `.session-model-alert`,
|
|
1153
|
+
# which the gateway relays to the operator chat once at boot. The normal apply
|
|
1154
|
+
# path is silent here — the gateway already acked the `/model` in chat.
|
|
1150
1155
|
#
|
|
1151
1156
|
# NB {{{modelQ}}} is already shell-single-quoted by the scaffold (it renders as
|
|
1152
1157
|
# a quoted token, e.g. 'claude-sonnet-5'), so it is assigned BARE here — never
|
|
@@ -1154,63 +1159,42 @@ fi
|
|
|
1154
1159
|
# the value and break `claude --model`.
|
|
1155
1160
|
_EFFECTIVE_MODEL={{{modelQ}}}
|
|
1156
1161
|
# Record the RESOLVED configured default (raw, unquoted) every boot, before
|
|
1157
|
-
# override resolution. The gateway copies this into the
|
|
1162
|
+
# override resolution. The gateway copies this into the carrier's
|
|
1158
1163
|
# `configuredDefaultAtWrite`, so both sides of the invalidation compare below
|
|
1159
1164
|
# come from the same resolver. Overwrite, not consumed.
|
|
1160
1165
|
printf '%s\n' "$_EFFECTIVE_MODEL" > "{{agentDir}}/.configured-default-model" 2>/dev/null || true
|
|
1161
1166
|
|
|
1162
|
-
#
|
|
1163
|
-
#
|
|
1164
|
-
#
|
|
1165
|
-
#
|
|
1166
|
-
|
|
1167
|
-
|
|
1167
|
+
# Rev-4 hygiene (#3184 review LOW-2): remove the files retired with the
|
|
1168
|
+
# keep/revert intent subsystem and the crashloop self-heal. Nothing reads them
|
|
1169
|
+
# anymore; without this rm they would linger in the bind-mounted state dir
|
|
1170
|
+
# forever after rollout. Idempotent — a cheap no-op once clean.
|
|
1171
|
+
rm -f "{{agentDir}}/.relaunch-model-intent" "{{agentDir}}/.session-model-boot-attempts" "{{agentDir}}/.session-model-kept-notified"
|
|
1172
|
+
|
|
1173
|
+
# Migration shim (one release): a leftover one-shot `.session-model-override`
|
|
1174
|
+
# carrier from a pre-consume-once gateway means an OLD gateway wrote it
|
|
1175
|
+
# immediately before this very bounce — convert it to `.session-model` so the
|
|
1176
|
+
# block below applies + consumes it this boot (same one-shot semantics).
|
|
1168
1177
|
if [ -f "{{agentDir}}/.session-model-override" ]; then
|
|
1169
1178
|
_mig="$(tr -d '[:space:]' < "{{agentDir}}/.session-model-override" 2>/dev/null || true)"
|
|
1170
1179
|
rm -f "{{agentDir}}/.session-model-override"
|
|
1171
1180
|
if printf '%s' "$_mig" | grep -Eq '^[A-Za-z0-9][]A-Za-z0-9._[/-]{0,99}$'; then
|
|
1172
1181
|
printf '{"model":"%s","configuredDefaultAtWrite":"%s","ts":%s}\n' "$_mig" "$_EFFECTIVE_MODEL" "$(( $(date +%s) * 1000 ))" > "{{agentDir}}/.session-model" 2>/dev/null || true
|
|
1173
|
-
|
|
1174
|
-
echo "session-model: migrated legacy one-shot carrier '$_mig' to durable .session-model (applying this boot)" >&2
|
|
1182
|
+
echo "session-model: migrated legacy one-shot carrier '$_mig' to .session-model (applying + consuming this boot)" >&2
|
|
1175
1183
|
else
|
|
1176
1184
|
echo "session-model: ignoring malformed legacy .session-model-override (failed shape gate)" >&2
|
|
1177
1185
|
fi
|
|
1178
1186
|
unset _mig
|
|
1179
1187
|
fi
|
|
1180
1188
|
|
|
1181
|
-
# One-shot intent: consume unconditionally, honor only a fresh "keep".
|
|
1182
|
-
# Freshness clock is the EMBEDDED ts (ms), never file mtime — same clock the
|
|
1183
|
-
# gateway writes with.
|
|
1184
|
-
_sm_reason=""
|
|
1185
|
-
_sm_revert=""
|
|
1186
|
-
if [ -f "{{agentDir}}/.relaunch-model-intent" ]; then
|
|
1187
|
-
_int_raw="$(cat "{{agentDir}}/.relaunch-model-intent" 2>/dev/null || true)"
|
|
1188
|
-
rm -f "{{agentDir}}/.relaunch-model-intent"
|
|
1189
|
-
_int="$(printf '%s' "$_int_raw" | sed -n 's/.*"intent"[[:space:]]*:[[:space:]]*"\([a-z]*\)".*/\1/p')"
|
|
1190
|
-
_int_ts="$(printf '%s' "$_int_raw" | sed -n 's/.*"ts"[[:space:]]*:[[:space:]]*\([0-9]\{1,\}\).*/\1/p')"
|
|
1191
|
-
_sm_reason="$(printf '%s' "$_int_raw" | sed -n 's/.*"reason"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1192
|
-
# #3039: boot default is KEEP. Only a FRESH explicit "revert" intent
|
|
1193
|
-
# (stamped by an explicit user/gateway revert path) clears the override.
|
|
1194
|
-
# A stale or corrupt intent counts as no intent → keep.
|
|
1195
|
-
if [ "$_int" = "revert" ] && [ -n "$_int_ts" ] && [ $(( $(date +%s) * 1000 - _int_ts )) -lt 600000 ]; then
|
|
1196
|
-
_sm_revert="1"
|
|
1197
|
-
fi
|
|
1198
|
-
unset _int_raw _int _int_ts
|
|
1199
|
-
fi
|
|
1200
|
-
|
|
1201
1189
|
if [ -f "{{agentDir}}/.session-model" ]; then
|
|
1202
1190
|
_smf="$(cat "{{agentDir}}/.session-model" 2>/dev/null || true)"
|
|
1203
1191
|
_sm_model="$(printf '%s' "$_smf" | sed -n 's/.*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1204
1192
|
_sm_cfg="$(printf '%s' "$_smf" | sed -n 's/.*"configuredDefaultAtWrite"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
_sm_why="${_sm_reason:-explicit revert intent}"
|
|
1211
|
-
echo "session-model: reverting to configured default '$_EFFECTIVE_MODEL' — $_sm_why (session override '$_sm_model' cleared)" >&2
|
|
1212
|
-
printf 'Session model override `%s` was cleared as requested (%s) — this relaunch booted the configured default `%s`. Re-issue /model %s to switch back.\n' "$_sm_model" "$_sm_why" "$_EFFECTIVE_MODEL" "$_sm_model" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1213
|
-
unset _sm_why
|
|
1193
|
+
# CONSUME-ONCE: delete the carrier now, before any apply/exec. The carrier
|
|
1194
|
+
# governs exactly this one boot; a bad token can therefore crash at most one
|
|
1195
|
+
# boot (the next boot has no carrier and reverts), so no crashloop self-heal
|
|
1196
|
+
# is needed. Every branch below has already removed the file.
|
|
1197
|
+
rm -f "{{agentDir}}/.session-model"
|
|
1214
1198
|
# Shape gate — kept BYTE-IDENTICAL with MODEL_ARG_RE in
|
|
1215
1199
|
# telegram-plugin/gateway/model-command.ts. `/` is allowed for
|
|
1216
1200
|
# OpenRouter-style `sr-vendor/model` ids; it is not a shell metachar inside
|
|
@@ -1219,116 +1203,69 @@ if [ -f "{{agentDir}}/.session-model" ]; then
|
|
|
1219
1203
|
# extraction above emits one match per matching LINE, and `grep -Eq`
|
|
1220
1204
|
# passes if ANY line matches — a multiline value must never reach
|
|
1221
1205
|
# `claude --model` (parity with parseSessionModel's single-string check).
|
|
1222
|
-
|
|
1223
|
-
# Invalid carrier
|
|
1224
|
-
#
|
|
1225
|
-
rm -f "{{agentDir}}/.session-model" "{{agentDir}}/.session-model-kept-notified"
|
|
1206
|
+
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
|
|
1207
|
+
# Invalid carrier: fall back to the configured default AND tell the
|
|
1208
|
+
# operator once — never a silent stderr-only drop.
|
|
1226
1209
|
echo "session-model: ignoring malformed .session-model (failed shape gate) — using configured default '$_EFFECTIVE_MODEL'" >&2
|
|
1227
1210
|
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
1211
|
elif [ "$_sm_cfg" != "$_EFFECTIVE_MODEL" ]; then
|
|
1229
|
-
# switchroom.yaml `model:` changed
|
|
1230
|
-
# against a default that no longer exists
|
|
1231
|
-
#
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1212
|
+
# switchroom.yaml `model:` changed between the switch and this apply-boot →
|
|
1213
|
+
# the carrier is against a default that no longer exists. Invalidate +
|
|
1214
|
+
# announce once.
|
|
1215
|
+
echo "session-model: configured default changed ('$_sm_cfg' → '$_EFFECTIVE_MODEL') — dropping session override '$_sm_model'" >&2
|
|
1216
|
+
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
|
|
1217
|
+
elif [ "${_sm_model#sr-}" != "$_sm_model" ] && [ -z "$_LITELLM_OK" ]; then
|
|
1218
|
+
# sr-* + LiteLLM unreachable at the apply-boot: booting the override would
|
|
1219
|
+
# 4xx against Anthropic, and consume-once means we can't retain it for a
|
|
1220
|
+
# later relaunch. Boot the configured default and tell the operator to
|
|
1221
|
+
# re-issue once the proxy is back.
|
|
1222
|
+
echo "session-model: LiteLLM proxy unreachable at boot — NOT applying sr-* override '$_sm_model'; booting configured default '$_EFFECTIVE_MODEL' (re-issue /model when the proxy is back)" >&2
|
|
1223
|
+
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
|
|
1235
1224
|
else
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
# Anthropic. Boot the configured default but RETAIN the durable file
|
|
1241
|
-
# — it re-applies on the next keep relaunch once the proxy is back.
|
|
1242
|
-
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
|
|
1243
|
-
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
|
|
1244
|
-
else
|
|
1245
|
-
_EFFECTIVE_MODEL="$_sm_model"
|
|
1246
|
-
fi
|
|
1247
|
-
;;
|
|
1248
|
-
*)
|
|
1249
|
-
_EFFECTIVE_MODEL="$_sm_model"
|
|
1250
|
-
;;
|
|
1251
|
-
esac
|
|
1252
|
-
if [ "$_EFFECTIVE_MODEL" = "$_sm_model" ]; then
|
|
1253
|
-
echo "session-model: keeping session override '$_sm_model' across this relaunch${_sm_reason:+ ($_sm_reason)}" >&2
|
|
1254
|
-
# #3042 item 4: notify the chat ONCE per kept value — a watchdog bounce
|
|
1255
|
-
# loop must not storm the operator with identical "kept" alerts. The
|
|
1256
|
-
# sentinel is cleared on every path that clears the override.
|
|
1257
|
-
if [ "$(cat "{{agentDir}}/.session-model-kept-notified" 2>/dev/null || true)" != "$_sm_model" ]; then
|
|
1258
|
-
printf 'Session model override `%s` kept across this relaunch%s. It persists across restarts and deploys; /model default clears it.\n' "$_sm_model" "${_sm_reason:+ ($_sm_reason)}" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1259
|
-
printf '%s\n' "$_sm_model" > "{{agentDir}}/.session-model-kept-notified" 2>/dev/null || true
|
|
1260
|
-
fi
|
|
1261
|
-
fi
|
|
1225
|
+
# Apply the carrier for THIS session. No boot alert — the gateway already
|
|
1226
|
+
# acked the /model switch in chat, and the override reverts on next restart.
|
|
1227
|
+
_EFFECTIVE_MODEL="$_sm_model"
|
|
1228
|
+
echo "session-model: applying session override '$_sm_model' this boot (consume-once — reverts on next restart)" >&2
|
|
1262
1229
|
fi
|
|
1263
|
-
unset _smf _sm_model _sm_cfg
|
|
1230
|
+
unset _smf _sm_model _sm_cfg
|
|
1264
1231
|
fi
|
|
1265
|
-
unset _sm_keep _sm_reason _sm_revert
|
|
1266
1232
|
|
|
1267
|
-
# ---
|
|
1233
|
+
# --- Session effort resolution (consume-once .session-effort carrier, #3186) ---
|
|
1268
1234
|
#
|
|
1269
|
-
#
|
|
1270
|
-
#
|
|
1271
|
-
#
|
|
1272
|
-
#
|
|
1273
|
-
#
|
|
1274
|
-
#
|
|
1275
|
-
#
|
|
1276
|
-
#
|
|
1277
|
-
#
|
|
1278
|
-
|
|
1279
|
-
if [ -n "$_SM_CFG_RECORDED" ] && [ "$_EFFECTIVE_MODEL" != "$_SM_CFG_RECORDED" ] && [ -f "{{agentDir}}/.session-model" ]; then
|
|
1280
|
-
_bl_now="$(date +%s)"
|
|
1281
|
-
_bl_cnt=0
|
|
1282
|
-
_bl_prev=0
|
|
1283
|
-
if [ -f "{{agentDir}}/.session-model-boot-attempts" ]; then
|
|
1284
|
-
read -r _bl_cnt _bl_prev < "{{agentDir}}/.session-model-boot-attempts" 2>/dev/null || true
|
|
1285
|
-
fi
|
|
1286
|
-
case "$_bl_cnt" in (''|*[!0-9]*) _bl_cnt=0;; esac
|
|
1287
|
-
case "$_bl_prev" in (''|*[!0-9]*) _bl_prev=0;; esac
|
|
1288
|
-
if [ $(( _bl_now - _bl_prev )) -lt 150 ]; then _bl_cnt=$(( _bl_cnt + 1 )); else _bl_cnt=1; fi
|
|
1289
|
-
if [ "$_bl_cnt" -ge 3 ]; then
|
|
1290
|
-
echo "session-model: override '$_EFFECTIVE_MODEL' appears to be crashlooping the session ($_bl_cnt fast boots) — clearing it, booting configured default '$_SM_CFG_RECORDED'" >&2
|
|
1291
|
-
printf 'Session model override `%s` was cleared automatically: the agent failed to stay up %s boots in a row with it active (the model may be invalid or retired). Booting the configured default `%s`. Re-issue /model <name> if you want a different model.\n' "$_EFFECTIVE_MODEL" "$_bl_cnt" "$_SM_CFG_RECORDED" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1292
|
-
rm -f "{{agentDir}}/.session-model" "{{agentDir}}/.session-model-boot-attempts" "{{agentDir}}/.session-model-kept-notified"
|
|
1293
|
-
_EFFECTIVE_MODEL="$_SM_CFG_RECORDED"
|
|
1294
|
-
else
|
|
1295
|
-
printf '%s %s\n' "$_bl_cnt" "$_bl_now" > "{{agentDir}}/.session-model-boot-attempts" 2>/dev/null || true
|
|
1296
|
-
fi
|
|
1297
|
-
unset _bl_now _bl_cnt _bl_prev
|
|
1298
|
-
else
|
|
1299
|
-
# No active override this boot — a stale counter must not bite a future one.
|
|
1300
|
-
rm -f "{{agentDir}}/.session-model-boot-attempts"
|
|
1301
|
-
fi
|
|
1302
|
-
unset _SM_CFG_RECORDED
|
|
1303
|
-
|
|
1304
|
-
# --- Session effort resolution (durable .session-effort, #3039) ---
|
|
1235
|
+
# The `/effort` sibling of the block above, session-scoped like /model
|
|
1236
|
+
# (rev 4). A live `/effort <level>` applies in-session via the applyEffort
|
|
1237
|
+
# driver and writes NO carrier — the explicit `--effort <configured>` below
|
|
1238
|
+
# reverts it on the next boot. The ONLY boot-applied case is a queued
|
|
1239
|
+
# mid-turn `/effort` the gateway persisted at shutdown: this block APPLIES
|
|
1240
|
+
# that carrier on the single boot that reads it and then DELETES it
|
|
1241
|
+
# (consume-once), so any subsequent restart reverts to the configured
|
|
1242
|
+
# `thinking_effort`. Invalidation (corrupt file / configured default
|
|
1243
|
+
# changed) appends to `.session-model-alert` so the gateway's boot relay
|
|
1244
|
+
# tells the operator once.
|
|
1305
1245
|
#
|
|
1306
|
-
#
|
|
1307
|
-
#
|
|
1308
|
-
#
|
|
1309
|
-
# honored on every boot and cleared only by `/effort default` (live delete)
|
|
1310
|
-
# or invalidation here (corrupt file / configured `thinking_effort:` changed)
|
|
1311
|
-
# — each invalidation appends to `.session-model-alert` so the gateway's
|
|
1312
|
-
# boot relay tells the operator once.
|
|
1246
|
+
# NB: the CONFIGURED default resolution is untouched (#3186 constraint) —
|
|
1247
|
+
# the fleet `thinking_effort: low` pin (see #1978 /
|
|
1248
|
+
# src/config/thinking-effort-risk.ts) resolves exactly as before.
|
|
1313
1249
|
_EFFECTIVE_EFFORT={{#if thinkingEffort}}'{{thinkingEffort}}'{{else}}''{{/if}}
|
|
1314
1250
|
if [ -f "{{agentDir}}/.session-effort" ]; then
|
|
1315
1251
|
_sef="$(cat "{{agentDir}}/.session-effort" 2>/dev/null || true)"
|
|
1316
1252
|
_se_level="$(printf '%s' "$_sef" | sed -n 's/.*"level"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1317
1253
|
_se_cfg="$(printf '%s' "$_sef" | sed -n 's/.*"configuredDefaultAtWrite"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1254
|
+
# CONSUME-ONCE: delete the carrier now, before any apply/exec — it governs
|
|
1255
|
+
# exactly this one boot (parity with the .session-model block above).
|
|
1256
|
+
rm -f "{{agentDir}}/.session-effort"
|
|
1318
1257
|
# Allowlist gate — kept in sync with EFFORT_LEVELS in
|
|
1319
1258
|
# telegram-plugin/gateway/effort-command.ts. The level is passed verbatim
|
|
1320
1259
|
# to `claude --effort`.
|
|
1321
1260
|
if [ "$(printf '%s' "$_se_level" | wc -l)" -ne 0 ] || ! printf '%s' "$_se_level" | grep -Eq '^(low|medium|high|xhigh|max)$'; then
|
|
1322
|
-
rm -f "{{agentDir}}/.session-effort"
|
|
1323
1261
|
echo "session-effort: ignoring malformed .session-effort (failed allowlist gate) — using configured default '${_EFFECTIVE_EFFORT:-<unset>}'" >&2
|
|
1324
1262
|
printf 'Your saved session effort override could not be read (invalid or corrupt), so the agent booted on its configured default effort. Re-issue /effort <level> if you want a different one.\n' >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1325
1263
|
elif [ "$_se_cfg" != "$_EFFECTIVE_EFFORT" ]; then
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
printf 'The configured default effort changed (`%s` → `%s`), so your session effort override `%s` was cleared — the agent booted on the new configured default. Re-issue /effort %s if you still want it.\n' "${_se_cfg:-<unset>}" "${_EFFECTIVE_EFFORT:-<unset>}" "$_se_level" "$_se_level" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1264
|
+
echo "session-effort: configured thinking_effort changed ('$_se_cfg' → '${_EFFECTIVE_EFFORT:-<unset>}') — dropping session override '$_se_level'" >&2
|
|
1265
|
+
printf 'The configured default effort changed (`%s` → `%s`), so your session effort override `%s` was not applied — the agent booted on the new configured default. Re-issue /effort %s if you still want it.\n' "${_se_cfg:-<unset>}" "${_EFFECTIVE_EFFORT:-<unset>}" "$_se_level" "$_se_level" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1329
1266
|
else
|
|
1330
1267
|
_EFFECTIVE_EFFORT="$_se_level"
|
|
1331
|
-
echo "session-effort:
|
|
1268
|
+
echo "session-effort: applying session effort override '$_se_level' this boot (consume-once — reverts on next restart)" >&2
|
|
1332
1269
|
fi
|
|
1333
1270
|
unset _sef _se_level _se_cfg
|
|
1334
1271
|
fi
|
|
@@ -1337,6 +1274,11 @@ if [ -n "$_EFFECTIVE_EFFORT" ]; then
|
|
|
1337
1274
|
else
|
|
1338
1275
|
_EFFORT_ARG=""
|
|
1339
1276
|
fi
|
|
1277
|
+
# Record the EFFECTIVE launched effort so the gateway can re-hydrate its
|
|
1278
|
+
# in-memory session-effort state after a queued-apply boot, keeping the
|
|
1279
|
+
# /effort menu highlight honest. Overwrite (not consumed) — the effort
|
|
1280
|
+
# sibling of `.active-session-model` below.
|
|
1281
|
+
printf '%s\n' "$_EFFECTIVE_EFFORT" > "{{agentDir}}/.active-session-effort" 2>/dev/null || true
|
|
1340
1282
|
|
|
1341
1283
|
# sr-* passthrough→router repoint — ONE post-resolution gate covering BOTH the
|
|
1342
1284
|
# /model override path AND the configured-default path (`model: sr-*` in
|
|
@@ -21,6 +21,7 @@ import { homedir } from 'os'
|
|
|
21
21
|
|
|
22
22
|
import { installPluginLogger } from '../plugin-logger.js'
|
|
23
23
|
import { startSessionTail, type SessionEvent, type SessionTailHandle } from '../session-tail.js'
|
|
24
|
+
import { truncateDetailPreservingRequestId } from '../raw-error-scrub.js'
|
|
24
25
|
import {
|
|
25
26
|
startPtyTail,
|
|
26
27
|
V1ToolActivityExtractor,
|
|
@@ -851,7 +852,12 @@ if (sessionTailEnabled) {
|
|
|
851
852
|
type: 'operator_event',
|
|
852
853
|
kind: ev.kind,
|
|
853
854
|
agent: AGENT_NAME,
|
|
854
|
-
|
|
855
|
+
// Preserve the Anthropic request_id through the 1000-char cap: it
|
|
856
|
+
// sits in the trailing byte-blob (often past char 1000), and it is
|
|
857
|
+
// the reliable EXACT key for the cross-surface dedup gate. A naive
|
|
858
|
+
// slice would drop it → the gate degrades to the coarse per-kind key
|
|
859
|
+
// and wrongly collapses two distinct same-kind errors within 60s.
|
|
860
|
+
detail: truncateDetailPreservingRequestId(ev.detail, 1000),
|
|
855
861
|
chatId: '',
|
|
856
862
|
})
|
|
857
863
|
} catch (err) {
|
|
@@ -23323,6 +23323,10 @@ function projectTranscriptLine(line) {
|
|
|
23323
23323
|
const content = message?.content;
|
|
23324
23324
|
if (!Array.isArray(content))
|
|
23325
23325
|
return [];
|
|
23326
|
+
if (obj.isApiErrorMessage === true) {
|
|
23327
|
+
const mainModel2 = message?.model;
|
|
23328
|
+
return typeof mainModel2 === "string" && !isModelSentinel(mainModel2) ? [{ kind: "model", model: mainModel2 }] : [];
|
|
23329
|
+
}
|
|
23326
23330
|
const events = [];
|
|
23327
23331
|
const mainModel = message?.model;
|
|
23328
23332
|
if (typeof mainModel === "string" && !isModelSentinel(mainModel)) {
|
|
@@ -23952,6 +23956,27 @@ function startSessionTail(config2) {
|
|
|
23952
23956
|
};
|
|
23953
23957
|
}
|
|
23954
23958
|
|
|
23959
|
+
// raw-error-scrub.ts
|
|
23960
|
+
function extractRequestId(raw) {
|
|
23961
|
+
if (typeof raw !== "string" || raw.length === 0)
|
|
23962
|
+
return;
|
|
23963
|
+
const m = raw.match(/["']request[_-]?id["']\s*:\s*["']([A-Za-z0-9._-]+)["']/i) ?? raw.match(/\brequest[_-]?id[=:]\s*([A-Za-z0-9._-]+)/i);
|
|
23964
|
+
return m ? m[1] : undefined;
|
|
23965
|
+
}
|
|
23966
|
+
function truncateDetailPreservingRequestId(detail, max) {
|
|
23967
|
+
if (typeof detail !== "string")
|
|
23968
|
+
return "";
|
|
23969
|
+
if (detail.length <= max)
|
|
23970
|
+
return detail;
|
|
23971
|
+
const rid = extractRequestId(detail);
|
|
23972
|
+
const head = detail.slice(0, max);
|
|
23973
|
+
if (rid == null || head.includes(rid))
|
|
23974
|
+
return head;
|
|
23975
|
+
const suffix = ` request_id=${rid}`;
|
|
23976
|
+
const headBudget = Math.max(0, max - suffix.length);
|
|
23977
|
+
return `${detail.slice(0, headBudget)}${suffix}`;
|
|
23978
|
+
}
|
|
23979
|
+
|
|
23955
23980
|
// pty-tail.ts
|
|
23956
23981
|
var import_headless = __toESM(require_xterm_headless(), 1);
|
|
23957
23982
|
import { existsSync as existsSync4, statSync as statSync4, watch as watch2, openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
|
|
@@ -25475,7 +25500,7 @@ if (sessionTailEnabled) {
|
|
|
25475
25500
|
type: "operator_event",
|
|
25476
25501
|
kind: ev.kind,
|
|
25477
25502
|
agent: AGENT_NAME,
|
|
25478
|
-
detail: ev.detail
|
|
25503
|
+
detail: truncateDetailPreservingRequestId(ev.detail, 1000),
|
|
25479
25504
|
chatId: ""
|
|
25480
25505
|
});
|
|
25481
25506
|
} catch (err) {
|