@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.
Files changed (189) hide show
  1. package/README.md +417 -0
  2. package/bin/mesh-master.sh +439 -0
  3. package/bin/rotom +29 -0
  4. package/bin/rotom-link.sh +136 -0
  5. package/bin/rotom-send-with-status +57 -0
  6. package/bin/rotom-up.sh +428 -0
  7. package/dist/cli/ask.js +62 -0
  8. package/dist/cli/common.js +321 -0
  9. package/dist/cli/config.js +65 -0
  10. package/dist/cli/directory.js +17 -0
  11. package/dist/cli/executor.js +58 -0
  12. package/dist/cli/fed.js +91 -0
  13. package/dist/cli/group.js +273 -0
  14. package/dist/cli/identity.js +62 -0
  15. package/dist/cli/init.js +268 -0
  16. package/dist/cli/issue.js +202 -0
  17. package/dist/cli/join.js +170 -0
  18. package/dist/cli/link.js +47 -0
  19. package/dist/cli/master.js +51 -0
  20. package/dist/cli/memory.js +307 -0
  21. package/dist/cli/note.js +68 -0
  22. package/dist/cli/repo.js +77 -0
  23. package/dist/cli/rotom.js +277 -0
  24. package/dist/cli/routes.js +118 -0
  25. package/dist/cli/run.js +45 -0
  26. package/dist/cli/schedule.js +237 -0
  27. package/dist/cli/skill.js +173 -0
  28. package/dist/cli/team.js +106 -0
  29. package/dist/executor/claude-code-hook.cjs +80 -0
  30. package/dist/executor/cli-executor.js +8 -0
  31. package/dist/executor/executors/claude-code.js +780 -0
  32. package/dist/executor/executors/codex.js +719 -0
  33. package/dist/executor/executors/hermes-cli.js +855 -0
  34. package/dist/executor/executors/openclaw.js +467 -0
  35. package/dist/executor/executors/pi.js +514 -0
  36. package/dist/executor/index.js +269 -0
  37. package/dist/executor/jsonrpc-transport.js +125 -0
  38. package/dist/executor/process-runner.js +101 -0
  39. package/dist/executor/reasoning-status.js +83 -0
  40. package/dist/executor/repo-cache.js +502 -0
  41. package/dist/executor/session-store.js +188 -0
  42. package/dist/executor/worker-chat.js +257 -0
  43. package/dist/executor/worker-connection.js +89 -0
  44. package/dist/executor/worker-issue.js +264 -0
  45. package/dist/executor/worker.js +877 -0
  46. package/dist/link/pending-requests.js +72 -0
  47. package/dist/link/server.js +233 -0
  48. package/dist/link/visibility-store.js +58 -0
  49. package/dist/master/api/agents.js +333 -0
  50. package/dist/master/api/artifacts.js +271 -0
  51. package/dist/master/api/domains.js +64 -0
  52. package/dist/master/api/groups.js +635 -0
  53. package/dist/master/api/guidance-templates.js +147 -0
  54. package/dist/master/api/index.js +89 -0
  55. package/dist/master/api/issues-patrol.js +172 -0
  56. package/dist/master/api/issues.js +663 -0
  57. package/dist/master/api/links-patrol.js +168 -0
  58. package/dist/master/api/links.js +114 -0
  59. package/dist/master/api/memory.js +259 -0
  60. package/dist/master/api/messages.js +157 -0
  61. package/dist/master/api/notes.js +77 -0
  62. package/dist/master/api/schedule-patterns.js +133 -0
  63. package/dist/master/api/schedules.js +272 -0
  64. package/dist/master/api/sessions.js +158 -0
  65. package/dist/master/api/share.js +269 -0
  66. package/dist/master/api/skills.js +190 -0
  67. package/dist/master/api/teams.js +122 -0
  68. package/dist/master/api/uploads.js +245 -0
  69. package/dist/master/auth.js +134 -0
  70. package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
  71. package/dist/master/dashboard/animations/calico-error.apng +0 -0
  72. package/dist/master/dashboard/animations/calico-happy.apng +0 -0
  73. package/dist/master/dashboard/animations/calico-notification.apng +0 -0
  74. package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
  75. package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
  76. package/dist/master/dashboard/animations/calico-waking.apng +0 -0
  77. package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
  78. package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
  79. package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
  80. package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
  81. package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
  82. package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
  83. package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
  84. package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
  85. package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
  86. package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
  87. package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
  88. package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
  89. package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
  90. package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
  91. package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
  92. package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
  93. package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
  94. package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
  95. package/dist/master/dashboard/debug-auth.html +197 -0
  96. package/dist/master/dashboard/favicon.ico +0 -0
  97. package/dist/master/dashboard/index.html +20 -0
  98. package/dist/master/dashboard/rotom-avatar.png +0 -0
  99. package/dist/master/db/agent-sessions.js +60 -0
  100. package/dist/master/db/agent-visibility.js +64 -0
  101. package/dist/master/db/agents.js +119 -0
  102. package/dist/master/db/ask-bridges.js +157 -0
  103. package/dist/master/db/build-update.js +59 -0
  104. package/dist/master/db/core.js +82 -0
  105. package/dist/master/db/domains.js +80 -0
  106. package/dist/master/db/groups.js +316 -0
  107. package/dist/master/db/guidance-templates.js +58 -0
  108. package/dist/master/db/index.js +12 -0
  109. package/dist/master/db/internal.js +45 -0
  110. package/dist/master/db/issues-patrol.js +81 -0
  111. package/dist/master/db/issues.js +373 -0
  112. package/dist/master/db/links.js +221 -0
  113. package/dist/master/db/master-node.js +43 -0
  114. package/dist/master/db/memory.js +272 -0
  115. package/dist/master/db/messages.js +210 -0
  116. package/dist/master/db/notes.js +55 -0
  117. package/dist/master/db/schedule-patterns.js +56 -0
  118. package/dist/master/db/schedules.js +135 -0
  119. package/dist/master/db/skills.js +144 -0
  120. package/dist/master/db/team.js +88 -0
  121. package/dist/master/db/types.js +10 -0
  122. package/dist/master/db.js +12 -0
  123. package/dist/master/embedded.js +133 -0
  124. package/dist/master/federation/client.js +283 -0
  125. package/dist/master/federation/identity.js +133 -0
  126. package/dist/master/federation/manager.js +267 -0
  127. package/dist/master/federation/publisher.js +87 -0
  128. package/dist/master/federation/self-publisher.js +69 -0
  129. package/dist/master/federation/server.js +487 -0
  130. package/dist/master/group-paths.js +208 -0
  131. package/dist/master/offline-queue.js +38 -0
  132. package/dist/master/opc-bootstrap.js +245 -0
  133. package/dist/master/patrol-terminal.js +275 -0
  134. package/dist/master/repo-scan.js +188 -0
  135. package/dist/master/router.js +214 -0
  136. package/dist/master/scheduler-handlers.js +510 -0
  137. package/dist/master/scheduler.js +201 -0
  138. package/dist/master/server.js +203 -0
  139. package/dist/master/services/link-collector.js +82 -0
  140. package/dist/master/services/link-patrol-bootstrap.js +50 -0
  141. package/dist/master/services/memory-extract-prompt.js +34 -0
  142. package/dist/master/services/patrol-bootstrap.js +63 -0
  143. package/dist/master/share-tokens.js +56 -0
  144. package/dist/master/terminal-hub.js +300 -0
  145. package/dist/master/uploads.js +108 -0
  146. package/dist/master/util/fs.js +100 -0
  147. package/dist/master/util/paths.js +50 -0
  148. package/dist/master/util/persona.js +10 -0
  149. package/dist/master/ws-hub/connection.js +928 -0
  150. package/dist/master/ws-hub/conversation.js +290 -0
  151. package/dist/master/ws-hub/directory.js +70 -0
  152. package/dist/master/ws-hub/dispatch-enrich.js +34 -0
  153. package/dist/master/ws-hub/hub.js +136 -0
  154. package/dist/master/ws-hub/index.js +9 -0
  155. package/dist/master/ws-hub/internal.js +35 -0
  156. package/dist/master/ws-hub/routing.js +295 -0
  157. package/dist/master/ws-hub/sessions.js +130 -0
  158. package/dist/master/ws-hub.js +11 -0
  159. package/dist/shared/agent-profile.js +44 -0
  160. package/dist/shared/constants.js +55 -0
  161. package/dist/shared/dedup.js +33 -0
  162. package/dist/shared/group-context.js +62 -0
  163. package/dist/shared/json-codec.js +33 -0
  164. package/dist/shared/logger.js +136 -0
  165. package/dist/shared/mention.js +22 -0
  166. package/dist/shared/network.js +40 -0
  167. package/dist/shared/parse.js +18 -0
  168. package/dist/shared/prompt-composer.js +171 -0
  169. package/dist/shared/protocol/client-messages.js +8 -0
  170. package/dist/shared/protocol/enums.js +6 -0
  171. package/dist/shared/protocol/federation.js +62 -0
  172. package/dist/shared/protocol/guards.js +87 -0
  173. package/dist/shared/protocol/server-messages.js +8 -0
  174. package/dist/shared/protocol/types.js +8 -0
  175. package/dist/shared/protocol.js +19 -0
  176. package/dist/shared/readonly-allowlist.js +122 -0
  177. package/dist/shared/rotom-cli-prompt.js +23 -0
  178. package/dist/shared/skill-context.js +19 -0
  179. package/dist/shared/skill-md.js +43 -0
  180. package/dist/shared/slash-commands.js +50 -0
  181. package/dist/shared/time.js +80 -0
  182. package/dist/shared/title.js +46 -0
  183. package/dist/shared/url-extractor.js +99 -0
  184. package/migrations/001-schema.sql +942 -0
  185. package/package.json +68 -0
  186. package/scripts/fix-node-pty-perms.mjs +46 -0
  187. package/skill/rotom-a2a-communicate/SKILL.md +257 -0
  188. package/skill/rotom-bus-host/SKILL.md +78 -0
  189. 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
+ }