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.
Files changed (52) hide show
  1. package/bin/triflux.mjs +1296 -1055
  2. package/hooks/hooks.json +17 -0
  3. package/hooks/keyword-rules.json +20 -4
  4. package/hooks/pipeline-stop.mjs +54 -0
  5. package/hub/bridge.mjs +517 -318
  6. package/hub/hitl.mjs +45 -31
  7. package/hub/pipe.mjs +457 -0
  8. package/hub/pipeline/index.mjs +121 -0
  9. package/hub/pipeline/state.mjs +164 -0
  10. package/hub/pipeline/transitions.mjs +114 -0
  11. package/hub/router.mjs +422 -161
  12. package/hub/schema.sql +14 -0
  13. package/hub/server.mjs +499 -424
  14. package/hub/store.mjs +388 -314
  15. package/hub/team/cli-team-common.mjs +348 -0
  16. package/hub/team/cli-team-control.mjs +393 -0
  17. package/hub/team/cli-team-start.mjs +516 -0
  18. package/hub/team/cli-team-status.mjs +269 -0
  19. package/hub/team/cli.mjs +75 -1475
  20. package/hub/team/dashboard.mjs +1 -9
  21. package/hub/team/native.mjs +190 -130
  22. package/hub/team/nativeProxy.mjs +165 -78
  23. package/hub/team/orchestrator.mjs +15 -20
  24. package/hub/team/pane.mjs +137 -103
  25. package/hub/team/psmux.mjs +506 -0
  26. package/hub/team/session.mjs +393 -330
  27. package/hub/team/shared.mjs +13 -0
  28. package/hub/team/staleState.mjs +299 -0
  29. package/hub/tools.mjs +105 -31
  30. package/hub/workers/claude-worker.mjs +446 -0
  31. package/hub/workers/codex-mcp.mjs +414 -0
  32. package/hub/workers/factory.mjs +18 -0
  33. package/hub/workers/gemini-worker.mjs +349 -0
  34. package/hub/workers/interface.mjs +41 -0
  35. package/hud/hud-qos-status.mjs +1790 -1788
  36. package/package.json +4 -1
  37. package/scripts/__tests__/keyword-detector.test.mjs +8 -8
  38. package/scripts/keyword-detector.mjs +15 -0
  39. package/scripts/lib/keyword-rules.mjs +4 -1
  40. package/scripts/preflight-cache.mjs +72 -0
  41. package/scripts/psmux-steering-prototype.sh +368 -0
  42. package/scripts/setup.mjs +136 -71
  43. package/scripts/tfx-route-worker.mjs +161 -0
  44. package/scripts/tfx-route.sh +485 -91
  45. package/skills/tfx-auto/SKILL.md +90 -564
  46. package/skills/tfx-auto-codex/SKILL.md +1 -3
  47. package/skills/tfx-codex/SKILL.md +1 -4
  48. package/skills/tfx-doctor/SKILL.md +1 -0
  49. package/skills/tfx-gemini/SKILL.md +1 -4
  50. package/skills/tfx-multi/SKILL.md +378 -0
  51. package/skills/tfx-setup/SKILL.md +1 -4
  52. package/skills/tfx-team/SKILL.md +0 -304
package/hub/router.mjs CHANGED
@@ -1,73 +1,306 @@
1
- // hub/router.mjs — Actor mailbox 라우터 + QoS 스케줄러
2
- // 메시지 라우팅, ask/publish/handoff 처리, TTL 정리
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
- * @param {object} store — createStore() 반환 객체
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
- const router = {
17
- /**
18
- * 메시지를 대상에게 라우팅
19
- * "topic:XXX" 토픽 구독자 전체 fanout
20
- * 직접 agent_id → 1:1 배달
21
- * @returns {number} 배달된 에이전트 수
22
- */
23
- route(msg) {
24
- const to = msg.to_agent ?? msg.to;
25
- if (to.startsWith('topic:')) {
26
- return store.deliverToTopic(msg.id, to.slice(6));
27
- }
28
- store.deliverToAgent(msg.id, to);
29
- return 1;
30
- },
31
-
32
- /**
33
- * ask — 질문 요청 (request/reply 패턴)
34
- * await_response_ms > 0 이면 짧은 폴링으로 응답 대기
35
- * 0 이면 티켓(correlation_id) 즉시 반환
36
- */
37
- async handleAsk({
38
- from, to, topic, question, context_refs,
39
- payload = {}, priority = 5, ttl_ms = 300000,
40
- await_response_ms = 0, trace_id, correlation_id,
41
- }) {
42
- const cid = correlation_id || uuidv7();
43
- const tid = trace_id || uuidv7();
44
-
45
- const msg = store.enqueueMessage({
46
- type: 'request', from, to, topic, priority, ttl_ms,
47
- payload: { question, context_refs, ...payload },
48
- correlation_id: cid, trace_id: tid,
49
- });
50
- router.route(msg);
51
-
52
- // 티켓 모드: 즉시 반환
53
- if (await_response_ms <= 0) {
54
- return {
55
- ok: true,
56
- data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'queued' },
57
- };
58
- }
59
-
60
- // 이벤트 기반 대기 (최대 30초 제한)
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 [payload] = await once(responseEmitter, cid, {
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: payload },
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
- * publish 이벤트/응답 발행
87
- * correlation_id 존재 response 타입, 없으면 event 타입
88
- */
89
- handlePublish({
90
- from, to, topic, priority = 5, ttl_ms = 300000,
91
- payload = {}, trace_id, correlation_id,
92
- }) {
93
- const type = correlation_id ? 'response' : 'event';
94
- const msg = store.enqueueMessage({
95
- type, from, to, topic, priority, ttl_ms, payload,
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: { message_id: msg.id, fanout_count: fanout, expires_at_ms: msg.expires_at_ms },
361
+ data: { handoff_message_id: msg.id, state: 'queued', assigned_to: to },
106
362
  };
107
- },
108
-
109
- /**
110
- * handoff 작업 인계
111
- * acceptance_criteria, context_refs 포함 가능
112
- */
113
- handleHandoff({
114
- from, to, topic, task, acceptance_criteria, context_refs,
115
- priority = 5, ttl_ms = 600000, trace_id, correlation_id,
116
- }) {
117
- const msg = store.enqueueMessage({
118
- type: 'handoff', from, to, topic, priority, ttl_ms,
119
- payload: { task, acceptance_criteria, context_refs },
120
- correlation_id: correlation_id || uuidv7(),
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 { store.sweepExpired(); } catch { /* 무시 */ }
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
- stopSweeper() {
147
- if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
148
- if (staleTimer) { clearInterval(staleTimer); staleTimer = null; }
149
- },
150
-
151
- // ── 상태 조회 ──
152
-
153
- /**
154
- * 허브/에이전트/큐/트레이스 상태 조회
155
- * @param {'hub'|'agent'|'queue'|'trace'} scope
156
- */
157
- getStatus(scope = 'hub', { agent_id, trace_id, include_metrics = true } = {}) {
158
- const data = {};
159
-
160
- if (scope === 'hub' || scope === 'queue') {
161
- data.hub = {
162
- state: 'healthy',
163
- uptime_ms: process.uptime() * 1000 | 0,
164
- db_wal_mode: true,
165
- };
166
- if (include_metrics) {
167
- const depths = store.getQueueDepths();
168
- const stats = store.getDeliveryStats();
169
- data.queues = {
170
- urgent_depth: depths.urgent,
171
- normal_depth: depths.normal,
172
- dlq_depth: depths.dlq,
173
- p95_delivery_ms: stats.avg_delivery_ms,
174
- timeout_rate: 0,
175
- };
176
- }
177
- }
178
-
179
- if (scope === 'agent' && agent_id) {
180
- const agent = store.getAgent(agent_id);
181
- if (agent) {
182
- data.agent = {
183
- agent_id: agent.agent_id,
184
- status: agent.status,
185
- pending: 0,
186
- last_seen_ms: agent.last_seen_ms,
187
- };
188
- }
189
- }
190
-
191
- if (scope === 'trace' && trace_id) {
192
- data.trace = store.getMessagesByTrace(trace_id);
193
- }
194
-
195
- return { ok: true, data };
196
- },
197
- };
198
-
199
- return { ...router, responseEmitter };
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
+ );