triflux 3.2.0-dev.9 → 3.3.0-dev.3
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/bin/triflux.mjs +1516 -1386
- package/hooks/hooks.json +22 -0
- package/hooks/keyword-rules.json +4 -4
- package/hooks/pipeline-stop.mjs +54 -0
- package/hub/bridge.mjs +120 -13
- package/hub/pipe.mjs +23 -0
- package/hub/pipeline/index.mjs +121 -0
- package/hub/pipeline/state.mjs +164 -0
- package/hub/pipeline/transitions.mjs +114 -0
- package/hub/router.mjs +322 -1
- package/hub/schema.sql +49 -2
- package/hub/server.mjs +173 -8
- package/hub/store.mjs +259 -1
- package/hub/team/cli-team-control.mjs +381 -381
- package/hub/team/cli-team-start.mjs +474 -470
- package/hub/team/cli-team-status.mjs +238 -238
- package/hub/team/cli.mjs +86 -86
- package/hub/team/native.mjs +144 -6
- package/hub/team/nativeProxy.mjs +51 -38
- package/hub/team/orchestrator.mjs +15 -20
- package/hub/team/pane.mjs +101 -90
- package/hub/team/psmux.mjs +721 -72
- package/hub/team/session.mjs +450 -450
- package/hub/tools.mjs +223 -63
- package/hub/workers/delegator-mcp.mjs +900 -0
- package/hub/workers/factory.mjs +3 -0
- package/hub/workers/interface.mjs +2 -2
- package/hud/hud-qos-status.mjs +89 -144
- package/package.json +1 -1
- package/scripts/__tests__/keyword-detector.test.mjs +11 -11
- package/scripts/__tests__/smoke.test.mjs +34 -0
- package/scripts/hub-ensure.mjs +21 -3
- package/scripts/preflight-cache.mjs +72 -0
- package/scripts/setup.mjs +23 -11
- package/scripts/tfx-route.sh +74 -15
- package/skills/{tfx-team → tfx-multi}/SKILL.md +115 -32
package/hub/server.mjs
CHANGED
|
@@ -14,6 +14,15 @@ import { createRouter } from './router.mjs';
|
|
|
14
14
|
import { createHitlManager } from './hitl.mjs';
|
|
15
15
|
import { createPipeServer } from './pipe.mjs';
|
|
16
16
|
import { createTools } from './tools.mjs';
|
|
17
|
+
import {
|
|
18
|
+
ensurePipelineTable,
|
|
19
|
+
createPipeline,
|
|
20
|
+
} from './pipeline/index.mjs';
|
|
21
|
+
import {
|
|
22
|
+
readPipelineState,
|
|
23
|
+
initPipelineState,
|
|
24
|
+
listPipelineStates,
|
|
25
|
+
} from './pipeline/state.mjs';
|
|
17
26
|
import {
|
|
18
27
|
teamInfo,
|
|
19
28
|
teamTaskList,
|
|
@@ -43,6 +52,14 @@ async function parseBody(req) {
|
|
|
43
52
|
|
|
44
53
|
const PID_DIR = join(homedir(), '.claude', 'cache', 'tfx-hub');
|
|
45
54
|
const PID_FILE = join(PID_DIR, 'hub.pid');
|
|
55
|
+
const TOKEN_FILE = join(homedir(), '.claude', '.tfx-hub-token');
|
|
56
|
+
|
|
57
|
+
// localhost 계열 Origin만 허용
|
|
58
|
+
const ALLOWED_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/;
|
|
59
|
+
|
|
60
|
+
function isAllowedOrigin(origin) {
|
|
61
|
+
return origin && ALLOWED_ORIGIN_RE.test(origin);
|
|
62
|
+
}
|
|
46
63
|
|
|
47
64
|
/**
|
|
48
65
|
* tfx-hub 시작
|
|
@@ -57,6 +74,11 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
57
74
|
dbPath = join(PID_DIR, 'state.db');
|
|
58
75
|
}
|
|
59
76
|
|
|
77
|
+
// 인증 토큰 생성 (환경변수 우선, 없으면 자동 생성)
|
|
78
|
+
const HUB_TOKEN = process.env.TFX_HUB_TOKEN || randomUUID();
|
|
79
|
+
mkdirSync(join(homedir(), '.claude'), { recursive: true });
|
|
80
|
+
writeFileSync(TOKEN_FILE, HUB_TOKEN, { mode: 0o600 });
|
|
81
|
+
|
|
60
82
|
const store = createStore(dbPath);
|
|
61
83
|
const router = createRouter(store);
|
|
62
84
|
const pipe = createPipeServer({ router, store, sessionId });
|
|
@@ -100,12 +122,16 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
100
122
|
}
|
|
101
123
|
|
|
102
124
|
const httpServer = createHttpServer(async (req, res) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
// CORS: localhost 계열 Origin만 허용
|
|
126
|
+
const origin = req.headers['origin'];
|
|
127
|
+
if (isAllowedOrigin(origin)) {
|
|
128
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
129
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
130
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-session-id, Last-Event-ID');
|
|
131
|
+
}
|
|
106
132
|
|
|
107
133
|
if (req.method === 'OPTIONS') {
|
|
108
|
-
res.writeHead(204);
|
|
134
|
+
res.writeHead(isAllowedOrigin(origin) ? 204 : 403);
|
|
109
135
|
return res.end();
|
|
110
136
|
}
|
|
111
137
|
|
|
@@ -132,6 +158,14 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
132
158
|
if (req.url.startsWith('/bridge')) {
|
|
133
159
|
res.setHeader('Content-Type', 'application/json');
|
|
134
160
|
|
|
161
|
+
// Bearer 토큰 인증
|
|
162
|
+
const authHeader = req.headers['authorization'] || '';
|
|
163
|
+
const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
|
|
164
|
+
if (bearerToken !== HUB_TOKEN) {
|
|
165
|
+
res.writeHead(401);
|
|
166
|
+
return res.end(JSON.stringify({ ok: false, error: 'Unauthorized' }));
|
|
167
|
+
}
|
|
168
|
+
|
|
135
169
|
if (req.method !== 'POST' && req.method !== 'DELETE') {
|
|
136
170
|
res.writeHead(405);
|
|
137
171
|
return res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed' }));
|
|
@@ -211,16 +245,111 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
211
245
|
return res.end(JSON.stringify(result));
|
|
212
246
|
}
|
|
213
247
|
|
|
248
|
+
if (path === '/bridge/assign/async' && req.method === 'POST') {
|
|
249
|
+
const {
|
|
250
|
+
supervisor_agent,
|
|
251
|
+
worker_agent,
|
|
252
|
+
task,
|
|
253
|
+
topic = 'assign.job',
|
|
254
|
+
payload = {},
|
|
255
|
+
priority = 5,
|
|
256
|
+
ttl_ms = 600000,
|
|
257
|
+
timeout_ms = 600000,
|
|
258
|
+
max_retries = 0,
|
|
259
|
+
trace_id,
|
|
260
|
+
correlation_id,
|
|
261
|
+
} = body;
|
|
262
|
+
|
|
263
|
+
if (!supervisor_agent || !worker_agent || !task) {
|
|
264
|
+
res.writeHead(400);
|
|
265
|
+
return res.end(JSON.stringify({ ok: false, error: 'supervisor_agent, worker_agent, task 필수' }));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const result = await pipe.executeCommand('assign', {
|
|
269
|
+
supervisor_agent,
|
|
270
|
+
worker_agent,
|
|
271
|
+
task,
|
|
272
|
+
topic,
|
|
273
|
+
payload,
|
|
274
|
+
priority,
|
|
275
|
+
ttl_ms,
|
|
276
|
+
timeout_ms,
|
|
277
|
+
max_retries,
|
|
278
|
+
trace_id,
|
|
279
|
+
correlation_id,
|
|
280
|
+
});
|
|
281
|
+
res.writeHead(result.ok ? 200 : 400);
|
|
282
|
+
return res.end(JSON.stringify(result));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (path === '/bridge/assign/result' && req.method === 'POST') {
|
|
286
|
+
const {
|
|
287
|
+
job_id,
|
|
288
|
+
worker_agent,
|
|
289
|
+
status,
|
|
290
|
+
attempt,
|
|
291
|
+
result: assignResult,
|
|
292
|
+
error: assignError,
|
|
293
|
+
payload = {},
|
|
294
|
+
metadata = {},
|
|
295
|
+
} = body;
|
|
296
|
+
|
|
297
|
+
if (!job_id || !status) {
|
|
298
|
+
res.writeHead(400);
|
|
299
|
+
return res.end(JSON.stringify({ ok: false, error: 'job_id, status 필수' }));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const result = await pipe.executeCommand('assign_result', {
|
|
303
|
+
job_id,
|
|
304
|
+
worker_agent,
|
|
305
|
+
status,
|
|
306
|
+
attempt,
|
|
307
|
+
result: assignResult,
|
|
308
|
+
error: assignError,
|
|
309
|
+
payload,
|
|
310
|
+
metadata,
|
|
311
|
+
});
|
|
312
|
+
res.writeHead(result.ok ? 200 : 409);
|
|
313
|
+
return res.end(JSON.stringify(result));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (path === '/bridge/assign/status' && req.method === 'POST') {
|
|
317
|
+
const result = await pipe.executeQuery('assign_status', body);
|
|
318
|
+
const statusCode = result.ok ? 200 : (result.error?.code === 'ASSIGN_NOT_FOUND' ? 404 : 400);
|
|
319
|
+
res.writeHead(statusCode);
|
|
320
|
+
return res.end(JSON.stringify(result));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (path === '/bridge/assign/retry' && req.method === 'POST') {
|
|
324
|
+
const { job_id, reason, requested_by } = body;
|
|
325
|
+
if (!job_id) {
|
|
326
|
+
res.writeHead(400);
|
|
327
|
+
return res.end(JSON.stringify({ ok: false, error: 'job_id 필수' }));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const result = await pipe.executeCommand('assign_retry', {
|
|
331
|
+
job_id,
|
|
332
|
+
reason,
|
|
333
|
+
requested_by,
|
|
334
|
+
});
|
|
335
|
+
const statusCode = result.ok ? 200
|
|
336
|
+
: result.error?.code === 'ASSIGN_NOT_FOUND' ? 404
|
|
337
|
+
: result.error?.code === 'ASSIGN_RETRY_EXHAUSTED' ? 409
|
|
338
|
+
: 400;
|
|
339
|
+
res.writeHead(statusCode);
|
|
340
|
+
return res.end(JSON.stringify(result));
|
|
341
|
+
}
|
|
342
|
+
|
|
214
343
|
if (req.method === 'POST') {
|
|
215
344
|
let teamResult = null;
|
|
216
345
|
if (path === '/bridge/team/info' || path === '/bridge/team-info') {
|
|
217
|
-
teamResult = teamInfo(body);
|
|
346
|
+
teamResult = await teamInfo(body);
|
|
218
347
|
} else if (path === '/bridge/team/task-list' || path === '/bridge/team-task-list') {
|
|
219
|
-
teamResult = teamTaskList(body);
|
|
348
|
+
teamResult = await teamTaskList(body);
|
|
220
349
|
} else if (path === '/bridge/team/task-update' || path === '/bridge/team-task-update') {
|
|
221
|
-
teamResult = teamTaskUpdate(body);
|
|
350
|
+
teamResult = await teamTaskUpdate(body);
|
|
222
351
|
} else if (path === '/bridge/team/send-message' || path === '/bridge/team-send-message') {
|
|
223
|
-
teamResult = teamSendMessage(body);
|
|
352
|
+
teamResult = await teamSendMessage(body);
|
|
224
353
|
}
|
|
225
354
|
|
|
226
355
|
if (teamResult) {
|
|
@@ -240,6 +369,41 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
240
369
|
res.writeHead(404);
|
|
241
370
|
return res.end(JSON.stringify({ ok: false, error: `Unknown team endpoint: ${path}` }));
|
|
242
371
|
}
|
|
372
|
+
|
|
373
|
+
// ── 파이프라인 엔드포인트 ──
|
|
374
|
+
if (path === '/bridge/pipeline/state' && req.method === 'POST') {
|
|
375
|
+
ensurePipelineTable(store.db);
|
|
376
|
+
const { team_name } = body;
|
|
377
|
+
const state = readPipelineState(store.db, team_name);
|
|
378
|
+
res.writeHead(state ? 200 : 404);
|
|
379
|
+
return res.end(JSON.stringify(state
|
|
380
|
+
? { ok: true, data: state }
|
|
381
|
+
: { ok: false, error: 'pipeline_not_found' }));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (path === '/bridge/pipeline/advance' && req.method === 'POST') {
|
|
385
|
+
ensurePipelineTable(store.db);
|
|
386
|
+
const { team_name, phase } = body;
|
|
387
|
+
const pipeline = createPipeline(store.db, team_name);
|
|
388
|
+
const result = pipeline.advance(phase);
|
|
389
|
+
res.writeHead(result.ok ? 200 : 400);
|
|
390
|
+
return res.end(JSON.stringify(result));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (path === '/bridge/pipeline/init' && req.method === 'POST') {
|
|
394
|
+
ensurePipelineTable(store.db);
|
|
395
|
+
const { team_name, fix_max, ralph_max } = body;
|
|
396
|
+
const state = initPipelineState(store.db, team_name, { fix_max, ralph_max });
|
|
397
|
+
res.writeHead(200);
|
|
398
|
+
return res.end(JSON.stringify({ ok: true, data: state }));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (path === '/bridge/pipeline/list' && req.method === 'POST') {
|
|
402
|
+
ensurePipelineTable(store.db);
|
|
403
|
+
const states = listPipelineStates(store.db);
|
|
404
|
+
res.writeHead(200);
|
|
405
|
+
return res.end(JSON.stringify({ ok: true, data: states }));
|
|
406
|
+
}
|
|
243
407
|
}
|
|
244
408
|
|
|
245
409
|
if (path === '/bridge/context' && req.method === 'POST') {
|
|
@@ -408,6 +572,7 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
408
572
|
await pipe.stop();
|
|
409
573
|
store.close();
|
|
410
574
|
try { unlinkSync(PID_FILE); } catch {}
|
|
575
|
+
try { unlinkSync(TOKEN_FILE); } catch {}
|
|
411
576
|
await new Promise((resolveClose) => httpServer.close(resolveClose));
|
|
412
577
|
};
|
|
413
578
|
|
package/hub/store.mjs
CHANGED
|
@@ -80,6 +80,17 @@ function parseHumanRequestRow(row) {
|
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function parseAssignRow(row) {
|
|
84
|
+
if (!row) return null;
|
|
85
|
+
const { payload_json, result_json, error_json, ...rest } = row;
|
|
86
|
+
return {
|
|
87
|
+
...rest,
|
|
88
|
+
payload: parseJson(payload_json, {}),
|
|
89
|
+
result: parseJson(result_json, null),
|
|
90
|
+
error: parseJson(error_json, null),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
/**
|
|
84
95
|
* 저장소 생성
|
|
85
96
|
* @param {string} dbPath
|
|
@@ -96,7 +107,7 @@ export function createStore(dbPath) {
|
|
|
96
107
|
|
|
97
108
|
const schemaSQL = readFileSync(join(__dirname, 'schema.sql'), 'utf8');
|
|
98
109
|
db.exec("CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)");
|
|
99
|
-
const SCHEMA_VERSION = '
|
|
110
|
+
const SCHEMA_VERSION = '2';
|
|
100
111
|
const curVer = (() => {
|
|
101
112
|
try { return db.prepare("SELECT value FROM _meta WHERE key='schema_version'").pluck().get(); }
|
|
102
113
|
catch { return null; }
|
|
@@ -162,6 +173,45 @@ export function createStore(dbPath) {
|
|
|
162
173
|
insertDL: db.prepare('INSERT OR REPLACE INTO dead_letters (message_id, reason, failed_at_ms, last_error) VALUES (?,?,?,?)'),
|
|
163
174
|
getDL: db.prepare('SELECT * FROM dead_letters ORDER BY failed_at_ms DESC LIMIT ?'),
|
|
164
175
|
|
|
176
|
+
insertAssign: db.prepare(`
|
|
177
|
+
INSERT INTO assign_jobs (
|
|
178
|
+
job_id, supervisor_agent, worker_agent, topic, task, payload_json,
|
|
179
|
+
status, attempt, retry_count, max_retries, priority, ttl_ms, timeout_ms, deadline_ms,
|
|
180
|
+
trace_id, correlation_id, last_message_id, result_json, error_json,
|
|
181
|
+
created_at_ms, updated_at_ms, started_at_ms, completed_at_ms, last_retry_at_ms
|
|
182
|
+
) VALUES (
|
|
183
|
+
@job_id, @supervisor_agent, @worker_agent, @topic, @task, @payload_json,
|
|
184
|
+
@status, @attempt, @retry_count, @max_retries, @priority, @ttl_ms, @timeout_ms, @deadline_ms,
|
|
185
|
+
@trace_id, @correlation_id, @last_message_id, @result_json, @error_json,
|
|
186
|
+
@created_at_ms, @updated_at_ms, @started_at_ms, @completed_at_ms, @last_retry_at_ms
|
|
187
|
+
)`),
|
|
188
|
+
getAssign: db.prepare('SELECT * FROM assign_jobs WHERE job_id = ?'),
|
|
189
|
+
updateAssign: db.prepare(`
|
|
190
|
+
UPDATE assign_jobs SET
|
|
191
|
+
supervisor_agent=@supervisor_agent,
|
|
192
|
+
worker_agent=@worker_agent,
|
|
193
|
+
topic=@topic,
|
|
194
|
+
task=@task,
|
|
195
|
+
payload_json=@payload_json,
|
|
196
|
+
status=@status,
|
|
197
|
+
attempt=@attempt,
|
|
198
|
+
retry_count=@retry_count,
|
|
199
|
+
max_retries=@max_retries,
|
|
200
|
+
priority=@priority,
|
|
201
|
+
ttl_ms=@ttl_ms,
|
|
202
|
+
timeout_ms=@timeout_ms,
|
|
203
|
+
deadline_ms=@deadline_ms,
|
|
204
|
+
trace_id=@trace_id,
|
|
205
|
+
correlation_id=@correlation_id,
|
|
206
|
+
last_message_id=@last_message_id,
|
|
207
|
+
result_json=@result_json,
|
|
208
|
+
error_json=@error_json,
|
|
209
|
+
updated_at_ms=@updated_at_ms,
|
|
210
|
+
started_at_ms=@started_at_ms,
|
|
211
|
+
completed_at_ms=@completed_at_ms,
|
|
212
|
+
last_retry_at_ms=@last_retry_at_ms
|
|
213
|
+
WHERE job_id=@job_id`),
|
|
214
|
+
|
|
165
215
|
findExpired: db.prepare("SELECT id FROM messages WHERE status='queued' AND expires_at_ms < ?"),
|
|
166
216
|
urgentDepth: db.prepare("SELECT COUNT(*) as cnt FROM messages WHERE status='queued' AND priority >= 7"),
|
|
167
217
|
normalDepth: db.prepare("SELECT COUNT(*) as cnt FROM messages WHERE status='queued' AND priority < 7"),
|
|
@@ -169,6 +219,8 @@ export function createStore(dbPath) {
|
|
|
169
219
|
msgCount: db.prepare('SELECT COUNT(*) as cnt FROM messages'),
|
|
170
220
|
dlqDepth: db.prepare('SELECT COUNT(*) as cnt FROM dead_letters'),
|
|
171
221
|
ackedRecent: db.prepare("SELECT COUNT(*) as cnt FROM messages WHERE status='acked' AND created_at_ms > ? - 300000"),
|
|
222
|
+
assignCountByStatus: db.prepare('SELECT COUNT(*) as cnt FROM assign_jobs WHERE status = ?'),
|
|
223
|
+
activeAssignCount: db.prepare("SELECT COUNT(*) as cnt FROM assign_jobs WHERE status IN ('queued','running')"),
|
|
172
224
|
};
|
|
173
225
|
|
|
174
226
|
function clampMaxMessages(value, fallback = 20) {
|
|
@@ -177,6 +229,18 @@ export function createStore(dbPath) {
|
|
|
177
229
|
return Math.max(1, Math.min(Math.trunc(num), 100));
|
|
178
230
|
}
|
|
179
231
|
|
|
232
|
+
function clampPriority(value, fallback = 5) {
|
|
233
|
+
const num = Number(value);
|
|
234
|
+
if (!Number.isFinite(num)) return fallback;
|
|
235
|
+
return Math.max(1, Math.min(Math.trunc(num), 9));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function clampDuration(value, fallback = 600000, min = 1000, max = 86400000) {
|
|
239
|
+
const num = Number(value);
|
|
240
|
+
if (!Number.isFinite(num)) return fallback;
|
|
241
|
+
return Math.max(min, Math.min(Math.trunc(num), max));
|
|
242
|
+
}
|
|
243
|
+
|
|
180
244
|
const store = {
|
|
181
245
|
db,
|
|
182
246
|
uuidv7,
|
|
@@ -365,6 +429,195 @@ export function createStore(dbPath) {
|
|
|
365
429
|
return S.getDL.all(limit);
|
|
366
430
|
},
|
|
367
431
|
|
|
432
|
+
createAssign({
|
|
433
|
+
job_id,
|
|
434
|
+
supervisor_agent,
|
|
435
|
+
worker_agent,
|
|
436
|
+
topic = 'assign.job',
|
|
437
|
+
task = '',
|
|
438
|
+
payload = {},
|
|
439
|
+
status = 'queued',
|
|
440
|
+
attempt = 1,
|
|
441
|
+
retry_count = 0,
|
|
442
|
+
max_retries = 0,
|
|
443
|
+
priority = 5,
|
|
444
|
+
ttl_ms = 600000,
|
|
445
|
+
timeout_ms = 600000,
|
|
446
|
+
deadline_ms,
|
|
447
|
+
trace_id,
|
|
448
|
+
correlation_id,
|
|
449
|
+
last_message_id = null,
|
|
450
|
+
result = null,
|
|
451
|
+
error = null,
|
|
452
|
+
}) {
|
|
453
|
+
const now = Date.now();
|
|
454
|
+
const normalizedTimeout = clampDuration(timeout_ms, 600000);
|
|
455
|
+
const row = {
|
|
456
|
+
job_id: job_id || uuidv7(),
|
|
457
|
+
supervisor_agent,
|
|
458
|
+
worker_agent,
|
|
459
|
+
topic: String(topic || 'assign.job'),
|
|
460
|
+
task: String(task || ''),
|
|
461
|
+
payload_json: JSON.stringify(payload || {}),
|
|
462
|
+
status,
|
|
463
|
+
attempt: Math.max(1, Number(attempt) || 1),
|
|
464
|
+
retry_count: Math.max(0, Number(retry_count) || 0),
|
|
465
|
+
max_retries: Math.max(0, Number(max_retries) || 0),
|
|
466
|
+
priority: clampPriority(priority, 5),
|
|
467
|
+
ttl_ms: clampDuration(ttl_ms, normalizedTimeout),
|
|
468
|
+
timeout_ms: normalizedTimeout,
|
|
469
|
+
deadline_ms: Number.isFinite(Number(deadline_ms))
|
|
470
|
+
? Math.trunc(Number(deadline_ms))
|
|
471
|
+
: now + normalizedTimeout,
|
|
472
|
+
trace_id: trace_id || uuidv7(),
|
|
473
|
+
correlation_id: correlation_id || uuidv7(),
|
|
474
|
+
last_message_id,
|
|
475
|
+
result_json: result == null ? null : JSON.stringify(result),
|
|
476
|
+
error_json: error == null ? null : JSON.stringify(error),
|
|
477
|
+
created_at_ms: now,
|
|
478
|
+
updated_at_ms: now,
|
|
479
|
+
started_at_ms: status === 'running' ? now : null,
|
|
480
|
+
completed_at_ms: ['succeeded', 'failed', 'timed_out'].includes(status) ? now : null,
|
|
481
|
+
last_retry_at_ms: retry_count > 0 ? now : null,
|
|
482
|
+
};
|
|
483
|
+
S.insertAssign.run(row);
|
|
484
|
+
return store.getAssign(row.job_id);
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
getAssign(jobId) {
|
|
488
|
+
return parseAssignRow(S.getAssign.get(jobId));
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
updateAssignStatus(jobId, status, patch = {}) {
|
|
492
|
+
const current = store.getAssign(jobId);
|
|
493
|
+
if (!current) return null;
|
|
494
|
+
|
|
495
|
+
const now = Date.now();
|
|
496
|
+
const nextStatus = status || current.status;
|
|
497
|
+
const isTerminal = ['succeeded', 'failed', 'timed_out'].includes(nextStatus);
|
|
498
|
+
const nextTimeout = clampDuration(patch.timeout_ms ?? current.timeout_ms, current.timeout_ms);
|
|
499
|
+
const nextRow = {
|
|
500
|
+
job_id: current.job_id,
|
|
501
|
+
supervisor_agent: patch.supervisor_agent ?? current.supervisor_agent,
|
|
502
|
+
worker_agent: patch.worker_agent ?? current.worker_agent,
|
|
503
|
+
topic: patch.topic ?? current.topic,
|
|
504
|
+
task: patch.task ?? current.task,
|
|
505
|
+
payload_json: JSON.stringify(patch.payload ?? current.payload ?? {}),
|
|
506
|
+
status: nextStatus,
|
|
507
|
+
attempt: Math.max(1, Number(patch.attempt ?? current.attempt) || current.attempt || 1),
|
|
508
|
+
retry_count: Math.max(0, Number(patch.retry_count ?? current.retry_count) || 0),
|
|
509
|
+
max_retries: Math.max(0, Number(patch.max_retries ?? current.max_retries) || 0),
|
|
510
|
+
priority: clampPriority(patch.priority ?? current.priority, current.priority || 5),
|
|
511
|
+
ttl_ms: clampDuration(patch.ttl_ms ?? current.ttl_ms, current.ttl_ms || nextTimeout),
|
|
512
|
+
timeout_ms: nextTimeout,
|
|
513
|
+
deadline_ms: (() => {
|
|
514
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'deadline_ms')) {
|
|
515
|
+
return patch.deadline_ms == null ? null : Math.trunc(Number(patch.deadline_ms));
|
|
516
|
+
}
|
|
517
|
+
if (isTerminal) return null;
|
|
518
|
+
if (nextStatus === 'running' && !current.deadline_ms) return now + nextTimeout;
|
|
519
|
+
return current.deadline_ms;
|
|
520
|
+
})(),
|
|
521
|
+
trace_id: patch.trace_id ?? current.trace_id,
|
|
522
|
+
correlation_id: patch.correlation_id ?? current.correlation_id,
|
|
523
|
+
last_message_id: Object.prototype.hasOwnProperty.call(patch, 'last_message_id')
|
|
524
|
+
? patch.last_message_id
|
|
525
|
+
: current.last_message_id,
|
|
526
|
+
result_json: Object.prototype.hasOwnProperty.call(patch, 'result')
|
|
527
|
+
? (patch.result == null ? null : JSON.stringify(patch.result))
|
|
528
|
+
: (current.result == null ? null : JSON.stringify(current.result)),
|
|
529
|
+
error_json: Object.prototype.hasOwnProperty.call(patch, 'error')
|
|
530
|
+
? (patch.error == null ? null : JSON.stringify(patch.error))
|
|
531
|
+
: (current.error == null ? null : JSON.stringify(current.error)),
|
|
532
|
+
updated_at_ms: now,
|
|
533
|
+
started_at_ms: Object.prototype.hasOwnProperty.call(patch, 'started_at_ms')
|
|
534
|
+
? patch.started_at_ms
|
|
535
|
+
: (nextStatus === 'running' ? (current.started_at_ms || now) : current.started_at_ms),
|
|
536
|
+
completed_at_ms: Object.prototype.hasOwnProperty.call(patch, 'completed_at_ms')
|
|
537
|
+
? patch.completed_at_ms
|
|
538
|
+
: (isTerminal ? (current.completed_at_ms || now) : current.completed_at_ms),
|
|
539
|
+
last_retry_at_ms: Object.prototype.hasOwnProperty.call(patch, 'last_retry_at_ms')
|
|
540
|
+
? patch.last_retry_at_ms
|
|
541
|
+
: current.last_retry_at_ms,
|
|
542
|
+
};
|
|
543
|
+
S.updateAssign.run(nextRow);
|
|
544
|
+
return store.getAssign(jobId);
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
listAssigns({
|
|
548
|
+
supervisor_agent,
|
|
549
|
+
worker_agent,
|
|
550
|
+
status,
|
|
551
|
+
statuses,
|
|
552
|
+
trace_id,
|
|
553
|
+
correlation_id,
|
|
554
|
+
active_before_ms,
|
|
555
|
+
limit = 50,
|
|
556
|
+
} = {}) {
|
|
557
|
+
const clauses = [];
|
|
558
|
+
const values = [];
|
|
559
|
+
|
|
560
|
+
if (supervisor_agent) {
|
|
561
|
+
clauses.push('supervisor_agent = ?');
|
|
562
|
+
values.push(supervisor_agent);
|
|
563
|
+
}
|
|
564
|
+
if (worker_agent) {
|
|
565
|
+
clauses.push('worker_agent = ?');
|
|
566
|
+
values.push(worker_agent);
|
|
567
|
+
}
|
|
568
|
+
if (trace_id) {
|
|
569
|
+
clauses.push('trace_id = ?');
|
|
570
|
+
values.push(trace_id);
|
|
571
|
+
}
|
|
572
|
+
if (correlation_id) {
|
|
573
|
+
clauses.push('correlation_id = ?');
|
|
574
|
+
values.push(correlation_id);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const statusList = Array.isArray(statuses) && statuses.length
|
|
578
|
+
? statuses
|
|
579
|
+
: (status ? [status] : []);
|
|
580
|
+
if (statusList.length) {
|
|
581
|
+
clauses.push(`status IN (${statusList.map(() => '?').join(',')})`);
|
|
582
|
+
values.push(...statusList);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (Number.isFinite(Number(active_before_ms))) {
|
|
586
|
+
clauses.push('deadline_ms IS NOT NULL AND deadline_ms <= ?');
|
|
587
|
+
values.push(Math.trunc(Number(active_before_ms)));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const sql = `
|
|
591
|
+
SELECT * FROM assign_jobs
|
|
592
|
+
${clauses.length ? `WHERE ${clauses.join(' AND ')}` : ''}
|
|
593
|
+
ORDER BY updated_at_ms DESC
|
|
594
|
+
LIMIT ?`;
|
|
595
|
+
values.push(clampMaxMessages(limit, 50));
|
|
596
|
+
return db.prepare(sql).all(...values).map(parseAssignRow);
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
retryAssign(jobId, patch = {}) {
|
|
600
|
+
const current = store.getAssign(jobId);
|
|
601
|
+
if (!current) return null;
|
|
602
|
+
|
|
603
|
+
const nextRetryCount = Math.max(0, Number(patch.retry_count ?? current.retry_count + 1) || 0);
|
|
604
|
+
const nextAttempt = Math.max(current.attempt + 1, Number(patch.attempt ?? current.attempt + 1) || 1);
|
|
605
|
+
const nextTimeout = clampDuration(patch.timeout_ms ?? current.timeout_ms, current.timeout_ms);
|
|
606
|
+
return store.updateAssignStatus(jobId, 'queued', {
|
|
607
|
+
retry_count: nextRetryCount,
|
|
608
|
+
attempt: nextAttempt,
|
|
609
|
+
timeout_ms: nextTimeout,
|
|
610
|
+
ttl_ms: patch.ttl_ms ?? current.ttl_ms,
|
|
611
|
+
deadline_ms: Date.now() + nextTimeout,
|
|
612
|
+
completed_at_ms: null,
|
|
613
|
+
started_at_ms: null,
|
|
614
|
+
last_retry_at_ms: Date.now(),
|
|
615
|
+
result: patch.result ?? null,
|
|
616
|
+
error: Object.prototype.hasOwnProperty.call(patch, 'error') ? patch.error : current.error,
|
|
617
|
+
last_message_id: null,
|
|
618
|
+
});
|
|
619
|
+
},
|
|
620
|
+
|
|
368
621
|
sweepExpired() {
|
|
369
622
|
const now = Date.now();
|
|
370
623
|
return db.transaction(() => {
|
|
@@ -397,6 +650,7 @@ export function createStore(dbPath) {
|
|
|
397
650
|
return {
|
|
398
651
|
online_agents: S.onlineCount.get().cnt,
|
|
399
652
|
total_messages: S.msgCount.get().cnt,
|
|
653
|
+
active_assign_jobs: S.activeAssignCount.get().cnt,
|
|
400
654
|
...store.getQueueDepths(),
|
|
401
655
|
};
|
|
402
656
|
},
|
|
@@ -406,6 +660,10 @@ export function createStore(dbPath) {
|
|
|
406
660
|
online_agents: S.onlineCount.get().cnt,
|
|
407
661
|
total_messages: S.msgCount.get().cnt,
|
|
408
662
|
dlq: S.dlqDepth.get().cnt,
|
|
663
|
+
assign_queued: S.assignCountByStatus.get('queued').cnt,
|
|
664
|
+
assign_running: S.assignCountByStatus.get('running').cnt,
|
|
665
|
+
assign_failed: S.assignCountByStatus.get('failed').cnt,
|
|
666
|
+
assign_timed_out: S.assignCountByStatus.get('timed_out').cnt,
|
|
409
667
|
};
|
|
410
668
|
},
|
|
411
669
|
};
|