switchroom 0.18.27 → 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 +655 -587
- package/telegram-plugin/gateway/backstop-delivery.ts +272 -0
- package/telegram-plugin/gateway/forward-origin.ts +9 -1
- package/telegram-plugin/gateway/gateway.ts +511 -397
- package/telegram-plugin/gateway/model-command.ts +227 -602
- package/telegram-plugin/gateway/turn-record-status.ts +45 -0
- package/telegram-plugin/gateway/worker-pin-reaper.ts +54 -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/tests/worker-feed-migration-eviction.test.ts +140 -0
- package/telegram-plugin/tests/worker-pin-reaper.test.ts +78 -0
- package/telegram-plugin/tier-downgrade.ts +4 -3
- package/telegram-plugin/turn-flush-safety.ts +25 -1
- package/telegram-plugin/worker-activity-feed.ts +78 -5
- 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",
|
|
@@ -41118,6 +41275,12 @@ function createWorkerActivityFeed(opts) {
|
|
|
41118
41275
|
agentIndex.delete(agentId);
|
|
41119
41276
|
maybeDeleteGroup(g);
|
|
41120
41277
|
}
|
|
41278
|
+
function evictRowFromGroup(g, agentId) {
|
|
41279
|
+
g.workers.delete(agentId);
|
|
41280
|
+
if (agentIndex.get(agentId) === g.feedKey)
|
|
41281
|
+
agentIndex.delete(agentId);
|
|
41282
|
+
maybeDeleteGroup(g);
|
|
41283
|
+
}
|
|
41121
41284
|
function maybeDeleteGroup(g) {
|
|
41122
41285
|
if (g.workers.size === 0 && g.pendingFinalize.size === 0)
|
|
41123
41286
|
groups.delete(g.feedKey);
|
|
@@ -41309,7 +41472,7 @@ function createWorkerActivityFeed(opts) {
|
|
|
41309
41472
|
if (row.finished)
|
|
41310
41473
|
staleFinished.push({ g, agentId: row.agentId, reason });
|
|
41311
41474
|
else
|
|
41312
|
-
staleAgentIds.push({ agentId: row.agentId, reason });
|
|
41475
|
+
staleAgentIds.push({ g, agentId: row.agentId, reason });
|
|
41313
41476
|
}
|
|
41314
41477
|
}
|
|
41315
41478
|
for (const { g, agentId, reason } of staleFinished) {
|
|
@@ -41323,15 +41486,22 @@ function createWorkerActivityFeed(opts) {
|
|
|
41323
41486
|
removeWorker(g, agentId);
|
|
41324
41487
|
syncPin(g);
|
|
41325
41488
|
}
|
|
41326
|
-
for (const { agentId, reason } of staleAgentIds) {
|
|
41327
|
-
const row =
|
|
41489
|
+
for (const { g, agentId, reason } of staleAgentIds) {
|
|
41490
|
+
const row = g.workers.get(agentId);
|
|
41328
41491
|
if (reason === "absolute") {
|
|
41329
41492
|
const age = Math.floor((now - (row?.createdAtMs ?? now)) / 1000);
|
|
41330
41493
|
log(`worker-feed: ABSOLUTE cap reap agent=${agentId} \u2014 row age ${age}s (>= ${Math.floor(absoluteRowLifetimeCapMs / 1000)}s); force-terminating immortal row (survives lastUpdateAt reset)`);
|
|
41331
41494
|
} else {
|
|
41332
41495
|
log(`worker-feed: TTL reap agent=${agentId} \u2014 no update in ${Math.floor((now - (row?.lastUpdateAt ?? now)) / 1000)}s (>= ${Math.floor(staleWorkerTtlMs / 1000)}s); force-terminating leaked row`);
|
|
41333
41496
|
}
|
|
41334
|
-
|
|
41497
|
+
if (groupOfAgent(agentId) === g) {
|
|
41498
|
+
terminateWorker(agentId);
|
|
41499
|
+
} else {
|
|
41500
|
+
log(`worker-feed: force-evicting leaked row agent=${agentId} feed=${g.feedKey} \u2014 agentIndex desynced (points elsewhere); removing directly`);
|
|
41501
|
+
markFinalized(agentId);
|
|
41502
|
+
evictRowFromGroup(g, agentId);
|
|
41503
|
+
syncPin(g);
|
|
41504
|
+
}
|
|
41335
41505
|
}
|
|
41336
41506
|
for (const g of [...groups.values()]) {
|
|
41337
41507
|
if (g.pendingFinalize.size > 0 && now >= g.cooldownUntil) {
|
|
@@ -41378,6 +41548,14 @@ function createWorkerActivityFeed(opts) {
|
|
|
41378
41548
|
}
|
|
41379
41549
|
}
|
|
41380
41550
|
heartbeatTimer = setIntervalFn(heartbeatTick, heartbeatTickMs);
|
|
41551
|
+
opts.exposeTestControls?.({
|
|
41552
|
+
repointAgentIndex: (agentId, feedKey) => {
|
|
41553
|
+
if (feedKey == null)
|
|
41554
|
+
agentIndex.delete(agentId);
|
|
41555
|
+
else
|
|
41556
|
+
agentIndex.set(agentId, feedKey);
|
|
41557
|
+
}
|
|
41558
|
+
});
|
|
41381
41559
|
return {
|
|
41382
41560
|
has(agentId) {
|
|
41383
41561
|
const g = groupOfAgent(agentId);
|
|
@@ -41405,6 +41583,14 @@ function createWorkerActivityFeed(opts) {
|
|
|
41405
41583
|
if (existingRow?.finished === true)
|
|
41406
41584
|
return Promise.resolve();
|
|
41407
41585
|
const feedKey = feedKeyOf(chatId, threadId);
|
|
41586
|
+
const priorFeedKey = agentIndex.get(agentId);
|
|
41587
|
+
if (priorFeedKey != null && priorFeedKey !== feedKey) {
|
|
41588
|
+
const priorGroup = groups.get(priorFeedKey);
|
|
41589
|
+
if (priorGroup != null) {
|
|
41590
|
+
evictRowFromGroup(priorGroup, agentId);
|
|
41591
|
+
syncPin(priorGroup);
|
|
41592
|
+
}
|
|
41593
|
+
}
|
|
41408
41594
|
let g = groups.get(feedKey);
|
|
41409
41595
|
if (g == null) {
|
|
41410
41596
|
g = {
|
|
@@ -57142,40 +57328,6 @@ function sendGateConfigFromEnv(env = process.env) {
|
|
|
57142
57328
|
return out;
|
|
57143
57329
|
}
|
|
57144
57330
|
|
|
57145
|
-
// shared/local-time.ts
|
|
57146
|
-
function fmtLocalClock(ms, tz) {
|
|
57147
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
57148
|
-
timeZone: tz,
|
|
57149
|
-
hour: "numeric",
|
|
57150
|
-
minute: "2-digit",
|
|
57151
|
-
hour12: true
|
|
57152
|
-
}).format(new Date(ms)).replace(/\s([AP])M$/, (_m, p) => `${p.toLowerCase()}m`);
|
|
57153
|
-
}
|
|
57154
|
-
function fmtLocalDate(ms, tz) {
|
|
57155
|
-
return new Intl.DateTimeFormat("en-GB", {
|
|
57156
|
-
timeZone: tz,
|
|
57157
|
-
day: "numeric",
|
|
57158
|
-
month: "short"
|
|
57159
|
-
}).format(new Date(ms));
|
|
57160
|
-
}
|
|
57161
|
-
function localDay(ms, tz) {
|
|
57162
|
-
return new Intl.DateTimeFormat("en-CA", {
|
|
57163
|
-
timeZone: tz,
|
|
57164
|
-
year: "numeric",
|
|
57165
|
-
month: "2-digit",
|
|
57166
|
-
day: "2-digit"
|
|
57167
|
-
}).format(new Date(ms));
|
|
57168
|
-
}
|
|
57169
|
-
function tzAbbrev(ms, tz) {
|
|
57170
|
-
const at = new Date(ms);
|
|
57171
|
-
for (const loc of ["en-US", "en-AU", "en-GB"]) {
|
|
57172
|
-
const v = new Intl.DateTimeFormat(loc, { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value;
|
|
57173
|
-
if (v && !/^(?:GMT|UTC)/i.test(v))
|
|
57174
|
-
return v;
|
|
57175
|
-
}
|
|
57176
|
-
return new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value ?? tz;
|
|
57177
|
-
}
|
|
57178
|
-
|
|
57179
57331
|
// send-gate-observability.ts
|
|
57180
57332
|
function formatStatsLine(stats) {
|
|
57181
57333
|
const c = stats.global;
|
|
@@ -57210,7 +57362,7 @@ function createStatsLogger(config) {
|
|
|
57210
57362
|
function isAlertableScope(scopeKey) {
|
|
57211
57363
|
return scopeKey === "global" || scopeKey.startsWith("chat:") || scopeKey.startsWith("group:");
|
|
57212
57364
|
}
|
|
57213
|
-
function
|
|
57365
|
+
function fmtLocalStamp3(ms, tz, refMs) {
|
|
57214
57366
|
const clock = fmtLocalClock(ms, tz);
|
|
57215
57367
|
const sameDay = refMs != null && localDay(ms, tz) === localDay(refMs, tz);
|
|
57216
57368
|
const body = sameDay ? clock : `${fmtLocalDate(ms, tz)} ${clock}`;
|
|
@@ -57254,7 +57406,7 @@ function createFloodWindowObserver(config) {
|
|
|
57254
57406
|
}
|
|
57255
57407
|
function openAlertText(w, now) {
|
|
57256
57408
|
const openFor = fmtDur(now - w.observedAt);
|
|
57257
|
-
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.`;
|
|
57258
57410
|
}
|
|
57259
57411
|
function closeAlertText(recs) {
|
|
57260
57412
|
const observedAt = Math.min(...recs.map((r) => r.observedAt));
|
|
@@ -63285,6 +63437,7 @@ function endsWithSilentMarker(text4) {
|
|
|
63285
63437
|
return false;
|
|
63286
63438
|
return isSilentFlushMarker(lines[lines.length - 1]);
|
|
63287
63439
|
}
|
|
63440
|
+
var FLUSH_SUBSTANTIVE_MIN_CHARS = 200;
|
|
63288
63441
|
function selectFlushDeliveryText(blocks) {
|
|
63289
63442
|
const candidates = blocks.map((b) => b.trim()).filter((b) => b.length > 0);
|
|
63290
63443
|
if (candidates.length === 0)
|
|
@@ -63299,8 +63452,18 @@ function selectFlushDeliveryText(blocks) {
|
|
|
63299
63452
|
`);
|
|
63300
63453
|
}
|
|
63301
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
|
+
}
|
|
63302
63465
|
function isNarrationBlock(block) {
|
|
63303
|
-
return NARRATION_OPENER.test(block.trimStart());
|
|
63466
|
+
return NARRATION_OPENER.test(block.trimStart()) || isTrailingNarrationLine(block);
|
|
63304
63467
|
}
|
|
63305
63468
|
function decideTurnFlush(input) {
|
|
63306
63469
|
const flushEnabled = input.flushEnabled !== false;
|
|
@@ -69185,7 +69348,7 @@ function endsWithSilentMarker2(text4) {
|
|
|
69185
69348
|
return false;
|
|
69186
69349
|
return isSilentFlushMarker2(lines[lines.length - 1]);
|
|
69187
69350
|
}
|
|
69188
|
-
var
|
|
69351
|
+
var FLUSH_SUBSTANTIVE_MIN_CHARS2 = 200;
|
|
69189
69352
|
function selectFlushDeliveryText2(blocks) {
|
|
69190
69353
|
const candidates = blocks.map((b) => b.trim()).filter((b) => b.length > 0);
|
|
69191
69354
|
if (candidates.length === 0)
|
|
@@ -69200,8 +69363,18 @@ function selectFlushDeliveryText2(blocks) {
|
|
|
69200
69363
|
`);
|
|
69201
69364
|
}
|
|
69202
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
|
+
}
|
|
69203
69376
|
function isNarrationBlock2(block) {
|
|
69204
|
-
return NARRATION_OPENER2.test(block.trimStart());
|
|
69377
|
+
return NARRATION_OPENER2.test(block.trimStart()) || isTrailingNarrationLine2(block);
|
|
69205
69378
|
}
|
|
69206
69379
|
function decideTurnFlush2(input) {
|
|
69207
69380
|
const flushEnabled = input.flushEnabled !== false;
|
|
@@ -71136,16 +71309,6 @@ Allowed: \`${deps.escapeHtml(allow)}\``, { html: true });
|
|
|
71136
71309
|
await deps.reply(ctx, finalText, { html: true, accent });
|
|
71137
71310
|
}
|
|
71138
71311
|
|
|
71139
|
-
// ../src/agents/model-picker.ts
|
|
71140
|
-
function labelTag(label) {
|
|
71141
|
-
let h = 2166136261;
|
|
71142
|
-
for (const ch of label) {
|
|
71143
|
-
h ^= ch.codePointAt(0);
|
|
71144
|
-
h = Math.imul(h, 16777619) >>> 0;
|
|
71145
|
-
}
|
|
71146
|
-
return h.toString(16).padStart(8, "0");
|
|
71147
|
-
}
|
|
71148
|
-
|
|
71149
71312
|
// gateway/model-command.ts
|
|
71150
71313
|
var MODEL_ALIASES = ["opus", "sonnet", "haiku", "fable", "default"];
|
|
71151
71314
|
var MODEL_ARG_RE = /^[A-Za-z0-9][A-Za-z0-9._/\[\]-]{0,99}$/;
|
|
@@ -71155,12 +71318,6 @@ function isValidModelArg(arg) {
|
|
|
71155
71318
|
function isSrModel(name) {
|
|
71156
71319
|
return name.startsWith("sr-");
|
|
71157
71320
|
}
|
|
71158
|
-
function isClaudeModel(name) {
|
|
71159
|
-
const lower = name.toLowerCase();
|
|
71160
|
-
if (MODEL_ALIASES.includes(lower))
|
|
71161
|
-
return true;
|
|
71162
|
-
return lower.startsWith("claude-");
|
|
71163
|
-
}
|
|
71164
71321
|
function parseModelCommand(text4) {
|
|
71165
71322
|
const m = text4.match(/^\/model(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/);
|
|
71166
71323
|
if (!m)
|
|
@@ -71204,13 +71361,13 @@ function modelCommandReceiptLine(agent, parsed, busy) {
|
|
|
71204
71361
|
const arg = parsed.kind === "set" ? parsed.model : parsed.kind === "show" ? "(show)" : "(help)";
|
|
71205
71362
|
return `telegram gateway: gw /model received agent=${agent} kind=${parsed.kind} arg=${arg} busy=${busy}`;
|
|
71206
71363
|
}
|
|
71207
|
-
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._";
|
|
71208
71365
|
function helpText2(deps, reason) {
|
|
71209
71366
|
const srAliasExamples = Object.keys(SR_MODEL_ALIASES).map((a) => `\`${a}\``).join(" \u00b7 ");
|
|
71210
71367
|
const lines = [];
|
|
71211
71368
|
if (reason)
|
|
71212
71369
|
lines.push(`\u26a0\ufe0f ${deps.escapeHtml(reason)}`);
|
|
71213
|
-
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);
|
|
71214
71371
|
return { text: lines.join(`
|
|
71215
71372
|
`), html: true };
|
|
71216
71373
|
}
|
|
@@ -71244,132 +71401,46 @@ async function handleModelCommand(parsed, deps) {
|
|
|
71244
71401
|
html: true
|
|
71245
71402
|
};
|
|
71246
71403
|
}
|
|
71247
|
-
|
|
71248
|
-
|
|
71249
|
-
try {
|
|
71250
|
-
await deps.scheduleModelRelaunch(model, `user: /model ${model} (sr-to-claude restart)`);
|
|
71251
|
-
} catch (err) {
|
|
71252
|
-
if (isRestartInFlight(err)) {
|
|
71253
|
-
return {
|
|
71254
|
-
text: `\u23f3 A restart is already in flight \u2014 your switch to \`${deps.escapeHtml(model)}\` will apply as it completes (~15s).`,
|
|
71255
|
-
html: true
|
|
71256
|
-
};
|
|
71257
|
-
}
|
|
71258
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
71259
|
-
return {
|
|
71260
|
-
text: `\u274c Could not schedule restart: ${deps.escapeHtml(msg)}`,
|
|
71261
|
-
html: true
|
|
71262
|
-
};
|
|
71263
|
-
}
|
|
71264
|
-
return {
|
|
71265
|
-
text: [
|
|
71266
|
-
`Switching from \`${deps.escapeHtml(currentSession)}\` back to Claude \u2014 restarting session cleanly. Claude will be ready in ~30s.`,
|
|
71267
|
-
PERSIST_NOTE
|
|
71268
|
-
].join(`
|
|
71269
|
-
`),
|
|
71270
|
-
html: true
|
|
71271
|
-
};
|
|
71404
|
+
if (model.toLowerCase() === "default") {
|
|
71405
|
+
return scheduleDefaultRelaunchReply(deps, "user: /model default (revert relaunch)");
|
|
71272
71406
|
}
|
|
71273
|
-
|
|
71274
|
-
|
|
71275
|
-
|
|
71276
|
-
|
|
71277
|
-
|
|
71278
|
-
|
|
71279
|
-
|
|
71280
|
-
|
|
71281
|
-
};
|
|
71282
|
-
}
|
|
71283
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
71284
|
-
return {
|
|
71285
|
-
text: `\u274c Could not schedule model switch: ${deps.escapeHtml(msg)}`,
|
|
71286
|
-
html: true
|
|
71287
|
-
};
|
|
71288
|
-
}
|
|
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)) {
|
|
71289
71415
|
return {
|
|
71290
|
-
text:
|
|
71291
|
-
`Switching to \`${deps.escapeHtml(model)}\` \u2014 restarting session (~30s).`,
|
|
71292
|
-
PERSIST_NOTE
|
|
71293
|
-
].join(`
|
|
71294
|
-
`),
|
|
71416
|
+
text: `\u23f3 A restart is already in flight \u2014 your switch to \`${deps.escapeHtml(model)}\` will apply as it completes (~15s).`,
|
|
71295
71417
|
html: true
|
|
71296
71418
|
};
|
|
71297
71419
|
}
|
|
71298
|
-
const
|
|
71299
|
-
|
|
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) {
|
|
71300
71424
|
try {
|
|
71301
|
-
|
|
71302
|
-
successPattern: MODEL_SWITCH_CONFIRMATION_PREFIX,
|
|
71303
|
-
errorPattern: MODEL_SWITCH_ERROR_RE,
|
|
71304
|
-
settleBeforeSendMs: 1500
|
|
71305
|
-
});
|
|
71425
|
+
await deps.scheduleModelRelaunch(model, reason);
|
|
71306
71426
|
} catch (err) {
|
|
71307
|
-
|
|
71308
|
-
return {
|
|
71309
|
-
text: `\u274c ${verbHtml} \u2014 inject failed: ${deps.escapeHtml(msg)}`,
|
|
71310
|
-
html: true
|
|
71311
|
-
};
|
|
71427
|
+
return relaunchErrorReply(deps, model, err);
|
|
71312
71428
|
}
|
|
71313
|
-
|
|
71314
|
-
|
|
71315
|
-
|
|
71316
|
-
|
|
71317
|
-
|
|
71318
|
-
|
|
71319
|
-
|
|
71320
|
-
|
|
71321
|
-
...result.truncated ? ["_truncated_"] : [],
|
|
71322
|
-
PERSIST_NOTE
|
|
71323
|
-
].join(`
|
|
71324
|
-
`),
|
|
71325
|
-
html: true
|
|
71326
|
-
};
|
|
71327
|
-
}
|
|
71328
|
-
const confirmed = sessionModelFromConfirmation(confirmation) ?? model;
|
|
71329
|
-
return {
|
|
71330
|
-
text: [
|
|
71331
|
-
`${verbHtml}`,
|
|
71332
|
-
deps.preBlock(confirmation),
|
|
71333
|
-
...result.truncated ? ["_truncated_"] : [],
|
|
71334
|
-
PERSIST_NOTE
|
|
71335
|
-
].join(`
|
|
71336
|
-
`),
|
|
71337
|
-
html: true,
|
|
71338
|
-
selectedModel: confirmed
|
|
71339
|
-
};
|
|
71340
|
-
}
|
|
71341
|
-
const errLine = result.outcome === "ok" ? modelSwitchErrorLine(result.output) : null;
|
|
71342
|
-
if (errLine) {
|
|
71343
|
-
return {
|
|
71344
|
-
text: [
|
|
71345
|
-
`\u274c ${verbHtml} \u2014 the switch did not take:`,
|
|
71346
|
-
deps.preBlock(errLine),
|
|
71347
|
-
"Check `/model` for a valid, available model."
|
|
71348
|
-
].join(`
|
|
71349
|
-
`),
|
|
71350
|
-
html: true
|
|
71351
|
-
};
|
|
71352
|
-
}
|
|
71353
|
-
const optimisticLabel = optimisticModelRecordLabel(model);
|
|
71354
|
-
return {
|
|
71355
|
-
text: [
|
|
71356
|
-
`${verbHtml} \u2014 sent, but couldn't read a confirmation line. \`/status\` will show the live model once it's confirmed.`,
|
|
71357
|
-
PERSIST_NOTE
|
|
71358
|
-
].join(`
|
|
71359
|
-
`),
|
|
71360
|
-
html: true,
|
|
71361
|
-
selectedModel: optimisticLabel,
|
|
71362
|
-
optimistic: true
|
|
71363
|
-
};
|
|
71364
|
-
}
|
|
71365
|
-
if (result.errorCode === "session_missing") {
|
|
71366
|
-
return {
|
|
71367
|
-
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.",
|
|
71368
|
-
html: true
|
|
71369
|
-
};
|
|
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);
|
|
71370
71437
|
}
|
|
71371
71438
|
return {
|
|
71372
|
-
text:
|
|
71439
|
+
text: [
|
|
71440
|
+
"\uD83D\uDD04 Reverting to the configured default model \u2014 relaunching the session (~30s).",
|
|
71441
|
+
PERSIST_NOTE
|
|
71442
|
+
].join(`
|
|
71443
|
+
`),
|
|
71373
71444
|
html: true
|
|
71374
71445
|
};
|
|
71375
71446
|
}
|
|
@@ -71422,15 +71493,6 @@ function expandSrAlias(arg) {
|
|
|
71422
71493
|
function srFriendlyLabel(srName) {
|
|
71423
71494
|
return SR_MODEL_LABELS[srName] ?? srName.replace(/^sr-/, "").replace(/-/g, " ");
|
|
71424
71495
|
}
|
|
71425
|
-
function optimisticModelRecordLabel(token) {
|
|
71426
|
-
if (isSrModel(token))
|
|
71427
|
-
return token;
|
|
71428
|
-
const lower = token.toLowerCase();
|
|
71429
|
-
if (MODEL_ALIASES.includes(lower)) {
|
|
71430
|
-
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
71431
|
-
}
|
|
71432
|
-
return token;
|
|
71433
|
-
}
|
|
71434
71496
|
function classifyDiscoveredOptions(options) {
|
|
71435
71497
|
return {
|
|
71436
71498
|
claude: options.filter((o) => !o.label.startsWith("sr-") && !o.label.includes("/") && (/^[A-Z]/.test(o.label) || o.label.startsWith("claude-"))),
|
|
@@ -71438,7 +71500,19 @@ function classifyDiscoveredOptions(options) {
|
|
|
71438
71500
|
};
|
|
71439
71501
|
}
|
|
71440
71502
|
function modelSelectCallbackData(label) {
|
|
71441
|
-
|
|
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;
|
|
71442
71516
|
}
|
|
71443
71517
|
var BUSY_REFUSAL_TEXT = "\u23f3 The agent is mid-turn \u2014 the model picker needs an idle prompt. Your tap was not applied.";
|
|
71444
71518
|
function isBusyRefusalText(text4) {
|
|
@@ -71588,75 +71662,28 @@ async function handleModelMenuCallback(data, deps) {
|
|
|
71588
71662
|
if (data === MODEL_CALLBACK_PAGE_MAIN) {
|
|
71589
71663
|
return { answer: "Back", reply: await buildModelMenu(deps, "main") };
|
|
71590
71664
|
}
|
|
71591
|
-
if (data.startsWith(MODEL_CALLBACK_ALIAS)) {
|
|
71592
|
-
const alias = data.slice(MODEL_CALLBACK_ALIAS.length);
|
|
71593
|
-
if (!isValidModelArg(alias)) {
|
|
71594
|
-
return { answer: "Invalid model name", reply: await buildModelMenu(deps) };
|
|
71595
|
-
}
|
|
71596
|
-
if (deps.isBusy()) {
|
|
71597
|
-
return {
|
|
71598
|
-
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
71599
|
-
reply: { text: BUSY_REFUSAL_TEXT, html: true },
|
|
71600
|
-
toastOnly: true,
|
|
71601
|
-
busyRefusal: true
|
|
71602
|
-
};
|
|
71603
|
-
}
|
|
71604
|
-
let aliasResult;
|
|
71605
|
-
try {
|
|
71606
|
-
aliasResult = await deps.inject(deps.getAgentName(), `/model ${alias}`, {
|
|
71607
|
-
successPattern: MODEL_SWITCH_CONFIRMATION_PREFIX,
|
|
71608
|
-
errorPattern: MODEL_SWITCH_ERROR_RE,
|
|
71609
|
-
settleBeforeSendMs: 1500
|
|
71610
|
-
});
|
|
71611
|
-
} catch (err) {
|
|
71612
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
71613
|
-
return {
|
|
71614
|
-
answer: "Switch failed",
|
|
71615
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(alias)}** failed: ${deps.escapeHtml(msg)}`)
|
|
71616
|
-
};
|
|
71617
|
-
}
|
|
71618
|
-
if (aliasResult.outcome === "ok" || aliasResult.outcome === "ok_no_output") {
|
|
71619
|
-
const confirmation = aliasResult.outcome === "ok" ? modelSwitchConfirmationLine(aliasResult.output) : null;
|
|
71620
|
-
if (confirmation) {
|
|
71621
|
-
const kept = isKeptModelConfirmation(confirmation);
|
|
71622
|
-
return {
|
|
71623
|
-
answer: confirmation,
|
|
71624
|
-
reply: await menuWithBannerStatic(deps, `\u2705 ${deps.escapeHtml(confirmation)}`),
|
|
71625
|
-
...kept ? {} : {
|
|
71626
|
-
selectedModel: sessionModelFromConfirmation(confirmation) ?? optimisticModelRecordLabel(alias),
|
|
71627
|
-
selectedModelToken: alias
|
|
71628
|
-
}
|
|
71629
|
-
};
|
|
71630
|
-
}
|
|
71631
|
-
const aliasErr = aliasResult.outcome === "ok" ? modelSwitchErrorLine(aliasResult.output) : null;
|
|
71632
|
-
if (aliasErr) {
|
|
71633
|
-
return {
|
|
71634
|
-
answer: "Switch failed",
|
|
71635
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(alias)}** did not take: ${deps.escapeHtml(aliasErr)}`)
|
|
71636
|
-
};
|
|
71637
|
-
}
|
|
71638
|
-
const optimisticLabel = optimisticModelRecordLabel(alias);
|
|
71639
|
-
return {
|
|
71640
|
-
answer: `Sent /model ${alias} \u2014 check /status`,
|
|
71641
|
-
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.`),
|
|
71642
|
-
selectedModel: optimisticLabel,
|
|
71643
|
-
selectedModelToken: alias
|
|
71644
|
-
};
|
|
71645
|
-
}
|
|
71646
|
-
return {
|
|
71647
|
-
answer: "Switch failed",
|
|
71648
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(alias)}** failed \u2014 agent may be mid-turn`)
|
|
71649
|
-
};
|
|
71650
|
-
}
|
|
71651
71665
|
if (data === MODEL_CALLBACK_HEADER) {
|
|
71652
71666
|
return { answer: "Tap a model in this section to switch", reply: { text: "", html: true }, toastOnly: true };
|
|
71653
71667
|
}
|
|
71654
|
-
if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
71655
|
-
|
|
71656
|
-
|
|
71657
|
-
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)) {
|
|
71658
71682
|
return { answer: "Invalid model name", reply: await buildModelMenu(deps) };
|
|
71659
71683
|
}
|
|
71684
|
+
if (!isRecognizedSwitchToken(token)) {
|
|
71685
|
+
return { answer: "Model list changed \u2014 menu refreshed", reply: await buildModelMenu(deps) };
|
|
71686
|
+
}
|
|
71660
71687
|
if (deps.isBusy()) {
|
|
71661
71688
|
return {
|
|
71662
71689
|
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
@@ -71665,87 +71692,38 @@ async function handleModelMenuCallback(data, deps) {
|
|
|
71665
71692
|
busyRefusal: true
|
|
71666
71693
|
};
|
|
71667
71694
|
}
|
|
71668
|
-
|
|
71669
|
-
|
|
71670
|
-
|
|
71671
|
-
|
|
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)) {
|
|
71672
71709
|
return {
|
|
71673
|
-
answer: "
|
|
71674
|
-
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).`)
|
|
71675
71712
|
};
|
|
71676
71713
|
}
|
|
71714
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71677
71715
|
return {
|
|
71678
|
-
answer:
|
|
71679
|
-
reply: await menuWithBannerStatic(deps, `\
|
|
71680
|
-
${PERSIST_NOTE}`),
|
|
71681
|
-
selectedModel: srName
|
|
71682
|
-
};
|
|
71683
|
-
}
|
|
71684
|
-
if (!data.startsWith(MODEL_CALLBACK_SELECT)) {
|
|
71685
|
-
return { answer: "Unknown action", reply: await buildModelMenu(deps) };
|
|
71686
|
-
}
|
|
71687
|
-
if (deps.isBusy()) {
|
|
71688
|
-
return {
|
|
71689
|
-
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
71690
|
-
reply: { text: BUSY_REFUSAL_TEXT, html: true },
|
|
71691
|
-
toastOnly: true,
|
|
71692
|
-
busyRefusal: true
|
|
71693
|
-
};
|
|
71694
|
-
}
|
|
71695
|
-
const tag = data.slice(MODEL_CALLBACK_SELECT.length);
|
|
71696
|
-
const discovered = await deps.discover(deps.getAgentName());
|
|
71697
|
-
if (!discovered.ok) {
|
|
71698
|
-
return {
|
|
71699
|
-
answer: "Picker unavailable",
|
|
71700
|
-
reply: await menuWithBanner(deps, `\u274c Could not open the model picker: ${deps.escapeHtml(discovered.reason)}`)
|
|
71701
|
-
};
|
|
71702
|
-
}
|
|
71703
|
-
const target = discovered.options.find((o) => labelTag(o.label) === tag);
|
|
71704
|
-
if (!target) {
|
|
71705
|
-
const fresh = await buildModelMenu(deps);
|
|
71706
|
-
return { answer: "Model list changed \u2014 menu refreshed", reply: fresh };
|
|
71707
|
-
}
|
|
71708
|
-
const result = await deps.select(deps.getAgentName(), target.label);
|
|
71709
|
-
if (!result.ok) {
|
|
71710
|
-
return {
|
|
71711
|
-
answer: "Switch failed \u2014 see the menu",
|
|
71712
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(target.label)}** failed: ${deps.escapeHtml(result.reason)}`)
|
|
71713
|
-
};
|
|
71714
|
-
}
|
|
71715
|
-
if (isKeptModelConfirmation(result.confirmation)) {
|
|
71716
|
-
return {
|
|
71717
|
-
answer: deps.escapeHtml(result.confirmation),
|
|
71718
|
-
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)}`)
|
|
71719
71718
|
};
|
|
71720
71719
|
}
|
|
71721
|
-
const
|
|
71722
|
-
const selectedModel = sessionModelFromConfirmation(result.confirmation) ?? token ?? undefined;
|
|
71723
|
-
const clearedDefault = token == null && /^default\b/i.test(target.label.trim());
|
|
71720
|
+
const friendly = isDefault ? "the configured default" : label;
|
|
71724
71721
|
return {
|
|
71725
|
-
answer:
|
|
71726
|
-
reply: await
|
|
71727
|
-
|
|
71728
|
-
...token ? { selectedModelToken: token } : {},
|
|
71729
|
-
...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}`)
|
|
71730
71725
|
};
|
|
71731
71726
|
}
|
|
71732
|
-
function isSrToClaudeTransition(prevModel, nextModel) {
|
|
71733
|
-
return !!prevModel?.startsWith("sr-") && !nextModel.startsWith("sr-");
|
|
71734
|
-
}
|
|
71735
|
-
function modelSwitchConfirmationLine(output) {
|
|
71736
|
-
const line = output.split(`
|
|
71737
|
-
`).map((l) => l.trim()).find((l) => MODEL_SWITCH_CONFIRMATION_PREFIX.test(l));
|
|
71738
|
-
return line && line.length > 0 ? line : null;
|
|
71739
|
-
}
|
|
71740
|
-
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;
|
|
71741
|
-
function modelSwitchErrorLine(output) {
|
|
71742
|
-
const line = output.split(`
|
|
71743
|
-
`).map((l) => l.trim()).find((l) => MODEL_SWITCH_ERROR_RE.test(l));
|
|
71744
|
-
return line && line.length > 0 ? line : null;
|
|
71745
|
-
}
|
|
71746
|
-
function isKeptModelConfirmation(confirmation) {
|
|
71747
|
-
return /^\s*[\u23fa\u25cf\u2022>\u23bf-]?\s*Kept model as\b/i.test(confirmation.trim());
|
|
71748
|
-
}
|
|
71749
71727
|
function canonicalClaudeToken(label) {
|
|
71750
71728
|
const l = label.trim();
|
|
71751
71729
|
if (l.toLowerCase().startsWith("claude-"))
|
|
@@ -71760,21 +71738,6 @@ function canonicalClaudeToken(label) {
|
|
|
71760
71738
|
function isRestartInFlight(err) {
|
|
71761
71739
|
return !!err && typeof err === "object" && err.code === "restart_in_flight";
|
|
71762
71740
|
}
|
|
71763
|
-
var MODEL_SWITCH_CONFIRMATION_PREFIX = /^\s*[\u23fa\u25cf\u2022>\u23bf-]?\s*(?:Set model to|Switched to|Kept model as)\b/i;
|
|
71764
|
-
function sessionModelFromConfirmation(confirmation) {
|
|
71765
|
-
const m = /(?:Set model to|Switched to)\s+(.+?)(?:\s+for (?:this|the) session|\s+and saved\b|\s*\(|\s*$)/i.exec(confirmation.trim());
|
|
71766
|
-
const name = m?.[1]?.trim();
|
|
71767
|
-
return name && name.length > 0 ? name : null;
|
|
71768
|
-
}
|
|
71769
|
-
async function menuWithBanner(deps, banner) {
|
|
71770
|
-
const fresh = await buildModelMenu(deps);
|
|
71771
|
-
return {
|
|
71772
|
-
text: [banner, "", fresh.text].join(`
|
|
71773
|
-
`),
|
|
71774
|
-
html: true,
|
|
71775
|
-
...fresh.keyboard ? { keyboard: fresh.keyboard } : {}
|
|
71776
|
-
};
|
|
71777
|
-
}
|
|
71778
71741
|
async function menuWithBannerStatic(deps, banner) {
|
|
71779
71742
|
const v1 = await handleModelCommand({ kind: "show" }, deps);
|
|
71780
71743
|
return {
|
|
@@ -72186,70 +72149,6 @@ async function discoverModels(agentName3, opts = {}) {
|
|
|
72186
72149
|
return { ok: true, options: parsed.options, currentLabel: current?.label ?? null, ...tail };
|
|
72187
72150
|
});
|
|
72188
72151
|
}
|
|
72189
|
-
async function selectModel(agentName3, targetLabel, opts = {}) {
|
|
72190
|
-
const io = makeIo(agentName3, opts);
|
|
72191
|
-
if (!io.runner.hasSession(io.socket, io.session)) {
|
|
72192
|
-
return { ok: false, reason: "tmux session not found" };
|
|
72193
|
-
}
|
|
72194
|
-
return withPaneLock(`${io.socket}:${io.session}`, async () => {
|
|
72195
|
-
io.startedAt = Date.now();
|
|
72196
|
-
let selected = false;
|
|
72197
|
-
try {
|
|
72198
|
-
const parsed = await openPickerWithRetry(io);
|
|
72199
|
-
if (!parsed || !parsed.footerSeen) {
|
|
72200
|
-
return { ok: false, reason: "picker did not render \u2014 agent may be mid-turn" };
|
|
72201
|
-
}
|
|
72202
|
-
const target = parsed.options.find((o) => o.label === targetLabel);
|
|
72203
|
-
if (!target) {
|
|
72204
|
-
const have = parsed.options.map((o) => o.label).join(", ");
|
|
72205
|
-
return { ok: false, reason: `option "${targetLabel}" not offered (have: ${have})` };
|
|
72206
|
-
}
|
|
72207
|
-
if (parsed.cursorIndex < 0) {
|
|
72208
|
-
return { ok: false, reason: "cursor marker not visible \u2014 refusing blind navigation" };
|
|
72209
|
-
}
|
|
72210
|
-
const delta = target.index - parsed.cursorIndex;
|
|
72211
|
-
const key = delta > 0 ? "Down" : "Up";
|
|
72212
|
-
for (let i = 0;i < Math.abs(delta); i++) {
|
|
72213
|
-
if (expired(io))
|
|
72214
|
-
return { ok: false, reason: "timed out navigating picker" };
|
|
72215
|
-
sendKey(io, key);
|
|
72216
|
-
await io.sleep(Math.min(io.stepMs, 250));
|
|
72217
|
-
}
|
|
72218
|
-
await io.sleep(io.stepMs);
|
|
72219
|
-
const verifyPane = io.runner.capture(io.socket, io.session) ?? "";
|
|
72220
|
-
const verify = parseModelPicker(verifyPane);
|
|
72221
|
-
const atRow = verify?.options.find((o) => o.index === verify.cursorIndex);
|
|
72222
|
-
if (!verify || !atRow || atRow.label !== targetLabel) {
|
|
72223
|
-
return {
|
|
72224
|
-
ok: false,
|
|
72225
|
-
reason: `cursor verification failed (on "${atRow?.label ?? "?"}", wanted "${targetLabel}")`
|
|
72226
|
-
};
|
|
72227
|
-
}
|
|
72228
|
-
sendLiteral(io, "s");
|
|
72229
|
-
selected = true;
|
|
72230
|
-
await io.sleep(io.stepMs);
|
|
72231
|
-
const after = io.runner.capture(io.socket, io.session) ?? "";
|
|
72232
|
-
const confirmation = extractConfirmation(after) ?? `Switched to ${targetLabel} (session)`;
|
|
72233
|
-
return { ok: true, confirmation };
|
|
72234
|
-
} catch (err) {
|
|
72235
|
-
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
72236
|
-
} finally {
|
|
72237
|
-
if (!selected) {
|
|
72238
|
-
await dismissOrWarn(io, "select");
|
|
72239
|
-
}
|
|
72240
|
-
}
|
|
72241
|
-
});
|
|
72242
|
-
}
|
|
72243
|
-
function extractConfirmation(pane) {
|
|
72244
|
-
const lines = pane.split(`
|
|
72245
|
-
`);
|
|
72246
|
-
for (let i = lines.length - 1;i >= 0; i--) {
|
|
72247
|
-
const t = lines[i].replace(/^\s*\u23bf\s*/, "").trim();
|
|
72248
|
-
if (/^(Set model to|Kept model as|Switched to)/i.test(t))
|
|
72249
|
-
return t;
|
|
72250
|
-
}
|
|
72251
|
-
return null;
|
|
72252
|
-
}
|
|
72253
72152
|
|
|
72254
72153
|
// ../src/agents/scaffold.ts
|
|
72255
72154
|
import { join as join33, resolve as resolve6 } from "node:path";
|
|
@@ -76759,6 +76658,28 @@ function workerAgentIdOfPinKey(pinKey) {
|
|
|
76759
76658
|
const agentId = pinKey.slice(WORKER_PIN_KEY_PREFIX.length);
|
|
76760
76659
|
return agentId.length > 0 ? agentId : null;
|
|
76761
76660
|
}
|
|
76661
|
+
function storeOnlyWorkerPinCandidates(args) {
|
|
76662
|
+
const out = [];
|
|
76663
|
+
for (const r of args.rows) {
|
|
76664
|
+
if (workerAgentIdOfPinKey(r.pinKey) == null)
|
|
76665
|
+
continue;
|
|
76666
|
+
if (r.pending)
|
|
76667
|
+
continue;
|
|
76668
|
+
if (r.expiresAt != null)
|
|
76669
|
+
continue;
|
|
76670
|
+
if (r.chatId.length === 0)
|
|
76671
|
+
continue;
|
|
76672
|
+
if (args.inMemoryPinKeys.has(r.pinKey))
|
|
76673
|
+
continue;
|
|
76674
|
+
out.push({
|
|
76675
|
+
pinKey: r.pinKey,
|
|
76676
|
+
chatId: r.chatId,
|
|
76677
|
+
pinnedAt: args.now,
|
|
76678
|
+
messageId: r.messageId
|
|
76679
|
+
});
|
|
76680
|
+
}
|
|
76681
|
+
return out;
|
|
76682
|
+
}
|
|
76762
76683
|
function decideWorkerPinReaps(args) {
|
|
76763
76684
|
const reaps = [];
|
|
76764
76685
|
for (const pin of args.pins) {
|
|
@@ -77497,17 +77418,20 @@ function computeTurnStatus(turn) {
|
|
|
77497
77418
|
return turn.finalAnswerDelivered ? "complete" : "no_reply";
|
|
77498
77419
|
}
|
|
77499
77420
|
}
|
|
77500
|
-
function
|
|
77421
|
+
function backstopSendOutcomeGated(args) {
|
|
77501
77422
|
if (args.threw)
|
|
77502
77423
|
return "failed";
|
|
77503
77424
|
if (args.chunkCount === 0)
|
|
77504
77425
|
return "failed";
|
|
77505
|
-
|
|
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)
|
|
77506
77430
|
return "failed";
|
|
77507
77431
|
return "delivered";
|
|
77508
77432
|
}
|
|
77509
|
-
function
|
|
77510
|
-
const outcome =
|
|
77433
|
+
function finalizeBackstopSendGated(turn, send) {
|
|
77434
|
+
const outcome = backstopSendOutcomeGated(send);
|
|
77511
77435
|
turn.deliveryOutcome = outcome;
|
|
77512
77436
|
return outcome;
|
|
77513
77437
|
}
|
|
@@ -77522,6 +77446,120 @@ function buildTurnRecord(turn, endedAt) {
|
|
|
77522
77446
|
};
|
|
77523
77447
|
}
|
|
77524
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
|
+
|
|
77525
77563
|
// gateway/inbound-delivery-confirm.ts
|
|
77526
77564
|
function createDeliveryQueue() {
|
|
77527
77565
|
return { pending: new Map };
|
|
@@ -84131,10 +84169,10 @@ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
|
|
|
84131
84169
|
}
|
|
84132
84170
|
|
|
84133
84171
|
// ../src/build-info.ts
|
|
84134
|
-
var VERSION = "0.18.
|
|
84135
|
-
var COMMIT_SHA = "
|
|
84136
|
-
var COMMIT_DATE = "2026-07-
|
|
84137
|
-
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;
|
|
84138
84176
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
84139
84177
|
|
|
84140
84178
|
// gateway/boot-version.ts
|
|
@@ -86692,6 +86730,73 @@ var deferredDoneReactions = new DeferredDoneReactions({
|
|
|
86692
86730
|
});
|
|
86693
86731
|
var outboundDedup = new OutboundDedupCache;
|
|
86694
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
|
+
}
|
|
86695
86800
|
var chatAvailableReactions = new Map;
|
|
86696
86801
|
var chatProbesInFlight = new Set;
|
|
86697
86802
|
var activeTurnStartedAt = new Map;
|
|
@@ -87593,7 +87698,7 @@ function endCurrentTurnAtomic(turn, opts) {
|
|
|
87593
87698
|
process.stderr.write(`telegram gateway: status-surface DEGRADED reason=${degraded.reason} turnId=${turn.turnId} chat=${turn.sessionChatId} thread=${turn.sessionThreadId ?? "-"} ${degraded.detail}
|
|
87594
87699
|
`);
|
|
87595
87700
|
}
|
|
87596
|
-
if (OBLIGATION_LEDGER_ENABLED) {
|
|
87701
|
+
if (OBLIGATION_LEDGER_ENABLED && opts?.deferObligationClose !== true) {
|
|
87597
87702
|
if (decideObligationTurnEnd(turn.finalAnswerDelivered, turn.replyCalled) === "close") {
|
|
87598
87703
|
obligationLedger.close(turn.turnId);
|
|
87599
87704
|
} else {
|
|
@@ -89325,11 +89430,19 @@ async function runMidSessionCardReaper() {
|
|
|
89325
89430
|
}
|
|
89326
89431
|
if (WORKER_PIN_REAPER_ENABLED && PIN_STATUS_WHILE_WORKING) {
|
|
89327
89432
|
try {
|
|
89328
|
-
const
|
|
89433
|
+
const inMemoryKeys = new Set([...statusPinState.keys()].filter((k) => k.startsWith("wk:")));
|
|
89434
|
+
const inMemoryCandidates = [...inMemoryKeys].map((k) => ({
|
|
89329
89435
|
pinKey: k,
|
|
89330
89436
|
chatId: statusPinChatIds.get(k) ?? "",
|
|
89331
89437
|
pinnedAt: statusPinPinnedAt.get(k) ?? now
|
|
89332
89438
|
}));
|
|
89439
|
+
const storeOnlyCandidates = statusPinPersistEnabled ? storeOnlyWorkerPinCandidates({
|
|
89440
|
+
rows: loadStatusPins(STATUS_PIN_STORE_PATH, statusPinStoreFs),
|
|
89441
|
+
inMemoryPinKeys: inMemoryKeys,
|
|
89442
|
+
now
|
|
89443
|
+
}) : [];
|
|
89444
|
+
const storeOnlyKeys = new Set(storeOnlyCandidates.map((c) => c.pinKey));
|
|
89445
|
+
const candidates = [...inMemoryCandidates, ...storeOnlyCandidates];
|
|
89333
89446
|
const reaps = decideWorkerPinReaps({
|
|
89334
89447
|
pins: candidates,
|
|
89335
89448
|
statusOf: (agentId) => {
|
|
@@ -89356,9 +89469,20 @@ async function runMidSessionCardReaper() {
|
|
|
89356
89469
|
now
|
|
89357
89470
|
});
|
|
89358
89471
|
for (const reap of reaps) {
|
|
89359
|
-
|
|
89472
|
+
const storeOnly = storeOnlyKeys.has(reap.pinKey);
|
|
89473
|
+
process.stderr.write(`telegram gateway: worker-pin reaper unpinning ${reap.pinKey} (chat=${reap.chatId} reason=${reap.reason}${storeOnly ? " source=store-orphan" : ""})
|
|
89474
|
+
`);
|
|
89475
|
+
if (storeOnly && reap.messageId != null) {
|
|
89476
|
+
try {
|
|
89477
|
+
await statusPinApi().unpinChatMessage(reap.chatId, reap.messageId);
|
|
89478
|
+
} catch (err) {
|
|
89479
|
+
process.stderr.write(`telegram gateway: worker-pin reaper store-orphan unpin failed (${reap.pinKey} chat=${reap.chatId} msg=${reap.messageId}): ${err.message}
|
|
89360
89480
|
`);
|
|
89361
|
-
|
|
89481
|
+
}
|
|
89482
|
+
await mutateStatusPinRow(STATUS_PIN_STORE_PATH, statusPinStoreFs, reap.pinKey, null);
|
|
89483
|
+
} else {
|
|
89484
|
+
await reconcileStatusPin(reap.pinKey, reap.chatId, { pinned: false });
|
|
89485
|
+
}
|
|
89362
89486
|
}
|
|
89363
89487
|
} catch (err) {
|
|
89364
89488
|
process.stderr.write(`telegram gateway: worker-pin reaper error: ${err.message}
|
|
@@ -93086,7 +93210,7 @@ async function executeGetRecentMessages(args) {
|
|
|
93086
93210
|
const rows = query({ chat_id, thread_id, limit, before_message_id });
|
|
93087
93211
|
const summary = rows.map((r) => {
|
|
93088
93212
|
const who = r.role === "user" ? r.user ?? "user" : "assistant";
|
|
93089
|
-
const time =
|
|
93213
|
+
const time = fmtLocalStamp2(r.ts * 1000, resolveEnvTimezone2());
|
|
93090
93214
|
const attach = r.attachment_kind ? ` [${r.attachment_kind}]` : "";
|
|
93091
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" : ""}"` : ""}` : "";
|
|
93092
93216
|
const reactionTag = r.user_reaction ? ` [reaction: ${r.user_reaction}]` : "";
|
|
@@ -94218,16 +94342,15 @@ function handleSessionEvent(ev) {
|
|
|
94218
94342
|
}
|
|
94219
94343
|
turn.finalAnswerDelivered = true;
|
|
94220
94344
|
turn.finalAnswerSubstantive = true;
|
|
94221
|
-
|
|
94222
|
-
|
|
94223
|
-
}
|
|
94345
|
+
turn.answerDelivered = true;
|
|
94346
|
+
const backstopLatchClaimed = backstopDeliveryLedger.claim(turn.turnId);
|
|
94224
94347
|
const cardTakeover = progressDriver?.takeOverCard({
|
|
94225
94348
|
chatId: backstopChatId,
|
|
94226
94349
|
threadId: backstopThreadId != null ? String(backstopThreadId) : undefined
|
|
94227
94350
|
}) ?? { wasEmitted: false, turnKey: null };
|
|
94228
94351
|
const backstopCardMessageId = cardTakeover.wasEmitted && cardTakeover.turnKey != null ? getPinnedProgressCardMessageId?.(cardTakeover.turnKey) ?? null : null;
|
|
94229
94352
|
const backstopCardTurnKey = cardTakeover.turnKey;
|
|
94230
|
-
const backstopTurnEndedAt = endCurrentTurnAtomic(turn, { deferRecord: true });
|
|
94353
|
+
const backstopTurnEndedAt = endCurrentTurnAtomic(turn, { deferRecord: true, deferObligationClose: true });
|
|
94231
94354
|
preambleSuppressor.dropNow();
|
|
94232
94355
|
{
|
|
94233
94356
|
const tKey = statusKey(chatId, threadId);
|
|
@@ -94246,80 +94369,47 @@ function handleSessionEvent(ev) {
|
|
|
94246
94369
|
`);
|
|
94247
94370
|
if (backstopTurnEndedAt != null) {
|
|
94248
94371
|
turn.deliveryOutcome = "suppressed";
|
|
94372
|
+
if (OBLIGATION_LEDGER_ENABLED)
|
|
94373
|
+
obligationLedger.close(turn.turnId);
|
|
94249
94374
|
emitTurnRecord(turn, backstopTurnEndedAt);
|
|
94250
94375
|
}
|
|
94251
94376
|
return;
|
|
94252
94377
|
}
|
|
94253
94378
|
} catch {}
|
|
94254
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
|
+
}
|
|
94255
94385
|
process.stderr.write(`telegram gateway: turn-flush firing \u2014 ${capturedText.length} chars without reply tool (chat=${backstopChatId} cardMsgId=${backstopCardMessageId ?? "none"})
|
|
94256
94386
|
`);
|
|
94257
|
-
|
|
94258
|
-
|
|
94259
|
-
|
|
94260
|
-
};
|
|
94261
|
-
const limit = RICH_MESSAGE_MAX_CHARS2;
|
|
94262
|
-
let htmlChunks = [];
|
|
94263
|
-
const sentIds = [];
|
|
94264
|
-
let sendThrew = false;
|
|
94387
|
+
let sentIds = [];
|
|
94388
|
+
let chunkCount = 0;
|
|
94389
|
+
let delivered = false;
|
|
94265
94390
|
try {
|
|
94266
|
-
const
|
|
94267
|
-
|
|
94268
|
-
|
|
94269
|
-
|
|
94270
|
-
|
|
94271
|
-
|
|
94272
|
-
|
|
94273
|
-
|
|
94274
|
-
|
|
94275
|
-
|
|
94276
|
-
});
|
|
94277
|
-
sentIds.push(backstopCardMessageId);
|
|
94278
|
-
firstSendUsedEdit = true;
|
|
94279
|
-
} catch (err) {
|
|
94280
|
-
process.stderr.write(`telegram gateway: turn-flush card-takeover edit failed: ${err.message} \u2014 falling back to sendMessage
|
|
94281
|
-
`);
|
|
94282
|
-
if (err instanceof Error && err.message === "THREAD_NOT_FOUND") {
|
|
94283
|
-
liveThreadId = undefined;
|
|
94284
|
-
}
|
|
94285
|
-
}
|
|
94286
|
-
}
|
|
94287
|
-
const remainingChunks = firstSendUsedEdit ? htmlChunks.slice(1) : htmlChunks;
|
|
94288
|
-
for (const c of remainingChunks) {
|
|
94289
|
-
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => {
|
|
94290
|
-
const opts = {
|
|
94291
|
-
link_preview_options: { is_disabled: true },
|
|
94292
|
-
...tid != null ? { message_thread_id: tid } : {}
|
|
94293
|
-
};
|
|
94294
|
-
return bot.api.sendRichMessage(backstopChatId, richMessage2(c), opts);
|
|
94295
|
-
}, {
|
|
94296
|
-
threadId: liveThreadId,
|
|
94297
|
-
chat_id: backstopChatId,
|
|
94298
|
-
verb: "turn-flush.sendMessage"
|
|
94299
|
-
});
|
|
94300
|
-
if (liveThreadId != null) {
|
|
94301
|
-
const sentMsg = sent;
|
|
94302
|
-
if (sentMsg.message_thread_id == null)
|
|
94303
|
-
liveThreadId = undefined;
|
|
94304
|
-
}
|
|
94305
|
-
sentIds.push(sent.message_id);
|
|
94306
|
-
}
|
|
94307
|
-
if (HISTORY_ENABLED && sentIds.length > 0) {
|
|
94308
|
-
try {
|
|
94309
|
-
recordOutbound({
|
|
94310
|
-
chat_id: backstopChatId,
|
|
94311
|
-
thread_id: backstopThreadId ?? null,
|
|
94312
|
-
message_ids: sentIds,
|
|
94313
|
-
texts: htmlChunks
|
|
94314
|
-
});
|
|
94315
|
-
} catch {}
|
|
94316
|
-
}
|
|
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;
|
|
94317
94401
|
outboundDedup.record(backstopChatId, backstopThreadId, capturedText, Date.now(), currentTurn?.registryKey ?? null);
|
|
94318
94402
|
if (sentIds.length > 0) {
|
|
94319
94403
|
flushedTurnSupersede.record(backstopChatId, backstopThreadId, { turnId: turn.turnId, messageIds: sentIds, text: capturedText }, Date.now());
|
|
94320
94404
|
}
|
|
94321
|
-
if (
|
|
94405
|
+
if (!delivered) {
|
|
94406
|
+
if (backstopCtrl)
|
|
94407
|
+
backstopCtrl.finalize("error");
|
|
94408
|
+
backstopDeliveryLedger.release(turn.turnId);
|
|
94409
|
+
turn.answerDelivered = false;
|
|
94410
|
+
} else if (backstopCtrl) {
|
|
94322
94411
|
backstopCtrl.finalize("done");
|
|
94412
|
+
}
|
|
94323
94413
|
if (backstopCardTurnKey != null) {
|
|
94324
94414
|
completeProgressCardTurn?.({
|
|
94325
94415
|
chatId: backstopChatId,
|
|
@@ -94330,21 +94420,33 @@ function handleSessionEvent(ev) {
|
|
|
94330
94420
|
unpinProgressCardForChat?.(backstopChatId, backstopThreadId);
|
|
94331
94421
|
}
|
|
94332
94422
|
} catch (err) {
|
|
94333
|
-
|
|
94334
|
-
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}
|
|
94335
94424
|
`);
|
|
94336
|
-
|
|
94337
|
-
|
|
94338
|
-
|
|
94425
|
+
if (!delivered) {
|
|
94426
|
+
turn.answerDelivered = false;
|
|
94427
|
+
backstopDeliveryLedger.release(turn.turnId);
|
|
94428
|
+
if (backstopCtrl)
|
|
94429
|
+
backstopCtrl.finalize("error");
|
|
94430
|
+
}
|
|
94339
94431
|
} finally {
|
|
94340
94432
|
if (backstopTurnEndedAt != null) {
|
|
94341
|
-
|
|
94342
|
-
threw:
|
|
94343
|
-
|
|
94344
|
-
chunkCount
|
|
94433
|
+
finalizeBackstopSendGated(turn, {
|
|
94434
|
+
threw: !delivered,
|
|
94435
|
+
sentIds,
|
|
94436
|
+
chunkCount,
|
|
94437
|
+
cardMessageId: backstopCardMessageId
|
|
94345
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
|
+
}
|
|
94346
94447
|
emitTurnRecord(turn, backstopTurnEndedAt);
|
|
94347
94448
|
}
|
|
94449
|
+
backstopDeliveryLedger.clear(turn.turnId);
|
|
94348
94450
|
}
|
|
94349
94451
|
})();
|
|
94350
94452
|
return;
|
|
@@ -95399,7 +95501,7 @@ ${preBlock(write.output)}`;
|
|
|
95399
95501
|
...msgId != null ? { message_id: String(msgId) } : {},
|
|
95400
95502
|
user: displayUser,
|
|
95401
95503
|
user_id: String(from.id),
|
|
95402
|
-
ts:
|
|
95504
|
+
ts: fmtLocalStamp2((ctx.message?.date ?? 0) * 1000, resolveEnvTimezone2()),
|
|
95403
95505
|
...messageThreadId != null ? { message_thread_id: String(messageThreadId) } : {},
|
|
95404
95506
|
...originTurnId != null ? { origin_turn_id: originTurnId } : {},
|
|
95405
95507
|
...topicScope != null ? { topic_scope: topicScope } : {},
|
|
@@ -96541,7 +96643,6 @@ function buildModelDeps(restartCtx) {
|
|
|
96541
96643
|
return [];
|
|
96542
96644
|
}
|
|
96543
96645
|
},
|
|
96544
|
-
select: (a, label) => selectModel(a, label),
|
|
96545
96646
|
isBusy: () => currentTurn !== null,
|
|
96546
96647
|
getAgentName: getMyAgentName,
|
|
96547
96648
|
getQuotaBrief: async () => {
|
|
@@ -96560,14 +96661,12 @@ function buildModelDeps(restartCtx) {
|
|
|
96560
96661
|
} catch {}
|
|
96561
96662
|
return null;
|
|
96562
96663
|
},
|
|
96563
|
-
inject: injectSlashCommand,
|
|
96564
96664
|
getConfiguredModel: () => {
|
|
96565
96665
|
const data = switchroomExecJson(["agent", "list"]);
|
|
96566
96666
|
return data?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
96567
96667
|
},
|
|
96568
96668
|
escapeHtml: escapeHtmlForTg2,
|
|
96569
96669
|
preBlock,
|
|
96570
|
-
getActiveSessionModel: () => sessionModelSource.getOverride(),
|
|
96571
96670
|
scheduleRestart: async (reason) => {
|
|
96572
96671
|
const name = getMyAgentName();
|
|
96573
96672
|
const existing = readRestartMarker();
|
|
@@ -96610,6 +96709,30 @@ function buildModelDeps(restartCtx) {
|
|
|
96610
96709
|
const prevFileRaw = readSessionModelFileRaw(agentDir);
|
|
96611
96710
|
writeSessionModelFile(agentDir, model, readConfiguredDefaultModel(agentDir) ?? resolveMainModel(deps.getConfiguredModel() ?? undefined));
|
|
96612
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);
|
|
96613
96736
|
try {
|
|
96614
96737
|
await deps.scheduleRestart(reason);
|
|
96615
96738
|
} catch (err) {
|
|
@@ -96634,60 +96757,6 @@ function modelMenuReplyMarkup(reply) {
|
|
|
96634
96757
|
}
|
|
96635
96758
|
return kb;
|
|
96636
96759
|
}
|
|
96637
|
-
function recordTypedModelSwitch(reply, requestedModelArg, _deps) {
|
|
96638
|
-
const requested = requestedModelArg != null ? expandSrAlias(requestedModelArg) : null;
|
|
96639
|
-
if (requested?.toLowerCase() === "default") {
|
|
96640
|
-
const smDir = resolveAgentDirFromEnv();
|
|
96641
|
-
if (smDir)
|
|
96642
|
-
clearSessionModelFile(smDir);
|
|
96643
|
-
if (reply.selectedModel)
|
|
96644
|
-
sessionModelSource.setOverride(null);
|
|
96645
|
-
return "";
|
|
96646
|
-
}
|
|
96647
|
-
if (!reply.selectedModel)
|
|
96648
|
-
return "";
|
|
96649
|
-
sessionModelSource.setOverride(reply.selectedModel);
|
|
96650
|
-
clearPremiumRecoveryOnManualSwitch(reply.selectedModel);
|
|
96651
|
-
return "";
|
|
96652
|
-
}
|
|
96653
|
-
function recordModelMenuSideEffects(outcome, modelDeps, cbChatId, cbThreadId, prevSessionModel) {
|
|
96654
|
-
if (outcome.selectedModel) {
|
|
96655
|
-
sessionModelSource.setOverride(outcome.selectedModel);
|
|
96656
|
-
clearPremiumRecoveryOnManualSwitch(outcome.selectedModel);
|
|
96657
|
-
}
|
|
96658
|
-
if (outcome.clearedDefault) {
|
|
96659
|
-
const smDir = resolveAgentDirFromEnv();
|
|
96660
|
-
if (smDir)
|
|
96661
|
-
clearSessionModelFile(smDir);
|
|
96662
|
-
}
|
|
96663
|
-
if (outcome.selectedModel && isSrToClaudeTransition(prevSessionModel, outcome.selectedModel)) {
|
|
96664
|
-
const agentName3 = getMyAgentName();
|
|
96665
|
-
const agentDir = resolveAgentDirFromEnv();
|
|
96666
|
-
const token = outcome.selectedModelToken;
|
|
96667
|
-
if (agentDir && token) {
|
|
96668
|
-
try {
|
|
96669
|
-
writeSessionModelFile(agentDir, token, readConfiguredDefaultModel(agentDir) ?? resolveMainModel(modelDeps.getConfiguredModel() ?? undefined));
|
|
96670
|
-
sessionModelSource.setOverride(token);
|
|
96671
|
-
} catch (e) {
|
|
96672
|
-
process.stderr.write(`telegram gateway: sr-to-claude session-model write failed: ${e?.message ?? String(e)}
|
|
96673
|
-
`);
|
|
96674
|
-
}
|
|
96675
|
-
} else if (agentDir) {
|
|
96676
|
-
clearSessionModelFile(agentDir);
|
|
96677
|
-
}
|
|
96678
|
-
writeRestartMarker({ chat_id: cbChatId, thread_id: cbThreadId ?? null, ack_message_id: null, ts: Date.now() });
|
|
96679
|
-
stampUserRestartReason("user: sr-to-claude model switch (menu)");
|
|
96680
|
-
if (turnInFlightForGate()) {
|
|
96681
|
-
pendingRestarts.set(agentName3, Date.now());
|
|
96682
|
-
} else {
|
|
96683
|
-
sweepBeforeSelfRestart().finally(() => triggerSelfRestart(agentName3, "sr-to-claude-model-switch", 1500));
|
|
96684
|
-
}
|
|
96685
|
-
return {
|
|
96686
|
-
restartNotice: `\uD83D\uDD04 Switching from **${escapeHtmlForTg2(prevSessionModel)}** back to Claude \u2014 restarting session cleanly. Claude will be ready in ~30s.`
|
|
96687
|
-
};
|
|
96688
|
-
}
|
|
96689
|
-
return {};
|
|
96690
|
-
}
|
|
96691
96760
|
async function editPendingCommandCard(chatId, messageId, text5) {
|
|
96692
96761
|
try {
|
|
96693
96762
|
await robustApiCall(() => lockedBot.api.editMessageText(chatId, messageId, richMessage2(hardenCardBreaks2(text5)), {
|
|
@@ -96709,14 +96778,11 @@ function enqueueSessionCommand(cmd) {
|
|
|
96709
96778
|
async function applyQueuedModelCommand(cmd) {
|
|
96710
96779
|
const deps = buildModelDeps({ chatId: cmd.chatId, threadId: cmd.threadId });
|
|
96711
96780
|
if (cmd.origin === "menu") {
|
|
96712
|
-
const prevSessionModel = sessionModelSource.getOverride();
|
|
96713
96781
|
const outcome = await handleModelMenuCallback(cmd.arg, deps);
|
|
96714
|
-
|
|
96715
|
-
return restartNotice ?? outcome.reply.text;
|
|
96782
|
+
return outcome.reply.text;
|
|
96716
96783
|
}
|
|
96717
96784
|
const reply = await handleModelCommand({ kind: "set", model: cmd.arg }, deps);
|
|
96718
|
-
|
|
96719
|
-
return reply.text + warning;
|
|
96785
|
+
return reply.text;
|
|
96720
96786
|
}
|
|
96721
96787
|
async function applyQueuedEffortCommand(cmd) {
|
|
96722
96788
|
const deps = buildEffortDeps();
|
|
@@ -96854,8 +96920,7 @@ bot.command("model", async (ctx) => {
|
|
|
96854
96920
|
return;
|
|
96855
96921
|
}
|
|
96856
96922
|
const reply = await handleModelCommand(parsed, deps);
|
|
96857
|
-
|
|
96858
|
-
await switchroomReply(ctx, reply.text + persistWarning, { html: reply.html });
|
|
96923
|
+
await switchroomReply(ctx, reply.text, { html: reply.html });
|
|
96859
96924
|
});
|
|
96860
96925
|
var sessionEffortOverride = null;
|
|
96861
96926
|
function buildEffortDeps() {
|
|
@@ -98909,31 +98974,10 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
98909
98974
|
}
|
|
98910
98975
|
const isPageNav = data === MODEL_CALLBACK_PAGE_EXTERNAL || data === MODEL_CALLBACK_PAGE_MAIN;
|
|
98911
98976
|
await ctx.answerCallbackQuery({ text: isPageNav ? "Loading\u2026" : "Switching\u2026" }).catch(() => {});
|
|
98912
|
-
if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
98913
|
-
const srName = data.slice(MODEL_CALLBACK_SR.length);
|
|
98914
|
-
const srLabel = escapeHtmlForTg2(srFriendlyLabel(srName));
|
|
98915
|
-
if (!isValidModelArg(srName)) {
|
|
98916
|
-
await ctx.editMessageText(richMessage2("\u274C Invalid model name"), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
98917
|
-
return;
|
|
98918
|
-
}
|
|
98919
|
-
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(() => {});
|
|
98920
|
-
try {
|
|
98921
|
-
await modelDeps.scheduleModelRelaunch(srName, `user: /model ${srName} (session-only relaunch, menu)`);
|
|
98922
|
-
} catch (err) {
|
|
98923
|
-
await ctx.editMessageText(richMessage2(`\u274C Could not switch to **${srLabel}**: ${escapeHtmlForTg2(err?.message ?? String(err))}`), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
98924
|
-
}
|
|
98925
|
-
return;
|
|
98926
|
-
}
|
|
98927
98977
|
try {
|
|
98928
|
-
const prevSessionModel = sessionModelSource.getOverride();
|
|
98929
98978
|
const outcome = await handleModelMenuCallback(data, modelDeps);
|
|
98930
98979
|
if (outcome.toastOnly)
|
|
98931
98980
|
return;
|
|
98932
|
-
const { restartNotice } = recordModelMenuSideEffects(outcome, modelDeps, cbChatId, cbThreadId, prevSessionModel);
|
|
98933
|
-
if (restartNotice) {
|
|
98934
|
-
await ctx.editMessageText(richMessage2(restartNotice), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
98935
|
-
return;
|
|
98936
|
-
}
|
|
98937
98981
|
await ctx.editMessageText(richMessage2(outcome.reply.text), {
|
|
98938
98982
|
reply_markup: modelMenuReplyMarkup(outcome.reply) ?? { inline_keyboard: [] }
|
|
98939
98983
|
}).catch(() => {});
|
|
@@ -100638,6 +100682,8 @@ var didOneTimeSetup = false;
|
|
|
100638
100682
|
`);
|
|
100639
100683
|
}
|
|
100640
100684
|
} catch {}
|
|
100685
|
+
let modelSwitchMarkerChat = null;
|
|
100686
|
+
let modelSwitchReason = null;
|
|
100641
100687
|
try {
|
|
100642
100688
|
const nowMs2 = Date.now();
|
|
100643
100689
|
const marker = readRestartMarker();
|
|
@@ -100667,6 +100713,12 @@ var didOneTimeSetup = false;
|
|
|
100667
100713
|
const ageSec = Math.max(1, Math.round(ageMs / 1000));
|
|
100668
100714
|
process.stderr.write(`telegram gateway: boot: restart-marker present, chat_id=${marker.chat_id} age=${ageSec}s within5min=${ageMs < 300000}
|
|
100669
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
|
+
}
|
|
100670
100722
|
clearRestartMarker();
|
|
100671
100723
|
}
|
|
100672
100724
|
if (!isRealRestart) {
|
|
@@ -100692,7 +100744,11 @@ var didOneTimeSetup = false;
|
|
|
100692
100744
|
firstSeenAt: new Date
|
|
100693
100745
|
});
|
|
100694
100746
|
}
|
|
100695
|
-
|
|
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) {
|
|
100696
100752
|
const { chatId, threadId, ackMsgId } = target;
|
|
100697
100753
|
const bootDedupe = shouldSkipDuplicateBootCard({ activeBootCard, bootCardPending }, "boot");
|
|
100698
100754
|
if (bootDedupe.skip) {
|
|
@@ -100771,7 +100827,19 @@ var didOneTimeSetup = false;
|
|
|
100771
100827
|
const raw = d?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
100772
100828
|
return resolveMainModel(raw ?? undefined);
|
|
100773
100829
|
})();
|
|
100774
|
-
|
|
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
|
+
}
|
|
100775
100843
|
} catch {}
|
|
100776
100844
|
}
|
|
100777
100845
|
const activeEffortPath = join55(smAgentDir, ".active-session-effort");
|