triflux 10.2.0 → 10.3.0
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.md +236 -156
- package/hub/bridge.mjs +638 -290
- package/hub/codex-compat.mjs +1 -1
- package/hub/fullcycle.mjs +1 -1
- package/hub/intent.mjs +1 -0
- package/hub/lib/mcp-response-cache.mjs +205 -0
- package/hub/pipe.mjs +228 -119
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +1 -0
- package/hub/server.mjs +997 -611
- package/hub/team/ansi.mjs +1 -1
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +256 -125
- package/hub/team/execution-mode.mjs +105 -0
- package/hub/team/headless.mjs +686 -252
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/swarm-hypervisor.mjs +208 -86
- package/hub/team/tui-lite.mjs +18 -2
- package/hub/token-mode.mjs +1 -0
- package/hub/tools.mjs +474 -252
- package/package.json +5 -5
- package/scripts/codex-gateway-preflight.mjs +133 -0
- package/scripts/codex-mcp-gateway-sync.mjs +199 -0
- package/skills/star-prompt/SKILL.md +169 -69
- package/skills/tfx-setup/SKILL.md +124 -0
- package/skills/tfx-swarm/SKILL.md +124 -72
package/hub/pipe.mjs
CHANGED
|
@@ -1,38 +1,37 @@
|
|
|
1
1
|
// hub/pipe.mjs — Named Pipe/Unix socket 제어 채널
|
|
2
2
|
// NDJSON 프로토콜로 에이전트 실시간 제어/이벤트 푸시를 처리한다.
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import { existsSync, unlinkSync } from
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import { createPipeline } from './pipeline/index.mjs';
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
6
|
+
import net from "node:net";
|
|
7
|
+
import { createPipeline } from "./pipeline/index.mjs";
|
|
9
8
|
import {
|
|
10
9
|
ensurePipelineTable,
|
|
11
10
|
initPipelineState,
|
|
12
11
|
listPipelineStates,
|
|
13
12
|
readPipelineState,
|
|
14
|
-
} from
|
|
15
|
-
import { IS_WINDOWS, pipePath } from
|
|
16
|
-
import {
|
|
13
|
+
} from "./pipeline/state.mjs";
|
|
14
|
+
import { IS_WINDOWS, pipePath } from "./platform.mjs";
|
|
15
|
+
import { sendInputToConductorSession } from "./team/conductor-registry.mjs";
|
|
16
|
+
import { getTeamBridge } from "./team-bridge.mjs";
|
|
17
|
+
import { safeJsonParse } from "./workers/worker-utils.mjs";
|
|
17
18
|
|
|
18
19
|
const DEFAULT_HEARTBEAT_TTL_MS = 60000;
|
|
19
|
-
const TEAM_BRIDGE_NOT_REGISTERED =
|
|
20
|
+
const TEAM_BRIDGE_NOT_REGISTERED = "bridge_not_registered";
|
|
20
21
|
|
|
21
22
|
/** 플랫폼별 pipe 경로 계산 */
|
|
22
23
|
export function getPipePath(sessionId = process.pid) {
|
|
23
|
-
return pipePath(
|
|
24
|
+
return pipePath("triflux", sessionId);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
function normalizeTopics(topics) {
|
|
27
28
|
if (!Array.isArray(topics)) return [];
|
|
28
|
-
return topics
|
|
29
|
-
.map((topic) => String(topic || '').trim())
|
|
30
|
-
.filter(Boolean);
|
|
29
|
+
return topics.map((topic) => String(topic || "").trim()).filter(Boolean);
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
function getTeamBridgeMethod(methodName) {
|
|
34
33
|
const method = getTeamBridge()?.[methodName];
|
|
35
|
-
return typeof method ===
|
|
34
|
+
return typeof method === "function" ? method : null;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
function teamInfoFallback(payload = {}) {
|
|
@@ -101,7 +100,7 @@ function teamSendMessageFallback(payload = {}) {
|
|
|
101
100
|
ok: true,
|
|
102
101
|
data: {
|
|
103
102
|
message_id: null,
|
|
104
|
-
recipient: payload.to ??
|
|
103
|
+
recipient: payload.to ?? "team-lead",
|
|
105
104
|
inbox_file: null,
|
|
106
105
|
queued_at: null,
|
|
107
106
|
unread_count: 0,
|
|
@@ -125,9 +124,10 @@ export function createPipeServer({
|
|
|
125
124
|
sessionId = process.pid,
|
|
126
125
|
heartbeatTtlMs = DEFAULT_HEARTBEAT_TTL_MS,
|
|
127
126
|
delegatorService = null,
|
|
127
|
+
hitlManager = null,
|
|
128
128
|
} = {}) {
|
|
129
129
|
if (!router) {
|
|
130
|
-
throw new Error(
|
|
130
|
+
throw new Error("router is required");
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
const pipePath = getPipePath(sessionId);
|
|
@@ -146,14 +146,20 @@ export function createPipeServer({
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
function sendResponse(client, requestId, result) {
|
|
149
|
-
return sendFrame(client, {
|
|
149
|
+
return sendFrame(client, {
|
|
150
|
+
type: "response",
|
|
151
|
+
request_id: requestId,
|
|
152
|
+
...result,
|
|
153
|
+
});
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
function closeClient(client) {
|
|
153
157
|
if (!client || client.closed) return;
|
|
154
158
|
client.closed = true;
|
|
155
159
|
clients.delete(client.id);
|
|
156
|
-
try {
|
|
160
|
+
try {
|
|
161
|
+
client.socket.destroy();
|
|
162
|
+
} catch {}
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
function touchClient(client) {
|
|
@@ -163,7 +169,7 @@ export function createPipeServer({
|
|
|
163
169
|
function resolveAgentId(client, payload) {
|
|
164
170
|
const agentId = payload?.agent_id || client?.agentId;
|
|
165
171
|
if (!agentId) {
|
|
166
|
-
throw new Error(
|
|
172
|
+
throw new Error("agent_id required");
|
|
167
173
|
}
|
|
168
174
|
return agentId;
|
|
169
175
|
}
|
|
@@ -172,11 +178,13 @@ export function createPipeServer({
|
|
|
172
178
|
let delivered = false;
|
|
173
179
|
for (const client of clients.values()) {
|
|
174
180
|
if (client.agentId !== agentId) continue;
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
if (
|
|
182
|
+
sendFrame(client, {
|
|
183
|
+
type: "event",
|
|
184
|
+
event: "message",
|
|
185
|
+
payload: { agent_id: agentId, message },
|
|
186
|
+
})
|
|
187
|
+
) {
|
|
180
188
|
delivered = true;
|
|
181
189
|
}
|
|
182
190
|
}
|
|
@@ -199,18 +207,20 @@ export function createPipeServer({
|
|
|
199
207
|
|
|
200
208
|
async function processCommand(client, action, payload = {}) {
|
|
201
209
|
switch (action) {
|
|
202
|
-
case
|
|
210
|
+
case "register": {
|
|
203
211
|
const result = router.registerAgent(payload);
|
|
204
212
|
if (client) {
|
|
205
213
|
client.agentId = payload.agent_id;
|
|
206
|
-
client.subscriptions = new Set(
|
|
214
|
+
client.subscriptions = new Set(
|
|
215
|
+
router.getSubscribedTopics(client.agentId),
|
|
216
|
+
);
|
|
207
217
|
touchClient(client);
|
|
208
218
|
pushPendingMessages(client.agentId);
|
|
209
219
|
}
|
|
210
220
|
return { ok: true, data: { ...result, pipe_path: pipePath } };
|
|
211
221
|
}
|
|
212
222
|
|
|
213
|
-
case
|
|
223
|
+
case "subscribe": {
|
|
214
224
|
const agentId = resolveAgentId(client, payload);
|
|
215
225
|
const topics = normalizeTopics(payload.topics);
|
|
216
226
|
const result = router.subscribeAgent(agentId, topics, {
|
|
@@ -228,55 +238,61 @@ export function createPipeServer({
|
|
|
228
238
|
};
|
|
229
239
|
}
|
|
230
240
|
|
|
231
|
-
case
|
|
241
|
+
case "ack": {
|
|
232
242
|
const agentId = resolveAgentId(client, payload);
|
|
233
|
-
const acked = router.ackMessages(
|
|
243
|
+
const acked = router.ackMessages(
|
|
244
|
+
payload.message_ids || payload.ack_ids || [],
|
|
245
|
+
agentId,
|
|
246
|
+
);
|
|
234
247
|
if (client) touchClient(client);
|
|
235
248
|
return { ok: true, data: { agent_id: agentId, acked_count: acked } };
|
|
236
249
|
}
|
|
237
250
|
|
|
238
|
-
case
|
|
251
|
+
case "heartbeat": {
|
|
239
252
|
const agentId = resolveAgentId(client, payload);
|
|
240
|
-
const result = router.refreshAgentLease(
|
|
253
|
+
const result = router.refreshAgentLease(
|
|
254
|
+
agentId,
|
|
255
|
+
payload.heartbeat_ttl_ms || heartbeatTtlMs,
|
|
256
|
+
);
|
|
241
257
|
if (client) touchClient(client);
|
|
242
258
|
return { ok: true, data: result };
|
|
243
259
|
}
|
|
244
260
|
|
|
245
|
-
case
|
|
261
|
+
case "publish": {
|
|
246
262
|
const result = router.handlePublish(payload);
|
|
247
263
|
if (client) touchClient(client);
|
|
248
264
|
return result;
|
|
249
265
|
}
|
|
250
266
|
|
|
251
|
-
case
|
|
267
|
+
case "handoff": {
|
|
252
268
|
const result = router.handleHandoff(payload);
|
|
253
269
|
if (client) touchClient(client);
|
|
254
270
|
return result;
|
|
255
271
|
}
|
|
256
272
|
|
|
257
|
-
case
|
|
273
|
+
case "assign": {
|
|
258
274
|
const result = router.assignAsync(payload);
|
|
259
275
|
if (client) touchClient(client);
|
|
260
276
|
return result;
|
|
261
277
|
}
|
|
262
278
|
|
|
263
|
-
case
|
|
279
|
+
case "assign_result": {
|
|
264
280
|
const result = router.reportAssignResult(payload);
|
|
265
281
|
if (client) touchClient(client);
|
|
266
282
|
return result;
|
|
267
283
|
}
|
|
268
284
|
|
|
269
|
-
case
|
|
285
|
+
case "assign_retry": {
|
|
270
286
|
const result = router.retryAssign(payload.job_id, payload);
|
|
271
287
|
if (client) touchClient(client);
|
|
272
288
|
return result;
|
|
273
289
|
}
|
|
274
290
|
|
|
275
|
-
case
|
|
291
|
+
case "result": {
|
|
276
292
|
const result = router.handlePublish({
|
|
277
293
|
from: payload.agent_id,
|
|
278
|
-
to: `topic:${payload.topic ||
|
|
279
|
-
topic: payload.topic ||
|
|
294
|
+
to: `topic:${payload.topic || "task.result"}`,
|
|
295
|
+
topic: payload.topic || "task.result",
|
|
280
296
|
payload: payload.payload || {},
|
|
281
297
|
priority: 5,
|
|
282
298
|
ttl_ms: 3600000,
|
|
@@ -287,19 +303,22 @@ export function createPipeServer({
|
|
|
287
303
|
return result;
|
|
288
304
|
}
|
|
289
305
|
|
|
290
|
-
case
|
|
306
|
+
case "control": {
|
|
291
307
|
const result = router.handlePublish({
|
|
292
|
-
from: payload.from_agent ||
|
|
308
|
+
from: payload.from_agent || "lead",
|
|
293
309
|
to: payload.to_agent,
|
|
294
|
-
topic:
|
|
310
|
+
topic: "lead.control",
|
|
295
311
|
payload: {
|
|
296
312
|
command: payload.command,
|
|
297
|
-
reason: payload.reason ||
|
|
313
|
+
reason: payload.reason || "",
|
|
298
314
|
...(payload.payload || {}),
|
|
299
315
|
issued_at: Date.now(),
|
|
300
316
|
},
|
|
301
317
|
priority: 8,
|
|
302
|
-
ttl_ms: Math.max(
|
|
318
|
+
ttl_ms: Math.max(
|
|
319
|
+
1000,
|
|
320
|
+
Math.min(Number(payload.ttl_ms) || 3600000, 86400000),
|
|
321
|
+
),
|
|
303
322
|
trace_id: payload.trace_id,
|
|
304
323
|
correlation_id: payload.correlation_id,
|
|
305
324
|
});
|
|
@@ -307,18 +326,23 @@ export function createPipeServer({
|
|
|
307
326
|
return result;
|
|
308
327
|
}
|
|
309
328
|
|
|
310
|
-
case
|
|
329
|
+
case "deregister": {
|
|
311
330
|
const agentId = resolveAgentId(client, payload);
|
|
312
|
-
router.updateAgentStatus(agentId,
|
|
331
|
+
router.updateAgentStatus(agentId, "offline");
|
|
313
332
|
if (client) touchClient(client);
|
|
314
333
|
return {
|
|
315
334
|
ok: true,
|
|
316
|
-
data: { agent_id: agentId, status:
|
|
335
|
+
data: { agent_id: agentId, status: "offline" },
|
|
317
336
|
};
|
|
318
337
|
}
|
|
319
338
|
|
|
320
|
-
case
|
|
321
|
-
|
|
339
|
+
case "send_input": {
|
|
340
|
+
if (client) touchClient(client);
|
|
341
|
+
return sendInputToConductorSession(payload.session_id, payload.text);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
case "team_task_update": {
|
|
345
|
+
const teamTaskUpdate = getTeamBridgeMethod("teamTaskUpdate");
|
|
322
346
|
const result = teamTaskUpdate
|
|
323
347
|
? await teamTaskUpdate(payload)
|
|
324
348
|
: teamTaskUpdateFallback(payload);
|
|
@@ -326,8 +350,8 @@ export function createPipeServer({
|
|
|
326
350
|
return result;
|
|
327
351
|
}
|
|
328
352
|
|
|
329
|
-
case
|
|
330
|
-
const teamSendMessage = getTeamBridgeMethod(
|
|
353
|
+
case "team_send_message": {
|
|
354
|
+
const teamSendMessage = getTeamBridgeMethod("teamSendMessage");
|
|
331
355
|
const result = teamSendMessage
|
|
332
356
|
? await teamSendMessage(payload)
|
|
333
357
|
: teamSendMessageFallback(payload);
|
|
@@ -335,20 +359,20 @@ export function createPipeServer({
|
|
|
335
359
|
return result;
|
|
336
360
|
}
|
|
337
361
|
|
|
338
|
-
case
|
|
362
|
+
case "pipeline_advance": {
|
|
339
363
|
if (client) touchClient(client);
|
|
340
364
|
if (!store?.db) {
|
|
341
|
-
return { ok: false, error:
|
|
365
|
+
return { ok: false, error: "hub_db_not_found" };
|
|
342
366
|
}
|
|
343
367
|
ensurePipelineTable(store.db);
|
|
344
368
|
const pipeline = createPipeline(store.db, payload.team_name);
|
|
345
369
|
return pipeline.advance(payload.phase);
|
|
346
370
|
}
|
|
347
371
|
|
|
348
|
-
case
|
|
372
|
+
case "pipeline_init": {
|
|
349
373
|
if (client) touchClient(client);
|
|
350
374
|
if (!store?.db) {
|
|
351
|
-
return { ok: false, error:
|
|
375
|
+
return { ok: false, error: "hub_db_not_found" };
|
|
352
376
|
}
|
|
353
377
|
ensurePipelineTable(store.db);
|
|
354
378
|
const state = initPipelineState(store.db, payload.team_name, {
|
|
@@ -358,18 +382,46 @@ export function createPipeServer({
|
|
|
358
382
|
return { ok: true, data: state };
|
|
359
383
|
}
|
|
360
384
|
|
|
361
|
-
case
|
|
385
|
+
case "hitl_request": {
|
|
386
|
+
if (!hitlManager) {
|
|
387
|
+
return { ok: false, error: "hitl not available" };
|
|
388
|
+
}
|
|
389
|
+
if (client) touchClient(client);
|
|
390
|
+
return hitlManager.requestHumanInput(payload);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
case "hitl_submit": {
|
|
394
|
+
if (!hitlManager) {
|
|
395
|
+
return { ok: false, error: "hitl not available" };
|
|
396
|
+
}
|
|
397
|
+
if (client) touchClient(client);
|
|
398
|
+
return hitlManager.submitHumanInput(payload);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
case "delegator_delegate": {
|
|
362
402
|
if (!delegatorService) {
|
|
363
|
-
return {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
error: {
|
|
406
|
+
code: "DELEGATOR_NOT_AVAILABLE",
|
|
407
|
+
message: "Delegator service가 초기화되지 않았습니다",
|
|
408
|
+
},
|
|
409
|
+
};
|
|
364
410
|
}
|
|
365
411
|
if (client) touchClient(client);
|
|
366
412
|
const result = await delegatorService.delegate(payload);
|
|
367
413
|
return { ok: result?.ok !== false, data: result };
|
|
368
414
|
}
|
|
369
415
|
|
|
370
|
-
case
|
|
416
|
+
case "delegator_reply": {
|
|
371
417
|
if (!delegatorService) {
|
|
372
|
-
return {
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
error: {
|
|
421
|
+
code: "DELEGATOR_NOT_AVAILABLE",
|
|
422
|
+
message: "Delegator service가 초기화되지 않았습니다",
|
|
423
|
+
},
|
|
424
|
+
};
|
|
373
425
|
}
|
|
374
426
|
if (client) touchClient(client);
|
|
375
427
|
const result = await delegatorService.reply(payload);
|
|
@@ -379,13 +431,19 @@ export function createPipeServer({
|
|
|
379
431
|
default:
|
|
380
432
|
return {
|
|
381
433
|
ok: false,
|
|
382
|
-
error: {
|
|
434
|
+
error: {
|
|
435
|
+
code: "UNKNOWN_PIPE_COMMAND",
|
|
436
|
+
message: `지원하지 않는 command: ${action}`,
|
|
437
|
+
},
|
|
383
438
|
};
|
|
384
439
|
}
|
|
385
440
|
}
|
|
386
441
|
|
|
387
442
|
function buildReplayMessages(agentId, payload = {}) {
|
|
388
|
-
const maxMessages = Math.max(
|
|
443
|
+
const maxMessages = Math.max(
|
|
444
|
+
1,
|
|
445
|
+
Math.min(Number(payload.max_messages) || 20, 100),
|
|
446
|
+
);
|
|
389
447
|
const pending = router.getPendingMessages(agentId, {
|
|
390
448
|
max_messages: maxMessages,
|
|
391
449
|
include_topics: payload.topics,
|
|
@@ -410,7 +468,7 @@ export function createPipeServer({
|
|
|
410
468
|
|
|
411
469
|
async function processQuery(client, action, payload = {}) {
|
|
412
470
|
switch (action) {
|
|
413
|
-
case
|
|
471
|
+
case "drain": {
|
|
414
472
|
const agentId = resolveAgentId(client, payload);
|
|
415
473
|
const messages = router.drainAgent(agentId, {
|
|
416
474
|
max_messages: payload.max_messages,
|
|
@@ -420,69 +478,95 @@ export function createPipeServer({
|
|
|
420
478
|
if (client) touchClient(client);
|
|
421
479
|
return {
|
|
422
480
|
ok: true,
|
|
423
|
-
data: {
|
|
481
|
+
data: {
|
|
482
|
+
messages,
|
|
483
|
+
count: messages.length,
|
|
484
|
+
server_time_ms: Date.now(),
|
|
485
|
+
},
|
|
424
486
|
};
|
|
425
487
|
}
|
|
426
488
|
|
|
427
|
-
case
|
|
489
|
+
case "context": {
|
|
428
490
|
const agentId = resolveAgentId(client, payload);
|
|
429
491
|
const messages = buildReplayMessages(agentId, payload);
|
|
430
492
|
if (client) touchClient(client);
|
|
431
493
|
return {
|
|
432
494
|
ok: true,
|
|
433
|
-
data: {
|
|
495
|
+
data: {
|
|
496
|
+
messages,
|
|
497
|
+
count: messages.length,
|
|
498
|
+
server_time_ms: Date.now(),
|
|
499
|
+
},
|
|
434
500
|
};
|
|
435
501
|
}
|
|
436
502
|
|
|
437
|
-
case
|
|
438
|
-
const scope = payload.scope ||
|
|
503
|
+
case "status": {
|
|
504
|
+
const scope = payload.scope || "hub";
|
|
439
505
|
if (client) touchClient(client);
|
|
440
506
|
return router.getStatus(scope, payload);
|
|
441
507
|
}
|
|
442
508
|
|
|
443
|
-
case
|
|
509
|
+
case "assign_status": {
|
|
444
510
|
if (client) touchClient(client);
|
|
445
511
|
return router.getAssignStatus(payload);
|
|
446
512
|
}
|
|
447
513
|
|
|
448
|
-
case
|
|
449
|
-
const teamInfo = getTeamBridgeMethod(
|
|
450
|
-
const result = teamInfo
|
|
514
|
+
case "team_info": {
|
|
515
|
+
const teamInfo = getTeamBridgeMethod("teamInfo");
|
|
516
|
+
const result = teamInfo
|
|
517
|
+
? await teamInfo(payload)
|
|
518
|
+
: teamInfoFallback(payload);
|
|
451
519
|
if (client) touchClient(client);
|
|
452
520
|
return result;
|
|
453
521
|
}
|
|
454
522
|
|
|
455
|
-
case
|
|
456
|
-
const teamTaskList = getTeamBridgeMethod(
|
|
457
|
-
const result = teamTaskList
|
|
523
|
+
case "team_task_list": {
|
|
524
|
+
const teamTaskList = getTeamBridgeMethod("teamTaskList");
|
|
525
|
+
const result = teamTaskList
|
|
526
|
+
? await teamTaskList(payload)
|
|
527
|
+
: teamTaskListFallback(payload);
|
|
458
528
|
if (client) touchClient(client);
|
|
459
529
|
return result;
|
|
460
530
|
}
|
|
461
531
|
|
|
462
|
-
case
|
|
532
|
+
case "pipeline_state": {
|
|
463
533
|
if (client) touchClient(client);
|
|
464
534
|
if (!store?.db) {
|
|
465
|
-
return { ok: false, error:
|
|
535
|
+
return { ok: false, error: "hub_db_not_found" };
|
|
466
536
|
}
|
|
467
537
|
ensurePipelineTable(store.db);
|
|
468
538
|
const state = readPipelineState(store.db, payload.team_name);
|
|
469
539
|
return state
|
|
470
540
|
? { ok: true, data: state }
|
|
471
|
-
: { ok: false, error:
|
|
541
|
+
: { ok: false, error: "pipeline_not_found" };
|
|
472
542
|
}
|
|
473
543
|
|
|
474
|
-
case
|
|
544
|
+
case "pipeline_list": {
|
|
475
545
|
if (client) touchClient(client);
|
|
476
546
|
if (!store?.db) {
|
|
477
|
-
return { ok: false, error:
|
|
547
|
+
return { ok: false, error: "hub_db_not_found" };
|
|
478
548
|
}
|
|
479
549
|
ensurePipelineTable(store.db);
|
|
480
550
|
return { ok: true, data: listPipelineStates(store.db) };
|
|
481
551
|
}
|
|
482
552
|
|
|
483
|
-
case
|
|
553
|
+
case "hitl_pending": {
|
|
554
|
+
if (client) touchClient(client);
|
|
555
|
+
if (!hitlManager) {
|
|
556
|
+
return { ok: false, error: "hitl not available" };
|
|
557
|
+
}
|
|
558
|
+
return { ok: true, data: hitlManager.getPendingRequests() };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
case "delegator_status": {
|
|
484
562
|
if (!delegatorService) {
|
|
485
|
-
return {
|
|
563
|
+
return {
|
|
564
|
+
ok: false,
|
|
565
|
+
error: {
|
|
566
|
+
code: "DELEGATOR_NOT_AVAILABLE",
|
|
567
|
+
message: "Delegator service가 초기화되지 않았습니다",
|
|
568
|
+
},
|
|
569
|
+
};
|
|
486
570
|
}
|
|
487
571
|
if (client) touchClient(client);
|
|
488
572
|
const result = await delegatorService.status(payload);
|
|
@@ -492,7 +576,10 @@ export function createPipeServer({
|
|
|
492
576
|
default:
|
|
493
577
|
return {
|
|
494
578
|
ok: false,
|
|
495
|
-
error: {
|
|
579
|
+
error: {
|
|
580
|
+
code: "UNKNOWN_PIPE_QUERY",
|
|
581
|
+
message: `지원하지 않는 query: ${action}`,
|
|
582
|
+
},
|
|
496
583
|
};
|
|
497
584
|
}
|
|
498
585
|
}
|
|
@@ -507,41 +594,56 @@ export function createPipeServer({
|
|
|
507
594
|
}
|
|
508
595
|
|
|
509
596
|
async function handleFrame(client, frame) {
|
|
510
|
-
if (!frame || typeof frame !==
|
|
597
|
+
if (!frame || typeof frame !== "object") {
|
|
511
598
|
return sendResponse(client, null, {
|
|
512
599
|
ok: false,
|
|
513
|
-
error: { code:
|
|
600
|
+
error: { code: "INVALID_FRAME", message: "JSON object frame required" },
|
|
514
601
|
});
|
|
515
602
|
}
|
|
516
603
|
|
|
517
604
|
if (!frame.type) {
|
|
518
605
|
return sendResponse(client, frame.request_id || null, {
|
|
519
606
|
ok: false,
|
|
520
|
-
error: { code:
|
|
607
|
+
error: { code: "INVALID_FRAME", message: "type required" },
|
|
521
608
|
});
|
|
522
609
|
}
|
|
523
610
|
|
|
524
611
|
touchClient(client);
|
|
525
612
|
|
|
526
613
|
try {
|
|
527
|
-
if (frame.type ===
|
|
614
|
+
if (frame.type === "command") {
|
|
528
615
|
const action = frame.payload?.action || frame.payload?.command;
|
|
529
|
-
const result = await processCommand(
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
616
|
+
const result = await processCommand(
|
|
617
|
+
client,
|
|
618
|
+
action,
|
|
619
|
+
frame.payload || {},
|
|
620
|
+
);
|
|
621
|
+
return sendResponse(
|
|
622
|
+
client,
|
|
623
|
+
frame.payload?.request_id || frame.request_id || null,
|
|
624
|
+
result,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (frame.type === "query") {
|
|
533
628
|
const action = frame.payload?.action || frame.payload?.query;
|
|
534
629
|
const result = await processQuery(client, action, frame.payload || {});
|
|
535
|
-
return sendResponse(
|
|
630
|
+
return sendResponse(
|
|
631
|
+
client,
|
|
632
|
+
frame.payload?.request_id || frame.request_id || null,
|
|
633
|
+
result,
|
|
634
|
+
);
|
|
536
635
|
}
|
|
537
636
|
return sendResponse(client, frame.request_id || null, {
|
|
538
637
|
ok: false,
|
|
539
|
-
error: {
|
|
638
|
+
error: {
|
|
639
|
+
code: "INVALID_FRAME_TYPE",
|
|
640
|
+
message: `지원하지 않는 type: ${frame.type}`,
|
|
641
|
+
},
|
|
540
642
|
});
|
|
541
643
|
} catch (error) {
|
|
542
644
|
return sendResponse(client, frame.request_id || null, {
|
|
543
645
|
ok: false,
|
|
544
|
-
error: { code:
|
|
646
|
+
error: { code: "PIPE_REQUEST_FAILED", message: error.message },
|
|
545
647
|
});
|
|
546
648
|
}
|
|
547
649
|
}
|
|
@@ -550,7 +652,7 @@ export function createPipeServer({
|
|
|
550
652
|
const client = {
|
|
551
653
|
id: randomUUID(),
|
|
552
654
|
socket,
|
|
553
|
-
buffer:
|
|
655
|
+
buffer: "",
|
|
554
656
|
agentId: null,
|
|
555
657
|
subscriptions: new Set(),
|
|
556
658
|
lastHeartbeatMs: Date.now(),
|
|
@@ -558,10 +660,10 @@ export function createPipeServer({
|
|
|
558
660
|
};
|
|
559
661
|
clients.set(client.id, client);
|
|
560
662
|
|
|
561
|
-
socket.setEncoding(
|
|
562
|
-
socket.on(
|
|
663
|
+
socket.setEncoding("utf8");
|
|
664
|
+
socket.on("data", async (chunk) => {
|
|
563
665
|
client.buffer += chunk;
|
|
564
|
-
let newlineIndex = client.buffer.indexOf(
|
|
666
|
+
let newlineIndex = client.buffer.indexOf("\n");
|
|
565
667
|
while (newlineIndex >= 0) {
|
|
566
668
|
const line = client.buffer.slice(0, newlineIndex).trim();
|
|
567
669
|
client.buffer = client.buffer.slice(newlineIndex + 1);
|
|
@@ -569,27 +671,30 @@ export function createPipeServer({
|
|
|
569
671
|
const frame = safeJsonParse(line);
|
|
570
672
|
await handleFrame(client, frame);
|
|
571
673
|
}
|
|
572
|
-
newlineIndex = client.buffer.indexOf(
|
|
674
|
+
newlineIndex = client.buffer.indexOf("\n");
|
|
573
675
|
}
|
|
574
676
|
});
|
|
575
677
|
|
|
576
|
-
socket.on(
|
|
577
|
-
socket.on(
|
|
678
|
+
socket.on("close", () => closeClient(client));
|
|
679
|
+
socket.on("error", () => closeClient(client));
|
|
578
680
|
}
|
|
579
681
|
|
|
580
682
|
function startHeartbeatMonitor() {
|
|
581
|
-
heartbeatTimer = setInterval(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
683
|
+
heartbeatTimer = setInterval(
|
|
684
|
+
() => {
|
|
685
|
+
const now = Date.now();
|
|
686
|
+
for (const client of clients.values()) {
|
|
687
|
+
if (now - client.lastHeartbeatMs <= heartbeatTtlMs) continue;
|
|
688
|
+
sendFrame(client, {
|
|
689
|
+
type: "event",
|
|
690
|
+
event: "disconnect",
|
|
691
|
+
payload: { reason: "heartbeat_timeout" },
|
|
692
|
+
});
|
|
693
|
+
closeClient(client);
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
Math.max(1000, Math.floor(heartbeatTtlMs / 2)),
|
|
697
|
+
);
|
|
593
698
|
heartbeatTimer.unref();
|
|
594
699
|
}
|
|
595
700
|
|
|
@@ -600,16 +705,18 @@ export function createPipeServer({
|
|
|
600
705
|
if (server) return { path: pipePath };
|
|
601
706
|
|
|
602
707
|
if (!IS_WINDOWS && existsSync(pipePath)) {
|
|
603
|
-
try {
|
|
708
|
+
try {
|
|
709
|
+
unlinkSync(pipePath);
|
|
710
|
+
} catch {}
|
|
604
711
|
}
|
|
605
712
|
|
|
606
713
|
server = net.createServer(attachSocket);
|
|
607
|
-
router.deliveryEmitter.on(
|
|
714
|
+
router.deliveryEmitter.on("message", onMessage);
|
|
608
715
|
|
|
609
716
|
await new Promise((resolve, reject) => {
|
|
610
|
-
server.once(
|
|
717
|
+
server.once("error", reject);
|
|
611
718
|
server.listen(pipePath, () => {
|
|
612
|
-
server.off(
|
|
719
|
+
server.off("error", reject);
|
|
613
720
|
resolve();
|
|
614
721
|
});
|
|
615
722
|
});
|
|
@@ -624,7 +731,7 @@ export function createPipeServer({
|
|
|
624
731
|
heartbeatTimer = null;
|
|
625
732
|
}
|
|
626
733
|
|
|
627
|
-
router.deliveryEmitter.off(
|
|
734
|
+
router.deliveryEmitter.off("message", onMessage);
|
|
628
735
|
|
|
629
736
|
for (const client of clients.values()) {
|
|
630
737
|
closeClient(client);
|
|
@@ -637,14 +744,16 @@ export function createPipeServer({
|
|
|
637
744
|
}
|
|
638
745
|
|
|
639
746
|
if (!IS_WINDOWS && existsSync(pipePath)) {
|
|
640
|
-
try {
|
|
747
|
+
try {
|
|
748
|
+
unlinkSync(pipePath);
|
|
749
|
+
} catch {}
|
|
641
750
|
}
|
|
642
751
|
},
|
|
643
752
|
|
|
644
753
|
getStatus() {
|
|
645
754
|
return {
|
|
646
755
|
path: pipePath,
|
|
647
|
-
protocol:
|
|
756
|
+
protocol: "ndjson",
|
|
648
757
|
clients: clients.size,
|
|
649
758
|
pending_messages: Array.from(clients.values()).reduce((sum, client) => {
|
|
650
759
|
if (!client.agentId) return sum;
|