@triflux/remote 10.50.2 → 10.52.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.
Files changed (134) hide show
  1. package/cto/brief.mjs +0 -20
  2. package/cto/collect.mjs +1 -200
  3. package/cto/current.schema.json +2 -23
  4. package/cto/hygiene.mjs +2 -61
  5. package/cto/lake-root.mjs +0 -56
  6. package/cto/status.mjs +5 -187
  7. package/hub/index.mjs +0 -17
  8. package/hub/team/agent-map.json +0 -1
  9. package/hub/team/agents-row-attach.py +120 -0
  10. package/hub/team/agents-row.mjs +200 -0
  11. package/hub/team/ansi.mjs +1 -35
  12. package/hub/team/backend.mjs +9 -145
  13. package/hub/team/claude-daemon-control.mjs +1 -2
  14. package/hub/team/claude-session-projection.mjs +5 -73
  15. package/hub/team/cli/commands/attach.mjs +1 -17
  16. package/hub/team/cli/commands/control.mjs +12 -38
  17. package/hub/team/cli/commands/debug.mjs +2 -39
  18. package/hub/team/cli/commands/focus.mjs +5 -31
  19. package/hub/team/cli/commands/interrupt.mjs +6 -25
  20. package/hub/team/cli/commands/kill.mjs +1 -30
  21. package/hub/team/cli/commands/list.mjs +0 -16
  22. package/hub/team/cli/commands/send.mjs +6 -26
  23. package/hub/team/cli/commands/start/index.mjs +42 -133
  24. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  25. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  26. package/hub/team/cli/commands/start/start-mux.mjs +2 -7
  27. package/hub/team/cli/commands/status.mjs +2 -57
  28. package/hub/team/cli/commands/stop.mjs +14 -19
  29. package/hub/team/cli/help.mjs +4 -5
  30. package/hub/team/cli/manifest.mjs +5 -1
  31. package/hub/team/cli/render.mjs +0 -12
  32. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  33. package/hub/team/cli/services/state-store.mjs +5 -20
  34. package/hub/team/handoff.mjs +3 -34
  35. package/hub/team/headless.mjs +108 -1296
  36. package/hub/team/orchestrator.mjs +4 -30
  37. package/hub/team/pane.mjs +6 -28
  38. package/hub/team/psmux.mjs +3 -34
  39. package/hub/team/remote-session.mjs +1 -2
  40. package/hub/team/session-context.mjs +1 -1
  41. package/hub/team/session.mjs +7 -91
  42. package/hub/team/tui-core.mjs +1 -1
  43. package/hub/team/tui-viewer.mjs +11 -58
  44. package/hub/team/tui.mjs +8 -306
  45. package/hub/team/uds-orchestrator.mjs +2 -580
  46. package/hub/team/wt-manager.mjs +2 -2
  47. package/hub/workers/factory.mjs +6 -84
  48. package/hub/workers/interface.mjs +1 -7
  49. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  50. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
  51. package/package.json +3 -8
  52. package/scripts/lib/agent-route-policy.mjs +143 -62
  53. package/scripts/lib/cli-claude.mjs +0 -14
  54. package/scripts/lib/env-probe.mjs +2 -131
  55. package/scripts/lib/gemini-profiles.mjs +2 -69
  56. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  57. package/scripts/lib/machine-profile.mjs +3 -17
  58. package/scripts/lib/mcp-filter.mjs +109 -53
  59. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  60. package/scripts/lib/mcp-health.mjs +2 -2
  61. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  62. package/scripts/lib/psmux-info.mjs +11 -59
  63. package/scripts/lib/skill-template.mjs +0 -245
  64. package/hub/hub-lifecycle.mjs +0 -248
  65. package/hub/pipe.mjs +0 -825
  66. package/hub/promote-penalties.mjs +0 -138
  67. package/hub/public/dashboard.html +0 -416
  68. package/hub/schema.sql +0 -209
  69. package/hub/server.mjs +0 -3048
  70. package/hub/store-adapter.mjs +0 -182
  71. package/hub/store.mjs +0 -1736
  72. package/hub/team/build-worker-prompt.mjs +0 -92
  73. package/hub/team/check-mcp-hub.mjs +0 -49
  74. package/hub/team/claude-native-bridge.mjs +0 -987
  75. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  76. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  77. package/hub/team/cli/services/hub-client.mjs +0 -264
  78. package/hub/team/cli/services/native-control.mjs +0 -145
  79. package/hub/team/codex-review.mjs +0 -520
  80. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  81. package/hub/team/conductor-registry.mjs +0 -121
  82. package/hub/team/conductor.mjs +0 -1529
  83. package/hub/team/consensus-meta.mjs +0 -176
  84. package/hub/team/cto-auto-collect.mjs +0 -115
  85. package/hub/team/cto-pull.mjs +0 -31
  86. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  87. package/hub/team/dashboard.mjs +0 -322
  88. package/hub/team/event-log.mjs +0 -84
  89. package/hub/team/execution-mode.mjs +0 -324
  90. package/hub/team/extract-completion-payload.mjs +0 -110
  91. package/hub/team/headless-bridge-session.mjs +0 -17
  92. package/hub/team/health-probe.mjs +0 -435
  93. package/hub/team/interactive-native-launcher.mjs +0 -119
  94. package/hub/team/interactive-native-worker.mjs +0 -80
  95. package/hub/team/interactive-tui-transport.mjs +0 -479
  96. package/hub/team/launcher-template.mjs +0 -144
  97. package/hub/team/lead-control.mjs +0 -191
  98. package/hub/team/mcp-selector.mjs +0 -145
  99. package/hub/team/native-supervisor.mjs +0 -460
  100. package/hub/team/native.mjs +0 -727
  101. package/hub/team/recovery-store.mjs +0 -149
  102. package/hub/team/remote-probe.mjs +0 -292
  103. package/hub/team/remote-watcher.mjs +0 -506
  104. package/hub/team/runtime-strategy.mjs +0 -200
  105. package/hub/team/schemas/worker-completion.json +0 -39
  106. package/hub/team/sentinel-capture.mjs +0 -127
  107. package/hub/team/session-sync.mjs +0 -316
  108. package/hub/team/swarm-cli.mjs +0 -390
  109. package/hub/team/swarm-hypervisor.mjs +0 -2206
  110. package/hub/team/swarm-intent.mjs +0 -216
  111. package/hub/team/swarm-locks.mjs +0 -335
  112. package/hub/team/swarm-planner.mjs +0 -371
  113. package/hub/team/swarm-preflight.mjs +0 -366
  114. package/hub/team/swarm-reconciler.mjs +0 -242
  115. package/hub/team/synapse-cli.mjs +0 -458
  116. package/hub/team/synapse-http.mjs +0 -311
  117. package/hub/team/synapse-registry.mjs +0 -618
  118. package/hub/team/tui-remote-adapter.mjs +0 -377
  119. package/hub/team/tui-synapse.mjs +0 -223
  120. package/hub/team/worker-completion-validator.mjs +0 -79
  121. package/hub/team/worker-sandbox.mjs +0 -125
  122. package/hub/team/worktree-lifecycle.mjs +0 -639
  123. package/hub/team/wt-templates.json +0 -43
  124. package/hub/tools.mjs +0 -900
  125. package/hub/workers/antigravity-route-worker.mjs +0 -179
  126. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  127. package/hub/workers/delegator-mcp.mjs +0 -1098
  128. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  129. package/scripts/lib/claudemd-scanner.mjs +0 -223
  130. package/scripts/lib/context.mjs +0 -67
  131. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  132. package/scripts/lib/handoff.mjs +0 -187
  133. package/scripts/lib/logger.mjs +0 -122
  134. package/scripts/lib/stealth-fetch.mjs +0 -176
package/hub/pipe.mjs DELETED
@@ -1,825 +0,0 @@
1
- // hub/pipe.mjs — Named Pipe/Unix socket 제어 채널
2
- // NDJSON 프로토콜로 에이전트 실시간 제어/이벤트 푸시를 처리한다.
3
-
4
- import { randomUUID } from "node:crypto";
5
- import { existsSync, unlinkSync } from "node:fs";
6
- import net from "node:net";
7
- import { createPipeline } from "@triflux/core/hub/pipeline/index.mjs";
8
- import {
9
- ensurePipelineTable,
10
- initPipelineState,
11
- listPipelineStates,
12
- readPipelineState,
13
- } from "@triflux/core/hub/pipeline/state.mjs";
14
- import { IS_WINDOWS, pipePath } from "@triflux/core/hub/platform.mjs";
15
- import { sendInputToConductorSession } from "./team/conductor-registry.mjs";
16
- import { getTeamBridge } from "@triflux/core/hub/team-bridge.mjs";
17
- import { safeJsonParse } from "./workers/worker-utils.mjs";
18
-
19
- const DEFAULT_HEARTBEAT_TTL_MS = 60000;
20
- const TEAM_BRIDGE_NOT_REGISTERED = "bridge_not_registered";
21
-
22
- /** 플랫폼별 pipe 경로 계산 */
23
- export function getPipePath(sessionId = process.pid) {
24
- return pipePath("triflux", sessionId);
25
- }
26
-
27
- function normalizeTopics(topics) {
28
- if (!Array.isArray(topics)) return [];
29
- return topics.map((topic) => String(topic || "").trim()).filter(Boolean);
30
- }
31
-
32
- function normalizeReplayTopics(topics) {
33
- if (Array.isArray(topics)) return normalizeTopics(topics);
34
- if (typeof topics === "string") {
35
- return topics
36
- .split(/[,\s]+/u)
37
- .map((topic) => topic.trim())
38
- .filter(Boolean);
39
- }
40
- return [];
41
- }
42
-
43
- function getTeamBridgeMethod(methodName) {
44
- const method = getTeamBridge()?.[methodName];
45
- return typeof method === "function" ? method : null;
46
- }
47
-
48
- function teamInfoFallback(payload = {}) {
49
- return {
50
- ok: true,
51
- data: {
52
- team: {
53
- team_name: payload.team_name ?? null,
54
- description: null,
55
- },
56
- lead: {
57
- lead_agent_id: null,
58
- lead_session_id: null,
59
- },
60
- ...(payload.include_members !== false ? { members: [] } : {}),
61
- ...(payload.include_paths !== false
62
- ? {
63
- paths: {
64
- config_path: null,
65
- tasks_dir: null,
66
- inboxes_dir: null,
67
- tasks_dir_resolution: TEAM_BRIDGE_NOT_REGISTERED,
68
- },
69
- }
70
- : {}),
71
- bridge_installed: false,
72
- skipped: true,
73
- },
74
- };
75
- }
76
-
77
- function teamTaskListFallback() {
78
- return {
79
- ok: true,
80
- data: {
81
- tasks: [],
82
- count: 0,
83
- parse_warnings: 0,
84
- tasks_dir: null,
85
- tasks_dir_resolution: TEAM_BRIDGE_NOT_REGISTERED,
86
- bridge_installed: false,
87
- skipped: true,
88
- },
89
- };
90
- }
91
-
92
- function teamTaskUpdateFallback(payload = {}) {
93
- return {
94
- ok: true,
95
- data: {
96
- claimed: false,
97
- updated: false,
98
- task_before: null,
99
- task_after: null,
100
- task_file: null,
101
- task_id: payload.task_id ?? null,
102
- mtime_ms: null,
103
- bridge_installed: false,
104
- skipped: true,
105
- },
106
- };
107
- }
108
-
109
- function teamSendMessageFallback(payload = {}) {
110
- return {
111
- ok: true,
112
- data: {
113
- message_id: null,
114
- recipient: payload.to ?? "team-lead",
115
- inbox_file: null,
116
- queued_at: null,
117
- unread_count: 0,
118
- bridge_installed: false,
119
- skipped: true,
120
- },
121
- };
122
- }
123
-
124
- /**
125
- * Named Pipe 서버 생성
126
- * @param {object} opts
127
- * @param {object} opts.router
128
- * @param {object} [opts.store]
129
- * @param {string|number} [opts.sessionId]
130
- * @param {number} [opts.heartbeatTtlMs]
131
- */
132
- export function createPipeServer({
133
- router,
134
- store = null,
135
- sessionId = process.pid,
136
- heartbeatTtlMs = DEFAULT_HEARTBEAT_TTL_MS,
137
- delegatorService = null,
138
- hitlManager = null,
139
- onActivity = null,
140
- } = {}) {
141
- if (!router) {
142
- throw new Error("router is required");
143
- }
144
-
145
- const pipePath = getPipePath(sessionId);
146
- const clients = new Map();
147
- let server = null;
148
- let heartbeatTimer = null;
149
-
150
- function sendFrame(client, frame) {
151
- if (!client || client.closed || !client.socket.writable) return false;
152
- try {
153
- client.socket.write(`${JSON.stringify(frame)}\n`);
154
- return true;
155
- } catch {
156
- return false;
157
- }
158
- }
159
-
160
- function sendResponse(client, requestId, result) {
161
- return sendFrame(client, {
162
- type: "response",
163
- request_id: requestId,
164
- ...result,
165
- });
166
- }
167
-
168
- function closeClient(client) {
169
- if (!client || client.closed) return;
170
- client.closed = true;
171
- clients.delete(client.id);
172
- try {
173
- client.socket.destroy();
174
- } catch {}
175
- }
176
-
177
- function touchClient(client) {
178
- client.lastHeartbeatMs = Date.now();
179
- if (onActivity) onActivity();
180
- }
181
-
182
- function resolveAgentId(client, payload) {
183
- const agentId = payload?.agent_id || client?.agentId;
184
- if (!agentId) {
185
- throw new Error("agent_id required");
186
- }
187
- return agentId;
188
- }
189
-
190
- function pushEvent(agentId, message) {
191
- let delivered = false;
192
- for (const client of clients.values()) {
193
- if (client.agentId !== agentId) continue;
194
- if (
195
- sendFrame(client, {
196
- type: "event",
197
- event: "message",
198
- payload: { agent_id: agentId, message },
199
- })
200
- ) {
201
- delivered = true;
202
- }
203
- }
204
- return delivered;
205
- }
206
-
207
- function pushPendingMessages(agentId) {
208
- if (!agentId) return 0;
209
- const pending = router.getPendingMessages(agentId, { max_messages: 100 });
210
- let pushed = 0;
211
- for (const message of pending) {
212
- if (router.markMessagePushed(agentId, message.id)) {
213
- pushed += pushEvent(agentId, message) ? 1 : 0;
214
- } else if (pushEvent(agentId, message)) {
215
- pushed += 1;
216
- }
217
- }
218
- return pushed;
219
- }
220
-
221
- async function processCommand(client, action, payload = {}) {
222
- switch (action) {
223
- case "register": {
224
- const result = router.registerAgent(payload);
225
- if (!result?.ok) return result;
226
- if (client) {
227
- client.agentId = payload.agent_id;
228
- client.subscriptions = new Set(
229
- router.getSubscribedTopics(client.agentId),
230
- );
231
- touchClient(client);
232
- pushPendingMessages(client.agentId);
233
- }
234
- return { ok: true, data: { ...result.data, pipe_path: pipePath } };
235
- }
236
-
237
- case "subscribe": {
238
- const agentId = resolveAgentId(client, payload);
239
- const topics = normalizeTopics(payload.topics);
240
- const result = router.subscribeAgent(agentId, topics, {
241
- replace: Boolean(payload.replace),
242
- });
243
- if (client) {
244
- client.agentId = agentId;
245
- client.subscriptions = new Set(result.topics);
246
- touchClient(client);
247
- }
248
- const replayed = pushPendingMessages(agentId);
249
- return {
250
- ok: true,
251
- data: { ...result, replayed_messages: replayed },
252
- };
253
- }
254
-
255
- case "ack": {
256
- const agentId = resolveAgentId(client, payload);
257
- const acked = router.ackMessages(
258
- payload.message_ids || payload.ack_ids || [],
259
- agentId,
260
- );
261
- if (client) touchClient(client);
262
- return { ok: true, data: { agent_id: agentId, acked_count: acked } };
263
- }
264
-
265
- case "heartbeat": {
266
- const agentId = resolveAgentId(client, payload);
267
- const result = router.refreshAgentLease(
268
- agentId,
269
- payload.heartbeat_ttl_ms || heartbeatTtlMs,
270
- payload.presence_generation,
271
- payload,
272
- );
273
- if (client) touchClient(client);
274
- return result;
275
- }
276
-
277
- case "publish": {
278
- const result = router.handlePublish(payload);
279
- if (client) touchClient(client);
280
- return result;
281
- }
282
-
283
- case "takeover_role": {
284
- const result = router.takeoverRole(payload);
285
- if (client) touchClient(client);
286
- return result;
287
- }
288
-
289
- case "handoff": {
290
- const result = router.handleHandoff(payload);
291
- if (client) touchClient(client);
292
- return result;
293
- }
294
-
295
- case "assign": {
296
- const result = router.assignAsync(payload);
297
- if (client) touchClient(client);
298
- return result;
299
- }
300
-
301
- case "assign_result": {
302
- const result = router.reportAssignResult(payload);
303
- if (client) touchClient(client);
304
- return result;
305
- }
306
-
307
- case "assign_retry": {
308
- const result = router.retryAssign(payload.job_id, payload);
309
- if (client) touchClient(client);
310
- return result;
311
- }
312
-
313
- case "result": {
314
- const result = router.handlePublish({
315
- from: payload.agent_id,
316
- to: `topic:${payload.topic || "task.result"}`,
317
- topic: payload.topic || "task.result",
318
- payload: payload.payload || {},
319
- priority: 5,
320
- ttl_ms: 3600000,
321
- trace_id: payload.trace_id,
322
- correlation_id: payload.correlation_id,
323
- });
324
- if (client) touchClient(client);
325
- return result;
326
- }
327
-
328
- case "control": {
329
- const result = router.handlePublish({
330
- from: payload.from_agent || "lead",
331
- to: payload.to_agent,
332
- topic: "lead.control",
333
- payload: {
334
- command: payload.command,
335
- reason: payload.reason || "",
336
- ...(payload.payload || {}),
337
- issued_at: Date.now(),
338
- },
339
- priority: 8,
340
- ttl_ms: Math.max(
341
- 1000,
342
- Math.min(Number(payload.ttl_ms) || 3600000, 86400000),
343
- ),
344
- trace_id: payload.trace_id,
345
- correlation_id: payload.correlation_id,
346
- });
347
- if (client) touchClient(client);
348
- return result;
349
- }
350
-
351
- case "deregister": {
352
- const agentId = resolveAgentId(client, payload);
353
- const result = router.transitionAgentStatus(
354
- agentId,
355
- "offline",
356
- payload.presence_generation,
357
- payload,
358
- );
359
- if (client) touchClient(client);
360
- return result;
361
- }
362
-
363
- case "send_input": {
364
- if (client) touchClient(client);
365
- return sendInputToConductorSession(payload.session_id, payload.text);
366
- }
367
-
368
- case "team_task_update": {
369
- const teamTaskUpdate = getTeamBridgeMethod("teamTaskUpdate");
370
- const result = teamTaskUpdate
371
- ? await teamTaskUpdate(payload)
372
- : teamTaskUpdateFallback(payload);
373
- if (client) touchClient(client);
374
- return result;
375
- }
376
-
377
- case "team_send_message": {
378
- const teamSendMessage = getTeamBridgeMethod("teamSendMessage");
379
- const result = teamSendMessage
380
- ? await teamSendMessage(payload)
381
- : teamSendMessageFallback(payload);
382
- if (client) touchClient(client);
383
- return result;
384
- }
385
-
386
- case "pipeline_advance": {
387
- if (client) touchClient(client);
388
- if (!store?.db) {
389
- return { ok: false, error: "hub_db_not_found" };
390
- }
391
- ensurePipelineTable(store.db);
392
- const pipeline = createPipeline(store.db, payload.team_name);
393
- return pipeline.advance(payload.phase);
394
- }
395
-
396
- case "pipeline_init": {
397
- if (client) touchClient(client);
398
- if (!store?.db) {
399
- return { ok: false, error: "hub_db_not_found" };
400
- }
401
- ensurePipelineTable(store.db);
402
- const state = initPipelineState(store.db, payload.team_name, {
403
- fix_max: payload.fix_max,
404
- ralph_max: payload.ralph_max,
405
- });
406
- return { ok: true, data: state };
407
- }
408
-
409
- case "hitl_request": {
410
- if (!hitlManager) {
411
- return { ok: false, error: "hitl not available" };
412
- }
413
- if (client) touchClient(client);
414
- return hitlManager.requestHumanInput(payload);
415
- }
416
-
417
- case "hitl_submit": {
418
- if (!hitlManager) {
419
- return { ok: false, error: "hitl not available" };
420
- }
421
- if (client) touchClient(client);
422
- return hitlManager.submitHumanInput(payload);
423
- }
424
-
425
- case "delegator_delegate": {
426
- if (!delegatorService) {
427
- return {
428
- ok: false,
429
- error: {
430
- code: "DELEGATOR_NOT_AVAILABLE",
431
- message: "Delegator service가 초기화되지 않았습니다",
432
- },
433
- };
434
- }
435
- if (client) touchClient(client);
436
- const result = await delegatorService.delegate(payload);
437
- return { ok: result?.ok !== false, data: result };
438
- }
439
-
440
- case "delegator_reply": {
441
- if (!delegatorService) {
442
- return {
443
- ok: false,
444
- error: {
445
- code: "DELEGATOR_NOT_AVAILABLE",
446
- message: "Delegator service가 초기화되지 않았습니다",
447
- },
448
- };
449
- }
450
- if (client) touchClient(client);
451
- const result = await delegatorService.reply(payload);
452
- return { ok: result?.ok !== false, data: result };
453
- }
454
-
455
- default:
456
- return {
457
- ok: false,
458
- error: {
459
- code: "UNKNOWN_PIPE_COMMAND",
460
- message: `지원하지 않는 command: ${action}`,
461
- },
462
- };
463
- }
464
- }
465
-
466
- function buildReplayMessages(agentId, payload = {}) {
467
- const maxMessages = Math.max(
468
- 1,
469
- Math.min(Number(payload.max_messages) || 20, 100),
470
- );
471
- const pending = router.getPendingMessages(agentId, {
472
- max_messages: maxMessages,
473
- include_topics: payload.topics,
474
- });
475
- if (!store?.getAuditMessagesForAgent) {
476
- return pending.slice(0, maxMessages);
477
- }
478
-
479
- const requestedTopics = normalizeReplayTopics(payload.topics);
480
- const auditTopics = new Set(requestedTopics);
481
- if (auditTopics.size === 0) {
482
- const persistedTopics = store?.getAgent?.(agentId)?.topics || [];
483
- for (const topic of persistedTopics) auditTopics.add(topic);
484
- if (
485
- typeof router.canReplayMessageForAgent === "function" &&
486
- router.canReplayMessageForAgent(agentId, {
487
- to_agent: "topic:cto",
488
- topic: "cto",
489
- })
490
- ) {
491
- auditTopics.add("cto");
492
- }
493
- }
494
-
495
- const audit = store.getAuditMessagesForAgent(agentId, {
496
- max_messages: maxMessages,
497
- include_topics: Array.from(auditTopics),
498
- });
499
- const byId = new Map();
500
- for (const message of [...pending, ...audit]) {
501
- if (!message?.id || byId.has(message.id)) continue;
502
- if (
503
- typeof router.canReplayMessageForAgent === "function" &&
504
- !router.canReplayMessageForAgent(agentId, message)
505
- )
506
- continue;
507
- byId.set(message.id, message);
508
- }
509
- return Array.from(byId.values())
510
- .sort((left, right) => right.created_at_ms - left.created_at_ms)
511
- .slice(0, maxMessages);
512
- }
513
-
514
- async function processQuery(client, action, payload = {}) {
515
- switch (action) {
516
- case "drain": {
517
- const agentId = resolveAgentId(client, payload);
518
- const messages = router.drainAgent(agentId, {
519
- max_messages: payload.max_messages,
520
- include_topics: payload.topics,
521
- auto_ack: payload.auto_ack,
522
- });
523
- if (client) touchClient(client);
524
- return {
525
- ok: true,
526
- data: {
527
- messages,
528
- count: messages.length,
529
- server_time_ms: Date.now(),
530
- },
531
- };
532
- }
533
-
534
- case "context": {
535
- const agentId = resolveAgentId(client, payload);
536
- const messages = buildReplayMessages(agentId, payload);
537
- if (client) touchClient(client);
538
- return {
539
- ok: true,
540
- data: {
541
- messages,
542
- count: messages.length,
543
- server_time_ms: Date.now(),
544
- },
545
- };
546
- }
547
-
548
- case "status": {
549
- const scope = payload.scope || "hub";
550
- if (client) touchClient(client);
551
- return router.getStatus(scope, payload);
552
- }
553
-
554
- case "assign_status": {
555
- if (client) touchClient(client);
556
- return router.getAssignStatus(payload);
557
- }
558
-
559
- case "team_info": {
560
- const teamInfo = getTeamBridgeMethod("teamInfo");
561
- const result = teamInfo
562
- ? await teamInfo(payload)
563
- : teamInfoFallback(payload);
564
- if (client) touchClient(client);
565
- return result;
566
- }
567
-
568
- case "team_task_list": {
569
- const teamTaskList = getTeamBridgeMethod("teamTaskList");
570
- const result = teamTaskList
571
- ? await teamTaskList(payload)
572
- : teamTaskListFallback(payload);
573
- if (client) touchClient(client);
574
- return result;
575
- }
576
-
577
- case "pipeline_state": {
578
- if (client) touchClient(client);
579
- if (!store?.db) {
580
- return { ok: false, error: "hub_db_not_found" };
581
- }
582
- ensurePipelineTable(store.db);
583
- const state = readPipelineState(store.db, payload.team_name);
584
- return state
585
- ? { ok: true, data: state }
586
- : { ok: false, error: "pipeline_not_found" };
587
- }
588
-
589
- case "pipeline_list": {
590
- if (client) touchClient(client);
591
- if (!store?.db) {
592
- return { ok: false, error: "hub_db_not_found" };
593
- }
594
- ensurePipelineTable(store.db);
595
- return { ok: true, data: listPipelineStates(store.db) };
596
- }
597
-
598
- case "hitl_pending": {
599
- if (client) touchClient(client);
600
- if (!hitlManager) {
601
- return { ok: false, error: "hitl not available" };
602
- }
603
- return { ok: true, data: hitlManager.getPendingRequests() };
604
- }
605
-
606
- case "delegator_status": {
607
- if (!delegatorService) {
608
- return {
609
- ok: false,
610
- error: {
611
- code: "DELEGATOR_NOT_AVAILABLE",
612
- message: "Delegator service가 초기화되지 않았습니다",
613
- },
614
- };
615
- }
616
- if (client) touchClient(client);
617
- const result = await delegatorService.status(payload);
618
- return { ok: result?.ok !== false, data: result };
619
- }
620
-
621
- default:
622
- return {
623
- ok: false,
624
- error: {
625
- code: "UNKNOWN_PIPE_QUERY",
626
- message: `지원하지 않는 query: ${action}`,
627
- },
628
- };
629
- }
630
- }
631
-
632
- function onMessage(agentId, message) {
633
- if (!agentId || !message) return;
634
- if (router.markMessagePushed(agentId, message.id)) {
635
- pushEvent(agentId, message);
636
- return;
637
- }
638
- pushEvent(agentId, message);
639
- }
640
-
641
- async function handleFrame(client, frame) {
642
- if (!frame || typeof frame !== "object") {
643
- return sendResponse(client, null, {
644
- ok: false,
645
- error: { code: "INVALID_FRAME", message: "JSON object frame required" },
646
- });
647
- }
648
-
649
- if (!frame.type) {
650
- return sendResponse(client, frame.request_id || null, {
651
- ok: false,
652
- error: { code: "INVALID_FRAME", message: "type required" },
653
- });
654
- }
655
-
656
- touchClient(client);
657
-
658
- try {
659
- if (frame.type === "command") {
660
- const action = frame.payload?.action || frame.payload?.command;
661
- const result = await processCommand(
662
- client,
663
- action,
664
- frame.payload || {},
665
- );
666
- return sendResponse(
667
- client,
668
- frame.payload?.request_id || frame.request_id || null,
669
- result,
670
- );
671
- }
672
- if (frame.type === "query") {
673
- const action = frame.payload?.action || frame.payload?.query;
674
- const result = await processQuery(client, action, frame.payload || {});
675
- return sendResponse(
676
- client,
677
- frame.payload?.request_id || frame.request_id || null,
678
- result,
679
- );
680
- }
681
- return sendResponse(client, frame.request_id || null, {
682
- ok: false,
683
- error: {
684
- code: "INVALID_FRAME_TYPE",
685
- message: `지원하지 않는 type: ${frame.type}`,
686
- },
687
- });
688
- } catch (error) {
689
- return sendResponse(client, frame.request_id || null, {
690
- ok: false,
691
- error: { code: "PIPE_REQUEST_FAILED", message: error.message },
692
- });
693
- }
694
- }
695
-
696
- function attachSocket(socket) {
697
- const client = {
698
- id: randomUUID(),
699
- socket,
700
- buffer: "",
701
- agentId: null,
702
- subscriptions: new Set(),
703
- lastHeartbeatMs: Date.now(),
704
- closed: false,
705
- };
706
- clients.set(client.id, client);
707
-
708
- socket.setEncoding("utf8");
709
- socket.on("data", async (chunk) => {
710
- client.buffer += chunk;
711
- let newlineIndex = client.buffer.indexOf("\n");
712
- while (newlineIndex >= 0) {
713
- const line = client.buffer.slice(0, newlineIndex).trim();
714
- client.buffer = client.buffer.slice(newlineIndex + 1);
715
- if (line) {
716
- try {
717
- const frame = safeJsonParse(line);
718
- await handleFrame(client, frame);
719
- } catch (err) {
720
- pipeLog.error(
721
- { clientId: client.id, err: String(err?.message || err) },
722
- "pipe.frame_handler_error",
723
- );
724
- }
725
- }
726
- newlineIndex = client.buffer.indexOf("\n");
727
- }
728
- });
729
-
730
- socket.on("close", () => closeClient(client));
731
- socket.on("error", () => closeClient(client));
732
- }
733
-
734
- function startHeartbeatMonitor() {
735
- heartbeatTimer = setInterval(
736
- () => {
737
- const now = Date.now();
738
- for (const client of clients.values()) {
739
- if (now - client.lastHeartbeatMs <= heartbeatTtlMs) continue;
740
- sendFrame(client, {
741
- type: "event",
742
- event: "disconnect",
743
- payload: { reason: "heartbeat_timeout" },
744
- });
745
- closeClient(client);
746
- }
747
- },
748
- Math.max(1000, Math.floor(heartbeatTtlMs / 2)),
749
- );
750
- heartbeatTimer.unref();
751
- }
752
-
753
- return {
754
- path: pipePath,
755
-
756
- async start() {
757
- if (server) return { path: pipePath };
758
-
759
- if (!IS_WINDOWS && existsSync(pipePath)) {
760
- try {
761
- unlinkSync(pipePath);
762
- } catch {}
763
- }
764
-
765
- server = net.createServer(attachSocket);
766
- router.deliveryEmitter.on("message", onMessage);
767
-
768
- await new Promise((resolve, reject) => {
769
- server.once("error", reject);
770
- server.listen(pipePath, () => {
771
- server.off("error", reject);
772
- resolve();
773
- });
774
- });
775
-
776
- startHeartbeatMonitor();
777
- return { path: pipePath };
778
- },
779
-
780
- async stop() {
781
- if (heartbeatTimer) {
782
- clearInterval(heartbeatTimer);
783
- heartbeatTimer = null;
784
- }
785
-
786
- router.deliveryEmitter.off("message", onMessage);
787
-
788
- for (const client of clients.values()) {
789
- closeClient(client);
790
- }
791
-
792
- if (server) {
793
- const current = server;
794
- server = null;
795
- await new Promise((resolve) => current.close(resolve));
796
- }
797
-
798
- if (!IS_WINDOWS && existsSync(pipePath)) {
799
- try {
800
- unlinkSync(pipePath);
801
- } catch {}
802
- }
803
- },
804
-
805
- getStatus() {
806
- return {
807
- path: pipePath,
808
- protocol: "ndjson",
809
- clients: clients.size,
810
- pending_messages: Array.from(clients.values()).reduce((sum, client) => {
811
- if (!client.agentId) return sum;
812
- return sum + router.countPendingMessages(client.agentId);
813
- }, 0),
814
- };
815
- },
816
-
817
- async executeCommand(action, payload) {
818
- return await processCommand(null, action, payload);
819
- },
820
-
821
- async executeQuery(action, payload) {
822
- return await processQuery(null, action, payload);
823
- },
824
- };
825
- }