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.
- package/dist/cli/switchroom.js +9 -2
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +70 -41
- package/telegram-plugin/gateway/gateway.ts +49 -31
- package/telegram-plugin/gateway/model-command.ts +32 -4
- package/telegram-plugin/gateway/obligation-ledger.ts +1 -0
- package/telegram-plugin/tests/model-command.test.ts +55 -1
- package/telegram-plugin/tests/obligation-ledger.test.ts +1 -0
- package/telegram-plugin/uat/scenarios/jtbd-model-litellm-sr-dm.test.ts +3 -1
package/dist/cli/switchroom.js
CHANGED
|
@@ -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.
|
|
51678
|
-
var COMMIT_SHA = "
|
|
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.
|
|
22590
|
+
var VERSION = "0.16.12";
|
|
22591
22591
|
|
|
22592
22592
|
// src/cli/resolve-version.ts
|
|
22593
22593
|
function readPackageVersion() {
|
package/package.json
CHANGED
|
@@ -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 <name></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 <name></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 <full-model-id></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(
|
|
46070
|
+
if (currentSession !== null && isSrModel(currentSession) && isClaudeModel(model)) {
|
|
46067
46071
|
try {
|
|
46068
|
-
await deps.scheduleRestart(`user: /model ${
|
|
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(
|
|
46089
|
+
const verbHtml = `<code>/model ${deps.escapeHtml(model)}</code>`;
|
|
46086
46090
|
let result;
|
|
46087
46091
|
try {
|
|
46088
|
-
result = await deps.inject(deps.getAgentName(), `/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.
|
|
56051
|
-
var COMMIT_SHA = "
|
|
56052
|
-
var COMMIT_DATE = "2026-06-
|
|
56053
|
-
var LATEST_PR =
|
|
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(
|
|
57731
|
+
if (access.allowFrom.includes(id))
|
|
57714
57732
|
return;
|
|
57715
|
-
if (
|
|
57733
|
+
if (id in access.groups)
|
|
57716
57734
|
return;
|
|
57717
|
-
throw new Error(`chat ${
|
|
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
|
|
60782
|
-
if (!
|
|
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(
|
|
1036
|
-
if (
|
|
1037
|
-
throw new Error(`chat ${
|
|
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
|
|
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
|
|
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
|
|
7975
|
-
if (!
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
10360
|
-
await lockedBot.api.setMessageReaction(args.chat_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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 <name></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 <full-model-id></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(
|
|
198
|
+
if (currentSession !== null && isSrModel(currentSession) && isClaudeModel(model)) {
|
|
192
199
|
try {
|
|
193
|
-
await deps.scheduleRestart(`user: /model ${
|
|
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(
|
|
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 ${
|
|
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,
|
|
@@ -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:
|
|
49
|
+
timeout: 60_000,
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
// ── 1. Section headers ──────────────────────────────────────────
|