@brantrusnak/openclaw-omadeus 1.0.2 → 1.0.4

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 (41) hide show
  1. package/README.md +15 -61
  2. package/dist/_virtual/_rolldown/runtime.js +4 -0
  3. package/dist/api.js +5 -0
  4. package/dist/index.js +14 -0
  5. package/dist/runtime-api.js +15 -0
  6. package/dist/setup-entry.js +7 -0
  7. package/dist/src/allowed-reaction-emojis.js +21 -0
  8. package/dist/src/api/auth.api.js +118 -0
  9. package/dist/src/api/channel.api.js +23 -0
  10. package/dist/src/api/message.api.js +76 -0
  11. package/dist/src/api/nugget.api.js +127 -0
  12. package/dist/src/auth.js +30 -0
  13. package/dist/src/channel.js +626 -0
  14. package/dist/src/config.js +52 -0
  15. package/dist/src/defaults.js +5 -0
  16. package/dist/src/inbound-policy.js +205 -0
  17. package/dist/src/inbound.js +97 -0
  18. package/dist/src/member-resolve.js +53 -0
  19. package/dist/src/message-handler.js +262 -0
  20. package/dist/src/nugget-lookup.js +140 -0
  21. package/dist/src/onboarding.js +357 -0
  22. package/dist/src/outbound.js +17 -0
  23. package/dist/src/reply-dispatcher.js +46 -0
  24. package/dist/src/runtime.js +5 -0
  25. package/dist/src/setup-core.js +46 -0
  26. package/dist/src/setup-surface.js +2 -0
  27. package/dist/src/socket/dolphin.socket.js +18 -0
  28. package/dist/src/socket/jaguar.socket.js +22 -0
  29. package/dist/src/socket/socket.js +153 -0
  30. package/dist/src/store.js +13 -0
  31. package/dist/src/token.js +84 -0
  32. package/dist/src/types.js +15 -0
  33. package/dist/src/utils/http.util.js +43 -0
  34. package/dist/src/utils/jwt.util.js +15 -0
  35. package/package.json +10 -3
  36. package/src/api/auth.api.ts +27 -7
  37. package/src/channel.ts +127 -238
  38. package/src/member-resolve.ts +1 -1
  39. package/src/onboarding.ts +117 -163
  40. package/src/setup-core.ts +10 -1
  41. package/src/socket/socket.ts +24 -11
package/src/onboarding.ts CHANGED
@@ -8,29 +8,16 @@ import { listMemberChannelViews } from "./api/channel.api.js";
8
8
  import { authenticate } from "./auth.js";
9
9
  import { getOmadeusChannelConfig, resolveOmadeusAccount } from "./config.js";
10
10
  import { OMADEUS_CAS_URL, OMADEUS_MAESTRO_URL } from "./defaults.js";
11
+ import { formatMemberLabel } from "./member-resolve.js";
11
12
  import type {
12
13
  OmadeusChannelConfig,
13
14
  OmadeusChannelView,
14
15
  OmadeusInboundEntityKind,
15
16
  OmadeusOrganizationMember,
16
17
  } from "./types.js";
18
+ import { OMADEUS_INBOUND_ENTITY_KINDS } from "./types.js";
17
19
 
18
20
  const channel = "omadeus" as const;
19
- const DONE = "__done__";
20
- const ALL_ENTITY_KINDS: OmadeusInboundEntityKind[] = [
21
- "task",
22
- "nugget",
23
- "project",
24
- "release",
25
- "sprint",
26
- "summary",
27
- "client",
28
- "folder",
29
- ];
30
-
31
- type CoreConfig = OpenClawConfig & {
32
- channels?: { omadeus?: OmadeusChannelConfig };
33
- };
34
21
 
35
22
  type SelectOption = {
36
23
  value: string;
@@ -38,23 +25,20 @@ type SelectOption = {
38
25
  hint?: string;
39
26
  };
40
27
 
41
- type MultiSelectPrompter = {
42
- multiSelect?: (args: {
43
- message: string;
44
- options: SelectOption[];
45
- initialValues?: string[];
46
- initialValue?: string[];
47
- }) => Promise<string[]>;
48
- multiselect?: (args: {
49
- message: string;
50
- options: SelectOption[];
51
- initialValues?: string[];
52
- initialValue?: string[];
53
- }) => Promise<string[]>;
54
- };
55
-
56
- function getOmadeusSection(cfg: OpenClawConfig): OmadeusChannelConfig | undefined {
57
- return getOmadeusChannelConfig(cfg as CoreConfig);
28
+ function formatAuthError(err: unknown): string {
29
+ if (!(err instanceof Error)) return String(err);
30
+ const parts = [err.message];
31
+ const { cause } = err;
32
+ if (cause instanceof Error) {
33
+ parts.push(cause.message);
34
+ const code = (cause as Error & { code?: unknown }).code;
35
+ if (typeof code === "string" && code) {
36
+ parts.push(`(${code})`);
37
+ }
38
+ } else if (typeof cause === "string" && cause.trim()) {
39
+ parts.push(cause);
40
+ }
41
+ return parts.join(" — ");
58
42
  }
59
43
 
60
44
  async function noteOmadeusAuthHelp(prompter: WizardPrompter): Promise<void> {
@@ -80,35 +64,32 @@ async function promptOrganizationId(params: {
80
64
  }): Promise<number> {
81
65
  const { prompter, maestroUrl, email, existing } = params;
82
66
 
83
- // Try to list organizations from the API
84
- if (maestroUrl && email) {
85
- try {
86
- const orgs = await listOrganizations({ maestroUrl, email });
87
- if (orgs.length > 0) {
88
- if (orgs.length === 1) {
89
- await prompter.note(
90
- `Found organization: ${orgs[0]!.title} (${orgs[0]!.id})`,
91
- "Omadeus organization",
92
- );
93
- return orgs[0]!.id;
94
- }
95
- const choice = await prompter.select({
96
- message: "Select organization",
97
- options: orgs.map((org) => ({
98
- value: String(org.id),
99
- label: `${org.title} (${org.membersCount} members)`,
100
- hint: `ID: ${org.id}`,
101
- })),
102
- initialValue: existing ? String(existing) : String(orgs[0]!.id),
103
- });
104
- return Number(choice);
67
+ try {
68
+ const orgs = await listOrganizations({ maestroUrl, email });
69
+ if (orgs.length > 0) {
70
+ if (orgs.length === 1) {
71
+ await prompter.note(
72
+ `Found organization: ${orgs[0]!.title} (${orgs[0]!.id})`,
73
+ "Omadeus organization",
74
+ );
75
+ return orgs[0]!.id;
105
76
  }
106
- } catch {
107
- await prompter.note(
108
- "Could not fetch organizations from the API. Enter the ID manually.",
109
- "Omadeus organization",
110
- );
77
+ const choice = await prompter.select({
78
+ message: "Select organization",
79
+ options: orgs.map((org) => ({
80
+ value: String(org.id),
81
+ label: `${org.title} (${org.membersCount} members)`,
82
+ hint: `ID: ${org.id}`,
83
+ })),
84
+ initialValue: existing ? String(existing) : String(orgs[0]!.id),
85
+ });
86
+ return Number(choice);
111
87
  }
88
+ } catch {
89
+ await prompter.note(
90
+ "Could not fetch organizations from the API. Enter the ID manually.",
91
+ "Omadeus organization",
92
+ );
112
93
  }
113
94
 
114
95
  const raw = await prompter.text({
@@ -140,8 +121,21 @@ async function promptChannelSelection(params: {
140
121
  take: 100,
141
122
  });
142
123
  if (channels.length === 0) {
143
- throw new Error("No channels found for this account.");
124
+ await prompter.note(
125
+ "No channels found for this account. Channel listening will stay disabled.",
126
+ "Omadeus channels",
127
+ );
128
+ return [];
129
+ }
130
+
131
+ const listenToChannels = await prompter.confirm({
132
+ message: "Listen for messages in Omadeus channels?",
133
+ initialValue: (existingChannelViewIds?.length ?? 0) > 0,
134
+ });
135
+ if (!listenToChannels) {
136
+ return [];
144
137
  }
138
+
145
139
  const selected = await promptMultiSelect({
146
140
  prompter,
147
141
  message: "Which channels should OpenClaw listen to?",
@@ -155,27 +149,9 @@ async function promptChannelSelection(params: {
155
149
  initialValues:
156
150
  existingChannelViewIds && existingChannelViewIds.length > 0
157
151
  ? existingChannelViewIds.map(String)
158
- : [String(channels[0]!.id)],
152
+ : undefined,
159
153
  });
160
- const chosen = channels.filter((item) => selected.includes(String(item.id)));
161
- if (chosen.length === 0) {
162
- throw new Error("At least one channel must be selected.");
163
- }
164
- return chosen;
165
- }
166
-
167
- function memberLabel(member: OmadeusOrganizationMember): string {
168
- const fullName = `${member.firstName ?? ""} ${member.lastName ?? ""}`.trim();
169
- if (member.title?.trim()) {
170
- return member.title.trim();
171
- }
172
- if (fullName) {
173
- return fullName;
174
- }
175
- if (member.email?.trim()) {
176
- return member.email.trim();
177
- }
178
- return `Member ${member.referenceId}`;
154
+ return channels.filter((item) => selected.includes(String(item.id)));
179
155
  }
180
156
 
181
157
  function memberHint(member: OmadeusOrganizationMember): string | undefined {
@@ -190,40 +166,11 @@ async function promptMultiSelect(params: {
190
166
  options: SelectOption[];
191
167
  initialValues?: string[];
192
168
  }): Promise<string[]> {
193
- const multi = params.prompter as unknown as MultiSelectPrompter;
194
- const runMulti = multi.multiSelect ?? multi.multiselect;
195
- if (runMulti) {
196
- return runMulti({
197
- message: params.message,
198
- options: params.options,
199
- initialValues: params.initialValues,
200
- initialValue: params.initialValues,
201
- });
202
- }
203
-
204
- const selected = new Set(params.initialValues ?? []);
205
- while (true) {
206
- const next = await params.prompter.select({
207
- message: `${params.message} (${selected.size} selected)`,
208
- options: [
209
- { value: DONE, label: selected.size > 0 ? "Done" : "Done (select none)" },
210
- ...params.options.map((option) => ({
211
- ...option,
212
- label: selected.has(option.value) ? `[selected] ${option.label}` : option.label,
213
- })),
214
- ],
215
- initialValue: DONE,
216
- });
217
- const value = String(next);
218
- if (value === DONE) {
219
- return [...selected];
220
- }
221
- if (selected.has(value)) {
222
- selected.delete(value);
223
- } else {
224
- selected.add(value);
225
- }
226
- }
169
+ return params.prompter.multiselect({
170
+ message: params.message,
171
+ options: params.options,
172
+ initialValues: params.initialValues,
173
+ });
227
174
  }
228
175
 
229
176
  async function loadSelectableMembers(params: {
@@ -241,13 +188,13 @@ async function loadSelectableMembers(params: {
241
188
  })
242
189
  )
243
190
  .filter((member) => member.isSystem !== true && !excluded.has(member.referenceId))
244
- .sort((a, b) => memberLabel(a).localeCompare(memberLabel(b)));
191
+ .sort((a, b) => formatMemberLabel(a).localeCompare(formatMemberLabel(b)));
245
192
  }
246
193
 
247
194
  function memberOptions(members: OmadeusOrganizationMember[]): SelectOption[] {
248
195
  return members.map((member) => ({
249
196
  value: String(member.referenceId),
250
- label: memberLabel(member),
197
+ label: formatMemberLabel(member),
251
198
  hint: memberHint(member),
252
199
  }));
253
200
  }
@@ -258,6 +205,27 @@ function readReferenceIds(values: string[]): number[] {
258
205
  .filter((value) => Number.isInteger(value) && value > 0);
259
206
  }
260
207
 
208
+ async function promptCredentials(
209
+ prompter: WizardPrompter,
210
+ existing: { email?: string; password?: string },
211
+ ): Promise<{ email: string; password: string }> {
212
+ const email = String(
213
+ await prompter.text({
214
+ message: "Omadeus username (email)",
215
+ initialValue: existing.email,
216
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
217
+ }),
218
+ ).trim();
219
+ const password = String(
220
+ await prompter.text({
221
+ message: "Omadeus password",
222
+ sensitive: true,
223
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
224
+ }),
225
+ ).trim();
226
+ return { email, password };
227
+ }
228
+
261
229
  async function promptSenderAllowlist(params: {
262
230
  prompter: WizardPrompter;
263
231
  message: string;
@@ -277,7 +245,7 @@ async function promptSenderAllowlist(params: {
277
245
  ],
278
246
  initialValue: existingReferenceIds && existingReferenceIds.length > 0 ? "specific" : "all",
279
247
  });
280
- if (String(mode) === "all") {
248
+ if (mode === "all") {
281
249
  return undefined;
282
250
  }
283
251
 
@@ -297,16 +265,17 @@ async function promptEntityKindSelection(params: {
297
265
  const selected = await promptMultiSelect({
298
266
  prompter: params.prompter,
299
267
  message: "Which entity room types should OpenClaw listen to?",
300
- options: ALL_ENTITY_KINDS.map((kind) => ({
268
+ options: OMADEUS_INBOUND_ENTITY_KINDS.map((kind) => ({
301
269
  value: kind,
302
270
  label: kind,
303
271
  })),
304
272
  initialValues:
305
273
  params.existingKinds && params.existingKinds.length > 0
306
274
  ? params.existingKinds
307
- : ALL_ENTITY_KINDS,
275
+ : [...OMADEUS_INBOUND_ENTITY_KINDS],
308
276
  });
309
- return ALL_ENTITY_KINDS.filter((kind) => selected.includes(kind));
277
+ const selectedSet = new Set(selected);
278
+ return OMADEUS_INBOUND_ENTITY_KINDS.filter((kind) => selectedSet.has(kind));
310
279
  }
311
280
 
312
281
  export const omadeusSetupWizard: ChannelSetupWizard = {
@@ -343,7 +312,7 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
343
312
  credentials: [],
344
313
  finalize: async ({ cfg, prompter }) => {
345
314
  const account = resolveOmadeusAccount({ cfg });
346
- const section = getOmadeusSection(cfg) ?? {};
315
+ const section = getOmadeusChannelConfig(cfg) ?? {};
347
316
  let next = cfg;
348
317
 
349
318
  if (account.credentialSource === "none") {
@@ -356,21 +325,10 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
356
325
  const casUrl = OMADEUS_CAS_URL;
357
326
  const maestroUrl = OMADEUS_MAESTRO_URL;
358
327
 
359
- let email = String(
360
- await prompter.text({
361
- message: "Omadeus username (email)",
362
- initialValue: section.email ?? envEmail,
363
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
364
- }),
365
- ).trim();
366
-
367
- let password = String(
368
- await prompter.text({
369
- message: "Omadeus password",
370
- initialValue: section.password ?? envPassword,
371
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
372
- }),
373
- ).trim();
328
+ let { email, password } = await promptCredentials(prompter, {
329
+ email: section.email ?? envEmail,
330
+ password: section.password ?? envPassword,
331
+ });
374
332
 
375
333
  const organizationId = await promptOrganizationId({
376
334
  prompter,
@@ -379,7 +337,6 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
379
337
  existing: section.organizationId,
380
338
  });
381
339
 
382
- // Verify the full auth flow before saving
383
340
  let sessionToken: string | undefined;
384
341
  let selfReferenceId: number | undefined;
385
342
  while (true) {
@@ -396,8 +353,10 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
396
353
  await prompter.note(`Authenticated as ${payload.email}`, "Omadeus authentication");
397
354
  break;
398
355
  } catch (err) {
399
- const msg = err instanceof Error ? err.message : String(err);
400
- await prompter.note(`Authentication failed: ${msg}`, "Omadeus authentication");
356
+ await prompter.note(
357
+ `Authentication failed: ${formatAuthError(err)}`,
358
+ "Omadeus authentication",
359
+ );
401
360
  const retry = await prompter.confirm({
402
361
  message: "Re-enter email/password and try again?",
403
362
  initialValue: true,
@@ -409,20 +368,7 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
409
368
  );
410
369
  break;
411
370
  }
412
- email = String(
413
- await prompter.text({
414
- message: "Omadeus email",
415
- initialValue: email,
416
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
417
- }),
418
- ).trim();
419
- password = String(
420
- await prompter.text({
421
- message: "Omadeus password",
422
- initialValue: password,
423
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
424
- }),
425
- ).trim();
371
+ ({ email, password } = await promptCredentials(prompter, { email, password }));
426
372
  }
427
373
  }
428
374
 
@@ -457,12 +403,15 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
457
403
  existingChannelViewIds: existingInbound?.channels?.allowedChannelViewIds,
458
404
  });
459
405
 
460
- const channelSenderIds = await promptSenderAllowlist({
461
- prompter,
462
- message: "Which users can trigger OpenClaw from allowed channels?",
463
- members,
464
- existingReferenceIds: existingInbound?.channels?.allowedSenderReferenceIds,
465
- });
406
+ const channelSenderIds =
407
+ selectedChannels.length > 0
408
+ ? await promptSenderAllowlist({
409
+ prompter,
410
+ message: "Which users can trigger OpenClaw from allowed channels?",
411
+ members,
412
+ existingReferenceIds: existingInbound?.channels?.allowedSenderReferenceIds,
413
+ })
414
+ : undefined;
466
415
 
467
416
  const entityKinds = await promptEntityKindSelection({
468
417
  prompter,
@@ -495,11 +444,16 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
495
444
  const entityKindSummary =
496
445
  entityKinds.length > 0 ? entityKinds.join(", ") : "none (entity rooms disabled)";
497
446
 
447
+ const channelSummary =
448
+ selectedChannels.length > 0
449
+ ? `- Channels "${channelTitles}": rooms ${channelRoomIds.join(", ") || "(no room ids)"} from ${senderSummary(channelSenderIds)}; @mention not required in those rooms.`
450
+ : "- Channels: disabled (none selected).";
451
+
498
452
  await prompter.note(
499
453
  [
500
454
  `Inbound policy (Jaguar chat):`,
501
455
  `- Direct messages: enabled for ${senderSummary(directSenderIds)} (no @mention required).`,
502
- `- Channels "${channelTitles}": rooms ${channelRoomIds.join(", ") || "(no room ids)"} from ${senderSummary(channelSenderIds)}; @mention not required in those rooms.`,
456
+ channelSummary,
503
457
  `- Entity rooms (${entityKindSummary}): ${senderSummary(entitySenderIds)}; @mention required.`,
504
458
  ].join("\n"),
505
459
  "Omadeus inbound policy",
@@ -516,7 +470,7 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
516
470
  email,
517
471
  password,
518
472
  organizationId,
519
- ...(sessionToken ? { sessionToken } : {}),
473
+ sessionToken,
520
474
  inbound: {
521
475
  version: 1,
522
476
  direct: {
@@ -525,7 +479,7 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
525
479
  requireMention: "never",
526
480
  },
527
481
  channels: {
528
- enabled: true,
482
+ enabled: selectedChannels.length > 0,
529
483
  allowedRoomIds: channelRoomIds,
530
484
  allowedChannelViewIds: channelViewIds,
531
485
  ...(channelSenderIds ? { allowedSenderReferenceIds: channelSenderIds } : {}),
@@ -548,7 +502,7 @@ export const omadeusSetupWizard: ChannelSetupWizard = {
548
502
  ...cfg,
549
503
  channels: {
550
504
  ...cfg.channels,
551
- omadeus: { ...getOmadeusSection(cfg), enabled: false },
505
+ omadeus: { ...getOmadeusChannelConfig(cfg), enabled: false },
552
506
  },
553
507
  }),
554
508
  };
package/src/setup-core.ts CHANGED
@@ -35,12 +35,21 @@ export const omadeusSetupAdapter: ChannelSetupAdapter = {
35
35
  const password = input.password?.trim() || undefined;
36
36
  const organizationId = readSetupNumberField(rawInput, "organizationId");
37
37
 
38
+ const channelsRecord = cfg.channels as Record<string, unknown> | undefined;
39
+ const omadeusExisting = channelsRecord?.["omadeus"];
40
+ const omadeusPrevious =
41
+ omadeusExisting !== null &&
42
+ typeof omadeusExisting === "object" &&
43
+ !Array.isArray(omadeusExisting)
44
+ ? (omadeusExisting as Record<string, unknown>)
45
+ : {};
46
+
38
47
  return {
39
48
  ...cfg,
40
49
  channels: {
41
50
  ...cfg.channels,
42
51
  omadeus: {
43
- ...(cfg.channels as Record<string, unknown>)?.["omadeus"],
52
+ ...omadeusPrevious,
44
53
  enabled: true,
45
54
  ...(casUrl ? { casUrl } : {}),
46
55
  ...(maestroUrl ? { maestroUrl } : {}),
@@ -31,10 +31,12 @@ const HEARTBEAT_MISSED_MAX = 5;
31
31
  const KEEP_ALIVE_CONTENT = "keep-alive";
32
32
  const KEEP_ALIVE_ACTION = "answer";
33
33
 
34
- function isKeepAliveMessage(data: Record<string, unknown>): boolean {
35
- const content = (data as { content?: unknown }).content;
36
- const payloadData = (data as { data?: unknown }).data;
37
- return content === KEEP_ALIVE_CONTENT || payloadData === KEEP_ALIVE_CONTENT;
34
+ function isServerKeepAlive(data: Record<string, unknown>): boolean {
35
+ return (data as { content?: unknown }).content === KEEP_ALIVE_CONTENT;
36
+ }
37
+
38
+ function isClientKeepAlive(data: Record<string, unknown>): boolean {
39
+ return (data as { data?: unknown }).data === KEEP_ALIVE_CONTENT;
38
40
  }
39
41
 
40
42
  export function createOmadeusSocketClient(opts: OmadeusSocketOptions): OmadeusSocketClient {
@@ -87,7 +89,7 @@ export function createOmadeusSocketClient(opts: OmadeusSocketOptions): OmadeusSo
87
89
  return;
88
90
  }
89
91
  heartbeatMissCount += 1;
90
- ws.send(JSON.stringify({ data: KEEP_ALIVE_CONTENT, action: KEEP_ALIVE_ACTION }));
92
+ sendKeepAliveFrame();
91
93
 
92
94
  if (heartbeatMissCount >= HEARTBEAT_MISSED_MAX) {
93
95
  log?.warn(
@@ -104,6 +106,12 @@ export function createOmadeusSocketClient(opts: OmadeusSocketOptions): OmadeusSo
104
106
  }, HEARTBEAT_INTERVAL_MS);
105
107
  }
106
108
 
109
+ function sendKeepAliveFrame() {
110
+ if (ws?.readyState === WebSocket.OPEN) {
111
+ ws.send(JSON.stringify({ data: KEEP_ALIVE_CONTENT, action: KEEP_ALIVE_ACTION }));
112
+ }
113
+ }
114
+
107
115
  function connect() {
108
116
  if (ws) {
109
117
  ws.removeAllListeners();
@@ -144,19 +152,24 @@ export function createOmadeusSocketClient(opts: OmadeusSocketOptions): OmadeusSo
144
152
  const data = JSON.parse(String(raw)) as Record<string, unknown>;
145
153
 
146
154
  const action = (data as { action?: unknown }).action;
147
- if (isKeepAliveMessage(data) && action === KEEP_ALIVE_ACTION) {
155
+ if (isServerKeepAlive(data) && action === KEEP_ALIVE_ACTION) {
148
156
  resetHeartbeat();
149
157
  return;
150
158
  }
151
159
 
152
- // If backend sends heartbeat pings, answer them immediately.
153
- if (isKeepAliveMessage(data) && action === "heartbeat") {
154
- if (ws?.readyState === WebSocket.OPEN) {
155
- ws.send(JSON.stringify({ data: KEEP_ALIVE_CONTENT, action: KEEP_ALIVE_ACTION }));
156
- }
160
+ if (isClientKeepAlive(data) && action === KEEP_ALIVE_ACTION) {
161
+ resetHeartbeat();
162
+ return;
163
+ }
164
+
165
+ // If backend sends a heartbeat ping, answer it immediately.
166
+ if (isServerKeepAlive(data) && action === "heartbeat") {
167
+ resetHeartbeat();
168
+ sendKeepAliveFrame();
157
169
  return;
158
170
  }
159
171
 
172
+ resetHeartbeat();
160
173
  onEvent?.(data);
161
174
  } catch {
162
175
  log?.warn(`${logPrefix} unparseable message: ${String(raw).slice(0, 200)}`);