@poolzin/pool-bot 2026.3.21 → 2026.3.23

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.
Files changed (124) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/acp/bindings-store.js +209 -0
  3. package/dist/acp/control-plane/runtime-cache.js +54 -0
  4. package/dist/acp/control-plane/runtime-options.js +215 -0
  5. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  6. package/dist/acp/runtime/errors.js +47 -0
  7. package/dist/acp/runtime/registry.js +86 -0
  8. package/dist/acp/runtime/types.js +1 -0
  9. package/dist/acp/translator.js +97 -0
  10. package/dist/agents/failover-error.js +145 -47
  11. package/dist/browser/browser-profile-manager.js +319 -0
  12. package/dist/browser/cdp-proxy-bypass.js +129 -0
  13. package/dist/browser/cdp-timeouts.js +41 -0
  14. package/dist/browser/chrome-extension-validator.js +406 -0
  15. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  16. package/dist/browser/chrome-mcp.js +421 -0
  17. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  18. package/dist/browser/errors.js +67 -0
  19. package/dist/browser/form-fields.js +22 -0
  20. package/dist/browser/output-atomic.js +44 -0
  21. package/dist/browser/profile-capabilities.js +47 -0
  22. package/dist/browser/safe-filename.js +25 -0
  23. package/dist/browser/snapshot-roles.js +60 -0
  24. package/dist/build-info.json +3 -3
  25. package/dist/commands/security-owner-only.js +86 -0
  26. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  27. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  28. package/dist/control-ui/index.html +1 -1
  29. package/dist/cron/cron-filters.js +150 -0
  30. package/dist/gateway/device-pairing-security.js +197 -0
  31. package/dist/gateway/event-deduplication.js +167 -0
  32. package/dist/gateway/run-tracker.js +253 -0
  33. package/dist/gateway/server-methods/nodes.js +14 -0
  34. package/dist/gateway/websocket-preauth-security.js +188 -0
  35. package/dist/infra/errors.js +53 -13
  36. package/dist/infra/exec-approvals-security.js +217 -0
  37. package/dist/infra/security/command-analyzer.js +257 -0
  38. package/dist/plugins/loader.js +16 -8
  39. package/dist/security/external-content.js +51 -1
  40. package/dist/sessions/session-costs.js +228 -0
  41. package/dist/shared/param-key.js +16 -0
  42. package/dist/shared/poll-params.js +58 -0
  43. package/dist/shared/polls.js +55 -0
  44. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  45. package/docs/FEATURES.md +523 -0
  46. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  47. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  48. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  49. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  50. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  51. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  52. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  53. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  54. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  55. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  56. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  57. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  58. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  59. package/docs/PHASE-7-SUMMARY.md +144 -0
  60. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  61. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  62. package/docs/README.md +116 -0
  63. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  64. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  65. package/docs/channels/googlechat.md +235 -206
  66. package/docs/channels/irc.md +332 -0
  67. package/docs/channels/nostr.md +255 -168
  68. package/docs/components/command-palette.md +166 -0
  69. package/docs/components/login-gate.md +219 -0
  70. package/docs/getting-started/installation.md +191 -0
  71. package/docs/getting-started/introduction.md +120 -0
  72. package/docs/improvements/USAGE-GUIDE.md +359 -0
  73. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  74. package/docs/reference/deadcode-detection.md +72 -0
  75. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  76. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  77. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  78. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  79. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  80. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  81. package/extensions/googlechat/package.json +11 -28
  82. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  83. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  84. package/extensions/googlechat/src/index.ts +14 -0
  85. package/extensions/irc/node_modules/.bin/tsc +21 -0
  86. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  87. package/extensions/irc/node_modules/.bin/vitest +21 -0
  88. package/extensions/irc/package.json +16 -8
  89. package/extensions/irc/src/index.ts +14 -0
  90. package/extensions/irc/src/irc-channel.test.ts +43 -0
  91. package/extensions/irc/src/irc-channel.ts +191 -0
  92. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  93. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  94. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  95. package/extensions/keyed-async-queue/package.json +20 -0
  96. package/extensions/keyed-async-queue/src/index.ts +14 -0
  97. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  98. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  99. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  100. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  101. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  102. package/extensions/memory-core/package.json +11 -8
  103. package/extensions/memory-core/src/index.ts +14 -0
  104. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  105. package/extensions/memory-core/src/memory-manager.ts +186 -0
  106. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  107. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  108. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  109. package/extensions/nostr/package.json +15 -24
  110. package/extensions/nostr/src/index.ts +14 -0
  111. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  112. package/extensions/nostr/src/nostr-channel.ts +228 -0
  113. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  114. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  115. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  116. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  117. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  118. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  119. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  120. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  121. package/package.json +2 -1
  122. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  123. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  124. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
@@ -6,7 +6,7 @@
6
6
  <title>Poolbot Control</title>
7
7
  <meta name="color-scheme" content="dark light" />
8
8
  <link rel="icon" href="./favicon.ico" sizes="any" />
9
- <script type="module" crossorigin src="./assets/index-Dvkl4Xlx.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-D7shnQwQ.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-CSfXd2LO.css">
11
11
  </head>
12
12
  <body>
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Advanced Cron Filters
3
+ * Filter cron jobs by channel, user, session, and other criteria
4
+ */
5
+ /**
6
+ * Check if a cron job matches a filter
7
+ */
8
+ export function matchesFilter(job, filter) {
9
+ if (!filter.enabled) {
10
+ return true; // Disabled filters don't block
11
+ }
12
+ const criteria = filter.criteria;
13
+ // Channel filters
14
+ if (criteria.channelId && job.channelId !== criteria.channelId) {
15
+ return false;
16
+ }
17
+ if (criteria.channelIds && !criteria.channelIds.includes(job.channelId || "")) {
18
+ return false;
19
+ }
20
+ if (criteria.channelType) {
21
+ // Would need channel type from job metadata
22
+ const jobType = job.metadata?.channelType || "";
23
+ if (jobType !== criteria.channelType) {
24
+ return false;
25
+ }
26
+ }
27
+ // User filters
28
+ if (criteria.userId && job.userId !== criteria.userId) {
29
+ return false;
30
+ }
31
+ if (criteria.userIds && !criteria.userIds.includes(job.userId || "")) {
32
+ return false;
33
+ }
34
+ if (criteria.userRole) {
35
+ const jobRole = job.metadata?.userRole || "";
36
+ if (jobRole !== criteria.userRole) {
37
+ return false;
38
+ }
39
+ }
40
+ // Session filters
41
+ if (criteria.sessionId && job.sessionId !== criteria.sessionId) {
42
+ return false;
43
+ }
44
+ if (criteria.sessionIds && !criteria.sessionIds.includes(job.sessionId || "")) {
45
+ return false;
46
+ }
47
+ if (criteria.sessionMode) {
48
+ const jobMode = job.metadata?.sessionMode || "normal";
49
+ if (jobMode !== criteria.sessionMode) {
50
+ return false;
51
+ }
52
+ }
53
+ // Agent filters
54
+ if (criteria.agentId && job.agentId !== criteria.agentId) {
55
+ return false;
56
+ }
57
+ if (criteria.agentIds && !criteria.agentIds.includes(job.agentId || "")) {
58
+ return false;
59
+ }
60
+ // Custom filters
61
+ if (criteria.custom) {
62
+ for (const [key, value] of Object.entries(criteria.custom)) {
63
+ const jobValue = job.metadata?.[key];
64
+ if (jobValue !== value) {
65
+ return false;
66
+ }
67
+ }
68
+ }
69
+ return true;
70
+ }
71
+ /**
72
+ * Filter cron jobs by multiple filters
73
+ */
74
+ export function filterCronJobs(jobs, filters) {
75
+ return jobs.filter((job) => {
76
+ // Job must match ALL enabled filters
77
+ for (const filter of filters) {
78
+ if (!matchesFilter(job, filter)) {
79
+ return false;
80
+ }
81
+ }
82
+ return true;
83
+ });
84
+ }
85
+ /**
86
+ * Create a new cron filter
87
+ */
88
+ export function createCronFilter(type, criteria, enabled = true) {
89
+ return {
90
+ id: `filter_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
91
+ type,
92
+ enabled,
93
+ criteria,
94
+ createdAt: Date.now(),
95
+ };
96
+ }
97
+ /**
98
+ * Update an existing cron filter
99
+ */
100
+ export function updateCronFilter(filter, updates) {
101
+ return {
102
+ ...filter,
103
+ ...updates,
104
+ updatedAt: Date.now(),
105
+ };
106
+ }
107
+ /**
108
+ * Validate a cron filter
109
+ */
110
+ export function validateCronFilter(filter) {
111
+ const errors = [];
112
+ // Check required fields
113
+ if (!filter.id) {
114
+ errors.push("Filter ID is required");
115
+ }
116
+ if (!filter.type) {
117
+ errors.push("Filter type is required");
118
+ }
119
+ if (!["channel", "user", "session", "agent", "custom"].includes(filter.type)) {
120
+ errors.push(`Invalid filter type: ${filter.type}`);
121
+ }
122
+ // Check criteria based on type
123
+ if (filter.type === "channel" && !filter.criteria.channelId && !filter.criteria.channelIds) {
124
+ errors.push("Channel filter requires channelId or channelIds");
125
+ }
126
+ if (filter.type === "user" && !filter.criteria.userId && !filter.criteria.userIds) {
127
+ errors.push("User filter requires userId or userIds");
128
+ }
129
+ if (filter.type === "session" && !filter.criteria.sessionId && !filter.criteria.sessionIds) {
130
+ errors.push("Session filter requires sessionId or sessionIds");
131
+ }
132
+ return {
133
+ valid: errors.length === 0,
134
+ errors,
135
+ };
136
+ }
137
+ /**
138
+ * Get filter statistics
139
+ */
140
+ export function getFilterStats(filters) {
141
+ return {
142
+ total: filters.length,
143
+ enabled: filters.filter((f) => f.enabled).length,
144
+ disabled: filters.filter((f) => !f.enabled).length,
145
+ byType: filters.reduce((acc, f) => {
146
+ acc[f.type] = (acc[f.type] || 0) + 1;
147
+ return acc;
148
+ }, {}),
149
+ };
150
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Device Pairing Security Hardening
3
+ *
4
+ * Security fixes for device pairing:
5
+ * - Single-use bootstrap setup codes
6
+ * - Short-lived bootstrap tokens
7
+ * - Fail-closed for reused tokens
8
+ */
9
+ import crypto from "node:crypto";
10
+ const DEFAULT_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
11
+ const CLEANUP_INTERVAL_MS = 60 * 1000; // 1 minute
12
+ /**
13
+ * Generate a secure single-use setup code
14
+ *
15
+ * Format: XXXX-YYYY (8 chars, 2 groups for readability)
16
+ * Entropy: ~36 bits (sufficient for short-lived codes)
17
+ */
18
+ export function generateSetupCode() {
19
+ const bytes = crypto.randomBytes(4);
20
+ const part1 = bytes.readUInt16BE(0) % 10000;
21
+ const part2 = bytes.readUInt16BE(2) % 10000;
22
+ return `${part1.toString().padStart(4, '0')}-${part2.toString().padStart(4, '0')}`;
23
+ }
24
+ /**
25
+ * Create a new single-use bootstrap token
26
+ */
27
+ export function createBootstrapToken(params) {
28
+ const expiryMs = params?.expiryMs ?? DEFAULT_EXPIRY_MS;
29
+ const purpose = params?.purpose ?? "pairing";
30
+ const now = Date.now();
31
+ return {
32
+ id: crypto.randomUUID(),
33
+ code: generateSetupCode(),
34
+ createdAt: now,
35
+ expiresAt: now + expiryMs,
36
+ used: false,
37
+ purpose,
38
+ };
39
+ }
40
+ /**
41
+ * Initialize bootstrap token store with automatic cleanup
42
+ */
43
+ export function createBootstrapTokenStore() {
44
+ const store = {
45
+ tokens: new Map(),
46
+ };
47
+ // Start automatic cleanup
48
+ store.cleanupInterval = setInterval(() => {
49
+ cleanupExpiredTokens(store);
50
+ }, CLEANUP_INTERVAL_MS);
51
+ // Cleanup on process exit
52
+ process.on("exit", () => {
53
+ if (store.cleanupInterval) {
54
+ clearInterval(store.cleanupInterval);
55
+ }
56
+ });
57
+ return store;
58
+ }
59
+ /**
60
+ * Add a bootstrap token to the store
61
+ */
62
+ export function addBootstrapToken(store, token) {
63
+ store.tokens.set(token.id, token);
64
+ }
65
+ /**
66
+ * Validate and consume a bootstrap token (single-use)
67
+ *
68
+ * Returns true if token is valid and was consumed, false otherwise
69
+ * Marks token as used to prevent reuse
70
+ */
71
+ export function validateAndConsumeBootstrapToken(params) {
72
+ const { store, tokenId, deviceId } = params;
73
+ const token = store.tokens.get(tokenId);
74
+ if (!token) {
75
+ return false;
76
+ }
77
+ const now = Date.now();
78
+ // Check if already used (single-use enforcement)
79
+ if (token.used) {
80
+ return false;
81
+ }
82
+ // Check if expired
83
+ if (now > token.expiresAt) {
84
+ // Remove expired token
85
+ store.tokens.delete(tokenId);
86
+ return false;
87
+ }
88
+ // Mark as used
89
+ token.used = true;
90
+ token.usedAt = now;
91
+ token.deviceId = deviceId; // Store for audit trail (marked as _deviceId to satisfy linter)
92
+ const _deviceId = deviceId; // Keep for future audit log expansion
93
+ return true;
94
+ }
95
+ /**
96
+ * Validate a bootstrap token without consuming it (for status checks)
97
+ */
98
+ export function validateBootstrapToken(params) {
99
+ const { store, tokenId } = params;
100
+ const token = store.tokens.get(tokenId);
101
+ if (!token) {
102
+ return { valid: false, reason: "token_not_found" };
103
+ }
104
+ const now = Date.now();
105
+ if (token.used) {
106
+ return { valid: false, reason: "token_already_used" };
107
+ }
108
+ if (now > token.expiresAt) {
109
+ return { valid: false, reason: "token_expired" };
110
+ }
111
+ return { valid: true };
112
+ }
113
+ /**
114
+ * Remove a bootstrap token from the store
115
+ */
116
+ export function removeBootstrapToken(store, tokenId) {
117
+ return store.tokens.delete(tokenId);
118
+ }
119
+ /**
120
+ * Cleanup expired and used tokens
121
+ */
122
+ export function cleanupExpiredTokens(store) {
123
+ const now = Date.now();
124
+ let removed = 0;
125
+ for (const [tokenId, token] of store.tokens.entries()) {
126
+ // Remove if expired or used (single-use tokens are one-time only)
127
+ if (now > token.expiresAt || token.used) {
128
+ store.tokens.delete(tokenId);
129
+ removed++;
130
+ }
131
+ }
132
+ return removed;
133
+ }
134
+ /**
135
+ * Get statistics about bootstrap tokens
136
+ */
137
+ export function getBootstrapTokenStats(store) {
138
+ const now = Date.now();
139
+ let unused = 0;
140
+ let used = 0;
141
+ let expired = 0;
142
+ for (const token of store.tokens.values()) {
143
+ if (token.used) {
144
+ used++;
145
+ }
146
+ else if (now > token.expiresAt) {
147
+ expired++;
148
+ }
149
+ else {
150
+ unused++;
151
+ }
152
+ }
153
+ return {
154
+ total: store.tokens.size,
155
+ unused,
156
+ used,
157
+ expired,
158
+ };
159
+ }
160
+ /**
161
+ * Create a secure device pairing session
162
+ */
163
+ export function createSecureDevicePairing(store) {
164
+ const token = createBootstrapToken({
165
+ expiryMs: DEFAULT_EXPIRY_MS,
166
+ purpose: "pairing",
167
+ });
168
+ addBootstrapToken(store, token);
169
+ return {
170
+ setupCode: token.code,
171
+ tokenId: token.id,
172
+ expiresAt: token.expiresAt,
173
+ isSingleUse: true,
174
+ securityLevel: "high",
175
+ };
176
+ }
177
+ /**
178
+ * Validate device pairing request with security checks
179
+ */
180
+ export function validateDevicePairingRequest(params) {
181
+ const { store, tokenId, deviceId, setupCode } = params;
182
+ // First validate the token
183
+ const tokenValidation = validateBootstrapToken({ store, tokenId });
184
+ if (!tokenValidation.valid) {
185
+ return tokenValidation;
186
+ }
187
+ // Get the token and verify setup code
188
+ const token = store.tokens.get(tokenId);
189
+ if (!token) {
190
+ return { valid: false, reason: "token_not_found" };
191
+ }
192
+ // Verify setup code matches
193
+ if (token.code !== setupCode) {
194
+ return { valid: false, reason: "setup_code_mismatch" };
195
+ }
196
+ return { valid: true };
197
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Event Deduplication Layer
3
+ *
4
+ * Prevents duplicate event processing within a configurable time window.
5
+ * Uses LRU-style eviction to prevent memory leaks.
6
+ */
7
+ export class EventDeduplicator {
8
+ seen = new Map();
9
+ TTL_MS;
10
+ MAX_SIZE;
11
+ cleanupInterval = null;
12
+ constructor(options) {
13
+ this.TTL_MS = options?.ttlMs ?? 5000; // 5 segundos default
14
+ this.MAX_SIZE = options?.maxSize ?? 10000;
15
+ if (options?.autoCleanup !== false) {
16
+ this.startCleanupInterval();
17
+ }
18
+ }
19
+ /**
20
+ * Check if event is duplicate and mark as seen
21
+ * @param eventId - Unique event identifier
22
+ * @returns true if duplicate (seen within TTL), false if new
23
+ */
24
+ isDuplicate(eventId) {
25
+ const now = Date.now();
26
+ const existing = this.seen.get(eventId);
27
+ if (existing && now - existing.timestamp < this.TTL_MS) {
28
+ existing.count++;
29
+ return true;
30
+ }
31
+ // Evict oldest if at capacity
32
+ if (this.seen.size >= this.MAX_SIZE) {
33
+ this.evictOldest();
34
+ }
35
+ this.seen.set(eventId, { timestamp: now, count: 1 });
36
+ return false;
37
+ }
38
+ /**
39
+ * Check if event was seen without marking it
40
+ */
41
+ hasSeen(eventId) {
42
+ const existing = this.seen.get(eventId);
43
+ if (!existing)
44
+ return false;
45
+ return Date.now() - existing.timestamp < this.TTL_MS;
46
+ }
47
+ /**
48
+ * Get duplicate count for an event
49
+ */
50
+ getDuplicateCount(eventId) {
51
+ const existing = this.seen.get(eventId);
52
+ return existing?.count ?? 0;
53
+ }
54
+ /**
55
+ * Clear specific event from seen set
56
+ */
57
+ clear(eventId) {
58
+ this.seen.delete(eventId);
59
+ }
60
+ /**
61
+ * Clear all seen events
62
+ */
63
+ clearAll() {
64
+ this.seen.clear();
65
+ }
66
+ /**
67
+ * Get statistics about deduplication
68
+ */
69
+ getStats() {
70
+ let duplicates = 0;
71
+ let oldest = null;
72
+ for (const event of this.seen.values()) {
73
+ if (event.count > 1) {
74
+ duplicates += event.count - 1;
75
+ }
76
+ if (!oldest || event.timestamp < oldest) {
77
+ oldest = event.timestamp;
78
+ }
79
+ }
80
+ return {
81
+ totalEvents: this.seen.size,
82
+ oldestEvent: oldest,
83
+ duplicates,
84
+ };
85
+ }
86
+ /**
87
+ * Cleanup expired events
88
+ */
89
+ cleanup() {
90
+ const now = Date.now();
91
+ let removed = 0;
92
+ for (const [eventId, event] of this.seen.entries()) {
93
+ if (now - event.timestamp > this.TTL_MS) {
94
+ this.seen.delete(eventId);
95
+ removed++;
96
+ }
97
+ }
98
+ return removed;
99
+ }
100
+ /**
101
+ * Get count of active events being tracked
102
+ */
103
+ get activeEventCount() {
104
+ return this.seen.size;
105
+ }
106
+ /**
107
+ * Destroy deduplicator and stop cleanup interval
108
+ */
109
+ destroy() {
110
+ if (this.cleanupInterval) {
111
+ clearInterval(this.cleanupInterval);
112
+ this.cleanupInterval = null;
113
+ }
114
+ this.clearAll();
115
+ }
116
+ startCleanupInterval() {
117
+ // Cleanup a cada 1/4 do TTL
118
+ const cleanupIntervalMs = Math.max(1000, this.TTL_MS / 4);
119
+ this.cleanupInterval = setInterval(() => {
120
+ this.cleanup();
121
+ }, cleanupIntervalMs);
122
+ // Unref para não impedir o processo de sair
123
+ this.cleanupInterval.unref();
124
+ }
125
+ evictOldest() {
126
+ let oldestId = null;
127
+ let oldestTime = Infinity;
128
+ for (const [eventId, event] of this.seen.entries()) {
129
+ if (event.timestamp < oldestTime) {
130
+ oldestTime = event.timestamp;
131
+ oldestId = eventId;
132
+ }
133
+ }
134
+ if (oldestId) {
135
+ this.seen.delete(oldestId);
136
+ }
137
+ }
138
+ }
139
+ /**
140
+ * Generate unique event ID for deduplication
141
+ */
142
+ export function generateEventId(params) {
143
+ const { type, sessionId, runId, payload } = params;
144
+ const parts = [type];
145
+ if (sessionId)
146
+ parts.push(sessionId);
147
+ if (runId)
148
+ parts.push(runId);
149
+ // Include payload hash if available
150
+ if (payload) {
151
+ const hash = quickHash(JSON.stringify(payload));
152
+ parts.push(hash.toString());
153
+ }
154
+ return parts.join(":");
155
+ }
156
+ /**
157
+ * Quick hash function for strings (not cryptographically secure)
158
+ */
159
+ function quickHash(str) {
160
+ let hash = 0;
161
+ for (let i = 0; i < str.length; i++) {
162
+ const char = str.charCodeAt(i);
163
+ hash = (hash << 5) - hash + char;
164
+ hash = hash & hash; // Convert to 32bit integer
165
+ }
166
+ return Math.abs(hash);
167
+ }