switchroom 0.21.3 → 0.21.5
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 +23 -1
- package/dist/cli/switchroom.js +79 -16
- package/dist/host-control/main.js +1 -1
- package/package.json +4 -2
- package/telegram-plugin/dist/gateway/gateway.js +406 -121
- package/telegram-plugin/gateway/gateway.ts +24 -22
- package/telegram-plugin/gateway/inbound-router.ts +77 -26
- package/telegram-plugin/gateway/orphaned-db-sweep.ts +315 -0
- package/telegram-plugin/gateway/system-message-observer.ts +25 -6
- package/telegram-plugin/history.ts +328 -62
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -4
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +8 -2
- package/telegram-plugin/shared/bot-runtime.ts +119 -2
- package/telegram-plugin/tests/card-history-lane.test.ts +171 -2
- package/telegram-plugin/tests/orphaned-db-sweep.test.ts +721 -0
- package/telegram-plugin/tests/reply-to-buffer-fallback.test.ts +209 -4
- package/telegram-plugin/tests/system-message-observer.test.ts +84 -1
|
@@ -30100,6 +30100,7 @@ var exports_history = {};
|
|
|
30100
30100
|
__export(exports_history, {
|
|
30101
30101
|
verifyHistoryWritable: () => verifyHistoryWritable2,
|
|
30102
30102
|
updateSystemOutboundText: () => updateSystemOutboundText2,
|
|
30103
|
+
reopenHistory: () => reopenHistory2,
|
|
30103
30104
|
recordSystemOutbound: () => recordSystemOutbound2,
|
|
30104
30105
|
recordReaction: () => recordReaction2,
|
|
30105
30106
|
recordOutbound: () => recordOutbound2,
|
|
@@ -30112,8 +30113,10 @@ __export(exports_history, {
|
|
|
30112
30113
|
initHistory: () => initHistory2,
|
|
30113
30114
|
hasOutboundWithText: () => hasOutboundWithText2,
|
|
30114
30115
|
hasOutboundDeliveredSince: () => hasOutboundDeliveredSince2,
|
|
30116
|
+
hardCloseDb: () => hardCloseDb2,
|
|
30115
30117
|
getRecentOutboundCount: () => getRecentOutboundCount,
|
|
30116
30118
|
getLatestInboundMessageId: () => getLatestInboundMessageId2,
|
|
30119
|
+
getHistoryReopenFailure: () => getHistoryReopenFailure2,
|
|
30117
30120
|
getHistoryDbForBriefing: () => getHistoryDbForBriefing,
|
|
30118
30121
|
deliveryTextMatch: () => deliveryTextMatch2,
|
|
30119
30122
|
deleteFromHistory: () => deleteFromHistory2,
|
|
@@ -30140,6 +30143,32 @@ function loadDatabaseClass2() {
|
|
|
30140
30143
|
throw new Error(`history.ts requires Bun runtime (bun:sqlite). Caller: ${err.message}`);
|
|
30141
30144
|
}
|
|
30142
30145
|
}
|
|
30146
|
+
function prep2(sql) {
|
|
30147
|
+
const cached = stmtCache2.get(sql);
|
|
30148
|
+
if (cached != null)
|
|
30149
|
+
return cached;
|
|
30150
|
+
const stmt = requireDb2().prepare(sql);
|
|
30151
|
+
if (stmtCache2.size >= MAX_CACHED_STATEMENTS2) {
|
|
30152
|
+
const oldestKey = stmtCache2.keys().next().value;
|
|
30153
|
+
if (oldestKey != null) {
|
|
30154
|
+
const oldest = stmtCache2.get(oldestKey);
|
|
30155
|
+
stmtCache2.delete(oldestKey);
|
|
30156
|
+
try {
|
|
30157
|
+
oldest?.finalize?.();
|
|
30158
|
+
} catch {}
|
|
30159
|
+
}
|
|
30160
|
+
}
|
|
30161
|
+
stmtCache2.set(sql, stmt);
|
|
30162
|
+
return stmt;
|
|
30163
|
+
}
|
|
30164
|
+
function finalizeCachedStatements2() {
|
|
30165
|
+
for (const stmt of stmtCache2.values()) {
|
|
30166
|
+
try {
|
|
30167
|
+
stmt.finalize?.();
|
|
30168
|
+
} catch {}
|
|
30169
|
+
}
|
|
30170
|
+
stmtCache2.clear();
|
|
30171
|
+
}
|
|
30143
30172
|
function warnHistory2(msg) {
|
|
30144
30173
|
try {
|
|
30145
30174
|
process.stderr.write(`telegram history: ${msg}
|
|
@@ -30202,7 +30231,7 @@ function initHistory2(stateDir, retentionDays = 30) {
|
|
|
30202
30231
|
}
|
|
30203
30232
|
}
|
|
30204
30233
|
const LOGICAL_KEY_INDEX = "idx_messages_logical_key";
|
|
30205
|
-
const logicalKeyIndexExists =
|
|
30234
|
+
const logicalKeyIndexExists = prep2(`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?`).get(LOGICAL_KEY_INDEX) != null;
|
|
30206
30235
|
if (!logicalKeyIndexExists) {
|
|
30207
30236
|
db2.exec(`
|
|
30208
30237
|
DELETE FROM messages
|
|
@@ -30231,7 +30260,7 @@ function initHistory2(stateDir, retentionDays = 30) {
|
|
|
30231
30260
|
adoptSqliteOwnership(path2);
|
|
30232
30261
|
if (retentionDays > 0) {
|
|
30233
30262
|
const cutoff = Math.floor(Date.now() / 1000) - retentionDays * 86400;
|
|
30234
|
-
|
|
30263
|
+
prep2("DELETE FROM messages WHERE ts < ?").run(cutoff);
|
|
30235
30264
|
}
|
|
30236
30265
|
const check = verifyHistoryWritable2();
|
|
30237
30266
|
if (!check.ok) {
|
|
@@ -30244,11 +30273,11 @@ function verifyHistoryWritable2() {
|
|
|
30244
30273
|
const SENTINEL_CHAT = "__history_selfcheck__";
|
|
30245
30274
|
const sentinelId = Date.now();
|
|
30246
30275
|
try {
|
|
30247
|
-
|
|
30248
|
-
|
|
30276
|
+
prep2("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
|
|
30277
|
+
prep2(`INSERT OR REPLACE INTO messages
|
|
30249
30278
|
(chat_id, thread_id, message_id, role, ts, text)
|
|
30250
30279
|
VALUES (?, NULL, ?, 'assistant', ?, ?)`).run(SENTINEL_CHAT, sentinelId, Math.floor(Date.now() / 1000), "selfcheck");
|
|
30251
|
-
const row =
|
|
30280
|
+
const row = prep2("SELECT text FROM messages WHERE chat_id = ? AND message_id = ?").get(SENTINEL_CHAT, sentinelId);
|
|
30252
30281
|
if (row?.text !== "selfcheck") {
|
|
30253
30282
|
return { ok: false, error: "sentinel row not read back after insert" };
|
|
30254
30283
|
}
|
|
@@ -30257,24 +30286,32 @@ function verifyHistoryWritable2() {
|
|
|
30257
30286
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
30258
30287
|
} finally {
|
|
30259
30288
|
try {
|
|
30260
|
-
|
|
30289
|
+
prep2("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
|
|
30261
30290
|
} catch {}
|
|
30262
30291
|
}
|
|
30263
30292
|
}
|
|
30264
30293
|
function getHistoryDbForBriefing() {
|
|
30265
|
-
|
|
30294
|
+
if (db2 == null)
|
|
30295
|
+
return null;
|
|
30296
|
+
return {
|
|
30297
|
+
prepare(sql) {
|
|
30298
|
+
return { all: (...params) => prep2(sql).all(...params) };
|
|
30299
|
+
}
|
|
30300
|
+
};
|
|
30266
30301
|
}
|
|
30267
30302
|
function _resetForTests() {
|
|
30303
|
+
historyReopenFailure2 = null;
|
|
30268
30304
|
if (db2 != null) {
|
|
30269
|
-
db2
|
|
30305
|
+
const current = db2;
|
|
30270
30306
|
db2 = null;
|
|
30307
|
+
hardCloseDb2(current);
|
|
30271
30308
|
}
|
|
30272
30309
|
}
|
|
30273
30310
|
function checkpointWal2() {
|
|
30274
30311
|
if (db2 == null)
|
|
30275
30312
|
return false;
|
|
30276
30313
|
try {
|
|
30277
|
-
|
|
30314
|
+
prep2("PRAGMA wal_checkpoint(TRUNCATE)").run();
|
|
30278
30315
|
if (dbPath2) {
|
|
30279
30316
|
for (const suffix of ["-shm", "-wal"]) {
|
|
30280
30317
|
const f = dbPath2 + suffix;
|
|
@@ -30291,13 +30328,44 @@ function checkpointWal2() {
|
|
|
30291
30328
|
return false;
|
|
30292
30329
|
}
|
|
30293
30330
|
}
|
|
30331
|
+
function reopenHistory2(stateDir, retentionDays = 30) {
|
|
30332
|
+
const current = db2;
|
|
30333
|
+
if (current != null) {
|
|
30334
|
+
hardCloseDb2(current);
|
|
30335
|
+
}
|
|
30336
|
+
db2 = null;
|
|
30337
|
+
try {
|
|
30338
|
+
initHistory2(stateDir, retentionDays);
|
|
30339
|
+
const check = verifyHistoryWritable2();
|
|
30340
|
+
if (!check.ok) {
|
|
30341
|
+
throw new Error(`post-reopen writer self-check failed: ${check.error ?? "unknown"}`);
|
|
30342
|
+
}
|
|
30343
|
+
} catch (err) {
|
|
30344
|
+
historyReopenFailure2 = err instanceof Error ? err.message : String(err);
|
|
30345
|
+
throw err;
|
|
30346
|
+
}
|
|
30347
|
+
historyReopenFailure2 = null;
|
|
30348
|
+
}
|
|
30349
|
+
function hardCloseDb2(handle) {
|
|
30350
|
+
finalizeCachedStatements2();
|
|
30351
|
+
const gc = globalThis.Bun?.gc;
|
|
30352
|
+
if (typeof gc === "function") {
|
|
30353
|
+
try {
|
|
30354
|
+
gc(true);
|
|
30355
|
+
} catch {}
|
|
30356
|
+
}
|
|
30357
|
+
handle.close(true);
|
|
30358
|
+
}
|
|
30359
|
+
function getHistoryReopenFailure2() {
|
|
30360
|
+
return historyReopenFailure2;
|
|
30361
|
+
}
|
|
30294
30362
|
function pruneMessagesOlderThanDays2(retentionDays, nowSec, batchLimit = 5000) {
|
|
30295
30363
|
if (db2 == null)
|
|
30296
30364
|
return 0;
|
|
30297
30365
|
if (retentionDays <= 0)
|
|
30298
30366
|
return 0;
|
|
30299
30367
|
const cutoffSec = (nowSec ?? Math.floor(Date.now() / 1000)) - retentionDays * 86400;
|
|
30300
|
-
const stmt =
|
|
30368
|
+
const stmt = prep2(`
|
|
30301
30369
|
DELETE FROM messages
|
|
30302
30370
|
WHERE rowid IN (
|
|
30303
30371
|
SELECT rowid FROM messages WHERE ts < ? LIMIT ?
|
|
@@ -30326,7 +30394,7 @@ function recordInbound2(args) {
|
|
|
30326
30394
|
warnHistory2(`recordInbound: dropping row with invalid message_id=${String(args.message_id)} ` + `(chat=${args.chat_id}) \u2014 a delivered inbound will be absent from history`);
|
|
30327
30395
|
return;
|
|
30328
30396
|
}
|
|
30329
|
-
const stmt =
|
|
30397
|
+
const stmt = prep2(`
|
|
30330
30398
|
INSERT OR REPLACE INTO messages
|
|
30331
30399
|
(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)
|
|
30332
30400
|
VALUES (?, ?, ?, 'user', ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -30353,12 +30421,12 @@ function recordOutbound2(args) {
|
|
|
30353
30421
|
if (validRows.length === 0)
|
|
30354
30422
|
return;
|
|
30355
30423
|
const groupId = validRows[0].id;
|
|
30356
|
-
const stmt =
|
|
30424
|
+
const stmt = prep2(`
|
|
30357
30425
|
INSERT OR REPLACE INTO messages
|
|
30358
30426
|
(chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id)
|
|
30359
30427
|
VALUES (?, ?, ?, 'assistant', NULL, NULL, ?, ?, ?, ?)
|
|
30360
30428
|
`);
|
|
30361
|
-
const dropSystem =
|
|
30429
|
+
const dropSystem = prep2(`DELETE FROM messages WHERE chat_id = ? AND message_id = ? AND role = 'system'`);
|
|
30362
30430
|
const tx = requireDb2().transaction((rows) => {
|
|
30363
30431
|
for (const r of rows) {
|
|
30364
30432
|
dropSystem.run(args.chat_id, r.id);
|
|
@@ -30378,8 +30446,8 @@ function recordSystemOutbound2(args) {
|
|
|
30378
30446
|
if (db2 == null)
|
|
30379
30447
|
return false;
|
|
30380
30448
|
try {
|
|
30381
|
-
const res =
|
|
30382
|
-
|
|
30449
|
+
const res = prep2(`
|
|
30450
|
+
INSERT INTO messages
|
|
30383
30451
|
(chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id, kind)
|
|
30384
30452
|
SELECT ?, ?, ?, 'system', NULL, NULL, ?, ?, NULL, NULL, ?
|
|
30385
30453
|
WHERE NOT EXISTS (
|
|
@@ -30394,31 +30462,31 @@ function recordSystemOutbound2(args) {
|
|
|
30394
30462
|
}
|
|
30395
30463
|
function updateSystemOutboundText2(args) {
|
|
30396
30464
|
try {
|
|
30397
|
-
const res =
|
|
30465
|
+
const res = prep2(`UPDATE messages SET text = ? WHERE chat_id = ? AND message_id = ? AND role = 'system'`).run(redact(args.text), args.chat_id, args.message_id);
|
|
30398
30466
|
return (res?.changes ?? 0) > 0;
|
|
30399
30467
|
} catch {
|
|
30400
30468
|
return false;
|
|
30401
30469
|
}
|
|
30402
30470
|
}
|
|
30403
30471
|
function recordEdit2(args) {
|
|
30404
|
-
|
|
30405
|
-
|
|
30406
|
-
|
|
30407
|
-
|
|
30408
|
-
|
|
30472
|
+
prep2(`
|
|
30473
|
+
UPDATE messages
|
|
30474
|
+
SET text = ?
|
|
30475
|
+
WHERE chat_id = ? AND message_id = ?
|
|
30476
|
+
`).run(redact(args.text), args.chat_id, args.message_id);
|
|
30409
30477
|
}
|
|
30410
30478
|
function recordReaction2(args) {
|
|
30411
|
-
|
|
30412
|
-
|
|
30413
|
-
|
|
30414
|
-
|
|
30415
|
-
|
|
30479
|
+
prep2(`
|
|
30480
|
+
UPDATE messages
|
|
30481
|
+
SET user_reaction = ?
|
|
30482
|
+
WHERE chat_id = ? AND message_id = ?
|
|
30483
|
+
`).run(args.emoji, args.chat_id, args.message_id);
|
|
30416
30484
|
}
|
|
30417
30485
|
function deleteFromHistory2(args) {
|
|
30418
|
-
|
|
30419
|
-
|
|
30420
|
-
|
|
30421
|
-
|
|
30486
|
+
prep2(`
|
|
30487
|
+
DELETE FROM messages
|
|
30488
|
+
WHERE chat_id = ? AND message_id = ?
|
|
30489
|
+
`).run(args.chat_id, args.message_id);
|
|
30422
30490
|
}
|
|
30423
30491
|
function getLatestInboundMessageId2(chatId, threadId) {
|
|
30424
30492
|
const params = [chatId];
|
|
@@ -30432,19 +30500,21 @@ function getLatestInboundMessageId2(chatId, threadId) {
|
|
|
30432
30500
|
}
|
|
30433
30501
|
}
|
|
30434
30502
|
sql += " ORDER BY ts DESC, message_id DESC LIMIT 1";
|
|
30435
|
-
const row =
|
|
30503
|
+
const row = prep2(sql).get(...params);
|
|
30436
30504
|
return row?.message_id ?? null;
|
|
30437
30505
|
}
|
|
30438
30506
|
function lookupMessageRoleAndText2(chatId, messageId, opts) {
|
|
30507
|
+
if (db2 == null)
|
|
30508
|
+
return null;
|
|
30439
30509
|
const sql = `SELECT role, text, kind FROM messages WHERE chat_id = ? AND message_id = ?` + (opts?.includeSystem === true ? "" : ` AND role <> 'system'`) + ` LIMIT 1`;
|
|
30440
|
-
const row =
|
|
30510
|
+
const row = prep2(sql).get(chatId, messageId);
|
|
30441
30511
|
if (!row)
|
|
30442
30512
|
return null;
|
|
30443
30513
|
return { role: row.role, text: row.text ?? "", kind: row.kind ?? null };
|
|
30444
30514
|
}
|
|
30445
30515
|
function getRecentOutboundCount(chatId, withinSeconds) {
|
|
30446
30516
|
const cutoff = Math.floor(Date.now() / 1000) - withinSeconds;
|
|
30447
|
-
const row =
|
|
30517
|
+
const row = prep2("SELECT COUNT(*) as cnt FROM messages WHERE chat_id = ? AND role = ? AND ts >= ?").get(chatId, "assistant", cutoff);
|
|
30448
30518
|
return row?.cnt ?? 0;
|
|
30449
30519
|
}
|
|
30450
30520
|
function hasOutboundDeliveredSince2(chatId, sinceMs, threadId, minChars = 200) {
|
|
@@ -30462,7 +30532,7 @@ function hasOutboundDeliveredSince2(chatId, sinceMs, threadId, minChars = 200) {
|
|
|
30462
30532
|
}
|
|
30463
30533
|
}
|
|
30464
30534
|
sql += " LIMIT 1";
|
|
30465
|
-
const row =
|
|
30535
|
+
const row = prep2(sql).get(...params);
|
|
30466
30536
|
return row != null;
|
|
30467
30537
|
} catch {
|
|
30468
30538
|
return false;
|
|
@@ -30488,7 +30558,7 @@ function hasOutboundWithText2(chatId, text4, threadId, sinceMs) {
|
|
|
30488
30558
|
params.push(Math.floor(sinceMs / 1000));
|
|
30489
30559
|
}
|
|
30490
30560
|
sql += " ORDER BY ts DESC LIMIT 500";
|
|
30491
|
-
const rows =
|
|
30561
|
+
const rows = prep2(sql).all(...params);
|
|
30492
30562
|
for (const r of rows) {
|
|
30493
30563
|
const hay = normalizeDeliveryText2(r.text ?? "");
|
|
30494
30564
|
if (hay.length === 0)
|
|
@@ -30513,6 +30583,10 @@ function deliveryTextMatch2(hay, needle) {
|
|
|
30513
30583
|
return hay.startsWith(needle) || needle.startsWith(hay);
|
|
30514
30584
|
}
|
|
30515
30585
|
function query2(opts) {
|
|
30586
|
+
if (db2 == null) {
|
|
30587
|
+
warnHistory2("query: history DB is not open \u2014 returning no rows. If a reopen failed, the " + "orphaned-db-sweep is alarming about it every tick; RESTART the gateway.");
|
|
30588
|
+
return [];
|
|
30589
|
+
}
|
|
30516
30590
|
const limit = Math.min(MAX_LIMIT2, Math.max(1, opts.limit ?? DEFAULT_LIMIT2));
|
|
30517
30591
|
const params = [opts.chat_id];
|
|
30518
30592
|
let sql = "SELECT * FROM messages WHERE chat_id = ?";
|
|
@@ -30532,14 +30606,15 @@ function query2(opts) {
|
|
|
30532
30606
|
}
|
|
30533
30607
|
sql += " ORDER BY ts DESC, message_id DESC LIMIT ?";
|
|
30534
30608
|
params.push(limit);
|
|
30535
|
-
const rows =
|
|
30609
|
+
const rows = prep2(sql).all(...params);
|
|
30536
30610
|
rows.reverse();
|
|
30537
30611
|
return rows;
|
|
30538
30612
|
}
|
|
30539
|
-
var DatabaseClass2 = null, DEFAULT_LIMIT2 = 10, MAX_LIMIT2 = 50, db2 = null, dbPath2 = null, MIN_PREFIX_MATCH_CHARS2 = 40;
|
|
30613
|
+
var DatabaseClass2 = null, DEFAULT_LIMIT2 = 10, MAX_LIMIT2 = 50, db2 = null, dbPath2 = null, stmtCache2, MAX_CACHED_STATEMENTS2 = 128, historyReopenFailure2 = null, MIN_PREFIX_MATCH_CHARS2 = 40;
|
|
30540
30614
|
var init_history = __esm(() => {
|
|
30541
30615
|
init_state_owner();
|
|
30542
30616
|
init_redact();
|
|
30617
|
+
stmtCache2 = new Map;
|
|
30543
30618
|
});
|
|
30544
30619
|
|
|
30545
30620
|
// gateway/turn-flush-suppression.ts
|
|
@@ -39741,7 +39816,7 @@ import {
|
|
|
39741
39816
|
rmSync as rmSync9,
|
|
39742
39817
|
statSync as statSync25,
|
|
39743
39818
|
renameSync as renameSync30,
|
|
39744
|
-
realpathSync as
|
|
39819
|
+
realpathSync as realpathSync6,
|
|
39745
39820
|
chmodSync as chmodSync14,
|
|
39746
39821
|
openSync as openSync18,
|
|
39747
39822
|
closeSync as closeSync18,
|
|
@@ -39764,7 +39839,7 @@ function fsyncPathSync(path) {
|
|
|
39764
39839
|
|
|
39765
39840
|
// gateway/gateway.ts
|
|
39766
39841
|
import { homedir as homedir22 } from "os";
|
|
39767
|
-
import { join as join76, sep as sep6, basename as
|
|
39842
|
+
import { join as join76, sep as sep6, basename as basename18 } from "path";
|
|
39768
39843
|
|
|
39769
39844
|
// plugin-logger.ts
|
|
39770
39845
|
import { appendFileSync, mkdirSync, renameSync as renameSync2, statSync, existsSync } from "fs";
|
|
@@ -44654,6 +44729,7 @@ function shouldEmitShadowTrace(eventKind, effectCount, globalKind, verbose = gwT
|
|
|
44654
44729
|
// shared/bot-runtime.ts
|
|
44655
44730
|
init_rich_send();
|
|
44656
44731
|
var tgPostTagStore = new AsyncLocalStorage2;
|
|
44732
|
+
var sendContextStore = new AsyncLocalStorage2;
|
|
44657
44733
|
function escapeHtmlForTg(text) {
|
|
44658
44734
|
return text.replace(/([\\`*_~=\[\]|])/g, "\\$1");
|
|
44659
44735
|
}
|
|
@@ -45653,7 +45729,8 @@ function buildReplyForwardContext(p) {
|
|
|
45653
45729
|
const replyToMsg = p.ctx.message?.reply_to_message;
|
|
45654
45730
|
const replyToMessageId = replyToMsg?.message_id;
|
|
45655
45731
|
const quoteText = p.ctx.message?.quote?.text;
|
|
45656
|
-
const
|
|
45732
|
+
const richParentText = extractRichMessageText(replyToMsg?.rich_message);
|
|
45733
|
+
const replyToTextRaw = quoteText != null && quoteText.length > 0 ? quoteText : replyToMsg ? replyToMsg.text ?? replyToMsg.caption ?? richParentText ?? undefined : undefined;
|
|
45657
45734
|
const replyToText = replyToTextRaw != null ? replyToTextRaw.length > p.replyToTextMax ? replyToTextRaw.slice(0, p.replyToTextMax - 1) + "\u2026" : replyToTextRaw : undefined;
|
|
45658
45735
|
const replyToTextEscaped = formatReplyToText(replyToTextRaw, p.replyToTextMax);
|
|
45659
45736
|
const forwardOrigins = p.coalescedForwardOrigins ?? dedupeForwardOrigins2([parseForwardOrigin2(p.ctx.message?.forward_origin)]);
|
|
@@ -45674,18 +45751,18 @@ function resolveReplyToFromBuffer(p) {
|
|
|
45674
45751
|
let replyToRole;
|
|
45675
45752
|
let replyToKind;
|
|
45676
45753
|
const liveTextEmpty = replyToTextEscaped == null || replyToTextEscaped.length === 0;
|
|
45677
|
-
if (p.historyEnabled && p.replyToMessageId != null
|
|
45754
|
+
if (p.historyEnabled && p.replyToMessageId != null) {
|
|
45678
45755
|
try {
|
|
45679
45756
|
const recovered = p.lookup(p.replyToMessageId);
|
|
45680
|
-
if (recovered != null
|
|
45681
|
-
replyToKind = recovered.kind;
|
|
45682
|
-
}
|
|
45683
|
-
if (recovered && recovered.text.length > 0) {
|
|
45684
|
-
replyToText = recovered.text.length > p.replyToTextMax ? recovered.text.slice(0, p.replyToTextMax - 1) + "\u2026" : recovered.text;
|
|
45685
|
-
replyToTextEscaped = formatReplyToText(recovered.text, p.replyToTextMax);
|
|
45686
|
-
replyToRole = recovered.role;
|
|
45687
|
-
} else if (recovered) {
|
|
45757
|
+
if (recovered != null) {
|
|
45688
45758
|
replyToRole = recovered.role;
|
|
45759
|
+
if (recovered.role === "system" && recovered.kind) {
|
|
45760
|
+
replyToKind = recovered.kind;
|
|
45761
|
+
}
|
|
45762
|
+
if (liveTextEmpty && recovered.text.length > 0) {
|
|
45763
|
+
replyToText = recovered.text.length > p.replyToTextMax ? recovered.text.slice(0, p.replyToTextMax - 1) + "\u2026" : recovered.text;
|
|
45764
|
+
replyToTextEscaped = formatReplyToText(recovered.text, p.replyToTextMax);
|
|
45765
|
+
}
|
|
45689
45766
|
}
|
|
45690
45767
|
} catch {}
|
|
45691
45768
|
}
|
|
@@ -67145,6 +67222,37 @@ function installRichMarkdownGuard(bot) {
|
|
|
67145
67222
|
return prev(method, payload, signal);
|
|
67146
67223
|
});
|
|
67147
67224
|
}
|
|
67225
|
+
var sendContextStore2 = new AsyncLocalStorage5;
|
|
67226
|
+
function withTgSendContext(ctx, fn) {
|
|
67227
|
+
return sendContextStore2.run(ctx, fn);
|
|
67228
|
+
}
|
|
67229
|
+
function installSystemMessageObserver(bot, observe) {
|
|
67230
|
+
bot.api.config.use(async (prev, method, payload, signal) => {
|
|
67231
|
+
const res = await prev(method, payload, signal);
|
|
67232
|
+
try {
|
|
67233
|
+
const r = res;
|
|
67234
|
+
if (r != null && typeof r === "object" && r.ok === true) {
|
|
67235
|
+
observe(r.result, resolveSendContext(payload));
|
|
67236
|
+
}
|
|
67237
|
+
} catch {}
|
|
67238
|
+
return res;
|
|
67239
|
+
});
|
|
67240
|
+
}
|
|
67241
|
+
function resolveSendContext(payload) {
|
|
67242
|
+
const ctx = sendContextStore2.getStore();
|
|
67243
|
+
const p = payload ?? {};
|
|
67244
|
+
const rawChat = p.chat_id;
|
|
67245
|
+
const chat_id = ctx?.chat_id ?? (typeof rawChat === "string" || typeof rawChat === "number" ? String(rawChat) : undefined);
|
|
67246
|
+
const rawThread = p.message_thread_id;
|
|
67247
|
+
const threadId = ctx?.threadId ?? (typeof rawThread === "number" ? rawThread : undefined);
|
|
67248
|
+
if (chat_id == null && threadId == null && ctx?.verb == null)
|
|
67249
|
+
return;
|
|
67250
|
+
return {
|
|
67251
|
+
...chat_id != null ? { chat_id } : {},
|
|
67252
|
+
...threadId != null ? { threadId } : {},
|
|
67253
|
+
...ctx?.verb != null ? { verb: ctx.verb } : {}
|
|
67254
|
+
};
|
|
67255
|
+
}
|
|
67148
67256
|
|
|
67149
67257
|
// shared/sent-text-capture.ts
|
|
67150
67258
|
var SENT_TEXT = Symbol.for("switchroom.telegram.sentText");
|
|
@@ -76091,6 +76199,34 @@ var DEFAULT_LIMIT = 10;
|
|
|
76091
76199
|
var MAX_LIMIT = 50;
|
|
76092
76200
|
var db = null;
|
|
76093
76201
|
var dbPath = null;
|
|
76202
|
+
var stmtCache = new Map;
|
|
76203
|
+
var MAX_CACHED_STATEMENTS = 128;
|
|
76204
|
+
function prep(sql) {
|
|
76205
|
+
const cached = stmtCache.get(sql);
|
|
76206
|
+
if (cached != null)
|
|
76207
|
+
return cached;
|
|
76208
|
+
const stmt = requireDb().prepare(sql);
|
|
76209
|
+
if (stmtCache.size >= MAX_CACHED_STATEMENTS) {
|
|
76210
|
+
const oldestKey = stmtCache.keys().next().value;
|
|
76211
|
+
if (oldestKey != null) {
|
|
76212
|
+
const oldest = stmtCache.get(oldestKey);
|
|
76213
|
+
stmtCache.delete(oldestKey);
|
|
76214
|
+
try {
|
|
76215
|
+
oldest?.finalize?.();
|
|
76216
|
+
} catch {}
|
|
76217
|
+
}
|
|
76218
|
+
}
|
|
76219
|
+
stmtCache.set(sql, stmt);
|
|
76220
|
+
return stmt;
|
|
76221
|
+
}
|
|
76222
|
+
function finalizeCachedStatements() {
|
|
76223
|
+
for (const stmt of stmtCache.values()) {
|
|
76224
|
+
try {
|
|
76225
|
+
stmt.finalize?.();
|
|
76226
|
+
} catch {}
|
|
76227
|
+
}
|
|
76228
|
+
stmtCache.clear();
|
|
76229
|
+
}
|
|
76094
76230
|
function warnHistory(msg) {
|
|
76095
76231
|
try {
|
|
76096
76232
|
process.stderr.write(`telegram history: ${msg}
|
|
@@ -76153,7 +76289,7 @@ function initHistory(stateDir, retentionDays = 30) {
|
|
|
76153
76289
|
}
|
|
76154
76290
|
}
|
|
76155
76291
|
const LOGICAL_KEY_INDEX = "idx_messages_logical_key";
|
|
76156
|
-
const logicalKeyIndexExists =
|
|
76292
|
+
const logicalKeyIndexExists = prep(`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?`).get(LOGICAL_KEY_INDEX) != null;
|
|
76157
76293
|
if (!logicalKeyIndexExists) {
|
|
76158
76294
|
db.exec(`
|
|
76159
76295
|
DELETE FROM messages
|
|
@@ -76182,7 +76318,7 @@ function initHistory(stateDir, retentionDays = 30) {
|
|
|
76182
76318
|
adoptSqliteOwnership(path2);
|
|
76183
76319
|
if (retentionDays > 0) {
|
|
76184
76320
|
const cutoff = Math.floor(Date.now() / 1000) - retentionDays * 86400;
|
|
76185
|
-
|
|
76321
|
+
prep("DELETE FROM messages WHERE ts < ?").run(cutoff);
|
|
76186
76322
|
}
|
|
76187
76323
|
const check = verifyHistoryWritable();
|
|
76188
76324
|
if (!check.ok) {
|
|
@@ -76195,11 +76331,11 @@ function verifyHistoryWritable() {
|
|
|
76195
76331
|
const SENTINEL_CHAT = "__history_selfcheck__";
|
|
76196
76332
|
const sentinelId = Date.now();
|
|
76197
76333
|
try {
|
|
76198
|
-
|
|
76199
|
-
|
|
76334
|
+
prep("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
|
|
76335
|
+
prep(`INSERT OR REPLACE INTO messages
|
|
76200
76336
|
(chat_id, thread_id, message_id, role, ts, text)
|
|
76201
76337
|
VALUES (?, NULL, ?, 'assistant', ?, ?)`).run(SENTINEL_CHAT, sentinelId, Math.floor(Date.now() / 1000), "selfcheck");
|
|
76202
|
-
const row =
|
|
76338
|
+
const row = prep("SELECT text FROM messages WHERE chat_id = ? AND message_id = ?").get(SENTINEL_CHAT, sentinelId);
|
|
76203
76339
|
if (row?.text !== "selfcheck") {
|
|
76204
76340
|
return { ok: false, error: "sentinel row not read back after insert" };
|
|
76205
76341
|
}
|
|
@@ -76208,7 +76344,7 @@ function verifyHistoryWritable() {
|
|
|
76208
76344
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
76209
76345
|
} finally {
|
|
76210
76346
|
try {
|
|
76211
|
-
|
|
76347
|
+
prep("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
|
|
76212
76348
|
} catch {}
|
|
76213
76349
|
}
|
|
76214
76350
|
}
|
|
@@ -76216,7 +76352,7 @@ function checkpointWal() {
|
|
|
76216
76352
|
if (db == null)
|
|
76217
76353
|
return false;
|
|
76218
76354
|
try {
|
|
76219
|
-
|
|
76355
|
+
prep("PRAGMA wal_checkpoint(TRUNCATE)").run();
|
|
76220
76356
|
if (dbPath) {
|
|
76221
76357
|
for (const suffix of ["-shm", "-wal"]) {
|
|
76222
76358
|
const f = dbPath + suffix;
|
|
@@ -76233,13 +76369,45 @@ function checkpointWal() {
|
|
|
76233
76369
|
return false;
|
|
76234
76370
|
}
|
|
76235
76371
|
}
|
|
76372
|
+
function reopenHistory(stateDir, retentionDays = 30) {
|
|
76373
|
+
const current = db;
|
|
76374
|
+
if (current != null) {
|
|
76375
|
+
hardCloseDb(current);
|
|
76376
|
+
}
|
|
76377
|
+
db = null;
|
|
76378
|
+
try {
|
|
76379
|
+
initHistory(stateDir, retentionDays);
|
|
76380
|
+
const check = verifyHistoryWritable();
|
|
76381
|
+
if (!check.ok) {
|
|
76382
|
+
throw new Error(`post-reopen writer self-check failed: ${check.error ?? "unknown"}`);
|
|
76383
|
+
}
|
|
76384
|
+
} catch (err) {
|
|
76385
|
+
historyReopenFailure = err instanceof Error ? err.message : String(err);
|
|
76386
|
+
throw err;
|
|
76387
|
+
}
|
|
76388
|
+
historyReopenFailure = null;
|
|
76389
|
+
}
|
|
76390
|
+
function hardCloseDb(handle) {
|
|
76391
|
+
finalizeCachedStatements();
|
|
76392
|
+
const gc = globalThis.Bun?.gc;
|
|
76393
|
+
if (typeof gc === "function") {
|
|
76394
|
+
try {
|
|
76395
|
+
gc(true);
|
|
76396
|
+
} catch {}
|
|
76397
|
+
}
|
|
76398
|
+
handle.close(true);
|
|
76399
|
+
}
|
|
76400
|
+
var historyReopenFailure = null;
|
|
76401
|
+
function getHistoryReopenFailure() {
|
|
76402
|
+
return historyReopenFailure;
|
|
76403
|
+
}
|
|
76236
76404
|
function pruneMessagesOlderThanDays(retentionDays, nowSec, batchLimit = 5000) {
|
|
76237
76405
|
if (db == null)
|
|
76238
76406
|
return 0;
|
|
76239
76407
|
if (retentionDays <= 0)
|
|
76240
76408
|
return 0;
|
|
76241
76409
|
const cutoffSec = (nowSec ?? Math.floor(Date.now() / 1000)) - retentionDays * 86400;
|
|
76242
|
-
const stmt =
|
|
76410
|
+
const stmt = prep(`
|
|
76243
76411
|
DELETE FROM messages
|
|
76244
76412
|
WHERE rowid IN (
|
|
76245
76413
|
SELECT rowid FROM messages WHERE ts < ? LIMIT ?
|
|
@@ -76268,7 +76436,7 @@ function recordInbound(args) {
|
|
|
76268
76436
|
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`);
|
|
76269
76437
|
return;
|
|
76270
76438
|
}
|
|
76271
|
-
const stmt =
|
|
76439
|
+
const stmt = prep(`
|
|
76272
76440
|
INSERT OR REPLACE INTO messages
|
|
76273
76441
|
(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)
|
|
76274
76442
|
VALUES (?, ?, ?, 'user', ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -76295,12 +76463,12 @@ function recordOutbound(args) {
|
|
|
76295
76463
|
if (validRows.length === 0)
|
|
76296
76464
|
return;
|
|
76297
76465
|
const groupId = validRows[0].id;
|
|
76298
|
-
const stmt =
|
|
76466
|
+
const stmt = prep(`
|
|
76299
76467
|
INSERT OR REPLACE INTO messages
|
|
76300
76468
|
(chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id)
|
|
76301
76469
|
VALUES (?, ?, ?, 'assistant', NULL, NULL, ?, ?, ?, ?)
|
|
76302
76470
|
`);
|
|
76303
|
-
const dropSystem =
|
|
76471
|
+
const dropSystem = prep(`DELETE FROM messages WHERE chat_id = ? AND message_id = ? AND role = 'system'`);
|
|
76304
76472
|
const tx = requireDb().transaction((rows) => {
|
|
76305
76473
|
for (const r of rows) {
|
|
76306
76474
|
dropSystem.run(args.chat_id, r.id);
|
|
@@ -76320,8 +76488,8 @@ function recordSystemOutbound(args) {
|
|
|
76320
76488
|
if (db == null)
|
|
76321
76489
|
return false;
|
|
76322
76490
|
try {
|
|
76323
|
-
const res =
|
|
76324
|
-
|
|
76491
|
+
const res = prep(`
|
|
76492
|
+
INSERT INTO messages
|
|
76325
76493
|
(chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id, kind)
|
|
76326
76494
|
SELECT ?, ?, ?, 'system', NULL, NULL, ?, ?, NULL, NULL, ?
|
|
76327
76495
|
WHERE NOT EXISTS (
|
|
@@ -76336,31 +76504,31 @@ function recordSystemOutbound(args) {
|
|
|
76336
76504
|
}
|
|
76337
76505
|
function updateSystemOutboundText(args) {
|
|
76338
76506
|
try {
|
|
76339
|
-
const res =
|
|
76507
|
+
const res = prep(`UPDATE messages SET text = ? WHERE chat_id = ? AND message_id = ? AND role = 'system'`).run(redact(args.text), args.chat_id, args.message_id);
|
|
76340
76508
|
return (res?.changes ?? 0) > 0;
|
|
76341
76509
|
} catch {
|
|
76342
76510
|
return false;
|
|
76343
76511
|
}
|
|
76344
76512
|
}
|
|
76345
76513
|
function recordEdit(args) {
|
|
76346
|
-
|
|
76347
|
-
|
|
76348
|
-
|
|
76349
|
-
|
|
76350
|
-
|
|
76514
|
+
prep(`
|
|
76515
|
+
UPDATE messages
|
|
76516
|
+
SET text = ?
|
|
76517
|
+
WHERE chat_id = ? AND message_id = ?
|
|
76518
|
+
`).run(redact(args.text), args.chat_id, args.message_id);
|
|
76351
76519
|
}
|
|
76352
76520
|
function recordReaction(args) {
|
|
76353
|
-
|
|
76354
|
-
|
|
76355
|
-
|
|
76356
|
-
|
|
76357
|
-
|
|
76521
|
+
prep(`
|
|
76522
|
+
UPDATE messages
|
|
76523
|
+
SET user_reaction = ?
|
|
76524
|
+
WHERE chat_id = ? AND message_id = ?
|
|
76525
|
+
`).run(args.emoji, args.chat_id, args.message_id);
|
|
76358
76526
|
}
|
|
76359
76527
|
function deleteFromHistory(args) {
|
|
76360
|
-
|
|
76361
|
-
|
|
76362
|
-
|
|
76363
|
-
|
|
76528
|
+
prep(`
|
|
76529
|
+
DELETE FROM messages
|
|
76530
|
+
WHERE chat_id = ? AND message_id = ?
|
|
76531
|
+
`).run(args.chat_id, args.message_id);
|
|
76364
76532
|
}
|
|
76365
76533
|
function getLatestInboundMessageId(chatId, threadId) {
|
|
76366
76534
|
const params = [chatId];
|
|
@@ -76374,12 +76542,14 @@ function getLatestInboundMessageId(chatId, threadId) {
|
|
|
76374
76542
|
}
|
|
76375
76543
|
}
|
|
76376
76544
|
sql += " ORDER BY ts DESC, message_id DESC LIMIT 1";
|
|
76377
|
-
const row =
|
|
76545
|
+
const row = prep(sql).get(...params);
|
|
76378
76546
|
return row?.message_id ?? null;
|
|
76379
76547
|
}
|
|
76380
76548
|
function lookupMessageRoleAndText(chatId, messageId, opts) {
|
|
76549
|
+
if (db == null)
|
|
76550
|
+
return null;
|
|
76381
76551
|
const sql = `SELECT role, text, kind FROM messages WHERE chat_id = ? AND message_id = ?` + (opts?.includeSystem === true ? "" : ` AND role <> 'system'`) + ` LIMIT 1`;
|
|
76382
|
-
const row =
|
|
76552
|
+
const row = prep(sql).get(chatId, messageId);
|
|
76383
76553
|
if (!row)
|
|
76384
76554
|
return null;
|
|
76385
76555
|
return { role: row.role, text: row.text ?? "", kind: row.kind ?? null };
|
|
@@ -76399,7 +76569,7 @@ function hasOutboundDeliveredSince(chatId, sinceMs, threadId, minChars = 200) {
|
|
|
76399
76569
|
}
|
|
76400
76570
|
}
|
|
76401
76571
|
sql += " LIMIT 1";
|
|
76402
|
-
const row =
|
|
76572
|
+
const row = prep(sql).get(...params);
|
|
76403
76573
|
return row != null;
|
|
76404
76574
|
} catch {
|
|
76405
76575
|
return false;
|
|
@@ -76425,7 +76595,7 @@ function hasOutboundWithText(chatId, text4, threadId, sinceMs) {
|
|
|
76425
76595
|
params.push(Math.floor(sinceMs / 1000));
|
|
76426
76596
|
}
|
|
76427
76597
|
sql += " ORDER BY ts DESC LIMIT 500";
|
|
76428
|
-
const rows =
|
|
76598
|
+
const rows = prep(sql).all(...params);
|
|
76429
76599
|
for (const r of rows) {
|
|
76430
76600
|
const hay = normalizeDeliveryText(r.text ?? "");
|
|
76431
76601
|
if (hay.length === 0)
|
|
@@ -76451,6 +76621,10 @@ function deliveryTextMatch(hay, needle) {
|
|
|
76451
76621
|
return hay.startsWith(needle) || needle.startsWith(hay);
|
|
76452
76622
|
}
|
|
76453
76623
|
function query(opts) {
|
|
76624
|
+
if (db == null) {
|
|
76625
|
+
warnHistory("query: history DB is not open \u2014 returning no rows. If a reopen failed, the " + "orphaned-db-sweep is alarming about it every tick; RESTART the gateway.");
|
|
76626
|
+
return [];
|
|
76627
|
+
}
|
|
76454
76628
|
const limit = Math.min(MAX_LIMIT, Math.max(1, opts.limit ?? DEFAULT_LIMIT));
|
|
76455
76629
|
const params = [opts.chat_id];
|
|
76456
76630
|
let sql = "SELECT * FROM messages WHERE chat_id = ?";
|
|
@@ -76470,11 +76644,126 @@ function query(opts) {
|
|
|
76470
76644
|
}
|
|
76471
76645
|
sql += " ORDER BY ts DESC, message_id DESC LIMIT ?";
|
|
76472
76646
|
params.push(limit);
|
|
76473
|
-
const rows =
|
|
76647
|
+
const rows = prep(sql).all(...params);
|
|
76474
76648
|
rows.reverse();
|
|
76475
76649
|
return rows;
|
|
76476
76650
|
}
|
|
76477
76651
|
|
|
76652
|
+
// gateway/orphaned-db-sweep.ts
|
|
76653
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
76654
|
+
import { opendir, readlink } from "fs/promises";
|
|
76655
|
+
import { basename as basename9 } from "path";
|
|
76656
|
+
var DELETED_SUFFIX = " (deleted)";
|
|
76657
|
+
var DB_BASENAME_RE = /\.db(-wal|-shm|-journal)?$/;
|
|
76658
|
+
function resolveStateDirPrefix(stateDir) {
|
|
76659
|
+
let resolved;
|
|
76660
|
+
try {
|
|
76661
|
+
resolved = realpathSync2(stateDir);
|
|
76662
|
+
} catch {
|
|
76663
|
+
return null;
|
|
76664
|
+
}
|
|
76665
|
+
return resolved.endsWith("/") ? resolved : resolved + "/";
|
|
76666
|
+
}
|
|
76667
|
+
async function detectOrphanedDbFds(stateDir) {
|
|
76668
|
+
if (process.platform !== "linux")
|
|
76669
|
+
return [];
|
|
76670
|
+
const prefix = resolveStateDirPrefix(stateDir);
|
|
76671
|
+
if (prefix == null)
|
|
76672
|
+
return [];
|
|
76673
|
+
const found = [];
|
|
76674
|
+
try {
|
|
76675
|
+
const dir = await opendir("/proc/self/fd");
|
|
76676
|
+
for await (const entry of dir) {
|
|
76677
|
+
let target;
|
|
76678
|
+
try {
|
|
76679
|
+
target = await readlink(`/proc/self/fd/${entry.name}`);
|
|
76680
|
+
} catch {
|
|
76681
|
+
continue;
|
|
76682
|
+
}
|
|
76683
|
+
const deleted = target.endsWith(DELETED_SUFFIX);
|
|
76684
|
+
const bare = deleted ? target.slice(0, -DELETED_SUFFIX.length) : target;
|
|
76685
|
+
if (!bare.startsWith(prefix))
|
|
76686
|
+
continue;
|
|
76687
|
+
if (!deleted)
|
|
76688
|
+
continue;
|
|
76689
|
+
if (!DB_BASENAME_RE.test(basename9(bare)))
|
|
76690
|
+
continue;
|
|
76691
|
+
found.push({ fd: Number(entry.name), target });
|
|
76692
|
+
}
|
|
76693
|
+
} catch {
|
|
76694
|
+
return found;
|
|
76695
|
+
}
|
|
76696
|
+
return found;
|
|
76697
|
+
}
|
|
76698
|
+
function orphanBasename(target) {
|
|
76699
|
+
return basename9(target.endsWith(DELETED_SUFFIX) ? target.slice(0, -DELETED_SUFFIX.length) : target);
|
|
76700
|
+
}
|
|
76701
|
+
async function runOrphanedDbSweepTick(opts) {
|
|
76702
|
+
if (process.platform === "linux" && resolveStateDirPrefix(opts.stateDir) == null) {
|
|
76703
|
+
opts.log(`telegram gateway: orphaned-db-sweep cannot resolve stateDir=${opts.stateDir} \u2014` + ` deleted-inode DB detection is DISABLED until it exists and is readable.
|
|
76704
|
+
`);
|
|
76705
|
+
}
|
|
76706
|
+
const orphans = await detectOrphanedDbFds(opts.stateDir);
|
|
76707
|
+
let historyHandled = false;
|
|
76708
|
+
if (orphans.length > 0) {
|
|
76709
|
+
const names = orphans.map((o) => orphanBasename(o.target));
|
|
76710
|
+
opts.log(`telegram gateway: orphaned-db-sweep DETECTED ${orphans.length} deleted-inode DB handle(s): ` + orphans.map((o) => `fd=${o.fd} ${o.target}`).join(", ") + ` \u2014 another process unlinked these files while we held them open; every row written` + ` since the last checkpoint is LOST and further writes would be lost too.
|
|
76711
|
+
`);
|
|
76712
|
+
if (names.some((n) => n.startsWith("history.db"))) {
|
|
76713
|
+
historyHandled = true;
|
|
76714
|
+
if (opts.reopenHistory) {
|
|
76715
|
+
attemptHistoryReopen(opts, "reopened history.db");
|
|
76716
|
+
} else {
|
|
76717
|
+
opts.log(`telegram gateway: orphaned-db-sweep found an orphaned history.db handle but no reopen` + ` is wired (history disabled) \u2014 RESTART the gateway to recover.
|
|
76718
|
+
`);
|
|
76719
|
+
}
|
|
76720
|
+
}
|
|
76721
|
+
if (names.some((n) => n.startsWith("registry.db"))) {
|
|
76722
|
+
opts.log(`telegram gateway: orphaned-db-sweep found an orphaned registry.db handle. An in-process` + ` reopen is NOT safe here \u2014 the turnsDb handle is captured by value into long-lived` + ` wiring, so closing it would leave those consumers on a closed handle. RESTART the` + ` gateway to recover; subagent/turn rows written since the last checkpoint are LOST.
|
|
76723
|
+
`);
|
|
76724
|
+
}
|
|
76725
|
+
const unowned = [...new Set(names.filter((n) => !n.startsWith("history.db") && !n.startsWith("registry.db")))];
|
|
76726
|
+
if (unowned.length > 0) {
|
|
76727
|
+
opts.log(`telegram gateway: orphaned-db-sweep found orphaned handle(s) on ${unowned.join(", ")},` + ` which no recovery lane owns \u2014 the gateway cannot reopen them in place. RESTART the` + ` gateway to recover; rows written to those files since the last checkpoint are LOST.
|
|
76728
|
+
`);
|
|
76729
|
+
}
|
|
76730
|
+
}
|
|
76731
|
+
if (!historyHandled) {
|
|
76732
|
+
const stuck = opts.historyReopenFailure?.();
|
|
76733
|
+
if (stuck != null && stuck !== "") {
|
|
76734
|
+
opts.log(`telegram gateway: orphaned-db-sweep history.db is CLOSED and a previous reopen failed` + ` (${stuck}) \u2014 every history read and write is dead and no fd evidence remains.` + ` Retrying the reopen; RESTART the gateway if this keeps repeating.
|
|
76735
|
+
`);
|
|
76736
|
+
if (opts.reopenHistory)
|
|
76737
|
+
attemptHistoryReopen(opts, "recovered history.db");
|
|
76738
|
+
}
|
|
76739
|
+
}
|
|
76740
|
+
return orphans;
|
|
76741
|
+
}
|
|
76742
|
+
function attemptHistoryReopen(opts, successVerb) {
|
|
76743
|
+
try {
|
|
76744
|
+
opts.reopenHistory?.();
|
|
76745
|
+
opts.log(`telegram gateway: orphaned-db-sweep ${successVerb} \u2014 writes are durable again` + ` (proved by the post-reopen writer self-check); rows written since the last` + ` checkpoint are NOT recoverable.
|
|
76746
|
+
`);
|
|
76747
|
+
} catch (err) {
|
|
76748
|
+
opts.log(`telegram gateway: orphaned-db-sweep FAILED to reopen history.db: ${err.message}` + ` \u2014 history writes are NOT durable; RESTART the gateway.
|
|
76749
|
+
`);
|
|
76750
|
+
}
|
|
76751
|
+
}
|
|
76752
|
+
var DEFAULT_INTERVAL_MS = 5 * 60000;
|
|
76753
|
+
function startOrphanedDbSweep(opts) {
|
|
76754
|
+
let running = false;
|
|
76755
|
+
const timer3 = setInterval(() => {
|
|
76756
|
+
if (running)
|
|
76757
|
+
return;
|
|
76758
|
+
running = true;
|
|
76759
|
+
runOrphanedDbSweepTick(opts).catch(() => {}).finally(() => {
|
|
76760
|
+
running = false;
|
|
76761
|
+
});
|
|
76762
|
+
}, opts.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
76763
|
+
timer3.unref?.();
|
|
76764
|
+
return () => clearInterval(timer3);
|
|
76765
|
+
}
|
|
76766
|
+
|
|
76478
76767
|
// shared/sent-text-capture.ts
|
|
76479
76768
|
var SENT_TEXT2 = Symbol.for("switchroom.telegram.sentText");
|
|
76480
76769
|
function readSentText(message) {
|
|
@@ -88236,7 +88525,7 @@ async function discoverModels(agentName3, opts = {}) {
|
|
|
88236
88525
|
init_atomic();
|
|
88237
88526
|
|
|
88238
88527
|
// ../src/util/shipped-assets.ts
|
|
88239
|
-
import { existsSync as existsSync35, readFileSync as readFileSync34, realpathSync as
|
|
88528
|
+
import { existsSync as existsSync35, readFileSync as readFileSync34, realpathSync as realpathSync3 } from "node:fs";
|
|
88240
88529
|
import { dirname as dirname20, resolve as resolve7 } from "node:path";
|
|
88241
88530
|
var FHS_SHARE_ROOTS = [
|
|
88242
88531
|
"/usr/local/share/switchroom",
|
|
@@ -88285,7 +88574,7 @@ function resolveShippedAsset(spec, probe) {
|
|
|
88285
88574
|
return { path: null, candidates: candidates.map((x) => x.path), source: "none" };
|
|
88286
88575
|
}
|
|
88287
88576
|
function canonicalise(path2, probe) {
|
|
88288
|
-
const realpath = probe.realpath ??
|
|
88577
|
+
const realpath = probe.realpath ?? realpathSync3;
|
|
88289
88578
|
try {
|
|
88290
88579
|
return realpath(path2);
|
|
88291
88580
|
} catch {
|
|
@@ -88363,7 +88652,7 @@ var AUDIT_ROOT = join45(homedir11(), ".switchroom", "audit");
|
|
|
88363
88652
|
|
|
88364
88653
|
// ../src/agents/profiles.ts
|
|
88365
88654
|
var import_handlebars = __toESM(require_lib(), 1);
|
|
88366
|
-
import { readFileSync as readFileSync36, writeFileSync as writeFileSync31, existsSync as existsSync37, readdirSync as readdirSync9, statSync as statSync14, copyFileSync, mkdirSync as mkdirSync33, realpathSync as
|
|
88655
|
+
import { readFileSync as readFileSync36, writeFileSync as writeFileSync31, existsSync as existsSync37, readdirSync as readdirSync9, statSync as statSync14, copyFileSync, mkdirSync as mkdirSync33, realpathSync as realpathSync4 } from "node:fs";
|
|
88367
88656
|
import { resolve as resolve8, join as join46, sep as pathSep } from "node:path";
|
|
88368
88657
|
function resolveProfilesRootDetailed() {
|
|
88369
88658
|
return resolveShippedAsset(PROFILES_ASSET, {
|
|
@@ -97868,7 +98157,7 @@ function buildSilencePokeOptions(deps) {
|
|
|
97868
98157
|
var MAX_LABEL_CHARS = 60;
|
|
97869
98158
|
var MAX_BASH_CHARS = 40;
|
|
97870
98159
|
var MAX_DESCRIPTION_CHARS = 160;
|
|
97871
|
-
function
|
|
98160
|
+
function basename11(p) {
|
|
97872
98161
|
if (!p)
|
|
97873
98162
|
return "";
|
|
97874
98163
|
const parts = p.split("/").filter(Boolean);
|
|
@@ -97941,7 +98230,7 @@ function toolLabel(tool, input, preamble, precomputedLabel) {
|
|
|
97941
98230
|
const pre = preambleLabel();
|
|
97942
98231
|
if (pre)
|
|
97943
98232
|
return pre;
|
|
97944
|
-
return truncate4(
|
|
98233
|
+
return truncate4(basename11(str("file_path") ?? ""));
|
|
97945
98234
|
}
|
|
97946
98235
|
case "Bash":
|
|
97947
98236
|
case "BashOutput": {
|
|
@@ -98026,7 +98315,7 @@ function toolLabel(tool, input, preamble, precomputedLabel) {
|
|
|
98026
98315
|
const v = str(k);
|
|
98027
98316
|
if (v != null && v.length > 0) {
|
|
98028
98317
|
if (k === "file_path" || k === "path")
|
|
98029
|
-
return truncate4(
|
|
98318
|
+
return truncate4(basename11(v));
|
|
98030
98319
|
if (k === "url")
|
|
98031
98320
|
return truncate4(hostFromUrl(v));
|
|
98032
98321
|
if (k === "description")
|
|
@@ -99830,7 +100119,7 @@ function sanitiseToolArg(name, raw) {
|
|
|
99830
100119
|
case "NotebookEdit": {
|
|
99831
100120
|
const fp = raw.file_path;
|
|
99832
100121
|
if (typeof fp === "string" && fp.length > 0)
|
|
99833
|
-
out =
|
|
100122
|
+
out = basename12(fp);
|
|
99834
100123
|
break;
|
|
99835
100124
|
}
|
|
99836
100125
|
case "Bash": {
|
|
@@ -99866,7 +100155,7 @@ function sanitiseToolArg(name, raw) {
|
|
|
99866
100155
|
out = out.slice(0, SANITISE_MAX_LEN - 1) + "\u2026";
|
|
99867
100156
|
return out;
|
|
99868
100157
|
}
|
|
99869
|
-
function
|
|
100158
|
+
function basename12(p) {
|
|
99870
100159
|
const idx = p.lastIndexOf("/");
|
|
99871
100160
|
return idx === -1 ? p : p.slice(idx + 1);
|
|
99872
100161
|
}
|
|
@@ -101183,13 +101472,13 @@ function recordExists(id) {
|
|
|
101183
101472
|
}
|
|
101184
101473
|
|
|
101185
101474
|
// worktree-watch-cwds.ts
|
|
101186
|
-
import { realpathSync as
|
|
101187
|
-
import { basename as
|
|
101475
|
+
import { realpathSync as realpathSync5 } from "node:fs";
|
|
101476
|
+
import { basename as basename14 } from "node:path";
|
|
101188
101477
|
var identityEscalated = false;
|
|
101189
101478
|
function defaultDeriveName(agentDir) {
|
|
101190
101479
|
if (!agentDir || agentDir.trim().length === 0)
|
|
101191
101480
|
return "";
|
|
101192
|
-
const leaf =
|
|
101481
|
+
const leaf = basename14(agentDir).trim();
|
|
101193
101482
|
return leaf;
|
|
101194
101483
|
}
|
|
101195
101484
|
function resolveOwnerIdentity(self, agentDir, deriveName) {
|
|
@@ -101209,7 +101498,7 @@ function ownedWorktreeCwds(opts) {
|
|
|
101209
101498
|
}
|
|
101210
101499
|
return [];
|
|
101211
101500
|
}
|
|
101212
|
-
const rp = opts.realpath ??
|
|
101501
|
+
const rp = opts.realpath ?? realpathSync5;
|
|
101213
101502
|
try {
|
|
101214
101503
|
return opts.listRecords().filter((r) => r.ownerAgent === resolved).map((r) => {
|
|
101215
101504
|
try {
|
|
@@ -102029,7 +102318,7 @@ function defaultReadEvents(stateDir) {
|
|
|
102029
102318
|
}
|
|
102030
102319
|
// permission-title.ts
|
|
102031
102320
|
init_card_format();
|
|
102032
|
-
import { basename as
|
|
102321
|
+
import { basename as basename15 } from "node:path";
|
|
102033
102322
|
init_redact();
|
|
102034
102323
|
var COMMAND_TITLE_MAX2 = 48;
|
|
102035
102324
|
var DESCRIPTION_LINE_MAX = 240;
|
|
@@ -102283,11 +102572,11 @@ function describeGrant(toolName, inputPreview, option) {
|
|
|
102283
102572
|
return m ? `run ${m[1]} commands` : "run that command";
|
|
102284
102573
|
}
|
|
102285
102574
|
if (t === "Edit" || t === "MultiEdit" || t === "NotebookEdit")
|
|
102286
|
-
return `edit ${
|
|
102575
|
+
return `edit ${basename15(arg)}`;
|
|
102287
102576
|
if (t === "Write")
|
|
102288
|
-
return `write ${
|
|
102577
|
+
return `write ${basename15(arg)}`;
|
|
102289
102578
|
if (t === "Read")
|
|
102290
|
-
return `read ${
|
|
102579
|
+
return `read ${basename15(arg)}`;
|
|
102291
102580
|
return naturalAction2(toolName, inputPreview);
|
|
102292
102581
|
}
|
|
102293
102582
|
switch (rule) {
|
|
@@ -102326,12 +102615,12 @@ function fileBase2(input, rawPreview) {
|
|
|
102326
102615
|
if (input) {
|
|
102327
102616
|
const p = readString3(input, "file_path") ?? readString3(input, "notebook_path");
|
|
102328
102617
|
if (p)
|
|
102329
|
-
return
|
|
102618
|
+
return basename15(p);
|
|
102330
102619
|
}
|
|
102331
102620
|
if (rawPreview) {
|
|
102332
102621
|
const p = extractFilePathFromRaw3(rawPreview);
|
|
102333
102622
|
if (p)
|
|
102334
|
-
return
|
|
102623
|
+
return basename15(p);
|
|
102335
102624
|
}
|
|
102336
102625
|
return null;
|
|
102337
102626
|
}
|
|
@@ -102462,7 +102751,7 @@ function truncate7(text4, max) {
|
|
|
102462
102751
|
}
|
|
102463
102752
|
|
|
102464
102753
|
// permission-rule.ts
|
|
102465
|
-
import { basename as
|
|
102754
|
+
import { basename as basename16 } from "node:path";
|
|
102466
102755
|
var FILE_TOOLS2 = new Set([
|
|
102467
102756
|
"Edit",
|
|
102468
102757
|
"Write",
|
|
@@ -102594,14 +102883,14 @@ function skillBasenameFromPath4(input) {
|
|
|
102594
102883
|
if (!path3)
|
|
102595
102884
|
return null;
|
|
102596
102885
|
const trimmed = path3.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
|
|
102597
|
-
return
|
|
102886
|
+
return basename16(trimmed) || null;
|
|
102598
102887
|
}
|
|
102599
102888
|
function isRulePersisted(resolvedAllow, ruleRule) {
|
|
102600
102889
|
return resolvedAllow.includes(ruleRule);
|
|
102601
102890
|
}
|
|
102602
102891
|
|
|
102603
102892
|
// scoped-approval.ts
|
|
102604
|
-
import { basename as
|
|
102893
|
+
import { basename as basename17 } from "node:path";
|
|
102605
102894
|
var SCOPED_APPROVAL_DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
102606
102895
|
function scopedApprovalTtlMs(env = process.env) {
|
|
102607
102896
|
const raw = env.SWITCHROOM_SCOPED_APPROVAL_TTL_MS;
|
|
@@ -102626,7 +102915,7 @@ function resolveTimeBox(toolName, inputPreview, choices) {
|
|
|
102626
102915
|
const fileMatch = FILE_RULE.exec(rule);
|
|
102627
102916
|
if (fileMatch) {
|
|
102628
102917
|
const verb = fileMatch[1] === "Read" ? "reads of" : "edits to";
|
|
102629
|
-
return { rule, breadth: `${verb} ${
|
|
102918
|
+
return { rule, breadth: `${verb} ${basename17(fileMatch[2])}` };
|
|
102630
102919
|
}
|
|
102631
102920
|
const bashMatch = BASH_FAMILY_RULE.exec(rule);
|
|
102632
102921
|
if (bashMatch) {
|
|
@@ -104178,10 +104467,10 @@ function startOutboxSweep(deps) {
|
|
|
104178
104467
|
}
|
|
104179
104468
|
|
|
104180
104469
|
// ../src/build-info.ts
|
|
104181
|
-
var VERSION2 = "0.21.
|
|
104182
|
-
var COMMIT_SHA = "
|
|
104183
|
-
var COMMIT_DATE = "2026-08-
|
|
104184
|
-
var LATEST_PR =
|
|
104470
|
+
var VERSION2 = "0.21.5";
|
|
104471
|
+
var COMMIT_SHA = "a9aceb2f";
|
|
104472
|
+
var COMMIT_DATE = "2026-08-10T21:47:53Z";
|
|
104473
|
+
var LATEST_PR = 4603;
|
|
104185
104474
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
104186
104475
|
|
|
104187
104476
|
// gateway/boot-version.ts
|
|
@@ -106777,8 +107066,8 @@ function assertSendable(f) {
|
|
|
106777
107066
|
}
|
|
106778
107067
|
let real, stateReal;
|
|
106779
107068
|
try {
|
|
106780
|
-
real =
|
|
106781
|
-
stateReal =
|
|
107069
|
+
real = realpathSync6(f);
|
|
107070
|
+
stateReal = realpathSync6(STATE_DIR);
|
|
106782
107071
|
} catch {
|
|
106783
107072
|
throw new Error(`refusing to send file \u2014 cannot resolve real path: ${f}`);
|
|
106784
107073
|
}
|
|
@@ -107067,6 +107356,8 @@ if (isGatewayMain)
|
|
|
107067
107356
|
if (isGatewayMain && !STATIC) {
|
|
107068
107357
|
setInterval(() => runHistoryReaperNow("periodic"), REGISTRY_REAPER_INTERVAL_MS).unref();
|
|
107069
107358
|
}
|
|
107359
|
+
if (isGatewayMain && !STATIC)
|
|
107360
|
+
startOrphanedDbSweep({ stateDir: STATE_DIR, reopenHistory: HISTORY_ENABLED ? () => reopenHistory(STATE_DIR, HISTORY_ACCESS.historyRetentionDays ?? 30) : undefined, historyReopenFailure: HISTORY_ENABLED ? getHistoryReopenFailure : undefined, log: (l) => process.stderr.write(l) });
|
|
107070
107361
|
function checkApprovals() {
|
|
107071
107362
|
let files;
|
|
107072
107363
|
try {
|
|
@@ -108353,15 +108644,7 @@ var rawRobustApiCall = createRetryApiCall2({
|
|
|
108353
108644
|
floodWaitRemainingMs: probeFloodWaitRemainingMs
|
|
108354
108645
|
});
|
|
108355
108646
|
var observeSentMessage = isGatewayMain && HISTORY_ENABLED ? makeSystemMessageObserver({ insert: recordSystemOutbound, updateText: updateSystemOutboundText }) : undefined;
|
|
108356
|
-
var robustApiCall = (fn, opts) =>
|
|
108357
|
-
const p = sendGate.gate(() => rawRobustApiCall(fn, opts), opts);
|
|
108358
|
-
if (observeSentMessage == null)
|
|
108359
|
-
return p;
|
|
108360
|
-
return p.then((res) => {
|
|
108361
|
-
observeSentMessage(res, opts);
|
|
108362
|
-
return res;
|
|
108363
|
-
});
|
|
108364
|
-
};
|
|
108647
|
+
var robustApiCall = (fn, opts) => sendGate.gate(() => withTgSendContext(opts, () => rawRobustApiCall(fn, opts)), opts);
|
|
108365
108648
|
var swallowingApiCall = createSwallowingRetryApiCall(robustApiCall, (line) => process.stderr.write(line));
|
|
108366
108649
|
var resetPrivacyForNewSession = makePrivacyResetForNewSession((chatId, threadId, text5) => void swallowingApiCall(() => lockedBot.api.sendMessage(chatId, text5, threadId != null ? { message_thread_id: threadId, disable_notification: false } : { disable_notification: false }), { chat_id: chatId, verb: "privacy-reset-alert", priorityClass: "critical" }));
|
|
108367
108650
|
var gatedSetMessageReaction = (chatId, messageId, reaction) => robustApiCall(() => lockedBot.api.setMessageReaction(chatId, messageId, reaction), {
|
|
@@ -110637,7 +110920,7 @@ if (isGatewayMain)
|
|
|
110637
110920
|
if (turnsDb != null && msg.activeFile != null) {
|
|
110638
110921
|
const stampKey = currentTurn?.registryKey ?? null;
|
|
110639
110922
|
if (stampKey != null && stampKey !== lastSessionStampedTurnKey) {
|
|
110640
|
-
const sessionId =
|
|
110923
|
+
const sessionId = basename18(msg.activeFile).replace(/\.jsonl$/, "");
|
|
110641
110924
|
if (sessionId) {
|
|
110642
110925
|
try {
|
|
110643
110926
|
stampTurnSessionId(turnsDb, stampKey, sessionId);
|
|
@@ -113370,7 +113653,7 @@ function getMyAgentName() {
|
|
|
113370
113653
|
const fromEnv = process.env.SWITCHROOM_AGENT_NAME;
|
|
113371
113654
|
if (fromEnv && fromEnv.trim().length > 0)
|
|
113372
113655
|
return fromEnv.trim();
|
|
113373
|
-
return
|
|
113656
|
+
return basename18(process.cwd());
|
|
113374
113657
|
}
|
|
113375
113658
|
function isSelfTargetingCommand(name) {
|
|
113376
113659
|
if (name === "all")
|
|
@@ -117407,6 +117690,8 @@ async function initGatewayBot() {
|
|
|
117407
117690
|
installTgPostLogger(bot);
|
|
117408
117691
|
installRichMarkdownGuard(bot);
|
|
117409
117692
|
installSentTextCapture(bot);
|
|
117693
|
+
if (observeSentMessage != null)
|
|
117694
|
+
installSystemMessageObserver(bot, observeSentMessage);
|
|
117410
117695
|
installEditFloodFuse(bot, {
|
|
117411
117696
|
...editFloodFuseConfigFromEnv(process.env),
|
|
117412
117697
|
onTrip: (i) => process.stderr.write(`edit-flood-fuse ${i.action} method=${i.method} key=${i.key} class=${i.cls}
|