@konglx/rotom 2.21.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 +417 -0
- package/bin/mesh-master.sh +439 -0
- package/bin/rotom +29 -0
- package/bin/rotom-link.sh +136 -0
- package/bin/rotom-send-with-status +57 -0
- package/bin/rotom-up.sh +428 -0
- package/dist/cli/ask.js +62 -0
- package/dist/cli/common.js +321 -0
- package/dist/cli/config.js +65 -0
- package/dist/cli/directory.js +17 -0
- package/dist/cli/executor.js +58 -0
- package/dist/cli/fed.js +91 -0
- package/dist/cli/group.js +273 -0
- package/dist/cli/identity.js +62 -0
- package/dist/cli/init.js +268 -0
- package/dist/cli/issue.js +202 -0
- package/dist/cli/join.js +170 -0
- package/dist/cli/link.js +47 -0
- package/dist/cli/master.js +51 -0
- package/dist/cli/memory.js +307 -0
- package/dist/cli/note.js +68 -0
- package/dist/cli/repo.js +77 -0
- package/dist/cli/rotom.js +277 -0
- package/dist/cli/routes.js +118 -0
- package/dist/cli/run.js +45 -0
- package/dist/cli/schedule.js +237 -0
- package/dist/cli/skill.js +173 -0
- package/dist/cli/team.js +106 -0
- package/dist/executor/claude-code-hook.cjs +80 -0
- package/dist/executor/cli-executor.js +8 -0
- package/dist/executor/executors/claude-code.js +780 -0
- package/dist/executor/executors/codex.js +719 -0
- package/dist/executor/executors/hermes-cli.js +855 -0
- package/dist/executor/executors/openclaw.js +467 -0
- package/dist/executor/executors/pi.js +514 -0
- package/dist/executor/index.js +269 -0
- package/dist/executor/jsonrpc-transport.js +125 -0
- package/dist/executor/process-runner.js +101 -0
- package/dist/executor/reasoning-status.js +83 -0
- package/dist/executor/repo-cache.js +502 -0
- package/dist/executor/session-store.js +188 -0
- package/dist/executor/worker-chat.js +257 -0
- package/dist/executor/worker-connection.js +89 -0
- package/dist/executor/worker-issue.js +264 -0
- package/dist/executor/worker.js +877 -0
- package/dist/link/pending-requests.js +72 -0
- package/dist/link/server.js +233 -0
- package/dist/link/visibility-store.js +58 -0
- package/dist/master/api/agents.js +333 -0
- package/dist/master/api/artifacts.js +271 -0
- package/dist/master/api/domains.js +64 -0
- package/dist/master/api/groups.js +635 -0
- package/dist/master/api/guidance-templates.js +147 -0
- package/dist/master/api/index.js +89 -0
- package/dist/master/api/issues-patrol.js +172 -0
- package/dist/master/api/issues.js +663 -0
- package/dist/master/api/links-patrol.js +168 -0
- package/dist/master/api/links.js +114 -0
- package/dist/master/api/memory.js +259 -0
- package/dist/master/api/messages.js +157 -0
- package/dist/master/api/notes.js +77 -0
- package/dist/master/api/schedule-patterns.js +133 -0
- package/dist/master/api/schedules.js +272 -0
- package/dist/master/api/sessions.js +158 -0
- package/dist/master/api/share.js +269 -0
- package/dist/master/api/skills.js +190 -0
- package/dist/master/api/teams.js +122 -0
- package/dist/master/api/uploads.js +245 -0
- package/dist/master/auth.js +134 -0
- package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
- package/dist/master/dashboard/animations/calico-error.apng +0 -0
- package/dist/master/dashboard/animations/calico-happy.apng +0 -0
- package/dist/master/dashboard/animations/calico-notification.apng +0 -0
- package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
- package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
- package/dist/master/dashboard/animations/calico-waking.apng +0 -0
- package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
- package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
- package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
- package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
- package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
- package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
- package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
- package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
- package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
- package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
- package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
- package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
- package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
- package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
- package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
- package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
- package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
- package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
- package/dist/master/dashboard/debug-auth.html +197 -0
- package/dist/master/dashboard/favicon.ico +0 -0
- package/dist/master/dashboard/index.html +20 -0
- package/dist/master/dashboard/rotom-avatar.png +0 -0
- package/dist/master/db/agent-sessions.js +60 -0
- package/dist/master/db/agent-visibility.js +64 -0
- package/dist/master/db/agents.js +119 -0
- package/dist/master/db/ask-bridges.js +157 -0
- package/dist/master/db/build-update.js +59 -0
- package/dist/master/db/core.js +82 -0
- package/dist/master/db/domains.js +80 -0
- package/dist/master/db/groups.js +316 -0
- package/dist/master/db/guidance-templates.js +58 -0
- package/dist/master/db/index.js +12 -0
- package/dist/master/db/internal.js +45 -0
- package/dist/master/db/issues-patrol.js +81 -0
- package/dist/master/db/issues.js +373 -0
- package/dist/master/db/links.js +221 -0
- package/dist/master/db/master-node.js +43 -0
- package/dist/master/db/memory.js +272 -0
- package/dist/master/db/messages.js +210 -0
- package/dist/master/db/notes.js +55 -0
- package/dist/master/db/schedule-patterns.js +56 -0
- package/dist/master/db/schedules.js +135 -0
- package/dist/master/db/skills.js +144 -0
- package/dist/master/db/team.js +88 -0
- package/dist/master/db/types.js +10 -0
- package/dist/master/db.js +12 -0
- package/dist/master/embedded.js +133 -0
- package/dist/master/federation/client.js +283 -0
- package/dist/master/federation/identity.js +133 -0
- package/dist/master/federation/manager.js +267 -0
- package/dist/master/federation/publisher.js +87 -0
- package/dist/master/federation/self-publisher.js +69 -0
- package/dist/master/federation/server.js +487 -0
- package/dist/master/group-paths.js +208 -0
- package/dist/master/offline-queue.js +38 -0
- package/dist/master/opc-bootstrap.js +245 -0
- package/dist/master/patrol-terminal.js +275 -0
- package/dist/master/repo-scan.js +188 -0
- package/dist/master/router.js +214 -0
- package/dist/master/scheduler-handlers.js +510 -0
- package/dist/master/scheduler.js +201 -0
- package/dist/master/server.js +203 -0
- package/dist/master/services/link-collector.js +82 -0
- package/dist/master/services/link-patrol-bootstrap.js +50 -0
- package/dist/master/services/memory-extract-prompt.js +34 -0
- package/dist/master/services/patrol-bootstrap.js +63 -0
- package/dist/master/share-tokens.js +56 -0
- package/dist/master/terminal-hub.js +300 -0
- package/dist/master/uploads.js +108 -0
- package/dist/master/util/fs.js +100 -0
- package/dist/master/util/paths.js +50 -0
- package/dist/master/util/persona.js +10 -0
- package/dist/master/ws-hub/connection.js +928 -0
- package/dist/master/ws-hub/conversation.js +290 -0
- package/dist/master/ws-hub/directory.js +70 -0
- package/dist/master/ws-hub/dispatch-enrich.js +34 -0
- package/dist/master/ws-hub/hub.js +136 -0
- package/dist/master/ws-hub/index.js +9 -0
- package/dist/master/ws-hub/internal.js +35 -0
- package/dist/master/ws-hub/routing.js +295 -0
- package/dist/master/ws-hub/sessions.js +130 -0
- package/dist/master/ws-hub.js +11 -0
- package/dist/shared/agent-profile.js +44 -0
- package/dist/shared/constants.js +55 -0
- package/dist/shared/dedup.js +33 -0
- package/dist/shared/group-context.js +62 -0
- package/dist/shared/json-codec.js +33 -0
- package/dist/shared/logger.js +136 -0
- package/dist/shared/mention.js +22 -0
- package/dist/shared/network.js +40 -0
- package/dist/shared/parse.js +18 -0
- package/dist/shared/prompt-composer.js +171 -0
- package/dist/shared/protocol/client-messages.js +8 -0
- package/dist/shared/protocol/enums.js +6 -0
- package/dist/shared/protocol/federation.js +62 -0
- package/dist/shared/protocol/guards.js +87 -0
- package/dist/shared/protocol/server-messages.js +8 -0
- package/dist/shared/protocol/types.js +8 -0
- package/dist/shared/protocol.js +19 -0
- package/dist/shared/readonly-allowlist.js +122 -0
- package/dist/shared/rotom-cli-prompt.js +23 -0
- package/dist/shared/skill-context.js +19 -0
- package/dist/shared/skill-md.js +43 -0
- package/dist/shared/slash-commands.js +50 -0
- package/dist/shared/time.js +80 -0
- package/dist/shared/title.js +46 -0
- package/dist/shared/url-extractor.js +99 -0
- package/migrations/001-schema.sql +942 -0
- package/package.json +68 -0
- package/scripts/fix-node-pty-perms.mjs +46 -0
- package/skill/rotom-a2a-communicate/SKILL.md +257 -0
- package/skill/rotom-bus-host/SKILL.md +78 -0
- package/skill/rotom-bus-host/scripts/poll-replies.sh +148 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh Executor — Agent 服务(多 Worker 架构)
|
|
3
|
+
*
|
|
4
|
+
* 一个 executor 进程管理多个数字员工(worker),每个 worker 拥有:
|
|
5
|
+
* - 独立身份(name/token/profile)
|
|
6
|
+
* - 独立 WebSocket 连接
|
|
7
|
+
* - 独立 CLI 后端(claude/codex/openclaw/...)
|
|
8
|
+
* - 独立任务队列
|
|
9
|
+
*
|
|
10
|
+
* 配置向后兼容:单 worker 格式仍然可用。
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npx tsx src/executor/index.ts # 读 ~/.rotom/executor.config.json
|
|
14
|
+
* npx tsx src/executor/index.ts --config /path/to/config.json # 显式指定
|
|
15
|
+
*/
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { ClaudeCodeExecutor } from "./executors/claude-code.js";
|
|
21
|
+
import { CodexExecutor } from "./executors/codex.js";
|
|
22
|
+
import { HermesCliExecutor } from "./executors/hermes-cli.js";
|
|
23
|
+
import { OpenclawExecutor } from "./executors/openclaw.js";
|
|
24
|
+
import { PiExecutor } from "./executors/pi.js";
|
|
25
|
+
import { ExecutorWorker } from "./worker.js";
|
|
26
|
+
import { SessionStore } from "./session-store.js";
|
|
27
|
+
import { ensureRotomSkillMd } from "../shared/skill-md.js";
|
|
28
|
+
import { createLogger } from "../shared/logger.js";
|
|
29
|
+
const log = createLogger("mesh-executor", { stream: "stderr" });
|
|
30
|
+
const ROTOM_HOME = process.env.ROTOM_HOME || path.join(os.homedir(), ".rotom");
|
|
31
|
+
const DEFAULT_CONFIG_PATH = path.join(ROTOM_HOME, "executor.config.json");
|
|
32
|
+
const AUTO_CONFIG_PATH = path.join(ROTOM_HOME, ".auto-executor.json");
|
|
33
|
+
function loadConfig() {
|
|
34
|
+
// 优先级:CLI --config > 用户配置 executor.config.json > master 自动生成 .auto-executor.json
|
|
35
|
+
const configIdx = process.argv.indexOf("--config");
|
|
36
|
+
if (configIdx !== -1 && process.argv[configIdx + 1]) {
|
|
37
|
+
const configPath = path.resolve(process.argv[configIdx + 1]);
|
|
38
|
+
if (!fs.existsSync(configPath)) {
|
|
39
|
+
log.error(`Config not found: ${configPath}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
43
|
+
}
|
|
44
|
+
if (fs.existsSync(DEFAULT_CONFIG_PATH)) {
|
|
45
|
+
return JSON.parse(fs.readFileSync(DEFAULT_CONFIG_PATH, "utf-8"));
|
|
46
|
+
}
|
|
47
|
+
if (fs.existsSync(AUTO_CONFIG_PATH)) {
|
|
48
|
+
const auto = JSON.parse(fs.readFileSync(AUTO_CONFIG_PATH, "utf-8"));
|
|
49
|
+
log.info(`Loaded auto-config from ${AUTO_CONFIG_PATH} (OPC mode, token optional)`);
|
|
50
|
+
if (auto.scanClis) {
|
|
51
|
+
// CLI 扫描模式:为每个本机已安装的 CLI 起一个 worker。
|
|
52
|
+
const installed = detectInstalledClis();
|
|
53
|
+
if (installed.length === 0) {
|
|
54
|
+
log.error("scanClis=true but no known CLI (claude/openclaw/codex/hermes/pi) found in PATH");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
master: auto.master,
|
|
59
|
+
workers: installed.map(cli => ({
|
|
60
|
+
name: cli,
|
|
61
|
+
cliTool: cli,
|
|
62
|
+
workingDir: auto.workingDir,
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (!auto.defaultAgent) {
|
|
67
|
+
log.error(`${AUTO_CONFIG_PATH} missing defaultAgent or scanClis — invalid OPC config`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
master: auto.master,
|
|
72
|
+
name: auto.defaultAgent.name,
|
|
73
|
+
// token 可能为 null(本机信任模式),undefined 比 null 更干净
|
|
74
|
+
token: auto.token ?? undefined,
|
|
75
|
+
cliTool: auto.defaultAgent.cliTool,
|
|
76
|
+
profile: auto.defaultAgent.profile,
|
|
77
|
+
workingDir: auto.workingDir,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
log.error(`No config found. Either:\n` +
|
|
81
|
+
` 1. Run mesh-master (it auto-spawns a local executor in OPC mode), or\n` +
|
|
82
|
+
` 2. Create ${DEFAULT_CONFIG_PATH}:\n` +
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
master: "ws://localhost:28800",
|
|
85
|
+
workers: [
|
|
86
|
+
{ name: "Claude·Agent", token: "mesh_xxx", cliTool: "claude", workingDir: "/path/to/project" },
|
|
87
|
+
],
|
|
88
|
+
}, null, 2));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
// ── CLI tool detection ──────────────────────────────────────────────────
|
|
92
|
+
const CLI_PRIORITY = ["claude", "openclaw", "codex", "pi"];
|
|
93
|
+
const ALL_KNOWN_CLIS = ["claude", "openclaw", "codex", "hermes", "pi"];
|
|
94
|
+
function detectCliTool() {
|
|
95
|
+
for (const tool of CLI_PRIORITY) {
|
|
96
|
+
try {
|
|
97
|
+
execSync(`which ${tool}`, { stdio: "pipe" });
|
|
98
|
+
return tool;
|
|
99
|
+
}
|
|
100
|
+
catch { /* not found */ }
|
|
101
|
+
}
|
|
102
|
+
return "claude";
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 扫描本机已安装的 CLI,返回数组。OPC 模式下若无 executor.config.json,
|
|
106
|
+
* 为每个已安装的 CLI 自动注册一个 agent(name 默认 = CLI 名,可通过后续
|
|
107
|
+
* 配置覆盖)。这是"每台机器 = 一个真人 + 多个 CLI agent"语义的关键。
|
|
108
|
+
*/
|
|
109
|
+
function detectInstalledClis() {
|
|
110
|
+
const found = [];
|
|
111
|
+
for (const tool of ALL_KNOWN_CLIS) {
|
|
112
|
+
try {
|
|
113
|
+
execSync(`which ${tool}`, { stdio: "pipe" });
|
|
114
|
+
found.push(tool);
|
|
115
|
+
}
|
|
116
|
+
catch { /* not installed */ }
|
|
117
|
+
}
|
|
118
|
+
return found;
|
|
119
|
+
}
|
|
120
|
+
function createExecutor(tool) {
|
|
121
|
+
switch (tool) {
|
|
122
|
+
case "claude":
|
|
123
|
+
return new ClaudeCodeExecutor();
|
|
124
|
+
case "hermes":
|
|
125
|
+
return new HermesCliExecutor();
|
|
126
|
+
case "openclaw":
|
|
127
|
+
return new OpenclawExecutor();
|
|
128
|
+
case "codex":
|
|
129
|
+
return new CodexExecutor();
|
|
130
|
+
case "pi":
|
|
131
|
+
return new PiExecutor();
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`Unknown cliTool "${tool}". Valid values: claude | codex | hermes | openclaw | pi. ` +
|
|
134
|
+
`Configure one of these explicitly in executor.config.json (the previous generic passthrough has been removed — set a known tool to avoid silent misconfiguration).`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ── Normalize config to WorkerConfig[] ──────────────────────────────────
|
|
138
|
+
function normalizeWorkers(config) {
|
|
139
|
+
// 兼容 .auto-executor.json 格式(由 master ensureLocalExecutor 生成):
|
|
140
|
+
// 该格式有 defaultAgent 字段,无顶层 name/token。统一在这里转换,无论配置
|
|
141
|
+
// 是从 CLI --config、executor.config.json 还是 .auto-executor.json 读来的。
|
|
142
|
+
const maybeAuto = config;
|
|
143
|
+
// scanClis 模式:扫本机 CLI,每个一个 worker
|
|
144
|
+
if (maybeAuto.scanClis === true) {
|
|
145
|
+
const auto = maybeAuto;
|
|
146
|
+
const installed = detectInstalledClis();
|
|
147
|
+
if (installed.length === 0) {
|
|
148
|
+
log.error("scanClis=true but no known CLI (claude/openclaw/codex/hermes/pi) found in PATH");
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
master: auto.master,
|
|
153
|
+
workers: installed.map(cli => ({
|
|
154
|
+
name: cli,
|
|
155
|
+
cliTool: cli,
|
|
156
|
+
workingDir: auto.workingDir,
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (maybeAuto.defaultAgent && typeof maybeAuto.defaultAgent === "object") {
|
|
161
|
+
const auto = maybeAuto;
|
|
162
|
+
const da = auto.defaultAgent;
|
|
163
|
+
return {
|
|
164
|
+
master: auto.master,
|
|
165
|
+
workers: [{
|
|
166
|
+
name: da.name,
|
|
167
|
+
token: auto.token ?? undefined,
|
|
168
|
+
cliTool: da.cliTool,
|
|
169
|
+
profile: da.profile,
|
|
170
|
+
workingDir: auto.workingDir,
|
|
171
|
+
}],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if ("workers" in config && Array.isArray(config.workers)) {
|
|
175
|
+
return { master: config.master, workers: config.workers };
|
|
176
|
+
}
|
|
177
|
+
// Single worker backward compatibility
|
|
178
|
+
const single = config;
|
|
179
|
+
return {
|
|
180
|
+
master: single.master,
|
|
181
|
+
workers: [{
|
|
182
|
+
name: single.name,
|
|
183
|
+
token: single.token,
|
|
184
|
+
cliTool: single.cliTool,
|
|
185
|
+
profile: single.profile,
|
|
186
|
+
workingDir: single.workingDir,
|
|
187
|
+
maxConcurrent: single.maxConcurrent,
|
|
188
|
+
}],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// ── Main ────────────────────────────────────────────────────────────────
|
|
192
|
+
const config = loadConfig();
|
|
193
|
+
const { master, workers } = normalizeWorkers(config);
|
|
194
|
+
if (!master) {
|
|
195
|
+
log.error("Config requires: master");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
if (workers.length === 0) {
|
|
199
|
+
log.error("Config requires at least one worker");
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
for (const w of workers) {
|
|
203
|
+
if (!w.name) {
|
|
204
|
+
log.error(`Worker requires: name (got: ${JSON.stringify({ name: w.name })})`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
if (!w.token) {
|
|
208
|
+
// OPC 模式下 token 可空 —— 走 master 端 isLoopback 信任。
|
|
209
|
+
// 但远程连接(用户配 ws://remote)时空 token 必失败,提示一下。
|
|
210
|
+
const isLoopbackMaster = /^wss?:\/\/(127\.0\.0\.1|localhost|\[?::1\]?)(:\d+)?(\/|$)/.test(master);
|
|
211
|
+
if (isLoopbackMaster) {
|
|
212
|
+
log.info(`worker "${w.name}" has no token — relying on master loopback trust`);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
log.warn(`worker "${w.name}" has no token but master is remote (${master}) — auth will likely fail`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// workingDir 必填 + 存在性 + 可读性校验
|
|
219
|
+
// 跨机器部署时该路径必须存在于 executor 本地 FS,与 master 无关
|
|
220
|
+
if (!w.workingDir) {
|
|
221
|
+
log.error(`worker "${w.name}" missing required "workingDir" in executor.config.json — agent needs a local project dir to read. Aborting.`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
if (!fs.existsSync(w.workingDir)) {
|
|
225
|
+
log.error(`worker "${w.name}" workingDir "${w.workingDir}" does not exist on this machine. Aborting.`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
fs.accessSync(w.workingDir, fs.constants.R_OK);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
log.error(`worker "${w.name}" workingDir "${w.workingDir}" is not readable. Aborting.`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const fallbackCli = detectCliTool();
|
|
237
|
+
const workerInstances = [];
|
|
238
|
+
// 进程级共享 SessionStore —— 所有 worker 共用同一份内存 map。持久化在 master
|
|
239
|
+
// DB 的 agent_sessions 表里,worker 启动时通过 session_sync_push 从 master 拉。
|
|
240
|
+
const sharedSessions = new SessionStore();
|
|
241
|
+
// 一次性迁移:把旧的 ~/.rotom/sessions.json 灌进内存,auth_ok 后会推给 master
|
|
242
|
+
// 落 DB。文件读完即删,后续启动直接走 master DB。
|
|
243
|
+
sharedSessions.backfillFromLegacyJson(ROTOM_HOME);
|
|
244
|
+
for (const w of workers) {
|
|
245
|
+
const cliTool = w.cliTool || fallbackCli;
|
|
246
|
+
const executor = createExecutor(cliTool);
|
|
247
|
+
// 第 5 个参数 rotomHome:SessionStore 文件落点,与 per-group cwd 派生路径解耦;
|
|
248
|
+
// 第 6 个参数 sharedSessions:进程级单例,所有 worker 共用。
|
|
249
|
+
const worker = new ExecutorWorker(w, executor, master, cliTool, ROTOM_HOME, sharedSessions);
|
|
250
|
+
workerInstances.push(worker);
|
|
251
|
+
const mapCount = w.workingDirMap ? Object.keys(w.workingDirMap).length : 0;
|
|
252
|
+
log.info(`worker "${w.name}" base cwd: ${w.workingDir} (per-group: <base>/<groupId>${mapCount > 0 ? `, ${mapCount} explicit override(s)` : ""})`);
|
|
253
|
+
}
|
|
254
|
+
log.info(`Starting ${workerInstances.length} worker(s) (fallback cli: ${fallbackCli})`);
|
|
255
|
+
// 启动时把 SKILL.md 落到 ~/.rotom/。幂等(内容相同则跳过),best-effort。
|
|
256
|
+
ensureRotomSkillMd();
|
|
257
|
+
for (const worker of workerInstances) {
|
|
258
|
+
worker.start();
|
|
259
|
+
}
|
|
260
|
+
// Graceful shutdown
|
|
261
|
+
function shutdown() {
|
|
262
|
+
log.info(`Shutting down ${workerInstances.length} worker(s)...`);
|
|
263
|
+
for (const worker of workerInstances) {
|
|
264
|
+
worker.stop();
|
|
265
|
+
}
|
|
266
|
+
process.exit(0);
|
|
267
|
+
}
|
|
268
|
+
process.on("SIGINT", shutdown);
|
|
269
|
+
process.on("SIGTERM", shutdown);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 transport for executor CLI backends that speak the protocol
|
|
3
|
+
* over stdio (codex app-server, hermes-cli acp_adapter).
|
|
4
|
+
*
|
|
5
|
+
* Owns:
|
|
6
|
+
* • pending-request map (id → resolver)
|
|
7
|
+
* • line framing from stdout
|
|
8
|
+
* • dispatch of responses / notifications / server-requests
|
|
9
|
+
*
|
|
10
|
+
* Does NOT own: process lifecycle (see process-runner.ts), stdin write
|
|
11
|
+
* (we hand the caller a `send` function that wraps JSON + newline).
|
|
12
|
+
*/
|
|
13
|
+
import { createInterface } from "node:readline";
|
|
14
|
+
import { encodeJsonLine } from "../shared/json-codec.js";
|
|
15
|
+
import { createLogger } from "../shared/logger.js";
|
|
16
|
+
const log = createLogger("mesh-executor-jsonrpc", { stream: "stderr" });
|
|
17
|
+
/**
|
|
18
|
+
* Build a JSON-RPC transport bound to `stdout` for incoming frames and
|
|
19
|
+
* `stdin` (if provided) for outgoing writes.
|
|
20
|
+
*
|
|
21
|
+
* The caller still owns the underlying process — when it exits, call
|
|
22
|
+
* `rejectPending` so any in-flight requests unblock with an error instead
|
|
23
|
+
* of hanging forever.
|
|
24
|
+
*/
|
|
25
|
+
export function createJsonRpcTransport(opts) {
|
|
26
|
+
const pending = new Map();
|
|
27
|
+
let idCounter = 1;
|
|
28
|
+
const nextId = () => idCounter++;
|
|
29
|
+
const send = (msg) => {
|
|
30
|
+
if (!opts.stdin || opts.stdin.destroyed)
|
|
31
|
+
return;
|
|
32
|
+
opts.stdin.write(encodeJsonLine(msg));
|
|
33
|
+
};
|
|
34
|
+
const notify = (method, params) => {
|
|
35
|
+
send({
|
|
36
|
+
jsonrpc: "2.0",
|
|
37
|
+
method,
|
|
38
|
+
...(params !== undefined ? { params } : {}),
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
const request = (method, params) => {
|
|
42
|
+
const id = nextId();
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
pending.set(id, { resolve, reject, method });
|
|
45
|
+
send({ jsonrpc: "2.0", id, method, params });
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const respond = (id, result) => {
|
|
49
|
+
send({ jsonrpc: "2.0", id, result });
|
|
50
|
+
};
|
|
51
|
+
const respondError = (id, code, message) => {
|
|
52
|
+
send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
53
|
+
};
|
|
54
|
+
const rejectPending = (err) => {
|
|
55
|
+
for (const [id, p] of pending) {
|
|
56
|
+
p.reject(err);
|
|
57
|
+
pending.delete(id);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const handleLine = (line) => {
|
|
61
|
+
if (!line.trim())
|
|
62
|
+
return;
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(line);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
log.warn(opts.label, "Discarding malformed JSON-RPC frame:", err.message);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const hasId = "id" in parsed;
|
|
72
|
+
const hasMethod = "method" in parsed;
|
|
73
|
+
if (hasMethod && hasId) {
|
|
74
|
+
// server → client request
|
|
75
|
+
try {
|
|
76
|
+
opts.onRequest?.(String(parsed.method), parsed.params, parsed.id);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
log.error(opts.label, "onRequest handler threw for", parsed.method, ":", err.message);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (hasMethod) {
|
|
84
|
+
// server → client notification
|
|
85
|
+
try {
|
|
86
|
+
opts.onNotification?.(String(parsed.method), parsed.params);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
log.error(opts.label, "onNotification handler threw for", parsed.method, ":", err.message);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (hasId) {
|
|
94
|
+
// response (success or error)
|
|
95
|
+
const response = parsed;
|
|
96
|
+
const p = pending.get(response.id);
|
|
97
|
+
if (p) {
|
|
98
|
+
pending.delete(response.id);
|
|
99
|
+
if (response.error) {
|
|
100
|
+
p.reject(new Error(`JSON-RPC ${p.method} failed: ${response.error.message}`));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
p.resolve(response.result);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
opts.onResponse?.(response);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
log.error(opts.label, "onResponse handler threw:", err.message);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const rl = createInterface({ input: opts.stdout });
|
|
115
|
+
rl.on("line", handleLine);
|
|
116
|
+
return {
|
|
117
|
+
send,
|
|
118
|
+
notify,
|
|
119
|
+
request,
|
|
120
|
+
respond,
|
|
121
|
+
respondError,
|
|
122
|
+
rejectPending,
|
|
123
|
+
nextId,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process lifecycle helper for CLI executors.
|
|
3
|
+
*
|
|
4
|
+
* Every CLI executor (claude-code / codex / hermes-cli / openclaw) repeats
|
|
5
|
+
* the same dance:
|
|
6
|
+
* 1. spawn(bin, args, { cwd, env, stdio: ["pipe","pipe","pipe"] })
|
|
7
|
+
* 2. on AbortSignal → kill SIGTERM, then SIGKILL after 3s
|
|
8
|
+
* 3. resolve on `close`, resolve(error=1) on `error`
|
|
9
|
+
* 4. emit a Spawning/Exited log line with a label
|
|
10
|
+
*
|
|
11
|
+
* This module centralizes that pattern so the executors only carry the
|
|
12
|
+
* protocol-specific line-parsing logic on top. Executors retain ownership
|
|
13
|
+
* of `proc.stdin.write/end` and the stdout/stderr listener wiring.
|
|
14
|
+
*
|
|
15
|
+
* The return type is `ChildProcessByStdio<Writable, Readable, Readable>` so
|
|
16
|
+
* callers can attach `.on("data", ...)` to `proc.stdout` / `proc.stderr` and
|
|
17
|
+
* `proc.stdin.write(...)` without non-null assertions. We cast to the typed
|
|
18
|
+
* spawn signature internally — all 4 executors actually use
|
|
19
|
+
* `["pipe","pipe","pipe"]` (openclaw's variant is also piped), so this is
|
|
20
|
+
* safe in practice.
|
|
21
|
+
*/
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { createLogger } from "../shared/logger.js";
|
|
24
|
+
const log = createLogger("mesh-executor-process-runner", { stream: "stderr" });
|
|
25
|
+
/**
|
|
26
|
+
* Spawn a long-running CLI process and wire up the abort/timeout/close
|
|
27
|
+
* sequence shared by every executor.
|
|
28
|
+
*
|
|
29
|
+
* The returned `proc` is fully owned by the caller — they can attach
|
|
30
|
+
* stdout/stderr listeners and write to stdin. The handle just owns the
|
|
31
|
+
* lifecycle (abort + done).
|
|
32
|
+
*/
|
|
33
|
+
export function runProcess(opts) {
|
|
34
|
+
const graceMs = opts.graceMs ?? 3000;
|
|
35
|
+
const stdio = opts.stdio ?? ["pipe", "pipe", "pipe"];
|
|
36
|
+
log.info(opts.label, "Spawning", opts.bin, `(cwd: ${opts.cwd}, args: ${opts.args.join(" ")})`);
|
|
37
|
+
const proc = spawn(opts.bin, opts.args, {
|
|
38
|
+
cwd: opts.cwd,
|
|
39
|
+
env: { ...process.env, ...(opts.env ?? {}) },
|
|
40
|
+
stdio,
|
|
41
|
+
});
|
|
42
|
+
let killedByAbort = false;
|
|
43
|
+
const killHard = () => {
|
|
44
|
+
try {
|
|
45
|
+
proc.kill("SIGKILL");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* already gone */
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const killGraceful = () => {
|
|
52
|
+
if (proc.killed || proc.exitCode != null)
|
|
53
|
+
return;
|
|
54
|
+
try {
|
|
55
|
+
proc.kill("SIGTERM");
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* already gone */
|
|
59
|
+
}
|
|
60
|
+
setTimeout(killHard, graceMs).unref();
|
|
61
|
+
};
|
|
62
|
+
const abort = (reason) => {
|
|
63
|
+
if (killedByAbort)
|
|
64
|
+
return;
|
|
65
|
+
killedByAbort = true;
|
|
66
|
+
log.info(opts.label, `Aborted${reason ? `: ${reason}` : ""}, killing pid=${proc.pid}`);
|
|
67
|
+
killGraceful();
|
|
68
|
+
};
|
|
69
|
+
if (opts.signal) {
|
|
70
|
+
if (opts.signal.aborted) {
|
|
71
|
+
abort("signal already aborted");
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
opts.signal.addEventListener("abort", () => abort("signal"), { once: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
let timeoutTimer;
|
|
78
|
+
if (opts.timeoutMs) {
|
|
79
|
+
timeoutTimer = setTimeout(() => {
|
|
80
|
+
log.info(opts.label, `Timeout ${opts.timeoutMs}ms exceeded, killing pid=${proc.pid}`);
|
|
81
|
+
killHard();
|
|
82
|
+
}, opts.timeoutMs);
|
|
83
|
+
timeoutTimer.unref();
|
|
84
|
+
}
|
|
85
|
+
const done = new Promise((resolve) => {
|
|
86
|
+
proc.on("close", (code, signal) => {
|
|
87
|
+
if (timeoutTimer)
|
|
88
|
+
clearTimeout(timeoutTimer);
|
|
89
|
+
const exitCode = code ?? (killedByAbort ? 1 : 0);
|
|
90
|
+
log.info(opts.label, `Exited code=${exitCode}${signal ? ` signal=${signal}` : ""}`);
|
|
91
|
+
resolve({ exitCode, signal });
|
|
92
|
+
});
|
|
93
|
+
proc.on("error", (err) => {
|
|
94
|
+
if (timeoutTimer)
|
|
95
|
+
clearTimeout(timeoutTimer);
|
|
96
|
+
log.error(opts.label, "Spawn error:", err.message);
|
|
97
|
+
resolve({ exitCode: 1, signal: null });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return { proc, done, abort };
|
|
101
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning status helpers — port of codex-rs/tui/src/chatwidget.rs:2017-2043
|
|
3
|
+
* (`extract_first_bold`) and the surrounding `set_status_header` /
|
|
4
|
+
* `restore_reasoning_status_header` pattern from
|
|
5
|
+
* codex-rs/tui/src/chatwidget/streaming.rs:8-17.
|
|
6
|
+
*
|
|
7
|
+
* The model is prompted (implicitly via the dashboard's own prompt
|
|
8
|
+
* composition) to begin each reasoning block with a bold title, e.g.
|
|
9
|
+
* **Reviewing the failing test**
|
|
10
|
+
* We accumulate the reasoning deltas, extract the first complete `**...**`
|
|
11
|
+
* segment, and emit it as a `[status:thinking]…[/status:thinking]` tag so the
|
|
12
|
+
* dashboard can show a sticky "currently thinking about X" pill at the top of
|
|
13
|
+
* the streaming message while keeping the full reasoning collapsed in a
|
|
14
|
+
* `<details>` block.
|
|
15
|
+
*
|
|
16
|
+
* The hand-scan (no regex) is intentional: it matches codex's exact behavior,
|
|
17
|
+
* including the "first `**` we see is the start; if it never closes, stop
|
|
18
|
+
* looking and wait for more deltas" rule, and avoids backtracking on long
|
|
19
|
+
* inputs.
|
|
20
|
+
*/
|
|
21
|
+
export const STATUS_TAG_OPEN = "[status:thinking]";
|
|
22
|
+
export const STATUS_TAG_CLOSE = "[/status:thinking]";
|
|
23
|
+
/**
|
|
24
|
+
* Find the first complete `**…**` pair in `s` and return the trimmed inner
|
|
25
|
+
* text. Returns null when no complete pair has been seen yet (which is the
|
|
26
|
+
* expected outcome when called on a partial delta — call again with more
|
|
27
|
+
* deltas later).
|
|
28
|
+
*/
|
|
29
|
+
export function extractFirstBold(s) {
|
|
30
|
+
const bytes = new TextEncoder().encode(s);
|
|
31
|
+
let i = 0;
|
|
32
|
+
while (i + 1 < bytes.length) {
|
|
33
|
+
if (bytes[i] === 0x2a && bytes[i + 1] === 0x2a) {
|
|
34
|
+
// 0x2a = '*'
|
|
35
|
+
const start = i + 2;
|
|
36
|
+
let j = start;
|
|
37
|
+
while (j + 1 < bytes.length) {
|
|
38
|
+
if (bytes[j] === 0x2a && bytes[j + 1] === 0x2a) {
|
|
39
|
+
const inner = s.slice(start, j);
|
|
40
|
+
const trimmed = inner.trim();
|
|
41
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
42
|
+
}
|
|
43
|
+
j++;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* State machine that owns the running reasoning buffer and emits a
|
|
53
|
+
* `[status:thinking]…[/status:thinking]` tag every time `extractFirstBold`
|
|
54
|
+
* yields a new header. The downstream `MarkdownContent` parser collapses
|
|
55
|
+
* repeated emissions down to the latest one and shows it as a sticky pill.
|
|
56
|
+
*/
|
|
57
|
+
export function createReasoningStatusBuffer(emit) {
|
|
58
|
+
let buffer = "";
|
|
59
|
+
let lastEmitted = null;
|
|
60
|
+
return {
|
|
61
|
+
append(delta) {
|
|
62
|
+
buffer += delta;
|
|
63
|
+
const header = extractFirstBold(buffer);
|
|
64
|
+
if (header && header !== lastEmitted) {
|
|
65
|
+
lastEmitted = header;
|
|
66
|
+
emit(`${STATUS_TAG_OPEN}${header}${STATUS_TAG_CLOSE}`);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
reset() {
|
|
70
|
+
buffer = "";
|
|
71
|
+
lastEmitted = null;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Imperative setter for non-reasoning status updates ("Working", "Running",
|
|
77
|
+
* "Done", "Failed", etc.). Use this from `item/started`, `step_start`,
|
|
78
|
+
* `lifecycle`, etc. — anywhere the executor wants to overwrite the current
|
|
79
|
+
* status without going through the reasoning buffer.
|
|
80
|
+
*/
|
|
81
|
+
export function emitStatus(onOutput, text) {
|
|
82
|
+
onOutput(`${STATUS_TAG_OPEN}${text}${STATUS_TAG_CLOSE}`);
|
|
83
|
+
}
|