switchroom 0.16.10 → 0.16.11

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.
@@ -51674,8 +51674,8 @@ import { existsSync, readFileSync } from "node:fs";
51674
51674
  import { dirname, join } from "node:path";
51675
51675
 
51676
51676
  // src/build-info.ts
51677
- var VERSION = "0.16.10";
51678
- var COMMIT_SHA = "8af198e9";
51677
+ var VERSION = "0.16.11";
51678
+ var COMMIT_SHA = "7846a496";
51679
51679
 
51680
51680
  // src/cli/resolve-version.ts
51681
51681
  function readPackageVersion() {
@@ -22587,7 +22587,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:f
22587
22587
  import { dirname as dirname4, join as join2 } from "node:path";
22588
22588
 
22589
22589
  // src/build-info.ts
22590
- var VERSION = "0.16.10";
22590
+ var VERSION = "0.16.11";
22591
22591
 
22592
22592
  // src/cli/resolve-version.ts
22593
22593
  function readPackageVersion() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.16.10",
3
+ "version": "0.16.11",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46034,10 +46034,11 @@ function parseModelCommand(text) {
46034
46034
  }
46035
46035
  var PERSIST_NOTE = "<i>Session-only \u2014 lasts until restart. To persist, set <code>model:</code> in switchroom.yaml and restart.</i>";
46036
46036
  function helpText2(deps, reason) {
46037
+ const srAliasExamples = Object.keys(SR_MODEL_ALIASES).map((a) => `<code>${a}</code>`).join(" \u00b7 ");
46037
46038
  const lines = [];
46038
46039
  if (reason)
46039
46040
  lines.push(`\u26a0\ufe0f ${deps.escapeHtml(reason)}`);
46040
- lines.push("<b>/model</b> \u2014 show or switch the Claude model", "<code>/model</code> \u2014 show the configured model", `<code>/model &lt;name&gt;</code> \u2014 switch the live session (${MODEL_ALIASES.map((a) => `<code>${a}</code>`).join(" \u00b7 ")} or a full model id)`, PERSIST_NOTE);
46041
+ lines.push("<b>/model</b> \u2014 show or switch the Claude model", "<code>/model</code> \u2014 show the configured model", `<code>/model &lt;name&gt;</code> \u2014 switch the live session (${MODEL_ALIASES.map((a) => `<code>${a}</code>`).join(" \u00b7 ")} or a full model id)`, `<i>OpenRouter shortcuts:</i> ${srAliasExamples}`, PERSIST_NOTE);
46041
46042
  return { text: lines.join(`
46042
46043
  `), html: true };
46043
46044
  }
@@ -46047,11 +46048,13 @@ async function handleModelCommand(parsed, deps) {
46047
46048
  if (parsed.kind === "show") {
46048
46049
  const configured = deps.getConfiguredModel();
46049
46050
  const shown = configured && configured.length > 0 ? configured : "default";
46051
+ const srAliasExamples = Object.keys(SR_MODEL_ALIASES).map((a) => `<code>/model ${a}</code>`).join(" \u00b7 ");
46050
46052
  return {
46051
46053
  text: [
46052
46054
  `<b>Model \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`,
46053
46055
  `Configured: <code>${deps.escapeHtml(shown)}</code>`,
46054
46056
  `Switch the live session: ${MODEL_ALIASES.map((a) => `<code>/model ${a}</code>`).join(" \u00b7 ")}`,
46057
+ `OpenRouter shortcuts: ${srAliasExamples}`,
46055
46058
  "or <code>/model &lt;full-model-id&gt;</code>",
46056
46059
  PERSIST_NOTE
46057
46060
  ].join(`
@@ -46062,10 +46065,11 @@ async function handleModelCommand(parsed, deps) {
46062
46065
  if (!isValidModelArg(parsed.model)) {
46063
46066
  return helpText2(deps, `not a valid model name: ${parsed.model}`);
46064
46067
  }
46068
+ const model = expandSrAlias(parsed.model);
46065
46069
  const currentSession = deps.getActiveSessionModel();
46066
- if (currentSession !== null && isSrModel(currentSession) && isClaudeModel(parsed.model)) {
46070
+ if (currentSession !== null && isSrModel(currentSession) && isClaudeModel(model)) {
46067
46071
  try {
46068
- await deps.scheduleRestart(`user: /model ${parsed.model} (sr-to-claude restart)`);
46072
+ await deps.scheduleRestart(`user: /model ${model} (sr-to-claude restart)`);
46069
46073
  } catch (err) {
46070
46074
  const msg = err instanceof Error ? err.message : String(err);
46071
46075
  return {
@@ -46082,10 +46086,10 @@ async function handleModelCommand(parsed, deps) {
46082
46086
  html: true
46083
46087
  };
46084
46088
  }
46085
- const verbHtml = `<code>/model ${deps.escapeHtml(parsed.model)}</code>`;
46089
+ const verbHtml = `<code>/model ${deps.escapeHtml(model)}</code>`;
46086
46090
  let result;
46087
46091
  try {
46088
- result = await deps.inject(deps.getAgentName(), `/model ${parsed.model}`);
46092
+ result = await deps.inject(deps.getAgentName(), `/model ${model}`);
46089
46093
  } catch (err) {
46090
46094
  const msg = err instanceof Error ? err.message : String(err);
46091
46095
  return {
@@ -46136,8 +46140,20 @@ var SR_MODEL_LABELS = {
46136
46140
  "sr-gemini-2.5-flash": "Gemini 2.5 Flash",
46137
46141
  "sr-deepseek-r1": "DeepSeek R1",
46138
46142
  "sr-deepseek-v3": "DeepSeek V3",
46139
- "sr-glm-5": "GLM-5"
46143
+ "sr-glm-5": "GLM-5",
46144
+ "sr-codex-5.5": "Codex 5.5"
46140
46145
  };
46146
+ var SR_MODEL_ALIASES = {
46147
+ flash: "sr-gemini-2.5-flash",
46148
+ gemini: "sr-gemini-2.5-pro",
46149
+ deepseek: "sr-deepseek-v3",
46150
+ r1: "sr-deepseek-r1",
46151
+ glm: "sr-glm-5",
46152
+ codex: "sr-codex-5.5"
46153
+ };
46154
+ function expandSrAlias(arg) {
46155
+ return SR_MODEL_ALIASES[arg.toLowerCase()] ?? arg;
46156
+ }
46141
46157
  function srFriendlyLabel(srName) {
46142
46158
  return SR_MODEL_LABELS[srName] ?? srName.replace(/^sr-/, "").replace(/-/g, " ");
46143
46159
  }
@@ -56047,10 +56063,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
56047
56063
  }
56048
56064
 
56049
56065
  // ../src/build-info.ts
56050
- var VERSION = "0.16.10";
56051
- var COMMIT_SHA = "8af198e9";
56052
- var COMMIT_DATE = "2026-06-28T07:14:15Z";
56053
- var LATEST_PR = 2625;
56066
+ var VERSION = "0.16.11";
56067
+ var COMMIT_SHA = "7846a496";
56068
+ var COMMIT_DATE = "2026-06-28T09:51:30Z";
56069
+ var LATEST_PR = 2630;
56054
56070
  var COMMITS_AHEAD_OF_TAG = 0;
56055
56071
 
56056
56072
  // gateway/boot-version.ts
@@ -57709,12 +57725,13 @@ function loadAccess() {
57709
57725
  return BOOT_ACCESS ?? readAccessFile();
57710
57726
  }
57711
57727
  function assertAllowedChat(chat_id) {
57728
+ const id = String(chat_id);
57712
57729
  const access = loadAccess();
57713
- if (access.allowFrom.includes(chat_id))
57730
+ if (access.allowFrom.includes(id))
57714
57731
  return;
57715
- if (chat_id in access.groups)
57732
+ if (id in access.groups)
57716
57733
  return;
57717
- throw new Error(`chat ${chat_id} is not allowlisted \u2014 add via /telegram:access`);
57734
+ throw new Error(`chat ${id} is not allowlisted \u2014 add via /telegram:access`);
57718
57735
  }
57719
57736
  function saveAccess(a) {
57720
57737
  if (STATIC)
@@ -60688,7 +60705,7 @@ async function executeToolCall(tool, args) {
60688
60705
  }
60689
60706
  }
60690
60707
  async function executeSendChecklist(args) {
60691
- const chat_id = args.chat_id;
60708
+ const chat_id = String(args.chat_id ?? "");
60692
60709
  if (!chat_id)
60693
60710
  throw new Error("send_checklist: chat_id is required");
60694
60711
  const title = args.title;
@@ -60754,7 +60771,7 @@ async function executeLinearAgentSetup(args) {
60754
60771
  return runLinearAgentSetup(args);
60755
60772
  }
60756
60773
  async function executeUpdateChecklist(args) {
60757
- const chat_id = args.chat_id;
60774
+ const chat_id = String(args.chat_id ?? "");
60758
60775
  if (!chat_id)
60759
60776
  throw new Error("update_checklist: chat_id is required");
60760
60777
  const message_id = args.message_id;
@@ -60778,7 +60795,7 @@ function redactOutboundText(text2, site) {
60778
60795
  }
60779
60796
  async function executeReply(args) {
60780
60797
  const turn = currentTurn;
60781
- const chat_id = args.chat_id;
60798
+ const chat_id = String(args.chat_id ?? "");
60782
60799
  if (!chat_id)
60783
60800
  throw new Error("reply: chat_id is required");
60784
60801
  const rawText = args.text;
@@ -61315,14 +61332,14 @@ async function executeStreamReply(args) {
61315
61332
  args.text = scrub.scrubbed;
61316
61333
  emitRuntimeMetric({
61317
61334
  kind: "voice_scrub_applied",
61318
- chatKey: statusKey(args.chat_id, args.message_thread_id != null ? Number(args.message_thread_id) : undefined),
61335
+ chatKey: statusKey(String(args.chat_id ?? ""), args.message_thread_id != null ? Number(args.message_thread_id) : undefined),
61319
61336
  replaced: scrub.replaced,
61320
61337
  site: "stream_reply"
61321
61338
  });
61322
61339
  }
61323
61340
  }
61324
61341
  if (args.done === true) {
61325
- const sChatId = args.chat_id;
61342
+ const sChatId = String(args.chat_id ?? "");
61326
61343
  const sThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
61327
61344
  const sText = args.text;
61328
61345
  const dup = outboundDedup.check(sChatId, sThreadId, sText, Date.now(), currentTurn?.registryKey ?? null);
@@ -61333,7 +61350,7 @@ async function executeStreamReply(args) {
61333
61350
  }
61334
61351
  }
61335
61352
  const access = loadAccess();
61336
- const streamChatId = args.chat_id;
61353
+ const streamChatId = String(args.chat_id ?? "");
61337
61354
  const streamIsPrivate = isDmChatId(streamChatId);
61338
61355
  const streamIsForumTopic = args.message_thread_id != null && args.message_thread_id !== "";
61339
61356
  let streamReplyMarkup;
@@ -61433,18 +61450,18 @@ async function executeStreamReply(args) {
61433
61450
  });
61434
61451
  if (result.messageId != null) {
61435
61452
  try {
61436
- progressDriver?.recordOutboundDelivered(args.chat_id, args.message_thread_id);
61453
+ progressDriver?.recordOutboundDelivered(String(args.chat_id ?? ""), args.message_thread_id);
61437
61454
  } catch {}
61438
61455
  try {
61439
61456
  const threadIdNum2 = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
61440
- noteSignal(statusKey(args.chat_id, threadIdNum2), Date.now());
61457
+ noteSignal(statusKey(String(args.chat_id ?? ""), threadIdNum2), Date.now());
61441
61458
  } catch {}
61442
61459
  }
61443
61460
  if (args.done === true && result.messageId != null && streamButtonMeta != null && streamButtonMeta.size > 0) {
61444
- rememberAgentButtonMeta(args.chat_id, result.messageId, streamButtonMeta);
61461
+ rememberAgentButtonMeta(String(args.chat_id ?? ""), result.messageId, streamButtonMeta);
61445
61462
  }
61446
61463
  if (args.done === true && result.messageId != null) {
61447
- const sChatId = args.chat_id;
61464
+ const sChatId = String(args.chat_id ?? "");
61448
61465
  const sThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
61449
61466
  outboundDedup.record(sChatId, sThreadId, args.text, Date.now(), currentTurn?.registryKey ?? null);
61450
61467
  const streamFormat = args.format ?? (access.parseMode ?? "html");
@@ -61477,7 +61494,7 @@ async function executeStreamReply(args) {
61477
61494
  clearSilentEndState(statusKey(streamChatId, streamThreadIdForClear));
61478
61495
  }
61479
61496
  {
61480
- const sChat = args.chat_id;
61497
+ const sChat = String(args.chat_id ?? "");
61481
61498
  const sThread = resolveThreadId(sChat, args.message_thread_id);
61482
61499
  releaseTurnBufferGate(statusKey(sChat, sThread), turn ?? undefined);
61483
61500
  if (turn?.finalAnswerDelivered === true) {
@@ -61491,7 +61508,7 @@ async function executeProgressUpdate(args) {
61491
61508
  throw new Error("progress_update: chat_id is required");
61492
61509
  if (!args.text)
61493
61510
  throw new Error("progress_update: text is required");
61494
- const chat_id = args.chat_id;
61511
+ const chat_id = String(args.chat_id ?? "");
61495
61512
  let text2 = args.text;
61496
61513
  const threadId = resolveThreadId(chat_id, args.message_thread_id);
61497
61514
  const key = statusKey(chat_id, threadId);
@@ -61816,7 +61833,7 @@ function renderVaultRequestSaveCard(req, agentSlug) {
61816
61833
  `);
61817
61834
  }
61818
61835
  async function executeVaultRequestSave(args) {
61819
- const chat_id = args.chat_id;
61836
+ const chat_id = String(args.chat_id ?? "");
61820
61837
  if (!chat_id)
61821
61838
  throw new Error("vault_request_save: chat_id is required");
61822
61839
  const key = args.key;
@@ -61904,7 +61921,7 @@ function renderSecretRequestCard(req) {
61904
61921
  `);
61905
61922
  }
61906
61923
  async function executeRequestSecret(args) {
61907
- const chat_id = args.chat_id;
61924
+ const chat_id = String(args.chat_id ?? "");
61908
61925
  if (!chat_id)
61909
61926
  throw new Error("request_secret: chat_id is required");
61910
61927
  const key = args.key;
@@ -62110,7 +62127,7 @@ function renderVaultRequestAccessCard(req) {
62110
62127
  `);
62111
62128
  }
62112
62129
  async function executeVaultRequestAccess(args) {
62113
- const chat_id = args.chat_id;
62130
+ const chat_id = String(args.chat_id ?? "");
62114
62131
  if (!chat_id)
62115
62132
  throw new Error("vault_request_access: chat_id is required");
62116
62133
  const key = args.key;
@@ -62195,8 +62212,8 @@ async function executeReact(args) {
62195
62212
  throw new Error("react: message_id is required");
62196
62213
  if (!args.emoji)
62197
62214
  throw new Error("react: emoji is required");
62198
- assertAllowedChat(args.chat_id);
62199
- await lockedBot.api.setMessageReaction(args.chat_id, Number(args.message_id), [
62215
+ assertAllowedChat(String(args.chat_id ?? ""));
62216
+ await lockedBot.api.setMessageReaction(String(args.chat_id ?? ""), Number(args.message_id), [
62200
62217
  { type: "emoji", emoji: args.emoji }
62201
62218
  ]);
62202
62219
  return { content: [{ type: "text", text: "reacted" }] };
@@ -62239,7 +62256,7 @@ async function executeEditMessage(args) {
62239
62256
  throw new Error("edit_message: message_id is required");
62240
62257
  if (args.text == null || args.text === "")
62241
62258
  throw new Error("edit_message: text is required and cannot be empty");
62242
- assertAllowedChat(args.chat_id);
62259
+ assertAllowedChat(String(args.chat_id ?? ""));
62243
62260
  const editAccess = loadAccess();
62244
62261
  const editConfigMode = editAccess.parseMode ?? "html";
62245
62262
  const editFormat = args.format ?? editConfigMode;
@@ -62251,7 +62268,7 @@ async function executeEditMessage(args) {
62251
62268
  editRawText = scrub.scrubbed;
62252
62269
  emitRuntimeMetric({
62253
62270
  kind: "voice_scrub_applied",
62254
- chatKey: statusKey(args.chat_id, undefined),
62271
+ chatKey: statusKey(String(args.chat_id ?? ""), undefined),
62255
62272
  replaced: scrub.replaced,
62256
62273
  site: "edit_message"
62257
62274
  });
@@ -62269,11 +62286,11 @@ async function executeEditMessage(args) {
62269
62286
  editParseMode = undefined;
62270
62287
  editText = editRawText;
62271
62288
  }
62272
- const edited = await robustApiCall(() => lockedBot.api.editMessageText(args.chat_id, Number(args.message_id), editText, ...editParseMode ? [{ parse_mode: editParseMode }] : []));
62289
+ const edited = await robustApiCall(() => lockedBot.api.editMessageText(String(args.chat_id ?? ""), Number(args.message_id), editText, ...editParseMode ? [{ parse_mode: editParseMode }] : []));
62273
62290
  const id = typeof edited === "object" && edited ? edited.message_id : args.message_id;
62274
62291
  if (HISTORY_ENABLED) {
62275
62292
  try {
62276
- recordEdit({ chat_id: args.chat_id, message_id: Number(args.message_id), text: args.text });
62293
+ recordEdit({ chat_id: String(args.chat_id ?? ""), message_id: Number(args.message_id), text: args.text });
62277
62294
  } catch (err) {
62278
62295
  process.stderr.write(`telegram gateway: history recordEdit failed: ${err}
62279
62296
  `);
@@ -62284,7 +62301,7 @@ async function executeEditMessage(args) {
62284
62301
  async function executeSendTyping(args) {
62285
62302
  if (!args.chat_id)
62286
62303
  throw new Error("send_typing: chat_id is required");
62287
- const stChatId = args.chat_id;
62304
+ const stChatId = String(args.chat_id ?? "");
62288
62305
  assertAllowedChat(stChatId);
62289
62306
  const rawAction = args.action;
62290
62307
  let action = "typing";
@@ -62308,7 +62325,7 @@ async function executePinMessage(args) {
62308
62325
  throw new Error("pin_message: chat_id is required");
62309
62326
  if (!args.message_id)
62310
62327
  throw new Error("pin_message: message_id is required");
62311
- const pinChatId = args.chat_id;
62328
+ const pinChatId = String(args.chat_id ?? "");
62312
62329
  assertAllowedChat(pinChatId);
62313
62330
  await robustApiCall(() => lockedBot.api.pinChatMessage(pinChatId, Number(args.message_id)), { chat_id: pinChatId, verb: "pin_message" });
62314
62331
  return { content: [{ type: "text", text: `pinned message ${args.message_id}` }] };
@@ -62318,7 +62335,7 @@ async function executeDeleteMessage(args) {
62318
62335
  throw new Error("delete_message: chat_id is required");
62319
62336
  if (!args.message_id)
62320
62337
  throw new Error("delete_message: message_id is required");
62321
- const delChatId = args.chat_id;
62338
+ const delChatId = String(args.chat_id ?? "");
62322
62339
  const delMessageId = Number(args.message_id);
62323
62340
  assertAllowedChat(delChatId);
62324
62341
  await robustApiCall(() => lockedBot.api.deleteMessage(delChatId, delMessageId), { chat_id: delChatId });
@@ -62339,7 +62356,7 @@ async function executeForwardMessage(args) {
62339
62356
  throw new Error("forward_message: from_chat_id is required");
62340
62357
  if (!args.message_id)
62341
62358
  throw new Error("forward_message: message_id is required");
62342
- const fwdChatId = args.chat_id;
62359
+ const fwdChatId = String(args.chat_id ?? "");
62343
62360
  const fwdFromChatId = args.from_chat_id;
62344
62361
  const fwdMsgId = Number(args.message_id);
62345
62362
  assertAllowedChat(fwdChatId);
@@ -62371,7 +62388,7 @@ async function executeGetRecentMessages(args) {
62371
62388
  }
62372
62389
  if (!args.chat_id)
62373
62390
  throw new Error("get_recent_messages: chat_id is required");
62374
- const chat_id = args.chat_id;
62391
+ const chat_id = String(args.chat_id ?? "");
62375
62392
  assertAllowedChat(chat_id);
62376
62393
  const rawThread = args.message_thread_id;
62377
62394
  let thread_id;
@@ -1030,11 +1030,12 @@ function loadAccess(): Access {
1030
1030
  return BOOT_ACCESS ?? readAccessFile()
1031
1031
  }
1032
1032
 
1033
- function assertAllowedChat(chat_id: string): void {
1033
+ function assertAllowedChat(chat_id: string | number): void {
1034
+ const id = String(chat_id)
1034
1035
  const access = loadAccess()
1035
- if (access.allowFrom.includes(chat_id)) return
1036
- if (chat_id in access.groups) return
1037
- throw new Error(`chat ${chat_id} is not allowlisted — add via /telegram:access`)
1036
+ if (access.allowFrom.includes(id)) return
1037
+ if (id in access.groups) return
1038
+ throw new Error(`chat ${id} is not allowlisted — add via /telegram:access`)
1038
1039
  }
1039
1040
 
1040
1041
  function saveAccess(a: Access): void {
@@ -7834,7 +7835,7 @@ async function executeToolCall(tool: string, args: Record<string, unknown>): Pro
7834
7835
  }
7835
7836
 
7836
7837
  async function executeSendChecklist(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
7837
- const chat_id = args.chat_id as string
7838
+ const chat_id = String(args.chat_id ?? '')
7838
7839
  if (!chat_id) throw new Error('send_checklist: chat_id is required')
7839
7840
  const title = args.title as string | undefined
7840
7841
  if (!title) throw new Error('send_checklist: title is required')
@@ -7922,7 +7923,7 @@ async function executeLinearAgentSetup(args: Record<string, unknown>): Promise<{
7922
7923
  }
7923
7924
 
7924
7925
  async function executeUpdateChecklist(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
7925
- const chat_id = args.chat_id as string
7926
+ const chat_id = String(args.chat_id ?? '')
7926
7927
  if (!chat_id) throw new Error('update_checklist: chat_id is required')
7927
7928
  const message_id = args.message_id as string | undefined
7928
7929
  if (!message_id) throw new Error('update_checklist: message_id is required')
@@ -7971,7 +7972,7 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
7971
7972
  // module-scope currentTurn, which a future refactor could let roll over
7972
7973
  // mid-call.
7973
7974
  const turn = currentTurn
7974
- const chat_id = args.chat_id as string
7975
+ const chat_id = String(args.chat_id ?? '')
7975
7976
  if (!chat_id) throw new Error('reply: chat_id is required')
7976
7977
  const rawText = args.text as string | undefined
7977
7978
  if (rawText == null || rawText === '') throw new Error('reply: text is required and cannot be empty')
@@ -8997,7 +8998,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
8997
8998
  args.text = scrub.scrubbed
8998
8999
  emitRuntimeMetric({
8999
9000
  kind: 'voice_scrub_applied',
9000
- chatKey: statusKey(args.chat_id as string, args.message_thread_id != null
9001
+ chatKey: statusKey(String(args.chat_id ?? ''), args.message_thread_id != null
9001
9002
  ? Number(args.message_thread_id) : undefined),
9002
9003
  replaced: scrub.replaced,
9003
9004
  site: 'stream_reply',
@@ -9012,7 +9013,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9012
9013
  // Only check on done=true (the terminal call); intermediate
9013
9014
  // streaming chunks are progress edits, not full sends.
9014
9015
  if (args.done === true) {
9015
- const sChatId = args.chat_id as string
9016
+ const sChatId = String(args.chat_id ?? '')
9016
9017
  const sThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
9017
9018
  const sText = args.text as string
9018
9019
  const dup = outboundDedup.check(sChatId, sThreadId, sText, Date.now(), currentTurn?.registryKey ?? null)
@@ -9028,7 +9029,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9028
9029
  const access = loadAccess()
9029
9030
  // Detect chat type for throttle-default selection.
9030
9031
  // Private (DM) chats have positive numeric IDs; groups/channels are negative.
9031
- const streamChatId = args.chat_id as string
9032
+ const streamChatId = String(args.chat_id ?? '')
9032
9033
  const streamIsPrivate = isDmChatId(streamChatId)
9033
9034
  const streamIsForumTopic = args.message_thread_id != null && args.message_thread_id !== ''
9034
9035
  // Pre-allocated draft handoff removed in #553 PR 5 — draft-stream
@@ -9192,7 +9193,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9192
9193
  if (result.messageId != null) {
9193
9194
  try {
9194
9195
  progressDriver?.recordOutboundDelivered(
9195
- args.chat_id as string,
9196
+ String(args.chat_id ?? ''),
9196
9197
  args.message_thread_id as string | undefined,
9197
9198
  )
9198
9199
  } catch { /* best-effort signal */ }
@@ -9207,7 +9208,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9207
9208
  ? Number(args.message_thread_id)
9208
9209
  : undefined
9209
9210
  signalTracker.noteSignal(
9210
- statusKey(args.chat_id as string, threadIdNum),
9211
+ statusKey(String(args.chat_id ?? ''), threadIdNum),
9211
9212
  Date.now(),
9212
9213
  )
9213
9214
  } catch { /* best-effort signal */ }
@@ -9220,13 +9221,13 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9220
9221
  && streamButtonMeta != null
9221
9222
  && streamButtonMeta.size > 0
9222
9223
  ) {
9223
- rememberAgentButtonMeta(args.chat_id as string, result.messageId, streamButtonMeta)
9224
+ rememberAgentButtonMeta(String(args.chat_id ?? ''), result.messageId, streamButtonMeta)
9224
9225
  }
9225
9226
  // #546 dedup record: capture the final stream_reply text on the
9226
9227
  // terminal call so a subsequent retry (different bridge, same
9227
9228
  // content) lands as a no-op instead of a second message.
9228
9229
  if (args.done === true && result.messageId != null) {
9229
- const sChatId = args.chat_id as string
9230
+ const sChatId = String(args.chat_id ?? '')
9230
9231
  const sThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
9231
9232
  outboundDedup.record(sChatId, sThreadId, args.text as string, Date.now(), currentTurn?.registryKey ?? null)
9232
9233
  // #1445 cross-turn pending-async ambient. The terminal stream_reply
@@ -9306,7 +9307,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9306
9307
  // (stream-reply-handler.ts:305) at this point — earlier failures
9307
9308
  // throw or return before reaching here.
9308
9309
  {
9309
- const sChat = args.chat_id as string
9310
+ const sChat = String(args.chat_id ?? '')
9310
9311
  const sThread = resolveThreadId(sChat, args.message_thread_id as string | undefined)
9311
9312
  // Component 1: pass the turn (finalAnswerDelivered set above for a
9312
9313
  // final stream emit). Interim stream chunks leave it false → no
@@ -9326,7 +9327,7 @@ async function executeProgressUpdate(args: Record<string, unknown>): Promise<unk
9326
9327
  if (!args.chat_id) throw new Error('progress_update: chat_id is required')
9327
9328
  if (!args.text) throw new Error('progress_update: text is required')
9328
9329
 
9329
- const chat_id = args.chat_id as string
9330
+ const chat_id = String(args.chat_id ?? '')
9330
9331
  let text = args.text as string
9331
9332
  const threadId = resolveThreadId(chat_id, args.message_thread_id as string | undefined)
9332
9333
  const key = statusKey(chat_id, threadId)
@@ -9810,7 +9811,7 @@ function renderVaultRequestSaveCard(req: PendingVaultRequestSave, agentSlug: str
9810
9811
  * written to vault only on user tap. See #969 P1a.
9811
9812
  */
9812
9813
  async function executeVaultRequestSave(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
9813
- const chat_id = args.chat_id as string
9814
+ const chat_id = String(args.chat_id ?? '')
9814
9815
  if (!chat_id) throw new Error('vault_request_save: chat_id is required')
9815
9816
  const key = args.key as string
9816
9817
  if (!key || typeof key !== 'string') throw new Error('vault_request_save: key is required')
@@ -9954,7 +9955,7 @@ function renderSecretRequestCard(req: PendingSecretRequest): string {
9954
9955
  * capture (the operator's next message after they tap [Provide securely]).
9955
9956
  */
9956
9957
  async function executeRequestSecret(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
9957
- const chat_id = args.chat_id as string
9958
+ const chat_id = String(args.chat_id ?? '')
9958
9959
  if (!chat_id) throw new Error('request_secret: chat_id is required')
9959
9960
  const key = args.key as string
9960
9961
  if (!key || typeof key !== 'string') throw new Error('request_secret: key is required')
@@ -10238,7 +10239,7 @@ function renderVaultRequestAccessCard(req: PendingVaultRequestAccess): string {
10238
10239
  * the agent itself can only REQUEST.
10239
10240
  */
10240
10241
  async function executeVaultRequestAccess(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
10241
- const chat_id = args.chat_id as string
10242
+ const chat_id = String(args.chat_id ?? '')
10242
10243
  if (!chat_id) throw new Error('vault_request_access: chat_id is required')
10243
10244
  const key = args.key as string
10244
10245
  if (!key || typeof key !== 'string') throw new Error('vault_request_access: key is required')
@@ -10356,8 +10357,8 @@ async function executeReact(args: Record<string, unknown>): Promise<unknown> {
10356
10357
  if (!args.chat_id) throw new Error('react: chat_id is required')
10357
10358
  if (!args.message_id) throw new Error('react: message_id is required')
10358
10359
  if (!args.emoji) throw new Error('react: emoji is required')
10359
- assertAllowedChat(args.chat_id as string)
10360
- await lockedBot.api.setMessageReaction(args.chat_id as string, Number(args.message_id), [
10360
+ assertAllowedChat(String(args.chat_id ?? ''))
10361
+ await lockedBot.api.setMessageReaction(String(args.chat_id ?? ''), Number(args.message_id), [
10361
10362
  { type: 'emoji', emoji: args.emoji as ReactionTypeEmoji['emoji'] },
10362
10363
  ])
10363
10364
  return { content: [{ type: 'text', text: 'reacted' }] }
@@ -10401,7 +10402,7 @@ async function executeEditMessage(args: Record<string, unknown>): Promise<unknow
10401
10402
  if (!args.chat_id) throw new Error('edit_message: chat_id is required')
10402
10403
  if (!args.message_id) throw new Error('edit_message: message_id is required')
10403
10404
  if (args.text == null || args.text === '') throw new Error('edit_message: text is required and cannot be empty')
10404
- assertAllowedChat(args.chat_id as string)
10405
+ assertAllowedChat(String(args.chat_id ?? ''))
10405
10406
  const editAccess = loadAccess()
10406
10407
  const editConfigMode = editAccess.parseMode ?? 'html'
10407
10408
  const editFormat = (args.format as string | undefined) ?? editConfigMode
@@ -10419,7 +10420,7 @@ async function executeEditMessage(args: Record<string, unknown>): Promise<unknow
10419
10420
  editRawText = scrub.scrubbed
10420
10421
  emitRuntimeMetric({
10421
10422
  kind: 'voice_scrub_applied',
10422
- chatKey: statusKey(args.chat_id as string, undefined),
10423
+ chatKey: statusKey(String(args.chat_id ?? ''), undefined),
10423
10424
  replaced: scrub.replaced,
10424
10425
  site: 'edit_message',
10425
10426
  })
@@ -10439,14 +10440,14 @@ async function executeEditMessage(args: Record<string, unknown>): Promise<unknow
10439
10440
  }
10440
10441
  const edited = await robustApiCall(
10441
10442
  () => lockedBot.api.editMessageText(
10442
- args.chat_id as string, Number(args.message_id), editText,
10443
+ String(args.chat_id ?? ''), Number(args.message_id), editText,
10443
10444
  ...(editParseMode ? [{ parse_mode: editParseMode }] : []),
10444
10445
  ),
10445
10446
  )
10446
10447
  const id = typeof edited === 'object' && edited ? (edited as any).message_id : args.message_id
10447
10448
  if (HISTORY_ENABLED) {
10448
10449
  try {
10449
- recordEdit({ chat_id: args.chat_id as string, message_id: Number(args.message_id), text: args.text as string })
10450
+ recordEdit({ chat_id: String(args.chat_id ?? ''), message_id: Number(args.message_id), text: args.text as string })
10450
10451
  } catch (err) {
10451
10452
  process.stderr.write(`telegram gateway: history recordEdit failed: ${err}\n`)
10452
10453
  }
@@ -10456,7 +10457,7 @@ async function executeEditMessage(args: Record<string, unknown>): Promise<unknow
10456
10457
 
10457
10458
  async function executeSendTyping(args: Record<string, unknown>): Promise<unknown> {
10458
10459
  if (!args.chat_id) throw new Error('send_typing: chat_id is required')
10459
- const stChatId = args.chat_id as string
10460
+ const stChatId = String(args.chat_id ?? '')
10460
10461
  assertAllowedChat(stChatId)
10461
10462
  // #273: granular chat actions. Default 'typing' preserves the
10462
10463
  // existing tool semantics; agents can opt in to upload_document,
@@ -10490,7 +10491,7 @@ async function executeSendTyping(args: Record<string, unknown>): Promise<unknown
10490
10491
  async function executePinMessage(args: Record<string, unknown>): Promise<unknown> {
10491
10492
  if (!args.chat_id) throw new Error('pin_message: chat_id is required')
10492
10493
  if (!args.message_id) throw new Error('pin_message: message_id is required')
10493
- const pinChatId = args.chat_id as string
10494
+ const pinChatId = String(args.chat_id ?? '')
10494
10495
  assertAllowedChat(pinChatId)
10495
10496
  // #1075: wrap through robustApiCall so flood-wait / transient network
10496
10497
  // errors are retried. THREAD_NOT_FOUND on a stale topic surfaces to the
@@ -10506,7 +10507,7 @@ async function executePinMessage(args: Record<string, unknown>): Promise<unknown
10506
10507
  async function executeDeleteMessage(args: Record<string, unknown>): Promise<unknown> {
10507
10508
  if (!args.chat_id) throw new Error('delete_message: chat_id is required')
10508
10509
  if (!args.message_id) throw new Error('delete_message: message_id is required')
10509
- const delChatId = args.chat_id as string
10510
+ const delChatId = String(args.chat_id ?? '')
10510
10511
  const delMessageId = Number(args.message_id)
10511
10512
  assertAllowedChat(delChatId)
10512
10513
  await robustApiCall(() => lockedBot.api.deleteMessage(delChatId, delMessageId), { chat_id: delChatId })
@@ -10522,7 +10523,7 @@ async function executeForwardMessage(args: Record<string, unknown>): Promise<unk
10522
10523
  if (!args.chat_id) throw new Error('forward_message: chat_id is required')
10523
10524
  if (!args.from_chat_id) throw new Error('forward_message: from_chat_id is required')
10524
10525
  if (!args.message_id) throw new Error('forward_message: message_id is required')
10525
- const fwdChatId = args.chat_id as string
10526
+ const fwdChatId = String(args.chat_id ?? '')
10526
10527
  const fwdFromChatId = args.from_chat_id as string
10527
10528
  const fwdMsgId = Number(args.message_id)
10528
10529
  assertAllowedChat(fwdChatId)
@@ -10556,7 +10557,7 @@ async function executeGetRecentMessages(args: Record<string, unknown>): Promise<
10556
10557
  }
10557
10558
  }
10558
10559
  if (!args.chat_id) throw new Error('get_recent_messages: chat_id is required')
10559
- const chat_id = args.chat_id as string
10560
+ const chat_id = String(args.chat_id ?? '')
10560
10561
  assertAllowedChat(chat_id)
10561
10562
  const rawThread = args.message_thread_id as string | undefined
10562
10563
  let thread_id: number | null | undefined
@@ -144,12 +144,14 @@ const PERSIST_NOTE =
144
144
  '<i>Session-only — lasts until restart. To persist, set <code>model:</code> in switchroom.yaml and restart.</i>'
145
145
 
146
146
  function helpText(deps: ModelCommandDeps, reason?: string): ModelCommandReply {
147
+ const srAliasExamples = Object.keys(SR_MODEL_ALIASES).map(a => `<code>${a}</code>`).join(' · ')
147
148
  const lines: string[] = []
148
149
  if (reason) lines.push(`⚠️ ${deps.escapeHtml(reason)}`)
149
150
  lines.push(
150
151
  '<b>/model</b> — show or switch the Claude model',
151
152
  '<code>/model</code> — show the configured model',
152
153
  `<code>/model &lt;name&gt;</code> — switch the live session (${MODEL_ALIASES.map(a => `<code>${a}</code>`).join(' · ')} or a full model id)`,
154
+ `<i>OpenRouter shortcuts:</i> ${srAliasExamples}`,
153
155
  PERSIST_NOTE,
154
156
  )
155
157
  return { text: lines.join('\n'), html: true }
@@ -164,11 +166,13 @@ export async function handleModelCommand(
164
166
  if (parsed.kind === 'show') {
165
167
  const configured = deps.getConfiguredModel()
166
168
  const shown = configured && configured.length > 0 ? configured : 'default'
169
+ const srAliasExamples = Object.keys(SR_MODEL_ALIASES).map(a => `<code>/model ${a}</code>`).join(' · ')
167
170
  return {
168
171
  text: [
169
172
  `<b>Model — ${deps.escapeHtml(deps.getAgentName())}</b>`,
170
173
  `Configured: <code>${deps.escapeHtml(shown)}</code>`,
171
174
  `Switch the live session: ${MODEL_ALIASES.map(a => `<code>/model ${a}</code>`).join(' · ')}`,
175
+ `OpenRouter shortcuts: ${srAliasExamples}`,
172
176
  'or <code>/model &lt;full-model-id&gt;</code>',
173
177
  PERSIST_NOTE,
174
178
  ].join('\n'),
@@ -182,15 +186,18 @@ export async function handleModelCommand(
182
186
  return helpText(deps, `not a valid model name: ${parsed.model}`)
183
187
  }
184
188
 
189
+ // Expand short aliases: `flash` → `sr-gemini-2.5-flash`, `codex` → `sr-codex-5.5`, etc.
190
+ const model = expandSrAlias(parsed.model)
191
+
185
192
  // sr-* → Claude: an in-place `/model` inject would leave LiteLLM routing
186
193
  // active in the live session because the sr-* model context was set by
187
194
  // the proxy at session start, not by claude's own REPL. A graceful restart
188
195
  // is the only clean path back to the native OAuth route. This matches the
189
196
  // behaviour of the `/restart` command (same mechanism, same marker logic).
190
197
  const currentSession = deps.getActiveSessionModel()
191
- if (currentSession !== null && isSrModel(currentSession) && isClaudeModel(parsed.model)) {
198
+ if (currentSession !== null && isSrModel(currentSession) && isClaudeModel(model)) {
192
199
  try {
193
- await deps.scheduleRestart(`user: /model ${parsed.model} (sr-to-claude restart)`)
200
+ await deps.scheduleRestart(`user: /model ${model} (sr-to-claude restart)`)
194
201
  } catch (err) {
195
202
  const msg = err instanceof Error ? err.message : String(err)
196
203
  return {
@@ -207,10 +214,10 @@ export async function handleModelCommand(
207
214
  }
208
215
  }
209
216
 
210
- const verbHtml = `<code>/model ${deps.escapeHtml(parsed.model)}</code>`
217
+ const verbHtml = `<code>/model ${deps.escapeHtml(model)}</code>`
211
218
  let result: InjectResult
212
219
  try {
213
- result = await deps.inject(deps.getAgentName(), `/model ${parsed.model}`)
220
+ result = await deps.inject(deps.getAgentName(), `/model ${model}`)
214
221
  } catch (err) {
215
222
  const msg = err instanceof Error ? err.message : String(err)
216
223
  return {
@@ -315,6 +322,27 @@ export const SR_MODEL_LABELS: Record<string, string> = {
315
322
  'sr-deepseek-r1': 'DeepSeek R1',
316
323
  'sr-deepseek-v3': 'DeepSeek V3',
317
324
  'sr-glm-5': 'GLM-5',
325
+ 'sr-codex-5.5': 'Codex 5.5',
326
+ }
327
+
328
+ /**
329
+ * Short text-command aliases for sr-* models. These let the operator type
330
+ * `/model flash`, `/model codex`, etc. instead of the full `sr-*` id.
331
+ * Expanded in handleModelCommand before injection; the full sr-* id is what
332
+ * reaches the agent session and LiteLLM.
333
+ */
334
+ export const SR_MODEL_ALIASES: Record<string, string> = {
335
+ flash: 'sr-gemini-2.5-flash',
336
+ gemini: 'sr-gemini-2.5-pro',
337
+ deepseek: 'sr-deepseek-v3',
338
+ r1: 'sr-deepseek-r1',
339
+ glm: 'sr-glm-5',
340
+ codex: 'sr-codex-5.5',
341
+ }
342
+
343
+ /** Expand a short alias (case-insensitive) to its full sr-* id, or return the original. */
344
+ export function expandSrAlias(arg: string): string {
345
+ return SR_MODEL_ALIASES[arg.toLowerCase()] ?? arg
318
346
  }
319
347
 
320
348
  function srFriendlyLabel(srName: string): string {
@@ -13,7 +13,7 @@
13
13
  * REPL verb — already on the inject allowlist) and relays the
14
14
  * captured output, with the session-only persistence caveat.
15
15
  */
16
- import { describe, it, expect } from "vitest";
16
+ import { describe, it, expect, beforeAll } from "vitest";
17
17
  import {
18
18
  parseModelCommand,
19
19
  handleModelCommand,
@@ -344,6 +344,60 @@ describe("inject allowlist contract", () => {
344
344
  // Picker-driven menu (v2) — buildModelMenu + handleModelMenuCallback
345
345
  // ---------------------------------------------------------------------------
346
346
 
347
+ describe("SR_MODEL_ALIASES / expandSrAlias", () => {
348
+ let expandSrAlias: (arg: string) => string;
349
+ let SR_MODEL_ALIASES: Record<string, string>;
350
+
351
+ beforeAll(async () => {
352
+ const mod = await import("../gateway/model-command.js");
353
+ expandSrAlias = mod.expandSrAlias;
354
+ SR_MODEL_ALIASES = mod.SR_MODEL_ALIASES;
355
+ });
356
+
357
+ it("expands known short aliases to full sr-* ids", () => {
358
+ expect(expandSrAlias("flash")).toBe("sr-gemini-2.5-flash");
359
+ expect(expandSrAlias("gemini")).toBe("sr-gemini-2.5-pro");
360
+ expect(expandSrAlias("deepseek")).toBe("sr-deepseek-v3");
361
+ expect(expandSrAlias("r1")).toBe("sr-deepseek-r1");
362
+ expect(expandSrAlias("glm")).toBe("sr-glm-5");
363
+ expect(expandSrAlias("codex")).toBe("sr-codex-5.5");
364
+ });
365
+
366
+ it("is case-insensitive", () => {
367
+ expect(expandSrAlias("Flash")).toBe("sr-gemini-2.5-flash");
368
+ expect(expandSrAlias("CODEX")).toBe("sr-codex-5.5");
369
+ });
370
+
371
+ it("passes through unknown names unchanged", () => {
372
+ expect(expandSrAlias("opus")).toBe("opus");
373
+ expect(expandSrAlias("sr-gemini-2.5-flash")).toBe("sr-gemini-2.5-flash");
374
+ expect(expandSrAlias("claude-opus-4-8")).toBe("claude-opus-4-8");
375
+ });
376
+
377
+ it("every alias target is a valid sr-* model arg", () => {
378
+ for (const [alias, target] of Object.entries(SR_MODEL_ALIASES)) {
379
+ expect(target.startsWith("sr-"), `${alias} → ${target} must start with sr-`).toBe(true);
380
+ }
381
+ });
382
+
383
+ it("handleModelCommand injects expanded sr-* id, not the short alias", async () => {
384
+ const { deps, calls } = makeDeps({ getActiveSessionModel: () => null });
385
+ await handleModelCommand({ kind: "set", model: "flash" }, deps);
386
+ expect(calls).toHaveLength(1);
387
+ expect(calls[0].command).toBe("/model sr-gemini-2.5-flash");
388
+ });
389
+
390
+ it("handleModelCommand with alias schedules restart when session is on sr-*", async () => {
391
+ const { deps, calls, restartCalls } = makeDeps({
392
+ getActiveSessionModel: () => "sr-deepseek-v3",
393
+ });
394
+ await handleModelCommand({ kind: "set", model: "opus" }, deps);
395
+ expect(calls).toHaveLength(0);
396
+ expect(restartCalls).toHaveLength(1);
397
+ expect(restartCalls[0]).toContain("opus");
398
+ });
399
+ });
400
+
347
401
  import {
348
402
  buildModelMenu,
349
403
  handleModelMenuCallback,
@@ -42,9 +42,11 @@ describe("uat: /model sr-* LiteLLM routing — section headers + session switch
42
42
  const sc = await spinUp({ agent: AGENT });
43
43
  try {
44
44
  await sc.sendDM("/model");
45
+ // 60s — if an obligation turn is being processed when /model lands,
46
+ // the menu may be buffered until the turn drains (~40s max).
45
47
  const menu = await sc.expectMessage(/Default \(new sessions\):/i, {
46
48
  from: "bot",
47
- timeout: 30_000,
49
+ timeout: 60_000,
48
50
  });
49
51
 
50
52
  // ── 1. Section headers ──────────────────────────────────────────