switchroom 0.21.3 → 0.21.4

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";
@@ -76091,6 +76166,34 @@ var DEFAULT_LIMIT = 10;
76091
76166
  var MAX_LIMIT = 50;
76092
76167
  var db = null;
76093
76168
  var dbPath = null;
76169
+ var stmtCache = new Map;
76170
+ var MAX_CACHED_STATEMENTS = 128;
76171
+ function prep(sql) {
76172
+ const cached = stmtCache.get(sql);
76173
+ if (cached != null)
76174
+ return cached;
76175
+ const stmt = requireDb().prepare(sql);
76176
+ if (stmtCache.size >= MAX_CACHED_STATEMENTS) {
76177
+ const oldestKey = stmtCache.keys().next().value;
76178
+ if (oldestKey != null) {
76179
+ const oldest = stmtCache.get(oldestKey);
76180
+ stmtCache.delete(oldestKey);
76181
+ try {
76182
+ oldest?.finalize?.();
76183
+ } catch {}
76184
+ }
76185
+ }
76186
+ stmtCache.set(sql, stmt);
76187
+ return stmt;
76188
+ }
76189
+ function finalizeCachedStatements() {
76190
+ for (const stmt of stmtCache.values()) {
76191
+ try {
76192
+ stmt.finalize?.();
76193
+ } catch {}
76194
+ }
76195
+ stmtCache.clear();
76196
+ }
76094
76197
  function warnHistory(msg) {
76095
76198
  try {
76096
76199
  process.stderr.write(`telegram history: ${msg}
@@ -76153,7 +76256,7 @@ function initHistory(stateDir, retentionDays = 30) {
76153
76256
  }
76154
76257
  }
76155
76258
  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;
76259
+ const logicalKeyIndexExists = prep(`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?`).get(LOGICAL_KEY_INDEX) != null;
76157
76260
  if (!logicalKeyIndexExists) {
76158
76261
  db.exec(`
76159
76262
  DELETE FROM messages
@@ -76182,7 +76285,7 @@ function initHistory(stateDir, retentionDays = 30) {
76182
76285
  adoptSqliteOwnership(path2);
76183
76286
  if (retentionDays > 0) {
76184
76287
  const cutoff = Math.floor(Date.now() / 1000) - retentionDays * 86400;
76185
- db.prepare("DELETE FROM messages WHERE ts < ?").run(cutoff);
76288
+ prep("DELETE FROM messages WHERE ts < ?").run(cutoff);
76186
76289
  }
76187
76290
  const check = verifyHistoryWritable();
76188
76291
  if (!check.ok) {
@@ -76195,11 +76298,11 @@ function verifyHistoryWritable() {
76195
76298
  const SENTINEL_CHAT = "__history_selfcheck__";
76196
76299
  const sentinelId = Date.now();
76197
76300
  try {
76198
- db.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
76199
- db.prepare(`INSERT OR REPLACE INTO messages
76301
+ prep("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
76302
+ prep(`INSERT OR REPLACE INTO messages
76200
76303
  (chat_id, thread_id, message_id, role, ts, text)
76201
76304
  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);
76305
+ const row = prep("SELECT text FROM messages WHERE chat_id = ? AND message_id = ?").get(SENTINEL_CHAT, sentinelId);
76203
76306
  if (row?.text !== "selfcheck") {
76204
76307
  return { ok: false, error: "sentinel row not read back after insert" };
76205
76308
  }
@@ -76208,7 +76311,7 @@ function verifyHistoryWritable() {
76208
76311
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
76209
76312
  } finally {
76210
76313
  try {
76211
- db.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
76314
+ prep("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
76212
76315
  } catch {}
76213
76316
  }
76214
76317
  }
@@ -76216,7 +76319,7 @@ function checkpointWal() {
76216
76319
  if (db == null)
76217
76320
  return false;
76218
76321
  try {
76219
- db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").run();
76322
+ prep("PRAGMA wal_checkpoint(TRUNCATE)").run();
76220
76323
  if (dbPath) {
76221
76324
  for (const suffix of ["-shm", "-wal"]) {
76222
76325
  const f = dbPath + suffix;
@@ -76233,13 +76336,45 @@ function checkpointWal() {
76233
76336
  return false;
76234
76337
  }
76235
76338
  }
76339
+ function reopenHistory(stateDir, retentionDays = 30) {
76340
+ const current = db;
76341
+ if (current != null) {
76342
+ hardCloseDb(current);
76343
+ }
76344
+ db = null;
76345
+ try {
76346
+ initHistory(stateDir, retentionDays);
76347
+ const check = verifyHistoryWritable();
76348
+ if (!check.ok) {
76349
+ throw new Error(`post-reopen writer self-check failed: ${check.error ?? "unknown"}`);
76350
+ }
76351
+ } catch (err) {
76352
+ historyReopenFailure = err instanceof Error ? err.message : String(err);
76353
+ throw err;
76354
+ }
76355
+ historyReopenFailure = null;
76356
+ }
76357
+ function hardCloseDb(handle) {
76358
+ finalizeCachedStatements();
76359
+ const gc = globalThis.Bun?.gc;
76360
+ if (typeof gc === "function") {
76361
+ try {
76362
+ gc(true);
76363
+ } catch {}
76364
+ }
76365
+ handle.close(true);
76366
+ }
76367
+ var historyReopenFailure = null;
76368
+ function getHistoryReopenFailure() {
76369
+ return historyReopenFailure;
76370
+ }
76236
76371
  function pruneMessagesOlderThanDays(retentionDays, nowSec, batchLimit = 5000) {
76237
76372
  if (db == null)
76238
76373
  return 0;
76239
76374
  if (retentionDays <= 0)
76240
76375
  return 0;
76241
76376
  const cutoffSec = (nowSec ?? Math.floor(Date.now() / 1000)) - retentionDays * 86400;
76242
- const stmt = db.prepare(`
76377
+ const stmt = prep(`
76243
76378
  DELETE FROM messages
76244
76379
  WHERE rowid IN (
76245
76380
  SELECT rowid FROM messages WHERE ts < ? LIMIT ?
@@ -76268,7 +76403,7 @@ function recordInbound(args) {
76268
76403
  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
76404
  return;
76270
76405
  }
76271
- const stmt = requireDb().prepare(`
76406
+ const stmt = prep(`
76272
76407
  INSERT OR REPLACE INTO messages
76273
76408
  (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
76409
  VALUES (?, ?, ?, 'user', ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)
@@ -76295,12 +76430,12 @@ function recordOutbound(args) {
76295
76430
  if (validRows.length === 0)
76296
76431
  return;
76297
76432
  const groupId = validRows[0].id;
76298
- const stmt = requireDb().prepare(`
76433
+ const stmt = prep(`
76299
76434
  INSERT OR REPLACE INTO messages
76300
76435
  (chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id)
76301
76436
  VALUES (?, ?, ?, 'assistant', NULL, NULL, ?, ?, ?, ?)
76302
76437
  `);
76303
- const dropSystem = requireDb().prepare(`DELETE FROM messages WHERE chat_id = ? AND message_id = ? AND role = 'system'`);
76438
+ const dropSystem = prep(`DELETE FROM messages WHERE chat_id = ? AND message_id = ? AND role = 'system'`);
76304
76439
  const tx = requireDb().transaction((rows) => {
76305
76440
  for (const r of rows) {
76306
76441
  dropSystem.run(args.chat_id, r.id);
@@ -76320,8 +76455,8 @@ function recordSystemOutbound(args) {
76320
76455
  if (db == null)
76321
76456
  return false;
76322
76457
  try {
76323
- const res = requireDb().prepare(`
76324
- INSERT INTO messages
76458
+ const res = prep(`
76459
+ INSERT INTO messages
76325
76460
  (chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id, kind)
76326
76461
  SELECT ?, ?, ?, 'system', NULL, NULL, ?, ?, NULL, NULL, ?
76327
76462
  WHERE NOT EXISTS (
@@ -76336,31 +76471,31 @@ function recordSystemOutbound(args) {
76336
76471
  }
76337
76472
  function updateSystemOutboundText(args) {
76338
76473
  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);
76474
+ 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
76475
  return (res?.changes ?? 0) > 0;
76341
76476
  } catch {
76342
76477
  return false;
76343
76478
  }
76344
76479
  }
76345
76480
  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);
76481
+ prep(`
76482
+ UPDATE messages
76483
+ SET text = ?
76484
+ WHERE chat_id = ? AND message_id = ?
76485
+ `).run(redact(args.text), args.chat_id, args.message_id);
76351
76486
  }
76352
76487
  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);
76488
+ prep(`
76489
+ UPDATE messages
76490
+ SET user_reaction = ?
76491
+ WHERE chat_id = ? AND message_id = ?
76492
+ `).run(args.emoji, args.chat_id, args.message_id);
76358
76493
  }
76359
76494
  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);
76495
+ prep(`
76496
+ DELETE FROM messages
76497
+ WHERE chat_id = ? AND message_id = ?
76498
+ `).run(args.chat_id, args.message_id);
76364
76499
  }
76365
76500
  function getLatestInboundMessageId(chatId, threadId) {
76366
76501
  const params = [chatId];
@@ -76374,12 +76509,14 @@ function getLatestInboundMessageId(chatId, threadId) {
76374
76509
  }
76375
76510
  }
76376
76511
  sql += " ORDER BY ts DESC, message_id DESC LIMIT 1";
76377
- const row = requireDb().prepare(sql).get(...params);
76512
+ const row = prep(sql).get(...params);
76378
76513
  return row?.message_id ?? null;
76379
76514
  }
76380
76515
  function lookupMessageRoleAndText(chatId, messageId, opts) {
76516
+ if (db == null)
76517
+ return null;
76381
76518
  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);
76519
+ const row = prep(sql).get(chatId, messageId);
76383
76520
  if (!row)
76384
76521
  return null;
76385
76522
  return { role: row.role, text: row.text ?? "", kind: row.kind ?? null };
@@ -76399,7 +76536,7 @@ function hasOutboundDeliveredSince(chatId, sinceMs, threadId, minChars = 200) {
76399
76536
  }
76400
76537
  }
76401
76538
  sql += " LIMIT 1";
76402
- const row = requireDb().prepare(sql).get(...params);
76539
+ const row = prep(sql).get(...params);
76403
76540
  return row != null;
76404
76541
  } catch {
76405
76542
  return false;
@@ -76425,7 +76562,7 @@ function hasOutboundWithText(chatId, text4, threadId, sinceMs) {
76425
76562
  params.push(Math.floor(sinceMs / 1000));
76426
76563
  }
76427
76564
  sql += " ORDER BY ts DESC LIMIT 500";
76428
- const rows = requireDb().prepare(sql).all(...params);
76565
+ const rows = prep(sql).all(...params);
76429
76566
  for (const r of rows) {
76430
76567
  const hay = normalizeDeliveryText(r.text ?? "");
76431
76568
  if (hay.length === 0)
@@ -76451,6 +76588,10 @@ function deliveryTextMatch(hay, needle) {
76451
76588
  return hay.startsWith(needle) || needle.startsWith(hay);
76452
76589
  }
76453
76590
  function query(opts) {
76591
+ if (db == null) {
76592
+ 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.");
76593
+ return [];
76594
+ }
76454
76595
  const limit = Math.min(MAX_LIMIT, Math.max(1, opts.limit ?? DEFAULT_LIMIT));
76455
76596
  const params = [opts.chat_id];
76456
76597
  let sql = "SELECT * FROM messages WHERE chat_id = ?";
@@ -76470,11 +76611,126 @@ function query(opts) {
76470
76611
  }
76471
76612
  sql += " ORDER BY ts DESC, message_id DESC LIMIT ?";
76472
76613
  params.push(limit);
76473
- const rows = requireDb().prepare(sql).all(...params);
76614
+ const rows = prep(sql).all(...params);
76474
76615
  rows.reverse();
76475
76616
  return rows;
76476
76617
  }
76477
76618
 
76619
+ // gateway/orphaned-db-sweep.ts
76620
+ import { realpathSync as realpathSync2 } from "fs";
76621
+ import { opendir, readlink } from "fs/promises";
76622
+ import { basename as basename9 } from "path";
76623
+ var DELETED_SUFFIX = " (deleted)";
76624
+ var DB_BASENAME_RE = /\.db(-wal|-shm|-journal)?$/;
76625
+ function resolveStateDirPrefix(stateDir) {
76626
+ let resolved;
76627
+ try {
76628
+ resolved = realpathSync2(stateDir);
76629
+ } catch {
76630
+ return null;
76631
+ }
76632
+ return resolved.endsWith("/") ? resolved : resolved + "/";
76633
+ }
76634
+ async function detectOrphanedDbFds(stateDir) {
76635
+ if (process.platform !== "linux")
76636
+ return [];
76637
+ const prefix = resolveStateDirPrefix(stateDir);
76638
+ if (prefix == null)
76639
+ return [];
76640
+ const found = [];
76641
+ try {
76642
+ const dir = await opendir("/proc/self/fd");
76643
+ for await (const entry of dir) {
76644
+ let target;
76645
+ try {
76646
+ target = await readlink(`/proc/self/fd/${entry.name}`);
76647
+ } catch {
76648
+ continue;
76649
+ }
76650
+ const deleted = target.endsWith(DELETED_SUFFIX);
76651
+ const bare = deleted ? target.slice(0, -DELETED_SUFFIX.length) : target;
76652
+ if (!bare.startsWith(prefix))
76653
+ continue;
76654
+ if (!deleted)
76655
+ continue;
76656
+ if (!DB_BASENAME_RE.test(basename9(bare)))
76657
+ continue;
76658
+ found.push({ fd: Number(entry.name), target });
76659
+ }
76660
+ } catch {
76661
+ return found;
76662
+ }
76663
+ return found;
76664
+ }
76665
+ function orphanBasename(target) {
76666
+ return basename9(target.endsWith(DELETED_SUFFIX) ? target.slice(0, -DELETED_SUFFIX.length) : target);
76667
+ }
76668
+ async function runOrphanedDbSweepTick(opts) {
76669
+ if (process.platform === "linux" && resolveStateDirPrefix(opts.stateDir) == null) {
76670
+ 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.
76671
+ `);
76672
+ }
76673
+ const orphans = await detectOrphanedDbFds(opts.stateDir);
76674
+ let historyHandled = false;
76675
+ if (orphans.length > 0) {
76676
+ const names = orphans.map((o) => orphanBasename(o.target));
76677
+ 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.
76678
+ `);
76679
+ if (names.some((n) => n.startsWith("history.db"))) {
76680
+ historyHandled = true;
76681
+ if (opts.reopenHistory) {
76682
+ attemptHistoryReopen(opts, "reopened history.db");
76683
+ } else {
76684
+ 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.
76685
+ `);
76686
+ }
76687
+ }
76688
+ if (names.some((n) => n.startsWith("registry.db"))) {
76689
+ 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.
76690
+ `);
76691
+ }
76692
+ const unowned = [...new Set(names.filter((n) => !n.startsWith("history.db") && !n.startsWith("registry.db")))];
76693
+ if (unowned.length > 0) {
76694
+ 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.
76695
+ `);
76696
+ }
76697
+ }
76698
+ if (!historyHandled) {
76699
+ const stuck = opts.historyReopenFailure?.();
76700
+ if (stuck != null && stuck !== "") {
76701
+ 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.
76702
+ `);
76703
+ if (opts.reopenHistory)
76704
+ attemptHistoryReopen(opts, "recovered history.db");
76705
+ }
76706
+ }
76707
+ return orphans;
76708
+ }
76709
+ function attemptHistoryReopen(opts, successVerb) {
76710
+ try {
76711
+ opts.reopenHistory?.();
76712
+ 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.
76713
+ `);
76714
+ } catch (err) {
76715
+ opts.log(`telegram gateway: orphaned-db-sweep FAILED to reopen history.db: ${err.message}` + ` \u2014 history writes are NOT durable; RESTART the gateway.
76716
+ `);
76717
+ }
76718
+ }
76719
+ var DEFAULT_INTERVAL_MS = 5 * 60000;
76720
+ function startOrphanedDbSweep(opts) {
76721
+ let running = false;
76722
+ const timer3 = setInterval(() => {
76723
+ if (running)
76724
+ return;
76725
+ running = true;
76726
+ runOrphanedDbSweepTick(opts).catch(() => {}).finally(() => {
76727
+ running = false;
76728
+ });
76729
+ }, opts.intervalMs ?? DEFAULT_INTERVAL_MS);
76730
+ timer3.unref?.();
76731
+ return () => clearInterval(timer3);
76732
+ }
76733
+
76478
76734
  // shared/sent-text-capture.ts
76479
76735
  var SENT_TEXT2 = Symbol.for("switchroom.telegram.sentText");
76480
76736
  function readSentText(message) {
@@ -88236,7 +88492,7 @@ async function discoverModels(agentName3, opts = {}) {
88236
88492
  init_atomic();
88237
88493
 
88238
88494
  // ../src/util/shipped-assets.ts
88239
- import { existsSync as existsSync35, readFileSync as readFileSync34, realpathSync as realpathSync2 } from "node:fs";
88495
+ import { existsSync as existsSync35, readFileSync as readFileSync34, realpathSync as realpathSync3 } from "node:fs";
88240
88496
  import { dirname as dirname20, resolve as resolve7 } from "node:path";
88241
88497
  var FHS_SHARE_ROOTS = [
88242
88498
  "/usr/local/share/switchroom",
@@ -88285,7 +88541,7 @@ function resolveShippedAsset(spec, probe) {
88285
88541
  return { path: null, candidates: candidates.map((x) => x.path), source: "none" };
88286
88542
  }
88287
88543
  function canonicalise(path2, probe) {
88288
- const realpath = probe.realpath ?? realpathSync2;
88544
+ const realpath = probe.realpath ?? realpathSync3;
88289
88545
  try {
88290
88546
  return realpath(path2);
88291
88547
  } catch {
@@ -88363,7 +88619,7 @@ var AUDIT_ROOT = join45(homedir11(), ".switchroom", "audit");
88363
88619
 
88364
88620
  // ../src/agents/profiles.ts
88365
88621
  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";
88622
+ 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
88623
  import { resolve as resolve8, join as join46, sep as pathSep } from "node:path";
88368
88624
  function resolveProfilesRootDetailed() {
88369
88625
  return resolveShippedAsset(PROFILES_ASSET, {
@@ -97868,7 +98124,7 @@ function buildSilencePokeOptions(deps) {
97868
98124
  var MAX_LABEL_CHARS = 60;
97869
98125
  var MAX_BASH_CHARS = 40;
97870
98126
  var MAX_DESCRIPTION_CHARS = 160;
97871
- function basename10(p) {
98127
+ function basename11(p) {
97872
98128
  if (!p)
97873
98129
  return "";
97874
98130
  const parts = p.split("/").filter(Boolean);
@@ -97941,7 +98197,7 @@ function toolLabel(tool, input, preamble, precomputedLabel) {
97941
98197
  const pre = preambleLabel();
97942
98198
  if (pre)
97943
98199
  return pre;
97944
- return truncate4(basename10(str("file_path") ?? ""));
98200
+ return truncate4(basename11(str("file_path") ?? ""));
97945
98201
  }
97946
98202
  case "Bash":
97947
98203
  case "BashOutput": {
@@ -98026,7 +98282,7 @@ function toolLabel(tool, input, preamble, precomputedLabel) {
98026
98282
  const v = str(k);
98027
98283
  if (v != null && v.length > 0) {
98028
98284
  if (k === "file_path" || k === "path")
98029
- return truncate4(basename10(v));
98285
+ return truncate4(basename11(v));
98030
98286
  if (k === "url")
98031
98287
  return truncate4(hostFromUrl(v));
98032
98288
  if (k === "description")
@@ -99830,7 +100086,7 @@ function sanitiseToolArg(name, raw) {
99830
100086
  case "NotebookEdit": {
99831
100087
  const fp = raw.file_path;
99832
100088
  if (typeof fp === "string" && fp.length > 0)
99833
- out = basename11(fp);
100089
+ out = basename12(fp);
99834
100090
  break;
99835
100091
  }
99836
100092
  case "Bash": {
@@ -99866,7 +100122,7 @@ function sanitiseToolArg(name, raw) {
99866
100122
  out = out.slice(0, SANITISE_MAX_LEN - 1) + "\u2026";
99867
100123
  return out;
99868
100124
  }
99869
- function basename11(p) {
100125
+ function basename12(p) {
99870
100126
  const idx = p.lastIndexOf("/");
99871
100127
  return idx === -1 ? p : p.slice(idx + 1);
99872
100128
  }
@@ -101183,13 +101439,13 @@ function recordExists(id) {
101183
101439
  }
101184
101440
 
101185
101441
  // worktree-watch-cwds.ts
101186
- import { realpathSync as realpathSync4 } from "node:fs";
101187
- import { basename as basename13 } from "node:path";
101442
+ import { realpathSync as realpathSync5 } from "node:fs";
101443
+ import { basename as basename14 } from "node:path";
101188
101444
  var identityEscalated = false;
101189
101445
  function defaultDeriveName(agentDir) {
101190
101446
  if (!agentDir || agentDir.trim().length === 0)
101191
101447
  return "";
101192
- const leaf = basename13(agentDir).trim();
101448
+ const leaf = basename14(agentDir).trim();
101193
101449
  return leaf;
101194
101450
  }
101195
101451
  function resolveOwnerIdentity(self, agentDir, deriveName) {
@@ -101209,7 +101465,7 @@ function ownedWorktreeCwds(opts) {
101209
101465
  }
101210
101466
  return [];
101211
101467
  }
101212
- const rp = opts.realpath ?? realpathSync4;
101468
+ const rp = opts.realpath ?? realpathSync5;
101213
101469
  try {
101214
101470
  return opts.listRecords().filter((r) => r.ownerAgent === resolved).map((r) => {
101215
101471
  try {
@@ -102029,7 +102285,7 @@ function defaultReadEvents(stateDir) {
102029
102285
  }
102030
102286
  // permission-title.ts
102031
102287
  init_card_format();
102032
- import { basename as basename14 } from "node:path";
102288
+ import { basename as basename15 } from "node:path";
102033
102289
  init_redact();
102034
102290
  var COMMAND_TITLE_MAX2 = 48;
102035
102291
  var DESCRIPTION_LINE_MAX = 240;
@@ -102283,11 +102539,11 @@ function describeGrant(toolName, inputPreview, option) {
102283
102539
  return m ? `run ${m[1]} commands` : "run that command";
102284
102540
  }
102285
102541
  if (t === "Edit" || t === "MultiEdit" || t === "NotebookEdit")
102286
- return `edit ${basename14(arg)}`;
102542
+ return `edit ${basename15(arg)}`;
102287
102543
  if (t === "Write")
102288
- return `write ${basename14(arg)}`;
102544
+ return `write ${basename15(arg)}`;
102289
102545
  if (t === "Read")
102290
- return `read ${basename14(arg)}`;
102546
+ return `read ${basename15(arg)}`;
102291
102547
  return naturalAction2(toolName, inputPreview);
102292
102548
  }
102293
102549
  switch (rule) {
@@ -102326,12 +102582,12 @@ function fileBase2(input, rawPreview) {
102326
102582
  if (input) {
102327
102583
  const p = readString3(input, "file_path") ?? readString3(input, "notebook_path");
102328
102584
  if (p)
102329
- return basename14(p);
102585
+ return basename15(p);
102330
102586
  }
102331
102587
  if (rawPreview) {
102332
102588
  const p = extractFilePathFromRaw3(rawPreview);
102333
102589
  if (p)
102334
- return basename14(p);
102590
+ return basename15(p);
102335
102591
  }
102336
102592
  return null;
102337
102593
  }
@@ -102462,7 +102718,7 @@ function truncate7(text4, max) {
102462
102718
  }
102463
102719
 
102464
102720
  // permission-rule.ts
102465
- import { basename as basename15 } from "node:path";
102721
+ import { basename as basename16 } from "node:path";
102466
102722
  var FILE_TOOLS2 = new Set([
102467
102723
  "Edit",
102468
102724
  "Write",
@@ -102594,14 +102850,14 @@ function skillBasenameFromPath4(input) {
102594
102850
  if (!path3)
102595
102851
  return null;
102596
102852
  const trimmed = path3.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
102597
- return basename15(trimmed) || null;
102853
+ return basename16(trimmed) || null;
102598
102854
  }
102599
102855
  function isRulePersisted(resolvedAllow, ruleRule) {
102600
102856
  return resolvedAllow.includes(ruleRule);
102601
102857
  }
102602
102858
 
102603
102859
  // scoped-approval.ts
102604
- import { basename as basename16 } from "node:path";
102860
+ import { basename as basename17 } from "node:path";
102605
102861
  var SCOPED_APPROVAL_DEFAULT_TTL_MS = 30 * 60 * 1000;
102606
102862
  function scopedApprovalTtlMs(env = process.env) {
102607
102863
  const raw = env.SWITCHROOM_SCOPED_APPROVAL_TTL_MS;
@@ -102626,7 +102882,7 @@ function resolveTimeBox(toolName, inputPreview, choices) {
102626
102882
  const fileMatch = FILE_RULE.exec(rule);
102627
102883
  if (fileMatch) {
102628
102884
  const verb = fileMatch[1] === "Read" ? "reads of" : "edits to";
102629
- return { rule, breadth: `${verb} ${basename16(fileMatch[2])}` };
102885
+ return { rule, breadth: `${verb} ${basename17(fileMatch[2])}` };
102630
102886
  }
102631
102887
  const bashMatch = BASH_FAMILY_RULE.exec(rule);
102632
102888
  if (bashMatch) {
@@ -104178,10 +104434,10 @@ function startOutboxSweep(deps) {
104178
104434
  }
104179
104435
 
104180
104436
  // ../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;
104437
+ var VERSION2 = "0.21.4";
104438
+ var COMMIT_SHA = "aa63e74c";
104439
+ var COMMIT_DATE = "2026-08-10T17:32:03Z";
104440
+ var LATEST_PR = 4597;
104185
104441
  var COMMITS_AHEAD_OF_TAG = 0;
104186
104442
 
104187
104443
  // gateway/boot-version.ts
@@ -106777,8 +107033,8 @@ function assertSendable(f) {
106777
107033
  }
106778
107034
  let real, stateReal;
106779
107035
  try {
106780
- real = realpathSync5(f);
106781
- stateReal = realpathSync5(STATE_DIR);
107036
+ real = realpathSync6(f);
107037
+ stateReal = realpathSync6(STATE_DIR);
106782
107038
  } catch {
106783
107039
  throw new Error(`refusing to send file \u2014 cannot resolve real path: ${f}`);
106784
107040
  }
@@ -107067,6 +107323,8 @@ if (isGatewayMain)
107067
107323
  if (isGatewayMain && !STATIC) {
107068
107324
  setInterval(() => runHistoryReaperNow("periodic"), REGISTRY_REAPER_INTERVAL_MS).unref();
107069
107325
  }
107326
+ if (isGatewayMain && !STATIC)
107327
+ 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
107328
  function checkApprovals() {
107071
107329
  let files;
107072
107330
  try {
@@ -110637,7 +110895,7 @@ if (isGatewayMain)
110637
110895
  if (turnsDb != null && msg.activeFile != null) {
110638
110896
  const stampKey = currentTurn?.registryKey ?? null;
110639
110897
  if (stampKey != null && stampKey !== lastSessionStampedTurnKey) {
110640
- const sessionId = basename17(msg.activeFile).replace(/\.jsonl$/, "");
110898
+ const sessionId = basename18(msg.activeFile).replace(/\.jsonl$/, "");
110641
110899
  if (sessionId) {
110642
110900
  try {
110643
110901
  stampTurnSessionId(turnsDb, stampKey, sessionId);
@@ -113370,7 +113628,7 @@ function getMyAgentName() {
113370
113628
  const fromEnv = process.env.SWITCHROOM_AGENT_NAME;
113371
113629
  if (fromEnv && fromEnv.trim().length > 0)
113372
113630
  return fromEnv.trim();
113373
- return basename17(process.cwd());
113631
+ return basename18(process.cwd());
113374
113632
  }
113375
113633
  function isSelfTargetingCommand(name) {
113376
113634
  if (name === "all")