aiden-runtime 4.0.2 → 4.1.1
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/README.md +19 -11
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +424 -7
- package/dist/cli/v4/aidenPrompt.js +317 -0
- package/dist/cli/v4/box.js +105 -39
- package/dist/cli/v4/callbacks.js +39 -6
- package/dist/cli/v4/chatSession.js +256 -55
- package/dist/cli/v4/citationFooter.js +97 -0
- package/dist/cli/v4/commands/channel.js +656 -0
- package/dist/cli/v4/commands/clear.js +1 -1
- package/dist/cli/v4/commands/compress.js +1 -1
- package/dist/cli/v4/commands/cron.js +44 -16
- package/dist/cli/v4/commands/fanout.js +236 -0
- package/dist/cli/v4/commands/help.js +15 -4
- package/dist/cli/v4/commands/history.js +84 -0
- package/dist/cli/v4/commands/index.js +16 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/show.js +43 -0
- package/dist/cli/v4/commands/skills.js +169 -4
- package/dist/cli/v4/commands/status.js +84 -0
- package/dist/cli/v4/commands/subagent.js +78 -0
- package/dist/cli/v4/commands/verbose.js +1 -1
- package/dist/cli/v4/commands/voice.js +218 -0
- package/dist/cli/v4/cronCli.js +103 -0
- package/dist/cli/v4/display.js +297 -13
- package/dist/cli/v4/doctor.js +102 -1
- package/dist/cli/v4/doctorLiveness.js +329 -0
- package/dist/cli/v4/envSources.js +105 -0
- package/dist/cli/v4/ghostMatch.js +74 -0
- package/dist/cli/v4/historyStore.js +163 -0
- package/dist/cli/v4/pasteCompression.js +124 -0
- package/dist/cli/v4/pasteIntercept.js +203 -0
- package/dist/cli/v4/replyRenderer.js +209 -0
- package/dist/cli/v4/resizeGuard.js +92 -0
- package/dist/cli/v4/shellInterpolation.js +139 -0
- package/dist/cli/v4/skinEngine.js +21 -1
- package/dist/cli/v4/streamingPrefix.js +121 -0
- package/dist/cli/v4/syntaxHighlight.js +345 -0
- package/dist/cli/v4/table.js +216 -0
- package/dist/cli/v4/themeDetect.js +81 -0
- package/dist/cli/v4/uiBuild.js +74 -0
- package/dist/cli/v4/voiceCli.js +113 -0
- package/dist/cli/v4/voicePromptApi.js +196 -0
- package/dist/core/channels/discord.js +16 -10
- package/dist/core/channels/email.js +13 -9
- package/dist/core/channels/imessage.js +13 -9
- package/dist/core/channels/manager.js +25 -7
- package/dist/core/channels/pdf-extract.js +180 -0
- package/dist/core/channels/photo-vision.js +157 -0
- package/dist/core/channels/signal.js +11 -7
- package/dist/core/channels/slack.js +13 -10
- package/dist/core/channels/telegram-commands.js +154 -0
- package/dist/core/channels/telegram-groups.js +198 -0
- package/dist/core/channels/telegram-rate-limit.js +124 -0
- package/dist/core/channels/telegram.js +1980 -0
- package/dist/core/channels/twilio.js +11 -7
- package/dist/core/channels/webhook.js +9 -5
- package/dist/core/channels/whatsapp.js +15 -11
- package/dist/core/channels/whisper-transcribe.js +163 -0
- package/dist/core/cronManager.js +33 -294
- package/dist/core/gateway.js +29 -8
- package/dist/core/playwrightBridge.js +90 -0
- package/dist/core/v4/aidenAgent.js +35 -0
- package/dist/core/v4/auxiliaryClient.js +2 -2
- package/dist/core/v4/cron/atomicWrite.js +18 -4
- package/dist/core/v4/cron/cronExecute.js +300 -0
- package/dist/core/v4/cron/cronManager.js +502 -0
- package/dist/core/v4/cron/cronState.js +314 -0
- package/dist/core/v4/cron/cronTick.js +90 -0
- package/dist/core/v4/cron/diagnostics.js +104 -0
- package/dist/core/v4/cron/graceWindow.js +79 -0
- package/dist/core/v4/logger/factory.js +110 -0
- package/dist/core/v4/logger/index.js +22 -0
- package/dist/core/v4/logger/logger.js +101 -0
- package/dist/core/v4/logger/sinks/fileSink.js +110 -0
- package/dist/core/v4/logger/sinks/multiSink.js +43 -0
- package/dist/core/v4/logger/sinks/nullSink.js +53 -0
- package/dist/core/v4/logger/sinks/stdSink.js +81 -0
- package/dist/core/v4/mcp/server/diagnostics.js +40 -0
- package/dist/core/v4/mcp/server/skillBridge.js +94 -0
- package/dist/core/v4/mcp/server/stdioServer.js +119 -0
- package/dist/core/v4/mcp/server/toolBridge.js +168 -0
- package/dist/core/v4/platformPaths.js +105 -0
- package/dist/core/v4/providerFallback.js +25 -0
- package/dist/core/v4/skillLoader.js +21 -5
- package/dist/core/v4/skillMining/candidateStore.js +164 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +118 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +140 -0
- package/dist/core/v4/skillMining/skillMiner.js +191 -0
- package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
- package/dist/core/v4/subagent/budget.js +76 -0
- package/dist/core/v4/subagent/diagnostics.js +22 -0
- package/dist/core/v4/subagent/fanout.js +216 -0
- package/dist/core/v4/subagent/merger.js +148 -0
- package/dist/core/v4/subagent/providerRotation.js +54 -0
- package/dist/core/v4/voice/audioStream.js +373 -0
- package/dist/core/v4/voice/cliVoice.js +393 -0
- package/dist/core/v4/voice/diagnostics.js +66 -0
- package/dist/core/v4/voice/ttsStream.js +193 -0
- package/dist/core/version.js +1 -1
- package/dist/core/visionAnalyze.js +291 -90
- package/dist/core/voice/audio.js +61 -5
- package/dist/core/voice/audioBackend.js +134 -0
- package/dist/core/voice/stt.js +61 -6
- package/dist/core/voice/tts.js +19 -3
- package/dist/moat/dangerousPatterns.js +1 -1
- package/dist/providers/v4/codexResponsesAdapter.js +7 -2
- package/dist/providers/v4/errors.js +51 -1
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +190 -0
- package/package.json +11 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// DevOS — Autonomous AI Execution System
|
|
4
|
+
// Copyright (c) 2026 Shiva Deore. All rights reserved.
|
|
5
|
+
// ============================================================
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.TelegramGroupStore = void 0;
|
|
11
|
+
// core/channels/telegram-groups.ts — Phase v4.1-2.
|
|
12
|
+
//
|
|
13
|
+
// Persistent per-group state for the Telegram channel:
|
|
14
|
+
// - paused — admin /pause stops the bot from replying
|
|
15
|
+
// - allowedUsers — opt-in restriction set by /allowusers
|
|
16
|
+
// - title — group display name (cached for /channel
|
|
17
|
+
// telegram groups list — Telegram's getChat
|
|
18
|
+
// costs an HTTP call per query)
|
|
19
|
+
// - lastMessageAt — wall-clock of the last seen inbound msg
|
|
20
|
+
// - lastAdminAction — when an admin last touched the state
|
|
21
|
+
// - firstSeenAt — when the bot first observed this group
|
|
22
|
+
//
|
|
23
|
+
// State lives at `<aidenRoot>/state/telegram-groups.json`. Atomic
|
|
24
|
+
// writes (tmp → rename) keep the file consistent across process
|
|
25
|
+
// crashes. Loaded once at adapter start; mutations debounce flushes
|
|
26
|
+
// at 1 s so a burst of admin commands doesn't hammer the disk.
|
|
27
|
+
//
|
|
28
|
+
// All diagnostics route through the v4.1-1.3a Logger contract.
|
|
29
|
+
// No console.* anywhere in this module.
|
|
30
|
+
const node_fs_1 = require("node:fs");
|
|
31
|
+
const node_fs_2 = require("node:fs");
|
|
32
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
33
|
+
const logger_1 = require("../v4/logger");
|
|
34
|
+
/**
|
|
35
|
+
* In-memory + disk-backed store of per-group state. Read paths are
|
|
36
|
+
* always synchronous reads of the in-memory map; mutations schedule
|
|
37
|
+
* a debounced flush so a burst of admin commands collapses to one
|
|
38
|
+
* write.
|
|
39
|
+
*/
|
|
40
|
+
class TelegramGroupStore {
|
|
41
|
+
constructor(opts) {
|
|
42
|
+
this.groups = new Map();
|
|
43
|
+
this.flushTimer = null;
|
|
44
|
+
this.loaded = false;
|
|
45
|
+
this.stateDir = node_path_1.default.join(opts.paths.root, 'state');
|
|
46
|
+
this.statePath = node_path_1.default.join(this.stateDir, 'telegram-groups.json');
|
|
47
|
+
this.log = opts.logger ?? (0, logger_1.noopLogger)();
|
|
48
|
+
this.flushDebounceMs = opts.flushDebounceMs ?? 1000;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Synchronously load on first call. Subsequent calls are no-ops.
|
|
52
|
+
* Failure to read is treated as "fresh state" — better than crashing
|
|
53
|
+
* the adapter on a malformed file.
|
|
54
|
+
*/
|
|
55
|
+
async load() {
|
|
56
|
+
if (this.loaded)
|
|
57
|
+
return;
|
|
58
|
+
this.loaded = true;
|
|
59
|
+
if (!(0, node_fs_2.existsSync)(this.statePath))
|
|
60
|
+
return;
|
|
61
|
+
try {
|
|
62
|
+
const raw = await node_fs_1.promises.readFile(this.statePath, 'utf8');
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
if (parsed?.version !== 1 || !parsed.groups)
|
|
65
|
+
return;
|
|
66
|
+
for (const [id, g] of Object.entries(parsed.groups)) {
|
|
67
|
+
if (g && typeof g === 'object' && 'groupId' in g) {
|
|
68
|
+
this.groups.set(id, normalizeOnLoad(g));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.log.info(`loaded ${this.groups.size} group(s)`);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
this.log.warn(`could not load state: ${err?.message ?? err}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** True when this group is allowed to interact with the bot. */
|
|
78
|
+
isPaused(groupId) {
|
|
79
|
+
return this.groups.get(groupId)?.paused === true;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* When an allowed-users list is set on a group, only those users may
|
|
83
|
+
* converse. Empty list (the default) → everyone in the group is OK.
|
|
84
|
+
* Returns true when the user is allowed.
|
|
85
|
+
*/
|
|
86
|
+
userIsAllowed(groupId, userId) {
|
|
87
|
+
const g = this.groups.get(groupId);
|
|
88
|
+
if (!g || g.allowedUsers.length === 0)
|
|
89
|
+
return true;
|
|
90
|
+
return g.allowedUsers.includes(userId);
|
|
91
|
+
}
|
|
92
|
+
/** Public accessor for /channel telegram groups list. */
|
|
93
|
+
list() {
|
|
94
|
+
return Array.from(this.groups.values()).sort((a, b) => (b.lastMessageAt ?? 0) - (a.lastMessageAt ?? 0));
|
|
95
|
+
}
|
|
96
|
+
get(groupId) {
|
|
97
|
+
return this.groups.get(groupId);
|
|
98
|
+
}
|
|
99
|
+
/** Record an inbound observation — bumps lastMessageAt + caches title. */
|
|
100
|
+
observeMessage(groupId, opts) {
|
|
101
|
+
const existing = this.groups.get(groupId);
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
if (existing) {
|
|
104
|
+
existing.lastMessageAt = now;
|
|
105
|
+
if (opts.title && existing.title !== opts.title)
|
|
106
|
+
existing.title = opts.title;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.groups.set(groupId, {
|
|
110
|
+
groupId,
|
|
111
|
+
title: opts.title,
|
|
112
|
+
paused: false,
|
|
113
|
+
allowedUsers: [],
|
|
114
|
+
firstSeenAt: now,
|
|
115
|
+
lastMessageAt: now,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
this.scheduleFlush();
|
|
119
|
+
}
|
|
120
|
+
setPaused(groupId, paused, actor) {
|
|
121
|
+
const g = this.ensureGroup(groupId);
|
|
122
|
+
g.paused = paused;
|
|
123
|
+
g.lastAdminAction = { actor, cmd: paused ? 'pause' : 'resume', at: Date.now() };
|
|
124
|
+
this.scheduleFlush();
|
|
125
|
+
}
|
|
126
|
+
setAllowedUsers(groupId, userIds, actor) {
|
|
127
|
+
const g = this.ensureGroup(groupId);
|
|
128
|
+
g.allowedUsers = [...new Set(userIds.map(s => s.trim()).filter(Boolean))];
|
|
129
|
+
g.lastAdminAction = { actor, cmd: 'allowusers', at: Date.now() };
|
|
130
|
+
this.scheduleFlush();
|
|
131
|
+
}
|
|
132
|
+
recordAdminAction(groupId, cmd, actor) {
|
|
133
|
+
const g = this.ensureGroup(groupId);
|
|
134
|
+
g.lastAdminAction = { actor, cmd, at: Date.now() };
|
|
135
|
+
this.scheduleFlush();
|
|
136
|
+
}
|
|
137
|
+
/** Force-flush + clear debounce timer (called on adapter teardown). */
|
|
138
|
+
async flushNow() {
|
|
139
|
+
if (this.flushTimer) {
|
|
140
|
+
clearTimeout(this.flushTimer);
|
|
141
|
+
this.flushTimer = null;
|
|
142
|
+
}
|
|
143
|
+
await this.writeFile();
|
|
144
|
+
}
|
|
145
|
+
// ── Internals ─────────────────────────────────────────────────
|
|
146
|
+
ensureGroup(groupId) {
|
|
147
|
+
let g = this.groups.get(groupId);
|
|
148
|
+
if (!g) {
|
|
149
|
+
g = {
|
|
150
|
+
groupId,
|
|
151
|
+
paused: false,
|
|
152
|
+
allowedUsers: [],
|
|
153
|
+
firstSeenAt: Date.now(),
|
|
154
|
+
};
|
|
155
|
+
this.groups.set(groupId, g);
|
|
156
|
+
}
|
|
157
|
+
return g;
|
|
158
|
+
}
|
|
159
|
+
scheduleFlush() {
|
|
160
|
+
if (this.flushTimer)
|
|
161
|
+
return;
|
|
162
|
+
this.flushTimer = setTimeout(() => {
|
|
163
|
+
this.flushTimer = null;
|
|
164
|
+
this.writeFile().catch((err) => this.log.warn(`flush failed: ${err?.message ?? err}`));
|
|
165
|
+
}, this.flushDebounceMs);
|
|
166
|
+
if (typeof this.flushTimer.unref === 'function')
|
|
167
|
+
this.flushTimer.unref();
|
|
168
|
+
}
|
|
169
|
+
async writeFile() {
|
|
170
|
+
const payload = {
|
|
171
|
+
version: 1,
|
|
172
|
+
groups: Object.fromEntries(this.groups),
|
|
173
|
+
};
|
|
174
|
+
await node_fs_1.promises.mkdir(this.stateDir, { recursive: true });
|
|
175
|
+
const tmp = `${this.statePath}.${process.pid}.tmp`;
|
|
176
|
+
await node_fs_1.promises.writeFile(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
177
|
+
await node_fs_1.promises.rename(tmp, this.statePath);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
exports.TelegramGroupStore = TelegramGroupStore;
|
|
181
|
+
/**
|
|
182
|
+
* Defensive load-time normaliser — older state files may be missing
|
|
183
|
+
* fields we've since added; fall back to safe defaults instead of
|
|
184
|
+
* propagating undefined into the rest of the adapter.
|
|
185
|
+
*/
|
|
186
|
+
function normalizeOnLoad(raw) {
|
|
187
|
+
return {
|
|
188
|
+
groupId: String(raw.groupId),
|
|
189
|
+
title: typeof raw.title === 'string' ? raw.title : undefined,
|
|
190
|
+
paused: raw.paused === true,
|
|
191
|
+
allowedUsers: Array.isArray(raw.allowedUsers) ? raw.allowedUsers.map(String) : [],
|
|
192
|
+
firstSeenAt: typeof raw.firstSeenAt === 'number' ? raw.firstSeenAt : Date.now(),
|
|
193
|
+
lastMessageAt: typeof raw.lastMessageAt === 'number' ? raw.lastMessageAt : undefined,
|
|
194
|
+
lastAdminAction: raw.lastAdminAction && typeof raw.lastAdminAction === 'object'
|
|
195
|
+
? raw.lastAdminAction
|
|
196
|
+
: undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// DevOS — Autonomous AI Execution System
|
|
4
|
+
// Copyright (c) 2026 Shiva Deore. All rights reserved.
|
|
5
|
+
// ============================================================
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.TelegramRateLimiter = void 0;
|
|
8
|
+
// core/channels/telegram-rate-limit.ts — Phase v4.1-2.
|
|
9
|
+
//
|
|
10
|
+
// Sliding-window per-user rate limiter for the Telegram channel.
|
|
11
|
+
//
|
|
12
|
+
// Design:
|
|
13
|
+
// - In-memory only — resets on aiden restart. The threat model is
|
|
14
|
+
// "stop a single chatter from burning the bot owner's quota in
|
|
15
|
+
// one sitting"; a restart-bypass that costs the abuser an entire
|
|
16
|
+
// restart-window's worth of messages is acceptable.
|
|
17
|
+
// - Single user-id keyspace across all chats — a spammer can't dodge
|
|
18
|
+
// by hopping between groups.
|
|
19
|
+
// - 1-minute sliding window, default 5 messages. Both knobs are
|
|
20
|
+
// configurable via env (`TELEGRAM_USER_RATE_LIMIT`,
|
|
21
|
+
// `TELEGRAM_USER_RATE_WINDOW_MS`) so an op who hosts the bot for a
|
|
22
|
+
// bigger community can bump them without code changes.
|
|
23
|
+
// - `shouldThrottle(userId)` is the only consumer-facing method —
|
|
24
|
+
// records the access *and* reports whether the caller should drop
|
|
25
|
+
// the message. One lookup, one mutation, one decision.
|
|
26
|
+
// - A coalescing sweeper trims stale buckets every 5 minutes so an
|
|
27
|
+
// adversary can't blow the heap by hammering with fresh user ids.
|
|
28
|
+
//
|
|
29
|
+
// `TelegramRateLimiter` accepts an injected logger from the unified
|
|
30
|
+
// `Logger` contract (Phase v4.1-1.3a) — diagnostics file-only, REPL
|
|
31
|
+
// stays sacred. No console.* anywhere in this module.
|
|
32
|
+
const logger_1 = require("../v4/logger");
|
|
33
|
+
const DEFAULT_LIMIT = 5;
|
|
34
|
+
const DEFAULT_WINDOW_MS = 60000;
|
|
35
|
+
const SWEEP_INTERVAL_MS = 5 * 60 * 1000; // prune buckets idle > sweep+window
|
|
36
|
+
class TelegramRateLimiter {
|
|
37
|
+
constructor(opts = {}) {
|
|
38
|
+
this.buckets = new Map();
|
|
39
|
+
this.sweepTimer = null;
|
|
40
|
+
this.limit = readPositiveInt(process.env.TELEGRAM_USER_RATE_LIMIT, opts.limit ?? DEFAULT_LIMIT);
|
|
41
|
+
this.windowMs = readPositiveInt(process.env.TELEGRAM_USER_RATE_WINDOW_MS, opts.windowMs ?? DEFAULT_WINDOW_MS);
|
|
42
|
+
this.now = opts.now ?? Date.now;
|
|
43
|
+
this.log = opts.logger ?? (0, logger_1.noopLogger)();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Record an attempted message + return true when the caller should
|
|
47
|
+
* drop it. The bucket is updated on every call (even ones that get
|
|
48
|
+
* throttled), so a sustained over-limit user stays throttled until
|
|
49
|
+
* the oldest message in their window ages out.
|
|
50
|
+
*/
|
|
51
|
+
shouldThrottle(userId) {
|
|
52
|
+
if (!userId)
|
|
53
|
+
return false;
|
|
54
|
+
const cutoff = this.now() - this.windowMs;
|
|
55
|
+
const bucket = this.buckets.get(userId) ?? [];
|
|
56
|
+
// Drop expired entries from the front (oldest-first list).
|
|
57
|
+
while (bucket.length > 0 && bucket[0] <= cutoff)
|
|
58
|
+
bucket.shift();
|
|
59
|
+
if (bucket.length >= this.limit) {
|
|
60
|
+
// Don't append on throttle so the window can age out cleanly —
|
|
61
|
+
// appending here would reset the user's clock every time they
|
|
62
|
+
// try to spam, locking them in indefinitely.
|
|
63
|
+
this.buckets.set(userId, bucket);
|
|
64
|
+
this.log.warn(`rate-limited`, { userId, count: bucket.length, limit: this.limit });
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
bucket.push(this.now());
|
|
68
|
+
this.buckets.set(userId, bucket);
|
|
69
|
+
this.scheduleSweep();
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/** Test / diagnostic accessor — current bucket size for a user. */
|
|
73
|
+
getCount(userId) {
|
|
74
|
+
const bucket = this.buckets.get(userId);
|
|
75
|
+
if (!bucket)
|
|
76
|
+
return 0;
|
|
77
|
+
const cutoff = this.now() - this.windowMs;
|
|
78
|
+
return bucket.filter((t) => t > cutoff).length;
|
|
79
|
+
}
|
|
80
|
+
/** Stop the sweep timer (called on adapter teardown). */
|
|
81
|
+
dispose() {
|
|
82
|
+
if (this.sweepTimer) {
|
|
83
|
+
clearInterval(this.sweepTimer);
|
|
84
|
+
this.sweepTimer = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ── Internal: prune fully-expired buckets so memory doesn't drift. ──
|
|
88
|
+
scheduleSweep() {
|
|
89
|
+
if (this.sweepTimer)
|
|
90
|
+
return;
|
|
91
|
+
this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
|
|
92
|
+
// Don't keep the process alive just for the sweeper.
|
|
93
|
+
if (typeof this.sweepTimer.unref === 'function')
|
|
94
|
+
this.sweepTimer.unref();
|
|
95
|
+
}
|
|
96
|
+
sweep() {
|
|
97
|
+
const cutoff = this.now() - this.windowMs;
|
|
98
|
+
let pruned = 0;
|
|
99
|
+
for (const [userId, bucket] of this.buckets) {
|
|
100
|
+
const filtered = bucket.filter((t) => t > cutoff);
|
|
101
|
+
if (filtered.length === 0) {
|
|
102
|
+
this.buckets.delete(userId);
|
|
103
|
+
pruned += 1;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
this.buckets.set(userId, filtered);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (pruned > 0)
|
|
110
|
+
this.log.debug(`swept ${pruned} stale buckets`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.TelegramRateLimiter = TelegramRateLimiter;
|
|
114
|
+
/**
|
|
115
|
+
* Parse a positive integer env var with a fallback. Negative / NaN /
|
|
116
|
+
* empty values fall back to the default — better than crashing the
|
|
117
|
+
* adapter because someone fat-fingered an env var.
|
|
118
|
+
*/
|
|
119
|
+
function readPositiveInt(envValue, fallback) {
|
|
120
|
+
if (typeof envValue !== 'string' || envValue.trim() === '')
|
|
121
|
+
return fallback;
|
|
122
|
+
const n = Number.parseInt(envValue, 10);
|
|
123
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
124
|
+
}
|