@supaku/agentfactory-server 0.7.10 → 0.7.12
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 +4 -0
- package/dist/src/governor-dedup.d.ts +15 -0
- package/dist/src/governor-dedup.d.ts.map +1 -0
- package/dist/src/governor-dedup.js +31 -0
- package/dist/src/governor-event-bus.d.ts +54 -0
- package/dist/src/governor-event-bus.d.ts.map +1 -0
- package/dist/src/governor-event-bus.js +152 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -23,6 +23,10 @@ Requires a Redis instance (works with Redis, Upstash, Vercel KV, or any Redis-co
|
|
|
23
23
|
| **TokenStorage** | OAuth token storage and retrieval |
|
|
24
24
|
| **RateLimit** | Token bucket rate limiting |
|
|
25
25
|
| **WorkerAuth** | API key verification for workers |
|
|
26
|
+
| **RedisEventBus** | Governor event bus backed by Redis Streams (consumer groups, MAXLEN trim) |
|
|
27
|
+
| **RedisEventDeduplicator** | Governor event dedup using SETNX with TTL |
|
|
28
|
+
| **RedisOverrideStorage** | Governor human override state (HOLD, PRIORITY) |
|
|
29
|
+
| **RedisProcessingStateStorage** | Top-of-funnel phase tracking (research, backlog-creation) |
|
|
26
30
|
|
|
27
31
|
## Quick Start
|
|
28
32
|
|
|
@@ -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
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.7.12",
|
|
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
|
|
48
|
-
"@supaku/agentfactory": "0.7.
|
|
47
|
+
"@supaku/agentfactory": "0.7.12",
|
|
48
|
+
"@supaku/agentfactory-linear": "0.7.12"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^22.5.4",
|