@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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hooks/mcp-config-watcher.mjs — PostToolUse:Edit|Write 훅
|
|
3
|
+
//
|
|
4
|
+
// 감시 대상 MCP 설정 파일 변경을 감지해 stdio 서버를 즉시 차단/치환한다.
|
|
5
|
+
// 경로가 watched_paths와 매칭되지 않으면 바로 종료해 일반 편집 성능에 영향이 없도록 한다.
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import {
|
|
9
|
+
isWatchedPath,
|
|
10
|
+
loadRegistry,
|
|
11
|
+
remediate,
|
|
12
|
+
scanForStdioServers,
|
|
13
|
+
} from "../scripts/lib/mcp-guard-engine.mjs";
|
|
14
|
+
|
|
15
|
+
function readStdin() {
|
|
16
|
+
try {
|
|
17
|
+
return readFileSync(0, "utf8");
|
|
18
|
+
} catch {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildSystemMessage(filePath, stdioServers, result) {
|
|
24
|
+
const lines = [`[mcp-guard] 감시 대상 MCP 설정 변경 감지: ${filePath}`];
|
|
25
|
+
|
|
26
|
+
if (result.modified) {
|
|
27
|
+
const actionLabel = result.replacement ? "자동 치환" : "자동 제거";
|
|
28
|
+
lines.push(`[mcp-guard] stdio MCP ${actionLabel}: ${stdioServers.map((server) => server.name).join(", ")}`);
|
|
29
|
+
|
|
30
|
+
if (result.replacement?.name && result.replacement?.url) {
|
|
31
|
+
lines.push(`[mcp-guard] 대체 서버: ${result.replacement.name} -> ${result.replacement.url}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (result.backupPath) {
|
|
35
|
+
lines.push(`[mcp-guard] 백업: ${result.backupPath}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const warning of result.warnings || []) {
|
|
40
|
+
lines.push(warning);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return lines.length > 0 ? lines.join("\n") : "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function main() {
|
|
47
|
+
const raw = readStdin();
|
|
48
|
+
if (!raw.trim()) process.exit(0);
|
|
49
|
+
|
|
50
|
+
let input;
|
|
51
|
+
try {
|
|
52
|
+
input = JSON.parse(raw);
|
|
53
|
+
} catch {
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const toolName = input.tool_name || "";
|
|
58
|
+
if (toolName !== "Edit" && toolName !== "Write") process.exit(0);
|
|
59
|
+
|
|
60
|
+
const filePath = input.tool_input?.file_path || "";
|
|
61
|
+
if (!filePath || !isWatchedPath(filePath)) process.exit(0);
|
|
62
|
+
|
|
63
|
+
let registry;
|
|
64
|
+
try {
|
|
65
|
+
registry = loadRegistry();
|
|
66
|
+
} catch {
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const stdioServers = scanForStdioServers(filePath);
|
|
71
|
+
if (stdioServers.length === 0) process.exit(0);
|
|
72
|
+
|
|
73
|
+
const result = remediate(filePath, stdioServers, registry.policies);
|
|
74
|
+
const systemMessage = buildSystemMessage(filePath, stdioServers, result);
|
|
75
|
+
|
|
76
|
+
if (systemMessage) {
|
|
77
|
+
process.stdout.write(JSON.stringify({ systemMessage }));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
main();
|
|
83
|
+
} catch {
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hooks/pipeline-stop.mjs — Stop 훅: 활성 파이프라인 감지 시 구조화 decision 반환
|
|
3
|
+
//
|
|
4
|
+
// Claude Code Stop 이벤트에서 실행.
|
|
5
|
+
// 비터미널 단계의 파이프라인이 있으면 decision:"block" + reason으로 중단을 방지한다.
|
|
6
|
+
// 파이프라인이 없으면 정상 종료를 허용한다.
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
|
|
10
|
+
let getPipelineStateDbPath;
|
|
11
|
+
let ensurePipelineTable;
|
|
12
|
+
let listPipelineStates;
|
|
13
|
+
try {
|
|
14
|
+
({
|
|
15
|
+
getPipelineStateDbPath,
|
|
16
|
+
ensurePipelineTable,
|
|
17
|
+
listPipelineStates,
|
|
18
|
+
} = await import("../hub/pipeline/state.mjs"));
|
|
19
|
+
} catch {
|
|
20
|
+
// hub/pipeline 모듈 없으면 훅 무동작
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PROJECT_ROOT = process.env.CLAUDE_CWD || process.cwd();
|
|
25
|
+
const HUB_DB_PATH = getPipelineStateDbPath(PROJECT_ROOT);
|
|
26
|
+
const TERMINAL = new Set(["complete", "failed"]);
|
|
27
|
+
|
|
28
|
+
async function checkActivePipelines() {
|
|
29
|
+
if (!existsSync(HUB_DB_PATH)) return [];
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const { default: Database } = await import("better-sqlite3");
|
|
33
|
+
|
|
34
|
+
const db = new Database(HUB_DB_PATH, { readonly: true });
|
|
35
|
+
ensurePipelineTable(db);
|
|
36
|
+
const states = listPipelineStates(db);
|
|
37
|
+
db.close();
|
|
38
|
+
|
|
39
|
+
return states.filter((s) => !TERMINAL.has(s.phase));
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const active = await checkActivePipelines();
|
|
47
|
+
|
|
48
|
+
if (active.length === 0) {
|
|
49
|
+
// 활성 파이프라인 없음 → 정상 종료 허용
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 활성 파이프라인 발견 → 구조화 decision으로 block
|
|
54
|
+
const lines = active.map(
|
|
55
|
+
(s) =>
|
|
56
|
+
` - 팀 ${s.team_name}: ${s.phase} 단계 (fix: ${s.fix_attempt}/${s.fix_max}, ralph: ${s.ralph_iteration}/${s.ralph_max})`
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const reason =
|
|
60
|
+
`[tfx-multi 파이프라인 진행 중]\n` +
|
|
61
|
+
`활성 파이프라인 ${active.length}개가 아직 완료되지 않았습니다:\n` +
|
|
62
|
+
`${lines.join("\n")}\n\n` +
|
|
63
|
+
`파이프라인을 이어서 진행하려면 /tfx-multi status 로 상태를 확인하세요.\n` +
|
|
64
|
+
`강제 종료하려면 /tfx-multi cancel 을 먼저 실행하세요.`;
|
|
65
|
+
|
|
66
|
+
// 구조화된 Stop hook 출력: decision + reason
|
|
67
|
+
const output = {
|
|
68
|
+
decision: "block",
|
|
69
|
+
reason,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
process.stdout.write(JSON.stringify(output));
|
|
73
|
+
} catch {
|
|
74
|
+
// 훅 실패 시 종료 허용
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hooks/safety-guard.mjs — PreToolUse:Bash 훅
|
|
3
|
+
//
|
|
4
|
+
// 위험한 Bash 명령을 사전 차단(exit 2)하거나 경고(additionalContext)한다.
|
|
5
|
+
// hooks.json에서 `if: "Bash(*)"` 필터와 함께 사용.
|
|
6
|
+
//
|
|
7
|
+
// 차단 레벨:
|
|
8
|
+
// BLOCK (exit 2) — 복구 불가능한 파괴적 명령
|
|
9
|
+
// WARN (allow + context) — 주의가 필요한 명령
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
// ── 차단 규칙 ──────────────────────────────────────────────
|
|
14
|
+
const BLOCK_RULES = [
|
|
15
|
+
{ pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!tmp\b)(?!\S*node_modules)/i, reason: "루트/홈 디렉토리 rm -rf 차단" },
|
|
16
|
+
{ pattern: /\brm\s+(-[^\s]*)?-rf?\s+\.\s*$/i, reason: "현재 디렉토리 rm -rf . 차단" },
|
|
17
|
+
{ pattern: /\bgit\s+push\s+.*--force\s+.*\b(main|master)\b/i, reason: "main/master force push 차단" },
|
|
18
|
+
{ pattern: /\bgit\s+push\s+--force\s*$/i, reason: "대상 미지정 force push 차단" },
|
|
19
|
+
{ pattern: /\bgit\s+reset\s+--hard\s+origin\//i, reason: "remote reset --hard 차단 — 로컬 작업 소실 위험" },
|
|
20
|
+
{ pattern: /\bdrop\s+(table|database|schema)\b/i, reason: "SQL DROP 차단" },
|
|
21
|
+
{ pattern: /\btruncate\s+table\b/i, reason: "SQL TRUNCATE 차단" },
|
|
22
|
+
{ pattern: /\bformat\s+[a-z]:/i, reason: "디스크 포맷 차단" },
|
|
23
|
+
{ pattern: /\b(del|rmdir)\s+\/[sq]\b/i, reason: "Windows 재귀 삭제 차단" },
|
|
24
|
+
{ pattern: /\bgit\s+clean\s+.*-fd/i, reason: "git clean -fd 차단 — 추적되지 않은 파일 소실 위험" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// ── 경고 규칙 ──────────────────────────────────────────────
|
|
28
|
+
const WARN_RULES = [
|
|
29
|
+
{ pattern: /\bgit\s+push\b(?!.*--force)/i, warn: "git push 감지. 원격 저장소에 반영됩니다." },
|
|
30
|
+
{ pattern: /\bgit\s+rebase\b/i, warn: "git rebase 감지. 커밋 히스토리가 변경됩니다." },
|
|
31
|
+
{ pattern: /\bgit\s+branch\s+-[dD]\b/i, warn: "브랜치 삭제 감지." },
|
|
32
|
+
{ pattern: /\bnpm\s+publish\b/i, warn: "npm publish 감지. 공개 레지스트리에 배포됩니다." },
|
|
33
|
+
{ pattern: /\brm\s+(-[^\s]*)?-rf?\s/i, warn: "재귀 삭제 감지. 대상을 확인하세요." },
|
|
34
|
+
{ pattern: /--no-verify\b/i, warn: "--no-verify 감지. 훅 건너뛰기는 권장하지 않습니다." },
|
|
35
|
+
{ pattern: /\bchmod\s+777\b/i, warn: "chmod 777 감지. 보안 위험." },
|
|
36
|
+
{ pattern: /\bcurl\s.*\|\s*(bash|sh)\b/i, warn: "curl | sh 감지. 원격 스크립트 실행 주의." },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function readStdin() {
|
|
40
|
+
try {
|
|
41
|
+
return readFileSync(0, "utf8");
|
|
42
|
+
} catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function main() {
|
|
48
|
+
const raw = readStdin();
|
|
49
|
+
if (!raw.trim()) process.exit(0);
|
|
50
|
+
|
|
51
|
+
let input;
|
|
52
|
+
try {
|
|
53
|
+
input = JSON.parse(raw);
|
|
54
|
+
} catch {
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (input.tool_name !== "Bash") process.exit(0);
|
|
59
|
+
|
|
60
|
+
const command = (input.tool_input?.command || "").trim();
|
|
61
|
+
if (!command) process.exit(0);
|
|
62
|
+
|
|
63
|
+
// 1. BLOCK 체크 — exit 2로 차단
|
|
64
|
+
for (const rule of BLOCK_RULES) {
|
|
65
|
+
if (rule.pattern.test(command)) {
|
|
66
|
+
process.stderr.write(
|
|
67
|
+
`[triflux safety-guard] BLOCKED: ${rule.reason}\n` +
|
|
68
|
+
`명령어: ${command.slice(0, 120)}${command.length > 120 ? "..." : ""}\n` +
|
|
69
|
+
`이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요.`
|
|
70
|
+
);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2. WARN 체크 — allow + additionalContext
|
|
76
|
+
const warnings = [];
|
|
77
|
+
for (const rule of WARN_RULES) {
|
|
78
|
+
if (rule.pattern.test(command)) {
|
|
79
|
+
warnings.push(rule.warn);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (warnings.length > 0) {
|
|
84
|
+
const output = {
|
|
85
|
+
hookSpecificOutput: {
|
|
86
|
+
hookEventName: "PreToolUse",
|
|
87
|
+
permissionDecision: "allow",
|
|
88
|
+
additionalContext:
|
|
89
|
+
`[safety-guard] ⚠ ${warnings.join(" | ")}\n` +
|
|
90
|
+
`명령어: ${command.slice(0, 200)}`,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
process.stdout.write(JSON.stringify(output));
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. 안전한 명령 → 통과
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
main();
|
|
103
|
+
} catch {
|
|
104
|
+
// 훅 실패 시 블로킹하지 않음
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hooks/subagent-verifier.mjs — SubagentStop 훅
|
|
3
|
+
//
|
|
4
|
+
// 서브에이전트 완료 시 결과 품질을 체크한다:
|
|
5
|
+
// - 빈 결과 감지 → 재시도 제안
|
|
6
|
+
// - 에러 종료 감지 → 원인 분석 컨텍스트 주입
|
|
7
|
+
// - 과도한 토큰 사용 감지 → 효율성 알림
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
function readStdin() {
|
|
12
|
+
try {
|
|
13
|
+
return readFileSync(0, "utf8");
|
|
14
|
+
} catch {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function main() {
|
|
20
|
+
const raw = readStdin();
|
|
21
|
+
if (!raw.trim()) process.exit(0);
|
|
22
|
+
|
|
23
|
+
let input;
|
|
24
|
+
try {
|
|
25
|
+
input = JSON.parse(raw);
|
|
26
|
+
} catch {
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const agentType = input.agent_type || input.subagent_type || "unknown";
|
|
31
|
+
const result = input.tool_output || input.result || "";
|
|
32
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
33
|
+
|
|
34
|
+
const issues = [];
|
|
35
|
+
|
|
36
|
+
// 1. 빈 결과 체크
|
|
37
|
+
if (!resultStr.trim() || resultStr.trim().length < 20) {
|
|
38
|
+
issues.push(
|
|
39
|
+
`서브에이전트(${agentType})가 거의 빈 결과를 반환했습니다. ` +
|
|
40
|
+
"프롬프트를 더 구체적으로 작성하거나, 다른 subagent_type을 시도하세요."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. 에러 키워드 감지
|
|
45
|
+
const errorPatterns = [
|
|
46
|
+
/error:|exception:|traceback|failed to|fatal:/i,
|
|
47
|
+
/❌|FAILED|ERROR/,
|
|
48
|
+
];
|
|
49
|
+
const hasError = errorPatterns.some((p) => p.test(resultStr));
|
|
50
|
+
if (hasError && resultStr.length > 50) {
|
|
51
|
+
issues.push(
|
|
52
|
+
`서브에이전트(${agentType}) 결과에 에러 신호가 감지되었습니다. ` +
|
|
53
|
+
"결과를 검토하고, 필요 시 다른 접근 방식을 사용하세요."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 3. 결과가 너무 길면 요약 필요 알림
|
|
58
|
+
if (resultStr.length > 15000) {
|
|
59
|
+
issues.push(
|
|
60
|
+
`서브에이전트(${agentType}) 결과가 ${Math.round(resultStr.length / 1000)}K 자입니다. ` +
|
|
61
|
+
"핵심만 추출하여 컨텍스트 윈도우를 절약하세요."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (issues.length === 0) process.exit(0);
|
|
66
|
+
|
|
67
|
+
const output = {
|
|
68
|
+
systemMessage:
|
|
69
|
+
`[subagent-verifier] ${agentType} 완료 — 주의사항:\n` +
|
|
70
|
+
issues.map((i) => ` → ${i}`).join("\n"),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
process.stdout.write(JSON.stringify(output));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
main();
|
|
78
|
+
} catch {
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// hub/assign-callbacks.mjs — assign job 상태 변경용 Named Pipe/Unix socket 브로드캐스터
|
|
2
|
+
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
5
|
+
import { IS_WINDOWS, pipePath } from './platform.mjs';
|
|
6
|
+
|
|
7
|
+
export function getAssignCallbackPipePath(sessionId = process.pid) {
|
|
8
|
+
return pipePath('triflux-assign-callback', sessionId);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildAssignCallbackEvent(event = {}, row = null) {
|
|
12
|
+
const source = row || event || {};
|
|
13
|
+
const updatedAtMs = Number(source.updated_at_ms);
|
|
14
|
+
const createdAtMs = Number(source.created_at_ms);
|
|
15
|
+
const timestampMs = Number.isFinite(updatedAtMs)
|
|
16
|
+
? updatedAtMs
|
|
17
|
+
: (Number.isFinite(createdAtMs) ? createdAtMs : Date.now());
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
event: 'assign_job_status',
|
|
21
|
+
job_id: source.job_id || event.job_id || null,
|
|
22
|
+
supervisor_agent: source.supervisor_agent || null,
|
|
23
|
+
worker_agent: source.worker_agent || null,
|
|
24
|
+
topic: source.topic || null,
|
|
25
|
+
task: source.task || null,
|
|
26
|
+
status: source.status || event.status || null,
|
|
27
|
+
attempt: Number.isFinite(Number(source.attempt)) ? Number(source.attempt) : null,
|
|
28
|
+
retry_count: Number.isFinite(Number(source.retry_count)) ? Number(source.retry_count) : null,
|
|
29
|
+
max_retries: Number.isFinite(Number(source.max_retries)) ? Number(source.max_retries) : null,
|
|
30
|
+
priority: Number.isFinite(Number(source.priority)) ? Number(source.priority) : null,
|
|
31
|
+
ttl_ms: Number.isFinite(Number(source.ttl_ms)) ? Number(source.ttl_ms) : null,
|
|
32
|
+
timeout_ms: Number.isFinite(Number(source.timeout_ms)) ? Number(source.timeout_ms) : null,
|
|
33
|
+
deadline_ms: Number.isFinite(Number(source.deadline_ms)) ? Number(source.deadline_ms) : null,
|
|
34
|
+
trace_id: source.trace_id || null,
|
|
35
|
+
correlation_id: source.correlation_id || null,
|
|
36
|
+
last_message_id: source.last_message_id || null,
|
|
37
|
+
result: Object.prototype.hasOwnProperty.call(source, 'result')
|
|
38
|
+
? source.result
|
|
39
|
+
: (Object.prototype.hasOwnProperty.call(event, 'result') ? event.result : null),
|
|
40
|
+
error: Object.prototype.hasOwnProperty.call(source, 'error')
|
|
41
|
+
? source.error
|
|
42
|
+
: (Object.prototype.hasOwnProperty.call(event, 'error') ? event.error : null),
|
|
43
|
+
created_at_ms: Number.isFinite(createdAtMs) ? createdAtMs : null,
|
|
44
|
+
updated_at_ms: Number.isFinite(updatedAtMs) ? updatedAtMs : null,
|
|
45
|
+
started_at_ms: Number.isFinite(Number(source.started_at_ms)) ? Number(source.started_at_ms) : null,
|
|
46
|
+
completed_at_ms: Number.isFinite(Number(source.completed_at_ms)) ? Number(source.completed_at_ms) : null,
|
|
47
|
+
last_retry_at_ms: Number.isFinite(Number(source.last_retry_at_ms)) ? Number(source.last_retry_at_ms) : null,
|
|
48
|
+
timestamp: new Date(timestampMs).toISOString(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createAssignCallbackServer({ store = null, sessionId = process.pid } = {}) {
|
|
53
|
+
const pipePath = getAssignCallbackPipePath(sessionId);
|
|
54
|
+
const clients = new Set();
|
|
55
|
+
let server = null;
|
|
56
|
+
let detachStoreListener = null;
|
|
57
|
+
|
|
58
|
+
function removeSocket(socket) {
|
|
59
|
+
if (!socket) return;
|
|
60
|
+
clients.delete(socket);
|
|
61
|
+
try { socket.destroy(); } catch {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function broadcast(event) {
|
|
65
|
+
const frame = `${JSON.stringify(event)}\n`;
|
|
66
|
+
for (const socket of Array.from(clients)) {
|
|
67
|
+
if (!socket.writable || socket.destroyed) {
|
|
68
|
+
removeSocket(socket);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
socket.write(frame);
|
|
73
|
+
} catch {
|
|
74
|
+
removeSocket(socket);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
path: pipePath,
|
|
81
|
+
getStatus() {
|
|
82
|
+
return {
|
|
83
|
+
path: pipePath,
|
|
84
|
+
clients: clients.size,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
async start() {
|
|
88
|
+
if (server) return { path: pipePath };
|
|
89
|
+
if (!IS_WINDOWS && existsSync(pipePath)) {
|
|
90
|
+
try { unlinkSync(pipePath); } catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
server = net.createServer((socket) => {
|
|
94
|
+
clients.add(socket);
|
|
95
|
+
socket.setEncoding('utf8');
|
|
96
|
+
socket.on('error', () => removeSocket(socket));
|
|
97
|
+
socket.on('close', () => removeSocket(socket));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
server.once('error', reject);
|
|
102
|
+
server.listen(pipePath, () => {
|
|
103
|
+
server?.off('error', reject);
|
|
104
|
+
resolve();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (store?.onAssignStatusChange && !detachStoreListener) {
|
|
109
|
+
detachStoreListener = store.onAssignStatusChange((event, row) => {
|
|
110
|
+
broadcast(buildAssignCallbackEvent(event, row));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { path: pipePath };
|
|
115
|
+
},
|
|
116
|
+
async stop() {
|
|
117
|
+
if (detachStoreListener) {
|
|
118
|
+
try { detachStoreListener(); } catch {}
|
|
119
|
+
detachStoreListener = null;
|
|
120
|
+
}
|
|
121
|
+
if (!server) return;
|
|
122
|
+
for (const socket of Array.from(clients)) {
|
|
123
|
+
removeSocket(socket);
|
|
124
|
+
}
|
|
125
|
+
await new Promise((resolve) => server.close(resolve));
|
|
126
|
+
server = null;
|
|
127
|
+
if (!IS_WINDOWS && existsSync(pipePath)) {
|
|
128
|
+
try { unlinkSync(pipePath); } catch {}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
broadcast,
|
|
132
|
+
};
|
|
133
|
+
}
|