switchroom 0.16.10 → 0.16.12

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.12";
51678
+ var COMMIT_SHA = "4cab85e1";
51679
51679
 
51680
51680
  // src/cli/resolve-version.ts
51681
51681
  function readPackageVersion() {
@@ -77783,6 +77783,13 @@ function extractBearerToken(req) {
77783
77783
  if (idx >= 0 && idx + 1 < parts.length)
77784
77784
  return parts[idx + 1];
77785
77785
  }
77786
+ const url = new URL(req.url);
77787
+ const queryToken = url.searchParams.get("token");
77788
+ if (queryToken)
77789
+ return queryToken;
77790
+ const hermesHeader = req.headers.get("X-Hermes-Session-Token");
77791
+ if (hermesHeader)
77792
+ return hermesHeader;
77786
77793
  return null;
77787
77794
  }
77788
77795
  function checkAuth(req, token, server) {
@@ -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.12";
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.12",
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
  }
@@ -50046,7 +50062,8 @@ function buildObligationRepresentInbound(o, now) {
50046
50062
  meta: {
50047
50063
  source: "obligation_represent",
50048
50064
  origin_turn_id: o.originTurnId,
50049
- represent_count: String(o.representCount + 1)
50065
+ represent_count: String(o.representCount + 1),
50066
+ chat_id: o.chatId
50050
50067
  }
50051
50068
  };
50052
50069
  }
@@ -56047,10 +56064,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
56047
56064
  }
56048
56065
 
56049
56066
  // ../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;
56067
+ var VERSION = "0.16.12";
56068
+ var COMMIT_SHA = "4cab85e1";
56069
+ var COMMIT_DATE = "2026-06-28T11:11:09Z";
56070
+ var LATEST_PR = 2634;
56054
56071
  var COMMITS_AHEAD_OF_TAG = 0;
56055
56072
 
56056
56073
  // gateway/boot-version.ts
@@ -57709,12 +57726,13 @@ function loadAccess() {
57709
57726
  return BOOT_ACCESS ?? readAccessFile();
57710
57727
  }
57711
57728
  function assertAllowedChat(chat_id) {
57729
+ const id = String(chat_id);
57712
57730
  const access = loadAccess();
57713
- if (access.allowFrom.includes(chat_id))
57731
+ if (access.allowFrom.includes(id))
57714
57732
  return;
57715
- if (chat_id in access.groups)
57733
+ if (id in access.groups)
57716
57734
  return;
57717
- throw new Error(`chat ${chat_id} is not allowlisted \u2014 add via /telegram:access`);
57735
+ throw new Error(`chat ${id} is not allowlisted \u2014 add via /telegram:access`);
57718
57736
  }
57719
57737
  function saveAccess(a) {
57720
57738
  if (STATIC)
@@ -60688,7 +60706,7 @@ async function executeToolCall(tool, args) {
60688
60706
  }
60689
60707
  }
60690
60708
  async function executeSendChecklist(args) {
60691
- const chat_id = args.chat_id;
60709
+ const chat_id = String(args.chat_id ?? "");
60692
60710
  if (!chat_id)
60693
60711
  throw new Error("send_checklist: chat_id is required");
60694
60712
  const title = args.title;
@@ -60754,7 +60772,7 @@ async function executeLinearAgentSetup(args) {
60754
60772
  return runLinearAgentSetup(args);
60755
60773
  }
60756
60774
  async function executeUpdateChecklist(args) {
60757
- const chat_id = args.chat_id;
60775
+ const chat_id = String(args.chat_id ?? "");
60758
60776
  if (!chat_id)
60759
60777
  throw new Error("update_checklist: chat_id is required");
60760
60778
  const message_id = args.message_id;
@@ -60778,9 +60796,20 @@ function redactOutboundText(text2, site) {
60778
60796
  }
60779
60797
  async function executeReply(args) {
60780
60798
  const turn = currentTurn;
60781
- const chat_id = args.chat_id;
60782
- if (!chat_id)
60799
+ const _rawChatId = String(args.chat_id ?? "");
60800
+ if (!_rawChatId)
60783
60801
  throw new Error("reply: chat_id is required");
60802
+ const chat_id = (() => {
60803
+ const _a = loadAccess();
60804
+ if (_a.allowFrom.includes(_rawChatId) || _rawChatId in _a.groups)
60805
+ return _rawChatId;
60806
+ if (turn?.sessionChatId && (_a.allowFrom.includes(turn.sessionChatId) || (turn.sessionChatId in _a.groups))) {
60807
+ process.stderr.write(`telegram gateway: reply: model passed chat_id "${_rawChatId}" (not allowlisted) \u2014 ` + `routing to active turn chat "${turn.sessionChatId}"
60808
+ `);
60809
+ return turn.sessionChatId;
60810
+ }
60811
+ return _rawChatId;
60812
+ })();
60784
60813
  const rawText = args.text;
60785
60814
  if (rawText == null || rawText === "")
60786
60815
  throw new Error("reply: text is required and cannot be empty");
@@ -61315,14 +61344,14 @@ async function executeStreamReply(args) {
61315
61344
  args.text = scrub.scrubbed;
61316
61345
  emitRuntimeMetric({
61317
61346
  kind: "voice_scrub_applied",
61318
- chatKey: statusKey(args.chat_id, args.message_thread_id != null ? Number(args.message_thread_id) : undefined),
61347
+ chatKey: statusKey(String(args.chat_id ?? ""), args.message_thread_id != null ? Number(args.message_thread_id) : undefined),
61319
61348
  replaced: scrub.replaced,
61320
61349
  site: "stream_reply"
61321
61350
  });
61322
61351
  }
61323
61352
  }
61324
61353
  if (args.done === true) {
61325
- const sChatId = args.chat_id;
61354
+ const sChatId = String(args.chat_id ?? "");
61326
61355
  const sThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
61327
61356
  const sText = args.text;
61328
61357
  const dup = outboundDedup.check(sChatId, sThreadId, sText, Date.now(), currentTurn?.registryKey ?? null);
@@ -61333,7 +61362,7 @@ async function executeStreamReply(args) {
61333
61362
  }
61334
61363
  }
61335
61364
  const access = loadAccess();
61336
- const streamChatId = args.chat_id;
61365
+ const streamChatId = String(args.chat_id ?? "");
61337
61366
  const streamIsPrivate = isDmChatId(streamChatId);
61338
61367
  const streamIsForumTopic = args.message_thread_id != null && args.message_thread_id !== "";
61339
61368
  let streamReplyMarkup;
@@ -61433,18 +61462,18 @@ async function executeStreamReply(args) {
61433
61462
  });
61434
61463
  if (result.messageId != null) {
61435
61464
  try {
61436
- progressDriver?.recordOutboundDelivered(args.chat_id, args.message_thread_id);
61465
+ progressDriver?.recordOutboundDelivered(String(args.chat_id ?? ""), args.message_thread_id);
61437
61466
  } catch {}
61438
61467
  try {
61439
61468
  const threadIdNum2 = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
61440
- noteSignal(statusKey(args.chat_id, threadIdNum2), Date.now());
61469
+ noteSignal(statusKey(String(args.chat_id ?? ""), threadIdNum2), Date.now());
61441
61470
  } catch {}
61442
61471
  }
61443
61472
  if (args.done === true && result.messageId != null && streamButtonMeta != null && streamButtonMeta.size > 0) {
61444
- rememberAgentButtonMeta(args.chat_id, result.messageId, streamButtonMeta);
61473
+ rememberAgentButtonMeta(String(args.chat_id ?? ""), result.messageId, streamButtonMeta);
61445
61474
  }
61446
61475
  if (args.done === true && result.messageId != null) {
61447
- const sChatId = args.chat_id;
61476
+ const sChatId = String(args.chat_id ?? "");
61448
61477
  const sThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
61449
61478
  outboundDedup.record(sChatId, sThreadId, args.text, Date.now(), currentTurn?.registryKey ?? null);
61450
61479
  const streamFormat = args.format ?? (access.parseMode ?? "html");
@@ -61477,7 +61506,7 @@ async function executeStreamReply(args) {
61477
61506
  clearSilentEndState(statusKey(streamChatId, streamThreadIdForClear));
61478
61507
  }
61479
61508
  {
61480
- const sChat = args.chat_id;
61509
+ const sChat = String(args.chat_id ?? "");
61481
61510
  const sThread = resolveThreadId(sChat, args.message_thread_id);
61482
61511
  releaseTurnBufferGate(statusKey(sChat, sThread), turn ?? undefined);
61483
61512
  if (turn?.finalAnswerDelivered === true) {
@@ -61491,7 +61520,7 @@ async function executeProgressUpdate(args) {
61491
61520
  throw new Error("progress_update: chat_id is required");
61492
61521
  if (!args.text)
61493
61522
  throw new Error("progress_update: text is required");
61494
- const chat_id = args.chat_id;
61523
+ const chat_id = String(args.chat_id ?? "");
61495
61524
  let text2 = args.text;
61496
61525
  const threadId = resolveThreadId(chat_id, args.message_thread_id);
61497
61526
  const key = statusKey(chat_id, threadId);
@@ -61816,7 +61845,7 @@ function renderVaultRequestSaveCard(req, agentSlug) {
61816
61845
  `);
61817
61846
  }
61818
61847
  async function executeVaultRequestSave(args) {
61819
- const chat_id = args.chat_id;
61848
+ const chat_id = String(args.chat_id ?? "");
61820
61849
  if (!chat_id)
61821
61850
  throw new Error("vault_request_save: chat_id is required");
61822
61851
  const key = args.key;
@@ -61904,7 +61933,7 @@ function renderSecretRequestCard(req) {
61904
61933
  `);
61905
61934
  }
61906
61935
  async function executeRequestSecret(args) {
61907
- const chat_id = args.chat_id;
61936
+ const chat_id = String(args.chat_id ?? "");
61908
61937
  if (!chat_id)
61909
61938
  throw new Error("request_secret: chat_id is required");
61910
61939
  const key = args.key;
@@ -62110,7 +62139,7 @@ function renderVaultRequestAccessCard(req) {
62110
62139
  `);
62111
62140
  }
62112
62141
  async function executeVaultRequestAccess(args) {
62113
- const chat_id = args.chat_id;
62142
+ const chat_id = String(args.chat_id ?? "");
62114
62143
  if (!chat_id)
62115
62144
  throw new Error("vault_request_access: chat_id is required");
62116
62145
  const key = args.key;
@@ -62195,8 +62224,8 @@ async function executeReact(args) {
62195
62224
  throw new Error("react: message_id is required");
62196
62225
  if (!args.emoji)
62197
62226
  throw new Error("react: emoji is required");
62198
- assertAllowedChat(args.chat_id);
62199
- await lockedBot.api.setMessageReaction(args.chat_id, Number(args.message_id), [
62227
+ assertAllowedChat(String(args.chat_id ?? ""));
62228
+ await lockedBot.api.setMessageReaction(String(args.chat_id ?? ""), Number(args.message_id), [
62200
62229
  { type: "emoji", emoji: args.emoji }
62201
62230
  ]);
62202
62231
  return { content: [{ type: "text", text: "reacted" }] };
@@ -62239,7 +62268,7 @@ async function executeEditMessage(args) {
62239
62268
  throw new Error("edit_message: message_id is required");
62240
62269
  if (args.text == null || args.text === "")
62241
62270
  throw new Error("edit_message: text is required and cannot be empty");
62242
- assertAllowedChat(args.chat_id);
62271
+ assertAllowedChat(String(args.chat_id ?? ""));
62243
62272
  const editAccess = loadAccess();
62244
62273
  const editConfigMode = editAccess.parseMode ?? "html";
62245
62274
  const editFormat = args.format ?? editConfigMode;
@@ -62251,7 +62280,7 @@ async function executeEditMessage(args) {
62251
62280
  editRawText = scrub.scrubbed;
62252
62281
  emitRuntimeMetric({
62253
62282
  kind: "voice_scrub_applied",
62254
- chatKey: statusKey(args.chat_id, undefined),
62283
+ chatKey: statusKey(String(args.chat_id ?? ""), undefined),
62255
62284
  replaced: scrub.replaced,
62256
62285
  site: "edit_message"
62257
62286
  });
@@ -62269,11 +62298,11 @@ async function executeEditMessage(args) {
62269
62298
  editParseMode = undefined;
62270
62299
  editText = editRawText;
62271
62300
  }
62272
- const edited = await robustApiCall(() => lockedBot.api.editMessageText(args.chat_id, Number(args.message_id), editText, ...editParseMode ? [{ parse_mode: editParseMode }] : []));
62301
+ const edited = await robustApiCall(() => lockedBot.api.editMessageText(String(args.chat_id ?? ""), Number(args.message_id), editText, ...editParseMode ? [{ parse_mode: editParseMode }] : []));
62273
62302
  const id = typeof edited === "object" && edited ? edited.message_id : args.message_id;
62274
62303
  if (HISTORY_ENABLED) {
62275
62304
  try {
62276
- recordEdit({ chat_id: args.chat_id, message_id: Number(args.message_id), text: args.text });
62305
+ recordEdit({ chat_id: String(args.chat_id ?? ""), message_id: Number(args.message_id), text: args.text });
62277
62306
  } catch (err) {
62278
62307
  process.stderr.write(`telegram gateway: history recordEdit failed: ${err}
62279
62308
  `);
@@ -62284,7 +62313,7 @@ async function executeEditMessage(args) {
62284
62313
  async function executeSendTyping(args) {
62285
62314
  if (!args.chat_id)
62286
62315
  throw new Error("send_typing: chat_id is required");
62287
- const stChatId = args.chat_id;
62316
+ const stChatId = String(args.chat_id ?? "");
62288
62317
  assertAllowedChat(stChatId);
62289
62318
  const rawAction = args.action;
62290
62319
  let action = "typing";
@@ -62308,7 +62337,7 @@ async function executePinMessage(args) {
62308
62337
  throw new Error("pin_message: chat_id is required");
62309
62338
  if (!args.message_id)
62310
62339
  throw new Error("pin_message: message_id is required");
62311
- const pinChatId = args.chat_id;
62340
+ const pinChatId = String(args.chat_id ?? "");
62312
62341
  assertAllowedChat(pinChatId);
62313
62342
  await robustApiCall(() => lockedBot.api.pinChatMessage(pinChatId, Number(args.message_id)), { chat_id: pinChatId, verb: "pin_message" });
62314
62343
  return { content: [{ type: "text", text: `pinned message ${args.message_id}` }] };
@@ -62318,7 +62347,7 @@ async function executeDeleteMessage(args) {
62318
62347
  throw new Error("delete_message: chat_id is required");
62319
62348
  if (!args.message_id)
62320
62349
  throw new Error("delete_message: message_id is required");
62321
- const delChatId = args.chat_id;
62350
+ const delChatId = String(args.chat_id ?? "");
62322
62351
  const delMessageId = Number(args.message_id);
62323
62352
  assertAllowedChat(delChatId);
62324
62353
  await robustApiCall(() => lockedBot.api.deleteMessage(delChatId, delMessageId), { chat_id: delChatId });
@@ -62339,7 +62368,7 @@ async function executeForwardMessage(args) {
62339
62368
  throw new Error("forward_message: from_chat_id is required");
62340
62369
  if (!args.message_id)
62341
62370
  throw new Error("forward_message: message_id is required");
62342
- const fwdChatId = args.chat_id;
62371
+ const fwdChatId = String(args.chat_id ?? "");
62343
62372
  const fwdFromChatId = args.from_chat_id;
62344
62373
  const fwdMsgId = Number(args.message_id);
62345
62374
  assertAllowedChat(fwdChatId);
@@ -62371,7 +62400,7 @@ async function executeGetRecentMessages(args) {
62371
62400
  }
62372
62401
  if (!args.chat_id)
62373
62402
  throw new Error("get_recent_messages: chat_id is required");
62374
- const chat_id = args.chat_id;
62403
+ const chat_id = String(args.chat_id ?? "");
62375
62404
  assertAllowedChat(chat_id);
62376
62405
  const rawThread = args.message_thread_id;
62377
62406
  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,8 +7972,25 @@ 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
- if (!chat_id) throw new Error('reply: chat_id is required')
7975
+ const _rawChatId = String(args.chat_id ?? '')
7976
+ if (!_rawChatId) throw new Error('reply: chat_id is required')
7977
+ // Non-Claude models (e.g. Gemini via LiteLLM sr-* routing) sometimes pass a
7978
+ // chat_id that is not in the allowlist — either an int/string mismatch, or
7979
+ // the model echoing the wrong identifier from context. When the raw value
7980
+ // fails the allowlist check and the active turn has a validated sessionChatId,
7981
+ // fall back to the turn's origin chat so the reply still lands correctly.
7982
+ const chat_id = (() => {
7983
+ const _a = loadAccess()
7984
+ if (_a.allowFrom.includes(_rawChatId) || _rawChatId in _a.groups) return _rawChatId
7985
+ if (turn?.sessionChatId && (_a.allowFrom.includes(turn.sessionChatId) || turn.sessionChatId in _a.groups)) {
7986
+ process.stderr.write(
7987
+ `telegram gateway: reply: model passed chat_id "${_rawChatId}" (not allowlisted) — ` +
7988
+ `routing to active turn chat "${turn.sessionChatId}"\n`,
7989
+ )
7990
+ return turn.sessionChatId
7991
+ }
7992
+ return _rawChatId // let assertAllowedChat below throw the human-readable error
7993
+ })()
7976
7994
  const rawText = args.text as string | undefined
7977
7995
  if (rawText == null || rawText === '') throw new Error('reply: text is required and cannot be empty')
7978
7996
  let text = repairEscapedWhitespace(rawText)
@@ -8997,7 +9015,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
8997
9015
  args.text = scrub.scrubbed
8998
9016
  emitRuntimeMetric({
8999
9017
  kind: 'voice_scrub_applied',
9000
- chatKey: statusKey(args.chat_id as string, args.message_thread_id != null
9018
+ chatKey: statusKey(String(args.chat_id ?? ''), args.message_thread_id != null
9001
9019
  ? Number(args.message_thread_id) : undefined),
9002
9020
  replaced: scrub.replaced,
9003
9021
  site: 'stream_reply',
@@ -9012,7 +9030,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9012
9030
  // Only check on done=true (the terminal call); intermediate
9013
9031
  // streaming chunks are progress edits, not full sends.
9014
9032
  if (args.done === true) {
9015
- const sChatId = args.chat_id as string
9033
+ const sChatId = String(args.chat_id ?? '')
9016
9034
  const sThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
9017
9035
  const sText = args.text as string
9018
9036
  const dup = outboundDedup.check(sChatId, sThreadId, sText, Date.now(), currentTurn?.registryKey ?? null)
@@ -9028,7 +9046,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9028
9046
  const access = loadAccess()
9029
9047
  // Detect chat type for throttle-default selection.
9030
9048
  // Private (DM) chats have positive numeric IDs; groups/channels are negative.
9031
- const streamChatId = args.chat_id as string
9049
+ const streamChatId = String(args.chat_id ?? '')
9032
9050
  const streamIsPrivate = isDmChatId(streamChatId)
9033
9051
  const streamIsForumTopic = args.message_thread_id != null && args.message_thread_id !== ''
9034
9052
  // Pre-allocated draft handoff removed in #553 PR 5 — draft-stream
@@ -9192,7 +9210,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9192
9210
  if (result.messageId != null) {
9193
9211
  try {
9194
9212
  progressDriver?.recordOutboundDelivered(
9195
- args.chat_id as string,
9213
+ String(args.chat_id ?? ''),
9196
9214
  args.message_thread_id as string | undefined,
9197
9215
  )
9198
9216
  } catch { /* best-effort signal */ }
@@ -9207,7 +9225,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9207
9225
  ? Number(args.message_thread_id)
9208
9226
  : undefined
9209
9227
  signalTracker.noteSignal(
9210
- statusKey(args.chat_id as string, threadIdNum),
9228
+ statusKey(String(args.chat_id ?? ''), threadIdNum),
9211
9229
  Date.now(),
9212
9230
  )
9213
9231
  } catch { /* best-effort signal */ }
@@ -9220,13 +9238,13 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9220
9238
  && streamButtonMeta != null
9221
9239
  && streamButtonMeta.size > 0
9222
9240
  ) {
9223
- rememberAgentButtonMeta(args.chat_id as string, result.messageId, streamButtonMeta)
9241
+ rememberAgentButtonMeta(String(args.chat_id ?? ''), result.messageId, streamButtonMeta)
9224
9242
  }
9225
9243
  // #546 dedup record: capture the final stream_reply text on the
9226
9244
  // terminal call so a subsequent retry (different bridge, same
9227
9245
  // content) lands as a no-op instead of a second message.
9228
9246
  if (args.done === true && result.messageId != null) {
9229
- const sChatId = args.chat_id as string
9247
+ const sChatId = String(args.chat_id ?? '')
9230
9248
  const sThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
9231
9249
  outboundDedup.record(sChatId, sThreadId, args.text as string, Date.now(), currentTurn?.registryKey ?? null)
9232
9250
  // #1445 cross-turn pending-async ambient. The terminal stream_reply
@@ -9306,7 +9324,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
9306
9324
  // (stream-reply-handler.ts:305) at this point — earlier failures
9307
9325
  // throw or return before reaching here.
9308
9326
  {
9309
- const sChat = args.chat_id as string
9327
+ const sChat = String(args.chat_id ?? '')
9310
9328
  const sThread = resolveThreadId(sChat, args.message_thread_id as string | undefined)
9311
9329
  // Component 1: pass the turn (finalAnswerDelivered set above for a
9312
9330
  // final stream emit). Interim stream chunks leave it false → no
@@ -9326,7 +9344,7 @@ async function executeProgressUpdate(args: Record<string, unknown>): Promise<unk
9326
9344
  if (!args.chat_id) throw new Error('progress_update: chat_id is required')
9327
9345
  if (!args.text) throw new Error('progress_update: text is required')
9328
9346
 
9329
- const chat_id = args.chat_id as string
9347
+ const chat_id = String(args.chat_id ?? '')
9330
9348
  let text = args.text as string
9331
9349
  const threadId = resolveThreadId(chat_id, args.message_thread_id as string | undefined)
9332
9350
  const key = statusKey(chat_id, threadId)
@@ -9810,7 +9828,7 @@ function renderVaultRequestSaveCard(req: PendingVaultRequestSave, agentSlug: str
9810
9828
  * written to vault only on user tap. See #969 P1a.
9811
9829
  */
9812
9830
  async function executeVaultRequestSave(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
9813
- const chat_id = args.chat_id as string
9831
+ const chat_id = String(args.chat_id ?? '')
9814
9832
  if (!chat_id) throw new Error('vault_request_save: chat_id is required')
9815
9833
  const key = args.key as string
9816
9834
  if (!key || typeof key !== 'string') throw new Error('vault_request_save: key is required')
@@ -9954,7 +9972,7 @@ function renderSecretRequestCard(req: PendingSecretRequest): string {
9954
9972
  * capture (the operator's next message after they tap [Provide securely]).
9955
9973
  */
9956
9974
  async function executeRequestSecret(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
9957
- const chat_id = args.chat_id as string
9975
+ const chat_id = String(args.chat_id ?? '')
9958
9976
  if (!chat_id) throw new Error('request_secret: chat_id is required')
9959
9977
  const key = args.key as string
9960
9978
  if (!key || typeof key !== 'string') throw new Error('request_secret: key is required')
@@ -10238,7 +10256,7 @@ function renderVaultRequestAccessCard(req: PendingVaultRequestAccess): string {
10238
10256
  * the agent itself can only REQUEST.
10239
10257
  */
10240
10258
  async function executeVaultRequestAccess(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
10241
- const chat_id = args.chat_id as string
10259
+ const chat_id = String(args.chat_id ?? '')
10242
10260
  if (!chat_id) throw new Error('vault_request_access: chat_id is required')
10243
10261
  const key = args.key as string
10244
10262
  if (!key || typeof key !== 'string') throw new Error('vault_request_access: key is required')
@@ -10356,8 +10374,8 @@ async function executeReact(args: Record<string, unknown>): Promise<unknown> {
10356
10374
  if (!args.chat_id) throw new Error('react: chat_id is required')
10357
10375
  if (!args.message_id) throw new Error('react: message_id is required')
10358
10376
  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), [
10377
+ assertAllowedChat(String(args.chat_id ?? ''))
10378
+ await lockedBot.api.setMessageReaction(String(args.chat_id ?? ''), Number(args.message_id), [
10361
10379
  { type: 'emoji', emoji: args.emoji as ReactionTypeEmoji['emoji'] },
10362
10380
  ])
10363
10381
  return { content: [{ type: 'text', text: 'reacted' }] }
@@ -10401,7 +10419,7 @@ async function executeEditMessage(args: Record<string, unknown>): Promise<unknow
10401
10419
  if (!args.chat_id) throw new Error('edit_message: chat_id is required')
10402
10420
  if (!args.message_id) throw new Error('edit_message: message_id is required')
10403
10421
  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)
10422
+ assertAllowedChat(String(args.chat_id ?? ''))
10405
10423
  const editAccess = loadAccess()
10406
10424
  const editConfigMode = editAccess.parseMode ?? 'html'
10407
10425
  const editFormat = (args.format as string | undefined) ?? editConfigMode
@@ -10419,7 +10437,7 @@ async function executeEditMessage(args: Record<string, unknown>): Promise<unknow
10419
10437
  editRawText = scrub.scrubbed
10420
10438
  emitRuntimeMetric({
10421
10439
  kind: 'voice_scrub_applied',
10422
- chatKey: statusKey(args.chat_id as string, undefined),
10440
+ chatKey: statusKey(String(args.chat_id ?? ''), undefined),
10423
10441
  replaced: scrub.replaced,
10424
10442
  site: 'edit_message',
10425
10443
  })
@@ -10439,14 +10457,14 @@ async function executeEditMessage(args: Record<string, unknown>): Promise<unknow
10439
10457
  }
10440
10458
  const edited = await robustApiCall(
10441
10459
  () => lockedBot.api.editMessageText(
10442
- args.chat_id as string, Number(args.message_id), editText,
10460
+ String(args.chat_id ?? ''), Number(args.message_id), editText,
10443
10461
  ...(editParseMode ? [{ parse_mode: editParseMode }] : []),
10444
10462
  ),
10445
10463
  )
10446
10464
  const id = typeof edited === 'object' && edited ? (edited as any).message_id : args.message_id
10447
10465
  if (HISTORY_ENABLED) {
10448
10466
  try {
10449
- recordEdit({ chat_id: args.chat_id as string, message_id: Number(args.message_id), text: args.text as string })
10467
+ recordEdit({ chat_id: String(args.chat_id ?? ''), message_id: Number(args.message_id), text: args.text as string })
10450
10468
  } catch (err) {
10451
10469
  process.stderr.write(`telegram gateway: history recordEdit failed: ${err}\n`)
10452
10470
  }
@@ -10456,7 +10474,7 @@ async function executeEditMessage(args: Record<string, unknown>): Promise<unknow
10456
10474
 
10457
10475
  async function executeSendTyping(args: Record<string, unknown>): Promise<unknown> {
10458
10476
  if (!args.chat_id) throw new Error('send_typing: chat_id is required')
10459
- const stChatId = args.chat_id as string
10477
+ const stChatId = String(args.chat_id ?? '')
10460
10478
  assertAllowedChat(stChatId)
10461
10479
  // #273: granular chat actions. Default 'typing' preserves the
10462
10480
  // existing tool semantics; agents can opt in to upload_document,
@@ -10490,7 +10508,7 @@ async function executeSendTyping(args: Record<string, unknown>): Promise<unknown
10490
10508
  async function executePinMessage(args: Record<string, unknown>): Promise<unknown> {
10491
10509
  if (!args.chat_id) throw new Error('pin_message: chat_id is required')
10492
10510
  if (!args.message_id) throw new Error('pin_message: message_id is required')
10493
- const pinChatId = args.chat_id as string
10511
+ const pinChatId = String(args.chat_id ?? '')
10494
10512
  assertAllowedChat(pinChatId)
10495
10513
  // #1075: wrap through robustApiCall so flood-wait / transient network
10496
10514
  // errors are retried. THREAD_NOT_FOUND on a stale topic surfaces to the
@@ -10506,7 +10524,7 @@ async function executePinMessage(args: Record<string, unknown>): Promise<unknown
10506
10524
  async function executeDeleteMessage(args: Record<string, unknown>): Promise<unknown> {
10507
10525
  if (!args.chat_id) throw new Error('delete_message: chat_id is required')
10508
10526
  if (!args.message_id) throw new Error('delete_message: message_id is required')
10509
- const delChatId = args.chat_id as string
10527
+ const delChatId = String(args.chat_id ?? '')
10510
10528
  const delMessageId = Number(args.message_id)
10511
10529
  assertAllowedChat(delChatId)
10512
10530
  await robustApiCall(() => lockedBot.api.deleteMessage(delChatId, delMessageId), { chat_id: delChatId })
@@ -10522,7 +10540,7 @@ async function executeForwardMessage(args: Record<string, unknown>): Promise<unk
10522
10540
  if (!args.chat_id) throw new Error('forward_message: chat_id is required')
10523
10541
  if (!args.from_chat_id) throw new Error('forward_message: from_chat_id is required')
10524
10542
  if (!args.message_id) throw new Error('forward_message: message_id is required')
10525
- const fwdChatId = args.chat_id as string
10543
+ const fwdChatId = String(args.chat_id ?? '')
10526
10544
  const fwdFromChatId = args.from_chat_id as string
10527
10545
  const fwdMsgId = Number(args.message_id)
10528
10546
  assertAllowedChat(fwdChatId)
@@ -10556,7 +10574,7 @@ async function executeGetRecentMessages(args: Record<string, unknown>): Promise<
10556
10574
  }
10557
10575
  }
10558
10576
  if (!args.chat_id) throw new Error('get_recent_messages: chat_id is required')
10559
- const chat_id = args.chat_id as string
10577
+ const chat_id = String(args.chat_id ?? '')
10560
10578
  assertAllowedChat(chat_id)
10561
10579
  const rawThread = args.message_thread_id as string | undefined
10562
10580
  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 {
@@ -381,6 +381,7 @@ export function buildObligationRepresentInbound(o: Obligation, now: number): Inb
381
381
  source: 'obligation_represent',
382
382
  origin_turn_id: o.originTurnId,
383
383
  represent_count: String(o.representCount + 1),
384
+ chat_id: o.chatId,
384
385
  },
385
386
  }
386
387
  }
@@ -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,
@@ -184,6 +184,7 @@ describe("buildObligationRepresentInbound", () => {
184
184
  expect(m.meta.origin_turn_id).toBe("-100123:3#715");
185
185
  expect(m.meta.source).toBe("obligation_represent"); // synthetic → not tracked, no new obligation
186
186
  expect(m.meta.represent_count).toBe("1");
187
+ expect(m.meta.chat_id).toBe("-100123"); // Bug C fix: chat_id in meta drives the <channel> tag
187
188
  expect(m.text).toContain("do the Meta report");
188
189
  expect(m.text).toMatch(/answer it now|reply tool/i);
189
190
  });
@@ -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 ──────────────────────────────────────────