switchroom 0.21.1 → 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
@@ -39117,15 +39192,24 @@ function summarizeExternalSpend(days, now = new Date) {
39117
39192
  const top = Object.entries(byModel).filter(([, usd]) => usd > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, EXTERNAL_SPEND_TOP_N).map(([name, usd]) => ({ label: shortModelLabel(name), usd }));
39118
39193
  return { day24hUsd, day7dUsd, top };
39119
39194
  }
39120
- function normalizeSpendLogRows(body) {
39121
- if (Array.isArray(body))
39122
- return body;
39123
- if (body && typeof body === "object") {
39124
- const data = body.data;
39125
- if (Array.isArray(data))
39126
- return data;
39195
+ function normalizeDailyActivityRows(body) {
39196
+ const results = body && typeof body === "object" && Array.isArray(body.results) ? body.results : [];
39197
+ const rows = [];
39198
+ for (const day of results) {
39199
+ if (!day || typeof day !== "object")
39200
+ continue;
39201
+ const models = {};
39202
+ const breakdown = day.breakdown?.models ?? {};
39203
+ for (const [name, entry] of Object.entries(breakdown)) {
39204
+ const raw = entry?.metrics?.spend;
39205
+ const n = typeof raw === "number" ? raw : Number(raw);
39206
+ if (!Number.isFinite(n))
39207
+ continue;
39208
+ models[name] = (models[name] ?? 0) + n;
39209
+ }
39210
+ rows.push({ startTime: day.date, models });
39127
39211
  }
39128
- return [];
39212
+ return rows;
39129
39213
  }
39130
39214
  var EXTERNAL_SPEND_TOP_N = 3, EXTERNAL_SPEND_CACHE_TTL_MS = 90000, BARE_EXTERNAL_NEEDLES;
39131
39215
  var init_external_spend = __esm(() => {
@@ -39341,7 +39425,7 @@ __export(exports_external_spend, {
39341
39425
  utcDateString: () => utcDateString,
39342
39426
  summarizeExternalSpend: () => summarizeExternalSpend,
39343
39427
  shortModelLabel: () => shortModelLabel,
39344
- normalizeSpendLogRows: () => normalizeSpendLogRows,
39428
+ normalizeDailyActivityRows: () => normalizeDailyActivityRows,
39345
39429
  isExternalModel: () => isExternalModel,
39346
39430
  formatUsd: () => formatUsd,
39347
39431
  formatExternalSpendBlock: () => formatExternalSpendBlock2,
@@ -39732,7 +39816,7 @@ import {
39732
39816
  rmSync as rmSync9,
39733
39817
  statSync as statSync25,
39734
39818
  renameSync as renameSync30,
39735
- realpathSync as realpathSync5,
39819
+ realpathSync as realpathSync6,
39736
39820
  chmodSync as chmodSync14,
39737
39821
  openSync as openSync18,
39738
39822
  closeSync as closeSync18,
@@ -39755,7 +39839,7 @@ function fsyncPathSync(path) {
39755
39839
 
39756
39840
  // gateway/gateway.ts
39757
39841
  import { homedir as homedir22 } from "os";
39758
- 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";
39759
39843
 
39760
39844
  // plugin-logger.ts
39761
39845
  import { appendFileSync, mkdirSync, renameSync as renameSync2, statSync, existsSync } from "fs";
@@ -76082,6 +76166,34 @@ var DEFAULT_LIMIT = 10;
76082
76166
  var MAX_LIMIT = 50;
76083
76167
  var db = null;
76084
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
+ }
76085
76197
  function warnHistory(msg) {
76086
76198
  try {
76087
76199
  process.stderr.write(`telegram history: ${msg}
@@ -76144,7 +76256,7 @@ function initHistory(stateDir, retentionDays = 30) {
76144
76256
  }
76145
76257
  }
76146
76258
  const LOGICAL_KEY_INDEX = "idx_messages_logical_key";
76147
- 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;
76148
76260
  if (!logicalKeyIndexExists) {
76149
76261
  db.exec(`
76150
76262
  DELETE FROM messages
@@ -76173,7 +76285,7 @@ function initHistory(stateDir, retentionDays = 30) {
76173
76285
  adoptSqliteOwnership(path2);
76174
76286
  if (retentionDays > 0) {
76175
76287
  const cutoff = Math.floor(Date.now() / 1000) - retentionDays * 86400;
76176
- db.prepare("DELETE FROM messages WHERE ts < ?").run(cutoff);
76288
+ prep("DELETE FROM messages WHERE ts < ?").run(cutoff);
76177
76289
  }
76178
76290
  const check = verifyHistoryWritable();
76179
76291
  if (!check.ok) {
@@ -76186,11 +76298,11 @@ function verifyHistoryWritable() {
76186
76298
  const SENTINEL_CHAT = "__history_selfcheck__";
76187
76299
  const sentinelId = Date.now();
76188
76300
  try {
76189
- db.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
76190
- 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
76191
76303
  (chat_id, thread_id, message_id, role, ts, text)
76192
76304
  VALUES (?, NULL, ?, 'assistant', ?, ?)`).run(SENTINEL_CHAT, sentinelId, Math.floor(Date.now() / 1000), "selfcheck");
76193
- 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);
76194
76306
  if (row?.text !== "selfcheck") {
76195
76307
  return { ok: false, error: "sentinel row not read back after insert" };
76196
76308
  }
@@ -76199,7 +76311,7 @@ function verifyHistoryWritable() {
76199
76311
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
76200
76312
  } finally {
76201
76313
  try {
76202
- db.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
76314
+ prep("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
76203
76315
  } catch {}
76204
76316
  }
76205
76317
  }
@@ -76207,7 +76319,7 @@ function checkpointWal() {
76207
76319
  if (db == null)
76208
76320
  return false;
76209
76321
  try {
76210
- db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").run();
76322
+ prep("PRAGMA wal_checkpoint(TRUNCATE)").run();
76211
76323
  if (dbPath) {
76212
76324
  for (const suffix of ["-shm", "-wal"]) {
76213
76325
  const f = dbPath + suffix;
@@ -76224,13 +76336,45 @@ function checkpointWal() {
76224
76336
  return false;
76225
76337
  }
76226
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
+ }
76227
76371
  function pruneMessagesOlderThanDays(retentionDays, nowSec, batchLimit = 5000) {
76228
76372
  if (db == null)
76229
76373
  return 0;
76230
76374
  if (retentionDays <= 0)
76231
76375
  return 0;
76232
76376
  const cutoffSec = (nowSec ?? Math.floor(Date.now() / 1000)) - retentionDays * 86400;
76233
- const stmt = db.prepare(`
76377
+ const stmt = prep(`
76234
76378
  DELETE FROM messages
76235
76379
  WHERE rowid IN (
76236
76380
  SELECT rowid FROM messages WHERE ts < ? LIMIT ?
@@ -76259,7 +76403,7 @@ function recordInbound(args) {
76259
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`);
76260
76404
  return;
76261
76405
  }
76262
- const stmt = requireDb().prepare(`
76406
+ const stmt = prep(`
76263
76407
  INSERT OR REPLACE INTO messages
76264
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)
76265
76409
  VALUES (?, ?, ?, 'user', ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)
@@ -76286,12 +76430,12 @@ function recordOutbound(args) {
76286
76430
  if (validRows.length === 0)
76287
76431
  return;
76288
76432
  const groupId = validRows[0].id;
76289
- const stmt = requireDb().prepare(`
76433
+ const stmt = prep(`
76290
76434
  INSERT OR REPLACE INTO messages
76291
76435
  (chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id)
76292
76436
  VALUES (?, ?, ?, 'assistant', NULL, NULL, ?, ?, ?, ?)
76293
76437
  `);
76294
- 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'`);
76295
76439
  const tx = requireDb().transaction((rows) => {
76296
76440
  for (const r of rows) {
76297
76441
  dropSystem.run(args.chat_id, r.id);
@@ -76311,8 +76455,8 @@ function recordSystemOutbound(args) {
76311
76455
  if (db == null)
76312
76456
  return false;
76313
76457
  try {
76314
- const res = requireDb().prepare(`
76315
- INSERT INTO messages
76458
+ const res = prep(`
76459
+ INSERT INTO messages
76316
76460
  (chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id, kind)
76317
76461
  SELECT ?, ?, ?, 'system', NULL, NULL, ?, ?, NULL, NULL, ?
76318
76462
  WHERE NOT EXISTS (
@@ -76327,31 +76471,31 @@ function recordSystemOutbound(args) {
76327
76471
  }
76328
76472
  function updateSystemOutboundText(args) {
76329
76473
  try {
76330
- 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);
76331
76475
  return (res?.changes ?? 0) > 0;
76332
76476
  } catch {
76333
76477
  return false;
76334
76478
  }
76335
76479
  }
76336
76480
  function recordEdit(args) {
76337
- requireDb().prepare(`
76338
- UPDATE messages
76339
- SET text = ?
76340
- WHERE chat_id = ? AND message_id = ?
76341
- `).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);
76342
76486
  }
76343
76487
  function recordReaction(args) {
76344
- requireDb().prepare(`
76345
- UPDATE messages
76346
- SET user_reaction = ?
76347
- WHERE chat_id = ? AND message_id = ?
76348
- `).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);
76349
76493
  }
76350
76494
  function deleteFromHistory(args) {
76351
- requireDb().prepare(`
76352
- DELETE FROM messages
76353
- WHERE chat_id = ? AND message_id = ?
76354
- `).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);
76355
76499
  }
76356
76500
  function getLatestInboundMessageId(chatId, threadId) {
76357
76501
  const params = [chatId];
@@ -76365,12 +76509,14 @@ function getLatestInboundMessageId(chatId, threadId) {
76365
76509
  }
76366
76510
  }
76367
76511
  sql += " ORDER BY ts DESC, message_id DESC LIMIT 1";
76368
- const row = requireDb().prepare(sql).get(...params);
76512
+ const row = prep(sql).get(...params);
76369
76513
  return row?.message_id ?? null;
76370
76514
  }
76371
76515
  function lookupMessageRoleAndText(chatId, messageId, opts) {
76516
+ if (db == null)
76517
+ return null;
76372
76518
  const sql = `SELECT role, text, kind FROM messages WHERE chat_id = ? AND message_id = ?` + (opts?.includeSystem === true ? "" : ` AND role <> 'system'`) + ` LIMIT 1`;
76373
- const row = requireDb().prepare(sql).get(chatId, messageId);
76519
+ const row = prep(sql).get(chatId, messageId);
76374
76520
  if (!row)
76375
76521
  return null;
76376
76522
  return { role: row.role, text: row.text ?? "", kind: row.kind ?? null };
@@ -76390,7 +76536,7 @@ function hasOutboundDeliveredSince(chatId, sinceMs, threadId, minChars = 200) {
76390
76536
  }
76391
76537
  }
76392
76538
  sql += " LIMIT 1";
76393
- const row = requireDb().prepare(sql).get(...params);
76539
+ const row = prep(sql).get(...params);
76394
76540
  return row != null;
76395
76541
  } catch {
76396
76542
  return false;
@@ -76416,7 +76562,7 @@ function hasOutboundWithText(chatId, text4, threadId, sinceMs) {
76416
76562
  params.push(Math.floor(sinceMs / 1000));
76417
76563
  }
76418
76564
  sql += " ORDER BY ts DESC LIMIT 500";
76419
- const rows = requireDb().prepare(sql).all(...params);
76565
+ const rows = prep(sql).all(...params);
76420
76566
  for (const r of rows) {
76421
76567
  const hay = normalizeDeliveryText(r.text ?? "");
76422
76568
  if (hay.length === 0)
@@ -76442,6 +76588,10 @@ function deliveryTextMatch(hay, needle) {
76442
76588
  return hay.startsWith(needle) || needle.startsWith(hay);
76443
76589
  }
76444
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
+ }
76445
76595
  const limit = Math.min(MAX_LIMIT, Math.max(1, opts.limit ?? DEFAULT_LIMIT));
76446
76596
  const params = [opts.chat_id];
76447
76597
  let sql = "SELECT * FROM messages WHERE chat_id = ?";
@@ -76461,11 +76611,126 @@ function query(opts) {
76461
76611
  }
76462
76612
  sql += " ORDER BY ts DESC, message_id DESC LIMIT ?";
76463
76613
  params.push(limit);
76464
- const rows = requireDb().prepare(sql).all(...params);
76614
+ const rows = prep(sql).all(...params);
76465
76615
  rows.reverse();
76466
76616
  return rows;
76467
76617
  }
76468
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
+
76469
76734
  // shared/sent-text-capture.ts
76470
76735
  var SENT_TEXT2 = Symbol.for("switchroom.telegram.sentText");
76471
76736
  function readSentText(message) {
@@ -88227,7 +88492,7 @@ async function discoverModels(agentName3, opts = {}) {
88227
88492
  init_atomic();
88228
88493
 
88229
88494
  // ../src/util/shipped-assets.ts
88230
- 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";
88231
88496
  import { dirname as dirname20, resolve as resolve7 } from "node:path";
88232
88497
  var FHS_SHARE_ROOTS = [
88233
88498
  "/usr/local/share/switchroom",
@@ -88276,7 +88541,7 @@ function resolveShippedAsset(spec, probe) {
88276
88541
  return { path: null, candidates: candidates.map((x) => x.path), source: "none" };
88277
88542
  }
88278
88543
  function canonicalise(path2, probe) {
88279
- const realpath = probe.realpath ?? realpathSync2;
88544
+ const realpath = probe.realpath ?? realpathSync3;
88280
88545
  try {
88281
88546
  return realpath(path2);
88282
88547
  } catch {
@@ -88354,7 +88619,7 @@ var AUDIT_ROOT = join45(homedir11(), ".switchroom", "audit");
88354
88619
 
88355
88620
  // ../src/agents/profiles.ts
88356
88621
  var import_handlebars = __toESM(require_lib(), 1);
88357
- 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";
88358
88623
  import { resolve as resolve8, join as join46, sep as pathSep } from "node:path";
88359
88624
  function resolveProfilesRootDetailed() {
88360
88625
  return resolveShippedAsset(PROFILES_ASSET, {
@@ -97859,7 +98124,7 @@ function buildSilencePokeOptions(deps) {
97859
98124
  var MAX_LABEL_CHARS = 60;
97860
98125
  var MAX_BASH_CHARS = 40;
97861
98126
  var MAX_DESCRIPTION_CHARS = 160;
97862
- function basename10(p) {
98127
+ function basename11(p) {
97863
98128
  if (!p)
97864
98129
  return "";
97865
98130
  const parts = p.split("/").filter(Boolean);
@@ -97932,7 +98197,7 @@ function toolLabel(tool, input, preamble, precomputedLabel) {
97932
98197
  const pre = preambleLabel();
97933
98198
  if (pre)
97934
98199
  return pre;
97935
- return truncate4(basename10(str("file_path") ?? ""));
98200
+ return truncate4(basename11(str("file_path") ?? ""));
97936
98201
  }
97937
98202
  case "Bash":
97938
98203
  case "BashOutput": {
@@ -98017,7 +98282,7 @@ function toolLabel(tool, input, preamble, precomputedLabel) {
98017
98282
  const v = str(k);
98018
98283
  if (v != null && v.length > 0) {
98019
98284
  if (k === "file_path" || k === "path")
98020
- return truncate4(basename10(v));
98285
+ return truncate4(basename11(v));
98021
98286
  if (k === "url")
98022
98287
  return truncate4(hostFromUrl(v));
98023
98288
  if (k === "description")
@@ -99821,7 +100086,7 @@ function sanitiseToolArg(name, raw) {
99821
100086
  case "NotebookEdit": {
99822
100087
  const fp = raw.file_path;
99823
100088
  if (typeof fp === "string" && fp.length > 0)
99824
- out = basename11(fp);
100089
+ out = basename12(fp);
99825
100090
  break;
99826
100091
  }
99827
100092
  case "Bash": {
@@ -99857,7 +100122,7 @@ function sanitiseToolArg(name, raw) {
99857
100122
  out = out.slice(0, SANITISE_MAX_LEN - 1) + "\u2026";
99858
100123
  return out;
99859
100124
  }
99860
- function basename11(p) {
100125
+ function basename12(p) {
99861
100126
  const idx = p.lastIndexOf("/");
99862
100127
  return idx === -1 ? p : p.slice(idx + 1);
99863
100128
  }
@@ -101174,13 +101439,13 @@ function recordExists(id) {
101174
101439
  }
101175
101440
 
101176
101441
  // worktree-watch-cwds.ts
101177
- import { realpathSync as realpathSync4 } from "node:fs";
101178
- import { basename as basename13 } from "node:path";
101442
+ import { realpathSync as realpathSync5 } from "node:fs";
101443
+ import { basename as basename14 } from "node:path";
101179
101444
  var identityEscalated = false;
101180
101445
  function defaultDeriveName(agentDir) {
101181
101446
  if (!agentDir || agentDir.trim().length === 0)
101182
101447
  return "";
101183
- const leaf = basename13(agentDir).trim();
101448
+ const leaf = basename14(agentDir).trim();
101184
101449
  return leaf;
101185
101450
  }
101186
101451
  function resolveOwnerIdentity(self, agentDir, deriveName) {
@@ -101200,7 +101465,7 @@ function ownedWorktreeCwds(opts) {
101200
101465
  }
101201
101466
  return [];
101202
101467
  }
101203
- const rp = opts.realpath ?? realpathSync4;
101468
+ const rp = opts.realpath ?? realpathSync5;
101204
101469
  try {
101205
101470
  return opts.listRecords().filter((r) => r.ownerAgent === resolved).map((r) => {
101206
101471
  try {
@@ -102020,7 +102285,7 @@ function defaultReadEvents(stateDir) {
102020
102285
  }
102021
102286
  // permission-title.ts
102022
102287
  init_card_format();
102023
- import { basename as basename14 } from "node:path";
102288
+ import { basename as basename15 } from "node:path";
102024
102289
  init_redact();
102025
102290
  var COMMAND_TITLE_MAX2 = 48;
102026
102291
  var DESCRIPTION_LINE_MAX = 240;
@@ -102274,11 +102539,11 @@ function describeGrant(toolName, inputPreview, option) {
102274
102539
  return m ? `run ${m[1]} commands` : "run that command";
102275
102540
  }
102276
102541
  if (t === "Edit" || t === "MultiEdit" || t === "NotebookEdit")
102277
- return `edit ${basename14(arg)}`;
102542
+ return `edit ${basename15(arg)}`;
102278
102543
  if (t === "Write")
102279
- return `write ${basename14(arg)}`;
102544
+ return `write ${basename15(arg)}`;
102280
102545
  if (t === "Read")
102281
- return `read ${basename14(arg)}`;
102546
+ return `read ${basename15(arg)}`;
102282
102547
  return naturalAction2(toolName, inputPreview);
102283
102548
  }
102284
102549
  switch (rule) {
@@ -102317,12 +102582,12 @@ function fileBase2(input, rawPreview) {
102317
102582
  if (input) {
102318
102583
  const p = readString3(input, "file_path") ?? readString3(input, "notebook_path");
102319
102584
  if (p)
102320
- return basename14(p);
102585
+ return basename15(p);
102321
102586
  }
102322
102587
  if (rawPreview) {
102323
102588
  const p = extractFilePathFromRaw3(rawPreview);
102324
102589
  if (p)
102325
- return basename14(p);
102590
+ return basename15(p);
102326
102591
  }
102327
102592
  return null;
102328
102593
  }
@@ -102453,7 +102718,7 @@ function truncate7(text4, max) {
102453
102718
  }
102454
102719
 
102455
102720
  // permission-rule.ts
102456
- import { basename as basename15 } from "node:path";
102721
+ import { basename as basename16 } from "node:path";
102457
102722
  var FILE_TOOLS2 = new Set([
102458
102723
  "Edit",
102459
102724
  "Write",
@@ -102585,14 +102850,14 @@ function skillBasenameFromPath4(input) {
102585
102850
  if (!path3)
102586
102851
  return null;
102587
102852
  const trimmed = path3.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
102588
- return basename15(trimmed) || null;
102853
+ return basename16(trimmed) || null;
102589
102854
  }
102590
102855
  function isRulePersisted(resolvedAllow, ruleRule) {
102591
102856
  return resolvedAllow.includes(ruleRule);
102592
102857
  }
102593
102858
 
102594
102859
  // scoped-approval.ts
102595
- import { basename as basename16 } from "node:path";
102860
+ import { basename as basename17 } from "node:path";
102596
102861
  var SCOPED_APPROVAL_DEFAULT_TTL_MS = 30 * 60 * 1000;
102597
102862
  function scopedApprovalTtlMs(env = process.env) {
102598
102863
  const raw = env.SWITCHROOM_SCOPED_APPROVAL_TTL_MS;
@@ -102617,7 +102882,7 @@ function resolveTimeBox(toolName, inputPreview, choices) {
102617
102882
  const fileMatch = FILE_RULE.exec(rule);
102618
102883
  if (fileMatch) {
102619
102884
  const verb = fileMatch[1] === "Read" ? "reads of" : "edits to";
102620
- return { rule, breadth: `${verb} ${basename16(fileMatch[2])}` };
102885
+ return { rule, breadth: `${verb} ${basename17(fileMatch[2])}` };
102621
102886
  }
102622
102887
  const bashMatch = BASH_FAMILY_RULE.exec(rule);
102623
102888
  if (bashMatch) {
@@ -104169,10 +104434,10 @@ function startOutboxSweep(deps) {
104169
104434
  }
104170
104435
 
104171
104436
  // ../src/build-info.ts
104172
- var VERSION2 = "0.21.1";
104173
- var COMMIT_SHA = "6aef7b24";
104174
- var COMMIT_DATE = "2026-08-10T02:48:34Z";
104175
- var LATEST_PR = 4583;
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;
104176
104441
  var COMMITS_AHEAD_OF_TAG = 0;
104177
104442
 
104178
104443
  // gateway/boot-version.ts
@@ -106768,8 +107033,8 @@ function assertSendable(f) {
106768
107033
  }
106769
107034
  let real, stateReal;
106770
107035
  try {
106771
- real = realpathSync5(f);
106772
- stateReal = realpathSync5(STATE_DIR);
107036
+ real = realpathSync6(f);
107037
+ stateReal = realpathSync6(STATE_DIR);
106773
107038
  } catch {
106774
107039
  throw new Error(`refusing to send file \u2014 cannot resolve real path: ${f}`);
106775
107040
  }
@@ -107058,6 +107323,8 @@ if (isGatewayMain)
107058
107323
  if (isGatewayMain && !STATIC) {
107059
107324
  setInterval(() => runHistoryReaperNow("periodic"), REGISTRY_REAPER_INTERVAL_MS).unref();
107060
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) });
107061
107328
  function checkApprovals() {
107062
107329
  let files;
107063
107330
  try {
@@ -110628,7 +110895,7 @@ if (isGatewayMain)
110628
110895
  if (turnsDb != null && msg.activeFile != null) {
110629
110896
  const stampKey = currentTurn?.registryKey ?? null;
110630
110897
  if (stampKey != null && stampKey !== lastSessionStampedTurnKey) {
110631
- const sessionId = basename17(msg.activeFile).replace(/\.jsonl$/, "");
110898
+ const sessionId = basename18(msg.activeFile).replace(/\.jsonl$/, "");
110632
110899
  if (sessionId) {
110633
110900
  try {
110634
110901
  stampTurnSessionId(turnsDb, stampKey, sessionId);
@@ -113361,7 +113628,7 @@ function getMyAgentName() {
113361
113628
  const fromEnv = process.env.SWITCHROOM_AGENT_NAME;
113362
113629
  if (fromEnv && fromEnv.trim().length > 0)
113363
113630
  return fromEnv.trim();
113364
- return basename17(process.cwd());
113631
+ return basename18(process.cwd());
113365
113632
  }
113366
113633
  function isSelfTargetingCommand(name) {
113367
113634
  if (name === "all")