@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,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw CLI Executor
|
|
3
|
+
*
|
|
4
|
+
* Spawns `openclaw agent --local --json --session-id <id> --message <prompt>`
|
|
5
|
+
* and parses its output. Mirrors Multica's Go reference implementation in
|
|
6
|
+
* server/pkg/agent/openclaw.go.
|
|
7
|
+
*
|
|
8
|
+
* OpenClaw output protocol:
|
|
9
|
+
* • Streaming NDJSON events (one of stdout/stderr depending on version)
|
|
10
|
+
* - { type: "text", text }
|
|
11
|
+
* - { type: "tool_use", tool, callId, input }
|
|
12
|
+
* - { type: "tool_result", tool, callId, text }
|
|
13
|
+
* - { type: "error", text | message | error }
|
|
14
|
+
* - { type: "lifecycle", phase: "error"|"failed"|"cancelled", ... }
|
|
15
|
+
* - { type: "step_start" } / { type: "step_finish", usage }
|
|
16
|
+
* - All events may carry a sessionId field.
|
|
17
|
+
* • Legacy single-blob result (pretty-printed multi-line JSON)
|
|
18
|
+
* { payloads: [{ text }], meta: { agentMeta: { sessionId, model, usage } } }
|
|
19
|
+
*
|
|
20
|
+
* Output stream: openclaw < 2026.5.5 writes the --json result to stderr;
|
|
21
|
+
* 2026.5.5+ writes it to stdout (PR #2101). We read both streams and parse
|
|
22
|
+
* whichever carries JSON — non-JSON lines are filtered as log noise.
|
|
23
|
+
*
|
|
24
|
+
* Errors: when openclaw exits non-zero and produces no parseable text, the
|
|
25
|
+
* executor surfaces "[错误] openclaw 返回内容为空 (exit N)" inside `fullOutput`
|
|
26
|
+
* so the chat reply path renders a useful message instead of an empty bubble.
|
|
27
|
+
* Mirrors multica's "openclaw returned no parseable output" canonical error.
|
|
28
|
+
*/
|
|
29
|
+
import { runProcess } from "../process-runner.js";
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import os from "node:os";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import { emitStatus } from "../reasoning-status.js";
|
|
34
|
+
export class OpenclawExecutor {
|
|
35
|
+
agentName;
|
|
36
|
+
constructor(agentName) {
|
|
37
|
+
this.agentName = agentName;
|
|
38
|
+
}
|
|
39
|
+
async execute(prompt, workingDir, onOutput, options) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const resumeSessionId = options?.sessionId;
|
|
42
|
+
const sessionId = resumeSessionId || `a2a-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
43
|
+
// prompt 已经由 worker 用 composePrompt() 拼好,executor 不再二次包装。
|
|
44
|
+
const args = [
|
|
45
|
+
"agent",
|
|
46
|
+
"--local",
|
|
47
|
+
"--json",
|
|
48
|
+
"--session-id", sessionId,
|
|
49
|
+
];
|
|
50
|
+
if (this.agentName) {
|
|
51
|
+
args.push("--agent", this.agentName);
|
|
52
|
+
}
|
|
53
|
+
const timeoutMs = options?.timeoutMs;
|
|
54
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
55
|
+
args.push("--timeout", String(Math.max(1, Math.ceil(timeoutMs / 1000))));
|
|
56
|
+
}
|
|
57
|
+
args.push("--message", prompt);
|
|
58
|
+
const spawnEnv = { ...process.env, ...options?.env };
|
|
59
|
+
console.log(`[openclaw] Spawning openclaw agent (cwd: ${workingDir}, session: ${sessionId}, agent: ${this.agentName ?? "(default)"}, timeoutMs=${timeoutMs ?? "none"})`);
|
|
60
|
+
// runProcess owns abort + the defensive wall-clock timer:
|
|
61
|
+
// - options.signal: SIGTERM → 3s → SIGKILL (graceful)
|
|
62
|
+
// - timeoutMs + 5_000: SIGKILL (defensive; openclaw may ignore its
|
|
63
|
+
// own --timeout if a network call hangs)
|
|
64
|
+
const { proc, done: procDone } = runProcess({
|
|
65
|
+
bin: "openclaw",
|
|
66
|
+
args,
|
|
67
|
+
cwd: workingDir,
|
|
68
|
+
env: spawnEnv,
|
|
69
|
+
label: "openclaw",
|
|
70
|
+
signal: options?.signal,
|
|
71
|
+
timeoutMs: timeoutMs && timeoutMs > 0 ? timeoutMs + 5_000 : undefined,
|
|
72
|
+
});
|
|
73
|
+
let timedOut = false;
|
|
74
|
+
// Detect whether the wall-clock fired so the close handler can attribute
|
|
75
|
+
// the failure correctly. runProcess doesn't expose "did timeout" — we
|
|
76
|
+
// infer it from the exit signal: SIGKILL with no user abort = timeout.
|
|
77
|
+
let killedByUser = false;
|
|
78
|
+
if (options?.signal) {
|
|
79
|
+
if (options.signal.aborted)
|
|
80
|
+
killedByUser = true;
|
|
81
|
+
else
|
|
82
|
+
options.signal.addEventListener("abort", () => { killedByUser = true; }, { once: true });
|
|
83
|
+
}
|
|
84
|
+
let fullOutput = "";
|
|
85
|
+
let emittedSessionId = "";
|
|
86
|
+
let failed = false;
|
|
87
|
+
let gotEvents = false;
|
|
88
|
+
let capturedUsage;
|
|
89
|
+
let capturedModel;
|
|
90
|
+
// Per-stream line buffers. openclaw 2026.5.5+ writes its result blob
|
|
91
|
+
// to stdout; older builds write to stderr. The rest of each stream is
|
|
92
|
+
// plugin-init / heartbeat log noise (non-JSON). We MUST keep them
|
|
93
|
+
// separate — the close-handler blob fallback joins all lines in a
|
|
94
|
+
// single buffer to reassemble the pretty-printed JSON, and the
|
|
95
|
+
// 4000+ stderr log lines would otherwise trail the JSON and break
|
|
96
|
+
// JSON.parse.
|
|
97
|
+
const rawStdoutLines = [];
|
|
98
|
+
const rawStderrLines = [];
|
|
99
|
+
let stdoutBuffer = "";
|
|
100
|
+
let stderrBuffer = "";
|
|
101
|
+
let done = false;
|
|
102
|
+
function handleEvent(event) {
|
|
103
|
+
if (!event.type)
|
|
104
|
+
return false;
|
|
105
|
+
gotEvents = true;
|
|
106
|
+
if (event.sessionId)
|
|
107
|
+
emittedSessionId = event.sessionId;
|
|
108
|
+
switch (event.type) {
|
|
109
|
+
case "text":
|
|
110
|
+
if (event.text) {
|
|
111
|
+
fullOutput += event.text;
|
|
112
|
+
onOutput(event.text);
|
|
113
|
+
emitStatus(onOutput, "Working");
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
case "tool_use":
|
|
117
|
+
onOutput(`[tool:exec]${JSON.stringify(event.input ?? {})}[/tool:exec]\n`);
|
|
118
|
+
emitStatus(onOutput, "Running");
|
|
119
|
+
return true;
|
|
120
|
+
case "tool_result":
|
|
121
|
+
if (event.text) {
|
|
122
|
+
const truncated = event.text.length > 500
|
|
123
|
+
? `${event.text.slice(0, 500)}...`
|
|
124
|
+
: event.text;
|
|
125
|
+
onOutput(`[tool-result:exec]${truncated}[/tool-result:exec]\n`);
|
|
126
|
+
}
|
|
127
|
+
emitStatus(onOutput, "Done");
|
|
128
|
+
return true;
|
|
129
|
+
case "error": {
|
|
130
|
+
const msg = extractErrorMessage(event);
|
|
131
|
+
console.error(`[openclaw] error event: ${msg}`);
|
|
132
|
+
onOutput(`[error] ${msg}\n`);
|
|
133
|
+
failed = true;
|
|
134
|
+
emitStatus(onOutput, "Failed");
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
case "lifecycle": {
|
|
138
|
+
const phase = event.phase ?? "";
|
|
139
|
+
if (phase === "error" || phase === "failed" || phase === "cancelled") {
|
|
140
|
+
const msg = extractErrorMessage(event);
|
|
141
|
+
console.error(`[openclaw] lifecycle ${phase}: ${msg}`);
|
|
142
|
+
onOutput(`[lifecycle:${phase}] ${msg}\n`);
|
|
143
|
+
failed = true;
|
|
144
|
+
emitStatus(onOutput, "Failed");
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
case "step_start":
|
|
149
|
+
emitStatus(onOutput, "Working");
|
|
150
|
+
return true;
|
|
151
|
+
case "step_finish":
|
|
152
|
+
if (event.usage) {
|
|
153
|
+
const u = event.usage;
|
|
154
|
+
capturedUsage = {
|
|
155
|
+
inputTokens: typeof u.input_tokens === "number" ? u.input_tokens : undefined,
|
|
156
|
+
outputTokens: typeof u.output_tokens === "number" ? u.output_tokens : undefined,
|
|
157
|
+
cacheReadTokens: typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : undefined,
|
|
158
|
+
cacheCreationTokens: typeof u.cache_creation_input_tokens === "number" ? u.cache_creation_input_tokens : undefined,
|
|
159
|
+
totalCostUsd: typeof u.total_cost_usd === "number" ? u.total_cost_usd : undefined,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
emitStatus(onOutput, "Answered");
|
|
163
|
+
return true;
|
|
164
|
+
default:
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function handleResultBlob(result) {
|
|
169
|
+
const payloads = result.payloads;
|
|
170
|
+
const meta = result.meta;
|
|
171
|
+
if (!payloads && !meta?.durationMs)
|
|
172
|
+
return false;
|
|
173
|
+
gotEvents = true;
|
|
174
|
+
if (Array.isArray(payloads)) {
|
|
175
|
+
for (const p of payloads) {
|
|
176
|
+
if (p?.text) {
|
|
177
|
+
fullOutput += p.text;
|
|
178
|
+
onOutput(p.text);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const agentMeta = meta?.agentMeta;
|
|
183
|
+
if (agentMeta && typeof agentMeta.sessionId === "string") {
|
|
184
|
+
emittedSessionId = agentMeta.sessionId;
|
|
185
|
+
}
|
|
186
|
+
if (agentMeta) {
|
|
187
|
+
if (typeof agentMeta.model === "string" && agentMeta.model) {
|
|
188
|
+
capturedModel = agentMeta.model;
|
|
189
|
+
}
|
|
190
|
+
const u = agentMeta.usage;
|
|
191
|
+
if (u) {
|
|
192
|
+
capturedUsage = {
|
|
193
|
+
inputTokens: typeof u.input_tokens === "number" ? u.input_tokens : undefined,
|
|
194
|
+
outputTokens: typeof u.output_tokens === "number" ? u.output_tokens : undefined,
|
|
195
|
+
cacheReadTokens: typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : undefined,
|
|
196
|
+
cacheCreationTokens: typeof u.cache_creation_input_tokens === "number" ? u.cache_creation_input_tokens : undefined,
|
|
197
|
+
totalCostUsd: typeof u.total_cost_usd === "number" ? u.total_cost_usd : undefined,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
function handleLine(line, sink) {
|
|
204
|
+
const trimmed = line.trim();
|
|
205
|
+
if (!trimmed)
|
|
206
|
+
return;
|
|
207
|
+
if (trimmed[0] !== "{") {
|
|
208
|
+
sink.push(trimmed);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let parsed;
|
|
212
|
+
try {
|
|
213
|
+
parsed = JSON.parse(trimmed);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
sink.push(trimmed);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const obj = parsed;
|
|
220
|
+
if (obj.type && handleEvent(obj))
|
|
221
|
+
return;
|
|
222
|
+
if (handleResultBlob(obj))
|
|
223
|
+
return;
|
|
224
|
+
sink.push(trimmed);
|
|
225
|
+
}
|
|
226
|
+
// Try to parse `source` (an array of per-stream non-JSON lines) as
|
|
227
|
+
// a pretty-printed result blob. Mirrors the Go fallback path:
|
|
228
|
+
// 1. join everything, try parsing
|
|
229
|
+
// 2. find the first line starting with `{`, slice from there, try parsing
|
|
230
|
+
// 3. last-resort: walk sub-windows looking for the largest parseable JSON
|
|
231
|
+
// Returns true if a result blob was consumed.
|
|
232
|
+
function tryParseResultBlob(source) {
|
|
233
|
+
if (source.length === 0)
|
|
234
|
+
return false;
|
|
235
|
+
const joined = source.join("\n").trim();
|
|
236
|
+
if (joined.startsWith("{")) {
|
|
237
|
+
try {
|
|
238
|
+
const parsed = JSON.parse(joined);
|
|
239
|
+
if (handleResultBlob(parsed))
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
catch { /* try slice */ }
|
|
243
|
+
}
|
|
244
|
+
for (let i = 0; i < source.length; i++) {
|
|
245
|
+
if (source[i].trimStart()[0] !== "{")
|
|
246
|
+
continue;
|
|
247
|
+
const fromFirstBrace = source.slice(i).join("\n").trim();
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(fromFirstBrace);
|
|
250
|
+
if (handleResultBlob(parsed))
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
catch { /* last-resort window scan */ }
|
|
254
|
+
// Last-resort: shrink the window from the end until it parses. The
|
|
255
|
+
// first "{ line is always the start of the blob, so this only
|
|
256
|
+
// costs O(N) and only when the simple slice strategy fails (e.g.
|
|
257
|
+
// stdout was truncated mid-write by SIGKILL).
|
|
258
|
+
for (let j = source.length; j > i; j--) {
|
|
259
|
+
const window = source.slice(i, j).join("\n").trim();
|
|
260
|
+
if (window.length < 2)
|
|
261
|
+
continue;
|
|
262
|
+
try {
|
|
263
|
+
const parsed = JSON.parse(window);
|
|
264
|
+
if (handleResultBlob(parsed))
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
catch { /* narrower window */ }
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
// Single finalize path: drains remaining buffers, runs blob fallback,
|
|
274
|
+
// applies the empty-output error placeholder, and resolves. Idempotent
|
|
275
|
+
// — both `close` and the early-resolve path route through here. `code`
|
|
276
|
+
// is null for the early-resolve case (we treat that as 0 since we
|
|
277
|
+
// already have a usable result).
|
|
278
|
+
function finalize(code, reason) {
|
|
279
|
+
if (done)
|
|
280
|
+
return;
|
|
281
|
+
done = true;
|
|
282
|
+
if (stdoutBuffer.trim())
|
|
283
|
+
handleLine(stdoutBuffer, rawStdoutLines);
|
|
284
|
+
if (stderrBuffer.trim())
|
|
285
|
+
handleLine(stderrBuffer.replace(/\x1b\[[0-9;]*m/g, ""), rawStderrLines);
|
|
286
|
+
if (!gotEvents) {
|
|
287
|
+
for (const source of [rawStdoutLines, rawStderrLines]) {
|
|
288
|
+
if (tryParseResultBlob(source))
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const finalCode = code ?? 0;
|
|
293
|
+
if (!fullOutput && (failed || finalCode !== 0)) {
|
|
294
|
+
if (timedOut) {
|
|
295
|
+
fullOutput = `[错误] openclaw 执行超时 (>${timeoutMs}ms),已强制结束`;
|
|
296
|
+
}
|
|
297
|
+
else if (reason === "error") {
|
|
298
|
+
fullOutput = `[错误] openclaw 启动失败`;
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
fullOutput = `[错误] openclaw 返回内容为空 (exit=${finalCode})`;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const reportedSessionId = resolveSessionId(resumeSessionId ?? "", emittedSessionId, failed || finalCode !== 0);
|
|
305
|
+
const exitCode = failed && finalCode === 0 ? 1 : finalCode;
|
|
306
|
+
console.log(`[openclaw] Exited code=${finalCode} reason=${reason}, output=${fullOutput.length} chars, session=${reportedSessionId}`);
|
|
307
|
+
resolve({
|
|
308
|
+
exitCode,
|
|
309
|
+
fullOutput,
|
|
310
|
+
sessionId: reportedSessionId || undefined,
|
|
311
|
+
usage: capturedUsage,
|
|
312
|
+
model: capturedModel,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
proc.stdout.on("data", (data) => {
|
|
316
|
+
stdoutBuffer += data.toString();
|
|
317
|
+
let idx;
|
|
318
|
+
while ((idx = stdoutBuffer.indexOf("\n")) !== -1) {
|
|
319
|
+
const line = stdoutBuffer.slice(0, idx);
|
|
320
|
+
stdoutBuffer = stdoutBuffer.slice(idx + 1);
|
|
321
|
+
handleLine(line, rawStdoutLines);
|
|
322
|
+
}
|
|
323
|
+
// openclaw 2026.6.x writes its result JSON in 8–10s and then hangs
|
|
324
|
+
// the process for 30+ seconds (known binary bug). If we already have
|
|
325
|
+
// a complete, parseable result blob on stdout — and we did not get it
|
|
326
|
+
// via streaming events — kill the hung process and resolve early so
|
|
327
|
+
// the user doesn't wait the full wall-clock timeout.
|
|
328
|
+
if (!done && !gotEvents && rawStdoutLines.length > 0) {
|
|
329
|
+
const tail = rawStdoutLines.join("\n").trim();
|
|
330
|
+
if (tail.startsWith("{") && tail.endsWith("}")) {
|
|
331
|
+
try {
|
|
332
|
+
const parsed = JSON.parse(tail);
|
|
333
|
+
if (parsed.payloads || parsed.meta?.durationMs) {
|
|
334
|
+
console.log(`[openclaw] Early-resolve: result blob fully written, killing hung process pid=${proc.pid}`);
|
|
335
|
+
try {
|
|
336
|
+
proc.kill("SIGTERM");
|
|
337
|
+
}
|
|
338
|
+
catch { /* noop */ }
|
|
339
|
+
setTimeout(() => { try {
|
|
340
|
+
proc.kill("SIGKILL");
|
|
341
|
+
}
|
|
342
|
+
catch { /* noop */ } }, 3_000);
|
|
343
|
+
setImmediate(() => finalize(0, "early"));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch { /* not a complete blob yet, wait for more chunks */ }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
// openclaw < 2026.5.5 writes its --json output to stderr; newer builds
|
|
352
|
+
// write to stdout. Feed stderr through the same parser — log overflow
|
|
353
|
+
// (non-JSON lines) is harmlessly collected in rawStderrLines and only
|
|
354
|
+
// used as a secondary fallback when no streaming events were parsed.
|
|
355
|
+
proc.stderr.on("data", (data) => {
|
|
356
|
+
stderrBuffer += data.toString();
|
|
357
|
+
let idx;
|
|
358
|
+
while ((idx = stderrBuffer.indexOf("\n")) !== -1) {
|
|
359
|
+
const line = stderrBuffer.slice(0, idx);
|
|
360
|
+
stderrBuffer = stderrBuffer.slice(idx + 1);
|
|
361
|
+
// Strip ANSI color codes so JSON detection isn't fooled by escapes.
|
|
362
|
+
handleLine(line.replace(/\x1b\[[0-9;]*m/g, ""), rawStderrLines);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
// runProcess owns close + error handling; resolve done() gives us
|
|
366
|
+
// `{ exitCode, signal }`. A SIGKILL with no user abort = our wall-clock
|
|
367
|
+
// timer fired; attribute the failure accordingly.
|
|
368
|
+
procDone.then(({ exitCode, signal }) => {
|
|
369
|
+
if (signal === "SIGKILL" && !killedByUser && timeoutMs && timeoutMs > 0) {
|
|
370
|
+
timedOut = true;
|
|
371
|
+
console.warn(`[openclaw] Wall-clock timeout (${timeoutMs}ms + 5_000ms grace) reached, SIGKILL pid=${proc.pid}`);
|
|
372
|
+
}
|
|
373
|
+
finalize(exitCode, "close");
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Read the tail of openclaw's session transcript. openclaw stores per-agent
|
|
379
|
+
* transcripts at
|
|
380
|
+
* `~/.openclaw/agents/<agentName>/sessions/<sessionId>.jsonl`
|
|
381
|
+
* (each file is NDJSON; first record is `{type:"session", id:<sessionId>, …}`).
|
|
382
|
+
*
|
|
383
|
+
* The executor is constructed without an agentName (rotom's a2a flow uses
|
|
384
|
+
* the default agent), so we can't pin the path — we glob across every
|
|
385
|
+
* agent's sessions directory for `<sessionId>.jsonl` and pick the first hit.
|
|
386
|
+
*
|
|
387
|
+
* Tolerant of missing files — returns empty content + an explanatory `error`
|
|
388
|
+
* so the dashboard can distinguish "file gone" from "session started but
|
|
389
|
+
* no output yet".
|
|
390
|
+
*/
|
|
391
|
+
async readSessionContent(args) {
|
|
392
|
+
const file = findOpenclawSessionFile(args.sessionId, this.agentName);
|
|
393
|
+
if (!file) {
|
|
394
|
+
return {
|
|
395
|
+
format: "jsonl",
|
|
396
|
+
content: "",
|
|
397
|
+
error: "openclaw session 文件不存在(可能已被 openclaw 清理,或 agent 名称不匹配)",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const text = fs.readFileSync(file, "utf-8");
|
|
401
|
+
const lines = text.split("\n");
|
|
402
|
+
const tail = args.tailLines ?? 200;
|
|
403
|
+
const sliced = lines.length > tail ? lines.slice(-tail).join("\n") : text;
|
|
404
|
+
return { format: "jsonl", content: sliced };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// ── Openclaw session-file lookup ────────────────────────────────────────
|
|
408
|
+
/**
|
|
409
|
+
* Resolve `~/.openclaw/agents/[<agentName>/]sessions/<sessionId>.jsonl`.
|
|
410
|
+
*
|
|
411
|
+
* When `agentName` is known we look in just that agent's directory; the
|
|
412
|
+
* executor's instance field carries it when constructed with one. The
|
|
413
|
+
* rotom-side a2a flow instantiates without a name (default agent), so we
|
|
414
|
+
* fall back to scanning every agent's sessions directory for `<id>.jsonl`
|
|
415
|
+
* and return the first match.
|
|
416
|
+
*/
|
|
417
|
+
function findOpenclawSessionFile(sessionId, agentName) {
|
|
418
|
+
const target = `${sessionId}.jsonl`;
|
|
419
|
+
if (agentName) {
|
|
420
|
+
const pinned = path.join(os.homedir(), ".openclaw", "agents", agentName, "sessions", target);
|
|
421
|
+
if (fs.existsSync(pinned))
|
|
422
|
+
return pinned;
|
|
423
|
+
}
|
|
424
|
+
const agentsRoot = path.join(os.homedir(), ".openclaw", "agents");
|
|
425
|
+
if (!fs.existsSync(agentsRoot))
|
|
426
|
+
return null;
|
|
427
|
+
let agents;
|
|
428
|
+
try {
|
|
429
|
+
agents = fs.readdirSync(agentsRoot);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
for (const a of agents) {
|
|
435
|
+
const candidate = path.join(agentsRoot, a, "sessions", target);
|
|
436
|
+
if (fs.existsSync(candidate))
|
|
437
|
+
return candidate;
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
function extractErrorMessage(event) {
|
|
442
|
+
if (event.error) {
|
|
443
|
+
const e = event.error;
|
|
444
|
+
if (e.data?.message)
|
|
445
|
+
return e.data.message;
|
|
446
|
+
if (e.message)
|
|
447
|
+
return e.message;
|
|
448
|
+
if (e.name)
|
|
449
|
+
return e.name;
|
|
450
|
+
}
|
|
451
|
+
if (event.text)
|
|
452
|
+
return event.text;
|
|
453
|
+
if (event.message)
|
|
454
|
+
return event.message;
|
|
455
|
+
return "unknown openclaw error";
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Decide which session id to report. When resume was requested but openclaw
|
|
459
|
+
* emitted a fresh, different session id AND the run failed, the resume did
|
|
460
|
+
* not land — return "" so the caller can retry fresh.
|
|
461
|
+
*/
|
|
462
|
+
function resolveSessionId(requestedResume, emitted, failed) {
|
|
463
|
+
if (failed && requestedResume && emitted && emitted !== requestedResume) {
|
|
464
|
+
return "";
|
|
465
|
+
}
|
|
466
|
+
return emitted;
|
|
467
|
+
}
|