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.
@@ -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 = db2.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?`).get(LOGICAL_KEY_INDEX) != null;
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
- db2.prepare("DELETE FROM messages WHERE ts < ?").run(cutoff);
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
- db2.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
30248
- db2.prepare(`INSERT OR REPLACE INTO messages
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 = db2.prepare("SELECT text FROM messages WHERE chat_id = ? AND message_id = ?").get(SENTINEL_CHAT, sentinelId);
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
- db2.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
30289
+ prep2("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
30261
30290
  } catch {}
30262
30291
  }
30263
30292
  }
30264
30293
  function getHistoryDbForBriefing() {
30265
- return db2;
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.close();
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
- db2.prepare("PRAGMA wal_checkpoint(TRUNCATE)").run();
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 = db2.prepare(`
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 = requireDb2().prepare(`
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 = requireDb2().prepare(`
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 = requireDb2().prepare(`DELETE FROM messages WHERE chat_id = ? AND message_id = ? AND role = 'system'`);
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 = requireDb2().prepare(`
30382
- INSERT INTO messages
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 = requireDb2().prepare(`UPDATE messages SET text = ? WHERE chat_id = ? AND message_id = ? AND role = 'system'`).run(redact(args.text), args.chat_id, args.message_id);
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
- requireDb2().prepare(`
30405
- UPDATE messages
30406
- SET text = ?
30407
- WHERE chat_id = ? AND message_id = ?
30408
- `).run(redact(args.text), args.chat_id, args.message_id);
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
- requireDb2().prepare(`
30412
- UPDATE messages
30413
- SET user_reaction = ?
30414
- WHERE chat_id = ? AND message_id = ?
30415
- `).run(args.emoji, args.chat_id, args.message_id);
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
- requireDb2().prepare(`
30419
- DELETE FROM messages
30420
- WHERE chat_id = ? AND message_id = ?
30421
- `).run(args.chat_id, args.message_id);
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 = requireDb2().prepare(sql).get(...params);
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 = requireDb2().prepare(sql).get(chatId, messageId);
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 = requireDb2().prepare("SELECT COUNT(*) as cnt FROM messages WHERE chat_id = ? AND role = ? AND ts >= ?").get(chatId, "assistant", cutoff);
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 = requireDb2().prepare(sql).get(...params);
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 = requireDb2().prepare(sql).all(...params);
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 = requireDb2().prepare(sql).all(...params);
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 realpathSync5,
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 basename17 } from "path";
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 replyToTextRaw = quoteText != null && quoteText.length > 0 ? quoteText : replyToMsg ? replyToMsg.text ?? replyToMsg.caption ?? undefined : undefined;
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 && liveTextEmpty) {
45754
+ if (p.historyEnabled && p.replyToMessageId != null) {
45678
45755
  try {
45679
45756
  const recovered = p.lookup(p.replyToMessageId);
45680
- if (recovered != null && recovered.role === "system" && recovered.kind) {
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 = db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?`).get(LOGICAL_KEY_INDEX) != null;
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
- db.prepare("DELETE FROM messages WHERE ts < ?").run(cutoff);
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
- db.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
76199
- db.prepare(`INSERT OR REPLACE INTO messages
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 = db.prepare("SELECT text FROM messages WHERE chat_id = ? AND message_id = ?").get(SENTINEL_CHAT, sentinelId);
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
- db.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
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
- db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").run();
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 = db.prepare(`
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 = requireDb().prepare(`
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 = requireDb().prepare(`
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 = requireDb().prepare(`DELETE FROM messages WHERE chat_id = ? AND message_id = ? AND role = 'system'`);
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 = requireDb().prepare(`
76324
- INSERT INTO messages
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 = requireDb().prepare(`UPDATE messages SET text = ? WHERE chat_id = ? AND message_id = ? AND role = 'system'`).run(redact(args.text), args.chat_id, args.message_id);
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
- requireDb().prepare(`
76347
- UPDATE messages
76348
- SET text = ?
76349
- WHERE chat_id = ? AND message_id = ?
76350
- `).run(redact(args.text), args.chat_id, args.message_id);
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
- requireDb().prepare(`
76354
- UPDATE messages
76355
- SET user_reaction = ?
76356
- WHERE chat_id = ? AND message_id = ?
76357
- `).run(args.emoji, args.chat_id, args.message_id);
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
- requireDb().prepare(`
76361
- DELETE FROM messages
76362
- WHERE chat_id = ? AND message_id = ?
76363
- `).run(args.chat_id, args.message_id);
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 = requireDb().prepare(sql).get(...params);
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 = requireDb().prepare(sql).get(chatId, messageId);
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 = requireDb().prepare(sql).get(...params);
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 = requireDb().prepare(sql).all(...params);
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 = requireDb().prepare(sql).all(...params);
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 realpathSync2 } from "node:fs";
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 ?? realpathSync2;
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 realpathSync3 } from "node:fs";
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 basename10(p) {
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(basename10(str("file_path") ?? ""));
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(basename10(v));
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 = basename11(fp);
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 basename11(p) {
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 realpathSync4 } from "node:fs";
101187
- import { basename as basename13 } from "node:path";
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 = basename13(agentDir).trim();
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 ?? realpathSync4;
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 basename14 } from "node:path";
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 ${basename14(arg)}`;
102575
+ return `edit ${basename15(arg)}`;
102287
102576
  if (t === "Write")
102288
- return `write ${basename14(arg)}`;
102577
+ return `write ${basename15(arg)}`;
102289
102578
  if (t === "Read")
102290
- return `read ${basename14(arg)}`;
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 basename14(p);
102618
+ return basename15(p);
102330
102619
  }
102331
102620
  if (rawPreview) {
102332
102621
  const p = extractFilePathFromRaw3(rawPreview);
102333
102622
  if (p)
102334
- return basename14(p);
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 basename15 } from "node:path";
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 basename15(trimmed) || null;
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 basename16 } from "node:path";
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} ${basename16(fileMatch[2])}` };
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.3";
104182
- var COMMIT_SHA = "8a74c2f6";
104183
- var COMMIT_DATE = "2026-08-10T08:58:41Z";
104184
- var LATEST_PR = 4593;
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 = realpathSync5(f);
106781
- stateReal = realpathSync5(STATE_DIR);
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 = basename17(msg.activeFile).replace(/\.jsonl$/, "");
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 basename17(process.cwd());
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}