@supaku/agentfactory-cli 0.2.0 → 0.4.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/dist/src/analyze-logs.d.ts +2 -2
- package/dist/src/analyze-logs.js +23 -194
- package/dist/src/cleanup.d.ts +2 -6
- package/dist/src/cleanup.d.ts.map +1 -1
- package/dist/src/cleanup.js +24 -225
- package/dist/src/lib/analyze-logs-runner.d.ts +47 -0
- package/dist/src/lib/analyze-logs-runner.d.ts.map +1 -0
- package/dist/src/lib/analyze-logs-runner.js +216 -0
- package/dist/src/lib/cleanup-runner.d.ts +28 -0
- package/dist/src/lib/cleanup-runner.d.ts.map +1 -0
- package/dist/src/lib/cleanup-runner.js +224 -0
- package/dist/src/lib/orchestrator-runner.d.ts +45 -0
- package/dist/src/lib/orchestrator-runner.d.ts.map +1 -0
- package/dist/src/lib/orchestrator-runner.js +144 -0
- package/dist/src/lib/queue-admin-runner.d.ts +30 -0
- package/dist/src/lib/queue-admin-runner.d.ts.map +1 -0
- package/dist/src/lib/queue-admin-runner.js +378 -0
- package/dist/src/lib/worker-fleet-runner.d.ts +28 -0
- package/dist/src/lib/worker-fleet-runner.d.ts.map +1 -0
- package/dist/src/lib/worker-fleet-runner.js +224 -0
- package/dist/src/lib/worker-runner.d.ts +31 -0
- package/dist/src/lib/worker-runner.d.ts.map +1 -0
- package/dist/src/lib/worker-runner.js +735 -0
- package/dist/src/orchestrator.d.ts +1 -1
- package/dist/src/orchestrator.js +42 -106
- package/dist/src/queue-admin.d.ts +3 -2
- package/dist/src/queue-admin.d.ts.map +1 -1
- package/dist/src/queue-admin.js +38 -360
- package/dist/src/worker-fleet.d.ts +1 -1
- package/dist/src/worker-fleet.js +23 -162
- package/dist/src/worker.d.ts +1 -0
- package/dist/src/worker.d.ts.map +1 -1
- package/dist/src/worker.js +33 -702
- package/package.json +28 -4
package/dist/src/orchestrator.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* AgentFactory Orchestrator CLI
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Thin wrapper around the orchestrator runner.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* af-orchestrator [options]
|
|
@@ -18,22 +18,10 @@
|
|
|
18
18
|
* LINEAR_API_KEY Required API key for Linear authentication
|
|
19
19
|
*/
|
|
20
20
|
import path from 'path';
|
|
21
|
-
import { execSync } from 'child_process';
|
|
22
21
|
import { config } from 'dotenv';
|
|
23
22
|
// Load environment variables from .env.local
|
|
24
23
|
config({ path: path.resolve(process.cwd(), '.env.local') });
|
|
25
|
-
import {
|
|
26
|
-
function getGitRoot() {
|
|
27
|
-
try {
|
|
28
|
-
return execSync('git rev-parse --show-toplevel', {
|
|
29
|
-
encoding: 'utf-8',
|
|
30
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
31
|
-
}).trim();
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return process.cwd();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
24
|
+
import { runOrchestrator } from './lib/orchestrator-runner.js';
|
|
37
25
|
function parseArgs() {
|
|
38
26
|
const args = process.argv.slice(2);
|
|
39
27
|
const result = {
|
|
@@ -98,18 +86,6 @@ Examples:
|
|
|
98
86
|
af-orchestrator --project MyProject --dry-run
|
|
99
87
|
`);
|
|
100
88
|
}
|
|
101
|
-
function formatDuration(ms) {
|
|
102
|
-
const seconds = Math.floor(ms / 1000);
|
|
103
|
-
const minutes = Math.floor(seconds / 60);
|
|
104
|
-
const hours = Math.floor(minutes / 60);
|
|
105
|
-
if (hours > 0) {
|
|
106
|
-
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
107
|
-
}
|
|
108
|
-
if (minutes > 0) {
|
|
109
|
-
return `${minutes}m ${seconds % 60}s`;
|
|
110
|
-
}
|
|
111
|
-
return `${seconds}s`;
|
|
112
|
-
}
|
|
113
89
|
async function main() {
|
|
114
90
|
const args = parseArgs();
|
|
115
91
|
if (!process.env.LINEAR_API_KEY) {
|
|
@@ -122,95 +98,43 @@ async function main() {
|
|
|
122
98
|
console.log(`Max concurrent: ${args.max}`);
|
|
123
99
|
console.log(`Dry run: ${args.dryRun}`);
|
|
124
100
|
console.log('');
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
project: args.project,
|
|
128
|
-
maxConcurrent: args.max,
|
|
129
|
-
worktreePath: path.resolve(gitRoot, '.worktrees'),
|
|
130
|
-
}, {
|
|
131
|
-
onIssueSelected: (issue) => {
|
|
132
|
-
console.log(`Selected: ${issue.identifier} - ${issue.title}`);
|
|
133
|
-
console.log(` URL: ${issue.url}`);
|
|
134
|
-
console.log(` Labels: ${issue.labels.join(', ') || 'none'}`);
|
|
135
|
-
},
|
|
136
|
-
onAgentStart: (agent) => {
|
|
137
|
-
console.log(`Agent started: ${agent.identifier} (PID: ${agent.pid})`);
|
|
138
|
-
console.log(` Worktree: ${agent.worktreePath}`);
|
|
139
|
-
},
|
|
140
|
-
onAgentComplete: (agent) => {
|
|
141
|
-
const duration = agent.completedAt
|
|
142
|
-
? formatDuration(agent.completedAt.getTime() - agent.startedAt.getTime())
|
|
143
|
-
: 'unknown';
|
|
144
|
-
console.log(`Agent completed: ${agent.identifier} (${duration})`);
|
|
145
|
-
},
|
|
146
|
-
onAgentError: (agent, error) => {
|
|
147
|
-
console.error(`Agent failed: ${agent.identifier}`);
|
|
148
|
-
console.error(` Error: ${error.message}`);
|
|
149
|
-
},
|
|
150
|
-
onAgentIncomplete: (agent) => {
|
|
151
|
-
const duration = agent.completedAt
|
|
152
|
-
? formatDuration(agent.completedAt.getTime() - agent.startedAt.getTime())
|
|
153
|
-
: 'unknown';
|
|
154
|
-
console.warn(`Agent incomplete: ${agent.identifier} (${duration})`);
|
|
155
|
-
console.warn(` Reason: ${agent.incompleteReason ?? 'unknown'}`);
|
|
156
|
-
console.warn(` Worktree preserved: ${agent.worktreePath}`);
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
try {
|
|
160
|
-
if (args.single) {
|
|
161
|
-
console.log(`Processing single issue: ${args.single}`);
|
|
162
|
-
if (args.dryRun) {
|
|
163
|
-
console.log('[DRY RUN] Would spawn agent for:', args.single);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
await orchestrator.spawnAgentForIssue(args.single);
|
|
167
|
-
console.log(`Agent spawned for ${args.single}`);
|
|
168
|
-
if (args.wait) {
|
|
169
|
-
console.log('Waiting for agent to complete...');
|
|
170
|
-
await orchestrator.waitForAll();
|
|
171
|
-
}
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
101
|
+
if (args.single) {
|
|
102
|
+
console.log(`Processing single issue: ${args.single}`);
|
|
174
103
|
if (args.dryRun) {
|
|
175
|
-
|
|
176
|
-
if (issues.length === 0) {
|
|
177
|
-
console.log('No backlog issues found');
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
console.log(`[DRY RUN] Would process ${issues.length} issue(s):`);
|
|
181
|
-
for (const issue of issues) {
|
|
182
|
-
console.log(` - ${issue.identifier}: ${issue.title}`);
|
|
183
|
-
console.log(` Priority: ${issue.priority || 'none'}`);
|
|
184
|
-
console.log(` Labels: ${issue.labels.join(', ') || 'none'}`);
|
|
185
|
-
}
|
|
104
|
+
console.log('[DRY RUN] Would spawn agent for:', args.single);
|
|
186
105
|
return;
|
|
187
106
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const result = await runOrchestrator({
|
|
110
|
+
linearApiKey: process.env.LINEAR_API_KEY,
|
|
111
|
+
project: args.project,
|
|
112
|
+
max: args.max,
|
|
113
|
+
single: args.single,
|
|
114
|
+
wait: args.wait,
|
|
115
|
+
dryRun: args.dryRun,
|
|
116
|
+
});
|
|
117
|
+
if (!args.single && !args.dryRun) {
|
|
194
118
|
console.log('');
|
|
195
|
-
console.log('
|
|
196
|
-
|
|
197
|
-
|
|
119
|
+
console.log('Orchestrator started');
|
|
120
|
+
console.log(` Agents spawned: ${result.agentsSpawned}`);
|
|
121
|
+
console.log(` Errors: ${result.errors.length}`);
|
|
122
|
+
if (result.errors.length > 0) {
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log('Errors:');
|
|
125
|
+
for (const { issueId, error } of result.errors) {
|
|
126
|
+
console.log(` ${issueId}: ${error.message}`);
|
|
127
|
+
}
|
|
198
128
|
}
|
|
199
129
|
}
|
|
200
|
-
if (args.
|
|
201
|
-
console.log(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
console.log('');
|
|
205
|
-
console.log('Received SIGINT, stopping agents...');
|
|
206
|
-
orchestrator.stopAll();
|
|
207
|
-
process.exit(1);
|
|
208
|
-
});
|
|
209
|
-
const completedAgents = await orchestrator.waitForAll();
|
|
130
|
+
if (args.single) {
|
|
131
|
+
console.log(`Agent spawned for ${args.single}`);
|
|
132
|
+
}
|
|
133
|
+
if (args.wait && result.completed.length > 0) {
|
|
210
134
|
console.log('');
|
|
211
135
|
console.log('All agents completed');
|
|
212
136
|
console.log('Results:');
|
|
213
|
-
for (const agent of
|
|
137
|
+
for (const agent of result.completed) {
|
|
214
138
|
const duration = agent.completedAt
|
|
215
139
|
? formatDuration(agent.completedAt.getTime() - agent.startedAt.getTime())
|
|
216
140
|
: 'unknown';
|
|
@@ -224,6 +148,18 @@ async function main() {
|
|
|
224
148
|
process.exit(1);
|
|
225
149
|
}
|
|
226
150
|
}
|
|
151
|
+
function formatDuration(ms) {
|
|
152
|
+
const seconds = Math.floor(ms / 1000);
|
|
153
|
+
const minutes = Math.floor(seconds / 60);
|
|
154
|
+
const hours = Math.floor(minutes / 60);
|
|
155
|
+
if (hours > 0) {
|
|
156
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
157
|
+
}
|
|
158
|
+
if (minutes > 0) {
|
|
159
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
160
|
+
}
|
|
161
|
+
return `${seconds}s`;
|
|
162
|
+
}
|
|
227
163
|
main().catch((error) => {
|
|
228
164
|
console.error('Fatal error:', error);
|
|
229
165
|
process.exit(1);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* AgentFactory Queue Admin
|
|
3
|
+
* AgentFactory Queue Admin CLI
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Thin wrapper around the queue-admin runner. Handles dotenv, arg parsing,
|
|
6
|
+
* and process.exit so the runner stays process-agnostic.
|
|
6
7
|
*
|
|
7
8
|
* Usage:
|
|
8
9
|
* af-queue-admin <command>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue-admin.d.ts","sourceRoot":"","sources":["../../src/queue-admin.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"queue-admin.d.ts","sourceRoot":"","sources":["../../src/queue-admin.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;GAqBG"}
|
package/dist/src/queue-admin.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* AgentFactory Queue Admin
|
|
3
|
+
* AgentFactory Queue Admin CLI
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Thin wrapper around the queue-admin runner. Handles dotenv, arg parsing,
|
|
6
|
+
* and process.exit so the runner stays process-agnostic.
|
|
6
7
|
*
|
|
7
8
|
* Usage:
|
|
8
9
|
* af-queue-admin <command>
|
|
@@ -24,329 +25,10 @@ import path from 'path';
|
|
|
24
25
|
import { config } from 'dotenv';
|
|
25
26
|
// Load environment variables from .env.local in CWD
|
|
26
27
|
config({ path: path.resolve(process.cwd(), '.env.local') });
|
|
27
|
-
import {
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const WORK_CLAIM_PREFIX = 'work:claim:';
|
|
32
|
-
const SESSION_KEY_PREFIX = 'agent:session:';
|
|
33
|
-
const WORKER_PREFIX = 'work:worker:';
|
|
34
|
-
// ANSI colors
|
|
35
|
-
const C = {
|
|
36
|
-
reset: '\x1b[0m',
|
|
37
|
-
red: '\x1b[31m',
|
|
38
|
-
green: '\x1b[32m',
|
|
39
|
-
yellow: '\x1b[33m',
|
|
40
|
-
cyan: '\x1b[36m',
|
|
41
|
-
gray: '\x1b[90m',
|
|
42
|
-
};
|
|
43
|
-
function ensureRedis() {
|
|
44
|
-
if (!process.env.REDIS_URL) {
|
|
45
|
-
console.error(`${C.red}Error: REDIS_URL not set${C.reset}`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
// Initialize the Redis client
|
|
49
|
-
getRedisClient();
|
|
50
|
-
}
|
|
51
|
-
async function listQueue() {
|
|
52
|
-
ensureRedis();
|
|
53
|
-
// Get items from sorted set (current queue format)
|
|
54
|
-
const queuedSessionIds = await redisZRangeByScore(WORK_QUEUE_KEY, '-inf', '+inf');
|
|
55
|
-
const workItems = await redisHGetAll(WORK_ITEMS_KEY);
|
|
56
|
-
const workItemCount = Object.keys(workItems).length;
|
|
57
|
-
console.log(`\n${C.cyan}Work Queue${C.reset} (${Math.max(queuedSessionIds.length, workItemCount)} items):`);
|
|
58
|
-
console.log('='.repeat(60));
|
|
59
|
-
if (workItemCount === 0 && queuedSessionIds.length === 0) {
|
|
60
|
-
console.log('(empty)');
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
for (const [sessionId, itemJson] of Object.entries(workItems)) {
|
|
64
|
-
try {
|
|
65
|
-
const work = JSON.parse(itemJson);
|
|
66
|
-
console.log(`- ${work.issueIdentifier ?? sessionId.slice(0, 8)} (session: ${sessionId.slice(0, 8)}...)`);
|
|
67
|
-
console.log(` Priority: ${work.priority ?? 'none'}, WorkType: ${work.workType ?? 'development'}`);
|
|
68
|
-
if (work.queuedAt) {
|
|
69
|
-
console.log(` Queued: ${new Date(work.queuedAt).toISOString()}`);
|
|
70
|
-
}
|
|
71
|
-
if (work.claudeSessionId) {
|
|
72
|
-
console.log(` ${C.yellow}Has claudeSessionId: ${work.claudeSessionId.substring(0, 12)}${C.reset}`);
|
|
73
|
-
}
|
|
74
|
-
if (work.prompt) {
|
|
75
|
-
console.log(` Prompt: "${work.prompt.slice(0, 50)}..."`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
console.log(`- [invalid JSON]: ${sessionId}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
await disconnectRedis();
|
|
84
|
-
}
|
|
85
|
-
async function listSessions() {
|
|
86
|
-
ensureRedis();
|
|
87
|
-
const keys = await redisKeys(`${SESSION_KEY_PREFIX}*`);
|
|
88
|
-
console.log(`\n${C.cyan}Sessions${C.reset} (${keys.length} total):`);
|
|
89
|
-
console.log('='.repeat(60));
|
|
90
|
-
if (keys.length === 0) {
|
|
91
|
-
console.log('(none)');
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
for (const key of keys) {
|
|
95
|
-
const session = await redisGet(key);
|
|
96
|
-
if (session) {
|
|
97
|
-
const statusColors = {
|
|
98
|
-
pending: C.yellow,
|
|
99
|
-
claimed: C.cyan,
|
|
100
|
-
running: C.cyan,
|
|
101
|
-
completed: C.green,
|
|
102
|
-
failed: C.red,
|
|
103
|
-
stopped: C.yellow,
|
|
104
|
-
};
|
|
105
|
-
const statusColor = statusColors[session.status] || '';
|
|
106
|
-
console.log(`- ${session.issueIdentifier || session.issueId.slice(0, 8)} [${statusColor}${session.status}${C.reset}]`);
|
|
107
|
-
console.log(` Session: ${session.linearSessionId.slice(0, 12)}...`);
|
|
108
|
-
if (session.workType) {
|
|
109
|
-
console.log(` WorkType: ${session.workType}`);
|
|
110
|
-
}
|
|
111
|
-
console.log(` Updated: ${new Date(session.updatedAt * 1000).toISOString()}`);
|
|
112
|
-
if (session.workerId) {
|
|
113
|
-
console.log(` Worker: ${session.workerId}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
await disconnectRedis();
|
|
119
|
-
}
|
|
120
|
-
async function listWorkersFn() {
|
|
121
|
-
ensureRedis();
|
|
122
|
-
const keys = await redisKeys(`${WORKER_PREFIX}*`);
|
|
123
|
-
console.log(`\n${C.cyan}Workers${C.reset} (${keys.length} total):`);
|
|
124
|
-
console.log('='.repeat(60));
|
|
125
|
-
if (keys.length === 0) {
|
|
126
|
-
console.log('(none)');
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
for (const key of keys) {
|
|
130
|
-
const worker = await redisGet(key);
|
|
131
|
-
if (worker) {
|
|
132
|
-
const statusColor = worker.status === 'active' ? C.green : C.yellow;
|
|
133
|
-
console.log(`- ${worker.id.slice(0, 12)} [${statusColor}${worker.status}${C.reset}]`);
|
|
134
|
-
if (worker.hostname) {
|
|
135
|
-
console.log(` Hostname: ${worker.hostname}`);
|
|
136
|
-
}
|
|
137
|
-
console.log(` Capacity: ${worker.activeCount ?? 0}/${worker.capacity ?? '?'}`);
|
|
138
|
-
if (worker.lastHeartbeat) {
|
|
139
|
-
const ago = Math.round((Date.now() - worker.lastHeartbeat) / 1000);
|
|
140
|
-
console.log(` Last heartbeat: ${ago}s ago`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
await disconnectRedis();
|
|
146
|
-
}
|
|
147
|
-
async function clearClaims() {
|
|
148
|
-
ensureRedis();
|
|
149
|
-
console.log('Clearing work claims...');
|
|
150
|
-
const claimKeys = await redisKeys(`${WORK_CLAIM_PREFIX}*`);
|
|
151
|
-
console.log(`Found ${claimKeys.length} claim(s)`);
|
|
152
|
-
if (claimKeys.length === 0) {
|
|
153
|
-
console.log('No claims to clear');
|
|
154
|
-
await disconnectRedis();
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
let deleted = 0;
|
|
158
|
-
for (const key of claimKeys) {
|
|
159
|
-
const result = await redisDel(key);
|
|
160
|
-
if (result > 0) {
|
|
161
|
-
console.log(` Deleted: ${key}`);
|
|
162
|
-
deleted++;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
console.log(`\nCleared ${deleted} claim(s)`);
|
|
166
|
-
await disconnectRedis();
|
|
167
|
-
}
|
|
168
|
-
async function clearQueue() {
|
|
169
|
-
ensureRedis();
|
|
170
|
-
console.log('Clearing work queue...');
|
|
171
|
-
const queuedSessionIds = await redisZRangeByScore(WORK_QUEUE_KEY, '-inf', '+inf');
|
|
172
|
-
console.log(`Found ${queuedSessionIds.length} item(s) in queue sorted set`);
|
|
173
|
-
const workItems = await redisHGetAll(WORK_ITEMS_KEY);
|
|
174
|
-
const workItemCount = Object.keys(workItems).length;
|
|
175
|
-
console.log(`Found ${workItemCount} item(s) in work items hash`);
|
|
176
|
-
// Show what we're clearing
|
|
177
|
-
for (const [sessionId, itemJson] of Object.entries(workItems)) {
|
|
178
|
-
try {
|
|
179
|
-
const work = JSON.parse(itemJson);
|
|
180
|
-
console.log(` - ${work.issueIdentifier ?? sessionId.slice(0, 8)} (workType: ${work.workType || 'development'})`);
|
|
181
|
-
if (work.claudeSessionId) {
|
|
182
|
-
console.log(` ${C.yellow}Has claudeSessionId: ${work.claudeSessionId.substring(0, 12)}${C.reset}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
console.log(` - [invalid JSON]: ${sessionId}`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
let cleared = 0;
|
|
190
|
-
if (queuedSessionIds.length > 0) {
|
|
191
|
-
await redisDel(WORK_QUEUE_KEY);
|
|
192
|
-
cleared++;
|
|
193
|
-
}
|
|
194
|
-
if (workItemCount > 0) {
|
|
195
|
-
await redisDel(WORK_ITEMS_KEY);
|
|
196
|
-
cleared++;
|
|
197
|
-
}
|
|
198
|
-
const totalItems = Math.max(queuedSessionIds.length, workItemCount);
|
|
199
|
-
if (cleared > 0) {
|
|
200
|
-
console.log(`\nCleared ${totalItems} item(s) from work queue`);
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
console.log('\nQueue was already empty');
|
|
204
|
-
}
|
|
205
|
-
await disconnectRedis();
|
|
206
|
-
}
|
|
207
|
-
async function clearAll() {
|
|
208
|
-
ensureRedis();
|
|
209
|
-
console.log('Clearing ALL state...\n');
|
|
210
|
-
// Clear work queue
|
|
211
|
-
const queuedSessionIds = await redisZRangeByScore(WORK_QUEUE_KEY, '-inf', '+inf');
|
|
212
|
-
const workItems = await redisHGetAll(WORK_ITEMS_KEY);
|
|
213
|
-
if (queuedSessionIds.length > 0)
|
|
214
|
-
await redisDel(WORK_QUEUE_KEY);
|
|
215
|
-
if (Object.keys(workItems).length > 0)
|
|
216
|
-
await redisDel(WORK_ITEMS_KEY);
|
|
217
|
-
console.log(`Cleared ${Math.max(queuedSessionIds.length, Object.keys(workItems).length)} queue items`);
|
|
218
|
-
// Clear all sessions
|
|
219
|
-
const sessionKeys = await redisKeys(`${SESSION_KEY_PREFIX}*`);
|
|
220
|
-
for (const key of sessionKeys) {
|
|
221
|
-
await redisDel(key);
|
|
222
|
-
}
|
|
223
|
-
console.log(`Cleared ${sessionKeys.length} sessions`);
|
|
224
|
-
// Clear all claims
|
|
225
|
-
const claimKeys = await redisKeys(`${WORK_CLAIM_PREFIX}*`);
|
|
226
|
-
for (const key of claimKeys) {
|
|
227
|
-
await redisDel(key);
|
|
228
|
-
}
|
|
229
|
-
console.log(`Cleared ${claimKeys.length} claims`);
|
|
230
|
-
// Clear all workers
|
|
231
|
-
const workerKeys = await redisKeys(`${WORKER_PREFIX}*`);
|
|
232
|
-
for (const key of workerKeys) {
|
|
233
|
-
await redisDel(key);
|
|
234
|
-
}
|
|
235
|
-
console.log(`Cleared ${workerKeys.length} worker registrations`);
|
|
236
|
-
console.log('\nAll cleared!');
|
|
237
|
-
await disconnectRedis();
|
|
238
|
-
}
|
|
239
|
-
async function resetWorkState() {
|
|
240
|
-
ensureRedis();
|
|
241
|
-
console.log('Resetting work state...');
|
|
242
|
-
console.log('-'.repeat(60));
|
|
243
|
-
let totalCleared = 0;
|
|
244
|
-
// 1. Clear work claims
|
|
245
|
-
console.log('\nClearing work claims...');
|
|
246
|
-
const claimKeys = await redisKeys(`${WORK_CLAIM_PREFIX}*`);
|
|
247
|
-
console.log(` Found ${claimKeys.length} claim(s)`);
|
|
248
|
-
for (const key of claimKeys) {
|
|
249
|
-
const result = await redisDel(key);
|
|
250
|
-
if (result > 0) {
|
|
251
|
-
console.log(` Deleted: ${key}`);
|
|
252
|
-
totalCleared++;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// 2. Clear work queue
|
|
256
|
-
console.log('\nClearing work queue...');
|
|
257
|
-
const queuedSessionIds = await redisZRangeByScore(WORK_QUEUE_KEY, '-inf', '+inf');
|
|
258
|
-
console.log(` Found ${queuedSessionIds.length} queued item(s) in sorted set`);
|
|
259
|
-
const workItems = await redisHGetAll(WORK_ITEMS_KEY);
|
|
260
|
-
const workItemCount = Object.keys(workItems).length;
|
|
261
|
-
console.log(` Found ${workItemCount} item(s) in work items hash`);
|
|
262
|
-
for (const [sessionId, itemJson] of Object.entries(workItems)) {
|
|
263
|
-
try {
|
|
264
|
-
const work = JSON.parse(itemJson);
|
|
265
|
-
console.log(` - ${work.issueIdentifier ?? sessionId.slice(0, 8)} (workType: ${work.workType || 'development'})`);
|
|
266
|
-
}
|
|
267
|
-
catch {
|
|
268
|
-
console.log(` - [invalid item: ${sessionId}]`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
if (queuedSessionIds.length > 0 || workItemCount > 0) {
|
|
272
|
-
await redisDel(WORK_QUEUE_KEY);
|
|
273
|
-
await redisDel(WORK_ITEMS_KEY);
|
|
274
|
-
totalCleared += Math.max(queuedSessionIds.length, workItemCount);
|
|
275
|
-
console.log(` Cleared queue and items hash`);
|
|
276
|
-
}
|
|
277
|
-
// 3. Reset stuck sessions
|
|
278
|
-
console.log('\nResetting stuck sessions...');
|
|
279
|
-
const sessionKeys = await redisKeys(`${SESSION_KEY_PREFIX}*`);
|
|
280
|
-
console.log(` Found ${sessionKeys.length} session(s)`);
|
|
281
|
-
let sessionsReset = 0;
|
|
282
|
-
for (const key of sessionKeys) {
|
|
283
|
-
const session = await redisGet(key);
|
|
284
|
-
if (!session)
|
|
285
|
-
continue;
|
|
286
|
-
if (session.status === 'running' || session.status === 'claimed') {
|
|
287
|
-
console.log(` Resetting: ${session.issueIdentifier || session.linearSessionId}`);
|
|
288
|
-
console.log(` Status: ${session.status}, WorkerId: ${session.workerId || 'none'}`);
|
|
289
|
-
const updated = {
|
|
290
|
-
...session,
|
|
291
|
-
status: 'pending',
|
|
292
|
-
workerId: undefined,
|
|
293
|
-
claimedAt: undefined,
|
|
294
|
-
claudeSessionId: undefined,
|
|
295
|
-
updatedAt: Math.floor(Date.now() / 1000),
|
|
296
|
-
};
|
|
297
|
-
await redisSet(key, updated, 24 * 60 * 60);
|
|
298
|
-
sessionsReset++;
|
|
299
|
-
console.log(` Reset to pending`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
console.log('\n' + '-'.repeat(60));
|
|
303
|
-
console.log(`\nReset complete:`);
|
|
304
|
-
console.log(` - Claims cleared: ${claimKeys.length}`);
|
|
305
|
-
console.log(` - Queue items cleared: ${Math.max(queuedSessionIds.length, workItemCount)}`);
|
|
306
|
-
console.log(` - Sessions reset: ${sessionsReset}`);
|
|
307
|
-
await disconnectRedis();
|
|
308
|
-
}
|
|
309
|
-
async function removeSession(sessionId) {
|
|
310
|
-
ensureRedis();
|
|
311
|
-
let found = false;
|
|
312
|
-
// Find session by partial ID match
|
|
313
|
-
const keys = await redisKeys(`${SESSION_KEY_PREFIX}*`);
|
|
314
|
-
for (const key of keys) {
|
|
315
|
-
if (key.includes(sessionId)) {
|
|
316
|
-
await redisDel(key);
|
|
317
|
-
console.log(`Removed session: ${key.replace(SESSION_KEY_PREFIX, '')}`);
|
|
318
|
-
found = true;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
// Also remove from queue if present
|
|
322
|
-
const workItems = await redisHGetAll(WORK_ITEMS_KEY);
|
|
323
|
-
for (const [sid, itemJson] of Object.entries(workItems)) {
|
|
324
|
-
if (sid.includes(sessionId)) {
|
|
325
|
-
// Remove from hash via direct Redis command
|
|
326
|
-
const redis = getRedisClient();
|
|
327
|
-
await redis.hdel(WORK_ITEMS_KEY, sid);
|
|
328
|
-
// Remove from sorted set
|
|
329
|
-
const { redisZRem } = await import('@supaku/agentfactory-server');
|
|
330
|
-
await redisZRem(WORK_QUEUE_KEY, sid);
|
|
331
|
-
const work = JSON.parse(itemJson);
|
|
332
|
-
console.log(`Removed from queue: ${work.issueIdentifier ?? sid.slice(0, 8)}`);
|
|
333
|
-
found = true;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
// Remove claim if present
|
|
337
|
-
const claimKeys = await redisKeys(`${WORK_CLAIM_PREFIX}*`);
|
|
338
|
-
for (const key of claimKeys) {
|
|
339
|
-
if (key.includes(sessionId)) {
|
|
340
|
-
await redisDel(key);
|
|
341
|
-
console.log(`Removed claim: ${key}`);
|
|
342
|
-
found = true;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
if (!found) {
|
|
346
|
-
console.log(`No session found matching: ${sessionId}`);
|
|
347
|
-
}
|
|
348
|
-
await disconnectRedis();
|
|
349
|
-
}
|
|
28
|
+
import { runQueueAdmin, C } from './lib/queue-admin-runner.js';
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Usage
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
350
32
|
function printUsage() {
|
|
351
33
|
console.log(`
|
|
352
34
|
${C.cyan}AgentFactory Queue Admin${C.reset} - Manage Redis work queue and sessions
|
|
@@ -372,45 +54,41 @@ ${C.yellow}Examples:${C.reset}
|
|
|
372
54
|
af-queue-admin remove abc123
|
|
373
55
|
`);
|
|
374
56
|
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Valid commands
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
const VALID_COMMANDS = new Set([
|
|
61
|
+
'list',
|
|
62
|
+
'sessions',
|
|
63
|
+
'workers',
|
|
64
|
+
'clear-claims',
|
|
65
|
+
'clear-queue',
|
|
66
|
+
'clear-all',
|
|
67
|
+
'reset',
|
|
68
|
+
'remove',
|
|
69
|
+
]);
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Main
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
375
73
|
async function main() {
|
|
376
74
|
const command = process.argv[2];
|
|
377
75
|
const arg = process.argv[3];
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
await clearClaims();
|
|
390
|
-
break;
|
|
391
|
-
case 'clear-queue':
|
|
392
|
-
await clearQueue();
|
|
393
|
-
break;
|
|
394
|
-
case 'clear-all':
|
|
395
|
-
await clearAll();
|
|
396
|
-
break;
|
|
397
|
-
case 'reset':
|
|
398
|
-
await resetWorkState();
|
|
399
|
-
break;
|
|
400
|
-
case 'remove':
|
|
401
|
-
if (!arg) {
|
|
402
|
-
console.error('Usage: af-queue-admin remove <session-id>');
|
|
403
|
-
process.exit(1);
|
|
404
|
-
}
|
|
405
|
-
await removeSession(arg);
|
|
406
|
-
break;
|
|
407
|
-
case '--help':
|
|
408
|
-
case '-h':
|
|
409
|
-
case 'help':
|
|
410
|
-
default:
|
|
411
|
-
printUsage();
|
|
412
|
-
break;
|
|
76
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
77
|
+
printUsage();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!VALID_COMMANDS.has(command)) {
|
|
81
|
+
printUsage();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (command === 'remove' && !arg) {
|
|
85
|
+
console.error('Usage: af-queue-admin remove <session-id>');
|
|
86
|
+
process.exit(1);
|
|
413
87
|
}
|
|
88
|
+
await runQueueAdmin({
|
|
89
|
+
command: command,
|
|
90
|
+
sessionId: arg,
|
|
91
|
+
});
|
|
414
92
|
}
|
|
415
93
|
main().catch((error) => {
|
|
416
94
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* AgentFactory Worker Fleet Manager
|
|
4
4
|
*
|
|
5
5
|
* Spawns and manages multiple worker processes for parallel agent execution.
|
|
6
|
-
*
|
|
6
|
+
* Thin wrapper around the programmatic runner in ./lib/worker-fleet-runner.js.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* af-worker-fleet [options]
|