switchroom 0.21.3 → 0.21.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -136,6 +136,7 @@ if [ -n "$TELEGRAM_STATE" ] && [ -d "$TELEGRAM_STATE" ]; then
136
136
  # under `2>/dev/null`, so the breadcrumb is a debug/manual-run diagnostic.
137
137
  TELEGRAM_ROWS=$(python3 - "$HISTORY_DB" "$MAX_MESSAGES" "$TARGET_CHAT_ID" "$TARGET_THREAD_ID" <<'PYEOF'
138
138
  import sys, sqlite3, datetime
139
+ from urllib.parse import quote
139
140
 
140
141
  db_path = sys.argv[1]
141
142
  limit = int(sys.argv[2])
@@ -143,7 +144,28 @@ target_chat = sys.argv[3] if len(sys.argv) > 3 else ""
143
144
  target_thread = sys.argv[4] if len(sys.argv) > 4 else ""
144
145
 
145
146
  try:
146
- conn = sqlite3.connect(db_path)
147
+ # READ-ONLY, ALWAYS. `sqlite3.connect(path)` defaults to READ-WRITE, which
148
+ # makes this briefing assembler a writer on the gateway's live WAL DB. A
149
+ # read-write connection that closes as the LAST connection runs SQLite's
150
+ # checkpoint-and-delete path and UNLINKS `history.db-wal` / `-shm` — and
151
+ # this script runs at agent boot, precisely when the gateway is down or
152
+ # starting and this process IS the last connection. Any handle still
153
+ # mapped to those inodes then writes into deleted files: every INSERT
154
+ # reports success and every row is gone at the next restart (the
155
+ # `/proc/<pid>/fd/N -> history.db-wal (deleted)` signature that #4595
156
+ # sweeps for after the fact).
157
+ #
158
+ # `mode=ro` removes that primitive entirely: a read-only connection cannot
159
+ # checkpoint and cannot unlink a sidecar. It still reads a WAL database
160
+ # correctly in every boot state we care about — sidecars present, `-shm`
161
+ # absent, and `-shm` absent with an unwritable directory (SQLite falls back
162
+ # to a heap wal-index rather than failing). Every statement below is a
163
+ # SELECT or a PRAGMA table_info, so nothing here needs write access.
164
+ #
165
+ # The path is percent-encoded: a `?` or `#` in the state dir would
166
+ # otherwise be parsed as the URI's query/fragment delimiter and silently
167
+ # truncate the filename.
168
+ conn = sqlite3.connect("file:" + quote(db_path) + "?mode=ro", uri=True)
147
169
  conn.row_factory = sqlite3.Row
148
170
  cur = conn.cursor()
149
171
 
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.21.3", COMMIT_SHA = "8a74c2f6";
2123
+ var VERSION = "0.21.5", COMMIT_SHA = "a9aceb2f";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -63125,6 +63125,17 @@ function rollbackHint(installDir, previousVersion) {
63125
63125
  function alreadySelfUpdated(env2) {
63126
63126
  return env2[SELF_UPDATE_ENV_SENTINEL] === "1";
63127
63127
  }
63128
+ function describeBinaryProbeFailure(opts) {
63129
+ const { probe: probe2, path: path7, subject } = opts;
63130
+ switch (probe2.kind) {
63131
+ case "not-executable":
63132
+ return `could not EXECUTE ${subject} at ${path7} (${probe2.detail}). This is a ` + `property of WHERE it was staged, not of the artifact \u2014 the usual causes ` + `are a staging directory mounted \`noexec\`, a lost execute bit, or an ` + `architecture this kernel cannot run. The download's sha256 already ` + `matched the release's checksums file, so re-downloading will not help.`;
63133
+ case "ran-but-failed":
63134
+ return `${subject} at ${path7} ran but exited non-zero for \`--version\` ` + `(${probe2.detail}) \u2014 the artifact itself is faulty.`;
63135
+ case "no-version":
63136
+ return `${subject} at ${path7} ran and exited 0 but printed no parseable version ` + `(${probe2.detail}) \u2014 the artifact itself is faulty.`;
63137
+ }
63138
+ }
63128
63139
  async function fetchLatestReleaseTag(io) {
63129
63140
  try {
63130
63141
  return parseLatestReleaseTag(await io.httpGetText(GITHUB_LATEST_RELEASE_URL));
@@ -63278,10 +63289,14 @@ async function performSelfUpdate(opts) {
63278
63289
  throw new Error(`self-update: SHA256 mismatch for ${assetName} (expected ${expected}, got ${actual}) \u2014 ` + `refusing to install. The installed CLI is unchanged.`);
63279
63290
  }
63280
63291
  io.chmodExec(tmp);
63281
- const proved = io.probeBinaryVersion(tmp);
63282
- if (!proved) {
63292
+ const proved = io.probeBinary(tmp);
63293
+ if (!proved.ok) {
63283
63294
  io.remove(tmp);
63284
- throw new Error(`self-update: the downloaded ${plan.to} binary did not run cleanly on this host ` + `(\`switchroom version\` failed) \u2014 refusing to install it. The installed CLI is unchanged.`);
63295
+ throw new Error(`self-update: ${describeBinaryProbeFailure({
63296
+ probe: proved,
63297
+ path: tmp,
63298
+ subject: `the downloaded ${plan.to} binary`
63299
+ })} Refusing to install it. The installed CLI is unchanged.`);
63285
63300
  }
63286
63301
  const payload = await installAssetPayload({
63287
63302
  tag: plan.to,
@@ -64920,6 +64935,7 @@ async function defaultLoadDecisions() {
64920
64935
  "exec",
64921
64936
  "switchroom-vault-broker",
64922
64937
  "sqlite3",
64938
+ "-readonly",
64923
64939
  "-separator",
64924
64940
  SEP2,
64925
64941
  GRANTS_DB_CONTAINER_PATH,
@@ -84377,7 +84393,8 @@ function detectGatewayFindings(agent, logText, logName = `logs/${agent}/gateway-
84377
84393
  const gw_hits = {
84378
84394
  "duplicate-delivery-represent": 0,
84379
84395
  "represent-escalation": 0,
84380
- "reply-delivery-failure": 0
84396
+ "reply-delivery-failure": 0,
84397
+ "orphaned-db-handle": 0
84381
84398
  };
84382
84399
  const lines = logText.split(`
84383
84400
  `);
@@ -84435,7 +84452,8 @@ var init_detect = __esm(() => {
84435
84452
  GATEWAY_SIGNATURES = {
84436
84453
  "duplicate-delivery-represent": /represent duplicate-send/,
84437
84454
  "represent-escalation": /obligation escalation/,
84438
- "reply-delivery-failure": /tg-post method=sendRichMessage[^\n]*status=err(?![a-z])/
84455
+ "reply-delivery-failure": /tg-post method=sendRichMessage[^\n]*status=err(?![a-z])/,
84456
+ "orphaned-db-handle": /orphaned-db-sweep DETECTED \d+ deleted-inode DB handle/
84439
84457
  };
84440
84458
  });
84441
84459
 
@@ -84499,6 +84517,12 @@ var init_mapping = __esm(() => {
84499
84517
  job_spec: "talk-to-agents-from-anywhere",
84500
84518
  signature: "reply-delivery-failure:sendRichMessage-err"
84501
84519
  },
84520
+ "orphaned-db-handle": {
84521
+ failure_mode: "success-theater",
84522
+ severity: 3,
84523
+ job_spec: "survive-reboots-and-real-life",
84524
+ signature: "orphaned-db-handle:deleted-inode-writes"
84525
+ },
84502
84526
  "hang-long-stalled": {
84503
84527
  failure_mode: "partial",
84504
84528
  severity: 2,
@@ -86288,6 +86312,7 @@ function readLastMessages(historyDbPath) {
86288
86312
  } catch {}
86289
86313
  try {
86290
86314
  const out = execFileSync11("sqlite3", [
86315
+ "-readonly",
86291
86316
  historyDbPath,
86292
86317
  "SELECT role, MAX(ts) FROM messages GROUP BY role;"
86293
86318
  ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
@@ -92344,7 +92369,7 @@ function scrubSqlite(path2, values, dryRun) {
92344
92369
  for (const { value } of values) {
92345
92370
  const esc = value.replace(/'/g, "''");
92346
92371
  try {
92347
- const out = execFileSync17("sqlite3", [path2, `SELECT COUNT(*) FROM messages WHERE text LIKE '%${esc}%'`], { encoding: "utf8", timeout: 1e4 });
92372
+ const out = execFileSync17("sqlite3", ["-readonly", path2, `SELECT COUNT(*) FROM messages WHERE text LIKE '%${esc}%'`], { encoding: "utf8", timeout: 1e4 });
92348
92373
  const n = parseInt(out.trim(), 10);
92349
92374
  if (!Number.isNaN(n))
92350
92375
  matches += n;
@@ -106609,7 +106634,7 @@ function readTopicsFromHistory(dbPath, chatId, limit) {
106609
106634
  fail2("`switchroom telegram topics` requires the Bun runtime (history DB uses bun:sqlite). " + "Run via `bun run dev` or via the installed `switchroom` CLI (bun-bundled).");
106610
106635
  }
106611
106636
  const { Database: Database2 } = metaRequire("bun:sqlite");
106612
- const db = new Database2(dbPath, { create: false });
106637
+ const db = new Database2(dbPath, { create: false, readonly: true });
106613
106638
  try {
106614
106639
  const aggregateRows = db.prepare(`
106615
106640
  SELECT
@@ -115306,6 +115331,11 @@ import { Readable } from "node:stream";
115306
115331
  import { pipeline } from "node:stream/promises";
115307
115332
  import { spawnSync as spawnSync18 } from "node:child_process";
115308
115333
  var HTTP_TIMEOUT_MS = 60000;
115334
+ function firstLine2(s) {
115335
+ const line = `${s ?? ""}`.split(`
115336
+ `).map((l) => l.trim()).find((l) => l.length > 0) ?? "";
115337
+ return line.length > 200 ? `${line.slice(0, 197)}...` : line;
115338
+ }
115309
115339
  function defaultSelfUpdateIO() {
115310
115340
  return {
115311
115341
  async httpGetText(url) {
@@ -115345,17 +115375,40 @@ function defaultSelfUpdateIO() {
115345
115375
  }
115346
115376
  return hash2.digest("hex");
115347
115377
  },
115348
- probeBinaryVersion(path7) {
115349
- const r = spawnSync18(path7, ["version"], {
115378
+ probeBinary(path7) {
115379
+ const r = spawnSync18(path7, ["--version"], {
115350
115380
  encoding: "utf-8",
115351
115381
  timeout: 30000,
115352
115382
  env: { ...process.env, SWITCHROOM_SELF_UPDATED: "" }
115353
115383
  });
115354
- if (r.status !== 0)
115355
- return null;
115384
+ if (r.error) {
115385
+ const code = r.error.code;
115386
+ return {
115387
+ ok: false,
115388
+ kind: "not-executable",
115389
+ detail: code ? `${code}: ${r.error.message}` : r.error.message
115390
+ };
115391
+ }
115392
+ if (r.signal) {
115393
+ return { ok: false, kind: "not-executable", detail: `killed by signal ${r.signal}` };
115394
+ }
115395
+ if (r.status !== 0) {
115396
+ return {
115397
+ ok: false,
115398
+ kind: "ran-but-failed",
115399
+ detail: `exit ${r.status}${firstLine2(r.stderr) ? `: ${firstLine2(r.stderr)}` : ""}`
115400
+ };
115401
+ }
115356
115402
  const out = `${r.stdout ?? ""}`.trim();
115357
115403
  const m = out.match(/\d+\.\d+\.\d+/);
115358
- return m ? m[0] : null;
115404
+ if (!m) {
115405
+ return {
115406
+ ok: false,
115407
+ kind: "no-version",
115408
+ detail: out ? `printed ${JSON.stringify(firstLine2(out))}` : "printed nothing"
115409
+ };
115410
+ }
115411
+ return { ok: true, version: m[0] };
115359
115412
  },
115360
115413
  mkdirp(dir) {
115361
115414
  mkdirSync49(dir, { recursive: true });
@@ -116807,11 +116860,21 @@ async function runHostCliUpgrade(opts, io = defaultIo(), log = () => {}) {
116807
116860
  } catch (err) {
116808
116861
  return { ok: false, error: err.message };
116809
116862
  }
116810
- const proven = io.selfUpdate.probeBinaryVersion(binary);
116811
- if (!proven || `v${proven.replace(/^v/, "")}` !== pin) {
116863
+ const proven = io.selfUpdate.probeBinary(binary);
116864
+ if (!proven.ok) {
116865
+ return {
116866
+ ok: false,
116867
+ error: `swapped ${binary} but ${describeBinaryProbeFailure({
116868
+ probe: proven,
116869
+ path: binary,
116870
+ subject: "the installed binary"
116871
+ })} ${result.message}`
116872
+ };
116873
+ }
116874
+ if (`v${proven.version.replace(/^v/, "")}` !== pin) {
116812
116875
  return {
116813
116876
  ok: false,
116814
- error: `swapped ${binary} but it reports ${proven ?? "<nothing>"}, not ${pin} \u2014 ` + `the install did not land. ${result.message}`
116877
+ error: `swapped ${binary} but it reports ${proven.version}, not ${pin} \u2014 ` + `the install did not land. ${result.message}`
116815
116878
  };
116816
116879
  }
116817
116880
  return { ok: true, version: pin, binaryPath: binary };
@@ -21565,7 +21565,7 @@ function allocateAgentUid(name) {
21565
21565
  }
21566
21566
 
21567
21567
  // src/build-info.ts
21568
- var VERSION = "0.21.3";
21568
+ var VERSION = "0.21.5";
21569
21569
 
21570
21570
  // src/setup/hindsight-recall-passthrough.ts
21571
21571
  var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.21.3",
4
+ "version": "0.21.5",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -28,7 +28,7 @@
28
28
  "test:vitest": "vitest run",
29
29
  "test:bun": "bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/hindsight-bank-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-scope-persist.test.ts src/vault/broker/server-tokenless-scope.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/self-approval-bypass.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/boot-briefing-builder.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-buzz-dedup.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/progress-fallback-cap.test.ts telegram-plugin/tests/progress-cap.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/worker-origin-gap-dispatch.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
30
30
  "test:watch": "vitest",
31
- "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-bench-baseline-anonymised.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-hindsight-bank-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-changelog-entry.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts && bun scripts/check-hostd-template-guard.ts",
31
+ "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-bench-baseline-anonymised.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-hindsight-bank-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-ctx-send-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-foreign-db-readonly.mjs && node scripts/check-changelog-entry.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts && bun scripts/check-hostd-template-guard.ts",
32
32
  "lint:tsc": "tsc --noEmit",
33
33
  "lint:hindsight-write-redaction": "node scripts/check-hindsight-write-redaction.mjs",
34
34
  "lint:secret-pattern-parity": "bun scripts/check-secret-pattern-parity.ts",
@@ -46,10 +46,12 @@
46
46
  "lint:gateway-line-ratchet": "node scripts/check-gateway-line-ratchet.mjs",
47
47
  "lint:retry-flood-hooks": "node scripts/check-retry-flood-hooks.mjs",
48
48
  "lint:callback-ctx-wrapping": "node scripts/check-callback-ctx-wrapping.mjs",
49
+ "lint:ctx-send-wrapping": "node scripts/check-ctx-send-wrapping.mjs",
49
50
  "lint:status-pin-single-path": "node scripts/check-status-pin-single-path.mjs",
50
51
  "lint:mcp-instructions-budget": "node scripts/check-mcp-instructions-budget.mjs",
51
52
  "lint:litellm-config-guard": "node scripts/check-litellm-config-guard.mjs",
52
53
  "lint:release-asset-contract": "node scripts/check-release-asset-names.mjs",
54
+ "lint:foreign-db-readonly": "node scripts/check-foreign-db-readonly.mjs",
53
55
  "lint:changelog-entry": "node scripts/check-changelog-entry.mjs",
54
56
  "changelog:generate": "node scripts/gen-changelog-entry.mjs",
55
57
  "lint:agent-attribution-trailers": "node scripts/check-agent-attribution-trailers.mjs",