triflux 3.2.0-dev.8 → 3.3.0-dev.1
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 +1296 -1055
- package/hooks/hooks.json +17 -0
- package/hooks/keyword-rules.json +20 -4
- package/hooks/pipeline-stop.mjs +54 -0
- package/hub/bridge.mjs +517 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -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 +422 -161
- package/hub/schema.sql +14 -0
- package/hub/server.mjs +499 -424
- 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 +75 -1475
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +190 -130
- package/hub/team/nativeProxy.mjs +165 -78
- package/hub/team/orchestrator.mjs +15 -20
- package/hub/team/pane.mjs +137 -103
- package/hub/team/psmux.mjs +506 -0
- package/hub/team/session.mjs +393 -330
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +105 -31
- 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 +1790 -1788
- package/package.json +4 -1
- package/scripts/__tests__/keyword-detector.test.mjs +8 -8
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/preflight-cache.mjs +72 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +136 -71
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +485 -91
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +1 -3
- 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 +378 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +0 -304
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
|
}
|
package/hub/schema.sql
CHANGED
|
@@ -78,3 +78,17 @@ CREATE INDEX IF NOT EXISTS idx_inbox_message ON message_inbox(message_id);
|
|
|
78
78
|
CREATE INDEX IF NOT EXISTS idx_human_requests_state ON human_requests(state);
|
|
79
79
|
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
|
|
80
80
|
CREATE INDEX IF NOT EXISTS idx_agents_lease ON agents(lease_expires_ms);
|
|
81
|
+
|
|
82
|
+
-- 파이프라인 상태 테이블 (Phase 2)
|
|
83
|
+
CREATE TABLE IF NOT EXISTS pipeline_state (
|
|
84
|
+
team_name TEXT PRIMARY KEY,
|
|
85
|
+
phase TEXT NOT NULL DEFAULT 'plan',
|
|
86
|
+
fix_attempt INTEGER DEFAULT 0,
|
|
87
|
+
fix_max INTEGER DEFAULT 3,
|
|
88
|
+
ralph_iteration INTEGER DEFAULT 0,
|
|
89
|
+
ralph_max INTEGER DEFAULT 10,
|
|
90
|
+
artifacts TEXT DEFAULT '{}',
|
|
91
|
+
phase_history TEXT DEFAULT '[]',
|
|
92
|
+
created_at INTEGER,
|
|
93
|
+
updated_at INTEGER
|
|
94
|
+
);
|