alvin-bot 4.16.1 โ 4.17.0
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 +29 -0
- package/dist/handlers/commands.js +31 -0
- package/dist/index.js +7 -1
- package/dist/platforms/whatsapp.js +13 -0
- package/dist/services/cdp-bootstrap.js +6 -0
- package/dist/services/disk-cleanup.js +203 -0
- package/dist/services/heartbeat.js +4 -0
- package/dist/services/updater.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [4.17.0] โ 2026-04-20
|
|
6
|
+
|
|
7
|
+
### ๐ก๏ธ Hardening: long-running stability audit + leak fixes
|
|
8
|
+
|
|
9
|
+
Ran a full audit of leak/stability hazards for 24/7 operation. Fixed the critical findings and added a disk-cleanup service so the bot stays lean over months of uptime.
|
|
10
|
+
|
|
11
|
+
**Fixes:**
|
|
12
|
+
- **WhatsApp event-listener leak on reconnect** (`src/platforms/whatsapp.ts`): Before every new socket, the previous socket's listeners are now removed and the old socket is ended. Without this, every reconnect stacked new listeners on top of old ones โ causing memory growth and duplicate message processing after long sessions.
|
|
13
|
+
- **CDP file-descriptor leak** (`src/services/cdp-bootstrap.ts`): The log-file fd passed to the detached Chromium spawn is now closed in the parent after the child inherits it. Previously leaked one fd per browser bootstrap.
|
|
14
|
+
- **Heartbeat + auto-update timers now `.unref()`'d** and explicitly stopped in the shutdown handler. Prevents timers from keeping the process alive during graceful exit.
|
|
15
|
+
|
|
16
|
+
### ๐งน Feature: disk-cleanup service
|
|
17
|
+
|
|
18
|
+
New service (`src/services/disk-cleanup.ts`) that runs automatically once a day. Deletes transient files that grow without bound on long-running installs:
|
|
19
|
+
- Bot log rotation (>100 MB by default)
|
|
20
|
+
- Browser screenshots (>30 days)
|
|
21
|
+
- Subagent output streams (>30 days)
|
|
22
|
+
- `/tmp/alvin-bot/` media (>7 days)
|
|
23
|
+
- WhatsApp media cache (>30 days)
|
|
24
|
+
- CDP log file
|
|
25
|
+
|
|
26
|
+
**NEVER touched:** memory, assets, workspaces, cron-jobs, .env, session-store, delivery-queue. Memory is protected.
|
|
27
|
+
|
|
28
|
+
**Configuration via env:** `CLEANUP_LOG_MAX_MB`, `CLEANUP_SCREENSHOTS_DAYS`, `CLEANUP_SUBAGENTS_DAYS`, `CLEANUP_TMP_DAYS`, `CLEANUP_WA_MEDIA_DAYS`. Set any to `0` to disable that category.
|
|
29
|
+
|
|
30
|
+
**Telegram command:**
|
|
31
|
+
- `/cleanup` โ show current policy + protected paths
|
|
32
|
+
- `/cleanup run` โ trigger manual pass, get stats back
|
|
33
|
+
|
|
5
34
|
## [4.16.1] โ 2026-04-20
|
|
6
35
|
|
|
7
36
|
### ๐ Feature: /update shows release highlights
|
|
@@ -28,6 +28,7 @@ import { getWebPort } from "../web/server.js";
|
|
|
28
28
|
import { getUsageSummary, getAllRateLimits, formatTokens } from "../services/usage-tracker.js";
|
|
29
29
|
import { runUpdate, getAutoUpdate, setAutoUpdate, startAutoUpdateLoop } from "../services/updater.js";
|
|
30
30
|
import { getReleaseHighlights } from "../services/release-highlights.js";
|
|
31
|
+
import { runCleanup, getCleanupPolicy } from "../services/disk-cleanup.js";
|
|
31
32
|
import { getHealthStatus, isFailedOver } from "../services/heartbeat.js";
|
|
32
33
|
import { t, LOCALE_NAMES, LOCALE_FLAGS } from "../i18n.js";
|
|
33
34
|
// Kick off auto-update loop on module load if the persistent flag is set.
|
|
@@ -1919,6 +1920,36 @@ export function registerCommands(bot) {
|
|
|
1919
1920
|
await ctx.reply(`${t("bot.autoupdate.statusLabel", lang)} *${status ? "ON" : "OFF"}*\n\n${t("bot.autoupdate.commandsLabel", lang)}\n\`/autoupdate on\`\n\`/autoupdate off\``, { parse_mode: "Markdown" });
|
|
1920
1921
|
}
|
|
1921
1922
|
});
|
|
1923
|
+
// /cleanup โ trigger disk cleanup manually, or show current policy.
|
|
1924
|
+
// /cleanup โ show policy
|
|
1925
|
+
// /cleanup run โ run a cleanup pass and report what was deleted
|
|
1926
|
+
bot.command("cleanup", async (ctx) => {
|
|
1927
|
+
const arg = (ctx.match || "").trim().toLowerCase();
|
|
1928
|
+
if (arg === "run" || arg === "now") {
|
|
1929
|
+
await ctx.reply("๐งน Running disk cleanup...");
|
|
1930
|
+
const r = await runCleanup();
|
|
1931
|
+
const bytes = r.bytesReclaimed;
|
|
1932
|
+
const human = bytes < 1024 * 1024
|
|
1933
|
+
? `${(bytes / 1024).toFixed(1)} KB`
|
|
1934
|
+
: bytes < 1024 * 1024 * 1024
|
|
1935
|
+
? `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
1936
|
+
: `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
1937
|
+
const errLine = r.errors.length > 0 ? `\nโ ๏ธ ${r.errors.length} error(s)` : "";
|
|
1938
|
+
await ctx.reply(`โ
Cleanup done\nโข Files deleted: ${r.filesDeleted}\nโข Logs rotated: ${r.logsRotated}\nโข Reclaimed: ${human}${errLine}`);
|
|
1939
|
+
}
|
|
1940
|
+
else {
|
|
1941
|
+
const p = getCleanupPolicy();
|
|
1942
|
+
await ctx.reply(`๐งน *Cleanup policy*\n` +
|
|
1943
|
+
`โข Log rotation: >${p.logMaxSizeMb} MB\n` +
|
|
1944
|
+
`โข Screenshots: >${p.screenshotsMaxAgeDays} days\n` +
|
|
1945
|
+
`โข Subagent outputs: >${p.subagentsMaxAgeDays} days\n` +
|
|
1946
|
+
`โข /tmp/alvin-bot: >${p.tmpMaxAgeDays} days\n` +
|
|
1947
|
+
`โข WhatsApp media: >${p.waMediaMaxAgeDays} days\n\n` +
|
|
1948
|
+
`Memory, assets, workspaces, cron jobs are NEVER touched.\n\n` +
|
|
1949
|
+
`Configure via env: \`CLEANUP_LOG_MAX_MB\`, \`CLEANUP_SCREENSHOTS_DAYS\`, \`CLEANUP_SUBAGENTS_DAYS\`, \`CLEANUP_TMP_DAYS\`, \`CLEANUP_WA_MEDIA_DAYS\`\n\n` +
|
|
1950
|
+
`Run manually: \`/cleanup run\``, { parse_mode: "Markdown" });
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1922
1953
|
// โโ /sub-agents โ manage background subagents (cron jobs + manual spawns) โโ
|
|
1923
1954
|
//
|
|
1924
1955
|
// /sub-agents โ show current config + running agents
|
package/dist/index.js
CHANGED
|
@@ -155,7 +155,9 @@ import { startSessionCleanup, stopSessionCleanup, attachPersistHook } from "./se
|
|
|
155
155
|
import { loadPersistedSessions, flushSessions, schedulePersist, } from "./services/session-persistence.js";
|
|
156
156
|
import { processQueue, cleanupQueue, setSenders, enqueue } from "./services/delivery-queue.js";
|
|
157
157
|
import { discoverTools } from "./services/tool-discovery.js";
|
|
158
|
-
import { startHeartbeat } from "./services/heartbeat.js";
|
|
158
|
+
import { startHeartbeat, stopHeartbeat } from "./services/heartbeat.js";
|
|
159
|
+
import { stopAutoUpdateLoop } from "./services/updater.js";
|
|
160
|
+
import { startCleanupLoop, stopCleanupLoop } from "./services/disk-cleanup.js";
|
|
159
161
|
import { initEmbeddings } from "./services/embeddings.js";
|
|
160
162
|
import { loadSkills } from "./services/skills.js";
|
|
161
163
|
import { loadHooks } from "./services/hooks.js";
|
|
@@ -335,6 +337,9 @@ const shutdown = async () => {
|
|
|
335
337
|
stopAsyncAgentWatcher();
|
|
336
338
|
stopSessionCleanup();
|
|
337
339
|
stopWorkspaceWatcher();
|
|
340
|
+
stopHeartbeat();
|
|
341
|
+
stopAutoUpdateLoop();
|
|
342
|
+
stopCleanupLoop();
|
|
338
343
|
// v4.11.0 โ Final immediate flush of in-memory sessions to disk before exit.
|
|
339
344
|
// The debounced timer might be pending; flushSessions() cancels it and writes
|
|
340
345
|
// synchronously so the next boot can rehydrate the latest state.
|
|
@@ -612,5 +617,6 @@ else {
|
|
|
612
617
|
// Start heartbeat monitor even without Telegram
|
|
613
618
|
startHeartbeat();
|
|
614
619
|
startWatchdog();
|
|
620
|
+
startCleanupLoop();
|
|
615
621
|
initEmbeddings().catch(() => { });
|
|
616
622
|
}
|
|
@@ -252,6 +252,19 @@ export class WhatsAppAdapter {
|
|
|
252
252
|
fs.mkdirSync(authDir, { recursive: true });
|
|
253
253
|
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
|
254
254
|
const { version } = await fetchLatestBaileysVersion();
|
|
255
|
+
// Cleanup previous socket (reconnect path) โ without this, every reconnect
|
|
256
|
+
// stacks a new set of listeners on baileys' EventEmitter, so messages get
|
|
257
|
+
// processed N times after N reconnects and closures leak.
|
|
258
|
+
if (this.sock) {
|
|
259
|
+
try {
|
|
260
|
+
this.sock.ev?.removeAllListeners?.();
|
|
261
|
+
this.sock.end?.(new Error("reconnect"));
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// best-effort cleanup โ ignore failures from already-dead socket
|
|
265
|
+
}
|
|
266
|
+
this.sock = null;
|
|
267
|
+
}
|
|
255
268
|
const sock = makeWASocket({
|
|
256
269
|
version,
|
|
257
270
|
auth: {
|
|
@@ -196,6 +196,12 @@ export async function ensureRunning(opts = {}) {
|
|
|
196
196
|
detached: true,
|
|
197
197
|
});
|
|
198
198
|
child.unref();
|
|
199
|
+
// The child inherits its own copy of the fd. Close our copy so the parent
|
|
200
|
+
// process doesn't leak a file descriptor per Chromium bootstrap.
|
|
201
|
+
try {
|
|
202
|
+
fs.closeSync(logStream);
|
|
203
|
+
}
|
|
204
|
+
catch { /* already closed โ fine */ }
|
|
199
205
|
if (!child.pid) {
|
|
200
206
|
throw new Error("Failed to spawn Chromium (no PID)");
|
|
201
207
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk Cleanup Service โ periodic cleanup of transient bot files.
|
|
3
|
+
*
|
|
4
|
+
* Targets files that are SAFE to delete (logs, temp screenshots, browser
|
|
5
|
+
* artifacts, old subagent streams) and leaves critical data alone
|
|
6
|
+
* (memory, assets, workspaces, cron-jobs, .env, session-store).
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* - Each path has a max age (days) OR a max size (MB, with rotation)
|
|
10
|
+
* - Defaults are conservative: keep 30 days of artifacts, rotate logs >100MB
|
|
11
|
+
* - All knobs overridable via env (CLEANUP_* vars) and via /cleanup set <key>
|
|
12
|
+
* - Runs once at boot + every 24h thereafter, unref'd so it doesn't
|
|
13
|
+
* prevent shutdown
|
|
14
|
+
*
|
|
15
|
+
* NEVER cleaned:
|
|
16
|
+
* ~/.alvin-bot/memory/ (daily logs, long-term memory)
|
|
17
|
+
* ~/.alvin-bot/assets/ (user-supplied files)
|
|
18
|
+
* ~/.alvin-bot/workspaces/ (user configuration)
|
|
19
|
+
* ~/.alvin-bot/cron-jobs.json (scheduled tasks)
|
|
20
|
+
* ~/.alvin-bot/.env (secrets)
|
|
21
|
+
* ~/.alvin-bot/session-store.json (resume tokens)
|
|
22
|
+
* ~/.alvin-bot/delivery-queue.json
|
|
23
|
+
* ~/.alvin-bot/standing-orders
|
|
24
|
+
* ~/.alvin-bot/auto-update.flag
|
|
25
|
+
*/
|
|
26
|
+
import fs from "fs";
|
|
27
|
+
import path from "path";
|
|
28
|
+
import os from "os";
|
|
29
|
+
import { DATA_DIR } from "../paths.js";
|
|
30
|
+
const DEFAULT_POLICY = {
|
|
31
|
+
logMaxSizeMb: parseInt(process.env.CLEANUP_LOG_MAX_MB || "100", 10),
|
|
32
|
+
screenshotsMaxAgeDays: parseInt(process.env.CLEANUP_SCREENSHOTS_DAYS || "30", 10),
|
|
33
|
+
subagentsMaxAgeDays: parseInt(process.env.CLEANUP_SUBAGENTS_DAYS || "30", 10),
|
|
34
|
+
tmpMaxAgeDays: parseInt(process.env.CLEANUP_TMP_DAYS || "7", 10),
|
|
35
|
+
waMediaMaxAgeDays: parseInt(process.env.CLEANUP_WA_MEDIA_DAYS || "30", 10),
|
|
36
|
+
};
|
|
37
|
+
const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000; // once a day
|
|
38
|
+
let cleanupTimer = null;
|
|
39
|
+
/**
|
|
40
|
+
* Return the current effective policy (env-overridden defaults).
|
|
41
|
+
*/
|
|
42
|
+
export function getCleanupPolicy() {
|
|
43
|
+
return { ...DEFAULT_POLICY };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Run a cleanup pass once. Safe to call manually (e.g. /cleanup command).
|
|
47
|
+
*/
|
|
48
|
+
export async function runCleanup(policyOverride) {
|
|
49
|
+
const policy = { ...DEFAULT_POLICY, ...policyOverride };
|
|
50
|
+
const result = {
|
|
51
|
+
filesDeleted: 0,
|
|
52
|
+
bytesReclaimed: 0,
|
|
53
|
+
logsRotated: 0,
|
|
54
|
+
errors: [],
|
|
55
|
+
details: [],
|
|
56
|
+
};
|
|
57
|
+
// 1. Rotate large log files (launchd stdout/stderr)
|
|
58
|
+
if (policy.logMaxSizeMb > 0) {
|
|
59
|
+
const logsDir = path.join(DATA_DIR, "logs");
|
|
60
|
+
try {
|
|
61
|
+
if (fs.existsSync(logsDir)) {
|
|
62
|
+
for (const name of fs.readdirSync(logsDir)) {
|
|
63
|
+
if (!name.endsWith(".log"))
|
|
64
|
+
continue;
|
|
65
|
+
const full = path.join(logsDir, name);
|
|
66
|
+
try {
|
|
67
|
+
const st = fs.statSync(full);
|
|
68
|
+
if (st.size > policy.logMaxSizeMb * 1024 * 1024) {
|
|
69
|
+
// Rotate: keep a .old, overwrite current. Launchd will reopen on next write.
|
|
70
|
+
const oldPath = full + ".old";
|
|
71
|
+
try {
|
|
72
|
+
fs.rmSync(oldPath, { force: true });
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
fs.renameSync(full, oldPath);
|
|
76
|
+
fs.writeFileSync(full, "");
|
|
77
|
+
result.logsRotated++;
|
|
78
|
+
result.bytesReclaimed += st.size;
|
|
79
|
+
result.details.push({ path: full, action: "rotated", size: st.size });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
result.errors.push(`log-rotate ${full}: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
result.errors.push(`logs scan: ${err.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 2. Browser screenshots (bot-owned CDP)
|
|
93
|
+
if (policy.screenshotsMaxAgeDays > 0) {
|
|
94
|
+
const dir = path.join(DATA_DIR, "browser", "screenshots");
|
|
95
|
+
cleanupOldFiles(dir, policy.screenshotsMaxAgeDays, result);
|
|
96
|
+
}
|
|
97
|
+
// 3. Subagent streaming outputs โ only delete FINISHED ones (older than N days).
|
|
98
|
+
// We trust that the async-agent-watcher has already marked them done โ files
|
|
99
|
+
// older than a few days are either delivered or definitively abandoned.
|
|
100
|
+
if (policy.subagentsMaxAgeDays > 0) {
|
|
101
|
+
const dir = path.join(DATA_DIR, "subagents");
|
|
102
|
+
cleanupOldFiles(dir, policy.subagentsMaxAgeDays, result, [".jsonl", ".err"]);
|
|
103
|
+
}
|
|
104
|
+
// 4. /tmp/alvin-bot/* (media, temp scrapes)
|
|
105
|
+
if (policy.tmpMaxAgeDays > 0) {
|
|
106
|
+
cleanupOldFiles("/tmp/alvin-bot", policy.tmpMaxAgeDays, result);
|
|
107
|
+
}
|
|
108
|
+
// 5. WhatsApp media cache
|
|
109
|
+
if (policy.waMediaMaxAgeDays > 0) {
|
|
110
|
+
const dir = path.join(DATA_DIR, "data", "wa-media");
|
|
111
|
+
cleanupOldFiles(dir, policy.waMediaMaxAgeDays, result);
|
|
112
|
+
}
|
|
113
|
+
// 6. CDP log (/tmp/chrome-cdp.log) โ always keep just the latest boot
|
|
114
|
+
const cdpLog = path.join(os.tmpdir(), "chrome-cdp.log");
|
|
115
|
+
try {
|
|
116
|
+
if (fs.existsSync(cdpLog)) {
|
|
117
|
+
const st = fs.statSync(cdpLog);
|
|
118
|
+
const ageDays = (Date.now() - st.mtimeMs) / (24 * 60 * 60 * 1000);
|
|
119
|
+
if (ageDays > 7) {
|
|
120
|
+
fs.unlinkSync(cdpLog);
|
|
121
|
+
result.filesDeleted++;
|
|
122
|
+
result.bytesReclaimed += st.size;
|
|
123
|
+
result.details.push({ path: cdpLog, action: "deleted", size: st.size });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Not critical
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Delete files in `dir` older than `maxAgeDays`. Safe if `dir` doesn't exist.
|
|
134
|
+
* Optional extension filter โ e.g. [".jsonl", ".err"] restricts to those types.
|
|
135
|
+
*/
|
|
136
|
+
function cleanupOldFiles(dir, maxAgeDays, result, extensions) {
|
|
137
|
+
if (!fs.existsSync(dir))
|
|
138
|
+
return;
|
|
139
|
+
const cutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
140
|
+
try {
|
|
141
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
142
|
+
const full = path.join(dir, entry.name);
|
|
143
|
+
if (!entry.isFile())
|
|
144
|
+
continue;
|
|
145
|
+
if (extensions && !extensions.some((ext) => entry.name.endsWith(ext)))
|
|
146
|
+
continue;
|
|
147
|
+
try {
|
|
148
|
+
const st = fs.statSync(full);
|
|
149
|
+
if (st.mtimeMs < cutoffMs) {
|
|
150
|
+
fs.unlinkSync(full);
|
|
151
|
+
result.filesDeleted++;
|
|
152
|
+
result.bytesReclaimed += st.size;
|
|
153
|
+
result.details.push({ path: full, action: "deleted", size: st.size });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
result.errors.push(`${full}: ${err.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
result.errors.push(`scan ${dir}: ${err.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Start the periodic cleanup loop. Runs first pass after 5 minutes (let the
|
|
167
|
+
* bot fully boot and avoid competing with startup I/O), then every 24h.
|
|
168
|
+
*/
|
|
169
|
+
export function startCleanupLoop() {
|
|
170
|
+
if (cleanupTimer)
|
|
171
|
+
return;
|
|
172
|
+
// First run delayed so we don't step on a restart that's still writing logs
|
|
173
|
+
setTimeout(() => {
|
|
174
|
+
void runCleanup().then((r) => {
|
|
175
|
+
if (r.filesDeleted > 0 || r.logsRotated > 0) {
|
|
176
|
+
console.log(`[cleanup] ${r.filesDeleted} files deleted, ${r.logsRotated} logs rotated, ${formatBytes(r.bytesReclaimed)} reclaimed`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}, 5 * 60 * 1000);
|
|
180
|
+
cleanupTimer = setInterval(() => {
|
|
181
|
+
void runCleanup().then((r) => {
|
|
182
|
+
if (r.filesDeleted > 0 || r.logsRotated > 0) {
|
|
183
|
+
console.log(`[cleanup] ${r.filesDeleted} files deleted, ${r.logsRotated} logs rotated, ${formatBytes(r.bytesReclaimed)} reclaimed`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}, CLEANUP_INTERVAL_MS);
|
|
187
|
+
cleanupTimer.unref?.();
|
|
188
|
+
}
|
|
189
|
+
export function stopCleanupLoop() {
|
|
190
|
+
if (cleanupTimer) {
|
|
191
|
+
clearInterval(cleanupTimer);
|
|
192
|
+
cleanupTimer = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function formatBytes(n) {
|
|
196
|
+
if (n < 1024)
|
|
197
|
+
return `${n} B`;
|
|
198
|
+
if (n < 1024 * 1024)
|
|
199
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
200
|
+
if (n < 1024 * 1024 * 1024)
|
|
201
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
202
|
+
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
203
|
+
}
|
|
@@ -72,6 +72,10 @@ export function startHeartbeat() {
|
|
|
72
72
|
setTimeout(() => {
|
|
73
73
|
runHeartbeat();
|
|
74
74
|
state.intervalId = setInterval(runHeartbeat, HEARTBEAT_INTERVAL_MS);
|
|
75
|
+
// .unref() so this interval alone doesn't keep the process alive during
|
|
76
|
+
// graceful shutdown โ the bot's main loop (grammy, platforms) keeps it
|
|
77
|
+
// running, and once those stop we want the process to exit cleanly.
|
|
78
|
+
state.intervalId?.unref?.();
|
|
75
79
|
}, 30_000);
|
|
76
80
|
}
|
|
77
81
|
/**
|
package/dist/services/updater.js
CHANGED
|
@@ -272,6 +272,7 @@ export function startAutoUpdateLoop() {
|
|
|
272
272
|
console.log(`[auto-update] check failed: ${result.message}`);
|
|
273
273
|
}
|
|
274
274
|
}, AUTO_CHECK_INTERVAL_MS);
|
|
275
|
+
autoTimer.unref?.();
|
|
275
276
|
console.log(`[auto-update] loop started (interval: 6h)`);
|
|
276
277
|
}
|
|
277
278
|
export function stopAutoUpdateLoop() {
|