switchroom 0.18.28 → 0.18.29
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/bin/handoff-briefing.sh +8 -1
- package/dist/auth-broker/index.js +0 -57
- package/dist/cli/switchroom.js +501 -497
- package/dist/host-control/main.js +1 -58
- package/dist/vault/approvals/kernel-server.js +0 -57
- package/dist/vault/broker/server.js +0 -57
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +37 -19
- package/telegram-plugin/dist/gateway/gateway.js +578 -580
- package/telegram-plugin/gateway/backstop-delivery.ts +272 -0
- package/telegram-plugin/gateway/forward-origin.ts +9 -1
- package/telegram-plugin/gateway/gateway.ts +458 -385
- package/telegram-plugin/gateway/model-command.ts +227 -602
- package/telegram-plugin/gateway/turn-record-status.ts +45 -0
- package/telegram-plugin/history.ts +153 -23
- package/telegram-plugin/shared/local-time.ts +56 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +250 -0
- package/telegram-plugin/tests/forward-origin.test.ts +30 -3
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +86 -59
- package/telegram-plugin/tests/history.test.ts +88 -0
- package/telegram-plugin/tests/local-time.test.ts +68 -0
- package/telegram-plugin/tests/model-command.test.ts +317 -1535
- package/telegram-plugin/tests/turn-flush-safety.test.ts +34 -0
- package/telegram-plugin/tier-downgrade.ts +4 -3
- package/telegram-plugin/turn-flush-safety.ts +25 -1
- package/vendor/hindsight-memory/scripts/lib/content.py +40 -6
- package/vendor/hindsight-memory/tests/test_content.py +28 -7
|
@@ -28638,6 +28638,7 @@ var init_redact = __esm(() => {
|
|
|
28638
28638
|
// history.ts
|
|
28639
28639
|
var exports_history = {};
|
|
28640
28640
|
__export(exports_history, {
|
|
28641
|
+
verifyHistoryWritable: () => verifyHistoryWritable,
|
|
28641
28642
|
recordReaction: () => recordReaction,
|
|
28642
28643
|
recordOutbound: () => recordOutbound,
|
|
28643
28644
|
recordInbound: () => recordInbound,
|
|
@@ -28676,6 +28677,15 @@ function loadDatabaseClass() {
|
|
|
28676
28677
|
throw new Error(`history.ts requires Bun runtime (bun:sqlite). Caller: ${err.message}`);
|
|
28677
28678
|
}
|
|
28678
28679
|
}
|
|
28680
|
+
function warnHistory(msg) {
|
|
28681
|
+
try {
|
|
28682
|
+
process.stderr.write(`telegram history: ${msg}
|
|
28683
|
+
`);
|
|
28684
|
+
} catch {}
|
|
28685
|
+
}
|
|
28686
|
+
function isValidMessageId(id) {
|
|
28687
|
+
return typeof id === "number" && Number.isInteger(id) && id > 0;
|
|
28688
|
+
}
|
|
28679
28689
|
function initHistory(stateDir, retentionDays = 30) {
|
|
28680
28690
|
if (db != null)
|
|
28681
28691
|
return;
|
|
@@ -28757,6 +28767,33 @@ function initHistory(stateDir, retentionDays = 30) {
|
|
|
28757
28767
|
const cutoff = Math.floor(Date.now() / 1000) - retentionDays * 86400;
|
|
28758
28768
|
db.prepare("DELETE FROM messages WHERE ts < ?").run(cutoff);
|
|
28759
28769
|
}
|
|
28770
|
+
const check = verifyHistoryWritable();
|
|
28771
|
+
if (!check.ok) {
|
|
28772
|
+
warnHistory(`WRITER SELF-CHECK FAILED at boot (path=${path2}): ${check.error ?? "unknown"} \u2014 ` + `history recording is NOT durable; get_recent_messages recovery and the ` + `reply-backstop already-replied suppression will be blind. Investigate the ` + `DB path/mount/permissions before trusting delivery accounting.`);
|
|
28773
|
+
}
|
|
28774
|
+
}
|
|
28775
|
+
function verifyHistoryWritable() {
|
|
28776
|
+
if (db == null)
|
|
28777
|
+
return { ok: false, error: "initHistory() not called" };
|
|
28778
|
+
const SENTINEL_CHAT = "__history_selfcheck__";
|
|
28779
|
+
const sentinelId = Date.now();
|
|
28780
|
+
try {
|
|
28781
|
+
db.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
|
|
28782
|
+
db.prepare(`INSERT OR REPLACE INTO messages
|
|
28783
|
+
(chat_id, thread_id, message_id, role, ts, text)
|
|
28784
|
+
VALUES (?, NULL, ?, 'assistant', ?, ?)`).run(SENTINEL_CHAT, sentinelId, Math.floor(Date.now() / 1000), "selfcheck");
|
|
28785
|
+
const row = db.prepare("SELECT text FROM messages WHERE chat_id = ? AND message_id = ?").get(SENTINEL_CHAT, sentinelId);
|
|
28786
|
+
if (row?.text !== "selfcheck") {
|
|
28787
|
+
return { ok: false, error: "sentinel row not read back after insert" };
|
|
28788
|
+
}
|
|
28789
|
+
return { ok: true };
|
|
28790
|
+
} catch (err) {
|
|
28791
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
28792
|
+
} finally {
|
|
28793
|
+
try {
|
|
28794
|
+
db.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
|
|
28795
|
+
} catch {}
|
|
28796
|
+
}
|
|
28760
28797
|
}
|
|
28761
28798
|
function _resetForTests() {
|
|
28762
28799
|
if (db != null) {
|
|
@@ -28815,6 +28852,10 @@ function requireDb() {
|
|
|
28815
28852
|
function recordInbound(args) {
|
|
28816
28853
|
if (args.message_id == null)
|
|
28817
28854
|
return;
|
|
28855
|
+
if (!isValidMessageId(args.message_id)) {
|
|
28856
|
+
warnHistory(`recordInbound: dropping row with invalid message_id=${String(args.message_id)} ` + `(chat=${args.chat_id}) \u2014 a delivered inbound will be absent from history`);
|
|
28857
|
+
return;
|
|
28858
|
+
}
|
|
28818
28859
|
const stmt = requireDb().prepare(`
|
|
28819
28860
|
INSERT OR REPLACE INTO messages
|
|
28820
28861
|
(chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id, reply_to_message_id, reply_to_text, forwarded_from, forwarded_from_type, forwarded_from_id, forwarded_date, forwarded_message_id)
|
|
@@ -28826,23 +28867,38 @@ function recordOutbound(args) {
|
|
|
28826
28867
|
if (args.message_ids.length === 0)
|
|
28827
28868
|
return;
|
|
28828
28869
|
const ts = args.ts ?? Math.floor(Date.now() / 1000);
|
|
28829
|
-
const
|
|
28870
|
+
const validRows = [];
|
|
28871
|
+
for (let i = 0;i < args.message_ids.length; i++) {
|
|
28872
|
+
const id = args.message_ids[i];
|
|
28873
|
+
if (!isValidMessageId(id)) {
|
|
28874
|
+
warnHistory(`recordOutbound: dropping chunk ${i} with invalid message_id=${String(id)} ` + `(chat=${args.chat_id}) \u2014 a delivered outbound will be absent from history, ` + `blinding the reply-backstop already-replied suppression`);
|
|
28875
|
+
continue;
|
|
28876
|
+
}
|
|
28877
|
+
validRows.push({
|
|
28878
|
+
id,
|
|
28879
|
+
text: redact(args.texts[i] ?? ""),
|
|
28880
|
+
attachKind: args.attachment_kinds?.[i] ?? null
|
|
28881
|
+
});
|
|
28882
|
+
}
|
|
28883
|
+
if (validRows.length === 0)
|
|
28884
|
+
return;
|
|
28885
|
+
const groupId = validRows[0].id;
|
|
28830
28886
|
const stmt = requireDb().prepare(`
|
|
28831
28887
|
INSERT OR REPLACE INTO messages
|
|
28832
28888
|
(chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id)
|
|
28833
28889
|
VALUES (?, ?, ?, 'assistant', NULL, NULL, ?, ?, ?, ?)
|
|
28834
28890
|
`);
|
|
28835
|
-
const tx = requireDb().transaction((
|
|
28836
|
-
for (const
|
|
28837
|
-
stmt.run(args.chat_id, args.thread_id ?? null,
|
|
28891
|
+
const tx = requireDb().transaction((rows) => {
|
|
28892
|
+
for (const r of rows) {
|
|
28893
|
+
stmt.run(args.chat_id, args.thread_id ?? null, r.id, ts, r.text, r.attachKind, groupId);
|
|
28838
28894
|
}
|
|
28839
28895
|
});
|
|
28840
|
-
|
|
28841
|
-
|
|
28842
|
-
|
|
28843
|
-
args.
|
|
28844
|
-
|
|
28845
|
-
|
|
28896
|
+
try {
|
|
28897
|
+
tx(validRows);
|
|
28898
|
+
} catch (err) {
|
|
28899
|
+
warnHistory(`recordOutbound: INSERT failed (chat=${args.chat_id} ids=[${validRows.map((r) => r.id).join(",")}]): ` + `${err instanceof Error ? err.message : String(err)} \u2014 this outbound will be absent from history`);
|
|
28900
|
+
throw err;
|
|
28901
|
+
}
|
|
28846
28902
|
}
|
|
28847
28903
|
function recordEdit(args) {
|
|
28848
28904
|
requireDb().prepare(`
|
|
@@ -39831,6 +39887,64 @@ function buildExtraAttachmentMeta(resolved) {
|
|
|
39831
39887
|
return out;
|
|
39832
39888
|
}
|
|
39833
39889
|
|
|
39890
|
+
// shared/local-time.ts
|
|
39891
|
+
function fmtLocalClock(ms, tz) {
|
|
39892
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
39893
|
+
timeZone: tz,
|
|
39894
|
+
hour: "numeric",
|
|
39895
|
+
minute: "2-digit",
|
|
39896
|
+
hour12: true
|
|
39897
|
+
}).format(new Date(ms)).replace(/\s([AP])M$/, (_m, p) => `${p.toLowerCase()}m`);
|
|
39898
|
+
}
|
|
39899
|
+
function fmtLocalDate(ms, tz) {
|
|
39900
|
+
return new Intl.DateTimeFormat("en-GB", {
|
|
39901
|
+
timeZone: tz,
|
|
39902
|
+
day: "numeric",
|
|
39903
|
+
month: "short"
|
|
39904
|
+
}).format(new Date(ms));
|
|
39905
|
+
}
|
|
39906
|
+
function localDay(ms, tz) {
|
|
39907
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
39908
|
+
timeZone: tz,
|
|
39909
|
+
year: "numeric",
|
|
39910
|
+
month: "2-digit",
|
|
39911
|
+
day: "2-digit"
|
|
39912
|
+
}).format(new Date(ms));
|
|
39913
|
+
}
|
|
39914
|
+
function tzAbbrev(ms, tz) {
|
|
39915
|
+
const at = new Date(ms);
|
|
39916
|
+
for (const loc of ["en-US", "en-AU", "en-GB"]) {
|
|
39917
|
+
const v = new Intl.DateTimeFormat(loc, { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value;
|
|
39918
|
+
if (v && !/^(?:GMT|UTC)/i.test(v))
|
|
39919
|
+
return v;
|
|
39920
|
+
}
|
|
39921
|
+
return new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value ?? tz;
|
|
39922
|
+
}
|
|
39923
|
+
function resolveEnvTimezone(env = process.env) {
|
|
39924
|
+
return env.SWITCHROOM_TIMEZONE ?? env.TZ ?? "UTC";
|
|
39925
|
+
}
|
|
39926
|
+
function fmtLocalStamp(ms, tz) {
|
|
39927
|
+
try {
|
|
39928
|
+
const at = new Date(ms);
|
|
39929
|
+
const weekday = new Intl.DateTimeFormat("en-US", {
|
|
39930
|
+
timeZone: tz,
|
|
39931
|
+
weekday: "long"
|
|
39932
|
+
}).format(at);
|
|
39933
|
+
const date = localDay(ms, tz);
|
|
39934
|
+
const time = new Intl.DateTimeFormat("en-US", {
|
|
39935
|
+
timeZone: tz,
|
|
39936
|
+
hour: "2-digit",
|
|
39937
|
+
minute: "2-digit",
|
|
39938
|
+
hour12: true
|
|
39939
|
+
}).format(at);
|
|
39940
|
+
return `${weekday} ${date} ${time} ${tzAbbrev(ms, tz)}`;
|
|
39941
|
+
} catch {
|
|
39942
|
+
if (tz !== "UTC")
|
|
39943
|
+
return fmtLocalStamp(ms, "UTC");
|
|
39944
|
+
return new Date(ms).toISOString();
|
|
39945
|
+
}
|
|
39946
|
+
}
|
|
39947
|
+
|
|
39834
39948
|
// steering.ts
|
|
39835
39949
|
function escapeXmlAttribute(s) {
|
|
39836
39950
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -39947,7 +40061,7 @@ function buildForwardOriginMeta(origins) {
|
|
|
39947
40061
|
if (o.id != null)
|
|
39948
40062
|
out[`forwarded_from_id${suffix}`] = String(o.id);
|
|
39949
40063
|
if (o.date != null) {
|
|
39950
|
-
out[`forwarded_date${suffix}`] =
|
|
40064
|
+
out[`forwarded_date${suffix}`] = fmtLocalStamp(o.date * 1000, resolveEnvTimezone());
|
|
39951
40065
|
}
|
|
39952
40066
|
});
|
|
39953
40067
|
return out;
|
|
@@ -39958,6 +40072,49 @@ function forwardOriginDateIso(o) {
|
|
|
39958
40072
|
return new Date(o.date * 1000).toISOString();
|
|
39959
40073
|
}
|
|
39960
40074
|
|
|
40075
|
+
// shared/local-time.ts
|
|
40076
|
+
function localDay2(ms, tz) {
|
|
40077
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
40078
|
+
timeZone: tz,
|
|
40079
|
+
year: "numeric",
|
|
40080
|
+
month: "2-digit",
|
|
40081
|
+
day: "2-digit"
|
|
40082
|
+
}).format(new Date(ms));
|
|
40083
|
+
}
|
|
40084
|
+
function tzAbbrev2(ms, tz) {
|
|
40085
|
+
const at = new Date(ms);
|
|
40086
|
+
for (const loc of ["en-US", "en-AU", "en-GB"]) {
|
|
40087
|
+
const v = new Intl.DateTimeFormat(loc, { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value;
|
|
40088
|
+
if (v && !/^(?:GMT|UTC)/i.test(v))
|
|
40089
|
+
return v;
|
|
40090
|
+
}
|
|
40091
|
+
return new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value ?? tz;
|
|
40092
|
+
}
|
|
40093
|
+
function resolveEnvTimezone2(env = process.env) {
|
|
40094
|
+
return env.SWITCHROOM_TIMEZONE ?? env.TZ ?? "UTC";
|
|
40095
|
+
}
|
|
40096
|
+
function fmtLocalStamp2(ms, tz) {
|
|
40097
|
+
try {
|
|
40098
|
+
const at = new Date(ms);
|
|
40099
|
+
const weekday = new Intl.DateTimeFormat("en-US", {
|
|
40100
|
+
timeZone: tz,
|
|
40101
|
+
weekday: "long"
|
|
40102
|
+
}).format(at);
|
|
40103
|
+
const date = localDay2(ms, tz);
|
|
40104
|
+
const time = new Intl.DateTimeFormat("en-US", {
|
|
40105
|
+
timeZone: tz,
|
|
40106
|
+
hour: "2-digit",
|
|
40107
|
+
minute: "2-digit",
|
|
40108
|
+
hour12: true
|
|
40109
|
+
}).format(at);
|
|
40110
|
+
return `${weekday} ${date} ${time} ${tzAbbrev2(ms, tz)}`;
|
|
40111
|
+
} catch {
|
|
40112
|
+
if (tz !== "UTC")
|
|
40113
|
+
return fmtLocalStamp2(ms, "UTC");
|
|
40114
|
+
return new Date(ms).toISOString();
|
|
40115
|
+
}
|
|
40116
|
+
}
|
|
40117
|
+
|
|
39961
40118
|
// status-reactions.ts
|
|
39962
40119
|
var TELEGRAM_REACTION_WHITELIST = new Set([
|
|
39963
40120
|
"\uD83D\uDC4D",
|
|
@@ -57171,40 +57328,6 @@ function sendGateConfigFromEnv(env = process.env) {
|
|
|
57171
57328
|
return out;
|
|
57172
57329
|
}
|
|
57173
57330
|
|
|
57174
|
-
// shared/local-time.ts
|
|
57175
|
-
function fmtLocalClock(ms, tz) {
|
|
57176
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
57177
|
-
timeZone: tz,
|
|
57178
|
-
hour: "numeric",
|
|
57179
|
-
minute: "2-digit",
|
|
57180
|
-
hour12: true
|
|
57181
|
-
}).format(new Date(ms)).replace(/\s([AP])M$/, (_m, p) => `${p.toLowerCase()}m`);
|
|
57182
|
-
}
|
|
57183
|
-
function fmtLocalDate(ms, tz) {
|
|
57184
|
-
return new Intl.DateTimeFormat("en-GB", {
|
|
57185
|
-
timeZone: tz,
|
|
57186
|
-
day: "numeric",
|
|
57187
|
-
month: "short"
|
|
57188
|
-
}).format(new Date(ms));
|
|
57189
|
-
}
|
|
57190
|
-
function localDay(ms, tz) {
|
|
57191
|
-
return new Intl.DateTimeFormat("en-CA", {
|
|
57192
|
-
timeZone: tz,
|
|
57193
|
-
year: "numeric",
|
|
57194
|
-
month: "2-digit",
|
|
57195
|
-
day: "2-digit"
|
|
57196
|
-
}).format(new Date(ms));
|
|
57197
|
-
}
|
|
57198
|
-
function tzAbbrev(ms, tz) {
|
|
57199
|
-
const at = new Date(ms);
|
|
57200
|
-
for (const loc of ["en-US", "en-AU", "en-GB"]) {
|
|
57201
|
-
const v = new Intl.DateTimeFormat(loc, { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value;
|
|
57202
|
-
if (v && !/^(?:GMT|UTC)/i.test(v))
|
|
57203
|
-
return v;
|
|
57204
|
-
}
|
|
57205
|
-
return new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value ?? tz;
|
|
57206
|
-
}
|
|
57207
|
-
|
|
57208
57331
|
// send-gate-observability.ts
|
|
57209
57332
|
function formatStatsLine(stats) {
|
|
57210
57333
|
const c = stats.global;
|
|
@@ -57239,7 +57362,7 @@ function createStatsLogger(config) {
|
|
|
57239
57362
|
function isAlertableScope(scopeKey) {
|
|
57240
57363
|
return scopeKey === "global" || scopeKey.startsWith("chat:") || scopeKey.startsWith("group:");
|
|
57241
57364
|
}
|
|
57242
|
-
function
|
|
57365
|
+
function fmtLocalStamp3(ms, tz, refMs) {
|
|
57243
57366
|
const clock = fmtLocalClock(ms, tz);
|
|
57244
57367
|
const sameDay = refMs != null && localDay(ms, tz) === localDay(refMs, tz);
|
|
57245
57368
|
const body = sameDay ? clock : `${fmtLocalDate(ms, tz)} ${clock}`;
|
|
@@ -57283,7 +57406,7 @@ function createFloodWindowObserver(config) {
|
|
|
57283
57406
|
}
|
|
57284
57407
|
function openAlertText(w, now) {
|
|
57285
57408
|
const openFor = fmtDur(now - w.observedAt);
|
|
57286
|
-
return `\u26a0\ufe0f Telegram flood ban active (scope \`${w.scopeKey}\`). ` + `Open for ${openFor}, expected to clear at ${
|
|
57409
|
+
return `\u26a0\ufe0f Telegram flood ban active (scope \`${w.scopeKey}\`). ` + `Open for ${openFor}, expected to clear at ${fmtLocalStamp3(w.untilTs, tz, now)}. ` + `Outbound to that scope is being suppressed.`;
|
|
57287
57410
|
}
|
|
57288
57411
|
function closeAlertText(recs) {
|
|
57289
57412
|
const observedAt = Math.min(...recs.map((r) => r.observedAt));
|
|
@@ -63314,6 +63437,7 @@ function endsWithSilentMarker(text4) {
|
|
|
63314
63437
|
return false;
|
|
63315
63438
|
return isSilentFlushMarker(lines[lines.length - 1]);
|
|
63316
63439
|
}
|
|
63440
|
+
var FLUSH_SUBSTANTIVE_MIN_CHARS = 200;
|
|
63317
63441
|
function selectFlushDeliveryText(blocks) {
|
|
63318
63442
|
const candidates = blocks.map((b) => b.trim()).filter((b) => b.length > 0);
|
|
63319
63443
|
if (candidates.length === 0)
|
|
@@ -63328,8 +63452,18 @@ function selectFlushDeliveryText(blocks) {
|
|
|
63328
63452
|
`);
|
|
63329
63453
|
}
|
|
63330
63454
|
var NARRATION_OPENER = /^(let me\b|lemme\b|i'?ll\b|i will\b|i am going to\b|i'?m going to\b|i'?m about to\b|going to\b|first,?\s+(?:let me|i'?ll|i will)\b|now,?\s+(?:let me|i'?ll|i will)\b|next,?\s+(?:let me|i'?ll|i will)\b|let'?s\b)/i;
|
|
63455
|
+
var NARRATION_TRAILER = /(?:\.{3}|\u2026|:)\s*$/;
|
|
63456
|
+
function isTrailingNarrationLine(block) {
|
|
63457
|
+
const t = block.trim();
|
|
63458
|
+
if (t.length === 0 || t.length >= FLUSH_SUBSTANTIVE_MIN_CHARS)
|
|
63459
|
+
return false;
|
|
63460
|
+
if (t.includes(`
|
|
63461
|
+
`))
|
|
63462
|
+
return false;
|
|
63463
|
+
return NARRATION_TRAILER.test(t);
|
|
63464
|
+
}
|
|
63331
63465
|
function isNarrationBlock(block) {
|
|
63332
|
-
return NARRATION_OPENER.test(block.trimStart());
|
|
63466
|
+
return NARRATION_OPENER.test(block.trimStart()) || isTrailingNarrationLine(block);
|
|
63333
63467
|
}
|
|
63334
63468
|
function decideTurnFlush(input) {
|
|
63335
63469
|
const flushEnabled = input.flushEnabled !== false;
|
|
@@ -69214,7 +69348,7 @@ function endsWithSilentMarker2(text4) {
|
|
|
69214
69348
|
return false;
|
|
69215
69349
|
return isSilentFlushMarker2(lines[lines.length - 1]);
|
|
69216
69350
|
}
|
|
69217
|
-
var
|
|
69351
|
+
var FLUSH_SUBSTANTIVE_MIN_CHARS2 = 200;
|
|
69218
69352
|
function selectFlushDeliveryText2(blocks) {
|
|
69219
69353
|
const candidates = blocks.map((b) => b.trim()).filter((b) => b.length > 0);
|
|
69220
69354
|
if (candidates.length === 0)
|
|
@@ -69229,8 +69363,18 @@ function selectFlushDeliveryText2(blocks) {
|
|
|
69229
69363
|
`);
|
|
69230
69364
|
}
|
|
69231
69365
|
var NARRATION_OPENER2 = /^(let me\b|lemme\b|i'?ll\b|i will\b|i am going to\b|i'?m going to\b|i'?m about to\b|going to\b|first,?\s+(?:let me|i'?ll|i will)\b|now,?\s+(?:let me|i'?ll|i will)\b|next,?\s+(?:let me|i'?ll|i will)\b|let'?s\b)/i;
|
|
69366
|
+
var NARRATION_TRAILER2 = /(?:\.{3}|\u2026|:)\s*$/;
|
|
69367
|
+
function isTrailingNarrationLine2(block) {
|
|
69368
|
+
const t = block.trim();
|
|
69369
|
+
if (t.length === 0 || t.length >= FLUSH_SUBSTANTIVE_MIN_CHARS2)
|
|
69370
|
+
return false;
|
|
69371
|
+
if (t.includes(`
|
|
69372
|
+
`))
|
|
69373
|
+
return false;
|
|
69374
|
+
return NARRATION_TRAILER2.test(t);
|
|
69375
|
+
}
|
|
69232
69376
|
function isNarrationBlock2(block) {
|
|
69233
|
-
return NARRATION_OPENER2.test(block.trimStart());
|
|
69377
|
+
return NARRATION_OPENER2.test(block.trimStart()) || isTrailingNarrationLine2(block);
|
|
69234
69378
|
}
|
|
69235
69379
|
function decideTurnFlush2(input) {
|
|
69236
69380
|
const flushEnabled = input.flushEnabled !== false;
|
|
@@ -71165,16 +71309,6 @@ Allowed: \`${deps.escapeHtml(allow)}\``, { html: true });
|
|
|
71165
71309
|
await deps.reply(ctx, finalText, { html: true, accent });
|
|
71166
71310
|
}
|
|
71167
71311
|
|
|
71168
|
-
// ../src/agents/model-picker.ts
|
|
71169
|
-
function labelTag(label) {
|
|
71170
|
-
let h = 2166136261;
|
|
71171
|
-
for (const ch of label) {
|
|
71172
|
-
h ^= ch.codePointAt(0);
|
|
71173
|
-
h = Math.imul(h, 16777619) >>> 0;
|
|
71174
|
-
}
|
|
71175
|
-
return h.toString(16).padStart(8, "0");
|
|
71176
|
-
}
|
|
71177
|
-
|
|
71178
71312
|
// gateway/model-command.ts
|
|
71179
71313
|
var MODEL_ALIASES = ["opus", "sonnet", "haiku", "fable", "default"];
|
|
71180
71314
|
var MODEL_ARG_RE = /^[A-Za-z0-9][A-Za-z0-9._/\[\]-]{0,99}$/;
|
|
@@ -71184,12 +71318,6 @@ function isValidModelArg(arg) {
|
|
|
71184
71318
|
function isSrModel(name) {
|
|
71185
71319
|
return name.startsWith("sr-");
|
|
71186
71320
|
}
|
|
71187
|
-
function isClaudeModel(name) {
|
|
71188
|
-
const lower = name.toLowerCase();
|
|
71189
|
-
if (MODEL_ALIASES.includes(lower))
|
|
71190
|
-
return true;
|
|
71191
|
-
return lower.startsWith("claude-");
|
|
71192
|
-
}
|
|
71193
71321
|
function parseModelCommand(text4) {
|
|
71194
71322
|
const m = text4.match(/^\/model(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/);
|
|
71195
71323
|
if (!m)
|
|
@@ -71233,13 +71361,13 @@ function modelCommandReceiptLine(agent, parsed, busy) {
|
|
|
71233
71361
|
const arg = parsed.kind === "set" ? parsed.model : parsed.kind === "show" ? "(show)" : "(help)";
|
|
71234
71362
|
return `telegram gateway: gw /model received agent=${agent} kind=${parsed.kind} arg=${arg} busy=${busy}`;
|
|
71235
71363
|
}
|
|
71236
|
-
var PERSIST_NOTE = "
|
|
71364
|
+
var PERSIST_NOTE = "_A `/model` switch relaunches the session (~30s) on the chosen model. Session-only \u2014 reverts to the configured `model:` on the next restart. `/model default` reverts now. Live scrollback is replaced by a fresh session; memory and the handoff briefing carry the context. To change the default permanently, set `model:` in switchroom.yaml._";
|
|
71237
71365
|
function helpText2(deps, reason) {
|
|
71238
71366
|
const srAliasExamples = Object.keys(SR_MODEL_ALIASES).map((a) => `\`${a}\``).join(" \u00b7 ");
|
|
71239
71367
|
const lines = [];
|
|
71240
71368
|
if (reason)
|
|
71241
71369
|
lines.push(`\u26a0\ufe0f ${deps.escapeHtml(reason)}`);
|
|
71242
|
-
lines.push("**/model** \u2014 show or switch the Claude model", "`/model` \u2014 show the configured model", `\`/model <name>\` \u2014 switch the live session (${MODEL_ALIASES.map((a) => `\`${a}\``).join(" \u00b7 ")} or a full model id)`, `_OpenRouter shortcuts:_ ${srAliasExamples}`, "
|
|
71370
|
+
lines.push("**/model** \u2014 show or switch the Claude model", "`/model` \u2014 show the configured model", `\`/model <name>\` \u2014 switch the live session (${MODEL_ALIASES.map((a) => `\`${a}\``).join(" \u00b7 ")} or a full model id)`, `_OpenRouter shortcuts:_ ${srAliasExamples}`, "_Every switch relaunches the session (~30s) on the chosen model \u2014 Claude and OpenRouter (sr-\\*) alike._", PERSIST_NOTE);
|
|
71243
71371
|
return { text: lines.join(`
|
|
71244
71372
|
`), html: true };
|
|
71245
71373
|
}
|
|
@@ -71273,132 +71401,46 @@ async function handleModelCommand(parsed, deps) {
|
|
|
71273
71401
|
html: true
|
|
71274
71402
|
};
|
|
71275
71403
|
}
|
|
71276
|
-
|
|
71277
|
-
|
|
71278
|
-
try {
|
|
71279
|
-
await deps.scheduleModelRelaunch(model, `user: /model ${model} (sr-to-claude restart)`);
|
|
71280
|
-
} catch (err) {
|
|
71281
|
-
if (isRestartInFlight(err)) {
|
|
71282
|
-
return {
|
|
71283
|
-
text: `\u23f3 A restart is already in flight \u2014 your switch to \`${deps.escapeHtml(model)}\` will apply as it completes (~15s).`,
|
|
71284
|
-
html: true
|
|
71285
|
-
};
|
|
71286
|
-
}
|
|
71287
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
71288
|
-
return {
|
|
71289
|
-
text: `\u274c Could not schedule restart: ${deps.escapeHtml(msg)}`,
|
|
71290
|
-
html: true
|
|
71291
|
-
};
|
|
71292
|
-
}
|
|
71293
|
-
return {
|
|
71294
|
-
text: [
|
|
71295
|
-
`Switching from \`${deps.escapeHtml(currentSession)}\` back to Claude \u2014 restarting session cleanly. Claude will be ready in ~30s.`,
|
|
71296
|
-
PERSIST_NOTE
|
|
71297
|
-
].join(`
|
|
71298
|
-
`),
|
|
71299
|
-
html: true
|
|
71300
|
-
};
|
|
71404
|
+
if (model.toLowerCase() === "default") {
|
|
71405
|
+
return scheduleDefaultRelaunchReply(deps, "user: /model default (revert relaunch)");
|
|
71301
71406
|
}
|
|
71302
|
-
|
|
71303
|
-
|
|
71304
|
-
|
|
71305
|
-
|
|
71306
|
-
|
|
71307
|
-
|
|
71308
|
-
|
|
71309
|
-
|
|
71310
|
-
};
|
|
71311
|
-
}
|
|
71312
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
71313
|
-
return {
|
|
71314
|
-
text: `\u274c Could not schedule model switch: ${deps.escapeHtml(msg)}`,
|
|
71315
|
-
html: true
|
|
71316
|
-
};
|
|
71317
|
-
}
|
|
71407
|
+
return scheduleRelaunchReply(deps, model, `user: /model ${model} (session-only relaunch)`);
|
|
71408
|
+
}
|
|
71409
|
+
function switchingLine(deps, model) {
|
|
71410
|
+
const friendly = isSrModel(model) ? srFriendlyLabel(model) : model;
|
|
71411
|
+
return `\uD83D\uDD04 Switching to \`${deps.escapeHtml(friendly)}\` \u2014 relaunching the session (~30s).`;
|
|
71412
|
+
}
|
|
71413
|
+
function relaunchErrorReply(deps, model, err) {
|
|
71414
|
+
if (isRestartInFlight(err)) {
|
|
71318
71415
|
return {
|
|
71319
|
-
text:
|
|
71320
|
-
`Switching to \`${deps.escapeHtml(model)}\` \u2014 restarting session (~30s).`,
|
|
71321
|
-
PERSIST_NOTE
|
|
71322
|
-
].join(`
|
|
71323
|
-
`),
|
|
71416
|
+
text: `\u23f3 A restart is already in flight \u2014 your switch to \`${deps.escapeHtml(model)}\` will apply as it completes (~15s).`,
|
|
71324
71417
|
html: true
|
|
71325
71418
|
};
|
|
71326
71419
|
}
|
|
71327
|
-
const
|
|
71328
|
-
|
|
71420
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71421
|
+
return { text: `\u274c Could not schedule model switch: ${deps.escapeHtml(msg)}`, html: true };
|
|
71422
|
+
}
|
|
71423
|
+
async function scheduleRelaunchReply(deps, model, reason) {
|
|
71329
71424
|
try {
|
|
71330
|
-
|
|
71331
|
-
successPattern: MODEL_SWITCH_CONFIRMATION_PREFIX,
|
|
71332
|
-
errorPattern: MODEL_SWITCH_ERROR_RE,
|
|
71333
|
-
settleBeforeSendMs: 1500
|
|
71334
|
-
});
|
|
71425
|
+
await deps.scheduleModelRelaunch(model, reason);
|
|
71335
71426
|
} catch (err) {
|
|
71336
|
-
|
|
71337
|
-
return {
|
|
71338
|
-
text: `\u274c ${verbHtml} \u2014 inject failed: ${deps.escapeHtml(msg)}`,
|
|
71339
|
-
html: true
|
|
71340
|
-
};
|
|
71427
|
+
return relaunchErrorReply(deps, model, err);
|
|
71341
71428
|
}
|
|
71342
|
-
|
|
71343
|
-
|
|
71344
|
-
|
|
71345
|
-
|
|
71346
|
-
|
|
71347
|
-
|
|
71348
|
-
|
|
71349
|
-
|
|
71350
|
-
...result.truncated ? ["_truncated_"] : [],
|
|
71351
|
-
PERSIST_NOTE
|
|
71352
|
-
].join(`
|
|
71353
|
-
`),
|
|
71354
|
-
html: true
|
|
71355
|
-
};
|
|
71356
|
-
}
|
|
71357
|
-
const confirmed = sessionModelFromConfirmation(confirmation) ?? model;
|
|
71358
|
-
return {
|
|
71359
|
-
text: [
|
|
71360
|
-
`${verbHtml}`,
|
|
71361
|
-
deps.preBlock(confirmation),
|
|
71362
|
-
...result.truncated ? ["_truncated_"] : [],
|
|
71363
|
-
PERSIST_NOTE
|
|
71364
|
-
].join(`
|
|
71365
|
-
`),
|
|
71366
|
-
html: true,
|
|
71367
|
-
selectedModel: confirmed
|
|
71368
|
-
};
|
|
71369
|
-
}
|
|
71370
|
-
const errLine = result.outcome === "ok" ? modelSwitchErrorLine(result.output) : null;
|
|
71371
|
-
if (errLine) {
|
|
71372
|
-
return {
|
|
71373
|
-
text: [
|
|
71374
|
-
`\u274c ${verbHtml} \u2014 the switch did not take:`,
|
|
71375
|
-
deps.preBlock(errLine),
|
|
71376
|
-
"Check `/model` for a valid, available model."
|
|
71377
|
-
].join(`
|
|
71378
|
-
`),
|
|
71379
|
-
html: true
|
|
71380
|
-
};
|
|
71381
|
-
}
|
|
71382
|
-
const optimisticLabel = optimisticModelRecordLabel(model);
|
|
71383
|
-
return {
|
|
71384
|
-
text: [
|
|
71385
|
-
`${verbHtml} \u2014 sent, but couldn't read a confirmation line. \`/status\` will show the live model once it's confirmed.`,
|
|
71386
|
-
PERSIST_NOTE
|
|
71387
|
-
].join(`
|
|
71388
|
-
`),
|
|
71389
|
-
html: true,
|
|
71390
|
-
selectedModel: optimisticLabel,
|
|
71391
|
-
optimistic: true
|
|
71392
|
-
};
|
|
71393
|
-
}
|
|
71394
|
-
if (result.errorCode === "session_missing") {
|
|
71395
|
-
return {
|
|
71396
|
-
text: "\u274c tmux session not found \u2014 the agent must be running under the tmux supervisor (the default). Remove `experimental.legacy_pty: true` if set.",
|
|
71397
|
-
html: true
|
|
71398
|
-
};
|
|
71429
|
+
return { text: [switchingLine(deps, model), PERSIST_NOTE].join(`
|
|
71430
|
+
`), html: true };
|
|
71431
|
+
}
|
|
71432
|
+
async function scheduleDefaultRelaunchReply(deps, reason) {
|
|
71433
|
+
try {
|
|
71434
|
+
await deps.scheduleModelDefaultRelaunch(reason);
|
|
71435
|
+
} catch (err) {
|
|
71436
|
+
return relaunchErrorReply(deps, "default", err);
|
|
71399
71437
|
}
|
|
71400
71438
|
return {
|
|
71401
|
-
text:
|
|
71439
|
+
text: [
|
|
71440
|
+
"\uD83D\uDD04 Reverting to the configured default model \u2014 relaunching the session (~30s).",
|
|
71441
|
+
PERSIST_NOTE
|
|
71442
|
+
].join(`
|
|
71443
|
+
`),
|
|
71402
71444
|
html: true
|
|
71403
71445
|
};
|
|
71404
71446
|
}
|
|
@@ -71451,15 +71493,6 @@ function expandSrAlias(arg) {
|
|
|
71451
71493
|
function srFriendlyLabel(srName) {
|
|
71452
71494
|
return SR_MODEL_LABELS[srName] ?? srName.replace(/^sr-/, "").replace(/-/g, " ");
|
|
71453
71495
|
}
|
|
71454
|
-
function optimisticModelRecordLabel(token) {
|
|
71455
|
-
if (isSrModel(token))
|
|
71456
|
-
return token;
|
|
71457
|
-
const lower = token.toLowerCase();
|
|
71458
|
-
if (MODEL_ALIASES.includes(lower)) {
|
|
71459
|
-
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
71460
|
-
}
|
|
71461
|
-
return token;
|
|
71462
|
-
}
|
|
71463
71496
|
function classifyDiscoveredOptions(options) {
|
|
71464
71497
|
return {
|
|
71465
71498
|
claude: options.filter((o) => !o.label.startsWith("sr-") && !o.label.includes("/") && (/^[A-Z]/.test(o.label) || o.label.startsWith("claude-"))),
|
|
@@ -71467,7 +71500,19 @@ function classifyDiscoveredOptions(options) {
|
|
|
71467
71500
|
};
|
|
71468
71501
|
}
|
|
71469
71502
|
function modelSelectCallbackData(label) {
|
|
71470
|
-
|
|
71503
|
+
const token = canonicalClaudeToken(label);
|
|
71504
|
+
if (token)
|
|
71505
|
+
return `${MODEL_CALLBACK_SELECT}${token}`;
|
|
71506
|
+
if (/^default\b/i.test(label.trim()))
|
|
71507
|
+
return `${MODEL_CALLBACK_SELECT}default`;
|
|
71508
|
+
return MODEL_CALLBACK_SELECT;
|
|
71509
|
+
}
|
|
71510
|
+
function isRecognizedSwitchToken(token) {
|
|
71511
|
+
if (token.toLowerCase() === "default")
|
|
71512
|
+
return true;
|
|
71513
|
+
if (isSrModel(token))
|
|
71514
|
+
return true;
|
|
71515
|
+
return canonicalClaudeToken(token) !== null;
|
|
71471
71516
|
}
|
|
71472
71517
|
var BUSY_REFUSAL_TEXT = "\u23f3 The agent is mid-turn \u2014 the model picker needs an idle prompt. Your tap was not applied.";
|
|
71473
71518
|
function isBusyRefusalText(text4) {
|
|
@@ -71617,75 +71662,28 @@ async function handleModelMenuCallback(data, deps) {
|
|
|
71617
71662
|
if (data === MODEL_CALLBACK_PAGE_MAIN) {
|
|
71618
71663
|
return { answer: "Back", reply: await buildModelMenu(deps, "main") };
|
|
71619
71664
|
}
|
|
71620
|
-
if (data.startsWith(MODEL_CALLBACK_ALIAS)) {
|
|
71621
|
-
const alias = data.slice(MODEL_CALLBACK_ALIAS.length);
|
|
71622
|
-
if (!isValidModelArg(alias)) {
|
|
71623
|
-
return { answer: "Invalid model name", reply: await buildModelMenu(deps) };
|
|
71624
|
-
}
|
|
71625
|
-
if (deps.isBusy()) {
|
|
71626
|
-
return {
|
|
71627
|
-
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
71628
|
-
reply: { text: BUSY_REFUSAL_TEXT, html: true },
|
|
71629
|
-
toastOnly: true,
|
|
71630
|
-
busyRefusal: true
|
|
71631
|
-
};
|
|
71632
|
-
}
|
|
71633
|
-
let aliasResult;
|
|
71634
|
-
try {
|
|
71635
|
-
aliasResult = await deps.inject(deps.getAgentName(), `/model ${alias}`, {
|
|
71636
|
-
successPattern: MODEL_SWITCH_CONFIRMATION_PREFIX,
|
|
71637
|
-
errorPattern: MODEL_SWITCH_ERROR_RE,
|
|
71638
|
-
settleBeforeSendMs: 1500
|
|
71639
|
-
});
|
|
71640
|
-
} catch (err) {
|
|
71641
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
71642
|
-
return {
|
|
71643
|
-
answer: "Switch failed",
|
|
71644
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(alias)}** failed: ${deps.escapeHtml(msg)}`)
|
|
71645
|
-
};
|
|
71646
|
-
}
|
|
71647
|
-
if (aliasResult.outcome === "ok" || aliasResult.outcome === "ok_no_output") {
|
|
71648
|
-
const confirmation = aliasResult.outcome === "ok" ? modelSwitchConfirmationLine(aliasResult.output) : null;
|
|
71649
|
-
if (confirmation) {
|
|
71650
|
-
const kept = isKeptModelConfirmation(confirmation);
|
|
71651
|
-
return {
|
|
71652
|
-
answer: confirmation,
|
|
71653
|
-
reply: await menuWithBannerStatic(deps, `\u2705 ${deps.escapeHtml(confirmation)}`),
|
|
71654
|
-
...kept ? {} : {
|
|
71655
|
-
selectedModel: sessionModelFromConfirmation(confirmation) ?? optimisticModelRecordLabel(alias),
|
|
71656
|
-
selectedModelToken: alias
|
|
71657
|
-
}
|
|
71658
|
-
};
|
|
71659
|
-
}
|
|
71660
|
-
const aliasErr = aliasResult.outcome === "ok" ? modelSwitchErrorLine(aliasResult.output) : null;
|
|
71661
|
-
if (aliasErr) {
|
|
71662
|
-
return {
|
|
71663
|
-
answer: "Switch failed",
|
|
71664
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(alias)}** did not take: ${deps.escapeHtml(aliasErr)}`)
|
|
71665
|
-
};
|
|
71666
|
-
}
|
|
71667
|
-
const optimisticLabel = optimisticModelRecordLabel(alias);
|
|
71668
|
-
return {
|
|
71669
|
-
answer: `Sent /model ${alias} \u2014 check /status`,
|
|
71670
|
-
reply: await menuWithBannerStatic(deps, `Sent \`/model ${deps.escapeHtml(alias)}\` \u2014 couldn\u2019t read a confirmation line. \`/status\` will show the live model once it\u2019s confirmed.`),
|
|
71671
|
-
selectedModel: optimisticLabel,
|
|
71672
|
-
selectedModelToken: alias
|
|
71673
|
-
};
|
|
71674
|
-
}
|
|
71675
|
-
return {
|
|
71676
|
-
answer: "Switch failed",
|
|
71677
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(alias)}** failed \u2014 agent may be mid-turn`)
|
|
71678
|
-
};
|
|
71679
|
-
}
|
|
71680
71665
|
if (data === MODEL_CALLBACK_HEADER) {
|
|
71681
71666
|
return { answer: "Tap a model in this section to switch", reply: { text: "", html: true }, toastOnly: true };
|
|
71682
71667
|
}
|
|
71683
|
-
if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
71684
|
-
|
|
71685
|
-
|
|
71686
|
-
if (
|
|
71668
|
+
if (data.startsWith(MODEL_CALLBACK_ALIAS) || data.startsWith(MODEL_CALLBACK_SR) || data.startsWith(MODEL_CALLBACK_SELECT)) {
|
|
71669
|
+
let token;
|
|
71670
|
+
let label;
|
|
71671
|
+
if (data.startsWith(MODEL_CALLBACK_ALIAS)) {
|
|
71672
|
+
token = data.slice(MODEL_CALLBACK_ALIAS.length);
|
|
71673
|
+
label = token;
|
|
71674
|
+
} else if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
71675
|
+
token = data.slice(MODEL_CALLBACK_SR.length);
|
|
71676
|
+
label = srFriendlyLabel(token);
|
|
71677
|
+
} else {
|
|
71678
|
+
token = data.slice(MODEL_CALLBACK_SELECT.length);
|
|
71679
|
+
label = token;
|
|
71680
|
+
}
|
|
71681
|
+
if (!isValidModelArg(token)) {
|
|
71687
71682
|
return { answer: "Invalid model name", reply: await buildModelMenu(deps) };
|
|
71688
71683
|
}
|
|
71684
|
+
if (!isRecognizedSwitchToken(token)) {
|
|
71685
|
+
return { answer: "Model list changed \u2014 menu refreshed", reply: await buildModelMenu(deps) };
|
|
71686
|
+
}
|
|
71689
71687
|
if (deps.isBusy()) {
|
|
71690
71688
|
return {
|
|
71691
71689
|
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
@@ -71694,87 +71692,38 @@ async function handleModelMenuCallback(data, deps) {
|
|
|
71694
71692
|
busyRefusal: true
|
|
71695
71693
|
};
|
|
71696
71694
|
}
|
|
71697
|
-
|
|
71698
|
-
|
|
71699
|
-
|
|
71700
|
-
|
|
71695
|
+
return menuRelaunchOutcome(deps, token, label);
|
|
71696
|
+
}
|
|
71697
|
+
return { answer: "Unknown action", reply: await buildModelMenu(deps) };
|
|
71698
|
+
}
|
|
71699
|
+
async function menuRelaunchOutcome(deps, token, label) {
|
|
71700
|
+
const isDefault = token.toLowerCase() === "default";
|
|
71701
|
+
try {
|
|
71702
|
+
if (isDefault) {
|
|
71703
|
+
await deps.scheduleModelDefaultRelaunch("user: /model default (revert relaunch, menu)");
|
|
71704
|
+
} else {
|
|
71705
|
+
await deps.scheduleModelRelaunch(token, `user: /model ${token} (session-only relaunch, menu)`);
|
|
71706
|
+
}
|
|
71707
|
+
} catch (err) {
|
|
71708
|
+
if (isRestartInFlight(err)) {
|
|
71701
71709
|
return {
|
|
71702
|
-
answer: "
|
|
71703
|
-
reply: await menuWithBannerStatic(deps, `\
|
|
71710
|
+
answer: "A restart is already in flight (~15s)",
|
|
71711
|
+
reply: await menuWithBannerStatic(deps, `\u23f3 A restart is already in flight \u2014 your switch to **${deps.escapeHtml(label)}** will apply as it completes (~15s).`)
|
|
71704
71712
|
};
|
|
71705
71713
|
}
|
|
71714
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71706
71715
|
return {
|
|
71707
|
-
answer:
|
|
71708
|
-
reply: await menuWithBannerStatic(deps, `\
|
|
71709
|
-
${PERSIST_NOTE}`),
|
|
71710
|
-
selectedModel: srName
|
|
71711
|
-
};
|
|
71712
|
-
}
|
|
71713
|
-
if (!data.startsWith(MODEL_CALLBACK_SELECT)) {
|
|
71714
|
-
return { answer: "Unknown action", reply: await buildModelMenu(deps) };
|
|
71715
|
-
}
|
|
71716
|
-
if (deps.isBusy()) {
|
|
71717
|
-
return {
|
|
71718
|
-
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
71719
|
-
reply: { text: BUSY_REFUSAL_TEXT, html: true },
|
|
71720
|
-
toastOnly: true,
|
|
71721
|
-
busyRefusal: true
|
|
71722
|
-
};
|
|
71723
|
-
}
|
|
71724
|
-
const tag = data.slice(MODEL_CALLBACK_SELECT.length);
|
|
71725
|
-
const discovered = await deps.discover(deps.getAgentName());
|
|
71726
|
-
if (!discovered.ok) {
|
|
71727
|
-
return {
|
|
71728
|
-
answer: "Picker unavailable",
|
|
71729
|
-
reply: await menuWithBanner(deps, `\u274c Could not open the model picker: ${deps.escapeHtml(discovered.reason)}`)
|
|
71730
|
-
};
|
|
71731
|
-
}
|
|
71732
|
-
const target = discovered.options.find((o) => labelTag(o.label) === tag);
|
|
71733
|
-
if (!target) {
|
|
71734
|
-
const fresh = await buildModelMenu(deps);
|
|
71735
|
-
return { answer: "Model list changed \u2014 menu refreshed", reply: fresh };
|
|
71736
|
-
}
|
|
71737
|
-
const result = await deps.select(deps.getAgentName(), target.label);
|
|
71738
|
-
if (!result.ok) {
|
|
71739
|
-
return {
|
|
71740
|
-
answer: "Switch failed \u2014 see the menu",
|
|
71741
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(target.label)}** failed: ${deps.escapeHtml(result.reason)}`)
|
|
71742
|
-
};
|
|
71743
|
-
}
|
|
71744
|
-
if (isKeptModelConfirmation(result.confirmation)) {
|
|
71745
|
-
return {
|
|
71746
|
-
answer: deps.escapeHtml(result.confirmation),
|
|
71747
|
-
reply: await menuWithBanner(deps, `\u2705 ${deps.escapeHtml(result.confirmation)}`)
|
|
71716
|
+
answer: "Switch failed",
|
|
71717
|
+
reply: await menuWithBannerStatic(deps, `\u274c Switch to **${deps.escapeHtml(label)}** failed: ${deps.escapeHtml(msg)}`)
|
|
71748
71718
|
};
|
|
71749
71719
|
}
|
|
71750
|
-
const
|
|
71751
|
-
const selectedModel = sessionModelFromConfirmation(result.confirmation) ?? token ?? undefined;
|
|
71752
|
-
const clearedDefault = token == null && /^default\b/i.test(target.label.trim());
|
|
71720
|
+
const friendly = isDefault ? "the configured default" : label;
|
|
71753
71721
|
return {
|
|
71754
|
-
answer:
|
|
71755
|
-
reply: await
|
|
71756
|
-
|
|
71757
|
-
...token ? { selectedModelToken: token } : {},
|
|
71758
|
-
...clearedDefault ? { clearedDefault: true } : {}
|
|
71722
|
+
answer: `Switching to ${isDefault ? "default" : label} \u2014 relaunching (~30s)`,
|
|
71723
|
+
reply: await menuWithBannerStatic(deps, `\uD83D\uDD04 Switching session to **${deps.escapeHtml(friendly)}** \u2014 relaunching (~30s).
|
|
71724
|
+
${PERSIST_NOTE}`)
|
|
71759
71725
|
};
|
|
71760
71726
|
}
|
|
71761
|
-
function isSrToClaudeTransition(prevModel, nextModel) {
|
|
71762
|
-
return !!prevModel?.startsWith("sr-") && !nextModel.startsWith("sr-");
|
|
71763
|
-
}
|
|
71764
|
-
function modelSwitchConfirmationLine(output) {
|
|
71765
|
-
const line = output.split(`
|
|
71766
|
-
`).map((l) => l.trim()).find((l) => MODEL_SWITCH_CONFIRMATION_PREFIX.test(l));
|
|
71767
|
-
return line && line.length > 0 ? line : null;
|
|
71768
|
-
}
|
|
71769
|
-
var MODEL_SWITCH_ERROR_RE = /^\s*[\u23fa\u25cf\u2022>\u23bf-]?\s*(?:Error:\s*)?(?:Model(?:\s+'[^']+')?\s+not found|Invalid model|Unknown model|No such model|(?:[\w'\u2019.\-]+\s+){0,4}(?:(?:is |are )?(?:not available|unavailable|not enabled|not supported)|access denied|requires\b[^\n]{0,40}\b(?:subscription|plan)|no access)\b)/i;
|
|
71770
|
-
function modelSwitchErrorLine(output) {
|
|
71771
|
-
const line = output.split(`
|
|
71772
|
-
`).map((l) => l.trim()).find((l) => MODEL_SWITCH_ERROR_RE.test(l));
|
|
71773
|
-
return line && line.length > 0 ? line : null;
|
|
71774
|
-
}
|
|
71775
|
-
function isKeptModelConfirmation(confirmation) {
|
|
71776
|
-
return /^\s*[\u23fa\u25cf\u2022>\u23bf-]?\s*Kept model as\b/i.test(confirmation.trim());
|
|
71777
|
-
}
|
|
71778
71727
|
function canonicalClaudeToken(label) {
|
|
71779
71728
|
const l = label.trim();
|
|
71780
71729
|
if (l.toLowerCase().startsWith("claude-"))
|
|
@@ -71789,21 +71738,6 @@ function canonicalClaudeToken(label) {
|
|
|
71789
71738
|
function isRestartInFlight(err) {
|
|
71790
71739
|
return !!err && typeof err === "object" && err.code === "restart_in_flight";
|
|
71791
71740
|
}
|
|
71792
|
-
var MODEL_SWITCH_CONFIRMATION_PREFIX = /^\s*[\u23fa\u25cf\u2022>\u23bf-]?\s*(?:Set model to|Switched to|Kept model as)\b/i;
|
|
71793
|
-
function sessionModelFromConfirmation(confirmation) {
|
|
71794
|
-
const m = /(?:Set model to|Switched to)\s+(.+?)(?:\s+for (?:this|the) session|\s+and saved\b|\s*\(|\s*$)/i.exec(confirmation.trim());
|
|
71795
|
-
const name = m?.[1]?.trim();
|
|
71796
|
-
return name && name.length > 0 ? name : null;
|
|
71797
|
-
}
|
|
71798
|
-
async function menuWithBanner(deps, banner) {
|
|
71799
|
-
const fresh = await buildModelMenu(deps);
|
|
71800
|
-
return {
|
|
71801
|
-
text: [banner, "", fresh.text].join(`
|
|
71802
|
-
`),
|
|
71803
|
-
html: true,
|
|
71804
|
-
...fresh.keyboard ? { keyboard: fresh.keyboard } : {}
|
|
71805
|
-
};
|
|
71806
|
-
}
|
|
71807
71741
|
async function menuWithBannerStatic(deps, banner) {
|
|
71808
71742
|
const v1 = await handleModelCommand({ kind: "show" }, deps);
|
|
71809
71743
|
return {
|
|
@@ -72215,70 +72149,6 @@ async function discoverModels(agentName3, opts = {}) {
|
|
|
72215
72149
|
return { ok: true, options: parsed.options, currentLabel: current?.label ?? null, ...tail };
|
|
72216
72150
|
});
|
|
72217
72151
|
}
|
|
72218
|
-
async function selectModel(agentName3, targetLabel, opts = {}) {
|
|
72219
|
-
const io = makeIo(agentName3, opts);
|
|
72220
|
-
if (!io.runner.hasSession(io.socket, io.session)) {
|
|
72221
|
-
return { ok: false, reason: "tmux session not found" };
|
|
72222
|
-
}
|
|
72223
|
-
return withPaneLock(`${io.socket}:${io.session}`, async () => {
|
|
72224
|
-
io.startedAt = Date.now();
|
|
72225
|
-
let selected = false;
|
|
72226
|
-
try {
|
|
72227
|
-
const parsed = await openPickerWithRetry(io);
|
|
72228
|
-
if (!parsed || !parsed.footerSeen) {
|
|
72229
|
-
return { ok: false, reason: "picker did not render \u2014 agent may be mid-turn" };
|
|
72230
|
-
}
|
|
72231
|
-
const target = parsed.options.find((o) => o.label === targetLabel);
|
|
72232
|
-
if (!target) {
|
|
72233
|
-
const have = parsed.options.map((o) => o.label).join(", ");
|
|
72234
|
-
return { ok: false, reason: `option "${targetLabel}" not offered (have: ${have})` };
|
|
72235
|
-
}
|
|
72236
|
-
if (parsed.cursorIndex < 0) {
|
|
72237
|
-
return { ok: false, reason: "cursor marker not visible \u2014 refusing blind navigation" };
|
|
72238
|
-
}
|
|
72239
|
-
const delta = target.index - parsed.cursorIndex;
|
|
72240
|
-
const key = delta > 0 ? "Down" : "Up";
|
|
72241
|
-
for (let i = 0;i < Math.abs(delta); i++) {
|
|
72242
|
-
if (expired(io))
|
|
72243
|
-
return { ok: false, reason: "timed out navigating picker" };
|
|
72244
|
-
sendKey(io, key);
|
|
72245
|
-
await io.sleep(Math.min(io.stepMs, 250));
|
|
72246
|
-
}
|
|
72247
|
-
await io.sleep(io.stepMs);
|
|
72248
|
-
const verifyPane = io.runner.capture(io.socket, io.session) ?? "";
|
|
72249
|
-
const verify = parseModelPicker(verifyPane);
|
|
72250
|
-
const atRow = verify?.options.find((o) => o.index === verify.cursorIndex);
|
|
72251
|
-
if (!verify || !atRow || atRow.label !== targetLabel) {
|
|
72252
|
-
return {
|
|
72253
|
-
ok: false,
|
|
72254
|
-
reason: `cursor verification failed (on "${atRow?.label ?? "?"}", wanted "${targetLabel}")`
|
|
72255
|
-
};
|
|
72256
|
-
}
|
|
72257
|
-
sendLiteral(io, "s");
|
|
72258
|
-
selected = true;
|
|
72259
|
-
await io.sleep(io.stepMs);
|
|
72260
|
-
const after = io.runner.capture(io.socket, io.session) ?? "";
|
|
72261
|
-
const confirmation = extractConfirmation(after) ?? `Switched to ${targetLabel} (session)`;
|
|
72262
|
-
return { ok: true, confirmation };
|
|
72263
|
-
} catch (err) {
|
|
72264
|
-
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
72265
|
-
} finally {
|
|
72266
|
-
if (!selected) {
|
|
72267
|
-
await dismissOrWarn(io, "select");
|
|
72268
|
-
}
|
|
72269
|
-
}
|
|
72270
|
-
});
|
|
72271
|
-
}
|
|
72272
|
-
function extractConfirmation(pane) {
|
|
72273
|
-
const lines = pane.split(`
|
|
72274
|
-
`);
|
|
72275
|
-
for (let i = lines.length - 1;i >= 0; i--) {
|
|
72276
|
-
const t = lines[i].replace(/^\s*\u23bf\s*/, "").trim();
|
|
72277
|
-
if (/^(Set model to|Kept model as|Switched to)/i.test(t))
|
|
72278
|
-
return t;
|
|
72279
|
-
}
|
|
72280
|
-
return null;
|
|
72281
|
-
}
|
|
72282
72152
|
|
|
72283
72153
|
// ../src/agents/scaffold.ts
|
|
72284
72154
|
import { join as join33, resolve as resolve6 } from "node:path";
|
|
@@ -77548,17 +77418,20 @@ function computeTurnStatus(turn) {
|
|
|
77548
77418
|
return turn.finalAnswerDelivered ? "complete" : "no_reply";
|
|
77549
77419
|
}
|
|
77550
77420
|
}
|
|
77551
|
-
function
|
|
77421
|
+
function backstopSendOutcomeGated(args) {
|
|
77552
77422
|
if (args.threw)
|
|
77553
77423
|
return "failed";
|
|
77554
77424
|
if (args.chunkCount === 0)
|
|
77555
77425
|
return "failed";
|
|
77556
|
-
|
|
77426
|
+
const freshCount = args.sentIds.filter((id) => args.cardMessageId == null || id !== args.cardMessageId).length;
|
|
77427
|
+
if (freshCount === 0)
|
|
77428
|
+
return "failed";
|
|
77429
|
+
if (freshCount < args.chunkCount)
|
|
77557
77430
|
return "failed";
|
|
77558
77431
|
return "delivered";
|
|
77559
77432
|
}
|
|
77560
|
-
function
|
|
77561
|
-
const outcome =
|
|
77433
|
+
function finalizeBackstopSendGated(turn, send) {
|
|
77434
|
+
const outcome = backstopSendOutcomeGated(send);
|
|
77562
77435
|
turn.deliveryOutcome = outcome;
|
|
77563
77436
|
return outcome;
|
|
77564
77437
|
}
|
|
@@ -77573,6 +77446,120 @@ function buildTurnRecord(turn, endedAt) {
|
|
|
77573
77446
|
};
|
|
77574
77447
|
}
|
|
77575
77448
|
|
|
77449
|
+
// gateway/backstop-delivery.ts
|
|
77450
|
+
class BackstopDeliveryLedger {
|
|
77451
|
+
latched = new Set;
|
|
77452
|
+
chunks = new Map;
|
|
77453
|
+
pending = new Map;
|
|
77454
|
+
claim(turnId) {
|
|
77455
|
+
if (this.latched.has(turnId))
|
|
77456
|
+
return false;
|
|
77457
|
+
this.latched.add(turnId);
|
|
77458
|
+
return true;
|
|
77459
|
+
}
|
|
77460
|
+
release(turnId) {
|
|
77461
|
+
this.latched.delete(turnId);
|
|
77462
|
+
}
|
|
77463
|
+
markPending(turnId, index2) {
|
|
77464
|
+
let set = this.pending.get(turnId);
|
|
77465
|
+
if (set == null) {
|
|
77466
|
+
set = new Set;
|
|
77467
|
+
this.pending.set(turnId, set);
|
|
77468
|
+
}
|
|
77469
|
+
set.add(index2);
|
|
77470
|
+
}
|
|
77471
|
+
recordChunk(turnId, index2, messageIds) {
|
|
77472
|
+
let m = this.chunks.get(turnId);
|
|
77473
|
+
if (m == null) {
|
|
77474
|
+
m = new Map;
|
|
77475
|
+
this.chunks.set(turnId, m);
|
|
77476
|
+
}
|
|
77477
|
+
m.set(index2, messageIds.slice());
|
|
77478
|
+
this.pending.get(turnId)?.delete(index2);
|
|
77479
|
+
}
|
|
77480
|
+
hasChunk(turnId, index2) {
|
|
77481
|
+
return (this.chunks.get(turnId)?.get(index2)?.length ?? 0) > 0;
|
|
77482
|
+
}
|
|
77483
|
+
sentIds(turnId) {
|
|
77484
|
+
const m = this.chunks.get(turnId);
|
|
77485
|
+
if (m == null)
|
|
77486
|
+
return [];
|
|
77487
|
+
const out = [];
|
|
77488
|
+
for (const index2 of Array.from(m.keys()).sort((a, b) => a - b)) {
|
|
77489
|
+
out.push(...m.get(index2) ?? []);
|
|
77490
|
+
}
|
|
77491
|
+
return out;
|
|
77492
|
+
}
|
|
77493
|
+
entries(turnId) {
|
|
77494
|
+
const m = this.chunks.get(turnId);
|
|
77495
|
+
if (m == null)
|
|
77496
|
+
return [];
|
|
77497
|
+
return Array.from(m.keys()).sort((a, b) => a - b).map((index2) => ({ index: index2, messageIds: (m.get(index2) ?? []).slice() }));
|
|
77498
|
+
}
|
|
77499
|
+
unsentIndices(turnId, chunkCount) {
|
|
77500
|
+
const out = [];
|
|
77501
|
+
for (let i = 0;i < chunkCount; i++) {
|
|
77502
|
+
if (!this.hasChunk(turnId, i))
|
|
77503
|
+
out.push(i);
|
|
77504
|
+
}
|
|
77505
|
+
return out;
|
|
77506
|
+
}
|
|
77507
|
+
clear(turnId) {
|
|
77508
|
+
this.latched.delete(turnId);
|
|
77509
|
+
this.chunks.delete(turnId);
|
|
77510
|
+
this.pending.delete(turnId);
|
|
77511
|
+
}
|
|
77512
|
+
}
|
|
77513
|
+
function backstopReceiptIds(sentIds, cardMessageId) {
|
|
77514
|
+
return sentIds.filter((id) => cardMessageId == null || id !== cardMessageId);
|
|
77515
|
+
}
|
|
77516
|
+
async function runBackstopDelivery(ledger, turnId, chunks, cardMessageId, deps, maxAttempts = 3) {
|
|
77517
|
+
const stderr = deps.stderr ?? (() => {});
|
|
77518
|
+
const chunkCount = chunks.length;
|
|
77519
|
+
let attempts = 0;
|
|
77520
|
+
for (let attempt = 1;attempt <= Math.max(1, maxAttempts); attempt++) {
|
|
77521
|
+
attempts = attempt;
|
|
77522
|
+
const resume = ledger.unsentIndices(turnId, chunkCount);
|
|
77523
|
+
if (resume.length === 0)
|
|
77524
|
+
break;
|
|
77525
|
+
if (attempt > 1) {
|
|
77526
|
+
stderr(`telegram gateway: backstop delivery retry ${attempt}/${maxAttempts} \u2014 ` + `resuming at unsent chunk(s) [${resume.join(", ")}] for turn ${turnId}
|
|
77527
|
+
`);
|
|
77528
|
+
}
|
|
77529
|
+
let attemptThrew = false;
|
|
77530
|
+
try {
|
|
77531
|
+
for (const i of resume) {
|
|
77532
|
+
if (ledger.hasChunk(turnId, i))
|
|
77533
|
+
continue;
|
|
77534
|
+
ledger.markPending(turnId, i);
|
|
77535
|
+
const ids = await deps.sendChunk(i, chunks[i]);
|
|
77536
|
+
ledger.recordChunk(turnId, i, ids);
|
|
77537
|
+
}
|
|
77538
|
+
} catch (err) {
|
|
77539
|
+
attemptThrew = true;
|
|
77540
|
+
stderr(`telegram gateway: backstop delivery attempt ${attempt}/${maxAttempts} failed: ` + `${err instanceof Error ? err.message : String(err)}
|
|
77541
|
+
`);
|
|
77542
|
+
}
|
|
77543
|
+
if (!attemptThrew && ledger.unsentIndices(turnId, chunkCount).length === 0)
|
|
77544
|
+
break;
|
|
77545
|
+
}
|
|
77546
|
+
const sentIds = ledger.sentIds(turnId);
|
|
77547
|
+
const delivered = chunkCount > 0 && ledger.unsentIndices(turnId, chunkCount).length === 0 && backstopReceiptIds(sentIds, cardMessageId).length > 0;
|
|
77548
|
+
const exhausted = !delivered;
|
|
77549
|
+
if (deps.recordOutbound && sentIds.length > 0) {
|
|
77550
|
+
const texts = [];
|
|
77551
|
+
const ids = [];
|
|
77552
|
+
for (const { index: index2, messageIds } of ledger.entries(turnId)) {
|
|
77553
|
+
for (const id of messageIds) {
|
|
77554
|
+
ids.push(id);
|
|
77555
|
+
texts.push(chunks[index2] ?? "");
|
|
77556
|
+
}
|
|
77557
|
+
}
|
|
77558
|
+
deps.recordOutbound(ids, texts);
|
|
77559
|
+
}
|
|
77560
|
+
return { sentIds, chunkCount, delivered, attempts, exhausted };
|
|
77561
|
+
}
|
|
77562
|
+
|
|
77576
77563
|
// gateway/inbound-delivery-confirm.ts
|
|
77577
77564
|
function createDeliveryQueue() {
|
|
77578
77565
|
return { pending: new Map };
|
|
@@ -84182,10 +84169,10 @@ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
|
|
|
84182
84169
|
}
|
|
84183
84170
|
|
|
84184
84171
|
// ../src/build-info.ts
|
|
84185
|
-
var VERSION = "0.18.
|
|
84186
|
-
var COMMIT_SHA = "
|
|
84187
|
-
var COMMIT_DATE = "2026-07-
|
|
84188
|
-
var LATEST_PR =
|
|
84172
|
+
var VERSION = "0.18.29";
|
|
84173
|
+
var COMMIT_SHA = "8f9b38b3";
|
|
84174
|
+
var COMMIT_DATE = "2026-07-16T21:47:27+10:00";
|
|
84175
|
+
var LATEST_PR = 3281;
|
|
84189
84176
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
84190
84177
|
|
|
84191
84178
|
// gateway/boot-version.ts
|
|
@@ -86743,6 +86730,73 @@ var deferredDoneReactions = new DeferredDoneReactions({
|
|
|
86743
86730
|
});
|
|
86744
86731
|
var outboundDedup = new OutboundDedupCache;
|
|
86745
86732
|
var flushedTurnSupersede = new FlushedTurnSupersedeRegistry;
|
|
86733
|
+
var backstopDeliveryLedger = new BackstopDeliveryLedger;
|
|
86734
|
+
var BACKSTOP_DELIVERY_MAX_ATTEMPTS = (() => {
|
|
86735
|
+
const raw = Number(process.env.SWITCHROOM_BACKSTOP_DELIVERY_MAX_ATTEMPTS);
|
|
86736
|
+
return Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : 3;
|
|
86737
|
+
})();
|
|
86738
|
+
async function deliverAnswer(args) {
|
|
86739
|
+
const { chatId, turnId } = args;
|
|
86740
|
+
const rendered = addParagraphSpacers2(args.text);
|
|
86741
|
+
const chunks = splitMarkdownChunks2(rendered, RICH_MESSAGE_MAX_CHARS2);
|
|
86742
|
+
const deps = {
|
|
86743
|
+
sendRich: (opts, body, tid) => robustApiCall(() => bot.api.sendRichMessage(chatId, body, opts), { threadId: tid, chat_id: chatId, priorityClass: "critical" }),
|
|
86744
|
+
sendLiteral: (opts, txt, tid) => robustApiCall(() => bot.api.sendMessage(chatId, txt, opts), { threadId: tid, chat_id: chatId, priorityClass: "critical" }),
|
|
86745
|
+
sendLiteralRaw: (opts, txt) => bot.api.sendMessage(chatId, txt, opts),
|
|
86746
|
+
sendRichRaw: (opts, body) => bot.api.sendRichMessage(chatId, body, opts),
|
|
86747
|
+
editPreview: (mid, body, opts, tid) => robustApiCall(() => bot.api.editMessageText(chatId, mid, body, opts), { threadId: tid, chat_id: chatId, priorityClass: "critical", messageId: mid, editPayload: body }),
|
|
86748
|
+
richMessage: richMessage2,
|
|
86749
|
+
logOutbound,
|
|
86750
|
+
deleteStalePreview: async (id) => {
|
|
86751
|
+
await swallowingApiCall(() => bot.api.deleteMessage(chatId, id), { chat_id: chatId, verb: "deliverAnswer.deleteStalePreview" });
|
|
86752
|
+
},
|
|
86753
|
+
stderr: (s) => {
|
|
86754
|
+
process.stderr.write(s);
|
|
86755
|
+
}
|
|
86756
|
+
};
|
|
86757
|
+
let liveThreadId = args.threadId;
|
|
86758
|
+
const sendChunk = async (_chunkIndex, text5) => {
|
|
86759
|
+
const chunkIds = [];
|
|
86760
|
+
const res = await sendReplyChunks(deps, {
|
|
86761
|
+
chatId,
|
|
86762
|
+
chunks: [text5],
|
|
86763
|
+
literalText: false,
|
|
86764
|
+
suppressText: false,
|
|
86765
|
+
threadId: liveThreadId,
|
|
86766
|
+
previewMessageId: null,
|
|
86767
|
+
sentIds: chunkIds,
|
|
86768
|
+
buildSendOpts: (_i, _isLast, tid) => ({
|
|
86769
|
+
...tid != null ? { message_thread_id: tid } : {},
|
|
86770
|
+
link_preview_options: { is_disabled: true }
|
|
86771
|
+
}),
|
|
86772
|
+
buildPreviewEditOpts: () => ({})
|
|
86773
|
+
});
|
|
86774
|
+
liveThreadId = res.threadId;
|
|
86775
|
+
return chunkIds;
|
|
86776
|
+
};
|
|
86777
|
+
const result = await runBackstopDelivery(backstopDeliveryLedger, turnId, chunks, args.cardMessageId, {
|
|
86778
|
+
sendChunk,
|
|
86779
|
+
recordOutbound: HISTORY_ENABLED ? (messageIds, texts) => {
|
|
86780
|
+
try {
|
|
86781
|
+
recordOutbound({
|
|
86782
|
+
chat_id: chatId,
|
|
86783
|
+
thread_id: args.threadId ?? null,
|
|
86784
|
+
message_ids: messageIds,
|
|
86785
|
+
texts
|
|
86786
|
+
});
|
|
86787
|
+
} catch {}
|
|
86788
|
+
} : undefined,
|
|
86789
|
+
stderr: (s) => {
|
|
86790
|
+
process.stderr.write(s);
|
|
86791
|
+
}
|
|
86792
|
+
}, BACKSTOP_DELIVERY_MAX_ATTEMPTS);
|
|
86793
|
+
return {
|
|
86794
|
+
sentIds: result.sentIds,
|
|
86795
|
+
chunkCount: result.chunkCount,
|
|
86796
|
+
delivered: result.delivered,
|
|
86797
|
+
exhausted: result.exhausted
|
|
86798
|
+
};
|
|
86799
|
+
}
|
|
86746
86800
|
var chatAvailableReactions = new Map;
|
|
86747
86801
|
var chatProbesInFlight = new Set;
|
|
86748
86802
|
var activeTurnStartedAt = new Map;
|
|
@@ -87644,7 +87698,7 @@ function endCurrentTurnAtomic(turn, opts) {
|
|
|
87644
87698
|
process.stderr.write(`telegram gateway: status-surface DEGRADED reason=${degraded.reason} turnId=${turn.turnId} chat=${turn.sessionChatId} thread=${turn.sessionThreadId ?? "-"} ${degraded.detail}
|
|
87645
87699
|
`);
|
|
87646
87700
|
}
|
|
87647
|
-
if (OBLIGATION_LEDGER_ENABLED) {
|
|
87701
|
+
if (OBLIGATION_LEDGER_ENABLED && opts?.deferObligationClose !== true) {
|
|
87648
87702
|
if (decideObligationTurnEnd(turn.finalAnswerDelivered, turn.replyCalled) === "close") {
|
|
87649
87703
|
obligationLedger.close(turn.turnId);
|
|
87650
87704
|
} else {
|
|
@@ -93156,7 +93210,7 @@ async function executeGetRecentMessages(args) {
|
|
|
93156
93210
|
const rows = query({ chat_id, thread_id, limit, before_message_id });
|
|
93157
93211
|
const summary = rows.map((r) => {
|
|
93158
93212
|
const who = r.role === "user" ? r.user ?? "user" : "assistant";
|
|
93159
|
-
const time =
|
|
93213
|
+
const time = fmtLocalStamp2(r.ts * 1000, resolveEnvTimezone2());
|
|
93160
93214
|
const attach = r.attachment_kind ? ` [${r.attachment_kind}]` : "";
|
|
93161
93215
|
const replyCtx = r.reply_to_message_id != null ? ` \u21AA\uFE0F#${r.reply_to_message_id}${r.reply_to_text ? `:"${r.reply_to_text.slice(0, 60)}${r.reply_to_text.length > 60 ? "\u2026" : ""}"` : ""}` : "";
|
|
93162
93216
|
const reactionTag = r.user_reaction ? ` [reaction: ${r.user_reaction}]` : "";
|
|
@@ -94288,16 +94342,15 @@ function handleSessionEvent(ev) {
|
|
|
94288
94342
|
}
|
|
94289
94343
|
turn.finalAnswerDelivered = true;
|
|
94290
94344
|
turn.finalAnswerSubstantive = true;
|
|
94291
|
-
|
|
94292
|
-
|
|
94293
|
-
}
|
|
94345
|
+
turn.answerDelivered = true;
|
|
94346
|
+
const backstopLatchClaimed = backstopDeliveryLedger.claim(turn.turnId);
|
|
94294
94347
|
const cardTakeover = progressDriver?.takeOverCard({
|
|
94295
94348
|
chatId: backstopChatId,
|
|
94296
94349
|
threadId: backstopThreadId != null ? String(backstopThreadId) : undefined
|
|
94297
94350
|
}) ?? { wasEmitted: false, turnKey: null };
|
|
94298
94351
|
const backstopCardMessageId = cardTakeover.wasEmitted && cardTakeover.turnKey != null ? getPinnedProgressCardMessageId?.(cardTakeover.turnKey) ?? null : null;
|
|
94299
94352
|
const backstopCardTurnKey = cardTakeover.turnKey;
|
|
94300
|
-
const backstopTurnEndedAt = endCurrentTurnAtomic(turn, { deferRecord: true });
|
|
94353
|
+
const backstopTurnEndedAt = endCurrentTurnAtomic(turn, { deferRecord: true, deferObligationClose: true });
|
|
94301
94354
|
preambleSuppressor.dropNow();
|
|
94302
94355
|
{
|
|
94303
94356
|
const tKey = statusKey(chatId, threadId);
|
|
@@ -94316,80 +94369,47 @@ function handleSessionEvent(ev) {
|
|
|
94316
94369
|
`);
|
|
94317
94370
|
if (backstopTurnEndedAt != null) {
|
|
94318
94371
|
turn.deliveryOutcome = "suppressed";
|
|
94372
|
+
if (OBLIGATION_LEDGER_ENABLED)
|
|
94373
|
+
obligationLedger.close(turn.turnId);
|
|
94319
94374
|
emitTurnRecord(turn, backstopTurnEndedAt);
|
|
94320
94375
|
}
|
|
94321
94376
|
return;
|
|
94322
94377
|
}
|
|
94323
94378
|
} catch {}
|
|
94324
94379
|
}
|
|
94380
|
+
if (!backstopLatchClaimed) {
|
|
94381
|
+
process.stderr.write(`telegram gateway: turn-flush skipped \u2014 turn ${turn.turnId} already claimed the delivery latch
|
|
94382
|
+
`);
|
|
94383
|
+
return;
|
|
94384
|
+
}
|
|
94325
94385
|
process.stderr.write(`telegram gateway: turn-flush firing \u2014 ${capturedText.length} chars without reply tool (chat=${backstopChatId} cardMsgId=${backstopCardMessageId ?? "none"})
|
|
94326
94386
|
`);
|
|
94327
|
-
|
|
94328
|
-
|
|
94329
|
-
|
|
94330
|
-
};
|
|
94331
|
-
const limit = RICH_MESSAGE_MAX_CHARS2;
|
|
94332
|
-
let htmlChunks = [];
|
|
94333
|
-
const sentIds = [];
|
|
94334
|
-
let sendThrew = false;
|
|
94387
|
+
let sentIds = [];
|
|
94388
|
+
let chunkCount = 0;
|
|
94389
|
+
let delivered = false;
|
|
94335
94390
|
try {
|
|
94336
|
-
const
|
|
94337
|
-
|
|
94338
|
-
|
|
94339
|
-
|
|
94340
|
-
|
|
94341
|
-
|
|
94342
|
-
|
|
94343
|
-
|
|
94344
|
-
|
|
94345
|
-
|
|
94346
|
-
});
|
|
94347
|
-
sentIds.push(backstopCardMessageId);
|
|
94348
|
-
firstSendUsedEdit = true;
|
|
94349
|
-
} catch (err) {
|
|
94350
|
-
process.stderr.write(`telegram gateway: turn-flush card-takeover edit failed: ${err.message} \u2014 falling back to sendMessage
|
|
94351
|
-
`);
|
|
94352
|
-
if (err instanceof Error && err.message === "THREAD_NOT_FOUND") {
|
|
94353
|
-
liveThreadId = undefined;
|
|
94354
|
-
}
|
|
94355
|
-
}
|
|
94356
|
-
}
|
|
94357
|
-
const remainingChunks = firstSendUsedEdit ? htmlChunks.slice(1) : htmlChunks;
|
|
94358
|
-
for (const c of remainingChunks) {
|
|
94359
|
-
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => {
|
|
94360
|
-
const opts = {
|
|
94361
|
-
link_preview_options: { is_disabled: true },
|
|
94362
|
-
...tid != null ? { message_thread_id: tid } : {}
|
|
94363
|
-
};
|
|
94364
|
-
return bot.api.sendRichMessage(backstopChatId, richMessage2(c), opts);
|
|
94365
|
-
}, {
|
|
94366
|
-
threadId: liveThreadId,
|
|
94367
|
-
chat_id: backstopChatId,
|
|
94368
|
-
verb: "turn-flush.sendMessage"
|
|
94369
|
-
});
|
|
94370
|
-
if (liveThreadId != null) {
|
|
94371
|
-
const sentMsg = sent;
|
|
94372
|
-
if (sentMsg.message_thread_id == null)
|
|
94373
|
-
liveThreadId = undefined;
|
|
94374
|
-
}
|
|
94375
|
-
sentIds.push(sent.message_id);
|
|
94376
|
-
}
|
|
94377
|
-
if (HISTORY_ENABLED && sentIds.length > 0) {
|
|
94378
|
-
try {
|
|
94379
|
-
recordOutbound({
|
|
94380
|
-
chat_id: backstopChatId,
|
|
94381
|
-
thread_id: backstopThreadId ?? null,
|
|
94382
|
-
message_ids: sentIds,
|
|
94383
|
-
texts: htmlChunks
|
|
94384
|
-
});
|
|
94385
|
-
} catch {}
|
|
94386
|
-
}
|
|
94391
|
+
const delivery = await deliverAnswer({
|
|
94392
|
+
chatId: backstopChatId,
|
|
94393
|
+
threadId: backstopThreadId,
|
|
94394
|
+
text: capturedText,
|
|
94395
|
+
turnId: turn.turnId,
|
|
94396
|
+
cardMessageId: backstopCardMessageId
|
|
94397
|
+
});
|
|
94398
|
+
sentIds = delivery.sentIds;
|
|
94399
|
+
chunkCount = delivery.chunkCount;
|
|
94400
|
+
delivered = delivery.delivered;
|
|
94387
94401
|
outboundDedup.record(backstopChatId, backstopThreadId, capturedText, Date.now(), currentTurn?.registryKey ?? null);
|
|
94388
94402
|
if (sentIds.length > 0) {
|
|
94389
94403
|
flushedTurnSupersede.record(backstopChatId, backstopThreadId, { turnId: turn.turnId, messageIds: sentIds, text: capturedText }, Date.now());
|
|
94390
94404
|
}
|
|
94391
|
-
if (
|
|
94405
|
+
if (!delivered) {
|
|
94406
|
+
if (backstopCtrl)
|
|
94407
|
+
backstopCtrl.finalize("error");
|
|
94408
|
+
backstopDeliveryLedger.release(turn.turnId);
|
|
94409
|
+
turn.answerDelivered = false;
|
|
94410
|
+
} else if (backstopCtrl) {
|
|
94392
94411
|
backstopCtrl.finalize("done");
|
|
94412
|
+
}
|
|
94393
94413
|
if (backstopCardTurnKey != null) {
|
|
94394
94414
|
completeProgressCardTurn?.({
|
|
94395
94415
|
chatId: backstopChatId,
|
|
@@ -94400,21 +94420,33 @@ function handleSessionEvent(ev) {
|
|
|
94400
94420
|
unpinProgressCardForChat?.(backstopChatId, backstopThreadId);
|
|
94401
94421
|
}
|
|
94402
94422
|
} catch (err) {
|
|
94403
|
-
|
|
94404
|
-
process.stderr.write(`telegram gateway: turn-flush send failed: ${err.message}
|
|
94423
|
+
process.stderr.write(`telegram gateway: turn-flush post-delivery bookkeeping failed: ${err.message}
|
|
94405
94424
|
`);
|
|
94406
|
-
|
|
94407
|
-
|
|
94408
|
-
|
|
94425
|
+
if (!delivered) {
|
|
94426
|
+
turn.answerDelivered = false;
|
|
94427
|
+
backstopDeliveryLedger.release(turn.turnId);
|
|
94428
|
+
if (backstopCtrl)
|
|
94429
|
+
backstopCtrl.finalize("error");
|
|
94430
|
+
}
|
|
94409
94431
|
} finally {
|
|
94410
94432
|
if (backstopTurnEndedAt != null) {
|
|
94411
|
-
|
|
94412
|
-
threw:
|
|
94413
|
-
|
|
94414
|
-
chunkCount
|
|
94433
|
+
finalizeBackstopSendGated(turn, {
|
|
94434
|
+
threw: !delivered,
|
|
94435
|
+
sentIds,
|
|
94436
|
+
chunkCount,
|
|
94437
|
+
cardMessageId: backstopCardMessageId
|
|
94415
94438
|
});
|
|
94439
|
+
if (OBLIGATION_LEDGER_ENABLED) {
|
|
94440
|
+
if (delivered) {
|
|
94441
|
+
obligationLedger.close(turn.turnId);
|
|
94442
|
+
} else {
|
|
94443
|
+
turn.finalAnswerDelivered = false;
|
|
94444
|
+
obligationLedger.noteTurnEnded(turn.turnId, Date.now());
|
|
94445
|
+
}
|
|
94446
|
+
}
|
|
94416
94447
|
emitTurnRecord(turn, backstopTurnEndedAt);
|
|
94417
94448
|
}
|
|
94449
|
+
backstopDeliveryLedger.clear(turn.turnId);
|
|
94418
94450
|
}
|
|
94419
94451
|
})();
|
|
94420
94452
|
return;
|
|
@@ -95469,7 +95501,7 @@ ${preBlock(write.output)}`;
|
|
|
95469
95501
|
...msgId != null ? { message_id: String(msgId) } : {},
|
|
95470
95502
|
user: displayUser,
|
|
95471
95503
|
user_id: String(from.id),
|
|
95472
|
-
ts:
|
|
95504
|
+
ts: fmtLocalStamp2((ctx.message?.date ?? 0) * 1000, resolveEnvTimezone2()),
|
|
95473
95505
|
...messageThreadId != null ? { message_thread_id: String(messageThreadId) } : {},
|
|
95474
95506
|
...originTurnId != null ? { origin_turn_id: originTurnId } : {},
|
|
95475
95507
|
...topicScope != null ? { topic_scope: topicScope } : {},
|
|
@@ -96611,7 +96643,6 @@ function buildModelDeps(restartCtx) {
|
|
|
96611
96643
|
return [];
|
|
96612
96644
|
}
|
|
96613
96645
|
},
|
|
96614
|
-
select: (a, label) => selectModel(a, label),
|
|
96615
96646
|
isBusy: () => currentTurn !== null,
|
|
96616
96647
|
getAgentName: getMyAgentName,
|
|
96617
96648
|
getQuotaBrief: async () => {
|
|
@@ -96630,14 +96661,12 @@ function buildModelDeps(restartCtx) {
|
|
|
96630
96661
|
} catch {}
|
|
96631
96662
|
return null;
|
|
96632
96663
|
},
|
|
96633
|
-
inject: injectSlashCommand,
|
|
96634
96664
|
getConfiguredModel: () => {
|
|
96635
96665
|
const data = switchroomExecJson(["agent", "list"]);
|
|
96636
96666
|
return data?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
96637
96667
|
},
|
|
96638
96668
|
escapeHtml: escapeHtmlForTg2,
|
|
96639
96669
|
preBlock,
|
|
96640
|
-
getActiveSessionModel: () => sessionModelSource.getOverride(),
|
|
96641
96670
|
scheduleRestart: async (reason) => {
|
|
96642
96671
|
const name = getMyAgentName();
|
|
96643
96672
|
const existing = readRestartMarker();
|
|
@@ -96680,6 +96709,30 @@ function buildModelDeps(restartCtx) {
|
|
|
96680
96709
|
const prevFileRaw = readSessionModelFileRaw(agentDir);
|
|
96681
96710
|
writeSessionModelFile(agentDir, model, readConfiguredDefaultModel(agentDir) ?? resolveMainModel(deps.getConfiguredModel() ?? undefined));
|
|
96682
96711
|
sessionModelSource.setOverride(model);
|
|
96712
|
+
process.stderr.write(`telegram gateway: gw /model relaunch scheduled agent=${getMyAgentName()} token=${model} reason=${JSON.stringify(reason)}
|
|
96713
|
+
`);
|
|
96714
|
+
clearPremiumRecoveryOnManualSwitch(model);
|
|
96715
|
+
try {
|
|
96716
|
+
await deps.scheduleRestart(reason);
|
|
96717
|
+
} catch (err) {
|
|
96718
|
+
if (err?.code !== "restart_in_flight") {
|
|
96719
|
+
restoreSessionModelFileRaw(agentDir, prevFileRaw);
|
|
96720
|
+
sessionModelSource.setOverride(prevOverride);
|
|
96721
|
+
}
|
|
96722
|
+
throw err;
|
|
96723
|
+
}
|
|
96724
|
+
},
|
|
96725
|
+
scheduleModelDefaultRelaunch: async (reason) => {
|
|
96726
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
96727
|
+
if (!agentDir)
|
|
96728
|
+
throw new Error("agent dir unresolvable \u2014 cannot clear session-model file");
|
|
96729
|
+
const prevOverride = sessionModelSource.getOverride();
|
|
96730
|
+
const prevFileRaw = readSessionModelFileRaw(agentDir);
|
|
96731
|
+
clearSessionModelFile(agentDir);
|
|
96732
|
+
sessionModelSource.setOverride(null);
|
|
96733
|
+
process.stderr.write(`telegram gateway: gw /model relaunch scheduled agent=${getMyAgentName()} token=(default) reason=${JSON.stringify(reason)}
|
|
96734
|
+
`);
|
|
96735
|
+
clearPremiumRecoveryOnManualSwitch(null);
|
|
96683
96736
|
try {
|
|
96684
96737
|
await deps.scheduleRestart(reason);
|
|
96685
96738
|
} catch (err) {
|
|
@@ -96704,60 +96757,6 @@ function modelMenuReplyMarkup(reply) {
|
|
|
96704
96757
|
}
|
|
96705
96758
|
return kb;
|
|
96706
96759
|
}
|
|
96707
|
-
function recordTypedModelSwitch(reply, requestedModelArg, _deps) {
|
|
96708
|
-
const requested = requestedModelArg != null ? expandSrAlias(requestedModelArg) : null;
|
|
96709
|
-
if (requested?.toLowerCase() === "default") {
|
|
96710
|
-
const smDir = resolveAgentDirFromEnv();
|
|
96711
|
-
if (smDir)
|
|
96712
|
-
clearSessionModelFile(smDir);
|
|
96713
|
-
if (reply.selectedModel)
|
|
96714
|
-
sessionModelSource.setOverride(null);
|
|
96715
|
-
return "";
|
|
96716
|
-
}
|
|
96717
|
-
if (!reply.selectedModel)
|
|
96718
|
-
return "";
|
|
96719
|
-
sessionModelSource.setOverride(reply.selectedModel);
|
|
96720
|
-
clearPremiumRecoveryOnManualSwitch(reply.selectedModel);
|
|
96721
|
-
return "";
|
|
96722
|
-
}
|
|
96723
|
-
function recordModelMenuSideEffects(outcome, modelDeps, cbChatId, cbThreadId, prevSessionModel) {
|
|
96724
|
-
if (outcome.selectedModel) {
|
|
96725
|
-
sessionModelSource.setOverride(outcome.selectedModel);
|
|
96726
|
-
clearPremiumRecoveryOnManualSwitch(outcome.selectedModel);
|
|
96727
|
-
}
|
|
96728
|
-
if (outcome.clearedDefault) {
|
|
96729
|
-
const smDir = resolveAgentDirFromEnv();
|
|
96730
|
-
if (smDir)
|
|
96731
|
-
clearSessionModelFile(smDir);
|
|
96732
|
-
}
|
|
96733
|
-
if (outcome.selectedModel && isSrToClaudeTransition(prevSessionModel, outcome.selectedModel)) {
|
|
96734
|
-
const agentName3 = getMyAgentName();
|
|
96735
|
-
const agentDir = resolveAgentDirFromEnv();
|
|
96736
|
-
const token = outcome.selectedModelToken;
|
|
96737
|
-
if (agentDir && token) {
|
|
96738
|
-
try {
|
|
96739
|
-
writeSessionModelFile(agentDir, token, readConfiguredDefaultModel(agentDir) ?? resolveMainModel(modelDeps.getConfiguredModel() ?? undefined));
|
|
96740
|
-
sessionModelSource.setOverride(token);
|
|
96741
|
-
} catch (e) {
|
|
96742
|
-
process.stderr.write(`telegram gateway: sr-to-claude session-model write failed: ${e?.message ?? String(e)}
|
|
96743
|
-
`);
|
|
96744
|
-
}
|
|
96745
|
-
} else if (agentDir) {
|
|
96746
|
-
clearSessionModelFile(agentDir);
|
|
96747
|
-
}
|
|
96748
|
-
writeRestartMarker({ chat_id: cbChatId, thread_id: cbThreadId ?? null, ack_message_id: null, ts: Date.now() });
|
|
96749
|
-
stampUserRestartReason("user: sr-to-claude model switch (menu)");
|
|
96750
|
-
if (turnInFlightForGate()) {
|
|
96751
|
-
pendingRestarts.set(agentName3, Date.now());
|
|
96752
|
-
} else {
|
|
96753
|
-
sweepBeforeSelfRestart().finally(() => triggerSelfRestart(agentName3, "sr-to-claude-model-switch", 1500));
|
|
96754
|
-
}
|
|
96755
|
-
return {
|
|
96756
|
-
restartNotice: `\uD83D\uDD04 Switching from **${escapeHtmlForTg2(prevSessionModel)}** back to Claude \u2014 restarting session cleanly. Claude will be ready in ~30s.`
|
|
96757
|
-
};
|
|
96758
|
-
}
|
|
96759
|
-
return {};
|
|
96760
|
-
}
|
|
96761
96760
|
async function editPendingCommandCard(chatId, messageId, text5) {
|
|
96762
96761
|
try {
|
|
96763
96762
|
await robustApiCall(() => lockedBot.api.editMessageText(chatId, messageId, richMessage2(hardenCardBreaks2(text5)), {
|
|
@@ -96779,14 +96778,11 @@ function enqueueSessionCommand(cmd) {
|
|
|
96779
96778
|
async function applyQueuedModelCommand(cmd) {
|
|
96780
96779
|
const deps = buildModelDeps({ chatId: cmd.chatId, threadId: cmd.threadId });
|
|
96781
96780
|
if (cmd.origin === "menu") {
|
|
96782
|
-
const prevSessionModel = sessionModelSource.getOverride();
|
|
96783
96781
|
const outcome = await handleModelMenuCallback(cmd.arg, deps);
|
|
96784
|
-
|
|
96785
|
-
return restartNotice ?? outcome.reply.text;
|
|
96782
|
+
return outcome.reply.text;
|
|
96786
96783
|
}
|
|
96787
96784
|
const reply = await handleModelCommand({ kind: "set", model: cmd.arg }, deps);
|
|
96788
|
-
|
|
96789
|
-
return reply.text + warning;
|
|
96785
|
+
return reply.text;
|
|
96790
96786
|
}
|
|
96791
96787
|
async function applyQueuedEffortCommand(cmd) {
|
|
96792
96788
|
const deps = buildEffortDeps();
|
|
@@ -96924,8 +96920,7 @@ bot.command("model", async (ctx) => {
|
|
|
96924
96920
|
return;
|
|
96925
96921
|
}
|
|
96926
96922
|
const reply = await handleModelCommand(parsed, deps);
|
|
96927
|
-
|
|
96928
|
-
await switchroomReply(ctx, reply.text + persistWarning, { html: reply.html });
|
|
96923
|
+
await switchroomReply(ctx, reply.text, { html: reply.html });
|
|
96929
96924
|
});
|
|
96930
96925
|
var sessionEffortOverride = null;
|
|
96931
96926
|
function buildEffortDeps() {
|
|
@@ -98979,31 +98974,10 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
98979
98974
|
}
|
|
98980
98975
|
const isPageNav = data === MODEL_CALLBACK_PAGE_EXTERNAL || data === MODEL_CALLBACK_PAGE_MAIN;
|
|
98981
98976
|
await ctx.answerCallbackQuery({ text: isPageNav ? "Loading\u2026" : "Switching\u2026" }).catch(() => {});
|
|
98982
|
-
if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
98983
|
-
const srName = data.slice(MODEL_CALLBACK_SR.length);
|
|
98984
|
-
const srLabel = escapeHtmlForTg2(srFriendlyLabel(srName));
|
|
98985
|
-
if (!isValidModelArg(srName)) {
|
|
98986
|
-
await ctx.editMessageText(richMessage2("\u274C Invalid model name"), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
98987
|
-
return;
|
|
98988
|
-
}
|
|
98989
|
-
await ctx.editMessageText(richMessage2(`\uD83D\uDD04 Switching session to **${srLabel}** \u2014 restarting (~30s). _Session-only; reverts to the configured default on the next restart._`), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
98990
|
-
try {
|
|
98991
|
-
await modelDeps.scheduleModelRelaunch(srName, `user: /model ${srName} (session-only relaunch, menu)`);
|
|
98992
|
-
} catch (err) {
|
|
98993
|
-
await ctx.editMessageText(richMessage2(`\u274C Could not switch to **${srLabel}**: ${escapeHtmlForTg2(err?.message ?? String(err))}`), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
98994
|
-
}
|
|
98995
|
-
return;
|
|
98996
|
-
}
|
|
98997
98977
|
try {
|
|
98998
|
-
const prevSessionModel = sessionModelSource.getOverride();
|
|
98999
98978
|
const outcome = await handleModelMenuCallback(data, modelDeps);
|
|
99000
98979
|
if (outcome.toastOnly)
|
|
99001
98980
|
return;
|
|
99002
|
-
const { restartNotice } = recordModelMenuSideEffects(outcome, modelDeps, cbChatId, cbThreadId, prevSessionModel);
|
|
99003
|
-
if (restartNotice) {
|
|
99004
|
-
await ctx.editMessageText(richMessage2(restartNotice), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
99005
|
-
return;
|
|
99006
|
-
}
|
|
99007
98981
|
await ctx.editMessageText(richMessage2(outcome.reply.text), {
|
|
99008
98982
|
reply_markup: modelMenuReplyMarkup(outcome.reply) ?? { inline_keyboard: [] }
|
|
99009
98983
|
}).catch(() => {});
|
|
@@ -100708,6 +100682,8 @@ var didOneTimeSetup = false;
|
|
|
100708
100682
|
`);
|
|
100709
100683
|
}
|
|
100710
100684
|
} catch {}
|
|
100685
|
+
let modelSwitchMarkerChat = null;
|
|
100686
|
+
let modelSwitchReason = null;
|
|
100711
100687
|
try {
|
|
100712
100688
|
const nowMs2 = Date.now();
|
|
100713
100689
|
const marker = readRestartMarker();
|
|
@@ -100737,6 +100713,12 @@ var didOneTimeSetup = false;
|
|
|
100737
100713
|
const ageSec = Math.max(1, Math.round(ageMs / 1000));
|
|
100738
100714
|
process.stderr.write(`telegram gateway: boot: restart-marker present, chat_id=${marker.chat_id} age=${ageSec}s within5min=${ageMs < 300000}
|
|
100739
100715
|
`);
|
|
100716
|
+
if (ageMs < 300000) {
|
|
100717
|
+
modelSwitchMarkerChat = { chatId: marker.chat_id, threadId: marker.thread_id };
|
|
100718
|
+
if (typeof cleanMarker?.reason === "string" && cleanMarker.reason.startsWith("user: /model")) {
|
|
100719
|
+
modelSwitchReason = cleanMarker.reason;
|
|
100720
|
+
}
|
|
100721
|
+
}
|
|
100740
100722
|
clearRestartMarker();
|
|
100741
100723
|
}
|
|
100742
100724
|
if (!isRealRestart) {
|
|
@@ -100762,7 +100744,11 @@ var didOneTimeSetup = false;
|
|
|
100762
100744
|
firstSeenAt: new Date
|
|
100763
100745
|
});
|
|
100764
100746
|
}
|
|
100765
|
-
|
|
100747
|
+
const suppressBootCardForModelSwitch = modelSwitchReason != null && modelSwitchMarkerChat != null;
|
|
100748
|
+
if (target && suppressBootCardForModelSwitch) {
|
|
100749
|
+
process.stderr.write(`telegram gateway: boot: suppressing generic boot card \u2014 /model switch apply-boot (reason=${JSON.stringify(modelSwitchReason)}); the confirmation card replaces it
|
|
100750
|
+
`);
|
|
100751
|
+
} else if (target) {
|
|
100766
100752
|
const { chatId, threadId, ackMsgId } = target;
|
|
100767
100753
|
const bootDedupe = shouldSkipDuplicateBootCard({ activeBootCard, bootCardPending }, "boot");
|
|
100768
100754
|
if (bootDedupe.skip) {
|
|
@@ -100841,7 +100827,19 @@ var didOneTimeSetup = false;
|
|
|
100841
100827
|
const raw = d?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
100842
100828
|
return resolveMainModel(raw ?? undefined);
|
|
100843
100829
|
})();
|
|
100844
|
-
|
|
100830
|
+
const isApplyBoot = launched.length > 0 && launched !== configured;
|
|
100831
|
+
sessionModelSource.setOverride(isApplyBoot ? launched : null);
|
|
100832
|
+
process.stderr.write(`telegram gateway: gw /model relaunch applied agent=${getMyAgentName()} launched=${launched || "(none)"} configured=${configured} override=${isApplyBoot ? "set" : "cleared"}
|
|
100833
|
+
`);
|
|
100834
|
+
if (modelSwitchReason != null && modelSwitchMarkerChat) {
|
|
100835
|
+
const chat = modelSwitchMarkerChat;
|
|
100836
|
+
const body = isApplyBoot ? `\u2705 Now running \`${launched}\` \u2014 session-only, reverts to the configured model on the next restart. Fresh session; memory and the handoff briefing carry the context.` : `\u2705 Now running \`${launched || configured}\` (the configured default) \u2014 fresh session; memory and the handoff briefing carry the context.`;
|
|
100837
|
+
lockedBot.api.sendMessage(chat.chatId, body, {
|
|
100838
|
+
parse_mode: "Markdown",
|
|
100839
|
+
...chat.threadId != null ? { message_thread_id: chat.threadId } : {}
|
|
100840
|
+
}).catch((err) => process.stderr.write(`telegram gateway: model-switch confirmation send failed: ${err?.message ?? String(err)}
|
|
100841
|
+
`));
|
|
100842
|
+
}
|
|
100845
100843
|
} catch {}
|
|
100846
100844
|
}
|
|
100847
100845
|
const activeEffortPath = join55(smAgentDir, ".active-session-effort");
|