triflux 3.2.0-dev.1 → 3.2.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +26 -18
- package/README.md +26 -18
- package/bin/triflux.mjs +1614 -1084
- package/hooks/hooks.json +12 -0
- package/hooks/keyword-rules.json +354 -0
- package/hub/bridge.mjs +371 -193
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/router.mjs +422 -161
- package/hub/server.mjs +429 -344
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +99 -368
- package/hub/team/dashboard.mjs +165 -64
- package/hub/team/native-supervisor.mjs +300 -0
- package/hub/team/native.mjs +62 -0
- package/hub/team/nativeProxy.mjs +534 -0
- package/hub/team/orchestrator.mjs +99 -35
- package/hub/team/pane.mjs +138 -101
- package/hub/team/psmux.mjs +297 -0
- package/hub/team/session.mjs +608 -186
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +140 -53
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +1789 -1732
- package/package.json +6 -2
- package/scripts/__tests__/keyword-detector.test.mjs +234 -0
- package/scripts/hub-ensure.mjs +83 -0
- package/scripts/keyword-detector.mjs +272 -0
- package/scripts/keyword-rules-expander.mjs +521 -0
- package/scripts/lib/keyword-rules.mjs +168 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/run.cjs +62 -0
- package/scripts/setup.mjs +189 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +943 -508
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +77 -0
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-multi/SKILL.md +296 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +0 -172
package/hub/router.mjs
CHANGED
|
@@ -1,73 +1,306 @@
|
|
|
1
|
-
// hub/router.mjs —
|
|
2
|
-
//
|
|
1
|
+
// hub/router.mjs — 실시간 라우팅/수신함 상태 관리자
|
|
2
|
+
// SQLite는 감사 로그만 담당하고, 실제 배달 상태는 메모리에서 관리한다.
|
|
3
3
|
import { EventEmitter, once } from 'node:events';
|
|
4
4
|
import { uuidv7 } from './store.mjs';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
|
|
6
|
+
function uniqueStrings(values = []) {
|
|
7
|
+
return Array.from(new Set((values || []).map((value) => String(value || '').trim()).filter(Boolean)));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeAgentTopics(store, agentId, runtimeTopics) {
|
|
11
|
+
const topics = new Set(runtimeTopics || []);
|
|
12
|
+
const persisted = store.getAgent(agentId)?.topics || [];
|
|
13
|
+
for (const topic of persisted) topics.add(topic);
|
|
14
|
+
return Array.from(topics);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 라우터 생성
|
|
19
|
+
* @param {object} store
|
|
20
|
+
*/
|
|
10
21
|
export function createRouter(store) {
|
|
11
22
|
let sweepTimer = null;
|
|
12
23
|
let staleTimer = null;
|
|
13
24
|
const responseEmitter = new EventEmitter();
|
|
25
|
+
const deliveryEmitter = new EventEmitter();
|
|
14
26
|
responseEmitter.setMaxListeners(200);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
27
|
+
deliveryEmitter.setMaxListeners(200);
|
|
28
|
+
|
|
29
|
+
const runtimeTopics = new Map();
|
|
30
|
+
const queuesByAgent = new Map();
|
|
31
|
+
const liveMessages = new Map();
|
|
32
|
+
const deliveryLatencies = [];
|
|
33
|
+
|
|
34
|
+
function ensureAgentQueue(agentId) {
|
|
35
|
+
let queue = queuesByAgent.get(agentId);
|
|
36
|
+
if (!queue) {
|
|
37
|
+
queue = new Map();
|
|
38
|
+
queuesByAgent.set(agentId, queue);
|
|
39
|
+
}
|
|
40
|
+
return queue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function pruneDeliveryStats(now = Date.now()) {
|
|
44
|
+
while (deliveryLatencies.length && deliveryLatencies[0].at < now - 300000) {
|
|
45
|
+
deliveryLatencies.shift();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function upsertRuntimeTopics(agentId, topics, { replace = true } = {}) {
|
|
50
|
+
const normalized = uniqueStrings(topics);
|
|
51
|
+
const current = replace ? new Set() : new Set(runtimeTopics.get(agentId) || []);
|
|
52
|
+
for (const topic of normalized) current.add(topic);
|
|
53
|
+
runtimeTopics.set(agentId, current);
|
|
54
|
+
store.updateAgentTopics(agentId, Array.from(current));
|
|
55
|
+
return Array.from(current);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function listRuntimeTopics(agentId) {
|
|
59
|
+
return normalizeAgentTopics(store, agentId, runtimeTopics.get(agentId));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function trackMessage(message, recipients) {
|
|
63
|
+
liveMessages.set(message.id, {
|
|
64
|
+
message,
|
|
65
|
+
recipients: new Set(recipients),
|
|
66
|
+
ackedBy: new Set(),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getMessageRecord(messageId) {
|
|
71
|
+
return liveMessages.get(messageId) || null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function removeMessage(messageId) {
|
|
75
|
+
const record = liveMessages.get(messageId);
|
|
76
|
+
if (!record) return;
|
|
77
|
+
for (const agentId of record.recipients) {
|
|
78
|
+
queuesByAgent.get(agentId)?.delete(messageId);
|
|
79
|
+
}
|
|
80
|
+
liveMessages.delete(messageId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function queueMessage(agentId, message) {
|
|
84
|
+
const queue = ensureAgentQueue(agentId);
|
|
85
|
+
queue.set(message.id, {
|
|
86
|
+
message,
|
|
87
|
+
attempts: 0,
|
|
88
|
+
delivered_at_ms: null,
|
|
89
|
+
acked_at_ms: null,
|
|
90
|
+
});
|
|
91
|
+
deliveryEmitter.emit('message', agentId, message);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveRecipients(msg) {
|
|
95
|
+
const to = msg.to_agent ?? msg.to;
|
|
96
|
+
if (!to?.startsWith('topic:')) {
|
|
97
|
+
return [to];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const topic = to.slice(6);
|
|
101
|
+
const recipients = new Set();
|
|
102
|
+
for (const [agentId, topics] of runtimeTopics) {
|
|
103
|
+
if (topics.has(topic)) recipients.add(agentId);
|
|
104
|
+
}
|
|
105
|
+
for (const agent of store.getAgentsByTopic(topic)) {
|
|
106
|
+
recipients.add(agent.agent_id);
|
|
107
|
+
}
|
|
108
|
+
return Array.from(recipients);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function sortedPending(agentId, { max_messages = 20, include_topics = null } = {}) {
|
|
112
|
+
const queue = ensureAgentQueue(agentId);
|
|
113
|
+
const topicFilter = include_topics?.length ? new Set(include_topics) : null;
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
const pending = [];
|
|
116
|
+
|
|
117
|
+
for (const delivery of queue.values()) {
|
|
118
|
+
const { message } = delivery;
|
|
119
|
+
if (delivery.acked_at_ms) continue;
|
|
120
|
+
if (message.expires_at_ms <= now) continue;
|
|
121
|
+
if (topicFilter && !topicFilter.has(message.topic)) continue;
|
|
122
|
+
pending.push(message);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pending.sort((a, b) => {
|
|
126
|
+
if (b.priority !== a.priority) return b.priority - a.priority;
|
|
127
|
+
return a.created_at_ms - b.created_at_ms;
|
|
128
|
+
});
|
|
129
|
+
return pending.slice(0, max_messages);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function markDelivered(agentId, messageId) {
|
|
133
|
+
const delivery = queuesByAgent.get(agentId)?.get(messageId);
|
|
134
|
+
const record = getMessageRecord(messageId);
|
|
135
|
+
if (!delivery || !record) return false;
|
|
136
|
+
|
|
137
|
+
delivery.attempts += 1;
|
|
138
|
+
if (!delivery.delivered_at_ms) {
|
|
139
|
+
delivery.delivered_at_ms = Date.now();
|
|
140
|
+
record.message.status = 'delivered';
|
|
141
|
+
store.updateMessageStatus(messageId, 'delivered');
|
|
142
|
+
deliveryLatencies.push({
|
|
143
|
+
at: delivery.delivered_at_ms,
|
|
144
|
+
ms: delivery.delivered_at_ms - record.message.created_at_ms,
|
|
145
|
+
});
|
|
146
|
+
pruneDeliveryStats(delivery.delivered_at_ms);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function ackMessages(ids, agentId) {
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
let count = 0;
|
|
155
|
+
|
|
156
|
+
for (const id of ids || []) {
|
|
157
|
+
const delivery = queuesByAgent.get(agentId)?.get(id);
|
|
158
|
+
const record = getMessageRecord(id);
|
|
159
|
+
if (!delivery || !record || delivery.acked_at_ms) continue;
|
|
160
|
+
|
|
161
|
+
delivery.acked_at_ms = now;
|
|
162
|
+
record.ackedBy.add(agentId);
|
|
163
|
+
count += 1;
|
|
164
|
+
|
|
165
|
+
if (record.ackedBy.size >= record.recipients.size) {
|
|
166
|
+
record.message.status = 'acked';
|
|
167
|
+
store.updateMessageStatus(id, 'acked');
|
|
168
|
+
removeMessage(id);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return count;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function dispatchMessage({ type, from, to, topic, priority = 5, ttl_ms = 300000, payload = {}, trace_id, correlation_id }) {
|
|
176
|
+
const msg = store.auditLog({
|
|
177
|
+
type,
|
|
178
|
+
from,
|
|
179
|
+
to,
|
|
180
|
+
topic,
|
|
181
|
+
priority,
|
|
182
|
+
ttl_ms,
|
|
183
|
+
payload,
|
|
184
|
+
trace_id,
|
|
185
|
+
correlation_id,
|
|
186
|
+
});
|
|
187
|
+
const recipients = uniqueStrings(resolveRecipients(msg));
|
|
188
|
+
if (recipients.length) {
|
|
189
|
+
trackMessage(msg, recipients);
|
|
190
|
+
for (const agentId of recipients) {
|
|
191
|
+
queueMessage(agentId, msg);
|
|
192
|
+
}
|
|
193
|
+
msg.status = 'delivered';
|
|
194
|
+
store.updateMessageStatus(msg.id, 'delivered');
|
|
195
|
+
}
|
|
196
|
+
if (msg.type === 'response') {
|
|
197
|
+
responseEmitter.emit(msg.correlation_id, msg.payload);
|
|
198
|
+
}
|
|
199
|
+
return { msg, recipients };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const router = {
|
|
203
|
+
responseEmitter,
|
|
204
|
+
deliveryEmitter,
|
|
205
|
+
|
|
206
|
+
registerAgent(args) {
|
|
207
|
+
const result = store.registerAgent(args);
|
|
208
|
+
upsertRuntimeTopics(args.agent_id, args.topics || [], { replace: true });
|
|
209
|
+
return result;
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
refreshAgentLease(agentId, ttlMs = 30000) {
|
|
213
|
+
return store.refreshLease(agentId, ttlMs);
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
subscribeAgent(agentId, topics, { replace = false } = {}) {
|
|
217
|
+
const nextTopics = upsertRuntimeTopics(agentId, topics, { replace });
|
|
218
|
+
return { agent_id: agentId, topics: nextTopics };
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
getSubscribedTopics(agentId) {
|
|
222
|
+
return listRuntimeTopics(agentId);
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
updateAgentStatus(agentId, status) {
|
|
226
|
+
if (status === 'offline') {
|
|
227
|
+
runtimeTopics.delete(agentId);
|
|
228
|
+
}
|
|
229
|
+
return store.updateAgentStatus(agentId, status);
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
route(msg) {
|
|
233
|
+
const recipients = uniqueStrings(resolveRecipients(msg));
|
|
234
|
+
if (!recipients.length) return 0;
|
|
235
|
+
if (!getMessageRecord(msg.id)) {
|
|
236
|
+
trackMessage(msg, recipients);
|
|
237
|
+
}
|
|
238
|
+
for (const agentId of recipients) {
|
|
239
|
+
queueMessage(agentId, msg);
|
|
240
|
+
}
|
|
241
|
+
store.updateMessageStatus(msg.id, 'delivered');
|
|
242
|
+
return recipients.length;
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
getPendingMessages(agentId, options = {}) {
|
|
246
|
+
return sortedPending(agentId, options);
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
markMessagePushed(agentId, messageId) {
|
|
250
|
+
return markDelivered(agentId, messageId);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
drainAgent(agentId, { max_messages = 20, include_topics = null, auto_ack = false } = {}) {
|
|
254
|
+
const messages = sortedPending(agentId, { max_messages, include_topics });
|
|
255
|
+
for (const message of messages) {
|
|
256
|
+
markDelivered(agentId, message.id);
|
|
257
|
+
}
|
|
258
|
+
if (auto_ack && messages.length) {
|
|
259
|
+
ackMessages(messages.map((message) => message.id), agentId);
|
|
260
|
+
}
|
|
261
|
+
return messages;
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
ackMessages(ids, agentId) {
|
|
265
|
+
return ackMessages(ids, agentId);
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
async handleAsk({
|
|
269
|
+
from, to, topic, question, context_refs,
|
|
270
|
+
payload = {}, priority = 5, ttl_ms = 300000,
|
|
271
|
+
await_response_ms = 0, trace_id, correlation_id,
|
|
272
|
+
}) {
|
|
273
|
+
const cid = correlation_id || uuidv7();
|
|
274
|
+
const tid = trace_id || uuidv7();
|
|
275
|
+
|
|
276
|
+
const { msg } = dispatchMessage({
|
|
277
|
+
type: 'request',
|
|
278
|
+
from,
|
|
279
|
+
to,
|
|
280
|
+
topic,
|
|
281
|
+
priority,
|
|
282
|
+
ttl_ms,
|
|
283
|
+
payload: { question, context_refs, ...payload },
|
|
284
|
+
correlation_id: cid,
|
|
285
|
+
trace_id: tid,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (await_response_ms <= 0) {
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'queued' },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
61
295
|
try {
|
|
62
|
-
const [
|
|
296
|
+
const [response] = await once(responseEmitter, cid, {
|
|
63
297
|
signal: AbortSignal.timeout(Math.min(await_response_ms, 30000)),
|
|
64
298
|
});
|
|
65
299
|
return {
|
|
66
300
|
ok: true,
|
|
67
|
-
data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'answered', response
|
|
301
|
+
data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'answered', response },
|
|
68
302
|
};
|
|
69
303
|
} catch {
|
|
70
|
-
// 타임아웃 — DB에서 최종 확인
|
|
71
304
|
const resp = store.getResponseByCorrelation(cid);
|
|
72
305
|
if (resp) {
|
|
73
306
|
return {
|
|
@@ -80,121 +313,149 @@ export function createRouter(store) {
|
|
|
80
313
|
data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'delivered' },
|
|
81
314
|
};
|
|
82
315
|
}
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
handlePublish({
|
|
319
|
+
from, to, topic, priority = 5, ttl_ms = 300000,
|
|
320
|
+
payload = {}, trace_id, correlation_id, message_type,
|
|
321
|
+
}) {
|
|
322
|
+
const type = message_type || (correlation_id ? 'response' : 'event');
|
|
323
|
+
const { msg, recipients } = dispatchMessage({
|
|
324
|
+
type,
|
|
325
|
+
from,
|
|
326
|
+
to,
|
|
327
|
+
topic,
|
|
328
|
+
priority,
|
|
329
|
+
ttl_ms,
|
|
330
|
+
payload,
|
|
331
|
+
trace_id: trace_id || uuidv7(),
|
|
96
332
|
correlation_id: correlation_id || uuidv7(),
|
|
333
|
+
});
|
|
334
|
+
return {
|
|
335
|
+
ok: true,
|
|
336
|
+
data: {
|
|
337
|
+
message_id: msg.id,
|
|
338
|
+
fanout_count: recipients.length,
|
|
339
|
+
expires_at_ms: msg.expires_at_ms,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
handleHandoff({
|
|
345
|
+
from, to, topic, task, acceptance_criteria, context_refs,
|
|
346
|
+
priority = 5, ttl_ms = 600000, trace_id, correlation_id,
|
|
347
|
+
}) {
|
|
348
|
+
const { msg } = dispatchMessage({
|
|
349
|
+
type: 'handoff',
|
|
350
|
+
from,
|
|
351
|
+
to,
|
|
352
|
+
topic,
|
|
353
|
+
priority,
|
|
354
|
+
ttl_ms,
|
|
355
|
+
payload: { task, acceptance_criteria, context_refs },
|
|
97
356
|
trace_id: trace_id || uuidv7(),
|
|
357
|
+
correlation_id: correlation_id || uuidv7(),
|
|
98
358
|
});
|
|
99
|
-
const fanout = router.route(msg);
|
|
100
|
-
if (correlation_id) {
|
|
101
|
-
responseEmitter.emit(correlation_id, msg.payload);
|
|
102
|
-
}
|
|
103
359
|
return {
|
|
104
360
|
ok: true,
|
|
105
|
-
data: {
|
|
361
|
+
data: { handoff_message_id: msg.id, state: 'queued', assigned_to: to },
|
|
106
362
|
};
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
trace_id: trace_id || uuidv7(),
|
|
122
|
-
});
|
|
123
|
-
router.route(msg);
|
|
124
|
-
return {
|
|
125
|
-
ok: true,
|
|
126
|
-
data: { handoff_message_id: msg.id, state: 'queued', assigned_to: to },
|
|
127
|
-
};
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
// ── 스위퍼 ──
|
|
131
|
-
|
|
132
|
-
/** 주기적 만료 정리 시작 (10초: 메시지, 60초: 비활성 에이전트) */
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
sweepExpired() {
|
|
366
|
+
const now = Date.now();
|
|
367
|
+
let expired = 0;
|
|
368
|
+
for (const [messageId, record] of Array.from(liveMessages.entries())) {
|
|
369
|
+
if (record.message.expires_at_ms > now) continue;
|
|
370
|
+
store.moveToDeadLetter(messageId, 'ttl_expired', null);
|
|
371
|
+
removeMessage(messageId);
|
|
372
|
+
expired += 1;
|
|
373
|
+
}
|
|
374
|
+
return { messages: expired };
|
|
375
|
+
},
|
|
376
|
+
|
|
133
377
|
startSweeper() {
|
|
134
378
|
if (sweepTimer) return;
|
|
135
379
|
sweepTimer = setInterval(() => {
|
|
136
|
-
try {
|
|
380
|
+
try { router.sweepExpired(); } catch {}
|
|
137
381
|
}, 10000);
|
|
138
382
|
staleTimer = setInterval(() => {
|
|
139
|
-
try { store.sweepStaleAgents(); } catch {
|
|
383
|
+
try { store.sweepStaleAgents(); } catch {}
|
|
140
384
|
}, 120000);
|
|
141
|
-
sweepTimer.unref();
|
|
142
|
-
staleTimer.unref();
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
385
|
+
sweepTimer.unref();
|
|
386
|
+
staleTimer.unref();
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
stopSweeper() {
|
|
390
|
+
if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
|
|
391
|
+
if (staleTimer) { clearInterval(staleTimer); staleTimer = null; }
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
getQueueDepths() {
|
|
395
|
+
const counts = { urgent: 0, normal: 0, dlq: store.getAuditStats().dlq };
|
|
396
|
+
for (const record of liveMessages.values()) {
|
|
397
|
+
const pending = record.recipients.size > record.ackedBy.size;
|
|
398
|
+
if (!pending) continue;
|
|
399
|
+
if (record.message.priority >= 7) counts.urgent += 1;
|
|
400
|
+
else counts.normal += 1;
|
|
401
|
+
}
|
|
402
|
+
return counts;
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
getDeliveryStats() {
|
|
406
|
+
pruneDeliveryStats();
|
|
407
|
+
if (!deliveryLatencies.length) {
|
|
408
|
+
return { total_deliveries: 0, avg_delivery_ms: 0 };
|
|
409
|
+
}
|
|
410
|
+
const total = deliveryLatencies.reduce((sum, item) => sum + item.ms, 0);
|
|
411
|
+
return {
|
|
412
|
+
total_deliveries: deliveryLatencies.length,
|
|
413
|
+
avg_delivery_ms: Math.round(total / deliveryLatencies.length),
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
getStatus(scope = 'hub', { agent_id, trace_id, include_metrics = true } = {}) {
|
|
418
|
+
const data = {};
|
|
419
|
+
|
|
420
|
+
if (scope === 'hub' || scope === 'queue') {
|
|
421
|
+
data.hub = {
|
|
422
|
+
state: 'healthy',
|
|
423
|
+
uptime_ms: process.uptime() * 1000 | 0,
|
|
424
|
+
realtime_transport: 'named-pipe',
|
|
425
|
+
audit_store: 'sqlite',
|
|
426
|
+
};
|
|
427
|
+
if (include_metrics) {
|
|
428
|
+
const depths = router.getQueueDepths();
|
|
429
|
+
const stats = router.getDeliveryStats();
|
|
430
|
+
data.queues = {
|
|
431
|
+
urgent_depth: depths.urgent,
|
|
432
|
+
normal_depth: depths.normal,
|
|
433
|
+
dlq_depth: depths.dlq,
|
|
434
|
+
avg_delivery_ms: stats.avg_delivery_ms,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (scope === 'agent' && agent_id) {
|
|
440
|
+
const agent = store.getAgent(agent_id);
|
|
441
|
+
if (agent) {
|
|
442
|
+
data.agent = {
|
|
443
|
+
agent_id: agent.agent_id,
|
|
444
|
+
status: agent.status,
|
|
445
|
+
pending: sortedPending(agent_id, { max_messages: 1000 }).length,
|
|
446
|
+
last_seen_ms: agent.last_seen_ms,
|
|
447
|
+
topics: listRuntimeTopics(agent_id),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (scope === 'trace' && trace_id) {
|
|
453
|
+
data.trace = store.getMessagesByTrace(trace_id);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return { ok: true, data };
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
return router;
|
|
200
461
|
}
|