@lobu/worker 2.8.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 (117) hide show
  1. package/dist/core/error-handler.d.ts +7 -0
  2. package/dist/core/error-handler.d.ts.map +1 -0
  3. package/dist/core/error-handler.js +58 -0
  4. package/dist/core/error-handler.js.map +1 -0
  5. package/dist/core/project-scanner.d.ts +9 -0
  6. package/dist/core/project-scanner.d.ts.map +1 -0
  7. package/dist/core/project-scanner.js +64 -0
  8. package/dist/core/project-scanner.js.map +1 -0
  9. package/dist/core/types.d.ts +102 -0
  10. package/dist/core/types.d.ts.map +1 -0
  11. package/dist/core/types.js +8 -0
  12. package/dist/core/types.js.map +1 -0
  13. package/dist/core/url-utils.d.ts +5 -0
  14. package/dist/core/url-utils.d.ts.map +1 -0
  15. package/dist/core/url-utils.js +13 -0
  16. package/dist/core/url-utils.js.map +1 -0
  17. package/dist/core/workspace.d.ts +29 -0
  18. package/dist/core/workspace.d.ts.map +1 -0
  19. package/dist/core/workspace.js +104 -0
  20. package/dist/core/workspace.js.map +1 -0
  21. package/dist/embedded/just-bash-bootstrap.d.ts +21 -0
  22. package/dist/embedded/just-bash-bootstrap.d.ts.map +1 -0
  23. package/dist/embedded/just-bash-bootstrap.js +215 -0
  24. package/dist/embedded/just-bash-bootstrap.js.map +1 -0
  25. package/dist/gateway/gateway-integration.d.ts +57 -0
  26. package/dist/gateway/gateway-integration.d.ts.map +1 -0
  27. package/dist/gateway/gateway-integration.js +209 -0
  28. package/dist/gateway/gateway-integration.js.map +1 -0
  29. package/dist/gateway/message-batcher.d.ts +27 -0
  30. package/dist/gateway/message-batcher.d.ts.map +1 -0
  31. package/dist/gateway/message-batcher.js +102 -0
  32. package/dist/gateway/message-batcher.js.map +1 -0
  33. package/dist/gateway/sse-client.d.ts +74 -0
  34. package/dist/gateway/sse-client.d.ts.map +1 -0
  35. package/dist/gateway/sse-client.js +748 -0
  36. package/dist/gateway/sse-client.js.map +1 -0
  37. package/dist/gateway/types.d.ts +60 -0
  38. package/dist/gateway/types.d.ts.map +1 -0
  39. package/dist/gateway/types.js +6 -0
  40. package/dist/gateway/types.js.map +1 -0
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +112 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/instructions/builder.d.ts +8 -0
  46. package/dist/instructions/builder.d.ts.map +1 -0
  47. package/dist/instructions/builder.js +53 -0
  48. package/dist/instructions/builder.js.map +1 -0
  49. package/dist/instructions/providers.d.ts +13 -0
  50. package/dist/instructions/providers.d.ts.map +1 -0
  51. package/dist/instructions/providers.js +26 -0
  52. package/dist/instructions/providers.js.map +1 -0
  53. package/dist/modules/lifecycle.d.ts +18 -0
  54. package/dist/modules/lifecycle.d.ts.map +1 -0
  55. package/dist/modules/lifecycle.js +56 -0
  56. package/dist/modules/lifecycle.js.map +1 -0
  57. package/dist/openclaw/custom-tools.d.ts +17 -0
  58. package/dist/openclaw/custom-tools.d.ts.map +1 -0
  59. package/dist/openclaw/custom-tools.js +195 -0
  60. package/dist/openclaw/custom-tools.js.map +1 -0
  61. package/dist/openclaw/instructions.d.ts +15 -0
  62. package/dist/openclaw/instructions.d.ts.map +1 -0
  63. package/dist/openclaw/instructions.js +32 -0
  64. package/dist/openclaw/instructions.js.map +1 -0
  65. package/dist/openclaw/model-resolver.d.ts +30 -0
  66. package/dist/openclaw/model-resolver.d.ts.map +1 -0
  67. package/dist/openclaw/model-resolver.js +147 -0
  68. package/dist/openclaw/model-resolver.js.map +1 -0
  69. package/dist/openclaw/plugin-loader.d.ts +39 -0
  70. package/dist/openclaw/plugin-loader.d.ts.map +1 -0
  71. package/dist/openclaw/plugin-loader.js +347 -0
  72. package/dist/openclaw/plugin-loader.js.map +1 -0
  73. package/dist/openclaw/processor.d.ts +38 -0
  74. package/dist/openclaw/processor.d.ts.map +1 -0
  75. package/dist/openclaw/processor.js +182 -0
  76. package/dist/openclaw/processor.js.map +1 -0
  77. package/dist/openclaw/session-context.d.ts +44 -0
  78. package/dist/openclaw/session-context.d.ts.map +1 -0
  79. package/dist/openclaw/session-context.js +151 -0
  80. package/dist/openclaw/session-context.js.map +1 -0
  81. package/dist/openclaw/tool-policy.d.ts +23 -0
  82. package/dist/openclaw/tool-policy.d.ts.map +1 -0
  83. package/dist/openclaw/tool-policy.js +151 -0
  84. package/dist/openclaw/tool-policy.js.map +1 -0
  85. package/dist/openclaw/tools.d.ts +6 -0
  86. package/dist/openclaw/tools.d.ts.map +1 -0
  87. package/dist/openclaw/tools.js +158 -0
  88. package/dist/openclaw/tools.js.map +1 -0
  89. package/dist/openclaw/worker.d.ts +39 -0
  90. package/dist/openclaw/worker.d.ts.map +1 -0
  91. package/dist/openclaw/worker.js +1340 -0
  92. package/dist/openclaw/worker.js.map +1 -0
  93. package/dist/server.d.ts +7 -0
  94. package/dist/server.d.ts.map +1 -0
  95. package/dist/server.js +304 -0
  96. package/dist/server.js.map +1 -0
  97. package/dist/shared/audio-provider-suggestions.d.ts +13 -0
  98. package/dist/shared/audio-provider-suggestions.d.ts.map +1 -0
  99. package/dist/shared/audio-provider-suggestions.js +105 -0
  100. package/dist/shared/audio-provider-suggestions.js.map +1 -0
  101. package/dist/shared/processor-utils.d.ts +6 -0
  102. package/dist/shared/processor-utils.d.ts.map +1 -0
  103. package/dist/shared/processor-utils.js +30 -0
  104. package/dist/shared/processor-utils.js.map +1 -0
  105. package/dist/shared/provider-auth-hints.d.ts +6 -0
  106. package/dist/shared/provider-auth-hints.d.ts.map +1 -0
  107. package/dist/shared/provider-auth-hints.js +51 -0
  108. package/dist/shared/provider-auth-hints.js.map +1 -0
  109. package/dist/shared/tool-display-config.d.ts +16 -0
  110. package/dist/shared/tool-display-config.d.ts.map +1 -0
  111. package/dist/shared/tool-display-config.js +67 -0
  112. package/dist/shared/tool-display-config.js.map +1 -0
  113. package/dist/shared/tool-implementations.d.ts +55 -0
  114. package/dist/shared/tool-implementations.d.ts.map +1 -0
  115. package/dist/shared/tool-implementations.js +519 -0
  116. package/dist/shared/tool-implementations.js.map +1 -0
  117. package/package.json +55 -0
@@ -0,0 +1,748 @@
1
+ "use strict";
2
+ /**
3
+ * SSE client for receiving jobs from dispatcher
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.GatewayClient = void 0;
40
+ exports.consumePendingConfigNotifications = consumePendingConfigNotifications;
41
+ const node_child_process_1 = require("node:child_process");
42
+ const core_1 = require("@lobu/core");
43
+ const zod_1 = require("zod");
44
+ const gateway_integration_1 = require("./gateway-integration");
45
+ const message_batcher_1 = require("./message-batcher");
46
+ const logger = (0, core_1.createLogger)("sse-client");
47
+ const pendingConfigNotifications = [];
48
+ /**
49
+ * Returns and clears all pending config change notifications.
50
+ * Called by the worker before building the next prompt.
51
+ */
52
+ function consumePendingConfigNotifications() {
53
+ if (pendingConfigNotifications.length === 0)
54
+ return [];
55
+ return pendingConfigNotifications.splice(0);
56
+ }
57
+ // Zod schemas for runtime validation of SSE event data
58
+ const ConnectedEventSchema = zod_1.z.object({
59
+ deploymentName: zod_1.z.string(),
60
+ });
61
+ // PlatformMetadata has known fields plus string index signature
62
+ const PlatformMetadataSchema = zod_1.z
63
+ .object({
64
+ team_id: zod_1.z.string().optional(),
65
+ channel: zod_1.z.string().optional(),
66
+ ts: zod_1.z.string().optional(),
67
+ thread_ts: zod_1.z.string().optional(),
68
+ files: zod_1.z.array(zod_1.z.any()).optional(),
69
+ })
70
+ .and(zod_1.z.record(zod_1.z.string(), zod_1.z.union([
71
+ zod_1.z.string(),
72
+ zod_1.z.number(),
73
+ zod_1.z.boolean(),
74
+ zod_1.z.array(zod_1.z.any()),
75
+ zod_1.z.undefined(),
76
+ ])));
77
+ // AgentOptions has known fields plus arbitrary extra fields (including nested objects)
78
+ const AgentOptionsSchema = zod_1.z
79
+ .object({
80
+ runtime: zod_1.z.string().optional(),
81
+ model: zod_1.z.string().optional(),
82
+ maxTokens: zod_1.z.number().optional(),
83
+ temperature: zod_1.z.number().optional(),
84
+ allowedTools: zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional(),
85
+ disallowedTools: zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional(),
86
+ timeoutMinutes: zod_1.z.union([zod_1.z.number(), zod_1.z.string()]).optional(),
87
+ // Additional settings passed through from gateway
88
+ networkConfig: zod_1.z.any().optional(),
89
+ envVars: zod_1.z.any().optional(),
90
+ })
91
+ .passthrough();
92
+ const JobEventSchema = zod_1.z.object({
93
+ payload: zod_1.z.object({
94
+ botId: zod_1.z.string(),
95
+ userId: zod_1.z.string(),
96
+ agentId: zod_1.z.string(),
97
+ conversationId: zod_1.z.string(),
98
+ platform: zod_1.z.string(),
99
+ channelId: zod_1.z.string(),
100
+ messageId: zod_1.z.string(),
101
+ messageText: zod_1.z.string(),
102
+ platformMetadata: PlatformMetadataSchema,
103
+ agentOptions: AgentOptionsSchema,
104
+ jobId: zod_1.z.string().optional(),
105
+ teamId: zod_1.z.string().optional(), // Optional for WhatsApp (top-level) and Slack (in platformMetadata)
106
+ }),
107
+ processedIds: zod_1.z.array(zod_1.z.string()).optional(),
108
+ });
109
+ /**
110
+ * Gateway client for workers - connects to dispatcher via SSE
111
+ * Receives jobs via SSE stream, sends responses via HTTP POST
112
+ */
113
+ class GatewayClient {
114
+ dispatcherUrl;
115
+ workerToken;
116
+ userId;
117
+ deploymentName;
118
+ isRunning = false;
119
+ currentWorker = null;
120
+ abortController;
121
+ currentJobId;
122
+ currentTraceId; // Trace ID for end-to-end observability
123
+ currentTraceparent; // W3C traceparent for distributed tracing
124
+ reconnectAttempts = 0;
125
+ maxReconnectAttempts = 10;
126
+ messageBatcher;
127
+ eventErrorCount = 0;
128
+ eventErrorThreshold = 10;
129
+ httpPort;
130
+ constructor(dispatcherUrl, workerToken, userId, deploymentName, httpPort) {
131
+ this.dispatcherUrl = dispatcherUrl;
132
+ this.workerToken = workerToken;
133
+ this.userId = userId;
134
+ this.deploymentName = deploymentName;
135
+ this.httpPort = httpPort;
136
+ // Get initial traceId from environment (set by deployment)
137
+ this.currentTraceId = process.env.TRACE_ID;
138
+ this.messageBatcher = new message_batcher_1.MessageBatcher({
139
+ onBatchReady: async (messages) => {
140
+ await this.processBatchedMessages(messages);
141
+ },
142
+ });
143
+ logger.info({ traceId: this.currentTraceId, deploymentName }, "Worker connected");
144
+ }
145
+ async start() {
146
+ this.isRunning = true;
147
+ while (this.isRunning) {
148
+ try {
149
+ await this.connectAndListen();
150
+ if (!this.isRunning)
151
+ break;
152
+ await this.handleReconnect();
153
+ }
154
+ catch (error) {
155
+ if (error instanceof Error && error.name === "AbortError") {
156
+ logger.info("SSE connection aborted");
157
+ break;
158
+ }
159
+ logger.error("SSE connection error:", error);
160
+ if (!this.isRunning)
161
+ break;
162
+ await this.handleReconnect();
163
+ }
164
+ }
165
+ }
166
+ async connectAndListen() {
167
+ // Abort previous controller before creating a new one
168
+ if (this.abortController) {
169
+ this.abortController.abort();
170
+ }
171
+ const abortController = new globalThis.AbortController();
172
+ this.abortController = abortController;
173
+ const streamUrl = this.httpPort
174
+ ? `${this.dispatcherUrl}/worker/stream?httpPort=${this.httpPort}`
175
+ : `${this.dispatcherUrl}/worker/stream`;
176
+ logger.info(`Connecting to dispatcher at ${streamUrl} (attempt ${this.reconnectAttempts + 1})`);
177
+ const response = await fetch(streamUrl, {
178
+ method: "GET",
179
+ headers: {
180
+ Authorization: `Bearer ${this.workerToken}`,
181
+ Accept: "text/event-stream",
182
+ },
183
+ signal: abortController.signal,
184
+ });
185
+ if (!response.ok) {
186
+ throw new Error(`Failed to connect to dispatcher: ${response.status} ${response.statusText}`);
187
+ }
188
+ logger.info("✅ Connected to dispatcher via SSE");
189
+ this.reconnectAttempts = 0;
190
+ const reader = response.body?.getReader();
191
+ const decoder = new TextDecoder();
192
+ if (!reader) {
193
+ throw new Error("No response body");
194
+ }
195
+ let buffer = "";
196
+ logger.info("[SSE-CLIENT] 🔄 Starting SSE stream reading loop");
197
+ while (this.isRunning) {
198
+ const { done, value } = await reader.read();
199
+ if (done) {
200
+ logger.info("[SSE-CLIENT] SSE stream ended");
201
+ break;
202
+ }
203
+ const chunk = decoder.decode(value, { stream: true });
204
+ logger.debug(`[SSE-CLIENT] 📨 Received chunk: ${chunk.substring(0, 200)}`);
205
+ buffer += chunk;
206
+ const events = buffer.split("\n\n");
207
+ buffer = events.pop() || "";
208
+ logger.debug(`[SSE-CLIENT] 📊 Parsed ${events.length} events from buffer`);
209
+ for (const event of events) {
210
+ if (!event.trim())
211
+ continue;
212
+ const lines = event.split("\n");
213
+ let eventType = "message";
214
+ let eventData = "";
215
+ for (const line of lines) {
216
+ if (line.startsWith("event:")) {
217
+ eventType = line.substring(6).trim();
218
+ }
219
+ else if (line.startsWith("data:")) {
220
+ eventData = line.substring(5).trim();
221
+ }
222
+ }
223
+ if (eventData) {
224
+ logger.info(`[SSE-CLIENT] 🎯 Processing event type: ${eventType}`);
225
+ // Don't await - fire async to avoid blocking SSE reading loop
226
+ this.handleEvent(eventType, eventData).catch((error) => {
227
+ this.eventErrorCount++;
228
+ logger.error(`[SSE-CLIENT] Error handling ${eventType} event (error ${this.eventErrorCount}/${this.eventErrorThreshold}):`, error);
229
+ // Trigger cleanup if too many errors
230
+ if (this.eventErrorCount >= this.eventErrorThreshold) {
231
+ logger.error(`❌ Event error threshold reached (${this.eventErrorCount} errors). Triggering cleanup...`);
232
+ this.cleanupOnEventError(eventType, error).catch((cleanupErr) => {
233
+ logger.error("Failed to cleanup after event errors:", cleanupErr);
234
+ });
235
+ }
236
+ });
237
+ }
238
+ }
239
+ }
240
+ }
241
+ /**
242
+ * Send a quick delivery receipt to the gateway confirming job was received.
243
+ * Fire-and-forget — don't block job processing on the receipt send.
244
+ */
245
+ sendDeliveryReceipt(jobId) {
246
+ const url = `${this.dispatcherUrl}/worker/response`;
247
+ fetch(url, {
248
+ method: "POST",
249
+ headers: {
250
+ "Content-Type": "application/json",
251
+ Authorization: `Bearer ${this.workerToken}`,
252
+ },
253
+ body: JSON.stringify({ jobId, received: true }),
254
+ signal: AbortSignal.timeout(10_000),
255
+ }).catch((err) => {
256
+ logger.warn(`Failed to send delivery receipt for job ${jobId}:`, err);
257
+ });
258
+ }
259
+ /**
260
+ * Send a heartbeat ACK back to the gateway so stale cleanup is based on
261
+ * verified inbound worker activity rather than outbound SSE writes.
262
+ */
263
+ sendHeartbeatAck() {
264
+ const url = `${this.dispatcherUrl}/worker/response`;
265
+ fetch(url, {
266
+ method: "POST",
267
+ headers: {
268
+ "Content-Type": "application/json",
269
+ Authorization: `Bearer ${this.workerToken}`,
270
+ },
271
+ body: JSON.stringify({ received: true, heartbeat: true }),
272
+ signal: AbortSignal.timeout(10_000),
273
+ }).catch((err) => {
274
+ logger.warn("Failed to send heartbeat ACK:", err);
275
+ });
276
+ }
277
+ async handleReconnect() {
278
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
279
+ logger.error("Max reconnection attempts reached, giving up");
280
+ this.isRunning = false;
281
+ return;
282
+ }
283
+ this.reconnectAttempts++;
284
+ const delay = Math.min(1000 * 2 ** (this.reconnectAttempts - 1), 60000);
285
+ logger.info(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
286
+ await new Promise((resolve) => setTimeout(resolve, delay));
287
+ }
288
+ async stop() {
289
+ try {
290
+ this.isRunning = false;
291
+ if (this.abortController) {
292
+ this.abortController.abort();
293
+ }
294
+ this.messageBatcher.stop();
295
+ if (this.currentWorker) {
296
+ await this.currentWorker.cleanup();
297
+ this.currentWorker = null;
298
+ }
299
+ logger.info("✅ Gateway client stopped");
300
+ }
301
+ catch (error) {
302
+ logger.error("Error stopping gateway client:", error);
303
+ throw error;
304
+ }
305
+ }
306
+ async handleEvent(eventType, data) {
307
+ try {
308
+ if (eventType === "connected") {
309
+ const parsedData = JSON.parse(data);
310
+ const validationResult = ConnectedEventSchema.safeParse(parsedData);
311
+ if (!validationResult.success) {
312
+ logger.error("Invalid connected event data:", validationResult.error.format());
313
+ throw new Error(`Connected event validation failed: ${validationResult.error.message}`);
314
+ }
315
+ const connData = validationResult.data;
316
+ logger.info(`Connected to dispatcher for deployment ${connData.deploymentName}`);
317
+ return;
318
+ }
319
+ if (eventType === "ping") {
320
+ logger.debug("Received heartbeat ping from dispatcher");
321
+ this.sendHeartbeatAck();
322
+ return;
323
+ }
324
+ if (eventType === "config_changed") {
325
+ logger.info("Received config_changed event from gateway, invalidating session context cache");
326
+ const { invalidateSessionContextCache } = await Promise.resolve().then(() => __importStar(require("../openclaw/session-context")));
327
+ invalidateSessionContextCache();
328
+ // Parse and queue config change notifications for the next prompt
329
+ try {
330
+ const parsed = JSON.parse(data);
331
+ const changes = Array.isArray(parsed?.changes)
332
+ ? parsed.changes
333
+ : [];
334
+ if (changes.length > 0) {
335
+ pendingConfigNotifications.push(...changes);
336
+ logger.info(`Queued ${changes.length} config change notification(s)`);
337
+ }
338
+ }
339
+ catch {
340
+ // Backward compat: old gateway may send empty or invalid payload
341
+ }
342
+ return;
343
+ }
344
+ if (eventType === "job") {
345
+ try {
346
+ const parsedData = JSON.parse(data);
347
+ const validationResult = JobEventSchema.safeParse(parsedData);
348
+ if (!validationResult.success) {
349
+ logger.error("Invalid job event data:", validationResult.error.format());
350
+ logger.debug(`Raw job data: ${data}`);
351
+ throw new Error(`Job event validation failed: ${validationResult.error.message}`);
352
+ }
353
+ // Send delivery receipt immediately so the gateway knows
354
+ // the job was actually received (not lost to a stale SSE connection).
355
+ // jobId is at the top level of the SSE event (set by job-router),
356
+ // not inside the validated payload.
357
+ const jobId = parsedData.jobId;
358
+ if (jobId) {
359
+ this.sendDeliveryReceipt(jobId);
360
+ }
361
+ // Zod validates structure but passthrough allows extra fields
362
+ // The validated payload matches MessagePayload interface
363
+ await this.handleThreadMessage(validationResult.data.payload);
364
+ }
365
+ catch (parseError) {
366
+ logger.error(`Failed to parse or validate job event data:`, parseError);
367
+ logger.debug(`Raw job data: ${data}`);
368
+ }
369
+ return;
370
+ }
371
+ logger.warn(`[DEBUG] Unknown SSE event type: ${eventType}, data: ${data}`);
372
+ }
373
+ catch (error) {
374
+ logger.error(`Error handling event ${eventType}:`, error);
375
+ }
376
+ }
377
+ async handleThreadMessage(data) {
378
+ // Extract traceparent for distributed tracing
379
+ // Prefer platformMetadata.traceparent, fall back to TRACEPARENT env var
380
+ const traceparent = data.platformMetadata?.traceparent || process.env.TRACEPARENT;
381
+ this.currentTraceparent = traceparent;
382
+ // Extract traceId for logging (backwards compatible)
383
+ const traceId = (0, core_1.extractTraceId)(data) || this.currentTraceId || process.env.TRACE_ID;
384
+ this.currentTraceId = traceId;
385
+ const conversationId = data.conversationId;
386
+ if (data.jobId) {
387
+ this.currentJobId = data.jobId;
388
+ // Create child span for job received (linked to parent via traceparent)
389
+ const span = (0, core_1.createChildSpan)("job_received", traceparent, {
390
+ "lobu.job_id": data.jobId,
391
+ "lobu.message_id": data.messageId,
392
+ "lobu.conversation_id": conversationId,
393
+ "lobu.job_type": data.jobType || "message",
394
+ });
395
+ span?.setStatus({ code: core_1.SpanStatusCode.OK });
396
+ span?.end();
397
+ // Flush job_received span immediately
398
+ void (0, core_1.flushTracing)();
399
+ logger.info({
400
+ traceparent,
401
+ traceId,
402
+ jobId: data.jobId,
403
+ messageId: data.messageId,
404
+ jobType: data.jobType,
405
+ }, "Job received");
406
+ }
407
+ if (data.userId.toLowerCase() !== this.userId.toLowerCase()) {
408
+ logger.warn({ traceId, receivedUserId: data.userId, expectedUserId: this.userId }, "Received message for wrong user");
409
+ return;
410
+ }
411
+ // Check job type and dispatch accordingly
412
+ if (data.jobType === "exec") {
413
+ await this.handleExecJob(data);
414
+ return;
415
+ }
416
+ // Default: message job
417
+ const queuedMessage = {
418
+ payload: data,
419
+ timestamp: Date.now(),
420
+ };
421
+ await this.messageBatcher.addMessage(queuedMessage);
422
+ logger.info({ traceId, messageId: data.messageId, conversationId }, "Message queued for processing");
423
+ }
424
+ /**
425
+ * Handle exec job - spawn command in sandbox and stream output back
426
+ */
427
+ async handleExecJob(data) {
428
+ const { execId, execCommand, execCwd, execEnv, execTimeout } = data;
429
+ const conversationId = data.conversationId;
430
+ const traceId = this.currentTraceId;
431
+ const traceparent = this.currentTraceparent;
432
+ if (!execId || !execCommand) {
433
+ logger.error({ traceId, execId }, "Invalid exec job: missing execId or execCommand");
434
+ return;
435
+ }
436
+ logger.info({ traceId, execId, command: execCommand.substring(0, 100) }, "Executing command in sandbox");
437
+ // Create span for exec execution
438
+ const span = (0, core_1.createChildSpan)("exec_execution", traceparent, {
439
+ "lobu.exec_id": execId,
440
+ "lobu.command": execCommand.substring(0, 100),
441
+ });
442
+ // Determine working directory
443
+ const workingDir = execCwd || process.env.WORKSPACE_DIR || "/workspace";
444
+ const timeout = execTimeout || 300000; // 5 minutes default
445
+ // Create transport for sending responses back to gateway
446
+ const transport = new gateway_integration_1.HttpWorkerTransport({
447
+ gatewayUrl: this.dispatcherUrl,
448
+ workerToken: this.workerToken,
449
+ userId: data.userId,
450
+ channelId: data.channelId,
451
+ conversationId,
452
+ originalMessageTs: execId,
453
+ teamId: data.teamId || "api",
454
+ platform: data.platform,
455
+ platformMetadata: data.platformMetadata,
456
+ });
457
+ let completed = false;
458
+ try {
459
+ // Spawn the command
460
+ const proc = (0, node_child_process_1.spawn)("sh", ["-c", execCommand], {
461
+ cwd: workingDir,
462
+ env: { ...process.env, ...execEnv },
463
+ stdio: ["ignore", "pipe", "pipe"],
464
+ });
465
+ // Setup timeout
466
+ const timeoutId = setTimeout(() => {
467
+ if (!completed) {
468
+ logger.warn({ traceId, execId }, "Exec timeout reached, killing process");
469
+ proc.kill("SIGTERM");
470
+ setTimeout(() => {
471
+ if (!completed) {
472
+ proc.kill("SIGKILL");
473
+ }
474
+ }, 5000);
475
+ }
476
+ }, timeout);
477
+ // Stream stdout
478
+ proc.stdout?.on("data", (chunk) => {
479
+ const content = chunk.toString();
480
+ transport.sendExecOutput(execId, "stdout", content).catch((err) => {
481
+ logger.error({ traceId, execId, error: err }, "Failed to send stdout");
482
+ });
483
+ });
484
+ // Stream stderr
485
+ proc.stderr?.on("data", (chunk) => {
486
+ const content = chunk.toString();
487
+ transport.sendExecOutput(execId, "stderr", content).catch((err) => {
488
+ logger.error({ traceId, execId, error: err }, "Failed to send stderr");
489
+ });
490
+ });
491
+ // Wait for process to complete
492
+ const exitCode = await new Promise((resolve, reject) => {
493
+ proc.on("close", (code) => {
494
+ completed = true;
495
+ clearTimeout(timeoutId);
496
+ resolve(code ?? 0);
497
+ });
498
+ proc.on("error", (error) => {
499
+ completed = true;
500
+ clearTimeout(timeoutId);
501
+ reject(error);
502
+ });
503
+ });
504
+ // Send completion
505
+ await transport.sendExecComplete(execId, exitCode);
506
+ span?.setAttribute("lobu.exit_code", exitCode);
507
+ span?.setStatus({ code: core_1.SpanStatusCode.OK });
508
+ span?.end();
509
+ await (0, core_1.flushTracing)();
510
+ logger.info({ traceId, execId, exitCode }, "Exec completed");
511
+ }
512
+ catch (error) {
513
+ const errorMessage = error instanceof Error ? error.message : String(error);
514
+ // Send error
515
+ await transport.sendExecError(execId, errorMessage).catch((err) => {
516
+ logger.error({ traceId, execId, error: err }, "Failed to send exec error");
517
+ });
518
+ span?.setStatus({ code: core_1.SpanStatusCode.ERROR, message: errorMessage });
519
+ span?.end();
520
+ await (0, core_1.flushTracing)();
521
+ logger.error({ traceId, execId, error: errorMessage }, "Exec failed");
522
+ }
523
+ finally {
524
+ this.currentJobId = undefined;
525
+ }
526
+ }
527
+ async processBatchedMessages(messages) {
528
+ if (messages.length === 0)
529
+ return;
530
+ if (messages.length === 1) {
531
+ const singleMessage = messages[0];
532
+ if (singleMessage) {
533
+ await this.processSingleMessage(singleMessage, [
534
+ singleMessage.payload.messageId,
535
+ ]);
536
+ }
537
+ return;
538
+ }
539
+ logger.info(`Batching ${messages.length} messages for combined processing`);
540
+ const firstMessage = messages[0];
541
+ if (!firstMessage)
542
+ return;
543
+ const combinedPrompt = messages
544
+ .map((msg, index) => `Message ${index + 1}: ${msg.payload.messageText}`)
545
+ .join("\n\n");
546
+ const batchedMessage = {
547
+ timestamp: firstMessage.timestamp,
548
+ payload: {
549
+ ...firstMessage.payload,
550
+ messageText: combinedPrompt,
551
+ agentOptions: firstMessage.payload.agentOptions,
552
+ },
553
+ };
554
+ const processedIds = messages
555
+ .map((m) => m.payload.messageId)
556
+ .filter(Boolean);
557
+ await this.processSingleMessage(batchedMessage, processedIds);
558
+ }
559
+ async processSingleMessage(message, processedIds) {
560
+ // Get traceparent for distributed tracing
561
+ const traceparent = message.payload.platformMetadata?.traceparent ||
562
+ this.currentTraceparent ||
563
+ process.env.TRACEPARENT;
564
+ const traceId = (0, core_1.extractTraceId)(message.payload) ||
565
+ this.currentTraceId ||
566
+ process.env.TRACE_ID;
567
+ const conversationId = message.payload.conversationId;
568
+ // Create child span for agent execution (linked to parent via traceparent)
569
+ const span = (0, core_1.createChildSpan)("agent_execution", traceparent, {
570
+ "lobu.message_id": message.payload.messageId,
571
+ "lobu.conversation_id": conversationId,
572
+ "lobu.user_id": message.payload.userId,
573
+ "lobu.model": message.payload.agentOptions?.model || "default",
574
+ });
575
+ try {
576
+ if (!process.env.USER_ID) {
577
+ logger.warn(`USER_ID not set in environment, using userId from payload: ${message.payload.userId}`);
578
+ process.env.USER_ID = message.payload.userId;
579
+ }
580
+ const workerConfig = this.payloadToWorkerConfig(message.payload);
581
+ logger.info({
582
+ traceparent,
583
+ traceId,
584
+ messageId: message.payload.messageId,
585
+ model: message.payload.agentOptions?.model,
586
+ }, "Agent starting");
587
+ // Worker will decide whether to continue session based on workspace state
588
+ const { OpenClawWorker } = await Promise.resolve().then(() => __importStar(require("../openclaw/worker")));
589
+ this.currentWorker = new OpenClawWorker(workerConfig);
590
+ const workerTransport = this.currentWorker.getWorkerTransport();
591
+ if (workerTransport && workerTransport instanceof gateway_integration_1.HttpWorkerTransport) {
592
+ if (this.currentJobId) {
593
+ workerTransport.setJobId(this.currentJobId);
594
+ }
595
+ // Set processedMessageIds directly on the integration instance
596
+ const messageIds = processedIds && processedIds.length > 0
597
+ ? processedIds
598
+ : message?.payload?.messageId
599
+ ? [message.payload.messageId]
600
+ : [];
601
+ workerTransport.processedMessageIds = messageIds;
602
+ }
603
+ await this.currentWorker.execute();
604
+ this.currentJobId = undefined;
605
+ // Reset error count on successful message processing
606
+ this.eventErrorCount = 0;
607
+ // End span with success
608
+ span?.setStatus({ code: core_1.SpanStatusCode.OK });
609
+ span?.end();
610
+ // Flush traces immediately to ensure spans are exported before worker scales down
611
+ await (0, core_1.flushTracing)();
612
+ logger.info({
613
+ traceparent,
614
+ messageId: message.payload.messageId,
615
+ conversationId,
616
+ }, "Agent completed");
617
+ }
618
+ catch (error) {
619
+ // End span with error
620
+ span?.setStatus({
621
+ code: core_1.SpanStatusCode.ERROR,
622
+ message: error instanceof Error ? error.message : String(error),
623
+ });
624
+ span?.end();
625
+ // Flush traces on error too
626
+ await (0, core_1.flushTracing)();
627
+ logger.error({
628
+ traceparent,
629
+ messageId: message.payload.messageId,
630
+ conversationId,
631
+ error: error instanceof Error ? error.message : String(error),
632
+ }, "Agent failed");
633
+ const workerTransport = this.currentWorker?.getWorkerTransport();
634
+ if (workerTransport) {
635
+ try {
636
+ const enhancedError = error instanceof Error ? error : new Error(String(error));
637
+ await workerTransport.signalError(enhancedError);
638
+ }
639
+ catch (errorSendError) {
640
+ logger.error({ traceId, error: errorSendError }, "Failed to send error to dispatcher");
641
+ }
642
+ }
643
+ throw error;
644
+ }
645
+ finally {
646
+ if (this.currentWorker) {
647
+ try {
648
+ await this.currentWorker.cleanup();
649
+ }
650
+ catch (cleanupError) {
651
+ logger.error({ traceId, error: cleanupError }, "Error during worker cleanup");
652
+ }
653
+ this.currentWorker = null;
654
+ }
655
+ }
656
+ }
657
+ payloadToWorkerConfig(payload) {
658
+ const conversationId = payload.conversationId || "default";
659
+ const platformMetadata = payload.platformMetadata;
660
+ const agentOptions = {
661
+ ...(payload.agentOptions || {}),
662
+ ...(payload.agentOptions?.allowedTools
663
+ ? { allowedTools: payload.agentOptions.allowedTools }
664
+ : {}),
665
+ ...(payload.agentOptions?.disallowedTools
666
+ ? { disallowedTools: payload.agentOptions.disallowedTools }
667
+ : {}),
668
+ ...(payload.agentOptions?.timeoutMinutes
669
+ ? { timeoutMinutes: payload.agentOptions.timeoutMinutes }
670
+ : {}),
671
+ };
672
+ return {
673
+ sessionKey: `session-${conversationId}`,
674
+ userId: payload.userId,
675
+ agentId: payload.agentId,
676
+ channelId: payload.channelId,
677
+ conversationId,
678
+ userPrompt: Buffer.from(payload.messageText).toString("base64"),
679
+ responseChannel: String(platformMetadata.responseChannel || payload.channelId),
680
+ responseId: String(platformMetadata.responseId || payload.messageId),
681
+ botResponseId: platformMetadata.botResponseId
682
+ ? String(platformMetadata.botResponseId)
683
+ : undefined,
684
+ // Check both payload.teamId (WhatsApp) and platformMetadata.teamId (Slack)
685
+ teamId: (payload.teamId ?? platformMetadata.teamId)
686
+ ? String(payload.teamId ?? platformMetadata.teamId)
687
+ : undefined,
688
+ platform: payload.platform,
689
+ platformMetadata: platformMetadata, // Include full platformMetadata for files and other metadata
690
+ agentOptions: JSON.stringify(agentOptions),
691
+ workspace: {
692
+ baseDirectory: process.env.WORKSPACE_DIR || "/workspace",
693
+ },
694
+ };
695
+ }
696
+ /**
697
+ * Cleanup resources after event handling errors exceed threshold
698
+ */
699
+ async cleanupOnEventError(eventType, _error) {
700
+ logger.warn(`Cleaning up after ${this.eventErrorCount} event handling errors (last: ${eventType})`);
701
+ try {
702
+ // Clean up current worker if it exists
703
+ if (this.currentWorker) {
704
+ logger.info("Cleaning up current worker due to event errors");
705
+ try {
706
+ await this.currentWorker.cleanup?.();
707
+ }
708
+ catch (cleanupError) {
709
+ logger.error("Worker cleanup failed:", cleanupError);
710
+ }
711
+ this.currentWorker = null;
712
+ }
713
+ // Reset current job
714
+ if (this.currentJobId) {
715
+ logger.info(`Clearing stuck job: ${this.currentJobId}`);
716
+ this.currentJobId = undefined;
717
+ }
718
+ // Abort SSE connection to trigger reconnect
719
+ if (this.abortController) {
720
+ logger.info("Aborting SSE connection to trigger reconnect");
721
+ this.abortController.abort();
722
+ this.abortController = undefined;
723
+ }
724
+ // Reset error count after cleanup
725
+ this.eventErrorCount = 0;
726
+ logger.info("Event error cleanup completed, will reconnect");
727
+ }
728
+ catch (cleanupError) {
729
+ logger.error("Fatal error during event error cleanup:", cleanupError);
730
+ // Last resort: stop the client entirely
731
+ this.isRunning = false;
732
+ }
733
+ }
734
+ isHealthy() {
735
+ return this.isRunning && !this.messageBatcher.isCurrentlyProcessing();
736
+ }
737
+ getStatus() {
738
+ return {
739
+ isRunning: this.isRunning,
740
+ isProcessing: this.messageBatcher.isCurrentlyProcessing(),
741
+ userId: this.userId,
742
+ deploymentName: this.deploymentName,
743
+ pendingMessages: this.messageBatcher.getPendingCount(),
744
+ };
745
+ }
746
+ }
747
+ exports.GatewayClient = GatewayClient;
748
+ //# sourceMappingURL=sse-client.js.map