@chrysb/alphaclaw 0.5.5 → 0.5.7-beta.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 (86) hide show
  1. package/bin/alphaclaw.js +6 -1
  2. package/lib/public/css/agents.css +92 -0
  3. package/lib/public/css/explorer.css +101 -0
  4. package/lib/public/css/shell.css +15 -4
  5. package/lib/public/js/app.js +69 -3
  6. package/lib/public/js/components/action-button.js +5 -0
  7. package/lib/public/js/components/agents-tab/agent-bindings-section/helpers.js +76 -0
  8. package/lib/public/js/components/agents-tab/agent-bindings-section/index.js +490 -0
  9. package/lib/public/js/components/agents-tab/agent-bindings-section/use-agent-bindings.js +256 -0
  10. package/lib/public/js/components/agents-tab/agent-detail-panel.js +74 -0
  11. package/lib/public/js/components/agents-tab/agent-identity-section.js +175 -0
  12. package/lib/public/js/components/agents-tab/agent-overview/index.js +53 -0
  13. package/lib/public/js/components/agents-tab/agent-overview/manage-card.js +44 -0
  14. package/lib/public/js/components/agents-tab/agent-overview/model-card.js +158 -0
  15. package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +169 -0
  16. package/lib/public/js/components/agents-tab/agent-overview/use-workspace-card.js +45 -0
  17. package/lib/public/js/components/agents-tab/agent-overview/workspace-card.js +47 -0
  18. package/lib/public/js/components/agents-tab/agent-pairing-section.js +265 -0
  19. package/lib/public/js/components/agents-tab/create-agent-modal.js +189 -0
  20. package/lib/public/js/components/agents-tab/create-channel-modal.js +323 -0
  21. package/lib/public/js/components/agents-tab/delete-agent-dialog.js +50 -0
  22. package/lib/public/js/components/agents-tab/edit-agent-modal.js +109 -0
  23. package/lib/public/js/components/agents-tab/index.js +148 -0
  24. package/lib/public/js/components/agents-tab/use-agents.js +89 -0
  25. package/lib/public/js/components/channel-account-status-badge.js +35 -0
  26. package/lib/public/js/components/channel-operations-panel.js +33 -0
  27. package/lib/public/js/components/channels.js +545 -60
  28. package/lib/public/js/components/envars.js +25 -4
  29. package/lib/public/js/components/general/index.js +21 -11
  30. package/lib/public/js/components/general/use-general-tab.js +78 -16
  31. package/lib/public/js/components/google/gmail-setup-wizard.js +1 -3
  32. package/lib/public/js/components/google/index.js +28 -30
  33. package/lib/public/js/components/icons.js +37 -0
  34. package/lib/public/js/components/models-tab/index.js +58 -224
  35. package/lib/public/js/components/models-tab/model-picker.js +212 -0
  36. package/lib/public/js/components/models-tab/use-models.js +17 -14
  37. package/lib/public/js/components/onboarding/use-welcome-pairing.js +4 -4
  38. package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
  39. package/lib/public/js/components/overflow-menu.js +122 -0
  40. package/lib/public/js/components/pairings.js +36 -8
  41. package/lib/public/js/components/routes/agents-route.js +27 -0
  42. package/lib/public/js/components/routes/general-route.js +2 -0
  43. package/lib/public/js/components/routes/index.js +1 -0
  44. package/lib/public/js/components/routes/telegram-route.js +2 -2
  45. package/lib/public/js/components/secret-input.js +8 -1
  46. package/lib/public/js/components/sidebar.js +65 -39
  47. package/lib/public/js/components/telegram-workspace/index.js +175 -74
  48. package/lib/public/js/components/telegram-workspace/manage.js +83 -10
  49. package/lib/public/js/components/telegram-workspace/onboarding.js +9 -8
  50. package/lib/public/js/components/webhooks.js +43 -18
  51. package/lib/public/js/hooks/use-app-shell-controller.js +7 -0
  52. package/lib/public/js/hooks/use-browse-navigation.js +8 -5
  53. package/lib/public/js/hooks/use-destination-session-selection.js +8 -1
  54. package/lib/public/js/lib/api.js +163 -9
  55. package/lib/public/js/lib/app-navigation.js +2 -1
  56. package/lib/public/js/lib/channel-create-operation.js +102 -0
  57. package/lib/public/js/lib/format.js +14 -0
  58. package/lib/public/js/lib/sse.js +51 -0
  59. package/lib/public/js/lib/telegram-api.js +38 -18
  60. package/lib/public/setup.html +1 -0
  61. package/lib/public/shared/browse-file-policies.json +0 -1
  62. package/lib/server/agents/service.js +1478 -0
  63. package/lib/server/constants.js +2 -2
  64. package/lib/server/env.js +3 -1
  65. package/lib/server/gateway.js +104 -20
  66. package/lib/server/gmail-serve.js +2 -12
  67. package/lib/server/gmail-watch.js +29 -2
  68. package/lib/server/onboarding/import/import-applier.js +0 -1
  69. package/lib/server/onboarding/index.js +0 -6
  70. package/lib/server/onboarding/workspace.js +74 -38
  71. package/lib/server/openclaw-config.js +23 -0
  72. package/lib/server/operation-events.js +141 -0
  73. package/lib/server/routes/agents.js +266 -0
  74. package/lib/server/routes/pairings.js +135 -25
  75. package/lib/server/routes/system.js +90 -10
  76. package/lib/server/routes/telegram.js +247 -51
  77. package/lib/server/startup.js +23 -0
  78. package/lib/server/telegram-workspace.js +61 -10
  79. package/lib/server/topic-registry.js +66 -7
  80. package/lib/server/watchdog.js +151 -27
  81. package/lib/server/webhooks.js +60 -12
  82. package/lib/server.js +40 -27
  83. package/lib/setup/core-prompts/AGENTS.md +6 -5
  84. package/lib/setup/core-prompts/TOOLS.md +1 -8
  85. package/package.json +1 -1
  86. package/lib/setup/skills/control-ui/SKILL.md +0 -62
@@ -2,6 +2,14 @@ const fs = require("fs");
2
2
  const { WORKSPACE_DIR } = require("./constants");
3
3
 
4
4
  const kRegistryPath = `${WORKSPACE_DIR}/topic-registry.json`;
5
+ const kDefaultAccountId = "default";
6
+ const kDefaultAgentId = "default";
7
+
8
+ const normalizeAccountId = (value) =>
9
+ String(value || "").trim() || kDefaultAccountId;
10
+
11
+ const normalizeGroupAgentId = (value) =>
12
+ String(value || "").trim() || kDefaultAgentId;
5
13
 
6
14
  const readRegistry = () => {
7
15
  try {
@@ -36,6 +44,20 @@ const setGroup = (groupId, groupData) => {
36
44
  return registry;
37
45
  };
38
46
 
47
+ const getGroupsForAccount = (accountId) => {
48
+ const registry = readRegistry();
49
+ const normalizedAccountId = normalizeAccountId(accountId);
50
+ const groups = registry.groups && typeof registry.groups === "object"
51
+ ? registry.groups
52
+ : {};
53
+ return Object.fromEntries(
54
+ Object.entries(groups).filter(([, group]) => {
55
+ const groupAccountId = normalizeAccountId(group?.accountId);
56
+ return groupAccountId === normalizedAccountId;
57
+ }),
58
+ );
59
+ };
60
+
39
61
  const addTopic = (groupId, threadId, topicData) => {
40
62
  const registry = readRegistry();
41
63
  if (!registry.groups[groupId]) {
@@ -90,21 +112,56 @@ const getTotalTopicCount = () => {
90
112
  return count;
91
113
  };
92
114
 
93
- // Render the topic registry as a markdown section for TOOLS.md
94
- const renderTopicRegistryMarkdown = ({ includeSyncGuidance = false } = {}) => {
115
+ const getTopicsForAgent = (agentId) => {
95
116
  const registry = readRegistry();
117
+ const groups = registry.groups && typeof registry.groups === "object"
118
+ ? registry.groups
119
+ : {};
120
+ const normalizedAgentId = normalizeGroupAgentId(agentId);
96
121
  const rows = [];
97
- for (const [groupId, group] of Object.entries(registry.groups)) {
98
- for (const [threadId, topic] of Object.entries(group.topics || {})) {
122
+ for (const [groupId, group] of Object.entries(groups)) {
123
+ const groupAgentId = normalizeGroupAgentId(group?.agentId);
124
+ const groupName = String(group?.name || "").trim() || groupId;
125
+ const topics = group?.topics && typeof group.topics === "object"
126
+ ? group.topics
127
+ : {};
128
+ const isGroupOwner = groupAgentId === normalizedAgentId;
129
+ for (const [threadId, topic] of Object.entries(topics)) {
130
+ const topicAgentId = String(topic?.agentId || "").trim();
131
+ if (!isGroupOwner && topicAgentId !== normalizedAgentId) continue;
99
132
  rows.push({
100
- groupName: group.name || groupId,
133
+ groupName,
101
134
  groupId,
102
- topicName: topic.name,
135
+ topicName: topic?.name,
103
136
  threadId,
137
+ groupAgentId,
138
+ topicAgentId,
104
139
  });
105
140
  }
106
141
  }
107
- if (rows.length === 0 && !includeSyncGuidance) return "";
142
+ return rows;
143
+ };
144
+
145
+ // Render the topic registry as a markdown section for TOOLS.md
146
+ const renderTopicRegistryMarkdown = ({
147
+ includeSyncGuidance = false,
148
+ agentId = "",
149
+ } = {}) => {
150
+ const registry = readRegistry();
151
+ const groups = registry.groups && typeof registry.groups === "object"
152
+ ? registry.groups
153
+ : {};
154
+ const normalizedAgentId = String(agentId || "").trim();
155
+ const rows = normalizedAgentId
156
+ ? getTopicsForAgent(normalizedAgentId)
157
+ : Object.entries(groups).flatMap(([groupId, group]) =>
158
+ Object.entries(group.topics || {}).map(([threadId, topic]) => ({
159
+ groupName: group.name || groupId,
160
+ groupId,
161
+ topicName: topic.name,
162
+ threadId,
163
+ })));
164
+ if (rows.length === 0) return "";
108
165
 
109
166
  const lines = [
110
167
  "",
@@ -144,9 +201,11 @@ module.exports = {
144
201
  writeRegistry,
145
202
  getGroup,
146
203
  setGroup,
204
+ getGroupsForAccount,
147
205
  addTopic,
148
206
  updateTopic,
149
207
  removeTopic,
150
208
  getTotalTopicCount,
209
+ getTopicsForAgent,
151
210
  renderTopicRegistryMarkdown,
152
211
  };
@@ -12,10 +12,15 @@ const kBootstrapHealthCheckMs = 5 * 1000;
12
12
  const kExpectedRestartWindowMs = 15 * 1000;
13
13
 
14
14
  const isTruthy = (value) =>
15
- ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
15
+ ["1", "true", "yes", "on"].includes(
16
+ String(value || "")
17
+ .trim()
18
+ .toLowerCase(),
19
+ );
16
20
 
17
21
  const parseHealthResult = (result) => {
18
- if (!result?.ok) return { ok: false, reason: result?.stderr || "health command failed" };
22
+ if (!result?.ok)
23
+ return { ok: false, reason: result?.stderr || "health command failed" };
19
24
  const raw = String(result.stdout || "").trim();
20
25
  if (!raw) return { ok: true };
21
26
  try {
@@ -39,6 +44,18 @@ const parseHealthResult = (result) => {
39
44
  }
40
45
  return { ok: true };
41
46
  };
47
+ const isDuplicateGatewayLaunchExit = ({ code, stderrTail = [] } = {}) => {
48
+ if (code !== 1) return false;
49
+ const stderrText = (Array.isArray(stderrTail) ? stderrTail : [])
50
+ .map((entry) => String(entry || ""))
51
+ .join("\n")
52
+ .toLowerCase();
53
+ if (!stderrText) return false;
54
+ return (
55
+ stderrText.includes("another gateway instance is already listening") ||
56
+ (stderrText.includes("port") && stderrText.includes("already in use"))
57
+ );
58
+ };
42
59
 
43
60
  const createWatchdog = ({
44
61
  clawCmd,
@@ -58,7 +75,9 @@ const createWatchdog = ({
58
75
  repairAttempts: 0,
59
76
  crashTimestamps: [],
60
77
  autoRepair: isTruthy(process.env.WATCHDOG_AUTO_REPAIR),
61
- notificationsDisabled: isTruthy(process.env.WATCHDOG_NOTIFICATIONS_DISABLED),
78
+ notificationsDisabled: isTruthy(
79
+ process.env.WATCHDOG_NOTIFICATIONS_DISABLED,
80
+ ),
62
81
  operationInProgress: false,
63
82
  gatewayStartedAt: null,
64
83
  gatewayPid: null,
@@ -92,7 +111,8 @@ const createWatchdog = ({
92
111
  scheduleDegradedHealthCheck();
93
112
  }
94
113
  }, kWatchdogDegradedCheckIntervalMs);
95
- if (typeof degradedHealthTimer.unref === "function") degradedHealthTimer.unref();
114
+ if (typeof degradedHealthTimer.unref === "function")
115
+ degradedHealthTimer.unref();
96
116
  };
97
117
 
98
118
  const clearExpectedRestartWindow = () => {
@@ -101,7 +121,10 @@ const createWatchdog = ({
101
121
  };
102
122
 
103
123
  const markExpectedRestartWindow = (durationMs = kExpectedRestartWindowMs) => {
104
- const safeDuration = Math.max(5000, Number(durationMs) || kExpectedRestartWindowMs);
124
+ const safeDuration = Math.max(
125
+ 5000,
126
+ Number(durationMs) || kExpectedRestartWindowMs,
127
+ );
105
128
  state.expectedRestartInProgress = true;
106
129
  state.expectedRestartUntilMs = Date.now() + safeDuration;
107
130
  };
@@ -142,13 +165,21 @@ const createWatchdog = ({
142
165
 
143
166
  const trimCrashWindow = () => {
144
167
  const threshold = Date.now() - kWatchdogCrashLoopWindowMs;
145
- state.crashTimestamps = state.crashTimestamps.filter((ts) => ts >= threshold);
168
+ state.crashTimestamps = state.crashTimestamps.filter(
169
+ (ts) => ts >= threshold,
170
+ );
146
171
  };
147
172
 
148
173
  const createCorrelationId = () =>
149
174
  `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
150
175
 
151
- const logEvent = (eventType, source, status, details = null, correlationId = "") => {
176
+ const logEvent = (
177
+ eventType,
178
+ source,
179
+ status,
180
+ details = null,
181
+ correlationId = "",
182
+ ) => {
152
183
  try {
153
184
  insertWatchdogEvent({
154
185
  eventType,
@@ -168,16 +199,25 @@ const createWatchdog = ({
168
199
  }
169
200
  if (!notifier?.notify) return { ok: false, reason: "notifier_unavailable" };
170
201
  const result = await notifier.notify(message);
171
- logEvent("notification", "watchdog", result.ok ? "ok" : "failed", result, correlationId);
202
+ logEvent(
203
+ "notification",
204
+ "watchdog",
205
+ result.ok ? "ok" : "failed",
206
+ result,
207
+ correlationId,
208
+ );
172
209
  return result;
173
210
  };
174
211
 
175
212
  const getWatchdogSetupUrl = () => {
176
213
  try {
177
214
  const base =
178
- typeof resolveSetupUrl === "function" ? String(resolveSetupUrl() || "") : "";
215
+ typeof resolveSetupUrl === "function"
216
+ ? String(resolveSetupUrl() || "")
217
+ : "";
179
218
  if (base) return `${base.replace(/\/+$/, "")}/#/watchdog`;
180
- const fallbackPort = Number.parseInt(String(process.env.PORT || "3000"), 10) || 3000;
219
+ const fallbackPort =
220
+ Number.parseInt(String(process.env.PORT || "3000"), 10) || 3000;
181
221
  return `http://localhost:${fallbackPort}/#/watchdog`;
182
222
  } catch {
183
223
  return "";
@@ -190,7 +230,8 @@ const createWatchdog = ({
190
230
  return `${line} - [View logs](${setupUrl})`;
191
231
  };
192
232
 
193
- const asInlineCode = (value) => `\`${String(value || "").replace(/`/g, "")}\``;
233
+ const asInlineCode = (value) =>
234
+ `\`${String(value || "").replace(/`/g, "")}\``;
194
235
 
195
236
  const notifyAutoRepairOutcome = async ({
196
237
  source,
@@ -225,11 +266,15 @@ const createWatchdog = ({
225
266
  const hasAutoRepair = typeof autoRepair === "boolean";
226
267
  const hasNotificationsEnabled = typeof notificationsEnabled === "boolean";
227
268
  if (!hasAutoRepair && !hasNotificationsEnabled) {
228
- throw new Error("Expected autoRepair and/or notificationsEnabled boolean");
269
+ throw new Error(
270
+ "Expected autoRepair and/or notificationsEnabled boolean",
271
+ );
229
272
  }
230
273
  const envVars = readEnvFile();
231
274
  if (hasAutoRepair) {
232
- const existingIdx = envVars.findIndex((item) => item.key === "WATCHDOG_AUTO_REPAIR");
275
+ const existingIdx = envVars.findIndex(
276
+ (item) => item.key === "WATCHDOG_AUTO_REPAIR",
277
+ );
233
278
  const nextValue = autoRepair ? "true" : "false";
234
279
  if (existingIdx >= 0) {
235
280
  envVars[existingIdx] = { ...envVars[existingIdx], value: nextValue };
@@ -254,7 +299,9 @@ const createWatchdog = ({
254
299
  writeEnvFile(envVars);
255
300
  reloadEnv();
256
301
  state.autoRepair = isTruthy(process.env.WATCHDOG_AUTO_REPAIR);
257
- state.notificationsDisabled = isTruthy(process.env.WATCHDOG_NOTIFICATIONS_DISABLED);
302
+ state.notificationsDisabled = isTruthy(
303
+ process.env.WATCHDOG_NOTIFICATIONS_DISABLED,
304
+ );
258
305
  return getSettings();
259
306
  };
260
307
 
@@ -277,7 +324,13 @@ const createWatchdog = ({
277
324
  const child = launchGatewayProcess();
278
325
  launchedGateway = !!child;
279
326
  if (launchedGateway) {
280
- logEvent("restart", "repair", "ok", { pid: child.pid }, correlationId);
327
+ logEvent(
328
+ "restart",
329
+ "repair",
330
+ "ok",
331
+ { pid: child.pid },
332
+ correlationId,
333
+ );
281
334
  } else {
282
335
  logEvent(
283
336
  "restart",
@@ -288,7 +341,13 @@ const createWatchdog = ({
288
341
  );
289
342
  }
290
343
  } catch (err) {
291
- logEvent("restart", "repair", "failed", { error: err.message }, correlationId);
344
+ logEvent(
345
+ "restart",
346
+ "repair",
347
+ "failed",
348
+ { error: err.message },
349
+ correlationId,
350
+ );
292
351
  }
293
352
  state.health = "unknown";
294
353
  state.lifecycle = "running";
@@ -344,7 +403,10 @@ const createWatchdog = ({
344
403
  source = "health_timer",
345
404
  allowAutoRepair = true,
346
405
  } = {}) => {
347
- if (state.expectedRestartInProgress && Date.now() >= state.expectedRestartUntilMs) {
406
+ if (
407
+ state.expectedRestartInProgress &&
408
+ Date.now() >= state.expectedRestartUntilMs
409
+ ) {
348
410
  clearExpectedRestartWindow();
349
411
  }
350
412
  if (state.operationInProgress && !allowDuringOperation) return false;
@@ -358,7 +420,8 @@ const createWatchdog = ({
358
420
  state.gatewayStartedAt != null &&
359
421
  state.gatewayStartedAt !== gatewayStartedAtAtStart;
360
422
  const restartWindowActive =
361
- state.expectedRestartInProgress && Date.now() < state.expectedRestartUntilMs;
423
+ state.expectedRestartInProgress &&
424
+ Date.now() < state.expectedRestartUntilMs;
362
425
  if (staleAfterRestart) {
363
426
  return false;
364
427
  }
@@ -370,7 +433,8 @@ const createWatchdog = ({
370
433
  clearExpectedRestartWindow();
371
434
  state.health = "healthy";
372
435
  state.lifecycle = "running";
373
- if (!state.uptimeStartedAt || wasUnhealthy) state.uptimeStartedAt = Date.now();
436
+ if (!state.uptimeStartedAt || wasUnhealthy)
437
+ state.uptimeStartedAt = Date.now();
374
438
  state.repairAttempts = 0;
375
439
  state.crashRecoveryActive = false;
376
440
  if (recoveredFromCrashLoop) {
@@ -384,6 +448,13 @@ const createWatchdog = ({
384
448
  },
385
449
  correlationId,
386
450
  );
451
+ await notify(
452
+ [
453
+ "🐺 *AlphaClaw Watchdog*",
454
+ withViewLogsSuffix("🟢 Gateway healthy again"),
455
+ ].join("\n"),
456
+ correlationId,
457
+ );
387
458
  }
388
459
  if (state.pendingRecoveryNoticeSource) {
389
460
  const recoverySource = state.pendingRecoveryNoticeSource;
@@ -392,12 +463,17 @@ const createWatchdog = ({
392
463
  [
393
464
  "🐺 *AlphaClaw Watchdog*",
394
465
  withViewLogsSuffix("🟢 Gateway healthy again"),
395
- `Trigger: ${asInlineCode(recoverySource)}`,
396
466
  ].join("\n"),
397
467
  correlationId,
398
468
  );
399
469
  }
400
- logEvent("health_check", source, "ok", parsed.details || result, correlationId);
470
+ logEvent(
471
+ "health_check",
472
+ source,
473
+ "ok",
474
+ parsed.details || result,
475
+ correlationId,
476
+ );
401
477
  return true;
402
478
  }
403
479
  if (restartWindowActive) {
@@ -445,7 +521,10 @@ const createWatchdog = ({
445
521
 
446
522
  if (state.health === "unknown" && state.lifecycle === "running") {
447
523
  state.startupConsecutiveHealthFailures += 1;
448
- if (state.startupConsecutiveHealthFailures < kWatchdogStartupFailureThreshold) {
524
+ if (
525
+ state.startupConsecutiveHealthFailures <
526
+ kWatchdogStartupFailureThreshold
527
+ ) {
449
528
  logEvent(
450
529
  "health_check",
451
530
  source,
@@ -486,7 +565,13 @@ const createWatchdog = ({
486
565
  try {
487
566
  const child = launchGatewayProcess();
488
567
  if (child) {
489
- logEvent("restart", "exit_event", "ok", { pid: child.pid }, correlationId);
568
+ logEvent(
569
+ "restart",
570
+ "exit_event",
571
+ "ok",
572
+ { pid: child.pid },
573
+ correlationId,
574
+ );
490
575
  } else {
491
576
  logEvent(
492
577
  "restart",
@@ -497,18 +582,30 @@ const createWatchdog = ({
497
582
  );
498
583
  }
499
584
  } catch (err) {
500
- logEvent("restart", "exit_event", "failed", { error: err.message }, correlationId);
585
+ logEvent(
586
+ "restart",
587
+ "exit_event",
588
+ "failed",
589
+ { error: err.message },
590
+ correlationId,
591
+ );
501
592
  } finally {
502
593
  state.operationInProgress = false;
503
594
  }
504
595
  };
505
596
 
506
- const onGatewayExit = ({ code, signal, expectedExit = false, stderrTail = [] } = {}) => {
597
+ const onGatewayExit = ({
598
+ code,
599
+ signal,
600
+ expectedExit = false,
601
+ stderrTail = [],
602
+ } = {}) => {
507
603
  const correlationId = createCorrelationId();
508
604
  clearDegradedHealthCheckTimer();
509
605
  if (expectedExit && (code == null || code === 0)) {
510
606
  state.lifecycle = "restarting";
511
607
  state.health = "unknown";
608
+ state.uptimeStartedAt = null;
512
609
  state.crashRecoveryActive = false;
513
610
  markExpectedRestartWindow();
514
611
  startBootstrapHealthChecks();
@@ -521,9 +618,33 @@ const createWatchdog = ({
521
618
  );
522
619
  return;
523
620
  }
621
+ if (isDuplicateGatewayLaunchExit({ code, stderrTail })) {
622
+ state.lifecycle = "running";
623
+ state.health = "unknown";
624
+ state.crashRecoveryActive = false;
625
+ state.startupConsecutiveHealthFailures = 0;
626
+ if (!state.uptimeStartedAt) {
627
+ state.uptimeStartedAt = Date.now();
628
+ }
629
+ startBootstrapHealthChecks();
630
+ logEvent(
631
+ "restart",
632
+ "exit_event",
633
+ "ok",
634
+ {
635
+ duplicateLaunch: true,
636
+ code: code ?? null,
637
+ signal: signal ?? null,
638
+ stderrTail,
639
+ },
640
+ correlationId,
641
+ );
642
+ return;
643
+ }
524
644
 
525
645
  state.lifecycle = "crashed";
526
646
  state.health = "unhealthy";
647
+ state.uptimeStartedAt = null;
527
648
  state.crashRecoveryActive = true;
528
649
  state.crashTimestamps.push(Date.now());
529
650
  trimCrashWindow();
@@ -557,7 +678,9 @@ const createWatchdog = ({
557
678
  ),
558
679
  `Crashes: ${state.crashTimestamps.length} in the last ${Math.floor(kWatchdogCrashLoopWindowMs / 1000)}s`,
559
680
  `Last exit code: ${code ?? "unknown"}`,
560
- ...(state.autoRepair ? [] : ["Auto-restart paused; manual action required."]),
681
+ ...(state.autoRepair
682
+ ? []
683
+ : ["Auto-restart paused; manual action required."]),
561
684
  ].join("\n"),
562
685
  correlationId,
563
686
  );
@@ -591,6 +714,7 @@ const createWatchdog = ({
591
714
  clearDegradedHealthCheckTimer();
592
715
  state.lifecycle = "restarting";
593
716
  state.health = "unknown";
717
+ state.uptimeStartedAt = null;
594
718
  state.startupConsecutiveHealthFailures = 0;
595
719
  state.crashRecoveryActive = false;
596
720
  markExpectedRestartWindow();
@@ -612,7 +736,6 @@ const createWatchdog = ({
612
736
  state.lifecycle = "running";
613
737
  state.health = "unknown";
614
738
  state.startupConsecutiveHealthFailures = 0;
615
- state.uptimeStartedAt = Date.now();
616
739
  state.gatewayStartedAt = Date.now();
617
740
  startBootstrapHealthChecks();
618
741
  };
@@ -628,6 +751,7 @@ const createWatchdog = ({
628
751
  healthTimer = null;
629
752
  }
630
753
  state.lifecycle = "stopped";
754
+ state.uptimeStartedAt = null;
631
755
  state.startupConsecutiveHealthFailures = 0;
632
756
  };
633
757
 
@@ -99,11 +99,16 @@ const normalizeDestination = (destination = null) => {
99
99
  if (!destination || typeof destination !== "object") return null;
100
100
  const channel = String(destination?.channel || "").trim();
101
101
  const to = String(destination?.to || "").trim();
102
+ const agentId = String(destination?.agentId || "").trim();
102
103
  if (!channel && !to) return null;
103
104
  if (!channel || !to) {
104
105
  throw new Error("destination.channel and destination.to are required");
105
106
  }
106
- return { channel, to };
107
+ return {
108
+ channel,
109
+ to,
110
+ ...(agentId ? { agentId } : {}),
111
+ };
107
112
  };
108
113
 
109
114
  const resolveTransformPathFromMapping = (name, mapping) => {
@@ -137,8 +142,7 @@ const normalizeMappingTransformModules = (mappings) => {
137
142
  return changed;
138
143
  };
139
144
 
140
- const buildDefaultTransformSource = (name, destination = null) => {
141
- const normalizedDestination = normalizeDestination(destination);
145
+ const buildDefaultTransformSource = (name) => {
142
146
  return [
143
147
  "export default async function transform(payload, context) {",
144
148
  " const data = payload.payload || payload;",
@@ -146,12 +150,6 @@ const buildDefaultTransformSource = (name, destination = null) => {
146
150
  " message: data.message,",
147
151
  ` name: data.name || "${name}",`,
148
152
  ' wakeMode: data.wakeMode || "now",',
149
- ...(normalizedDestination
150
- ? [
151
- ` channel: ${JSON.stringify(normalizedDestination.channel)},`,
152
- ` to: ${JSON.stringify(normalizedDestination.to)},`,
153
- ]
154
- : []),
155
153
  " };",
156
154
  "}",
157
155
  "",
@@ -164,6 +162,7 @@ const ensureWebhookTransform = ({
164
162
  name,
165
163
  source = "",
166
164
  destination = null,
165
+ forceWrite = false,
167
166
  }) => {
168
167
  const webhookName = validateWebhookName(name);
169
168
  const transformAbsolutePath = getTransformAbsolutePath(
@@ -171,14 +170,14 @@ const ensureWebhookTransform = ({
171
170
  webhookName,
172
171
  );
173
172
  fs.mkdirSync(path.dirname(transformAbsolutePath), { recursive: true });
174
- if (fs.existsSync(transformAbsolutePath)) {
173
+ if (fs.existsSync(transformAbsolutePath) && !forceWrite) {
175
174
  return { changed: false, path: transformAbsolutePath };
176
175
  }
177
176
  fs.writeFileSync(
178
177
  transformAbsolutePath,
179
178
  String(source || "").trim()
180
179
  ? `${String(source).replace(/\s+$/, "")}\n`
181
- : buildDefaultTransformSource(webhookName, destination),
180
+ : buildDefaultTransformSource(webhookName),
182
181
  );
183
182
  return { changed: true, path: transformAbsolutePath };
184
183
  };
@@ -235,6 +234,27 @@ const ensureWebhookMapping = ({ cfg, name, mapping = {} }) => {
235
234
  };
236
235
  };
237
236
 
237
+ const resolveDefaultAgentId = (cfg) => {
238
+ const agents = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
239
+ const explicitDefault = agents.find((entry) => !!entry?.default);
240
+ const defaultId = String(explicitDefault?.id || "").trim();
241
+ if (defaultId) return defaultId;
242
+ const firstId = String(agents[0]?.id || "").trim();
243
+ return firstId || "main";
244
+ };
245
+
246
+ const resolveWebhookAgentId = ({ cfg, requestedAgentId = "" }) => {
247
+ const normalizedRequested = String(requestedAgentId || "").trim();
248
+ const agents = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
249
+ if (
250
+ normalizedRequested &&
251
+ agents.some((entry) => String(entry?.id || "").trim() === normalizedRequested)
252
+ ) {
253
+ return normalizedRequested;
254
+ }
255
+ return resolveDefaultAgentId(cfg);
256
+ };
257
+
238
258
  const listManagedWebhooksFromConfig = ({ cfg }) => {
239
259
  const presets = Array.isArray(cfg?.hooks?.presets) ? cfg.hooks.presets : [];
240
260
  return kManagedWebhookConfigs
@@ -289,6 +309,10 @@ const listWebhooks = ({ fs, constants }) => {
289
309
  path: `/hooks/${name}`,
290
310
  transformPath,
291
311
  transformExists: fs.existsSync(transformAbsolutePath),
312
+ deliver: Boolean(mapping?.deliver),
313
+ channel: String(mapping?.channel || "").trim(),
314
+ to: String(mapping?.to || "").trim(),
315
+ agentId: String(mapping?.agentId || "").trim(),
292
316
  managed: Boolean(managed),
293
317
  managedReason: managed?.managedReason || "",
294
318
  };
@@ -332,6 +356,7 @@ const createWebhook = ({
332
356
  mapping = {},
333
357
  transformSource = "",
334
358
  destination = null,
359
+ overwriteTransform = false,
335
360
  }) => {
336
361
  const webhookName = validateWebhookName(name);
337
362
  const normalizedDestination = normalizeDestination(destination);
@@ -346,10 +371,32 @@ const createWebhook = ({
346
371
  if (exists && !upsert) {
347
372
  throw new Error(`Webhook "${webhookName}" already exists`);
348
373
  }
374
+ const agentId = resolveWebhookAgentId({
375
+ cfg,
376
+ requestedAgentId:
377
+ String(mapping?.agentId || "").trim() ||
378
+ String(normalizedDestination?.agentId || "").trim(),
379
+ });
380
+ const resolvedMapping = {
381
+ ...mapping,
382
+ deliver: true,
383
+ channel:
384
+ String(mapping?.channel || "").trim() ||
385
+ String(normalizedDestination?.channel || "").trim() ||
386
+ "last",
387
+ ...(String(mapping?.to || "").trim() || String(normalizedDestination?.to || "").trim()
388
+ ? {
389
+ to:
390
+ String(mapping?.to || "").trim() ||
391
+ String(normalizedDestination?.to || "").trim(),
392
+ }
393
+ : {}),
394
+ agentId,
395
+ };
349
396
  const ensuredMapping = ensureWebhookMapping({
350
397
  cfg,
351
398
  name: webhookName,
352
- mapping,
399
+ mapping: resolvedMapping,
353
400
  });
354
401
  const ensuredTransform = ensureWebhookTransform({
355
402
  fs,
@@ -357,6 +404,7 @@ const createWebhook = ({
357
404
  name: webhookName,
358
405
  source: transformSource,
359
406
  destination: normalizedDestination,
407
+ forceWrite: overwriteTransform,
360
408
  });
361
409
  if (ensuredMapping.changed || ensuredTransform.changed || !exists) {
362
410
  writeConfig({ fs, configPath, cfg });