@advicenxt/sbp-server 0.1.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.
Files changed (63) hide show
  1. package/benchmarks/bench.ts +272 -0
  2. package/dist/auth.d.ts +20 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +69 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/blackboard.d.ts +84 -0
  7. package/dist/blackboard.d.ts.map +1 -0
  8. package/dist/blackboard.js +502 -0
  9. package/dist/blackboard.js.map +1 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +102 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/conditions.d.ts +27 -0
  15. package/dist/conditions.d.ts.map +1 -0
  16. package/dist/conditions.js +240 -0
  17. package/dist/conditions.js.map +1 -0
  18. package/dist/decay.d.ts +21 -0
  19. package/dist/decay.d.ts.map +1 -0
  20. package/dist/decay.js +88 -0
  21. package/dist/decay.js.map +1 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +13 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/rate-limiter.d.ts +21 -0
  27. package/dist/rate-limiter.d.ts.map +1 -0
  28. package/dist/rate-limiter.js +75 -0
  29. package/dist/rate-limiter.js.map +1 -0
  30. package/dist/server.d.ts +63 -0
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +401 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/store.d.ts +54 -0
  35. package/dist/store.d.ts.map +1 -0
  36. package/dist/store.js +55 -0
  37. package/dist/store.js.map +1 -0
  38. package/dist/types.d.ts +247 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +26 -0
  41. package/dist/types.js.map +1 -0
  42. package/dist/validation.d.ts +296 -0
  43. package/dist/validation.d.ts.map +1 -0
  44. package/dist/validation.js +205 -0
  45. package/dist/validation.js.map +1 -0
  46. package/eslint.config.js +26 -0
  47. package/package.json +66 -0
  48. package/src/auth.ts +89 -0
  49. package/src/blackboard.test.ts +287 -0
  50. package/src/blackboard.ts +651 -0
  51. package/src/cli.ts +116 -0
  52. package/src/conditions.ts +305 -0
  53. package/src/conformance.test.ts +686 -0
  54. package/src/decay.ts +103 -0
  55. package/src/index.ts +24 -0
  56. package/src/rate-limiter.ts +104 -0
  57. package/src/server.integration.test.ts +436 -0
  58. package/src/server.ts +500 -0
  59. package/src/store.ts +108 -0
  60. package/src/types.ts +314 -0
  61. package/src/validation.ts +251 -0
  62. package/tsconfig.eslint.json +5 -0
  63. package/tsconfig.json +20 -0
@@ -0,0 +1,651 @@
1
+ /**
2
+ * SBP Blackboard - Core State Management
3
+ */
4
+
5
+ import { v7 as uuidv7 } from "uuid";
6
+ import type { PheromoneStore } from "./store.js";
7
+ import { MemoryStore } from "./store.js";
8
+ import type {
9
+ Pheromone,
10
+ PheromoneSnapshot,
11
+ Scent,
12
+ DecayModel,
13
+ EmitParams,
14
+ EmitResult,
15
+ SniffParams,
16
+ SniffResult,
17
+ AggregateStats,
18
+ RegisterScentParams,
19
+ RegisterScentResult,
20
+ DeregisterScentParams,
21
+ DeregisterScentResult,
22
+ EvaporateParams,
23
+ EvaporateResult,
24
+ InspectParams,
25
+ InspectResult,
26
+ TriggerPayload,
27
+ TagFilter,
28
+ } from "./types.js";
29
+ import { computeIntensity, isEvaporated, defaultDecay } from "./decay.js";
30
+ import { evaluateCondition, createSnapshot } from "./conditions.js";
31
+ import { createHash } from "crypto";
32
+
33
+ export interface BlackboardOptions {
34
+ /** Interval for scent evaluation in ms (default: 100) */
35
+ evaluationInterval?: number;
36
+ /** Default decay model for new pheromones */
37
+ defaultDecay?: DecayModel;
38
+ /** Default TTL floor for evaporation */
39
+ defaultTtlFloor?: number;
40
+ /** Maximum pheromones before GC triggers */
41
+ maxPheromones?: number;
42
+ /** Enable emission history tracking for rate conditions */
43
+ trackEmissionHistory?: boolean;
44
+ /** How long to keep emission history (ms) */
45
+ emissionHistoryWindow?: number;
46
+ /** Pluggable pheromone storage backend (default: MemoryStore) */
47
+ store?: PheromoneStore;
48
+ }
49
+
50
+ export interface TriggerHandler {
51
+ (payload: TriggerPayload): Promise<void>;
52
+ }
53
+
54
+ export class Blackboard {
55
+ private store: PheromoneStore;
56
+ private scents = new Map<string, Scent>();
57
+ private triggerHandlers = new Map<string, TriggerHandler>();
58
+ private emissionHistory: Array<{ trail: string; type: string; timestamp: number }> = [];
59
+ private evaluationTimer: ReturnType<typeof setInterval> | null = null;
60
+ private startTime = Date.now();
61
+
62
+ private options: Omit<Required<BlackboardOptions>, "store">;
63
+
64
+ constructor(options: BlackboardOptions = {}) {
65
+ this.store = options.store ?? new MemoryStore();
66
+ this.options = {
67
+ evaluationInterval: options.evaluationInterval ?? 100,
68
+ defaultDecay: options.defaultDecay ?? defaultDecay(),
69
+ defaultTtlFloor: options.defaultTtlFloor ?? 0.01,
70
+ maxPheromones: options.maxPheromones ?? 100000,
71
+ trackEmissionHistory: options.trackEmissionHistory ?? true,
72
+ emissionHistoryWindow: options.emissionHistoryWindow ?? 60000,
73
+ };
74
+ }
75
+
76
+ // ==========================================================================
77
+ // EMIT
78
+ // ==========================================================================
79
+
80
+ emit(params: EmitParams): EmitResult {
81
+ const now = Date.now();
82
+ const {
83
+ trail,
84
+ type,
85
+ intensity,
86
+ decay = this.options.defaultDecay,
87
+ payload = {},
88
+ tags = [],
89
+ merge_strategy = "reinforce",
90
+ source_agent,
91
+ } = params;
92
+
93
+ // Validate intensity
94
+ const clampedIntensity = Math.max(0, Math.min(1, intensity));
95
+
96
+ // Track emission for rate conditions
97
+ if (this.options.trackEmissionHistory) {
98
+ this.emissionHistory.push({ trail, type, timestamp: now });
99
+ this.pruneEmissionHistory(now);
100
+ }
101
+
102
+ // Generate payload hash for matching
103
+ const payloadHash = this.hashPayload(payload);
104
+
105
+ // Find existing pheromone for merge
106
+ let existing: Pheromone | undefined;
107
+ if (merge_strategy !== "new") {
108
+ for (const p of this.store.values()) {
109
+ if (
110
+ p.trail === trail &&
111
+ p.type === type &&
112
+ this.hashPayload(p.payload) === payloadHash &&
113
+ !isEvaporated(p, now)
114
+ ) {
115
+ existing = p;
116
+ break;
117
+ }
118
+ }
119
+ }
120
+
121
+ if (existing) {
122
+ const previousIntensity = computeIntensity(existing, now);
123
+ let action: EmitResult["action"] = "reinforced";
124
+
125
+ switch (merge_strategy) {
126
+ case "reinforce":
127
+ existing.initial_intensity = clampedIntensity;
128
+ existing.last_reinforced_at = now;
129
+ action = "reinforced";
130
+ break;
131
+
132
+ case "replace":
133
+ existing.initial_intensity = clampedIntensity;
134
+ existing.last_reinforced_at = now;
135
+ existing.payload = payload;
136
+ existing.tags = tags;
137
+ if (source_agent) existing.source_agent = source_agent;
138
+ action = "replaced";
139
+ break;
140
+
141
+ case "max":
142
+ existing.initial_intensity = Math.max(previousIntensity, clampedIntensity);
143
+ existing.last_reinforced_at = now;
144
+ action = "merged";
145
+ break;
146
+
147
+ case "add":
148
+ existing.initial_intensity = Math.min(1, previousIntensity + clampedIntensity);
149
+ existing.last_reinforced_at = now;
150
+ action = "merged";
151
+ break;
152
+ }
153
+
154
+ return {
155
+ pheromone_id: existing.id,
156
+ action,
157
+ previous_intensity: previousIntensity,
158
+ new_intensity: computeIntensity(existing, now),
159
+ };
160
+ }
161
+
162
+ // Create new pheromone
163
+ const id = uuidv7();
164
+ const pheromone: Pheromone = {
165
+ id,
166
+ trail,
167
+ type,
168
+ emitted_at: now,
169
+ last_reinforced_at: now,
170
+ initial_intensity: clampedIntensity,
171
+ decay_model: decay,
172
+ payload,
173
+ source_agent,
174
+ tags,
175
+ ttl_floor: this.options.defaultTtlFloor,
176
+ };
177
+
178
+ this.store.set(id, pheromone);
179
+
180
+ // Trigger GC if needed
181
+ if (this.store.size > this.options.maxPheromones) {
182
+ this.gc();
183
+ }
184
+
185
+ return {
186
+ pheromone_id: id,
187
+ action: "created",
188
+ new_intensity: clampedIntensity,
189
+ };
190
+ }
191
+
192
+ // ==========================================================================
193
+ // SNIFF
194
+ // ==========================================================================
195
+
196
+ sniff(params: SniffParams = {}): SniffResult {
197
+ const now = Date.now();
198
+ const {
199
+ trails,
200
+ types,
201
+ min_intensity = 0,
202
+ max_age_ms,
203
+ tags,
204
+ limit = 100,
205
+ include_evaporated = false,
206
+ } = params;
207
+
208
+ const results: PheromoneSnapshot[] = [];
209
+ const aggregates = new Map<string, { count: number; sum: number; max: number }>();
210
+
211
+ for (const p of this.store.values()) {
212
+ // Filter by trail
213
+ if (trails && trails.length > 0 && !trails.includes(p.trail)) continue;
214
+
215
+ // Filter by type
216
+ if (types && types.length > 0 && !types.includes(p.type)) continue;
217
+
218
+ const intensity = computeIntensity(p, now);
219
+
220
+ // Filter evaporated
221
+ if (!include_evaporated && intensity < p.ttl_floor) continue;
222
+
223
+ // Filter by min intensity
224
+ if (intensity < min_intensity) continue;
225
+
226
+ // Filter by max age
227
+ if (max_age_ms !== undefined && now - p.emitted_at > max_age_ms) continue;
228
+
229
+ // Filter by tags
230
+ if (tags && !this.matchTags(p.tags, tags)) continue;
231
+
232
+ results.push(createSnapshot(p, now));
233
+
234
+ // Aggregate
235
+ const key = `${p.trail}/${p.type}`;
236
+ const agg = aggregates.get(key) || { count: 0, sum: 0, max: 0 };
237
+ agg.count++;
238
+ agg.sum += intensity;
239
+ agg.max = Math.max(agg.max, intensity);
240
+ aggregates.set(key, agg);
241
+ }
242
+
243
+ // Sort by intensity descending
244
+ results.sort((a, b) => b.current_intensity - a.current_intensity);
245
+
246
+ // Build aggregates result
247
+ const aggregatesResult: Record<string, AggregateStats> = {};
248
+ for (const [key, agg] of aggregates) {
249
+ aggregatesResult[key] = {
250
+ count: agg.count,
251
+ sum_intensity: agg.sum,
252
+ max_intensity: agg.max,
253
+ avg_intensity: agg.count > 0 ? agg.sum / agg.count : 0,
254
+ };
255
+ }
256
+
257
+ return {
258
+ timestamp: now,
259
+ pheromones: results.slice(0, limit),
260
+ aggregates: aggregatesResult,
261
+ };
262
+ }
263
+
264
+ // ==========================================================================
265
+ // REGISTER_SCENT
266
+ // ==========================================================================
267
+
268
+ registerScent(params: RegisterScentParams): RegisterScentResult {
269
+ const {
270
+ scent_id,
271
+ agent_endpoint,
272
+ condition,
273
+ cooldown_ms = 0,
274
+ activation_payload = {},
275
+ trigger_mode = "level",
276
+ hysteresis = 0,
277
+ max_execution_ms = 30000,
278
+ context_trails,
279
+ } = params;
280
+
281
+ const isUpdate = this.scents.has(scent_id);
282
+
283
+ const scent: Scent = {
284
+ scent_id,
285
+ agent_endpoint,
286
+ condition,
287
+ cooldown_ms,
288
+ activation_payload,
289
+ trigger_mode,
290
+ hysteresis,
291
+ max_execution_ms,
292
+ last_triggered_at: null,
293
+ last_condition_met: false,
294
+ context_trails,
295
+ };
296
+
297
+ this.scents.set(scent_id, scent);
298
+
299
+ // Evaluate current state
300
+ const now = Date.now();
301
+ const evalResult = evaluateCondition(condition, {
302
+ pheromones: [...this.store.values()],
303
+ now,
304
+ emissionHistory: this.emissionHistory,
305
+ });
306
+
307
+ return {
308
+ scent_id,
309
+ status: isUpdate ? "updated" : "registered",
310
+ current_condition_state: {
311
+ met: evalResult.met,
312
+ },
313
+ };
314
+ }
315
+
316
+ // ==========================================================================
317
+ // DEREGISTER_SCENT
318
+ // ==========================================================================
319
+
320
+ deregisterScent(params: DeregisterScentParams): DeregisterScentResult {
321
+ const { scent_id } = params;
322
+
323
+ if (this.scents.has(scent_id)) {
324
+ this.scents.delete(scent_id);
325
+ this.triggerHandlers.delete(scent_id);
326
+ return { scent_id, status: "deregistered" };
327
+ }
328
+
329
+ return { scent_id, status: "not_found" };
330
+ }
331
+
332
+ // ==========================================================================
333
+ // EVAPORATE
334
+ // ==========================================================================
335
+
336
+ evaporate(params: EvaporateParams = {}): EvaporateResult {
337
+ const now = Date.now();
338
+ const { trail, types, older_than_ms, below_intensity, tags } = params;
339
+
340
+ const toRemove: string[] = [];
341
+ const trailsAffected = new Set<string>();
342
+
343
+ for (const [id, p] of this.store.entries()) {
344
+ if (trail && p.trail !== trail) continue;
345
+ if (types && types.length > 0 && !types.includes(p.type)) continue;
346
+ if (older_than_ms !== undefined && now - p.emitted_at < older_than_ms) continue;
347
+ if (below_intensity !== undefined && computeIntensity(p, now) >= below_intensity) continue;
348
+ if (tags && !this.matchTags(p.tags, tags)) continue;
349
+
350
+ toRemove.push(id);
351
+ trailsAffected.add(p.trail);
352
+ }
353
+
354
+ for (const id of toRemove) {
355
+ this.store.delete(id);
356
+ }
357
+
358
+ return {
359
+ evaporated_count: toRemove.length,
360
+ trails_affected: [...trailsAffected],
361
+ };
362
+ }
363
+
364
+ // ==========================================================================
365
+ // INSPECT
366
+ // ==========================================================================
367
+
368
+ inspect(params: InspectParams = {}): InspectResult {
369
+ const now = Date.now();
370
+ const include = params.include ?? ["trails", "scents", "stats"];
371
+ const result: InspectResult = { timestamp: now };
372
+
373
+ if (include.includes("trails")) {
374
+ const trailMap = new Map<string, { count: number; intensity: number }>();
375
+
376
+ for (const p of this.store.values()) {
377
+ if (isEvaporated(p, now)) continue;
378
+
379
+ const current = trailMap.get(p.trail) || { count: 0, intensity: 0 };
380
+ current.count++;
381
+ current.intensity += computeIntensity(p, now);
382
+ trailMap.set(p.trail, current);
383
+ }
384
+
385
+ result.trails = [...trailMap.entries()].map(([name, data]) => ({
386
+ name,
387
+ pheromone_count: data.count,
388
+ total_intensity: data.intensity,
389
+ avg_intensity: data.count > 0 ? data.intensity / data.count : 0,
390
+ }));
391
+ }
392
+
393
+ if (include.includes("scents")) {
394
+ result.scents = [...this.scents.values()].map((s) => ({
395
+ scent_id: s.scent_id,
396
+ agent_endpoint: s.agent_endpoint,
397
+ condition_met: s.last_condition_met,
398
+ in_cooldown: s.last_triggered_at
399
+ ? now - s.last_triggered_at < s.cooldown_ms
400
+ : false,
401
+ last_triggered_at: s.last_triggered_at,
402
+ }));
403
+ }
404
+
405
+ if (include.includes("stats")) {
406
+ let activeCount = 0;
407
+ for (const p of this.store.values()) {
408
+ if (!isEvaporated(p, now)) activeCount++;
409
+ }
410
+
411
+ result.stats = {
412
+ total_pheromones: this.store.size,
413
+ active_pheromones: activeCount,
414
+ total_scents: this.scents.size,
415
+ uptime_ms: now - this.startTime,
416
+ };
417
+ }
418
+
419
+ return result;
420
+ }
421
+
422
+ // ==========================================================================
423
+ // TRIGGER HANDLING
424
+ // ==========================================================================
425
+
426
+ /**
427
+ * Register a local handler for a scent
428
+ */
429
+ onTrigger(scentId: string, handler: TriggerHandler): void {
430
+ this.triggerHandlers.set(scentId, handler);
431
+ }
432
+
433
+ /**
434
+ * Remove a local trigger handler
435
+ */
436
+ offTrigger(scentId: string): void {
437
+ this.triggerHandlers.delete(scentId);
438
+ }
439
+
440
+ // ==========================================================================
441
+ // EVALUATION LOOP
442
+ // ==========================================================================
443
+
444
+ /**
445
+ * Start the background scent evaluation loop
446
+ */
447
+ start(): void {
448
+ if (this.evaluationTimer) return;
449
+
450
+ this.evaluationTimer = setInterval(() => {
451
+ this.evaluateScents().catch((err) => {
452
+ console.error("[SBP] Evaluation error:", err);
453
+ });
454
+ }, this.options.evaluationInterval);
455
+ }
456
+
457
+ /**
458
+ * Stop the background evaluation loop
459
+ */
460
+ stop(): void {
461
+ if (this.evaluationTimer) {
462
+ clearInterval(this.evaluationTimer);
463
+ this.evaluationTimer = null;
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Evaluate all scents and trigger as needed
469
+ */
470
+ async evaluateScents(): Promise<void> {
471
+ const now = Date.now();
472
+ const pheromones = [...this.store.values()];
473
+
474
+ for (const scent of this.scents.values()) {
475
+ // Check cooldown
476
+ if (scent.last_triggered_at && now - scent.last_triggered_at < scent.cooldown_ms) {
477
+ continue;
478
+ }
479
+
480
+ const evalResult = evaluateCondition(scent.condition, {
481
+ pheromones,
482
+ now,
483
+ emissionHistory: this.emissionHistory,
484
+ });
485
+
486
+ const shouldTrigger = this.shouldTrigger(scent, evalResult.met, now);
487
+ scent.last_condition_met = evalResult.met;
488
+
489
+ if (shouldTrigger) {
490
+ scent.last_triggered_at = now;
491
+ await this.dispatchTrigger(scent, evalResult, now);
492
+ }
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Determine if a scent should trigger based on mode
498
+ */
499
+ private shouldTrigger(scent: Scent, conditionMet: boolean, _now: number): boolean {
500
+ switch (scent.trigger_mode) {
501
+ case "level":
502
+ return conditionMet;
503
+
504
+ case "edge_rising":
505
+ return conditionMet && !scent.last_condition_met;
506
+
507
+ case "edge_falling":
508
+ return !conditionMet && scent.last_condition_met;
509
+
510
+ default:
511
+ return conditionMet;
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Dispatch a trigger to the agent
517
+ */
518
+ private async dispatchTrigger(
519
+ scent: Scent,
520
+ evalResult: { met: boolean; value: number; matchingPheromoneIds: string[] },
521
+ now: number
522
+ ): Promise<void> {
523
+ // Build context pheromones
524
+ const contextTrails = scent.context_trails || [];
525
+ const contextPheromones: PheromoneSnapshot[] = [];
526
+
527
+ if (contextTrails.length > 0) {
528
+ for (const p of this.store.values()) {
529
+ if (contextTrails.includes(p.trail) && !isEvaporated(p, now)) {
530
+ contextPheromones.push(createSnapshot(p, now));
531
+ }
532
+ }
533
+ } else {
534
+ // Include matching pheromones
535
+ for (const id of evalResult.matchingPheromoneIds) {
536
+ const p = this.store.get(id);
537
+ if (p) {
538
+ contextPheromones.push(createSnapshot(p, now));
539
+ }
540
+ }
541
+ }
542
+
543
+ const payload: TriggerPayload = {
544
+ scent_id: scent.scent_id,
545
+ triggered_at: now,
546
+ condition_snapshot: {
547
+ [scent.scent_id]: {
548
+ value: evalResult.value,
549
+ pheromone_ids: evalResult.matchingPheromoneIds,
550
+ },
551
+ },
552
+ context_pheromones: contextPheromones,
553
+ activation_payload: scent.activation_payload,
554
+ };
555
+
556
+ // Try local handler first
557
+ const localHandler = this.triggerHandlers.get(scent.scent_id);
558
+ if (localHandler) {
559
+ try {
560
+ await localHandler(payload);
561
+ } catch (err) {
562
+ console.error(`[SBP] Trigger handler error for ${scent.scent_id}:`, err);
563
+ }
564
+ return;
565
+ }
566
+
567
+ // Otherwise, HTTP POST to endpoint
568
+ try {
569
+ const response = await fetch(scent.agent_endpoint, {
570
+ method: "POST",
571
+ headers: {
572
+ "Content-Type": "application/json",
573
+ "X-SBP-Version": "0.1",
574
+ },
575
+ body: JSON.stringify({
576
+ jsonrpc: "2.0",
577
+ method: "sbp/trigger",
578
+ params: payload,
579
+ }),
580
+ signal: AbortSignal.timeout(scent.max_execution_ms),
581
+ });
582
+
583
+ if (!response.ok) {
584
+ console.error(`[SBP] Trigger failed for ${scent.scent_id}: ${response.status}`);
585
+ }
586
+ } catch (err) {
587
+ console.error(`[SBP] Trigger dispatch error for ${scent.scent_id}:`, err);
588
+ }
589
+ }
590
+
591
+ // ==========================================================================
592
+ // UTILITIES
593
+ // ==========================================================================
594
+
595
+ /**
596
+ * Garbage collect evaporated pheromones
597
+ */
598
+ gc(): number {
599
+ const now = Date.now();
600
+ const toRemove: string[] = [];
601
+
602
+ for (const [id, p] of this.store.entries()) {
603
+ if (isEvaporated(p, now)) {
604
+ toRemove.push(id);
605
+ }
606
+ }
607
+
608
+ for (const id of toRemove) {
609
+ this.store.delete(id);
610
+ }
611
+
612
+ return toRemove.length;
613
+ }
614
+
615
+ /**
616
+ * Get raw pheromone count (for testing)
617
+ */
618
+ get size(): number {
619
+ return this.store.size;
620
+ }
621
+
622
+ /**
623
+ * Get scent count
624
+ */
625
+ get scentCount(): number {
626
+ return this.scents.size;
627
+ }
628
+
629
+ private hashPayload(payload: Record<string, unknown>): string {
630
+ const content = JSON.stringify(payload, Object.keys(payload).sort());
631
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
632
+ }
633
+
634
+ private matchTags(tags: string[], filter: TagFilter): boolean {
635
+ if (filter.any && filter.any.length > 0) {
636
+ if (!filter.any.some((t) => tags.includes(t))) return false;
637
+ }
638
+ if (filter.all && filter.all.length > 0) {
639
+ if (!filter.all.every((t) => tags.includes(t))) return false;
640
+ }
641
+ if (filter.none && filter.none.length > 0) {
642
+ if (filter.none.some((t) => tags.includes(t))) return false;
643
+ }
644
+ return true;
645
+ }
646
+
647
+ private pruneEmissionHistory(now: number): void {
648
+ const cutoff = now - this.options.emissionHistoryWindow;
649
+ this.emissionHistory = this.emissionHistory.filter((e) => e.timestamp >= cutoff);
650
+ }
651
+ }