alvin-bot 4.4.5 โ†’ 4.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,87 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [4.4.7] โ€” 2026-04-09
6
+
7
+ ### ๐Ÿ” Security / Dependencies
8
+
9
+ **6 of 9 npm audit vulnerabilities fixed (non-breaking)** โ€” Ran `npm audit fix` to patch the transitive `@xmldom/xmldom` XML injection, `basic-ftp` CRLF command injection, and `brace-expansion` DoS vulnerabilities. Also upgraded the direct dependency `@anthropic-ai/claude-agent-sdk` from `0.2.92` to `0.2.97` (latest, non-breaking patch release with no changes to the `query()` API surface Alvin-Bot uses).
10
+
11
+ Remaining unaddressed (by design, require breaking upgrades or overrides):
12
+ - `@anthropic-ai/sdk` Memory Tool sandbox escape โ€” **not exploitable** in Alvin-Bot because the `Memory` tool is not listed in `allowedTools` (we only use `Read`, `Write`, `Edit`, `Bash`, `Glob`, `Grep`, `WebSearch`, `WebFetch`, `Task`).
13
+ - `electron` (17 advisories) โ€” waiting for a planned breaking upgrade to `electron@41.x`.
14
+
15
+ ### โœจ Stability Improvements
16
+
17
+ **Session memory hygiene (`src/services/session.ts`)** โ€” The in-memory `sessions` Map grew unbounded: every user that ever messaged the bot kept a full session object (including conversation history, cost breakdown, abort controller) forever. On a single-user bot like Ali's this is a non-issue; on any multi-user deployment it's a steady leak.
18
+
19
+ New behavior:
20
+ - **Conservative 7-day TTL**: a session is only eligible for cleanup after 7 full days of complete inactivity. Configurable via `ALVIN_SESSION_TTL_DAYS` env var.
21
+ - **Never touches active sessions**: the cleanup loop explicitly skips any session with `isProcessing === true`.
22
+ - **`lastActivity` touched on every `getSession()` call**: any interaction at all keeps the session alive indefinitely.
23
+ - **Orphaned `abortController` cleanup** before removal (defensive).
24
+ - Runs hourly; logs a message when it actually purges something.
25
+
26
+ This is memory hygiene only โ€” it cannot reduce Alvin-Bot's capabilities, permissions, or responsiveness. Active users see zero behavioral change.
27
+
28
+ **MAX_BUDGET_USD tracking (`src/services/session.ts:trackProviderUsage`)** โ€” The `MAX_BUDGET_USD` config was declared but never read anywhere. Now it's tracked as a **soft warning** (never a block):
29
+ - When a session's cumulative cost crosses 80% of the configured budget, a `โš ๏ธ Session budget 80% consumed` message is logged.
30
+ - When it crosses 100%, a `๐Ÿ’ธ Session budget exceeded โ€ฆ bot continues (no hard limit enforced)` message is logged.
31
+ - **The bot never blocks** โ€” the warnings exist purely as operator signals. `/new` resets the warning flags so subsequent sessions get fresh thresholds.
32
+ - `session.totalCost` is now correctly incremented (previously declared in the interface but never written to).
33
+
34
+ ### ๐Ÿ“ฆ Compatibility
35
+
36
+ No breaking changes. User-facing behavior is identical โ€” same commands, same permissions, same response patterns. The only visible change is new log messages for cleanup events and budget thresholds.
37
+
38
+ ```bash
39
+ npm update -g alvin-bot
40
+ ```
41
+
42
+ ## [4.4.6] โ€” 2026-04-09
43
+
44
+ ### ๐Ÿ› Bug Fixes
45
+
46
+ **`alvin-bot audit` now reads `.env` from `DATA_DIR`** โ€” Before this release, `audit` was a subprocess that never loaded the bot's config: it only inspected `process.env`, which for an ad-hoc CLI invocation is the shell environment, not the bot's actual runtime state. Result: `ALLOWED_USERS` and `WEB_PASSWORD` were always reported as "not set" even when the bot was correctly configured and running. `audit` now calls `dotenv.config({ path: ENV_FILE })` at the start of `runAudit()` so its output matches `alvin-bot doctor` and the actual engine.
47
+
48
+ **`alvin-bot doctor` no longer hangs indefinitely on missing `.env`** โ€” The CLI's `readline` interface was created eagerly at module load, which made `stdin` readable for the entire process lifetime. Commands like `doctor`, `audit`, `version` that have no interactive prompts would therefore never terminate โ€” even though the `doctor()` function correctly early-returned when `.env` was missing, `node` refused to exit because the event loop still saw stdin as a live resource. Readline is now lazy-created only when `ask()` is actually called. Measured improvement: **doctor with missing .env terminates in 82 ms** (previously: 20+ second hang, often requiring Ctrl+C).
49
+
50
+ **`validateProviderKey("claude-sdk", โ€ฆ)` no longer false-negatives on Agent SDK auth** โ€” The CLI's Claude check ran `claude auth status` and hard-failed on `loggedIn: false`. But the Claude Agent SDK has multiple auth paths that the CLI doesn't see: `ANTHROPIC_API_KEY` env var, Claude Code IDE sessions, and native-binary session cookies. Real-world example: a bot that was actively answering Telegram messages correctly was reported as "โŒ Claude CLI not authenticated" by `doctor`. The validation is now:
51
+ - `ANTHROPIC_API_KEY` set โ†’ `ok: true` (immediate pass, CLI irrelevant)
52
+ - `claude` binary present + `auth status: loggedIn: true` โ†’ `ok: true`
53
+ - `claude` binary present + `auth status: loggedIn: false` โ†’ `ok: true` with a **warning** (the Agent SDK may still work via session / env var; user is advised to run `claude auth login` only if the bot fails to respond)
54
+ - `claude` binary missing โ†’ `ok: false` (hard error with install hint)
55
+
56
+ `doctor` now renders the warning as โš ๏ธ instead of โŒ, making the output match actual behavior.
57
+
58
+ ### โœจ New Feature
59
+
60
+ **`alvin-bot setup --non-interactive` for CI, Docker, and scripted installs** โ€” The interactive setup wizard was the only way to write `~/.alvin-bot/.env`, which blocked automated provisioning. Now supports flag-driven, non-interactive setup:
61
+
62
+ ```bash
63
+ alvin-bot setup --non-interactive \
64
+ --bot-token=123456789:AAE... \
65
+ --allowed-users=12345,67890 \
66
+ --primary-provider=claude-sdk \
67
+ --fallback-providers=ollama \
68
+ --groq-key=gsk_... \
69
+ --google-key=AIza... \
70
+ --openai-key=sk-... \
71
+ --nvidia-key=nvapi-... \
72
+ --anthropic-key=sk-ant-... \
73
+ --openrouter-key=sk-or-... \
74
+ --web-password=... \
75
+ --platform=telegram \
76
+ --skip-validation # optional, skips the live Telegram getMe call
77
+ ```
78
+
79
+ - Refuses to overwrite an existing `.env` (exits 1 with a clear message).
80
+ - Writes with mode `0600`.
81
+ - Validates `--bot-token` format and `--allowed-users` numeric format before writing.
82
+ - Optionally pings Telegram `getMe` unless `--skip-validation` is passed.
83
+
84
+ `-y` and `--yes` work as aliases for `--non-interactive`.
85
+
5
86
  ## [4.4.5] โ€” 2026-04-09
6
87
 
7
88
  ### ๐Ÿ” Security / Information Disclosure
package/bin/cli.js CHANGED
@@ -29,8 +29,20 @@ const DATA_DIR = process.env.ALVIN_DATA_DIR || join(homedir(), ".alvin-bot");
29
29
  // Init i18n early
30
30
  initI18n();
31
31
 
32
- const rl = createInterface({ input: process.stdin, output: process.stdout });
33
- const ask = (q) => new Promise((r) => rl.question(q, r));
32
+ // Lazy-create the readline interface. If we create it eagerly, stdin becomes
33
+ // "active" and Node refuses to exit even when a command like `doctor` has
34
+ // finished synchronously. Before v4.4.6 this caused `alvin-bot doctor` to
35
+ // hang indefinitely when .env was missing โ€” early-return worked, but the
36
+ // process couldn't terminate. Creating rl only when `ask()` is actually
37
+ // called keeps non-interactive commands (audit/doctor/version/start/stop)
38
+ // terminating cleanly.
39
+ let rl = null;
40
+ const ensureRL = () => {
41
+ if (!rl) rl = createInterface({ input: process.stdin, output: process.stdout });
42
+ return rl;
43
+ };
44
+ const closeRL = () => { if (rl) { rl.close(); rl = null; } };
45
+ const ask = (q) => new Promise((r) => ensureRL().question(q, r));
34
46
 
35
47
  const LOGO = `
36
48
  โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
@@ -164,6 +176,17 @@ async function validateProviderKey(providerKey, apiKey) {
164
176
  }
165
177
 
166
178
  case "claude-sdk": {
179
+ // The Claude Agent SDK can authenticate through multiple paths that
180
+ // `claude auth status` does not see:
181
+ // 1. ANTHROPIC_API_KEY env var (bypasses CLI entirely)
182
+ // 2. An active Claude Code session (IDE-initiated, not via `auth login`)
183
+ // 3. Native binary session cookies
184
+ // Prior to v4.4.6 this check hard-failed on `loggedIn: false` even when
185
+ // the engine ran fine โ€” confusing users whose bot was actually working.
186
+ if (process.env.ANTHROPIC_API_KEY) {
187
+ return { ok: true, detail: "Claude SDK via ANTHROPIC_API_KEY env var" };
188
+ }
189
+
167
190
  // Find claude binary โ€” check PATH and common locations
168
191
  let claudeBin = null;
169
192
  try {
@@ -180,10 +203,13 @@ async function validateProviderKey(providerKey, apiKey) {
180
203
  }
181
204
  }
182
205
  if (!claudeBin) {
183
- return { ok: false, error: "Claude CLI not installed" };
206
+ return { ok: false, error: "Claude CLI not installed. Run: curl -fsSL https://claude.ai/install.sh | sh" };
184
207
  }
208
+
209
+ // Check `claude auth status`. On mismatch, we DON'T hard-fail:
210
+ // we return a warning so the Agent SDK still gets a chance to run
211
+ // (it has independent auth paths the CLI doesn't know about).
185
212
  try {
186
- // Use `auth status` instead of `-p "ping"` โ€” faster and doesn't require a full query
187
213
  const authJson = execSync(`${claudeBin} auth status`, {
188
214
  stdio: "pipe", timeout: 10000, encoding: "utf-8",
189
215
  });
@@ -191,7 +217,11 @@ async function validateProviderKey(providerKey, apiKey) {
191
217
  if (authData.loggedIn) {
192
218
  return { ok: true, detail: `Claude SDK authenticated (${authData.authMethod || "OK"})` };
193
219
  }
194
- return { ok: false, error: "Claude CLI not authenticated. Run: claude auth login" };
220
+ return {
221
+ ok: true,
222
+ warning: "Claude CLI reports not logged in, but the Agent SDK may still work via session/env-var. If the bot fails to respond, run: claude auth login",
223
+ detail: "Claude CLI present (not logged in via `auth status` โ€” Agent SDK may still work)",
224
+ };
195
225
  } catch (err) {
196
226
  const msg = err.stdout?.toString() || err.stderr?.toString() || err.message || "";
197
227
  // Try parsing JSON from stdout (auth status exits with code 1 when not logged in)
@@ -201,7 +231,11 @@ async function validateProviderKey(providerKey, apiKey) {
201
231
  return { ok: true, detail: `Claude SDK authenticated (${authData.authMethod || "OK"})` };
202
232
  }
203
233
  } catch {}
204
- return { ok: false, error: "Claude CLI not authenticated. Run: claude auth login" };
234
+ return {
235
+ ok: true,
236
+ warning: "Claude CLI `auth status` failed. Agent SDK may still work via session/env-var. If the bot fails to respond, run: claude auth login",
237
+ detail: "Claude CLI present (auth status check failed โ€” Agent SDK may still work)",
238
+ };
205
239
  }
206
240
  }
207
241
 
@@ -298,9 +332,148 @@ async function runPostSetupValidation(providerKey, apiKey, botToken, webPort) {
298
332
  return allGood;
299
333
  }
300
334
 
335
+ // โ”€โ”€ Setup: Argument Parsing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
336
+
337
+ /**
338
+ * Parse `alvin-bot setup --non-interactive` style CLI args.
339
+ * Returns a flat object with the values found (or null for unset fields).
340
+ *
341
+ * Supports both `--flag=value` and `--flag value` syntax.
342
+ */
343
+ function parseSetupArgs(argv) {
344
+ const args = {};
345
+ const flags = new Set(["--non-interactive", "-y", "--yes", "--skip-validation"]);
346
+ const valueFlags = [
347
+ "--bot-token", "--allowed-users", "--primary-provider",
348
+ "--groq-key", "--google-key", "--openai-key", "--nvidia-key",
349
+ "--openrouter-key", "--anthropic-key",
350
+ "--fallback-providers", "--web-password", "--platform",
351
+ ];
352
+ for (let i = 3; i < argv.length; i++) {
353
+ const a = argv[i];
354
+ if (flags.has(a)) {
355
+ args[a.replace(/^-+/, "")] = true;
356
+ continue;
357
+ }
358
+ for (const vf of valueFlags) {
359
+ if (a === vf) {
360
+ args[vf.replace(/^-+/, "")] = argv[++i];
361
+ break;
362
+ }
363
+ if (a.startsWith(vf + "=")) {
364
+ args[vf.replace(/^-+/, "")] = a.slice(vf.length + 1);
365
+ break;
366
+ }
367
+ }
368
+ }
369
+ return args;
370
+ }
371
+
372
+ /**
373
+ * Non-interactive setup path for CI/Docker/automation.
374
+ *
375
+ * Writes ~/.alvin-bot/.env directly from CLI args, with no prompts.
376
+ * The bot's own `ensureDataDirs()` + `seedDefaults()` handle the rest
377
+ * on the next `alvin-bot start`.
378
+ *
379
+ * Usage:
380
+ * alvin-bot setup --non-interactive \
381
+ * --bot-token=123:AAE... \
382
+ * --allowed-users=12345,67890 \
383
+ * --primary-provider=claude-sdk \
384
+ * --groq-key=gsk_... \
385
+ * --fallback-providers=groq,ollama
386
+ */
387
+ async function setupNonInteractive(args) {
388
+ console.log("๐Ÿค– Alvin Bot โ€” Non-interactive setup\n");
389
+
390
+ // Ensure DATA_DIR exists (will also be recreated on bot start, but we
391
+ // need it now to write the .env).
392
+ if (!existsSync(DATA_DIR)) {
393
+ mkdirSync(DATA_DIR, { recursive: true });
394
+ console.log(`โœ“ Created ${DATA_DIR}`);
395
+ }
396
+
397
+ const envFile = resolve(DATA_DIR, ".env");
398
+ if (existsSync(envFile)) {
399
+ console.log(`โš ๏ธ ${envFile} already exists โ€” refusing to overwrite.`);
400
+ console.log(` Delete it manually first, or edit it directly.`);
401
+ process.exit(1);
402
+ }
403
+
404
+ // Validate required fields
405
+ const token = args["bot-token"];
406
+ if (token && !/^\d+:[A-Za-z0-9_-]+$/.test(token)) {
407
+ console.log(`โŒ --bot-token format invalid. Expected: 123456789:ABCdef...`);
408
+ process.exit(1);
409
+ }
410
+ const users = args["allowed-users"];
411
+ if (users) {
412
+ const ids = users.split(",").map(s => s.trim());
413
+ const bad = ids.filter(id => !/^\d+$/.test(id));
414
+ if (bad.length > 0) {
415
+ console.log(`โŒ --allowed-users must be comma-separated numeric IDs. Got invalid: ${bad.join(", ")}`);
416
+ process.exit(1);
417
+ }
418
+ }
419
+
420
+ // Optional: validate token with Telegram API unless --skip-validation
421
+ if (token && !args["skip-validation"]) {
422
+ const tgResult = await validateTelegramToken(token);
423
+ if (tgResult.ok) {
424
+ console.log(`โœ“ Telegram: ${tgResult.botName}`);
425
+ } else {
426
+ console.log(`โš ๏ธ Telegram token validation failed: ${tgResult.error}`);
427
+ console.log(` Writing .env anyway. Pass --skip-validation to suppress this warning.`);
428
+ }
429
+ }
430
+
431
+ const primary = args["primary-provider"] || "groq";
432
+ const fallbacks = args["fallback-providers"] || "";
433
+ const platform = args["platform"] || "telegram";
434
+
435
+ const envLines = [
436
+ "# === Telegram ===",
437
+ `BOT_TOKEN=${token || ""}`,
438
+ `ALLOWED_USERS=${users || ""}`,
439
+ "",
440
+ "# === AI Provider ===",
441
+ `PRIMARY_PROVIDER=${primary}`,
442
+ `FALLBACK_PROVIDERS=${fallbacks}`,
443
+ "",
444
+ "# === API Keys ===",
445
+ ];
446
+ if (args["groq-key"]) envLines.push(`GROQ_API_KEY=${args["groq-key"]}`);
447
+ if (args["google-key"]) envLines.push(`GOOGLE_API_KEY=${args["google-key"]}`);
448
+ if (args["openai-key"]) envLines.push(`OPENAI_API_KEY=${args["openai-key"]}`);
449
+ if (args["nvidia-key"]) envLines.push(`NVIDIA_API_KEY=${args["nvidia-key"]}`);
450
+ if (args["openrouter-key"]) envLines.push(`OPENROUTER_API_KEY=${args["openrouter-key"]}`);
451
+ if (args["anthropic-key"]) envLines.push(`ANTHROPIC_API_KEY=${args["anthropic-key"]}`);
452
+ envLines.push("");
453
+ envLines.push("# === Agent ===");
454
+ envLines.push("WORKING_DIR=" + homedir());
455
+ envLines.push("MAX_BUDGET_USD=5.0");
456
+ envLines.push("WEB_PORT=3100");
457
+ if (args["web-password"]) envLines.push(`WEB_PASSWORD=${args["web-password"]}`);
458
+ envLines.push("");
459
+ envLines.push("# === Platforms ===");
460
+ envLines.push(`WHATSAPP_ENABLED=${platform === "whatsapp" ? "true" : "false"}`);
461
+
462
+ writeFileSync(envFile, envLines.join("\n") + "\n", { mode: 0o600 });
463
+ console.log(`โœ“ Wrote ${envFile} (mode 0600)`);
464
+ console.log(`\nโœ… Setup complete. Start the bot with: alvin-bot start\n`);
465
+ }
466
+
301
467
  // โ”€โ”€ Setup Wizard โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
302
468
 
303
469
  async function setup() {
470
+ // Non-interactive path for CI/automation/scripted installs.
471
+ const args = parseSetupArgs(process.argv);
472
+ if (args["non-interactive"] || args["yes"] || args["y"]) {
473
+ await setupNonInteractive(args);
474
+ return;
475
+ }
476
+
304
477
  console.log(LOGO);
305
478
 
306
479
  // โ”€โ”€ Prerequisites
@@ -318,7 +491,7 @@ async function setup() {
318
491
 
319
492
  if (!hasNode) {
320
493
  console.log(`\nโŒ ${t("setup.nodeRequired")}`);
321
- rl.close();
494
+ closeRL();
322
495
  return;
323
496
  }
324
497
 
@@ -337,7 +510,7 @@ async function setup() {
337
510
  console.log(` Get one from @BotFather on Telegram.\n`);
338
511
  const proceed = (await ask(` Continue anyway? (y/n): `)).trim().toLowerCase();
339
512
  if (proceed !== "y" && proceed !== "yes" && proceed !== "j" && proceed !== "ja") {
340
- rl.close();
513
+ closeRL();
341
514
  return;
342
515
  }
343
516
  }
@@ -381,7 +554,7 @@ async function setup() {
381
554
  console.log(` Send /start to @userinfobot on Telegram to get your numeric ID.\n`);
382
555
  const proceed = (await ask(` Continue anyway? (y/n): `)).trim().toLowerCase();
383
556
  if (proceed !== "y" && proceed !== "yes" && proceed !== "j" && proceed !== "ja") {
384
- rl.close();
557
+ closeRL();
385
558
  return;
386
559
  }
387
560
  }
@@ -759,7 +932,7 @@ async function setup() {
759
932
  console.log(`\n โŒ ${t("setup.buildFailed")}`);
760
933
  console.log(` The bot cannot start without a successful build.`);
761
934
  console.log(` Try running 'npm run build' manually to see the error.\n`);
762
- rl.close();
935
+ closeRL();
763
936
  return;
764
937
  }
765
938
  }
@@ -804,7 +977,7 @@ Bot commands:
804
977
  ${t("setup.haveFun")}
805
978
  `);
806
979
 
807
- rl.close();
980
+ closeRL();
808
981
  }
809
982
 
810
983
  // โ”€โ”€ Doctor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -866,7 +1039,10 @@ async function doctor() {
866
1039
 
867
1040
  console.log(` Validating ${primary}...`);
868
1041
  const result = await validateProviderKey(primary, key);
869
- if (result.ok) {
1042
+ if (result.ok && result.warning) {
1043
+ console.log(` โš ๏ธ ${primary} โ€” ${result.detail}`);
1044
+ console.log(` ${result.warning}`);
1045
+ } else if (result.ok) {
870
1046
  console.log(` โœ… ${primary} โ€” ${result.detail}`);
871
1047
  } else {
872
1048
  console.log(` โŒ ${primary} โ€” ${result.error}`);
package/dist/index.js CHANGED
@@ -60,6 +60,7 @@ import { loadPlugins, registerPluginCommands, unloadPlugins } from "./services/p
60
60
  import { initMCP, disconnectMCP, hasMCPConfig } from "./services/mcp.js";
61
61
  import { startWebServer } from "./web/server.js";
62
62
  import { startScheduler, stopScheduler, setNotifyCallback } from "./services/cron.js";
63
+ import { startSessionCleanup, stopSessionCleanup } from "./services/session.js";
63
64
  import { processQueue, cleanupQueue, setSenders, enqueue } from "./services/delivery-queue.js";
64
65
  import { discoverTools } from "./services/tool-discovery.js";
65
66
  import { startHeartbeat } from "./services/heartbeat.js";
@@ -214,6 +215,7 @@ const shutdown = async () => {
214
215
  console.log("Graceful shutdown initiated...");
215
216
  cancelAllSubAgents();
216
217
  stopScheduler();
218
+ stopSessionCleanup();
217
219
  if (queueInterval)
218
220
  clearInterval(queueInterval);
219
221
  if (queueCleanupInterval)
@@ -348,6 +350,9 @@ setNotifyCallback(async (target, text) => {
348
350
  enqueue(target.platform, String(target.chatId), text);
349
351
  });
350
352
  startScheduler();
353
+ // Session memory hygiene: purge sessions idle > 7 days (configurable via
354
+ // ALVIN_SESSION_TTL_DAYS). Never touches active sessions โ€” see session.ts.
355
+ startSessionCleanup();
351
356
  // Wire delivery queue senders
352
357
  setSenders({
353
358
  telegram: async (chatId, content) => {
@@ -1,11 +1,19 @@
1
1
  import fs from "fs";
2
2
  import { execSync } from "child_process";
3
- import { resolve } from "path";
4
- import { DATA_DIR } from "../paths.js";
3
+ import dotenv from "dotenv";
4
+ import { DATA_DIR, ENV_FILE } from "../paths.js";
5
5
  export function runAudit() {
6
6
  const checks = [];
7
+ // `alvin-bot audit` runs in its own process (outside the main bot) and
8
+ // does NOT go through src/config.ts, so process.env is empty by default.
9
+ // We must load the .env ourselves or ALLOWED_USERS/WEB_PASSWORD checks
10
+ // will always report as "not set" โ€” which silently contradicts the bot's
11
+ // actual runtime state (a bug up to v4.4.5).
12
+ if (fs.existsSync(ENV_FILE)) {
13
+ dotenv.config({ path: ENV_FILE });
14
+ }
7
15
  // 1. .env file permissions
8
- const envFile = resolve(DATA_DIR, ".env");
16
+ const envFile = ENV_FILE;
9
17
  if (fs.existsSync(envFile)) {
10
18
  const stat = fs.statSync(envFile);
11
19
  const mode = (stat.mode & 0o777).toString(8);
@@ -39,6 +39,11 @@ export function getSession(key) {
39
39
  };
40
40
  sessions.set(k, session);
41
41
  }
42
+ else {
43
+ // Touch lastActivity on every access so the cleanup interval
44
+ // never kills a session that's still being interacted with.
45
+ session.lastActivity = Date.now();
46
+ }
42
47
  return session;
43
48
  }
44
49
  export function resetSession(key) {
@@ -53,16 +58,85 @@ export function resetSession(key) {
53
58
  session.totalOutputTokens = 0;
54
59
  session.history = [];
55
60
  session.startedAt = Date.now();
61
+ // Reset budget warning flags so the user gets fresh warnings in the new session.
62
+ session._budgetWarned80 = false;
63
+ session._budgetWarned100 = false;
56
64
  }
57
65
  /** Track cost, query count, and tokens for a provider. */
58
66
  export function trackProviderUsage(key, providerKey, cost, inputTokens, outputTokens) {
59
67
  const session = getSession(key);
60
68
  session.costByProvider[providerKey] = (session.costByProvider[providerKey] || 0) + cost;
61
69
  session.queriesByProvider[providerKey] = (session.queriesByProvider[providerKey] || 0) + 1;
70
+ session.totalCost += cost;
62
71
  if (inputTokens)
63
72
  session.totalInputTokens += inputTokens;
64
73
  if (outputTokens)
65
74
  session.totalOutputTokens += outputTokens;
75
+ // Soft budget warnings โ€” these NEVER block the bot. They exist purely
76
+ // as log signals so the operator (Ali) can notice unusually expensive
77
+ // sessions. Each threshold fires at most once per session (reset on /new).
78
+ const budget = config.maxBudgetUsd;
79
+ if (budget > 0) {
80
+ const pct = (session.totalCost / budget) * 100;
81
+ if (pct >= 100 && !session._budgetWarned100) {
82
+ console.warn(`๐Ÿ’ธ Session budget exceeded: $${session.totalCost.toFixed(4)} / $${budget.toFixed(2)} (${pct.toFixed(0)}%) โ€” bot continues (no hard limit enforced)`);
83
+ session._budgetWarned100 = true;
84
+ }
85
+ else if (pct >= 80 && !session._budgetWarned80) {
86
+ console.warn(`โš ๏ธ Session budget 80% consumed: $${session.totalCost.toFixed(4)} / $${budget.toFixed(2)}`);
87
+ session._budgetWarned80 = true;
88
+ }
89
+ }
90
+ }
91
+ // โ”€โ”€ Session Cleanup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
92
+ //
93
+ // Memory hygiene for long-running deployments. The sessions Map would
94
+ // otherwise grow unbounded as new users arrive. The cleanup is deliberately
95
+ // *conservative*:
96
+ // โ€ข Default TTL: 7 days of complete inactivity (not 24h)
97
+ // โ€ข Never touches sessions where isProcessing === true
98
+ // โ€ข Touches lastActivity on every getSession() call, so any interaction
99
+ // in the last 7 days keeps the session alive indefinitely
100
+ // โ€ข Aborts orphaned abort controllers defensively before removal
101
+ //
102
+ // Override via ALVIN_SESSION_TTL_DAYS env var if you want different behavior.
103
+ const SESSION_TTL_DAYS = Number(process.env.ALVIN_SESSION_TTL_DAYS) || 7;
104
+ const SESSION_INACTIVE_TTL_MS = SESSION_TTL_DAYS * 24 * 60 * 60 * 1000;
105
+ const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // check hourly
106
+ let cleanupTimer = null;
107
+ /** Start the periodic session cleanup. Safe to call multiple times. */
108
+ export function startSessionCleanup() {
109
+ if (cleanupTimer)
110
+ return;
111
+ cleanupTimer = setInterval(() => {
112
+ const now = Date.now();
113
+ let purged = 0;
114
+ for (const [key, s] of sessions) {
115
+ // NEVER kill a session that's actively processing a query.
116
+ if (s.isProcessing)
117
+ continue;
118
+ if (now - s.lastActivity > SESSION_INACTIVE_TTL_MS) {
119
+ if (s.abortController) {
120
+ try {
121
+ s.abortController.abort();
122
+ }
123
+ catch { /* ignore */ }
124
+ }
125
+ sessions.delete(key);
126
+ purged++;
127
+ }
128
+ }
129
+ if (purged > 0) {
130
+ console.log(`๐Ÿงน Session cleanup: purged ${purged} inactive session(s) (TTL: ${SESSION_TTL_DAYS} days)`);
131
+ }
132
+ }, CLEANUP_INTERVAL_MS);
133
+ }
134
+ /** Stop the cleanup timer (for graceful shutdown). */
135
+ export function stopSessionCleanup() {
136
+ if (cleanupTimer) {
137
+ clearInterval(cleanupTimer);
138
+ cleanupTimer = null;
139
+ }
66
140
  }
67
141
  /** Add a message to conversation history (for non-SDK providers). */
68
142
  export function addToHistory(key, message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alvin-bot",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Alvin Bot โ€” Your personal AI agent on Telegram, WhatsApp, Discord, Signal, and Web.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -159,7 +159,7 @@
159
159
  "url": "git+https://github.com/alvbln/Alvin-Bot.git"
160
160
  },
161
161
  "dependencies": {
162
- "@anthropic-ai/claude-agent-sdk": "^0.2.92",
162
+ "@anthropic-ai/claude-agent-sdk": "^0.2.97",
163
163
  "@hapi/boom": "^10.0.1",
164
164
  "@slack/bolt": "^4.6.0",
165
165
  "@types/node": "^22.0.0",