@supaku/agentfactory-server 0.7.10 → 0.7.11

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.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Redis Event Deduplicator
3
+ *
4
+ * Production EventDeduplicator backed by Redis SETNX with TTL.
5
+ * Key pattern: `governor:dedup:{dedupKey}`
6
+ */
7
+ import type { EventDeduplicator, EventDeduplicatorConfig } from '@supaku/agentfactory';
8
+ export declare class RedisEventDeduplicator implements EventDeduplicator {
9
+ private readonly windowMs;
10
+ private readonly keyPrefix;
11
+ constructor(config?: Partial<EventDeduplicatorConfig>, keyPrefix?: string);
12
+ isDuplicate(key: string): Promise<boolean>;
13
+ clear(): Promise<void>;
14
+ }
15
+ //# sourceMappingURL=governor-dedup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"governor-dedup.d.ts","sourceRoot":"","sources":["../../src/governor-dedup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AAQtF,qBAAa,sBAAuB,YAAW,iBAAiB;IAC9D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;gBAGhC,MAAM,GAAE,OAAO,CAAC,uBAAuB,CAAM,EAC7C,SAAS,SAAmB;IAMxB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAK7B"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Redis Event Deduplicator
3
+ *
4
+ * Production EventDeduplicator backed by Redis SETNX with TTL.
5
+ * Key pattern: `governor:dedup:{dedupKey}`
6
+ */
7
+ import { DEFAULT_DEDUP_CONFIG } from '@supaku/agentfactory';
8
+ import { redisSetNX } from './redis.js';
9
+ // ---------------------------------------------------------------------------
10
+ // RedisEventDeduplicator
11
+ // ---------------------------------------------------------------------------
12
+ export class RedisEventDeduplicator {
13
+ windowMs;
14
+ keyPrefix;
15
+ constructor(config = {}, keyPrefix = 'governor:dedup') {
16
+ this.windowMs = config.windowMs ?? DEFAULT_DEDUP_CONFIG.windowMs;
17
+ this.keyPrefix = keyPrefix;
18
+ }
19
+ async isDuplicate(key) {
20
+ const redisKey = `${this.keyPrefix}:${key}`;
21
+ const ttlSeconds = Math.max(1, Math.ceil(this.windowMs / 1000));
22
+ // SETNX returns true if key was newly set (not a duplicate)
23
+ const wasSet = await redisSetNX(redisKey, '1', ttlSeconds);
24
+ return !wasSet; // if we couldn't set, it's a duplicate
25
+ }
26
+ async clear() {
27
+ // In production, keys auto-expire via TTL.
28
+ // Explicit clear is mainly for testing — not implemented for Redis
29
+ // since test suites should use InMemoryEventDeduplicator.
30
+ }
31
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Redis Event Bus
3
+ *
4
+ * Production GovernorEventBus backed by Redis Streams.
5
+ * Uses a consumer group for reliable delivery and horizontal scaling.
6
+ *
7
+ * Stream: `governor:events`
8
+ * Consumer group: `governor-group`
9
+ * MAXLEN: 10,000 (approximate trim)
10
+ */
11
+ import type { GovernorEvent, GovernorEventBus } from '@supaku/agentfactory';
12
+ export interface RedisEventBusConfig {
13
+ /** Redis stream key (default: 'governor:events') */
14
+ streamKey?: string;
15
+ /** Consumer group name (default: 'governor-group') */
16
+ groupName?: string;
17
+ /** Consumer name within the group (default: hostname or random) */
18
+ consumerName?: string;
19
+ /** Max stream length — approximate trim (default: 10000) */
20
+ maxLen?: number;
21
+ /** Block timeout in milliseconds for XREADGROUP (default: 5000) */
22
+ blockMs?: number;
23
+ }
24
+ export declare class RedisEventBus implements GovernorEventBus {
25
+ private readonly streamKey;
26
+ private readonly groupName;
27
+ private readonly consumerName;
28
+ private readonly maxLen;
29
+ private readonly blockMs;
30
+ private closed;
31
+ private groupCreated;
32
+ constructor(config?: RedisEventBusConfig);
33
+ /**
34
+ * Ensure the consumer group exists. Idempotent — safe to call multiple times.
35
+ */
36
+ private ensureGroup;
37
+ publish(event: GovernorEvent): Promise<string>;
38
+ subscribe(): AsyncGenerator<{
39
+ id: string;
40
+ event: GovernorEvent;
41
+ }>;
42
+ ack(eventId: string): Promise<void>;
43
+ close(): Promise<void>;
44
+ /**
45
+ * Re-deliver pending messages (claimed but not acked from a previous run).
46
+ */
47
+ private readPending;
48
+ /**
49
+ * Parse a Redis stream message fields array into a GovernorEvent.
50
+ * Fields come as [key1, val1, key2, val2, ...].
51
+ */
52
+ private parseEvent;
53
+ }
54
+ //# sourceMappingURL=governor-event-bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"governor-event-bus.d.ts","sourceRoot":"","sources":["../../src/governor-event-bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAO3E,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAWD,qBAAa,aAAc,YAAW,gBAAgB;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAQ;gBAEhB,MAAM,GAAE,mBAAwB;IAQ5C;;OAEG;YACW,WAAW;IAenB,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IA0B7C,SAAS,IAAI,cAAc,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC;IAiDlE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B;;OAEG;YACY,WAAW;IAiC1B;;;OAGG;IACH,OAAO,CAAC,UAAU;CAanB"}
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Redis Event Bus
3
+ *
4
+ * Production GovernorEventBus backed by Redis Streams.
5
+ * Uses a consumer group for reliable delivery and horizontal scaling.
6
+ *
7
+ * Stream: `governor:events`
8
+ * Consumer group: `governor-group`
9
+ * MAXLEN: 10,000 (approximate trim)
10
+ */
11
+ import { getRedisClient } from './redis.js';
12
+ const DEFAULT_STREAM_KEY = 'governor:events';
13
+ const DEFAULT_GROUP_NAME = 'governor-group';
14
+ const DEFAULT_MAX_LEN = 10_000;
15
+ const DEFAULT_BLOCK_MS = 5_000;
16
+ // ---------------------------------------------------------------------------
17
+ // RedisEventBus
18
+ // ---------------------------------------------------------------------------
19
+ export class RedisEventBus {
20
+ streamKey;
21
+ groupName;
22
+ consumerName;
23
+ maxLen;
24
+ blockMs;
25
+ closed = false;
26
+ groupCreated = false;
27
+ constructor(config = {}) {
28
+ this.streamKey = config.streamKey ?? DEFAULT_STREAM_KEY;
29
+ this.groupName = config.groupName ?? DEFAULT_GROUP_NAME;
30
+ this.consumerName = config.consumerName ?? `governor-${process.pid}-${Date.now()}`;
31
+ this.maxLen = config.maxLen ?? DEFAULT_MAX_LEN;
32
+ this.blockMs = config.blockMs ?? DEFAULT_BLOCK_MS;
33
+ }
34
+ /**
35
+ * Ensure the consumer group exists. Idempotent — safe to call multiple times.
36
+ */
37
+ async ensureGroup() {
38
+ if (this.groupCreated)
39
+ return;
40
+ const redis = getRedisClient();
41
+ try {
42
+ await redis.xgroup('CREATE', this.streamKey, this.groupName, '0', 'MKSTREAM');
43
+ }
44
+ catch (err) {
45
+ // BUSYGROUP = group already exists, which is fine
46
+ if (err instanceof Error && !err.message.includes('BUSYGROUP')) {
47
+ throw err;
48
+ }
49
+ }
50
+ this.groupCreated = true;
51
+ }
52
+ async publish(event) {
53
+ if (this.closed) {
54
+ throw new Error('Event bus is closed');
55
+ }
56
+ const redis = getRedisClient();
57
+ const payload = JSON.stringify(event);
58
+ // XADD with approximate MAXLEN trim
59
+ const id = await redis.xadd(this.streamKey, 'MAXLEN', '~', String(this.maxLen), '*', 'data', payload);
60
+ if (!id) {
61
+ throw new Error('XADD returned null — stream write failed');
62
+ }
63
+ return id;
64
+ }
65
+ async *subscribe() {
66
+ await this.ensureGroup();
67
+ const redis = getRedisClient();
68
+ // First, re-deliver any pending messages (from previous crash)
69
+ yield* this.readPending(redis);
70
+ // Then read new messages
71
+ while (!this.closed) {
72
+ try {
73
+ const results = await redis.xreadgroup('GROUP', this.groupName, this.consumerName, 'COUNT', '1', 'BLOCK', this.blockMs, 'STREAMS', this.streamKey, '>');
74
+ if (!results || results.length === 0) {
75
+ continue; // timeout, loop back
76
+ }
77
+ for (const [, messages] of results) {
78
+ for (const [id, fields] of messages) {
79
+ const event = this.parseEvent(fields);
80
+ if (event) {
81
+ yield { id, event };
82
+ }
83
+ else {
84
+ // Malformed event — ack and skip
85
+ await this.ack(id);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ catch (err) {
91
+ if (this.closed)
92
+ return;
93
+ // Log and continue on transient errors
94
+ console.error('[redis-event-bus] Error reading from stream:', err);
95
+ // Brief backoff before retry
96
+ await new Promise((r) => setTimeout(r, 1000));
97
+ }
98
+ }
99
+ }
100
+ async ack(eventId) {
101
+ const redis = getRedisClient();
102
+ await redis.xack(this.streamKey, this.groupName, eventId);
103
+ }
104
+ async close() {
105
+ this.closed = true;
106
+ }
107
+ // ---- Internal ----
108
+ /**
109
+ * Re-deliver pending messages (claimed but not acked from a previous run).
110
+ */
111
+ async *readPending(redis) {
112
+ try {
113
+ const results = await redis.xreadgroup('GROUP', this.groupName, this.consumerName, 'COUNT', '100', 'STREAMS', this.streamKey, '0');
114
+ if (!results || results.length === 0)
115
+ return;
116
+ for (const [, messages] of results) {
117
+ for (const [id, fields] of messages) {
118
+ if (!fields || fields.length === 0)
119
+ continue; // already acked
120
+ const event = this.parseEvent(fields);
121
+ if (event) {
122
+ yield { id, event };
123
+ }
124
+ else {
125
+ await this.ack(id);
126
+ }
127
+ }
128
+ }
129
+ }
130
+ catch (err) {
131
+ console.error('[redis-event-bus] Error reading pending messages:', err);
132
+ }
133
+ }
134
+ /**
135
+ * Parse a Redis stream message fields array into a GovernorEvent.
136
+ * Fields come as [key1, val1, key2, val2, ...].
137
+ */
138
+ parseEvent(fields) {
139
+ try {
140
+ // Find the 'data' field
141
+ for (let i = 0; i < fields.length; i += 2) {
142
+ if (fields[i] === 'data') {
143
+ return JSON.parse(fields[i + 1]);
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ }
152
+ }
@@ -16,4 +16,6 @@ export * from './token-storage.js';
16
16
  export * from './env-validation.js';
17
17
  export * from './governor-storage.js';
18
18
  export * from './processing-state-storage.js';
19
+ export * from './governor-event-bus.js';
20
+ export * from './governor-dedup.js';
19
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAG3B,cAAc,YAAY,CAAA;AAG1B,cAAc,YAAY,CAAA;AAG1B,cAAc,sBAAsB,CAAA;AAGpC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,qBAAqB,CAAA;AAGnC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,qBAAqB,CAAA;AAGnC,cAAc,0BAA0B,CAAA;AAGxC,cAAc,sBAAsB,CAAA;AAGpC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,kBAAkB,CAAA;AAGhC,cAAc,mBAAmB,CAAA;AAGjC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,oBAAoB,CAAA;AAGlC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,uBAAuB,CAAA;AAGrC,cAAc,+BAA+B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAG3B,cAAc,YAAY,CAAA;AAG1B,cAAc,YAAY,CAAA;AAG1B,cAAc,sBAAsB,CAAA;AAGpC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,qBAAqB,CAAA;AAGnC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,qBAAqB,CAAA;AAGnC,cAAc,0BAA0B,CAAA;AAGxC,cAAc,sBAAsB,CAAA;AAGpC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,kBAAkB,CAAA;AAGhC,cAAc,mBAAmB,CAAA;AAGjC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,oBAAoB,CAAA;AAGlC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,uBAAuB,CAAA;AAGrC,cAAc,+BAA+B,CAAA;AAG7C,cAAc,yBAAyB,CAAA;AAGvC,cAAc,qBAAqB,CAAA"}
package/dist/src/index.js CHANGED
@@ -34,3 +34,7 @@ export * from './env-validation.js';
34
34
  export * from './governor-storage.js';
35
35
  // Processing state storage (Redis-backed top-of-funnel phase tracking)
36
36
  export * from './processing-state-storage.js';
37
+ // Governor event bus (Redis Streams)
38
+ export * from './governor-event-bus.js';
39
+ // Governor event deduplicator (Redis SETNX)
40
+ export * from './governor-dedup.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supaku/agentfactory-server",
3
- "version": "0.7.10",
3
+ "version": "0.7.11",
4
4
  "type": "module",
5
5
  "description": "Webhook server and distributed worker pool for AgentFactory — Redis queues, issue locks, session management",
6
6
  "author": "Supaku (https://supaku.com)",
@@ -44,8 +44,8 @@
44
44
  ],
45
45
  "dependencies": {
46
46
  "ioredis": "^5.4.2",
47
- "@supaku/agentfactory-linear": "0.7.10",
48
- "@supaku/agentfactory": "0.7.10"
47
+ "@supaku/agentfactory": "0.7.11",
48
+ "@supaku/agentfactory-linear": "0.7.11"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^22.5.4",