@robbiesrobotics/alice-agents 1.4.4 → 1.4.6

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.
@@ -2,35 +2,119 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diagnostics-otel";
5
- import { emptyPluginConfigSchema, onDiagnosticEvent } from "openclaw/plugin-sdk/diagnostics-otel";
5
+ import {
6
+ emptyPluginConfigSchema,
7
+ onDiagnosticEvent,
8
+ } from "openclaw/plugin-sdk/diagnostics-otel";
6
9
  import type { DiagnosticEventPayload } from "openclaw/plugin-sdk/diagnostics-otel";
7
10
 
8
11
  const OPENCLAW_HOME = process.env.OPENCLAW_HOME ?? join(homedir(), ".openclaw");
9
12
  const MC_CONFIG_PATH = join(OPENCLAW_HOME, ".alice-mission-control.json");
10
- const DEFAULT_INGEST_URL = "https://alice.av3.ai/api/v1/ingest";
13
+ const DEFAULT_DASHBOARD_URL = "https://alice.av3.ai";
14
+ const DEFAULT_ADMIN_URL = "https://admin.av3.ai";
15
+ const DEFAULT_INGEST_URL = `${DEFAULT_DASHBOARD_URL}/api/v1/ingest`;
16
+ const DEFAULT_RUNTIME_BASE_URL = `${DEFAULT_DASHBOARD_URL}/api/v1/runtime`;
17
+ const DEFAULT_GATEWAY_URL = process.env.OPENCLAW_GATEWAY_URL ?? "http://127.0.0.1:18789";
18
+ const DEFAULT_GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN ?? process.env.MC_GATEWAY_TOKEN ?? "";
19
+ const DEFAULT_CHAT_USER = process.env.MC_CHAT_USER ?? "mission-control-worker";
20
+ const HEARTBEAT_INTERVAL_MS = 15000;
21
+ const COMMAND_POLL_INTERVAL_MS = 5000;
11
22
 
12
- function readMissionControlConfig(): Record<string, unknown> {
23
+ type JsonRecord = Record<string, unknown>;
24
+
25
+ interface RuntimeNodeRegistrationPayload {
26
+ nodeId: string;
27
+ installId: string;
28
+ nodeName: string;
29
+ teamId?: string;
30
+ sourceNode: string;
31
+ connectionMode: "relay";
32
+ gatewayUrl: string;
33
+ runtimeVersion: string;
34
+ platform: string;
35
+ capabilities: JsonRecord;
36
+ metadata: JsonRecord;
37
+ }
38
+
39
+ interface RuntimeCommand {
40
+ id: string;
41
+ nodeId: string;
42
+ type: string;
43
+ status: string;
44
+ threadId: string | null;
45
+ sessionId: string | null;
46
+ payload: JsonRecord;
47
+ }
48
+
49
+ function readMissionControlConfig(): JsonRecord {
13
50
  try {
14
51
  if (!existsSync(MC_CONFIG_PATH)) return {};
15
- return JSON.parse(readFileSync(MC_CONFIG_PATH, "utf8"));
52
+ return JSON.parse(readFileSync(MC_CONFIG_PATH, "utf8")) as JsonRecord;
16
53
  } catch {
17
54
  return {};
18
55
  }
19
56
  }
20
57
 
21
- function getCloudConfig(): Record<string, unknown> {
58
+ function getCloudConfig(): JsonRecord {
22
59
  const fileConfig = readMissionControlConfig();
23
- return typeof fileConfig.cloud === "object" && fileConfig.cloud ? fileConfig.cloud as Record<string, unknown> : {};
60
+ return typeof fileConfig.cloud === "object" && fileConfig.cloud
61
+ ? (fileConfig.cloud as JsonRecord)
62
+ : {};
24
63
  }
25
64
 
26
65
  function getString(value: unknown, fallback = ""): string {
27
66
  return typeof value === "string" && value.trim() ? value.trim() : fallback;
28
67
  }
29
68
 
69
+ function normalizeUrl(value: string): string {
70
+ return value.replace(/\/+$/, "");
71
+ }
72
+
30
73
  const cloudConfig = getCloudConfig();
31
- const INGEST_URL = getString(process.env.MC_INGEST_URL, getString(cloudConfig.ingestUrl, DEFAULT_INGEST_URL));
74
+ const DASHBOARD_URL = normalizeUrl(
75
+ getString(process.env.MC_DASHBOARD_URL, getString(cloudConfig.dashboardUrl, DEFAULT_DASHBOARD_URL)),
76
+ );
77
+ const ADMIN_URL = normalizeUrl(
78
+ getString(process.env.MC_ADMIN_URL, getString(cloudConfig.adminUrl, DEFAULT_ADMIN_URL)),
79
+ );
80
+ const INGEST_URL = normalizeUrl(
81
+ getString(process.env.MC_INGEST_URL, getString(cloudConfig.ingestUrl, DEFAULT_INGEST_URL)),
82
+ );
83
+ const RUNTIME_BASE_URL = normalizeUrl(
84
+ getString(process.env.MC_RUNTIME_BASE_URL, getString(cloudConfig.runtimeBaseUrl, DEFAULT_RUNTIME_BASE_URL)),
85
+ );
86
+ const NODE_REGISTER_URL = normalizeUrl(
87
+ getString(process.env.MC_NODE_REGISTER_URL, getString(cloudConfig.nodeRegisterUrl, `${RUNTIME_BASE_URL}/nodes/register`)),
88
+ );
89
+ const NODE_HEARTBEAT_URL = normalizeUrl(
90
+ getString(
91
+ process.env.MC_NODE_HEARTBEAT_URL,
92
+ getString(cloudConfig.nodeHeartbeatUrl, `${RUNTIME_BASE_URL}/nodes/heartbeat`),
93
+ ),
94
+ );
95
+ const ADMIN_HEARTBEAT_URL = normalizeUrl(
96
+ getString(
97
+ process.env.MC_ADMIN_HEARTBEAT_URL,
98
+ getString(cloudConfig.adminHeartbeatUrl, `${ADMIN_URL}/api/admin/v1/node-heartbeat`),
99
+ ),
100
+ );
101
+ const COMMANDS_URL = normalizeUrl(
102
+ getString(process.env.MC_COMMANDS_URL, getString(cloudConfig.commandsUrl, `${RUNTIME_BASE_URL}/commands`)),
103
+ );
32
104
  const INGEST_TOKEN = getString(process.env.MC_INGEST_TOKEN, getString(cloudConfig.ingestToken));
105
+ const WORKER_TOKEN = getString(
106
+ process.env.MC_RUNTIME_WORKER_TOKEN,
107
+ getString(cloudConfig.workerToken, INGEST_TOKEN),
108
+ );
33
109
  const SOURCE_NODE = getString(process.env.MC_SOURCE_NODE, getString(cloudConfig.sourceNode, "openclaw-local"));
110
+ const TEAM_ID = getString(process.env.MC_TEAM_ID, getString(cloudConfig.teamId));
111
+ const TEAM_SLUG = getString(process.env.MC_TEAM_SLUG, getString(cloudConfig.teamSlug));
112
+ const TEAM_NAME = getString(process.env.MC_TEAM_NAME, getString(cloudConfig.teamName));
113
+ const TEAM_PLAN = getString(process.env.MC_TEAM_PLAN, getString(cloudConfig.teamPlan));
114
+ const INSTALL_ID = getString(process.env.MC_INSTALL_ID, SOURCE_NODE);
115
+ const NODE_ID = getString(process.env.MC_NODE_ID, SOURCE_NODE);
116
+ const GATEWAY_URL = normalizeUrl(getString(process.env.MC_GATEWAY_URL, DEFAULT_GATEWAY_URL));
117
+ const GATEWAY_TOKEN = getString(process.env.MC_GATEWAY_TOKEN, DEFAULT_GATEWAY_TOKEN);
34
118
 
35
119
  function now(): string {
36
120
  return new Date().toISOString();
@@ -43,30 +127,325 @@ function sessionKeyToAgentId(sessionKey?: string): string {
43
127
  }
44
128
 
45
129
  let seq = 0;
46
- function nextEventId(): string {
47
- return `mc-bridge-${Date.now()}-${++seq}`;
130
+ function nextId(prefix: string): string {
131
+ return `${prefix}-${Date.now()}-${++seq}`;
48
132
  }
49
133
 
50
- async function postToIngest(events: object[]): Promise<void> {
51
- const headers: Record<string, string> = {
134
+ function authHeaders(token: string): Record<string, string> {
135
+ return token ? { Authorization: `Bearer ${token}` } : {};
136
+ }
137
+
138
+ function headers(token = ""): Record<string, string> {
139
+ return {
52
140
  "Content-Type": "application/json",
141
+ ...authHeaders(token),
142
+ };
143
+ }
144
+
145
+ async function postJson(url: string, token: string, body: object): Promise<Response> {
146
+ return fetch(url, {
147
+ method: "POST",
148
+ headers: headers(token),
149
+ body: JSON.stringify(body),
150
+ signal: AbortSignal.timeout(8000),
151
+ });
152
+ }
153
+
154
+ async function postToIngest(events: object[]): Promise<void> {
155
+ try {
156
+ const res = await postJson(INGEST_URL, INGEST_TOKEN, events);
157
+ if (!res.ok) {
158
+ console.warn(`[mc-bridge] ingest HTTP ${res.status} — ${await res.text().catch(() => "")}`);
159
+ }
160
+ } catch (err) {
161
+ console.warn(`[mc-bridge] ingest post failed: ${String(err)}`);
162
+ }
163
+ }
164
+
165
+ function buildNodeRegistrationPayload(): RuntimeNodeRegistrationPayload {
166
+ return {
167
+ nodeId: NODE_ID,
168
+ installId: INSTALL_ID,
169
+ nodeName: SOURCE_NODE,
170
+ ...(TEAM_ID ? { teamId: TEAM_ID } : {}),
171
+ sourceNode: SOURCE_NODE,
172
+ connectionMode: "relay",
173
+ gatewayUrl: GATEWAY_URL,
174
+ runtimeVersion: process.version,
175
+ platform: process.platform,
176
+ capabilities: {
177
+ telemetry: true,
178
+ commands: true,
179
+ directGateway: true,
180
+ commandTypes: ["agent.message.send"],
181
+ },
182
+ metadata: {
183
+ dashboardUrl: DASHBOARD_URL,
184
+ pluginId: "mission-control-bridge",
185
+ ...(TEAM_SLUG ? { teamSlug: TEAM_SLUG } : {}),
186
+ ...(TEAM_NAME ? { teamName: TEAM_NAME } : {}),
187
+ ...(TEAM_PLAN ? { teamPlan: TEAM_PLAN } : {}),
188
+ },
189
+ };
190
+ }
191
+
192
+ async function registerNode(logger: { info(message: string): void; warn(message: string): void }): Promise<void> {
193
+ if (!WORKER_TOKEN) {
194
+ logger.warn("[mc-bridge] worker token missing; node registration skipped");
195
+ return;
196
+ }
197
+ try {
198
+ const res = await postJson(NODE_REGISTER_URL, WORKER_TOKEN, buildNodeRegistrationPayload());
199
+ if (!res.ok) {
200
+ logger.warn(`[mc-bridge] register failed ${res.status}: ${await res.text().catch(() => "")}`);
201
+ }
202
+ } catch (err) {
203
+ logger.warn(`[mc-bridge] register failed: ${String(err)}`);
204
+ }
205
+ }
206
+
207
+ async function heartbeatNode(logger: { warn(message: string): void }): Promise<void> {
208
+ if (!WORKER_TOKEN) return;
209
+ const payload = {
210
+ nodeId: NODE_ID,
211
+ ...(TEAM_ID ? { teamId: TEAM_ID } : {}),
212
+ status: "online",
213
+ runtimeVersion: process.version,
214
+ platform: process.platform,
215
+ capabilities: {
216
+ telemetry: true,
217
+ commands: true,
218
+ directGateway: true,
219
+ commandTypes: ["agent.message.send"],
220
+ },
221
+ metadata: {
222
+ dashboardUrl: DASHBOARD_URL,
223
+ ...(TEAM_SLUG ? { teamSlug: TEAM_SLUG } : {}),
224
+ ...(TEAM_NAME ? { teamName: TEAM_NAME } : {}),
225
+ ...(TEAM_PLAN ? { teamPlan: TEAM_PLAN } : {}),
226
+ },
53
227
  };
54
- if (INGEST_TOKEN) {
55
- headers["Authorization"] = `Bearer ${INGEST_TOKEN}`;
228
+ try {
229
+ const res = await postJson(NODE_HEARTBEAT_URL, WORKER_TOKEN, payload);
230
+ if (!res.ok) {
231
+ logger.warn(`[mc-bridge] heartbeat failed ${res.status}: ${await res.text().catch(() => "")}`);
232
+ }
233
+ } catch (err) {
234
+ logger.warn(`[mc-bridge] heartbeat failed: ${String(err)}`);
56
235
  }
57
236
 
58
237
  try {
59
- const res = await fetch(INGEST_URL, {
60
- method: "POST",
61
- headers,
62
- body: JSON.stringify(events),
238
+ const res = await postJson(ADMIN_HEARTBEAT_URL, WORKER_TOKEN, {
239
+ instanceId: NODE_ID,
240
+ nodeName: SOURCE_NODE,
241
+ ...(TEAM_ID ? { teamId: TEAM_ID } : {}),
242
+ ...(TEAM_SLUG ? { teamSlug: TEAM_SLUG } : {}),
243
+ ...(TEAM_NAME ? { teamName: TEAM_NAME } : {}),
244
+ ...(TEAM_PLAN ? { teamPlan: TEAM_PLAN } : {}),
245
+ tailscaleIp: getString(process.env.TAILSCALE_IP),
246
+ deployedVersion: getString(process.env.MC_DEPLOYED_VERSION),
247
+ runtimeVersion: process.version,
248
+ status: "healthy",
249
+ publicUrl: DASHBOARD_URL,
250
+ region: getString(process.env.MC_REGION),
251
+ backupStatus: "unknown",
252
+ memorySyncStatus: "healthy",
253
+ });
254
+ if (!res.ok) {
255
+ logger.warn(`[mc-bridge] admin heartbeat failed ${res.status}: ${await res.text().catch(() => "")}`);
256
+ }
257
+ } catch (err) {
258
+ logger.warn(`[mc-bridge] admin heartbeat failed: ${String(err)}`);
259
+ }
260
+ }
261
+
262
+ async function listQueuedCommands(logger: { warn(message: string): void }): Promise<RuntimeCommand[]> {
263
+ if (!WORKER_TOKEN) return [];
264
+ const url = `${COMMANDS_URL}?nodeId=${encodeURIComponent(NODE_ID)}&limit=5`;
265
+ try {
266
+ const res = await fetch(url, {
267
+ method: "GET",
268
+ headers: authHeaders(WORKER_TOKEN),
63
269
  signal: AbortSignal.timeout(8000),
64
270
  });
65
271
  if (!res.ok) {
66
- console.warn(`[mc-bridge] ingest HTTP ${res.status} ${await res.text().catch(() => "")}`);
272
+ logger.warn(`[mc-bridge] command poll failed ${res.status}: ${await res.text().catch(() => "")}`);
273
+ return [];
67
274
  }
275
+ const data = (await res.json()) as { commands?: RuntimeCommand[] };
276
+ return Array.isArray(data.commands) ? data.commands : [];
68
277
  } catch (err) {
69
- console.warn(`[mc-bridge] ingest post failed: ${String(err)}`);
278
+ logger.warn(`[mc-bridge] command poll failed: ${String(err)}`);
279
+ return [];
280
+ }
281
+ }
282
+
283
+ async function leaseCommand(commandId: string, logger: { warn(message: string): void }): Promise<boolean> {
284
+ try {
285
+ const res = await postJson(`${COMMANDS_URL}/${commandId}/lease`, WORKER_TOKEN, {
286
+ nodeId: NODE_ID,
287
+ leaseOwner: NODE_ID,
288
+ });
289
+ if (res.ok) return true;
290
+ if (res.status !== 409) {
291
+ logger.warn(`[mc-bridge] command lease failed ${res.status}: ${await res.text().catch(() => "")}`);
292
+ }
293
+ return false;
294
+ } catch (err) {
295
+ logger.warn(`[mc-bridge] command lease failed: ${String(err)}`);
296
+ return false;
297
+ }
298
+ }
299
+
300
+ async function emitCommandEvent(
301
+ commandId: string,
302
+ eventType: string,
303
+ payload: JsonRecord,
304
+ logger: { warn(message: string): void },
305
+ ): Promise<void> {
306
+ try {
307
+ const res = await postJson(`${COMMANDS_URL}/${commandId}/events`, WORKER_TOKEN, {
308
+ nodeId: NODE_ID,
309
+ eventType,
310
+ payload,
311
+ });
312
+ if (!res.ok) {
313
+ logger.warn(`[mc-bridge] command event failed ${res.status}: ${await res.text().catch(() => "")}`);
314
+ }
315
+ } catch (err) {
316
+ logger.warn(`[mc-bridge] command event failed: ${String(err)}`);
317
+ }
318
+ }
319
+
320
+ async function completeCommand(
321
+ commandId: string,
322
+ result: JsonRecord,
323
+ logger: { warn(message: string): void },
324
+ ): Promise<void> {
325
+ try {
326
+ const res = await postJson(`${COMMANDS_URL}/${commandId}/complete`, WORKER_TOKEN, {
327
+ nodeId: NODE_ID,
328
+ result,
329
+ });
330
+ if (!res.ok) {
331
+ logger.warn(`[mc-bridge] command completion failed ${res.status}: ${await res.text().catch(() => "")}`);
332
+ }
333
+ } catch (err) {
334
+ logger.warn(`[mc-bridge] command completion failed: ${String(err)}`);
335
+ }
336
+ }
337
+
338
+ async function failCommand(
339
+ commandId: string,
340
+ error: string,
341
+ logger: { warn(message: string): void },
342
+ ): Promise<void> {
343
+ try {
344
+ const res = await postJson(`${COMMANDS_URL}/${commandId}/fail`, WORKER_TOKEN, {
345
+ nodeId: NODE_ID,
346
+ error,
347
+ });
348
+ if (!res.ok) {
349
+ logger.warn(`[mc-bridge] command failure update failed ${res.status}: ${await res.text().catch(() => "")}`);
350
+ }
351
+ } catch (err) {
352
+ logger.warn(`[mc-bridge] command failure update failed: ${String(err)}`);
353
+ }
354
+ }
355
+
356
+ async function executeAgentMessage(command: RuntimeCommand): Promise<JsonRecord> {
357
+ const payload = command.payload ?? {};
358
+ const message = getString(payload.message);
359
+ const agentId = getString(payload.agentId, "olivia");
360
+ const chatUser = getString(payload.user, DEFAULT_CHAT_USER);
361
+
362
+ if (!message) {
363
+ throw new Error("agent.message.send requires payload.message");
364
+ }
365
+
366
+ const gatewayHeaders: Record<string, string> = {
367
+ "Content-Type": "application/json",
368
+ };
369
+ if (GATEWAY_TOKEN) {
370
+ gatewayHeaders.Authorization = `Bearer ${GATEWAY_TOKEN}`;
371
+ }
372
+
373
+ const res = await fetch(`${GATEWAY_URL}/v1/responses`, {
374
+ method: "POST",
375
+ headers: gatewayHeaders,
376
+ body: JSON.stringify({
377
+ model: `openclaw:${agentId}`,
378
+ input: message,
379
+ user: chatUser,
380
+ stream: false,
381
+ }),
382
+ signal: AbortSignal.timeout(30000),
383
+ });
384
+
385
+ if (!res.ok) {
386
+ throw new Error(`gateway ${res.status}: ${await res.text().catch(() => "")}`);
387
+ }
388
+
389
+ const data = (await res.json()) as JsonRecord;
390
+ const outputText = extractOutputText(data);
391
+
392
+ return {
393
+ agentId,
394
+ user: chatUser,
395
+ responseId: getString(data.id),
396
+ outputText,
397
+ raw: data,
398
+ };
399
+ }
400
+
401
+ function extractOutputText(data: JsonRecord): string {
402
+ const output = Array.isArray(data.output) ? data.output : [];
403
+ const parts: string[] = [];
404
+
405
+ for (const item of output) {
406
+ if (!item || typeof item !== "object") continue;
407
+ const content = Array.isArray((item as JsonRecord).content) ? ((item as JsonRecord).content as unknown[]) : [];
408
+ for (const block of content) {
409
+ if (!block || typeof block !== "object") continue;
410
+ const record = block as JsonRecord;
411
+ const text =
412
+ getString(record.text) ||
413
+ getString(record.value) ||
414
+ getString(typeof record.output_text === "string" ? record.output_text : "");
415
+ if (text) parts.push(text);
416
+ }
417
+ }
418
+
419
+ return parts.join("\n\n").trim();
420
+ }
421
+
422
+ async function executeCommand(
423
+ command: RuntimeCommand,
424
+ logger: { info(message: string): void; warn(message: string): void },
425
+ ): Promise<void> {
426
+ await emitCommandEvent(command.id, "command.started", {
427
+ commandType: command.type,
428
+ startedAt: now(),
429
+ }, logger);
430
+
431
+ try {
432
+ switch (command.type) {
433
+ case "agent.message.send": {
434
+ const result = await executeAgentMessage(command);
435
+ await emitCommandEvent(command.id, "agent.message.completed", result, logger);
436
+ await completeCommand(command.id, result, logger);
437
+ break;
438
+ }
439
+ default:
440
+ throw new Error(`unsupported command type: ${command.type}`);
441
+ }
442
+ } catch (err) {
443
+ const message = err instanceof Error ? err.message : String(err);
444
+ await emitCommandEvent(command.id, "command.failed", {
445
+ error: message,
446
+ failedAt: now(),
447
+ }, logger);
448
+ await failCommand(command.id, message, logger);
70
449
  }
71
450
  }
72
451
 
@@ -75,7 +454,7 @@ function handleModelUsage(evt: Extract<DiagnosticEventPayload, { type: "model.us
75
454
  const totalTokens = evt.usage.total ?? (evt.usage.input ?? 0) + (evt.usage.output ?? 0);
76
455
 
77
456
  const event = {
78
- event_id: nextEventId(),
457
+ event_id: nextId("mc-bridge"),
79
458
  event_type: "agent.session.completed",
80
459
  event_version: "1.0",
81
460
  source_system: "openclaw",
@@ -85,8 +464,9 @@ function handleModelUsage(evt: Extract<DiagnosticEventPayload, { type: "model.us
85
464
  actor_type: "agent",
86
465
  correlation_id: evt.sessionId ?? null,
87
466
  payload: {
88
- session_id: evt.sessionId ?? evt.sessionKey ?? nextEventId(),
467
+ session_id: evt.sessionId ?? evt.sessionKey ?? nextId("sess"),
89
468
  agent_id: agentId,
469
+ ...(TEAM_ID ? { teamId: TEAM_ID } : {}),
90
470
  model: evt.model ?? "unknown",
91
471
  channel: evt.channel ?? "unknown",
92
472
  total_tokens: totalTokens,
@@ -102,7 +482,7 @@ function handleModelUsage(evt: Extract<DiagnosticEventPayload, { type: "model.us
102
482
  },
103
483
  };
104
484
 
105
- postToIngest([event]);
485
+ void postToIngest([event]);
106
486
  }
107
487
 
108
488
  function handleMessageProcessed(evt: Extract<DiagnosticEventPayload, { type: "message.processed" }>) {
@@ -118,7 +498,7 @@ function handleMessageProcessed(evt: Extract<DiagnosticEventPayload, { type: "me
118
498
  if (!eventType) return;
119
499
 
120
500
  const event = {
121
- event_id: nextEventId(),
501
+ event_id: nextId("mc-bridge"),
122
502
  event_type: eventType,
123
503
  event_version: "1.0",
124
504
  source_system: "openclaw",
@@ -128,8 +508,9 @@ function handleMessageProcessed(evt: Extract<DiagnosticEventPayload, { type: "me
128
508
  actor_type: "agent",
129
509
  correlation_id: evt.sessionId ?? null,
130
510
  payload: {
131
- session_id: evt.sessionId ?? evt.sessionKey ?? nextEventId(),
511
+ session_id: evt.sessionId ?? evt.sessionKey ?? nextId("sess"),
132
512
  agent_id: agentId,
513
+ ...(TEAM_ID ? { teamId: TEAM_ID } : {}),
133
514
  channel: evt.channel ?? "unknown",
134
515
  status: evt.outcome === "error" ? "failed" : "completed",
135
516
  duration_ms: evt.durationMs ?? 0,
@@ -137,7 +518,7 @@ function handleMessageProcessed(evt: Extract<DiagnosticEventPayload, { type: "me
137
518
  },
138
519
  };
139
520
 
140
- postToIngest([event]);
521
+ void postToIngest([event]);
141
522
  }
142
523
 
143
524
  function handleSessionState(evt: Extract<DiagnosticEventPayload, { type: "session.state" }>) {
@@ -151,7 +532,7 @@ function handleSessionState(evt: Extract<DiagnosticEventPayload, { type: "sessio
151
532
  if (!eventType) return;
152
533
 
153
534
  const event = {
154
- event_id: nextEventId(),
535
+ event_id: nextId("mc-bridge"),
155
536
  event_type: eventType,
156
537
  event_version: "1.0",
157
538
  source_system: "openclaw",
@@ -161,20 +542,21 @@ function handleSessionState(evt: Extract<DiagnosticEventPayload, { type: "sessio
161
542
  actor_type: "agent",
162
543
  correlation_id: evt.sessionId ?? null,
163
544
  payload: {
164
- session_id: evt.sessionId ?? evt.sessionKey ?? nextEventId(),
545
+ session_id: evt.sessionId ?? evt.sessionKey ?? nextId("sess"),
165
546
  agent_id: agentId,
547
+ ...(TEAM_ID ? { teamId: TEAM_ID } : {}),
166
548
  state: evt.state,
167
549
  prev_state: evt.prevState ?? null,
168
550
  },
169
551
  };
170
552
 
171
- postToIngest([event]);
553
+ void postToIngest([event]);
172
554
  }
173
555
 
174
556
  const plugin = {
175
557
  id: "mission-control-bridge",
176
558
  name: "Mission Control Bridge",
177
- description: "Forwards OpenClaw diagnostic events to the A.L.I.C.E. Mission Control ingest endpoint",
559
+ description: "Forwards OpenClaw diagnostic events to A.L.I.C.E. Mission Control and executes hosted runtime commands",
178
560
  configSchema: emptyPluginConfigSchema(),
179
561
 
180
562
  register(api: OpenClawPluginApi) {
@@ -183,6 +565,10 @@ const plugin = {
183
565
 
184
566
  async start(ctx) {
185
567
  ctx.logger.info(`[mc-bridge] starting — ingest URL: ${INGEST_URL}`);
568
+ ctx.logger.info(`[mc-bridge] runtime worker — commands URL: ${COMMANDS_URL}`);
569
+
570
+ let pollInFlight = false;
571
+ let stopped = false;
186
572
 
187
573
  const unsubscribe = onDiagnosticEvent((evt: DiagnosticEventPayload) => {
188
574
  try {
@@ -202,11 +588,25 @@ const plugin = {
202
588
  }
203
589
  });
204
590
 
205
- (this as { _unsub?: () => void })._unsub = unsubscribe;
591
+ const pollCommands = async () => {
592
+ if (stopped || pollInFlight || !WORKER_TOKEN) return;
593
+ pollInFlight = true;
594
+ try {
595
+ const commands = await listQueuedCommands(ctx.logger);
596
+ for (const command of commands) {
597
+ if (!(await leaseCommand(command.id, ctx.logger))) continue;
598
+ await executeCommand(command, ctx.logger);
599
+ }
600
+ } finally {
601
+ pollInFlight = false;
602
+ }
603
+ };
206
604
 
605
+ await registerNode(ctx.logger);
606
+ await heartbeatNode(ctx.logger);
207
607
  await postToIngest([
208
608
  {
209
- event_id: nextEventId(),
609
+ event_id: nextId("mc-bridge"),
210
610
  event_type: "node.registered",
211
611
  event_version: "1.0",
212
612
  source_system: "openclaw",
@@ -214,17 +614,79 @@ const plugin = {
214
614
  occurred_at: now(),
215
615
  payload: {
216
616
  node_name: SOURCE_NODE,
617
+ node_id: NODE_ID,
618
+ ...(TEAM_ID ? { teamId: TEAM_ID } : {}),
619
+ install_id: INSTALL_ID,
217
620
  platform: process.platform,
218
621
  node_version: process.version,
622
+ gateway_url: GATEWAY_URL,
219
623
  },
220
624
  },
221
625
  ]);
626
+
627
+ const heartbeatTimer = setInterval(() => {
628
+ void heartbeatNode(ctx.logger);
629
+ }, HEARTBEAT_INTERVAL_MS);
630
+
631
+ const pollTimer = setInterval(() => {
632
+ void pollCommands();
633
+ }, COMMAND_POLL_INTERVAL_MS);
634
+
635
+ await pollCommands();
636
+
637
+ (
638
+ this as {
639
+ _unsub?: () => void;
640
+ _heartbeatTimer?: ReturnType<typeof setInterval>;
641
+ _pollTimer?: ReturnType<typeof setInterval>;
642
+ _stop?: () => void;
643
+ }
644
+ )._unsub = unsubscribe;
645
+ (
646
+ this as {
647
+ _unsub?: () => void;
648
+ _heartbeatTimer?: ReturnType<typeof setInterval>;
649
+ _pollTimer?: ReturnType<typeof setInterval>;
650
+ _stop?: () => void;
651
+ }
652
+ )._heartbeatTimer = heartbeatTimer;
653
+ (
654
+ this as {
655
+ _unsub?: () => void;
656
+ _heartbeatTimer?: ReturnType<typeof setInterval>;
657
+ _pollTimer?: ReturnType<typeof setInterval>;
658
+ _stop?: () => void;
659
+ }
660
+ )._pollTimer = pollTimer;
661
+ (
662
+ this as {
663
+ _unsub?: () => void;
664
+ _heartbeatTimer?: ReturnType<typeof setInterval>;
665
+ _pollTimer?: ReturnType<typeof setInterval>;
666
+ _stop?: () => void;
667
+ }
668
+ )._stop = () => {
669
+ stopped = true;
670
+ clearInterval(heartbeatTimer);
671
+ clearInterval(pollTimer);
672
+ };
222
673
  },
223
674
 
224
675
  async stop() {
225
- const self = this as { _unsub?: () => void };
676
+ const self = this as {
677
+ _unsub?: () => void;
678
+ _heartbeatTimer?: ReturnType<typeof setInterval>;
679
+ _pollTimer?: ReturnType<typeof setInterval>;
680
+ _stop?: () => void;
681
+ };
226
682
  self._unsub?.();
683
+ self._stop?.();
684
+ if (self._heartbeatTimer) clearInterval(self._heartbeatTimer);
685
+ if (self._pollTimer) clearInterval(self._pollTimer);
227
686
  self._unsub = undefined;
687
+ self._heartbeatTimer = undefined;
688
+ self._pollTimer = undefined;
689
+ self._stop = undefined;
228
690
  },
229
691
  });
230
692
  },
@@ -35,7 +35,7 @@ Measured, precise, craftsman-energy. You take pride in clean implementations. Yo
35
35
 
36
36
  ## Tools
37
37
 
38
- - Use the `claude-code` skill for any non-trivial multi-file coding task
38
+ - Use the `coding-agent` skill for any non-trivial multi-file coding task
39
39
  - Use `exec` to run tests, check build output, and verify implementations
40
40
  - Use `read` to understand the codebase before proposing changes
41
41
  - Use `web_search` for API docs, error messages, and library references