@supaku/agentfactory-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.
- package/LICENSE +21 -0
- package/dist/src/agent-tracking.d.ts +98 -0
- package/dist/src/agent-tracking.d.ts.map +1 -0
- package/dist/src/agent-tracking.js +185 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +16 -0
- package/dist/src/issue-lock.d.ts +126 -0
- package/dist/src/issue-lock.d.ts.map +1 -0
- package/dist/src/issue-lock.js +499 -0
- package/dist/src/redis.d.ts +146 -0
- package/dist/src/redis.d.ts.map +1 -0
- package/dist/src/redis.js +343 -0
- package/dist/src/session-storage.d.ts +144 -0
- package/dist/src/session-storage.d.ts.map +1 -0
- package/dist/src/session-storage.js +349 -0
- package/dist/src/types.d.ts +11 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +7 -0
- package/dist/src/webhook-idempotency.d.ts +44 -0
- package/dist/src/webhook-idempotency.d.ts.map +1 -0
- package/dist/src/webhook-idempotency.js +148 -0
- package/dist/src/work-queue.d.ts +119 -0
- package/dist/src/work-queue.d.ts.map +1 -0
- package/dist/src/work-queue.js +366 -0
- package/dist/src/worker-storage.d.ts +100 -0
- package/dist/src/worker-storage.d.ts.map +1 -0
- package/dist/src/worker-storage.js +283 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Supaku
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Tracking Module
|
|
3
|
+
*
|
|
4
|
+
* Tracks which issues the agent has worked on to enable automated QA pickup.
|
|
5
|
+
* Also tracks QA attempts to prevent infinite loops.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Record of an issue that was worked on by an agent
|
|
9
|
+
*/
|
|
10
|
+
export interface AgentWorkRecord {
|
|
11
|
+
issueId: string;
|
|
12
|
+
issueIdentifier: string;
|
|
13
|
+
completedAt: number;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
prUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Record of QA attempts for an issue
|
|
19
|
+
*/
|
|
20
|
+
export interface QAAttemptRecord {
|
|
21
|
+
issueId: string;
|
|
22
|
+
attemptNumber: number;
|
|
23
|
+
startedAt: number;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
previousAttempts: Array<{
|
|
26
|
+
sessionId: string;
|
|
27
|
+
failedAt: number;
|
|
28
|
+
reason?: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Mark an issue as having been worked on by the agent
|
|
33
|
+
*/
|
|
34
|
+
export declare function markAgentWorked(issueId: string, data: Omit<AgentWorkRecord, 'issueId' | 'completedAt'>): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Check if an issue was worked on by the agent
|
|
37
|
+
*/
|
|
38
|
+
export declare function wasAgentWorked(issueId: string): Promise<AgentWorkRecord | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Record a QA attempt for an issue
|
|
41
|
+
*/
|
|
42
|
+
export declare function recordQAAttempt(issueId: string, sessionId: string): Promise<QAAttemptRecord>;
|
|
43
|
+
/**
|
|
44
|
+
* Get QA attempt count for an issue
|
|
45
|
+
*/
|
|
46
|
+
export declare function getQAAttemptCount(issueId: string): Promise<number>;
|
|
47
|
+
/**
|
|
48
|
+
* Mark an issue as having just failed QA (prevents immediate re-trigger)
|
|
49
|
+
*/
|
|
50
|
+
export declare function markQAFailed(issueId: string, reason?: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Check if an issue just failed QA (within cooldown period)
|
|
53
|
+
*/
|
|
54
|
+
export declare function didJustFailQA(issueId: string): Promise<boolean>;
|
|
55
|
+
/**
|
|
56
|
+
* Clear QA failed marker (when issue is fixed and moves to Finished again)
|
|
57
|
+
*/
|
|
58
|
+
export declare function clearQAFailed(issueId: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Clear agent worked record (e.g., when issue is moved back to Backlog)
|
|
61
|
+
*/
|
|
62
|
+
export declare function clearAgentWorked(issueId: string): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Clear QA attempt record (e.g., after successful QA)
|
|
65
|
+
*/
|
|
66
|
+
export declare function clearQAAttempts(issueId: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Mark an issue as having development work just queued
|
|
69
|
+
* Prevents rapid re-queuing if status is toggled back and forth
|
|
70
|
+
*/
|
|
71
|
+
export declare function markDevelopmentQueued(issueId: string): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Check if development work was just queued for an issue (within cooldown period)
|
|
74
|
+
*/
|
|
75
|
+
export declare function didJustQueueDevelopment(issueId: string): Promise<boolean>;
|
|
76
|
+
/**
|
|
77
|
+
* Clear development queued marker
|
|
78
|
+
*/
|
|
79
|
+
export declare function clearDevelopmentQueued(issueId: string): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Mark an issue as having acceptance work just queued
|
|
82
|
+
* Prevents rapid re-queuing if status is toggled back and forth
|
|
83
|
+
*/
|
|
84
|
+
export declare function markAcceptanceQueued(issueId: string): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Check if acceptance work was just queued for an issue (within cooldown period)
|
|
87
|
+
*/
|
|
88
|
+
export declare function didJustQueueAcceptance(issueId: string): Promise<boolean>;
|
|
89
|
+
/**
|
|
90
|
+
* Clear acceptance queued marker
|
|
91
|
+
*/
|
|
92
|
+
export declare function clearAcceptanceQueued(issueId: string): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Clean up all tracking data for an accepted issue
|
|
95
|
+
* Called after successful acceptance processing to remove all Redis state
|
|
96
|
+
*/
|
|
97
|
+
export declare function cleanupAcceptedIssue(issueId: string): Promise<void>;
|
|
98
|
+
//# sourceMappingURL=agent-tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-tracking.d.ts","sourceRoot":"","sources":["../../src/agent-tracking.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyBH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,eAAe,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,KAAK,CAAC;QACtB,SAAS,EAAE,MAAM,CAAA;QACjB,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAC,CAAA;CACH;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,aAAa,CAAC,GACrD,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAGjC;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CA2B1B;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAIxE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAIf;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGrE;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIlE;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIrE;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpE;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1E;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAG/E;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI3E;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIzE;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAG9E;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1E;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWzE"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Tracking Module
|
|
3
|
+
*
|
|
4
|
+
* Tracks which issues the agent has worked on to enable automated QA pickup.
|
|
5
|
+
* Also tracks QA attempts to prevent infinite loops.
|
|
6
|
+
*/
|
|
7
|
+
import { redisSet, redisGet, redisExists, redisDel } from './redis';
|
|
8
|
+
const log = {
|
|
9
|
+
info: (msg, data) => console.log(`[tracking] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
10
|
+
warn: (msg, data) => console.warn(`[tracking] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
11
|
+
error: (msg, data) => console.error(`[tracking] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
12
|
+
debug: (_msg, _data) => { },
|
|
13
|
+
};
|
|
14
|
+
// Redis key prefixes
|
|
15
|
+
const AGENT_WORKED_PREFIX = 'agent:worked:';
|
|
16
|
+
const QA_ATTEMPT_PREFIX = 'qa:attempt:';
|
|
17
|
+
const QA_FAILED_PREFIX = 'qa:failed:';
|
|
18
|
+
const DEV_QUEUED_PREFIX = 'agent:dev-queued:';
|
|
19
|
+
const ACCEPTANCE_QUEUED_PREFIX = 'agent:acceptance-queued:';
|
|
20
|
+
// TTLs in seconds
|
|
21
|
+
const AGENT_WORKED_TTL = 7 * 24 * 60 * 60; // 7 days
|
|
22
|
+
const QA_ATTEMPT_TTL = 24 * 60 * 60; // 24 hours
|
|
23
|
+
const QA_FAILED_TTL = 60 * 60; // 1 hour
|
|
24
|
+
const DEV_QUEUED_TTL = 10; // 10 seconds - just enough to prevent duplicate webhooks
|
|
25
|
+
const ACCEPTANCE_QUEUED_TTL = 10; // 10 seconds - just enough to prevent duplicate webhooks
|
|
26
|
+
/**
|
|
27
|
+
* Mark an issue as having been worked on by the agent
|
|
28
|
+
*/
|
|
29
|
+
export async function markAgentWorked(issueId, data) {
|
|
30
|
+
const key = `${AGENT_WORKED_PREFIX}${issueId}`;
|
|
31
|
+
const record = {
|
|
32
|
+
...data,
|
|
33
|
+
issueId,
|
|
34
|
+
completedAt: Date.now(),
|
|
35
|
+
};
|
|
36
|
+
await redisSet(key, record, AGENT_WORKED_TTL);
|
|
37
|
+
log.info('Marked agent worked', {
|
|
38
|
+
issueId,
|
|
39
|
+
issueIdentifier: data.issueIdentifier,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if an issue was worked on by the agent
|
|
44
|
+
*/
|
|
45
|
+
export async function wasAgentWorked(issueId) {
|
|
46
|
+
const key = `${AGENT_WORKED_PREFIX}${issueId}`;
|
|
47
|
+
return redisGet(key);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Record a QA attempt for an issue
|
|
51
|
+
*/
|
|
52
|
+
export async function recordQAAttempt(issueId, sessionId) {
|
|
53
|
+
const key = `${QA_ATTEMPT_PREFIX}${issueId}`;
|
|
54
|
+
const existing = await redisGet(key);
|
|
55
|
+
const record = {
|
|
56
|
+
issueId,
|
|
57
|
+
attemptNumber: (existing?.attemptNumber ?? 0) + 1,
|
|
58
|
+
startedAt: Date.now(),
|
|
59
|
+
sessionId,
|
|
60
|
+
previousAttempts: existing?.previousAttempts ?? [],
|
|
61
|
+
};
|
|
62
|
+
// Add current attempt to history if this is a retry
|
|
63
|
+
if (existing) {
|
|
64
|
+
record.previousAttempts.push({
|
|
65
|
+
sessionId: existing.sessionId,
|
|
66
|
+
failedAt: Date.now(),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
await redisSet(key, record, QA_ATTEMPT_TTL);
|
|
70
|
+
log.info('Recorded QA attempt', {
|
|
71
|
+
issueId,
|
|
72
|
+
attemptNumber: record.attemptNumber,
|
|
73
|
+
});
|
|
74
|
+
return record;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get QA attempt count for an issue
|
|
78
|
+
*/
|
|
79
|
+
export async function getQAAttemptCount(issueId) {
|
|
80
|
+
const key = `${QA_ATTEMPT_PREFIX}${issueId}`;
|
|
81
|
+
const record = await redisGet(key);
|
|
82
|
+
return record?.attemptNumber ?? 0;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Mark an issue as having just failed QA (prevents immediate re-trigger)
|
|
86
|
+
*/
|
|
87
|
+
export async function markQAFailed(issueId, reason) {
|
|
88
|
+
const key = `${QA_FAILED_PREFIX}${issueId}`;
|
|
89
|
+
await redisSet(key, { failedAt: Date.now(), reason }, QA_FAILED_TTL);
|
|
90
|
+
log.info('Marked QA failed', { issueId, reason });
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if an issue just failed QA (within cooldown period)
|
|
94
|
+
*/
|
|
95
|
+
export async function didJustFailQA(issueId) {
|
|
96
|
+
const key = `${QA_FAILED_PREFIX}${issueId}`;
|
|
97
|
+
return redisExists(key);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Clear QA failed marker (when issue is fixed and moves to Finished again)
|
|
101
|
+
*/
|
|
102
|
+
export async function clearQAFailed(issueId) {
|
|
103
|
+
const key = `${QA_FAILED_PREFIX}${issueId}`;
|
|
104
|
+
await redisDel(key);
|
|
105
|
+
log.debug('Cleared QA failed marker', { issueId });
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Clear agent worked record (e.g., when issue is moved back to Backlog)
|
|
109
|
+
*/
|
|
110
|
+
export async function clearAgentWorked(issueId) {
|
|
111
|
+
const key = `${AGENT_WORKED_PREFIX}${issueId}`;
|
|
112
|
+
await redisDel(key);
|
|
113
|
+
log.debug('Cleared agent worked marker', { issueId });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Clear QA attempt record (e.g., after successful QA)
|
|
117
|
+
*/
|
|
118
|
+
export async function clearQAAttempts(issueId) {
|
|
119
|
+
const key = `${QA_ATTEMPT_PREFIX}${issueId}`;
|
|
120
|
+
await redisDel(key);
|
|
121
|
+
log.debug('Cleared QA attempts', { issueId });
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Mark an issue as having development work just queued
|
|
125
|
+
* Prevents rapid re-queuing if status is toggled back and forth
|
|
126
|
+
*/
|
|
127
|
+
export async function markDevelopmentQueued(issueId) {
|
|
128
|
+
const key = `${DEV_QUEUED_PREFIX}${issueId}`;
|
|
129
|
+
await redisSet(key, { queuedAt: Date.now() }, DEV_QUEUED_TTL);
|
|
130
|
+
log.info('Marked development queued', { issueId });
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if development work was just queued for an issue (within cooldown period)
|
|
134
|
+
*/
|
|
135
|
+
export async function didJustQueueDevelopment(issueId) {
|
|
136
|
+
const key = `${DEV_QUEUED_PREFIX}${issueId}`;
|
|
137
|
+
return redisExists(key);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Clear development queued marker
|
|
141
|
+
*/
|
|
142
|
+
export async function clearDevelopmentQueued(issueId) {
|
|
143
|
+
const key = `${DEV_QUEUED_PREFIX}${issueId}`;
|
|
144
|
+
await redisDel(key);
|
|
145
|
+
log.debug('Cleared development queued marker', { issueId });
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Mark an issue as having acceptance work just queued
|
|
149
|
+
* Prevents rapid re-queuing if status is toggled back and forth
|
|
150
|
+
*/
|
|
151
|
+
export async function markAcceptanceQueued(issueId) {
|
|
152
|
+
const key = `${ACCEPTANCE_QUEUED_PREFIX}${issueId}`;
|
|
153
|
+
await redisSet(key, { queuedAt: Date.now() }, ACCEPTANCE_QUEUED_TTL);
|
|
154
|
+
log.info('Marked acceptance queued', { issueId });
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if acceptance work was just queued for an issue (within cooldown period)
|
|
158
|
+
*/
|
|
159
|
+
export async function didJustQueueAcceptance(issueId) {
|
|
160
|
+
const key = `${ACCEPTANCE_QUEUED_PREFIX}${issueId}`;
|
|
161
|
+
return redisExists(key);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Clear acceptance queued marker
|
|
165
|
+
*/
|
|
166
|
+
export async function clearAcceptanceQueued(issueId) {
|
|
167
|
+
const key = `${ACCEPTANCE_QUEUED_PREFIX}${issueId}`;
|
|
168
|
+
await redisDel(key);
|
|
169
|
+
log.debug('Cleared acceptance queued marker', { issueId });
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Clean up all tracking data for an accepted issue
|
|
173
|
+
* Called after successful acceptance processing to remove all Redis state
|
|
174
|
+
*/
|
|
175
|
+
export async function cleanupAcceptedIssue(issueId) {
|
|
176
|
+
const keysToDelete = [
|
|
177
|
+
`${AGENT_WORKED_PREFIX}${issueId}`,
|
|
178
|
+
`${QA_ATTEMPT_PREFIX}${issueId}`,
|
|
179
|
+
`${QA_FAILED_PREFIX}${issueId}`,
|
|
180
|
+
`${DEV_QUEUED_PREFIX}${issueId}`,
|
|
181
|
+
`${ACCEPTANCE_QUEUED_PREFIX}${issueId}`,
|
|
182
|
+
];
|
|
183
|
+
await Promise.all(keysToDelete.map((key) => redisDel(key)));
|
|
184
|
+
log.info('Cleaned up all tracking data for accepted issue', { issueId });
|
|
185
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './redis';
|
|
3
|
+
export * from './session-storage';
|
|
4
|
+
export * from './work-queue';
|
|
5
|
+
export * from './worker-storage';
|
|
6
|
+
export * from './issue-lock';
|
|
7
|
+
export * from './agent-tracking';
|
|
8
|
+
export * from './webhook-idempotency';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,cAAc,SAAS,CAAA;AAGvB,cAAc,SAAS,CAAA;AAGvB,cAAc,mBAAmB,CAAA;AAGjC,cAAc,cAAc,CAAA;AAG5B,cAAc,kBAAkB,CAAA;AAGhC,cAAc,cAAc,CAAA;AAG5B,cAAc,kBAAkB,CAAA;AAGhC,cAAc,uBAAuB,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export * from './types';
|
|
3
|
+
// Redis client
|
|
4
|
+
export * from './redis';
|
|
5
|
+
// Session management
|
|
6
|
+
export * from './session-storage';
|
|
7
|
+
// Work queue
|
|
8
|
+
export * from './work-queue';
|
|
9
|
+
// Worker pool
|
|
10
|
+
export * from './worker-storage';
|
|
11
|
+
// Issue locking
|
|
12
|
+
export * from './issue-lock';
|
|
13
|
+
// Agent tracking
|
|
14
|
+
export * from './agent-tracking';
|
|
15
|
+
// Webhook idempotency
|
|
16
|
+
export * from './webhook-idempotency';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue Lock Module
|
|
3
|
+
*
|
|
4
|
+
* Prevents overlapping agents for the same issue by providing:
|
|
5
|
+
* - Per-issue mutex (Redis SET NX) that gates work dispatch
|
|
6
|
+
* - Per-issue pending queue for parking incoming work while locked
|
|
7
|
+
* - Automatic promotion: releasing a lock dispatches the next pending item
|
|
8
|
+
*
|
|
9
|
+
* Redis Keys:
|
|
10
|
+
* - issue:lock:{issueId} -- String (JSON IssueLock), 2hr TTL
|
|
11
|
+
* - issue:pending:{issueId} -- Sorted Set (priority-ordered session IDs)
|
|
12
|
+
* - issue:pending:items:{issueId} -- Hash (sessionId -> JSON QueuedWork)
|
|
13
|
+
*/
|
|
14
|
+
import { type QueuedWork } from './work-queue';
|
|
15
|
+
import type { AgentWorkType } from './types';
|
|
16
|
+
/**
|
|
17
|
+
* Lock payload stored in Redis
|
|
18
|
+
*/
|
|
19
|
+
export interface IssueLock {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
workType: AgentWorkType;
|
|
22
|
+
workerId: string | null;
|
|
23
|
+
lockedAt: number;
|
|
24
|
+
issueIdentifier: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Result of a dispatchWork call
|
|
28
|
+
*/
|
|
29
|
+
export interface DispatchResult {
|
|
30
|
+
dispatched: boolean;
|
|
31
|
+
parked: boolean;
|
|
32
|
+
replaced: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Acquire an issue-level lock.
|
|
36
|
+
* Uses SET NX for atomicity -- only one caller wins.
|
|
37
|
+
*
|
|
38
|
+
* @returns true if lock was acquired
|
|
39
|
+
*/
|
|
40
|
+
export declare function acquireIssueLock(issueId: string, lock: IssueLock): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Read the current lock for an issue.
|
|
43
|
+
*/
|
|
44
|
+
export declare function getIssueLock(issueId: string): Promise<IssueLock | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Release an issue lock. Idempotent.
|
|
47
|
+
*/
|
|
48
|
+
export declare function releaseIssueLock(issueId: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Refresh the TTL on an issue lock (extend while agent is alive).
|
|
51
|
+
*/
|
|
52
|
+
export declare function refreshIssueLockTTL(issueId: string, ttlSeconds?: number): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Park work for a locked issue.
|
|
55
|
+
*
|
|
56
|
+
* Deduplication: at most one parked item per workType per issue.
|
|
57
|
+
* If a parked item with the same workType already exists, it's replaced
|
|
58
|
+
* (the latest webhook wins). Different workTypes can coexist.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parkWorkForIssue(issueId: string, work: QueuedWork): Promise<{
|
|
61
|
+
parked: boolean;
|
|
62
|
+
replaced: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Promote the next pending work item for an issue.
|
|
66
|
+
* Pops the highest-priority item, acquires the issue lock for it,
|
|
67
|
+
* and queues it in the global work queue.
|
|
68
|
+
*
|
|
69
|
+
* @returns The promoted work item, or null if nothing to promote
|
|
70
|
+
*/
|
|
71
|
+
export declare function promoteNextPendingWork(issueId: string): Promise<QueuedWork | null>;
|
|
72
|
+
/**
|
|
73
|
+
* Get the count of pending work items for an issue.
|
|
74
|
+
*/
|
|
75
|
+
export declare function getPendingWorkCount(issueId: string): Promise<number>;
|
|
76
|
+
/**
|
|
77
|
+
* Main entry point for dispatching work.
|
|
78
|
+
*
|
|
79
|
+
* Try to acquire the issue lock:
|
|
80
|
+
* - If acquired -> queue the work in the global queue
|
|
81
|
+
* - If locked -> park the work in the per-issue pending queue
|
|
82
|
+
*
|
|
83
|
+
* @returns DispatchResult indicating what happened
|
|
84
|
+
*/
|
|
85
|
+
export declare function dispatchWork(work: QueuedWork): Promise<DispatchResult>;
|
|
86
|
+
/**
|
|
87
|
+
* Remove a parked work item by sessionId.
|
|
88
|
+
*
|
|
89
|
+
* The issue-pending hash is keyed by workType, so we scan all entries
|
|
90
|
+
* to find the one matching the given sessionId.
|
|
91
|
+
*
|
|
92
|
+
* @returns true if a matching parked item was found and removed
|
|
93
|
+
*/
|
|
94
|
+
export declare function removeParkedWorkBySessionId(issueId: string, sessionId: string): Promise<boolean>;
|
|
95
|
+
/**
|
|
96
|
+
* Check if a session is parked in any issue-pending queue.
|
|
97
|
+
*
|
|
98
|
+
* Scans the issue:pending:items:{issueId} hash entries for a matching sessionId.
|
|
99
|
+
*
|
|
100
|
+
* @param issueId - The issue to check
|
|
101
|
+
* @param sessionId - The session to look for
|
|
102
|
+
* @returns true if the session is parked for this issue
|
|
103
|
+
*/
|
|
104
|
+
export declare function isSessionParkedForIssue(issueId: string, sessionId: string): Promise<boolean>;
|
|
105
|
+
/**
|
|
106
|
+
* Scan for expired issue locks that have pending work.
|
|
107
|
+
* If a lock expired naturally (TTL) but pending items remain, promote them.
|
|
108
|
+
*
|
|
109
|
+
* Called from orphan-cleanup to handle crashed workers that didn't release locks.
|
|
110
|
+
*/
|
|
111
|
+
export declare function cleanupExpiredLocksWithPendingWork(): Promise<number>;
|
|
112
|
+
/**
|
|
113
|
+
* Release issue locks held by sessions that have already reached a terminal state.
|
|
114
|
+
*
|
|
115
|
+
* This handles the case where a session completes but the lock release failed
|
|
116
|
+
* (e.g., network error during cleanup). The lock's 2-hour TTL would eventually
|
|
117
|
+
* expire, but this proactively clears it when workers have idle capacity.
|
|
118
|
+
*
|
|
119
|
+
* Only runs when workers are online -- if no workers are available, there's no
|
|
120
|
+
* point promoting parked work since nothing can pick it up.
|
|
121
|
+
*
|
|
122
|
+
* @param hasIdleWorkers - true if at least one worker is online with spare capacity
|
|
123
|
+
* @returns Number of stale locks released and parked work promoted
|
|
124
|
+
*/
|
|
125
|
+
export declare function cleanupStaleLocksWithIdleWorkers(hasIdleWorkers: boolean): Promise<number>;
|
|
126
|
+
//# sourceMappingURL=issue-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-lock.d.ts","sourceRoot":"","sources":["../../src/issue-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAmBH,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAoB5C;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,aAAa,CAAA;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,EAAE,OAAO,CAAA;IACf,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,GACd,OAAO,CAAC,OAAO,CAAC,CA4BlB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAY7E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUrE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,UAAU,GAAE,MAAyB,GACpC,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,UAAU,GACf,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CAoDjD;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAqE5B;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAU1E;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAoD5E;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAsClB;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAwBlB;AAED;;;;;GAKG;AACH,wBAAsB,kCAAkC,IAAI,OAAO,CAAC,MAAM,CAAC,CA0C1E;AAID;;;;;;;;;;;;GAYG;AACH,wBAAsB,gCAAgC,CACpD,cAAc,EAAE,OAAO,GACtB,OAAO,CAAC,MAAM,CAAC,CAgEjB"}
|