triflux 3.3.0-dev.1 → 3.3.0-dev.5
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 +169 -39
- package/hooks/hooks.json +5 -0
- package/hub/assign-callbacks.mjs +136 -0
- package/hub/bridge.mjs +283 -97
- package/hub/pipe.mjs +104 -0
- package/hub/router.mjs +322 -1
- package/hub/schema.sql +40 -7
- package/hub/server.mjs +151 -53
- package/hub/store.mjs +293 -1
- package/hub/team/cli-team-status.mjs +17 -3
- package/hub/team/native-supervisor.mjs +62 -22
- package/hub/team/native.mjs +86 -10
- package/hub/team/psmux.mjs +555 -115
- package/hub/tools.mjs +101 -26
- package/hub/workers/delegator-mcp.mjs +1045 -0
- package/hub/workers/factory.mjs +3 -0
- package/hub/workers/interface.mjs +2 -2
- package/hud/hud-qos-status.mjs +1735 -1790
- package/package.json +60 -60
- package/scripts/__tests__/keyword-detector.test.mjs +3 -3
- package/scripts/__tests__/smoke.test.mjs +34 -0
- package/scripts/hub-ensure.mjs +21 -3
- package/scripts/lib/mcp-filter.mjs +637 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/mcp-check.mjs +126 -88
- package/scripts/setup.mjs +15 -10
- package/scripts/test-tfx-route-no-claude-native.mjs +10 -2
- package/scripts/tfx-route.sh +434 -179
package/hub/server.mjs
CHANGED
|
@@ -13,22 +13,8 @@ import { createStore } from './store.mjs';
|
|
|
13
13
|
import { createRouter } from './router.mjs';
|
|
14
14
|
import { createHitlManager } from './hitl.mjs';
|
|
15
15
|
import { createPipeServer } from './pipe.mjs';
|
|
16
|
+
import { createAssignCallbackServer } from './assign-callbacks.mjs';
|
|
16
17
|
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';
|
|
26
|
-
import {
|
|
27
|
-
teamInfo,
|
|
28
|
-
teamTaskList,
|
|
29
|
-
teamTaskUpdate,
|
|
30
|
-
teamSendMessage,
|
|
31
|
-
} from './team/nativeProxy.mjs';
|
|
32
18
|
|
|
33
19
|
function isInitializeRequest(body) {
|
|
34
20
|
if (body?.method === 'initialize') return true;
|
|
@@ -61,6 +47,22 @@ function isAllowedOrigin(origin) {
|
|
|
61
47
|
return origin && ALLOWED_ORIGIN_RE.test(origin);
|
|
62
48
|
}
|
|
63
49
|
|
|
50
|
+
function resolveTeamStatusCode(result) {
|
|
51
|
+
if (result?.ok) return 200;
|
|
52
|
+
const code = result?.error?.code;
|
|
53
|
+
if (code === 'TEAM_NOT_FOUND' || code === 'TASK_NOT_FOUND' || code === 'TASKS_DIR_NOT_FOUND') return 404;
|
|
54
|
+
if (code === 'CLAIM_CONFLICT' || code === 'MTIME_CONFLICT') return 409;
|
|
55
|
+
if (code === 'INVALID_TEAM_NAME' || code === 'INVALID_TASK_ID' || code === 'INVALID_TEXT' || code === 'INVALID_FROM' || code === 'INVALID_STATUS') return 400;
|
|
56
|
+
return 500;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolvePipelineStatusCode(result) {
|
|
60
|
+
if (result?.ok) return 200;
|
|
61
|
+
if (result?.error === 'pipeline_not_found') return 404;
|
|
62
|
+
if (result?.error === 'hub_db_not_found') return 503;
|
|
63
|
+
return 400;
|
|
64
|
+
}
|
|
65
|
+
|
|
64
66
|
/**
|
|
65
67
|
* tfx-hub 시작
|
|
66
68
|
* @param {object} opts
|
|
@@ -82,6 +84,7 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
82
84
|
const store = createStore(dbPath);
|
|
83
85
|
const router = createRouter(store);
|
|
84
86
|
const pipe = createPipeServer({ router, store, sessionId });
|
|
87
|
+
const assignCallbacks = createAssignCallbackServer({ store, sessionId });
|
|
85
88
|
const hitl = createHitlManager(store, router);
|
|
86
89
|
const tools = createTools(store, router, hitl, pipe);
|
|
87
90
|
const transports = new Map();
|
|
@@ -145,6 +148,8 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
145
148
|
port,
|
|
146
149
|
pipe_path: pipe.path,
|
|
147
150
|
pipe: pipe.getStatus(),
|
|
151
|
+
assign_callback_pipe_path: assignCallbacks.path,
|
|
152
|
+
assign_callback_pipe: assignCallbacks.getStatus(),
|
|
148
153
|
}));
|
|
149
154
|
}
|
|
150
155
|
|
|
@@ -245,28 +250,115 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
245
250
|
return res.end(JSON.stringify(result));
|
|
246
251
|
}
|
|
247
252
|
|
|
253
|
+
if (path === '/bridge/assign/async' && req.method === 'POST') {
|
|
254
|
+
const {
|
|
255
|
+
supervisor_agent,
|
|
256
|
+
worker_agent,
|
|
257
|
+
task,
|
|
258
|
+
topic = 'assign.job',
|
|
259
|
+
payload = {},
|
|
260
|
+
priority = 5,
|
|
261
|
+
ttl_ms = 600000,
|
|
262
|
+
timeout_ms = 600000,
|
|
263
|
+
max_retries = 0,
|
|
264
|
+
trace_id,
|
|
265
|
+
correlation_id,
|
|
266
|
+
} = body;
|
|
267
|
+
|
|
268
|
+
if (!supervisor_agent || !worker_agent || !task) {
|
|
269
|
+
res.writeHead(400);
|
|
270
|
+
return res.end(JSON.stringify({ ok: false, error: 'supervisor_agent, worker_agent, task 필수' }));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const result = await pipe.executeCommand('assign', {
|
|
274
|
+
supervisor_agent,
|
|
275
|
+
worker_agent,
|
|
276
|
+
task,
|
|
277
|
+
topic,
|
|
278
|
+
payload,
|
|
279
|
+
priority,
|
|
280
|
+
ttl_ms,
|
|
281
|
+
timeout_ms,
|
|
282
|
+
max_retries,
|
|
283
|
+
trace_id,
|
|
284
|
+
correlation_id,
|
|
285
|
+
});
|
|
286
|
+
res.writeHead(result.ok ? 200 : 400);
|
|
287
|
+
return res.end(JSON.stringify(result));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (path === '/bridge/assign/result' && req.method === 'POST') {
|
|
291
|
+
const {
|
|
292
|
+
job_id,
|
|
293
|
+
worker_agent,
|
|
294
|
+
status,
|
|
295
|
+
attempt,
|
|
296
|
+
result: assignResult,
|
|
297
|
+
error: assignError,
|
|
298
|
+
payload = {},
|
|
299
|
+
metadata = {},
|
|
300
|
+
} = body;
|
|
301
|
+
|
|
302
|
+
if (!job_id || !status) {
|
|
303
|
+
res.writeHead(400);
|
|
304
|
+
return res.end(JSON.stringify({ ok: false, error: 'job_id, status 필수' }));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const result = await pipe.executeCommand('assign_result', {
|
|
308
|
+
job_id,
|
|
309
|
+
worker_agent,
|
|
310
|
+
status,
|
|
311
|
+
attempt,
|
|
312
|
+
result: assignResult,
|
|
313
|
+
error: assignError,
|
|
314
|
+
payload,
|
|
315
|
+
metadata,
|
|
316
|
+
});
|
|
317
|
+
res.writeHead(result.ok ? 200 : 409);
|
|
318
|
+
return res.end(JSON.stringify(result));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (path === '/bridge/assign/status' && req.method === 'POST') {
|
|
322
|
+
const result = await pipe.executeQuery('assign_status', body);
|
|
323
|
+
const statusCode = result.ok ? 200 : (result.error?.code === 'ASSIGN_NOT_FOUND' ? 404 : 400);
|
|
324
|
+
res.writeHead(statusCode);
|
|
325
|
+
return res.end(JSON.stringify(result));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (path === '/bridge/assign/retry' && req.method === 'POST') {
|
|
329
|
+
const { job_id, reason, requested_by } = body;
|
|
330
|
+
if (!job_id) {
|
|
331
|
+
res.writeHead(400);
|
|
332
|
+
return res.end(JSON.stringify({ ok: false, error: 'job_id 필수' }));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const result = await pipe.executeCommand('assign_retry', {
|
|
336
|
+
job_id,
|
|
337
|
+
reason,
|
|
338
|
+
requested_by,
|
|
339
|
+
});
|
|
340
|
+
const statusCode = result.ok ? 200
|
|
341
|
+
: result.error?.code === 'ASSIGN_NOT_FOUND' ? 404
|
|
342
|
+
: result.error?.code === 'ASSIGN_RETRY_EXHAUSTED' ? 409
|
|
343
|
+
: 400;
|
|
344
|
+
res.writeHead(statusCode);
|
|
345
|
+
return res.end(JSON.stringify(result));
|
|
346
|
+
}
|
|
347
|
+
|
|
248
348
|
if (req.method === 'POST') {
|
|
249
349
|
let teamResult = null;
|
|
250
350
|
if (path === '/bridge/team/info' || path === '/bridge/team-info') {
|
|
251
|
-
teamResult = await
|
|
351
|
+
teamResult = await pipe.executeQuery('team_info', body);
|
|
252
352
|
} else if (path === '/bridge/team/task-list' || path === '/bridge/team-task-list') {
|
|
253
|
-
teamResult = await
|
|
353
|
+
teamResult = await pipe.executeQuery('team_task_list', body);
|
|
254
354
|
} else if (path === '/bridge/team/task-update' || path === '/bridge/team-task-update') {
|
|
255
|
-
teamResult = await
|
|
355
|
+
teamResult = await pipe.executeCommand('team_task_update', body);
|
|
256
356
|
} else if (path === '/bridge/team/send-message' || path === '/bridge/team-send-message') {
|
|
257
|
-
teamResult = await
|
|
357
|
+
teamResult = await pipe.executeCommand('team_send_message', body);
|
|
258
358
|
}
|
|
259
359
|
|
|
260
360
|
if (teamResult) {
|
|
261
|
-
|
|
262
|
-
const code = teamResult?.error?.code;
|
|
263
|
-
if (!teamResult.ok) {
|
|
264
|
-
if (code === 'TEAM_NOT_FOUND' || code === 'TASK_NOT_FOUND' || code === 'TASKS_DIR_NOT_FOUND') status = 404;
|
|
265
|
-
else if (code === 'CLAIM_CONFLICT' || code === 'MTIME_CONFLICT') status = 409;
|
|
266
|
-
else if (code === 'INVALID_TEAM_NAME' || code === 'INVALID_TASK_ID' || code === 'INVALID_TEXT' || code === 'INVALID_FROM' || code === 'INVALID_STATUS') status = 400;
|
|
267
|
-
else status = 500;
|
|
268
|
-
}
|
|
269
|
-
res.writeHead(status);
|
|
361
|
+
res.writeHead(resolveTeamStatusCode(teamResult));
|
|
270
362
|
return res.end(JSON.stringify(teamResult));
|
|
271
363
|
}
|
|
272
364
|
|
|
@@ -277,51 +369,42 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
277
369
|
|
|
278
370
|
// ── 파이프라인 엔드포인트 ──
|
|
279
371
|
if (path === '/bridge/pipeline/state' && req.method === 'POST') {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
res.writeHead(state ? 200 : 404);
|
|
284
|
-
return res.end(JSON.stringify(state
|
|
285
|
-
? { ok: true, data: state }
|
|
286
|
-
: { ok: false, error: 'pipeline_not_found' }));
|
|
372
|
+
const result = await pipe.executeQuery('pipeline_state', body);
|
|
373
|
+
res.writeHead(resolvePipelineStatusCode(result));
|
|
374
|
+
return res.end(JSON.stringify(result));
|
|
287
375
|
}
|
|
288
376
|
|
|
289
377
|
if (path === '/bridge/pipeline/advance' && req.method === 'POST') {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const pipeline = createPipeline(store.db, team_name);
|
|
293
|
-
const result = pipeline.advance(phase);
|
|
294
|
-
res.writeHead(result.ok ? 200 : 400);
|
|
378
|
+
const result = await pipe.executeCommand('pipeline_advance', body);
|
|
379
|
+
res.writeHead(resolvePipelineStatusCode(result));
|
|
295
380
|
return res.end(JSON.stringify(result));
|
|
296
381
|
}
|
|
297
382
|
|
|
298
383
|
if (path === '/bridge/pipeline/init' && req.method === 'POST') {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
res.writeHead(200);
|
|
303
|
-
return res.end(JSON.stringify({ ok: true, data: state }));
|
|
384
|
+
const result = await pipe.executeCommand('pipeline_init', body);
|
|
385
|
+
res.writeHead(resolvePipelineStatusCode(result));
|
|
386
|
+
return res.end(JSON.stringify(result));
|
|
304
387
|
}
|
|
305
388
|
|
|
306
389
|
if (path === '/bridge/pipeline/list' && req.method === 'POST') {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
res.
|
|
310
|
-
return res.end(JSON.stringify({ ok: true, data: states }));
|
|
390
|
+
const result = await pipe.executeQuery('pipeline_list', body);
|
|
391
|
+
res.writeHead(resolvePipelineStatusCode(result));
|
|
392
|
+
return res.end(JSON.stringify(result));
|
|
311
393
|
}
|
|
312
394
|
}
|
|
313
395
|
|
|
314
396
|
if (path === '/bridge/context' && req.method === 'POST') {
|
|
315
|
-
const { agent_id, topics, max_messages = 10 } = body;
|
|
397
|
+
const { agent_id, topics, max_messages = 10, auto_ack = true } = body;
|
|
316
398
|
if (!agent_id) {
|
|
317
399
|
res.writeHead(400);
|
|
318
400
|
return res.end(JSON.stringify({ ok: false, error: 'agent_id 필수' }));
|
|
319
401
|
}
|
|
320
402
|
|
|
321
|
-
const result = await pipe.executeQuery('
|
|
403
|
+
const result = await pipe.executeQuery('drain', {
|
|
322
404
|
agent_id,
|
|
323
405
|
topics,
|
|
324
406
|
max_messages,
|
|
407
|
+
auto_ack,
|
|
325
408
|
});
|
|
326
409
|
res.writeHead(200);
|
|
327
410
|
return res.end(JSON.stringify(result));
|
|
@@ -441,6 +524,7 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
441
524
|
|
|
442
525
|
mkdirSync(PID_DIR, { recursive: true });
|
|
443
526
|
await pipe.start();
|
|
527
|
+
await assignCallbacks.start();
|
|
444
528
|
|
|
445
529
|
return new Promise((resolve, reject) => {
|
|
446
530
|
httpServer.listen(port, host, () => {
|
|
@@ -449,9 +533,12 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
449
533
|
host,
|
|
450
534
|
dbPath,
|
|
451
535
|
pid: process.pid,
|
|
536
|
+
hubToken: HUB_TOKEN,
|
|
452
537
|
url: `http://${host}:${port}/mcp`,
|
|
453
538
|
pipe_path: pipe.path,
|
|
454
539
|
pipePath: pipe.path,
|
|
540
|
+
assign_callback_pipe_path: assignCallbacks.path,
|
|
541
|
+
assignCallbackPipePath: assignCallbacks.path,
|
|
455
542
|
};
|
|
456
543
|
|
|
457
544
|
writeFileSync(PID_FILE, JSON.stringify({
|
|
@@ -461,10 +548,11 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
461
548
|
url: info.url,
|
|
462
549
|
pipe_path: pipe.path,
|
|
463
550
|
pipePath: pipe.path,
|
|
551
|
+
assign_callback_pipe_path: assignCallbacks.path,
|
|
464
552
|
started: Date.now(),
|
|
465
553
|
}));
|
|
466
554
|
|
|
467
|
-
console.log(`[tfx-hub] MCP 서버 시작: ${info.url} / pipe ${pipe.path} (PID ${process.pid})`);
|
|
555
|
+
console.log(`[tfx-hub] MCP 서버 시작: ${info.url} / pipe ${pipe.path} / assign-callback ${assignCallbacks.path} (PID ${process.pid})`);
|
|
468
556
|
|
|
469
557
|
const stopFn = async () => {
|
|
470
558
|
router.stopSweeper();
|
|
@@ -475,13 +563,23 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
|
|
|
475
563
|
}
|
|
476
564
|
transports.clear();
|
|
477
565
|
await pipe.stop();
|
|
566
|
+
await assignCallbacks.stop();
|
|
478
567
|
store.close();
|
|
479
568
|
try { unlinkSync(PID_FILE); } catch {}
|
|
480
569
|
try { unlinkSync(TOKEN_FILE); } catch {}
|
|
481
570
|
await new Promise((resolveClose) => httpServer.close(resolveClose));
|
|
482
571
|
};
|
|
483
572
|
|
|
484
|
-
resolve({
|
|
573
|
+
resolve({
|
|
574
|
+
...info,
|
|
575
|
+
httpServer,
|
|
576
|
+
store,
|
|
577
|
+
router,
|
|
578
|
+
hitl,
|
|
579
|
+
pipe,
|
|
580
|
+
assignCallbacks,
|
|
581
|
+
stop: stopFn,
|
|
582
|
+
});
|
|
485
583
|
});
|
|
486
584
|
httpServer.on('error', reject);
|
|
487
585
|
});
|
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,14 +219,46 @@ 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
|
|
|
226
|
+
const assignStatusListeners = new Set();
|
|
227
|
+
|
|
228
|
+
function buildAssignCallbackEvent(row) {
|
|
229
|
+
return {
|
|
230
|
+
job_id: row.job_id,
|
|
231
|
+
status: row.status,
|
|
232
|
+
result: row.result ?? row.error ?? null,
|
|
233
|
+
timestamp: new Date(row.updated_at_ms || Date.now()).toISOString(),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function notifyAssignStatusListeners(row) {
|
|
238
|
+
const event = buildAssignCallbackEvent(row);
|
|
239
|
+
for (const listener of Array.from(assignStatusListeners)) {
|
|
240
|
+
try { listener(event, row); } catch {}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
174
244
|
function clampMaxMessages(value, fallback = 20) {
|
|
175
245
|
const num = Number(value);
|
|
176
246
|
if (!Number.isFinite(num)) return fallback;
|
|
177
247
|
return Math.max(1, Math.min(Math.trunc(num), 100));
|
|
178
248
|
}
|
|
179
249
|
|
|
250
|
+
function clampPriority(value, fallback = 5) {
|
|
251
|
+
const num = Number(value);
|
|
252
|
+
if (!Number.isFinite(num)) return fallback;
|
|
253
|
+
return Math.max(1, Math.min(Math.trunc(num), 9));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function clampDuration(value, fallback = 600000, min = 1000, max = 86400000) {
|
|
257
|
+
const num = Number(value);
|
|
258
|
+
if (!Number.isFinite(num)) return fallback;
|
|
259
|
+
return Math.max(min, Math.min(Math.trunc(num), max));
|
|
260
|
+
}
|
|
261
|
+
|
|
180
262
|
const store = {
|
|
181
263
|
db,
|
|
182
264
|
uuidv7,
|
|
@@ -365,6 +447,201 @@ export function createStore(dbPath) {
|
|
|
365
447
|
return S.getDL.all(limit);
|
|
366
448
|
},
|
|
367
449
|
|
|
450
|
+
createAssign({
|
|
451
|
+
job_id,
|
|
452
|
+
supervisor_agent,
|
|
453
|
+
worker_agent,
|
|
454
|
+
topic = 'assign.job',
|
|
455
|
+
task = '',
|
|
456
|
+
payload = {},
|
|
457
|
+
status = 'queued',
|
|
458
|
+
attempt = 1,
|
|
459
|
+
retry_count = 0,
|
|
460
|
+
max_retries = 0,
|
|
461
|
+
priority = 5,
|
|
462
|
+
ttl_ms = 600000,
|
|
463
|
+
timeout_ms = 600000,
|
|
464
|
+
deadline_ms,
|
|
465
|
+
trace_id,
|
|
466
|
+
correlation_id,
|
|
467
|
+
last_message_id = null,
|
|
468
|
+
result = null,
|
|
469
|
+
error = null,
|
|
470
|
+
}) {
|
|
471
|
+
const now = Date.now();
|
|
472
|
+
const normalizedTimeout = clampDuration(timeout_ms, 600000);
|
|
473
|
+
const row = {
|
|
474
|
+
job_id: job_id || uuidv7(),
|
|
475
|
+
supervisor_agent,
|
|
476
|
+
worker_agent,
|
|
477
|
+
topic: String(topic || 'assign.job'),
|
|
478
|
+
task: String(task || ''),
|
|
479
|
+
payload_json: JSON.stringify(payload || {}),
|
|
480
|
+
status,
|
|
481
|
+
attempt: Math.max(1, Number(attempt) || 1),
|
|
482
|
+
retry_count: Math.max(0, Number(retry_count) || 0),
|
|
483
|
+
max_retries: Math.max(0, Number(max_retries) || 0),
|
|
484
|
+
priority: clampPriority(priority, 5),
|
|
485
|
+
ttl_ms: clampDuration(ttl_ms, normalizedTimeout),
|
|
486
|
+
timeout_ms: normalizedTimeout,
|
|
487
|
+
deadline_ms: Number.isFinite(Number(deadline_ms))
|
|
488
|
+
? Math.trunc(Number(deadline_ms))
|
|
489
|
+
: now + normalizedTimeout,
|
|
490
|
+
trace_id: trace_id || uuidv7(),
|
|
491
|
+
correlation_id: correlation_id || uuidv7(),
|
|
492
|
+
last_message_id,
|
|
493
|
+
result_json: result == null ? null : JSON.stringify(result),
|
|
494
|
+
error_json: error == null ? null : JSON.stringify(error),
|
|
495
|
+
created_at_ms: now,
|
|
496
|
+
updated_at_ms: now,
|
|
497
|
+
started_at_ms: status === 'running' ? now : null,
|
|
498
|
+
completed_at_ms: ['succeeded', 'failed', 'timed_out'].includes(status) ? now : null,
|
|
499
|
+
last_retry_at_ms: retry_count > 0 ? now : null,
|
|
500
|
+
};
|
|
501
|
+
S.insertAssign.run(row);
|
|
502
|
+
const inserted = store.getAssign(row.job_id);
|
|
503
|
+
notifyAssignStatusListeners(inserted);
|
|
504
|
+
return inserted;
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
getAssign(jobId) {
|
|
508
|
+
return parseAssignRow(S.getAssign.get(jobId));
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
updateAssignStatus(jobId, status, patch = {}) {
|
|
512
|
+
const current = store.getAssign(jobId);
|
|
513
|
+
if (!current) return null;
|
|
514
|
+
|
|
515
|
+
const now = Date.now();
|
|
516
|
+
const nextStatus = status || current.status;
|
|
517
|
+
const isTerminal = ['succeeded', 'failed', 'timed_out'].includes(nextStatus);
|
|
518
|
+
const nextTimeout = clampDuration(patch.timeout_ms ?? current.timeout_ms, current.timeout_ms);
|
|
519
|
+
const nextRow = {
|
|
520
|
+
job_id: current.job_id,
|
|
521
|
+
supervisor_agent: patch.supervisor_agent ?? current.supervisor_agent,
|
|
522
|
+
worker_agent: patch.worker_agent ?? current.worker_agent,
|
|
523
|
+
topic: patch.topic ?? current.topic,
|
|
524
|
+
task: patch.task ?? current.task,
|
|
525
|
+
payload_json: JSON.stringify(patch.payload ?? current.payload ?? {}),
|
|
526
|
+
status: nextStatus,
|
|
527
|
+
attempt: Math.max(1, Number(patch.attempt ?? current.attempt) || current.attempt || 1),
|
|
528
|
+
retry_count: Math.max(0, Number(patch.retry_count ?? current.retry_count) || 0),
|
|
529
|
+
max_retries: Math.max(0, Number(patch.max_retries ?? current.max_retries) || 0),
|
|
530
|
+
priority: clampPriority(patch.priority ?? current.priority, current.priority || 5),
|
|
531
|
+
ttl_ms: clampDuration(patch.ttl_ms ?? current.ttl_ms, current.ttl_ms || nextTimeout),
|
|
532
|
+
timeout_ms: nextTimeout,
|
|
533
|
+
deadline_ms: (() => {
|
|
534
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'deadline_ms')) {
|
|
535
|
+
return patch.deadline_ms == null ? null : Math.trunc(Number(patch.deadline_ms));
|
|
536
|
+
}
|
|
537
|
+
if (isTerminal) return null;
|
|
538
|
+
if (nextStatus === 'running' && !current.deadline_ms) return now + nextTimeout;
|
|
539
|
+
return current.deadline_ms;
|
|
540
|
+
})(),
|
|
541
|
+
trace_id: patch.trace_id ?? current.trace_id,
|
|
542
|
+
correlation_id: patch.correlation_id ?? current.correlation_id,
|
|
543
|
+
last_message_id: Object.prototype.hasOwnProperty.call(patch, 'last_message_id')
|
|
544
|
+
? patch.last_message_id
|
|
545
|
+
: current.last_message_id,
|
|
546
|
+
result_json: Object.prototype.hasOwnProperty.call(patch, 'result')
|
|
547
|
+
? (patch.result == null ? null : JSON.stringify(patch.result))
|
|
548
|
+
: (current.result == null ? null : JSON.stringify(current.result)),
|
|
549
|
+
error_json: Object.prototype.hasOwnProperty.call(patch, 'error')
|
|
550
|
+
? (patch.error == null ? null : JSON.stringify(patch.error))
|
|
551
|
+
: (current.error == null ? null : JSON.stringify(current.error)),
|
|
552
|
+
updated_at_ms: now,
|
|
553
|
+
started_at_ms: Object.prototype.hasOwnProperty.call(patch, 'started_at_ms')
|
|
554
|
+
? patch.started_at_ms
|
|
555
|
+
: (nextStatus === 'running' ? (current.started_at_ms || now) : current.started_at_ms),
|
|
556
|
+
completed_at_ms: Object.prototype.hasOwnProperty.call(patch, 'completed_at_ms')
|
|
557
|
+
? patch.completed_at_ms
|
|
558
|
+
: (isTerminal ? (current.completed_at_ms || now) : current.completed_at_ms),
|
|
559
|
+
last_retry_at_ms: Object.prototype.hasOwnProperty.call(patch, 'last_retry_at_ms')
|
|
560
|
+
? patch.last_retry_at_ms
|
|
561
|
+
: current.last_retry_at_ms,
|
|
562
|
+
};
|
|
563
|
+
S.updateAssign.run(nextRow);
|
|
564
|
+
const updated = store.getAssign(jobId);
|
|
565
|
+
if (updated && current.status !== updated.status) {
|
|
566
|
+
notifyAssignStatusListeners(updated);
|
|
567
|
+
}
|
|
568
|
+
return updated;
|
|
569
|
+
},
|
|
570
|
+
|
|
571
|
+
listAssigns({
|
|
572
|
+
supervisor_agent,
|
|
573
|
+
worker_agent,
|
|
574
|
+
status,
|
|
575
|
+
statuses,
|
|
576
|
+
trace_id,
|
|
577
|
+
correlation_id,
|
|
578
|
+
active_before_ms,
|
|
579
|
+
limit = 50,
|
|
580
|
+
} = {}) {
|
|
581
|
+
const clauses = [];
|
|
582
|
+
const values = [];
|
|
583
|
+
|
|
584
|
+
if (supervisor_agent) {
|
|
585
|
+
clauses.push('supervisor_agent = ?');
|
|
586
|
+
values.push(supervisor_agent);
|
|
587
|
+
}
|
|
588
|
+
if (worker_agent) {
|
|
589
|
+
clauses.push('worker_agent = ?');
|
|
590
|
+
values.push(worker_agent);
|
|
591
|
+
}
|
|
592
|
+
if (trace_id) {
|
|
593
|
+
clauses.push('trace_id = ?');
|
|
594
|
+
values.push(trace_id);
|
|
595
|
+
}
|
|
596
|
+
if (correlation_id) {
|
|
597
|
+
clauses.push('correlation_id = ?');
|
|
598
|
+
values.push(correlation_id);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const statusList = Array.isArray(statuses) && statuses.length
|
|
602
|
+
? statuses
|
|
603
|
+
: (status ? [status] : []);
|
|
604
|
+
if (statusList.length) {
|
|
605
|
+
clauses.push(`status IN (${statusList.map(() => '?').join(',')})`);
|
|
606
|
+
values.push(...statusList);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (Number.isFinite(Number(active_before_ms))) {
|
|
610
|
+
clauses.push('deadline_ms IS NOT NULL AND deadline_ms <= ?');
|
|
611
|
+
values.push(Math.trunc(Number(active_before_ms)));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const sql = `
|
|
615
|
+
SELECT * FROM assign_jobs
|
|
616
|
+
${clauses.length ? `WHERE ${clauses.join(' AND ')}` : ''}
|
|
617
|
+
ORDER BY updated_at_ms DESC
|
|
618
|
+
LIMIT ?`;
|
|
619
|
+
values.push(clampMaxMessages(limit, 50));
|
|
620
|
+
return db.prepare(sql).all(...values).map(parseAssignRow);
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
retryAssign(jobId, patch = {}) {
|
|
624
|
+
const current = store.getAssign(jobId);
|
|
625
|
+
if (!current) return null;
|
|
626
|
+
|
|
627
|
+
const nextRetryCount = Math.max(0, Number(patch.retry_count ?? current.retry_count + 1) || 0);
|
|
628
|
+
const nextAttempt = Math.max(current.attempt + 1, Number(patch.attempt ?? current.attempt + 1) || 1);
|
|
629
|
+
const nextTimeout = clampDuration(patch.timeout_ms ?? current.timeout_ms, current.timeout_ms);
|
|
630
|
+
return store.updateAssignStatus(jobId, 'queued', {
|
|
631
|
+
retry_count: nextRetryCount,
|
|
632
|
+
attempt: nextAttempt,
|
|
633
|
+
timeout_ms: nextTimeout,
|
|
634
|
+
ttl_ms: patch.ttl_ms ?? current.ttl_ms,
|
|
635
|
+
deadline_ms: Date.now() + nextTimeout,
|
|
636
|
+
completed_at_ms: null,
|
|
637
|
+
started_at_ms: null,
|
|
638
|
+
last_retry_at_ms: Date.now(),
|
|
639
|
+
result: patch.result ?? null,
|
|
640
|
+
error: Object.prototype.hasOwnProperty.call(patch, 'error') ? patch.error : current.error,
|
|
641
|
+
last_message_id: null,
|
|
642
|
+
});
|
|
643
|
+
},
|
|
644
|
+
|
|
368
645
|
sweepExpired() {
|
|
369
646
|
const now = Date.now();
|
|
370
647
|
return db.transaction(() => {
|
|
@@ -386,6 +663,16 @@ export function createStore(dbPath) {
|
|
|
386
663
|
};
|
|
387
664
|
},
|
|
388
665
|
|
|
666
|
+
onAssignStatusChange(listener) {
|
|
667
|
+
if (typeof listener !== 'function') {
|
|
668
|
+
return () => {};
|
|
669
|
+
}
|
|
670
|
+
assignStatusListeners.add(listener);
|
|
671
|
+
return () => {
|
|
672
|
+
assignStatusListeners.delete(listener);
|
|
673
|
+
};
|
|
674
|
+
},
|
|
675
|
+
|
|
389
676
|
getDeliveryStats() {
|
|
390
677
|
return {
|
|
391
678
|
total_deliveries: S.ackedRecent.get(Date.now()).cnt,
|
|
@@ -397,6 +684,7 @@ export function createStore(dbPath) {
|
|
|
397
684
|
return {
|
|
398
685
|
online_agents: S.onlineCount.get().cnt,
|
|
399
686
|
total_messages: S.msgCount.get().cnt,
|
|
687
|
+
active_assign_jobs: S.activeAssignCount.get().cnt,
|
|
400
688
|
...store.getQueueDepths(),
|
|
401
689
|
};
|
|
402
690
|
},
|
|
@@ -406,6 +694,10 @@ export function createStore(dbPath) {
|
|
|
406
694
|
online_agents: S.onlineCount.get().cnt,
|
|
407
695
|
total_messages: S.msgCount.get().cnt,
|
|
408
696
|
dlq: S.dlqDepth.get().cnt,
|
|
697
|
+
assign_queued: S.assignCountByStatus.get('queued').cnt,
|
|
698
|
+
assign_running: S.assignCountByStatus.get('running').cnt,
|
|
699
|
+
assign_failed: S.assignCountByStatus.get('failed').cnt,
|
|
700
|
+
assign_timed_out: S.assignCountByStatus.get('timed_out').cnt,
|
|
409
701
|
};
|
|
410
702
|
},
|
|
411
703
|
};
|