@triflux/core 10.0.0-alpha.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/hooks/agent-route-guard.mjs +109 -0
- package/hooks/cross-review-tracker.mjs +122 -0
- package/hooks/error-context.mjs +148 -0
- package/hooks/hook-manager.mjs +352 -0
- package/hooks/hook-orchestrator.mjs +312 -0
- package/hooks/hook-registry.json +213 -0
- package/hooks/hooks.json +89 -0
- package/hooks/keyword-rules.json +581 -0
- package/hooks/lib/resolve-root.mjs +59 -0
- package/hooks/mcp-config-watcher.mjs +85 -0
- package/hooks/pipeline-stop.mjs +76 -0
- package/hooks/safety-guard.mjs +106 -0
- package/hooks/subagent-verifier.mjs +80 -0
- package/hub/assign-callbacks.mjs +133 -0
- package/hub/bridge.mjs +799 -0
- package/hub/cli-adapter-base.mjs +192 -0
- package/hub/codex-adapter.mjs +190 -0
- package/hub/codex-compat.mjs +78 -0
- package/hub/codex-preflight.mjs +147 -0
- package/hub/delegator/contracts.mjs +37 -0
- package/hub/delegator/index.mjs +14 -0
- package/hub/delegator/schema/delegator-tools.schema.json +250 -0
- package/hub/delegator/service.mjs +307 -0
- package/hub/delegator/tool-definitions.mjs +35 -0
- package/hub/fullcycle.mjs +96 -0
- package/hub/gemini-adapter.mjs +179 -0
- package/hub/hitl.mjs +143 -0
- package/hub/intent.mjs +193 -0
- package/hub/lib/process-utils.mjs +361 -0
- package/hub/middleware/request-logger.mjs +81 -0
- package/hub/paths.mjs +30 -0
- package/hub/pipeline/gates/confidence.mjs +56 -0
- package/hub/pipeline/gates/consensus.mjs +94 -0
- package/hub/pipeline/gates/index.mjs +5 -0
- package/hub/pipeline/gates/selfcheck.mjs +82 -0
- package/hub/pipeline/index.mjs +318 -0
- package/hub/pipeline/state.mjs +191 -0
- package/hub/pipeline/transitions.mjs +124 -0
- package/hub/platform.mjs +225 -0
- package/hub/quality/deslop.mjs +253 -0
- package/hub/reflexion.mjs +372 -0
- package/hub/research.mjs +146 -0
- package/hub/router.mjs +791 -0
- package/hub/routing/complexity.mjs +166 -0
- package/hub/routing/index.mjs +117 -0
- package/hub/routing/q-learning.mjs +336 -0
- package/hub/session-fingerprint.mjs +352 -0
- package/hub/state.mjs +245 -0
- package/hub/team-bridge.mjs +25 -0
- package/hub/token-mode.mjs +224 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/hud/colors.mjs +88 -0
- package/hud/constants.mjs +81 -0
- package/hud/hud-qos-status.mjs +206 -0
- package/hud/providers/claude.mjs +309 -0
- package/hud/providers/codex.mjs +151 -0
- package/hud/providers/gemini.mjs +320 -0
- package/hud/renderers.mjs +424 -0
- package/hud/terminal.mjs +140 -0
- package/hud/utils.mjs +287 -0
- package/package.json +31 -0
- package/scripts/lib/claudemd-manager.mjs +325 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +51 -0
- package/scripts/lib/env-probe.mjs +241 -0
- package/scripts/lib/gemini-profiles.mjs +85 -0
- package/scripts/lib/hook-utils.mjs +14 -0
- package/scripts/lib/keyword-rules.mjs +166 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +739 -0
- package/scripts/lib/mcp-guard-engine.mjs +940 -0
- package/scripts/lib/mcp-manifest.mjs +79 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/lib/psmux-info.mjs +119 -0
- package/scripts/lib/remote-spawn-transfer.mjs +196 -0
package/hub/router.mjs
ADDED
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
// hub/router.mjs — 실시간 라우팅/수신함 상태 관리자
|
|
2
|
+
// SQLite는 감사 로그만 담당하고, 실제 배달 상태는 메모리에서 관리한다.
|
|
3
|
+
import { EventEmitter, once } from 'node:events';
|
|
4
|
+
import { uuidv7 } from './store.mjs';
|
|
5
|
+
|
|
6
|
+
const ASSIGN_PENDING_STATUSES = new Set(['queued', 'running']);
|
|
7
|
+
|
|
8
|
+
function uniqueStrings(values = []) {
|
|
9
|
+
return Array.from(new Set((values || []).map((value) => String(value || '').trim()).filter(Boolean)));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function clampAssignDuration(value, fallback = 600000, min = 1000, max = 86400000) {
|
|
13
|
+
const num = Number(value);
|
|
14
|
+
if (!Number.isFinite(num)) return fallback;
|
|
15
|
+
return Math.max(min, Math.min(Math.trunc(num), max));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeAssignTerminalStatus(input, metadata = {}) {
|
|
19
|
+
const status = String(input || '').trim().toLowerCase();
|
|
20
|
+
const resultTag = String(
|
|
21
|
+
metadata?.result
|
|
22
|
+
?? metadata?.status
|
|
23
|
+
?? metadata?.outcome
|
|
24
|
+
?? '',
|
|
25
|
+
).trim().toLowerCase();
|
|
26
|
+
|
|
27
|
+
if (status === 'queued') return 'queued';
|
|
28
|
+
if (status === 'running' || status === 'in_progress') return 'running';
|
|
29
|
+
if (status === 'timed_out' || status === 'timeout') return 'timed_out';
|
|
30
|
+
if (status === 'failed' || status === 'error') return 'failed';
|
|
31
|
+
if (status === 'succeeded' || status === 'success') return 'succeeded';
|
|
32
|
+
|
|
33
|
+
if (status === 'completed') {
|
|
34
|
+
if (resultTag === 'failed' || resultTag === 'error') return 'failed';
|
|
35
|
+
if (resultTag === 'timed_out' || resultTag === 'timeout') return 'timed_out';
|
|
36
|
+
return 'succeeded';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (resultTag === 'failed' || resultTag === 'error') return 'failed';
|
|
40
|
+
if (resultTag === 'timed_out' || resultTag === 'timeout') return 'timed_out';
|
|
41
|
+
if (resultTag === 'succeeded' || resultTag === 'success') return 'succeeded';
|
|
42
|
+
return 'succeeded';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeAgentTopics(store, agentId, runtimeTopics) {
|
|
46
|
+
const topics = new Set(runtimeTopics || []);
|
|
47
|
+
const persisted = store.getAgent(agentId)?.topics || [];
|
|
48
|
+
for (const topic of persisted) topics.add(topic);
|
|
49
|
+
return Array.from(topics);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 라우터 생성
|
|
54
|
+
* @param {object} store
|
|
55
|
+
*/
|
|
56
|
+
export function createRouter(store) {
|
|
57
|
+
let sweepTimer = null;
|
|
58
|
+
let staleTimer = null;
|
|
59
|
+
const responseEmitter = new EventEmitter();
|
|
60
|
+
const deliveryEmitter = new EventEmitter();
|
|
61
|
+
responseEmitter.setMaxListeners(200);
|
|
62
|
+
deliveryEmitter.setMaxListeners(200);
|
|
63
|
+
|
|
64
|
+
const runtimeTopics = new Map();
|
|
65
|
+
const queuesByAgent = new Map();
|
|
66
|
+
const liveMessages = new Map();
|
|
67
|
+
const MAX_LATENCY_SAMPLES = 100;
|
|
68
|
+
let latencyIdx = 0;
|
|
69
|
+
const deliveryLatencies = new Array(MAX_LATENCY_SAMPLES).fill(0);
|
|
70
|
+
|
|
71
|
+
function ensureAgentQueue(agentId) {
|
|
72
|
+
let queue = queuesByAgent.get(agentId);
|
|
73
|
+
if (!queue) {
|
|
74
|
+
queue = new Map();
|
|
75
|
+
queuesByAgent.set(agentId, queue);
|
|
76
|
+
}
|
|
77
|
+
return queue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function recordLatency(ms) {
|
|
81
|
+
deliveryLatencies[latencyIdx % MAX_LATENCY_SAMPLES] = ms;
|
|
82
|
+
latencyIdx++;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function upsertRuntimeTopics(agentId, topics, { replace = true } = {}) {
|
|
86
|
+
const normalized = uniqueStrings(topics);
|
|
87
|
+
const current = replace ? new Set() : new Set(runtimeTopics.get(agentId) || []);
|
|
88
|
+
for (const topic of normalized) current.add(topic);
|
|
89
|
+
runtimeTopics.set(agentId, current);
|
|
90
|
+
store.updateAgentTopics(agentId, Array.from(current));
|
|
91
|
+
return Array.from(current);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function listRuntimeTopics(agentId) {
|
|
95
|
+
return normalizeAgentTopics(store, agentId, runtimeTopics.get(agentId));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function trackMessage(message, recipients) {
|
|
99
|
+
liveMessages.set(message.id, {
|
|
100
|
+
message,
|
|
101
|
+
recipients: new Set(recipients),
|
|
102
|
+
ackedBy: new Set(),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getMessageRecord(messageId) {
|
|
107
|
+
return liveMessages.get(messageId) || null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function removeMessage(messageId) {
|
|
111
|
+
const record = liveMessages.get(messageId);
|
|
112
|
+
if (!record) return;
|
|
113
|
+
for (const agentId of record.recipients) {
|
|
114
|
+
queuesByAgent.get(agentId)?.delete(messageId);
|
|
115
|
+
}
|
|
116
|
+
liveMessages.delete(messageId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function queueMessage(agentId, message) {
|
|
120
|
+
const queue = ensureAgentQueue(agentId);
|
|
121
|
+
queue.set(message.id, {
|
|
122
|
+
message,
|
|
123
|
+
attempts: 0,
|
|
124
|
+
delivered_at_ms: null,
|
|
125
|
+
acked_at_ms: null,
|
|
126
|
+
});
|
|
127
|
+
deliveryEmitter.emit('message', agentId, message);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveRecipients(msg) {
|
|
131
|
+
const to = msg.to_agent ?? msg.to;
|
|
132
|
+
if (!to?.startsWith('topic:')) {
|
|
133
|
+
return [to];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const topic = to.slice(6);
|
|
137
|
+
const recipients = new Set();
|
|
138
|
+
for (const [agentId, topics] of runtimeTopics) {
|
|
139
|
+
if (topics.has(topic)) recipients.add(agentId);
|
|
140
|
+
}
|
|
141
|
+
for (const agent of store.getAgentsByTopic(topic)) {
|
|
142
|
+
recipients.add(agent.agent_id);
|
|
143
|
+
}
|
|
144
|
+
return Array.from(recipients);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function sortedPending(agentId, { max_messages = 20, include_topics = null } = {}) {
|
|
148
|
+
const queue = ensureAgentQueue(agentId);
|
|
149
|
+
const topicFilter = include_topics?.length ? new Set(include_topics) : null;
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
const pending = [];
|
|
152
|
+
|
|
153
|
+
for (const delivery of queue.values()) {
|
|
154
|
+
const { message } = delivery;
|
|
155
|
+
if (delivery.acked_at_ms) continue;
|
|
156
|
+
if (message.expires_at_ms <= now) continue;
|
|
157
|
+
if (topicFilter && !topicFilter.has(message.topic)) continue;
|
|
158
|
+
pending.push(message);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pending.sort((a, b) => {
|
|
162
|
+
if (b.priority !== a.priority) return b.priority - a.priority;
|
|
163
|
+
return a.created_at_ms - b.created_at_ms;
|
|
164
|
+
});
|
|
165
|
+
return pending.slice(0, max_messages);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function markDelivered(agentId, messageId) {
|
|
169
|
+
const delivery = queuesByAgent.get(agentId)?.get(messageId);
|
|
170
|
+
const record = getMessageRecord(messageId);
|
|
171
|
+
if (!delivery || !record) return false;
|
|
172
|
+
|
|
173
|
+
delivery.attempts += 1;
|
|
174
|
+
if (!delivery.delivered_at_ms) {
|
|
175
|
+
delivery.delivered_at_ms = Date.now();
|
|
176
|
+
record.message.status = 'delivered';
|
|
177
|
+
store.updateMessageStatus(messageId, 'delivered');
|
|
178
|
+
recordLatency(delivery.delivered_at_ms - record.message.created_at_ms);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function ackMessages(ids, agentId) {
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
let count = 0;
|
|
187
|
+
|
|
188
|
+
for (const id of ids || []) {
|
|
189
|
+
const delivery = queuesByAgent.get(agentId)?.get(id);
|
|
190
|
+
const record = getMessageRecord(id);
|
|
191
|
+
if (!delivery || !record || delivery.acked_at_ms) continue;
|
|
192
|
+
|
|
193
|
+
delivery.acked_at_ms = now;
|
|
194
|
+
record.ackedBy.add(agentId);
|
|
195
|
+
count += 1;
|
|
196
|
+
|
|
197
|
+
if (record.ackedBy.size >= record.recipients.size) {
|
|
198
|
+
record.message.status = 'acked';
|
|
199
|
+
store.updateMessageStatus(id, 'acked');
|
|
200
|
+
removeMessage(id);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return count;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function dispatchMessage({ type, from, to, topic, priority = 5, ttl_ms = 300000, payload = {}, trace_id, correlation_id }) {
|
|
208
|
+
const msg = store.auditLog({
|
|
209
|
+
type,
|
|
210
|
+
from,
|
|
211
|
+
to,
|
|
212
|
+
topic,
|
|
213
|
+
priority,
|
|
214
|
+
ttl_ms,
|
|
215
|
+
payload,
|
|
216
|
+
trace_id,
|
|
217
|
+
correlation_id,
|
|
218
|
+
});
|
|
219
|
+
const recipients = uniqueStrings(resolveRecipients(msg));
|
|
220
|
+
if (recipients.length) {
|
|
221
|
+
trackMessage(msg, recipients);
|
|
222
|
+
for (const agentId of recipients) {
|
|
223
|
+
queueMessage(agentId, msg);
|
|
224
|
+
}
|
|
225
|
+
msg.status = 'delivered';
|
|
226
|
+
store.updateMessageStatus(msg.id, 'delivered');
|
|
227
|
+
}
|
|
228
|
+
if (msg.type === 'response') {
|
|
229
|
+
responseEmitter.emit(msg.correlation_id, msg.payload);
|
|
230
|
+
}
|
|
231
|
+
return { msg, recipients };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildAssignSnapshot(job, extra = {}) {
|
|
235
|
+
if (!job) return null;
|
|
236
|
+
return {
|
|
237
|
+
job_id: job.job_id,
|
|
238
|
+
supervisor_agent: job.supervisor_agent,
|
|
239
|
+
worker_agent: job.worker_agent,
|
|
240
|
+
topic: job.topic,
|
|
241
|
+
task: job.task,
|
|
242
|
+
status: job.status,
|
|
243
|
+
attempt: job.attempt,
|
|
244
|
+
retry_count: job.retry_count,
|
|
245
|
+
max_retries: job.max_retries,
|
|
246
|
+
timeout_ms: job.timeout_ms,
|
|
247
|
+
deadline_ms: job.deadline_ms,
|
|
248
|
+
trace_id: job.trace_id,
|
|
249
|
+
correlation_id: job.correlation_id,
|
|
250
|
+
last_message_id: job.last_message_id,
|
|
251
|
+
result: job.result,
|
|
252
|
+
error: job.error,
|
|
253
|
+
updated_at_ms: job.updated_at_ms,
|
|
254
|
+
completed_at_ms: job.completed_at_ms,
|
|
255
|
+
...extra,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function notifyAssignSupervisor(job, event, extra = {}) {
|
|
260
|
+
if (!job?.supervisor_agent) return null;
|
|
261
|
+
const { msg } = dispatchMessage({
|
|
262
|
+
type: 'event',
|
|
263
|
+
from: job.worker_agent || 'assign-router',
|
|
264
|
+
to: job.supervisor_agent,
|
|
265
|
+
topic: 'assign.result',
|
|
266
|
+
priority: Math.max(5, job.priority || 5),
|
|
267
|
+
ttl_ms: job.ttl_ms || job.timeout_ms || 600000,
|
|
268
|
+
payload: {
|
|
269
|
+
event,
|
|
270
|
+
...buildAssignSnapshot(job),
|
|
271
|
+
...extra,
|
|
272
|
+
},
|
|
273
|
+
trace_id: job.trace_id,
|
|
274
|
+
correlation_id: job.correlation_id,
|
|
275
|
+
});
|
|
276
|
+
return msg;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function dispatchAssignJob(job, reason = 'dispatch') {
|
|
280
|
+
const { msg, recipients } = dispatchMessage({
|
|
281
|
+
type: 'handoff',
|
|
282
|
+
from: job.supervisor_agent,
|
|
283
|
+
to: job.worker_agent,
|
|
284
|
+
topic: job.topic || 'assign.job',
|
|
285
|
+
priority: job.priority || 5,
|
|
286
|
+
ttl_ms: job.ttl_ms || job.timeout_ms || 600000,
|
|
287
|
+
payload: {
|
|
288
|
+
kind: 'assign.job',
|
|
289
|
+
reason,
|
|
290
|
+
assign_job_id: job.job_id,
|
|
291
|
+
attempt: job.attempt,
|
|
292
|
+
retry_count: job.retry_count,
|
|
293
|
+
max_retries: job.max_retries,
|
|
294
|
+
timeout_ms: job.timeout_ms,
|
|
295
|
+
supervisor_agent: job.supervisor_agent,
|
|
296
|
+
worker_agent: job.worker_agent,
|
|
297
|
+
task: job.task,
|
|
298
|
+
payload: job.payload || {},
|
|
299
|
+
},
|
|
300
|
+
trace_id: job.trace_id,
|
|
301
|
+
correlation_id: job.correlation_id,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const updated = store.updateAssignStatus(job.job_id, job.status, {
|
|
305
|
+
last_message_id: msg.id,
|
|
306
|
+
});
|
|
307
|
+
return { job: updated || job, recipients, message_id: msg.id };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function scheduleAssignRetry(job, reason, error = null, requested_by = 'system') {
|
|
311
|
+
if (!job) {
|
|
312
|
+
return { ok: false, error: { code: 'ASSIGN_NOT_FOUND', message: 'assign job not found' } };
|
|
313
|
+
}
|
|
314
|
+
if (job.retry_count >= job.max_retries) {
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
error: {
|
|
318
|
+
code: 'ASSIGN_RETRY_EXHAUSTED',
|
|
319
|
+
message: `retry exhausted for ${job.job_id}`,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const queued = store.retryAssign(job.job_id, {
|
|
325
|
+
error,
|
|
326
|
+
timeout_ms: job.timeout_ms,
|
|
327
|
+
ttl_ms: job.ttl_ms,
|
|
328
|
+
});
|
|
329
|
+
const dispatched = dispatchAssignJob(queued, 'retry');
|
|
330
|
+
notifyAssignSupervisor(dispatched.job, 'retry_scheduled', {
|
|
331
|
+
retry_reason: reason,
|
|
332
|
+
requested_by,
|
|
333
|
+
});
|
|
334
|
+
return {
|
|
335
|
+
ok: true,
|
|
336
|
+
data: {
|
|
337
|
+
retried: true,
|
|
338
|
+
...buildAssignSnapshot(dispatched.job, {
|
|
339
|
+
retry_reason: reason,
|
|
340
|
+
requested_by,
|
|
341
|
+
}),
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function handleAssignTimeout(job) {
|
|
347
|
+
const timedOut = store.updateAssignStatus(job.job_id, 'timed_out', {
|
|
348
|
+
error: job.error ?? { message: 'assign job timed out' },
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
if (timedOut.retry_count < timedOut.max_retries) {
|
|
352
|
+
return scheduleAssignRetry(timedOut, 'timed_out', timedOut.error, 'sweeper');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
notifyAssignSupervisor(timedOut, 'completed', {
|
|
356
|
+
completion_reason: 'timed_out',
|
|
357
|
+
});
|
|
358
|
+
return { ok: true, data: buildAssignSnapshot(timedOut, { completion_reason: 'timed_out' }) };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const router = {
|
|
362
|
+
responseEmitter,
|
|
363
|
+
deliveryEmitter,
|
|
364
|
+
|
|
365
|
+
registerAgent(args) {
|
|
366
|
+
const result = store.registerAgent(args);
|
|
367
|
+
upsertRuntimeTopics(args.agent_id, args.topics || [], { replace: true });
|
|
368
|
+
return result;
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
refreshAgentLease(agentId, ttlMs = 30000) {
|
|
372
|
+
return store.refreshLease(agentId, ttlMs);
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
subscribeAgent(agentId, topics, { replace = false } = {}) {
|
|
376
|
+
const nextTopics = upsertRuntimeTopics(agentId, topics, { replace });
|
|
377
|
+
return { agent_id: agentId, topics: nextTopics };
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
getSubscribedTopics(agentId) {
|
|
381
|
+
return listRuntimeTopics(agentId);
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
updateAgentStatus(agentId, status) {
|
|
385
|
+
if (status === 'offline') {
|
|
386
|
+
runtimeTopics.delete(agentId);
|
|
387
|
+
}
|
|
388
|
+
return store.updateAgentStatus(agentId, status);
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
route(msg) {
|
|
392
|
+
const recipients = uniqueStrings(resolveRecipients(msg));
|
|
393
|
+
if (!recipients.length) return 0;
|
|
394
|
+
if (!getMessageRecord(msg.id)) {
|
|
395
|
+
trackMessage(msg, recipients);
|
|
396
|
+
}
|
|
397
|
+
for (const agentId of recipients) {
|
|
398
|
+
queueMessage(agentId, msg);
|
|
399
|
+
}
|
|
400
|
+
store.updateMessageStatus(msg.id, 'delivered');
|
|
401
|
+
return recipients.length;
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
getPendingMessages(agentId, options = {}) {
|
|
405
|
+
return sortedPending(agentId, options);
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
countPendingMessages(agentId) {
|
|
409
|
+
const queue = ensureAgentQueue(agentId);
|
|
410
|
+
const now = Date.now();
|
|
411
|
+
let count = 0;
|
|
412
|
+
for (const delivery of queue.values()) {
|
|
413
|
+
if (delivery.acked_at_ms) continue;
|
|
414
|
+
if (delivery.message.expires_at_ms <= now) continue;
|
|
415
|
+
count++;
|
|
416
|
+
}
|
|
417
|
+
return count;
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
markMessagePushed(agentId, messageId) {
|
|
421
|
+
return markDelivered(agentId, messageId);
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
drainAgent(agentId, { max_messages = 20, include_topics = null, auto_ack = false } = {}) {
|
|
425
|
+
const messages = sortedPending(agentId, { max_messages, include_topics });
|
|
426
|
+
for (const message of messages) {
|
|
427
|
+
markDelivered(agentId, message.id);
|
|
428
|
+
}
|
|
429
|
+
if (auto_ack && messages.length) {
|
|
430
|
+
ackMessages(messages.map((message) => message.id), agentId);
|
|
431
|
+
}
|
|
432
|
+
return messages;
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
ackMessages(ids, agentId) {
|
|
436
|
+
return ackMessages(ids, agentId);
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
async handleAsk({
|
|
440
|
+
from, to, topic, question, context_refs,
|
|
441
|
+
payload = {}, priority = 5, ttl_ms = 300000,
|
|
442
|
+
await_response_ms = 0, trace_id, correlation_id,
|
|
443
|
+
}) {
|
|
444
|
+
const cid = correlation_id || uuidv7();
|
|
445
|
+
const tid = trace_id || uuidv7();
|
|
446
|
+
|
|
447
|
+
const { msg } = dispatchMessage({
|
|
448
|
+
type: 'request',
|
|
449
|
+
from,
|
|
450
|
+
to,
|
|
451
|
+
topic,
|
|
452
|
+
priority,
|
|
453
|
+
ttl_ms,
|
|
454
|
+
payload: { question, context_refs, ...payload },
|
|
455
|
+
correlation_id: cid,
|
|
456
|
+
trace_id: tid,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (await_response_ms <= 0) {
|
|
460
|
+
return {
|
|
461
|
+
ok: true,
|
|
462
|
+
data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'queued' },
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
const [response] = await once(responseEmitter, cid, {
|
|
468
|
+
signal: AbortSignal.timeout(Math.min(await_response_ms, 30000)),
|
|
469
|
+
});
|
|
470
|
+
return {
|
|
471
|
+
ok: true,
|
|
472
|
+
data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'answered', response },
|
|
473
|
+
};
|
|
474
|
+
} catch {
|
|
475
|
+
const resp = store.getResponseByCorrelation(cid);
|
|
476
|
+
if (resp) {
|
|
477
|
+
return {
|
|
478
|
+
ok: true,
|
|
479
|
+
data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'answered', response: resp.payload },
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
ok: true,
|
|
484
|
+
data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'delivered' },
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
handlePublish({
|
|
490
|
+
from, to, topic, priority = 5, ttl_ms = 300000,
|
|
491
|
+
payload = {}, trace_id, correlation_id, message_type,
|
|
492
|
+
}) {
|
|
493
|
+
const type = message_type || (correlation_id ? 'response' : 'event');
|
|
494
|
+
const { msg, recipients } = dispatchMessage({
|
|
495
|
+
type,
|
|
496
|
+
from,
|
|
497
|
+
to,
|
|
498
|
+
topic,
|
|
499
|
+
priority,
|
|
500
|
+
ttl_ms,
|
|
501
|
+
payload,
|
|
502
|
+
trace_id: trace_id || uuidv7(),
|
|
503
|
+
correlation_id: correlation_id || uuidv7(),
|
|
504
|
+
});
|
|
505
|
+
return {
|
|
506
|
+
ok: true,
|
|
507
|
+
data: {
|
|
508
|
+
message_id: msg.id,
|
|
509
|
+
fanout_count: recipients.length,
|
|
510
|
+
expires_at_ms: msg.expires_at_ms,
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
handleHandoff({
|
|
516
|
+
from, to, topic, task, acceptance_criteria, context_refs,
|
|
517
|
+
priority = 5, ttl_ms = 600000, trace_id, correlation_id,
|
|
518
|
+
}) {
|
|
519
|
+
const { msg } = dispatchMessage({
|
|
520
|
+
type: 'handoff',
|
|
521
|
+
from,
|
|
522
|
+
to,
|
|
523
|
+
topic,
|
|
524
|
+
priority,
|
|
525
|
+
ttl_ms,
|
|
526
|
+
payload: { task, acceptance_criteria, context_refs },
|
|
527
|
+
trace_id: trace_id || uuidv7(),
|
|
528
|
+
correlation_id: correlation_id || uuidv7(),
|
|
529
|
+
});
|
|
530
|
+
return {
|
|
531
|
+
ok: true,
|
|
532
|
+
data: { handoff_message_id: msg.id, state: 'queued', assigned_to: to },
|
|
533
|
+
};
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
assignAsync({
|
|
537
|
+
supervisor_agent,
|
|
538
|
+
worker_agent,
|
|
539
|
+
topic = 'assign.job',
|
|
540
|
+
task = '',
|
|
541
|
+
payload = {},
|
|
542
|
+
priority = 5,
|
|
543
|
+
ttl_ms = 600000,
|
|
544
|
+
timeout_ms = 600000,
|
|
545
|
+
max_retries = 0,
|
|
546
|
+
trace_id,
|
|
547
|
+
correlation_id,
|
|
548
|
+
}) {
|
|
549
|
+
const job = store.createAssign({
|
|
550
|
+
supervisor_agent,
|
|
551
|
+
worker_agent,
|
|
552
|
+
topic,
|
|
553
|
+
task,
|
|
554
|
+
payload,
|
|
555
|
+
priority,
|
|
556
|
+
ttl_ms,
|
|
557
|
+
timeout_ms,
|
|
558
|
+
max_retries,
|
|
559
|
+
trace_id,
|
|
560
|
+
correlation_id,
|
|
561
|
+
});
|
|
562
|
+
const dispatched = dispatchAssignJob(job, 'create');
|
|
563
|
+
return {
|
|
564
|
+
ok: true,
|
|
565
|
+
data: {
|
|
566
|
+
assigned_to: worker_agent,
|
|
567
|
+
...buildAssignSnapshot(dispatched.job),
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
reportAssignResult({
|
|
573
|
+
job_id,
|
|
574
|
+
worker_agent,
|
|
575
|
+
status,
|
|
576
|
+
attempt,
|
|
577
|
+
result,
|
|
578
|
+
error,
|
|
579
|
+
payload = {},
|
|
580
|
+
metadata = {},
|
|
581
|
+
}) {
|
|
582
|
+
const job = store.getAssign(job_id);
|
|
583
|
+
if (!job) {
|
|
584
|
+
return {
|
|
585
|
+
ok: false,
|
|
586
|
+
error: { code: 'ASSIGN_NOT_FOUND', message: `assign job not found: ${job_id}` },
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
if (worker_agent && worker_agent !== job.worker_agent) {
|
|
590
|
+
return {
|
|
591
|
+
ok: false,
|
|
592
|
+
error: { code: 'ASSIGN_WORKER_MISMATCH', message: `worker mismatch: ${worker_agent}` },
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
if (Number.isFinite(Number(attempt)) && Number(attempt) !== job.attempt) {
|
|
596
|
+
return {
|
|
597
|
+
ok: false,
|
|
598
|
+
error: {
|
|
599
|
+
code: 'ASSIGN_ATTEMPT_MISMATCH',
|
|
600
|
+
message: `stale assign result for attempt ${attempt} (current ${job.attempt})`,
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const mergedMetadata = {
|
|
606
|
+
...(payload?.metadata || {}),
|
|
607
|
+
...(metadata || {}),
|
|
608
|
+
};
|
|
609
|
+
const normalizedStatus = normalizeAssignTerminalStatus(
|
|
610
|
+
status || payload?.status,
|
|
611
|
+
mergedMetadata,
|
|
612
|
+
);
|
|
613
|
+
const nextResult = result ?? (Object.prototype.hasOwnProperty.call(payload || {}, 'result') ? payload.result : payload);
|
|
614
|
+
const nextError = error ?? payload?.error ?? null;
|
|
615
|
+
|
|
616
|
+
if (normalizedStatus === 'running') {
|
|
617
|
+
const running = store.updateAssignStatus(job.job_id, 'running', {
|
|
618
|
+
started_at_ms: job.started_at_ms || Date.now(),
|
|
619
|
+
deadline_ms: Date.now() + clampAssignDuration(job.timeout_ms, job.timeout_ms),
|
|
620
|
+
result: nextResult,
|
|
621
|
+
error: nextError,
|
|
622
|
+
});
|
|
623
|
+
notifyAssignSupervisor(running, 'progress');
|
|
624
|
+
return { ok: true, data: buildAssignSnapshot(running) };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const finalized = store.updateAssignStatus(job.job_id, normalizedStatus, {
|
|
628
|
+
result: nextResult,
|
|
629
|
+
error: nextError,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
if ((normalizedStatus === 'failed' || normalizedStatus === 'timed_out')
|
|
633
|
+
&& finalized.retry_count < finalized.max_retries) {
|
|
634
|
+
return scheduleAssignRetry(finalized, normalizedStatus, nextError, worker_agent || finalized.worker_agent);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
notifyAssignSupervisor(finalized, 'completed');
|
|
638
|
+
return { ok: true, data: buildAssignSnapshot(finalized) };
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
getAssignStatus({ job_id, ...filters } = {}) {
|
|
642
|
+
if (job_id) {
|
|
643
|
+
const job = store.getAssign(job_id);
|
|
644
|
+
return job
|
|
645
|
+
? { ok: true, data: buildAssignSnapshot(job) }
|
|
646
|
+
: { ok: false, error: { code: 'ASSIGN_NOT_FOUND', message: `assign job not found: ${job_id}` } };
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
ok: true,
|
|
650
|
+
data: {
|
|
651
|
+
assigns: store.listAssigns(filters).map((job) => buildAssignSnapshot(job)),
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
retryAssign(job_id, { reason = 'manual', requested_by = 'manual' } = {}) {
|
|
657
|
+
const job = store.getAssign(job_id);
|
|
658
|
+
if (!job) {
|
|
659
|
+
return {
|
|
660
|
+
ok: false,
|
|
661
|
+
error: { code: 'ASSIGN_NOT_FOUND', message: `assign job not found: ${job_id}` },
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
return scheduleAssignRetry(job, reason, job.error, requested_by);
|
|
665
|
+
},
|
|
666
|
+
|
|
667
|
+
sweepExpired() {
|
|
668
|
+
const now = Date.now();
|
|
669
|
+
let expired = 0;
|
|
670
|
+
for (const [messageId, record] of Array.from(liveMessages.entries())) {
|
|
671
|
+
if (record.message.expires_at_ms > now) continue;
|
|
672
|
+
store.moveToDeadLetter(messageId, 'ttl_expired', null);
|
|
673
|
+
removeMessage(messageId);
|
|
674
|
+
expired += 1;
|
|
675
|
+
}
|
|
676
|
+
return { messages: expired };
|
|
677
|
+
},
|
|
678
|
+
|
|
679
|
+
sweepTimedOutAssigns() {
|
|
680
|
+
const expiredAssigns = store.listAssigns({
|
|
681
|
+
statuses: Array.from(ASSIGN_PENDING_STATUSES),
|
|
682
|
+
active_before_ms: Date.now(),
|
|
683
|
+
limit: 100,
|
|
684
|
+
});
|
|
685
|
+
let timed_out = 0;
|
|
686
|
+
let retried = 0;
|
|
687
|
+
|
|
688
|
+
for (const job of expiredAssigns) {
|
|
689
|
+
const result = handleAssignTimeout(job);
|
|
690
|
+
timed_out += 1;
|
|
691
|
+
if (result?.data?.retried) retried += 1;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return { timed_out, retried };
|
|
695
|
+
},
|
|
696
|
+
|
|
697
|
+
startSweeper() {
|
|
698
|
+
if (sweepTimer) return;
|
|
699
|
+
sweepTimer = setInterval(() => {
|
|
700
|
+
try {
|
|
701
|
+
router.sweepExpired();
|
|
702
|
+
router.sweepTimedOutAssigns();
|
|
703
|
+
} catch {}
|
|
704
|
+
}, 10000);
|
|
705
|
+
staleTimer = setInterval(() => {
|
|
706
|
+
try { store.sweepStaleAgents(); } catch {}
|
|
707
|
+
}, 120000);
|
|
708
|
+
sweepTimer.unref();
|
|
709
|
+
staleTimer.unref();
|
|
710
|
+
},
|
|
711
|
+
|
|
712
|
+
stopSweeper() {
|
|
713
|
+
if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
|
|
714
|
+
if (staleTimer) { clearInterval(staleTimer); staleTimer = null; }
|
|
715
|
+
},
|
|
716
|
+
|
|
717
|
+
getQueueDepths() {
|
|
718
|
+
const counts = { urgent: 0, normal: 0, dlq: store.getAuditStats().dlq };
|
|
719
|
+
for (const record of liveMessages.values()) {
|
|
720
|
+
const pending = record.recipients.size > record.ackedBy.size;
|
|
721
|
+
if (!pending) continue;
|
|
722
|
+
if (record.message.priority >= 7) counts.urgent += 1;
|
|
723
|
+
else counts.normal += 1;
|
|
724
|
+
}
|
|
725
|
+
return counts;
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
getDeliveryStats() {
|
|
729
|
+
if (latencyIdx === 0) {
|
|
730
|
+
return { total_deliveries: 0, avg_delivery_ms: 0 };
|
|
731
|
+
}
|
|
732
|
+
const filled = Math.min(latencyIdx, MAX_LATENCY_SAMPLES);
|
|
733
|
+
const total = deliveryLatencies.slice(0, filled).reduce((sum, ms) => sum + ms, 0);
|
|
734
|
+
return {
|
|
735
|
+
total_deliveries: latencyIdx,
|
|
736
|
+
avg_delivery_ms: Math.round(total / filled),
|
|
737
|
+
};
|
|
738
|
+
},
|
|
739
|
+
|
|
740
|
+
getStatus(scope = 'hub', { agent_id, trace_id, include_metrics = true } = {}) {
|
|
741
|
+
const data = {};
|
|
742
|
+
|
|
743
|
+
if (scope === 'hub' || scope === 'queue') {
|
|
744
|
+
data.hub = {
|
|
745
|
+
state: 'healthy',
|
|
746
|
+
uptime_ms: process.uptime() * 1000 | 0,
|
|
747
|
+
realtime_transport: 'named-pipe',
|
|
748
|
+
audit_store: store.type || 'sqlite',
|
|
749
|
+
};
|
|
750
|
+
if (include_metrics) {
|
|
751
|
+
const depths = router.getQueueDepths();
|
|
752
|
+
const stats = router.getDeliveryStats();
|
|
753
|
+
const auditStats = store.getAuditStats();
|
|
754
|
+
data.queues = {
|
|
755
|
+
urgent_depth: depths.urgent,
|
|
756
|
+
normal_depth: depths.normal,
|
|
757
|
+
dlq_depth: depths.dlq,
|
|
758
|
+
avg_delivery_ms: stats.avg_delivery_ms,
|
|
759
|
+
};
|
|
760
|
+
data.assigns = {
|
|
761
|
+
queued: auditStats.assign_queued,
|
|
762
|
+
running: auditStats.assign_running,
|
|
763
|
+
failed: auditStats.assign_failed,
|
|
764
|
+
timed_out: auditStats.assign_timed_out,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (scope === 'agent' && agent_id) {
|
|
770
|
+
const agent = store.getAgent(agent_id);
|
|
771
|
+
if (agent) {
|
|
772
|
+
data.agent = {
|
|
773
|
+
agent_id: agent.agent_id,
|
|
774
|
+
status: agent.status,
|
|
775
|
+
pending: sortedPending(agent_id, { max_messages: 1000 }).length,
|
|
776
|
+
last_seen_ms: agent.last_seen_ms,
|
|
777
|
+
topics: listRuntimeTopics(agent_id),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (scope === 'trace' && trace_id) {
|
|
783
|
+
data.trace = store.getMessagesByTrace(trace_id);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return { ok: true, data };
|
|
787
|
+
},
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
return router;
|
|
791
|
+
}
|