@brantrusnak/openclaw-omadeus 1.0.1 → 1.0.3

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 (49) hide show
  1. package/README.md +5 -2
  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 +115 -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 +363 -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/openclaw.plugin.json +144 -28
  36. package/package.json +12 -7
  37. package/src/api/auth.api.ts +0 -29
  38. package/src/api/channel.api.ts +29 -0
  39. package/src/api/nugget.api.ts +81 -10
  40. package/src/channel.ts +136 -247
  41. package/src/inbound-policy.ts +250 -0
  42. package/src/inbound.ts +20 -0
  43. package/src/member-resolve.ts +84 -0
  44. package/src/message-handler.ts +99 -53
  45. package/src/nugget-lookup.ts +67 -4
  46. package/src/onboarding.ts +283 -200
  47. package/src/setup-core.ts +10 -1
  48. package/src/socket/socket.ts +24 -11
  49. package/src/types.ts +77 -7
package/src/channel.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  } from "./api/nugget.api.js";
29
29
  import { addMessageReaction, deleteMessage, editMessage } from "./api/message.api.js";
30
30
  import {
31
+ getOmadeusChannelConfig,
31
32
  listOmadeusAccountIds,
32
33
  resolveDefaultOmadeusAccountId,
33
34
  resolveOmadeusAccount,
@@ -44,30 +45,55 @@ import { createJaguarSocketClient, type JaguarSocketClient } from "./socket/jagu
44
45
  import { createTokenManager, type OmadeusTokenManager } from "./token.js";
45
46
  import type { ResolvedOmadeusAccount as Account } from "./types.js";
46
47
 
47
- // Gateway-scoped state for the running account
48
- let activeTokenManager: OmadeusTokenManager | null = null;
49
- let activeDolphin: DolphinSocketClient | null = null;
50
- let activeJaguar: JaguarSocketClient | null = null;
48
+ const CHANNEL_ID = "omadeus" as const;
49
+
50
+ const gatewayState: {
51
+ tokenManager: OmadeusTokenManager | null;
52
+ dolphin: DolphinSocketClient | null;
53
+ jaguar: JaguarSocketClient | null;
54
+ } = { tokenManager: null, dolphin: null, jaguar: null };
55
+
56
+ const isUnconfigured = (account: Account) => account.credentialSource === "none";
57
+
58
+ let lastPersistedToken: string | null = null;
51
59
 
52
60
  async function persistSessionToken(token: string): Promise<void> {
61
+ if (lastPersistedToken === token) return;
53
62
  const runtime = getOmadeusRuntime();
54
- const cfg = runtime.config.loadConfig();
55
- const section = ((cfg.channels as Record<string, unknown> | undefined)?.["omadeus"] ??
56
- {}) as Record<string, unknown>;
57
- if (section["sessionToken"] === token) {
63
+ const cfg = runtime.config.current() as OpenClawConfig;
64
+ const section = getOmadeusChannelConfig(cfg) ?? {};
65
+ if (section.sessionToken === token) {
66
+ lastPersistedToken = token;
58
67
  return;
59
68
  }
60
-
61
- await runtime.config.writeConfigFile({
62
- ...cfg,
63
- channels: {
64
- ...cfg.channels,
65
- omadeus: {
66
- ...section,
67
- sessionToken: token,
68
- },
69
+ await runtime.config.mutateConfigFile({
70
+ afterWrite: { mode: "auto" },
71
+ mutate: (draft) => {
72
+ draft.channels = {
73
+ ...(draft.channels ?? {}),
74
+ omadeus: {
75
+ ...(getOmadeusChannelConfig(draft) ?? {}),
76
+ sessionToken: token,
77
+ },
78
+ };
69
79
  },
70
- } as OpenClawConfig);
80
+ });
81
+ lastPersistedToken = token;
82
+ }
83
+
84
+ function actionError(text: string, error = text) {
85
+ return {
86
+ isError: true as const,
87
+ content: [{ type: "text" as const, text }],
88
+ details: { error },
89
+ };
90
+ }
91
+
92
+ function actionOk(payload: Record<string, unknown>) {
93
+ return {
94
+ content: [{ type: "text" as const, text: JSON.stringify({ ok: true, channel: CHANNEL_ID, ...payload }) }],
95
+ details: { ok: true, channel: CHANNEL_ID, ...payload },
96
+ };
71
97
  }
72
98
 
73
99
  const omadeusConfigAdapter = createTopLevelChannelConfigAdapter<Account>({
@@ -83,11 +109,7 @@ const omadeusConfigAdapter = createTopLevelChannelConfigAdapter<Account>({
83
109
  "password",
84
110
  "organizationId",
85
111
  "sessionToken",
86
- "selectedMemberReferenceId",
87
- "selectedChannelViewId",
88
- "selectedChannelTitle",
89
- "selectedChannelPrivateRoomId",
90
- "selectedChannelPublicRoomId",
112
+ "inbound",
91
113
  ],
92
114
  // Keep adapter contract satisfied even though Omadeus no longer uses DM allowlists.
93
115
  resolveAllowFrom: () => [],
@@ -188,10 +210,10 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
188
210
  meta: {
189
211
  id: "omadeus",
190
212
  label: "Omadeus",
191
- selectionLabel: "Omadeus (WebSocket)",
192
- docsPath: "/channels/omadeus",
193
- docsLabel: "omadeus",
194
- blurb: "Omadeus project management tasks, chat rooms, and sprints.",
213
+ selectionLabel: "Omadeus (API + WebSocket)",
214
+ docsPath: "",
215
+ docsLabel: "",
216
+ blurb: "AI-native project management that knows your role, speaks your language, and keeps your team in sync. No noise.",
195
217
  },
196
218
  capabilities: {
197
219
  chatTypes: ["direct", "group"],
@@ -205,6 +227,9 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
205
227
  messageToolHints: () => [
206
228
  "- Omadeus routing: **send** uses **room id** (`to` / `target`, e.g. `room:117947` or `117947`). **edit**, **delete**, **react** use the Jaguar **message** `id` (`messageId`, or the current inbound message from context).",
207
229
  "- Create Omadeus task/nugget: use `action=send` with params `{ op: \"create_task\"|\"create_nugget\", title, description, priority?, stage?, kind?, memberReferenceId?, clientId?, folderId? }`.",
230
+ "- Omadeus **Task** and **Nugget** are distinct product types (Jaguar `subscribableKind`). **Project**, **Sprint**, **Release**, **Folder**, **Client**, **Summary**, etc. also have entity chat rooms. User \"task\" / \"the task\" → map to **this room's** `subscribableKind` (Task vs Nugget vs other), not an OpenClaw background task.",
231
+ "- In Task or Nugget rooms, inbound may include **Dolphin nuggetviews** JSON for this chat's `roomId` — **summarize that** for status questions. The payload may include a **`people`** object (Omadeus member names). Use those for assignees; do not read `referenceId` numbers as names. Do not tell the user to go use the Omadeus app instead of answering from that data or the thread.",
232
+ "- `session_status` / SessionKey: **OpenClaw** gateway only. Use the inbound SessionKey, \"current\", or the hint in **entity** rooms — never a fake `task/<...>` string from a title.",
208
233
  `- Reactions only allow these emojis (others are ignored): ${ALLOWED_OMADEUS_REACTION_EMOJI_LIST.join(" ")}`,
209
234
  "- Reply in chat with plain text; use the message tool for proactive sends, edits, deletes, or reactions.",
210
235
  ],
@@ -213,7 +238,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
213
238
  describeMessageTool: ({ cfg }) => {
214
239
  const enabled =
215
240
  cfg.channels?.omadeus?.enabled !== false &&
216
- resolveOmadeusAccount({ cfg }).credentialSource !== "none";
241
+ !isUnconfigured(resolveOmadeusAccount({ cfg }));
217
242
  return {
218
243
  actions: enabled ? ["send", "edit", "delete", "react"] : [],
219
244
  capabilities: [],
@@ -221,28 +246,19 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
221
246
  };
222
247
  },
223
248
  handleAction: async (ctx) => {
224
- const apiOptsForAccount = () => {
225
- const account = resolveOmadeusAccount({ cfg: ctx.cfg });
226
- if (!activeTokenManager) {
249
+ const account = resolveOmadeusAccount({ cfg: ctx.cfg });
250
+ const apiOpts = () => {
251
+ if (!gatewayState.tokenManager) {
227
252
  throw new Error("Omadeus: not connected; gateway must be running with Omadeus enabled.");
228
253
  }
229
- return { maestroUrl: account.maestroUrl, tokenManager: activeTokenManager };
254
+ return { maestroUrl: account.maestroUrl, tokenManager: gatewayState.tokenManager };
230
255
  };
231
256
 
232
257
  if (ctx.action === "send" && isCreateNuggetRequest(ctx.params)) {
233
258
  const title = readStringParam(ctx.params, ["title", "subject", "name"]);
234
259
  const description = readStringParam(ctx.params, ["description", "details", "body"]);
235
260
  if (!title || !description) {
236
- return {
237
- isError: true,
238
- content: [
239
- {
240
- type: "text" as const,
241
- text: "Omadeus create task/nugget requires `title` and `description`.",
242
- },
243
- ],
244
- details: { error: "Missing title/description." },
245
- };
261
+ return actionError("Omadeus create task/nugget requires `title` and `description`.", "Missing title/description.");
246
262
  }
247
263
 
248
264
  const kind = readNuggetKind(ctx.params);
@@ -250,25 +266,19 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
250
266
  const stage = readStringParam(ctx.params, ["stage"]) ?? "Triage";
251
267
  const memberReferenceId =
252
268
  readNumberParam(ctx.params, ["memberReferenceId", "assigneeReferenceId"]) ??
253
- activeTokenManager?.getPayload().referenceId;
269
+ gatewayState.tokenManager?.getPayload().referenceId;
254
270
  const clientId = readNumberParam(ctx.params, ["clientId"]) ?? 1;
255
271
  const folderId = readNumberParam(ctx.params, ["folderId"]) ?? 1;
256
272
 
257
273
  if (!memberReferenceId) {
258
- return {
259
- isError: true,
260
- content: [
261
- {
262
- type: "text" as const,
263
- text: "Omadeus create task/nugget needs `memberReferenceId` or an active authenticated user.",
264
- },
265
- ],
266
- details: { error: "Missing memberReferenceId." },
267
- };
274
+ return actionError(
275
+ "Omadeus create task/nugget needs `memberReferenceId` or an active authenticated user.",
276
+ "Missing memberReferenceId.",
277
+ );
268
278
  }
269
279
 
270
280
  try {
271
- const created = await createNugget(apiOptsForAccount(), {
281
+ const created = await createNugget(apiOpts(), {
272
282
  title,
273
283
  description,
274
284
  stage,
@@ -278,39 +288,10 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
278
288
  clientId,
279
289
  folderId,
280
290
  });
281
- const number = created["number"];
282
- const id = created["id"];
283
- return {
284
- content: [
285
- {
286
- type: "text" as const,
287
- text: JSON.stringify({
288
- ok: true,
289
- channel: "omadeus",
290
- action: "create",
291
- kind,
292
- number,
293
- id,
294
- title,
295
- }),
296
- },
297
- ],
298
- details: {
299
- ok: true,
300
- channel: "omadeus",
301
- action: "create",
302
- kind,
303
- number,
304
- id,
305
- },
306
- };
291
+ return actionOk({ action: "create", kind, number: created["number"], id: created["id"], title });
307
292
  } catch (err) {
308
293
  const msg = err instanceof Error ? err.message : String(err);
309
- return {
310
- isError: true,
311
- content: [{ type: "text" as const, text: msg }],
312
- details: { error: msg },
313
- };
294
+ return actionError(msg);
314
295
  }
315
296
  }
316
297
 
@@ -322,93 +303,47 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
322
303
  (typeof ctx.params.content === "string" && ctx.params.content.trim()) ||
323
304
  "";
324
305
  if (messageId == null) {
325
- return {
326
- isError: true,
327
- content: [
328
- {
329
- type: "text" as const,
330
- text: "Omadeus edit requires `messageId` (Jaguar message id) or current inbound MessageSid.",
331
- },
332
- ],
333
- details: { error: "Missing messageId for edit." },
334
- };
306
+ return actionError(
307
+ "Omadeus edit requires `messageId` (Jaguar message id) or current inbound MessageSid.",
308
+ "Missing messageId for edit.",
309
+ );
335
310
  }
336
311
  if (!body) {
337
- return {
338
- isError: true,
339
- content: [
340
- {
341
- type: "text" as const,
342
- text: "Omadeus edit requires new text in `message`, `text`, or `content`.",
343
- },
344
- ],
345
- details: { error: "Missing body for edit." },
346
- };
312
+ return actionError(
313
+ "Omadeus edit requires new text in `message`, `text`, or `content`.",
314
+ "Missing body for edit.",
315
+ );
347
316
  }
348
317
  try {
349
- await editMessage(apiOptsForAccount(), { messageId, body });
318
+ await editMessage(apiOpts(), { messageId, body });
350
319
  } catch (err) {
351
320
  const msg = err instanceof Error ? err.message : String(err);
352
- return {
353
- isError: true,
354
- content: [{ type: "text" as const, text: msg }],
355
- details: { error: msg },
356
- };
321
+ return actionError(msg);
357
322
  }
358
- return {
359
- content: [
360
- {
361
- type: "text" as const,
362
- text: JSON.stringify({ ok: true, channel: "omadeus", action: "edit", messageId }),
363
- },
364
- ],
365
- details: { ok: true, channel: "omadeus", messageId },
366
- };
323
+ return actionOk({ action: "edit", messageId });
367
324
  }
368
325
 
369
326
  if (ctx.action === "delete") {
370
327
  const messageId = readReactionMessageId(ctx.params, ctx.toolContext);
371
328
  if (messageId == null) {
372
- return {
373
- isError: true,
374
- content: [
375
- {
376
- type: "text" as const,
377
- text: "Omadeus delete requires `messageId` (Jaguar message id) or current inbound MessageSid.",
378
- },
379
- ],
380
- details: { error: "Missing messageId for delete." },
381
- };
329
+ return actionError(
330
+ "Omadeus delete requires `messageId` (Jaguar message id) or current inbound MessageSid.",
331
+ "Missing messageId for delete.",
332
+ );
382
333
  }
383
334
  try {
384
- await deleteMessage(apiOptsForAccount(), { messageId });
335
+ await deleteMessage(apiOpts(), { messageId });
385
336
  } catch (err) {
386
337
  const msg = err instanceof Error ? err.message : String(err);
387
- return {
388
- isError: true,
389
- content: [{ type: "text" as const, text: msg }],
390
- details: { error: msg },
391
- };
338
+ return actionError(msg);
392
339
  }
393
- return {
394
- content: [
395
- {
396
- type: "text" as const,
397
- text: JSON.stringify({ ok: true, channel: "omadeus", action: "delete", messageId }),
398
- },
399
- ],
400
- details: { ok: true, channel: "omadeus", messageId },
401
- };
340
+ return actionOk({ action: "delete", messageId });
402
341
  }
403
342
 
404
343
  if (ctx.action === "react") {
405
344
  const emoji = typeof ctx.params.emoji === "string" ? ctx.params.emoji.trim() : "";
406
345
  if (!emoji) {
407
- return {
408
- isError: true,
409
- content: [{ type: "text" as const, text: "Omadeus react requires `emoji`." }],
410
- details: { error: "Omadeus react requires emoji." },
411
- };
346
+ return actionError("Omadeus react requires `emoji`.", "Omadeus react requires emoji.");
412
347
  }
413
348
  if (!isAllowedOmadeusReactionEmoji(emoji)) {
414
349
  return {
@@ -417,7 +352,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
417
352
  type: "text" as const,
418
353
  text: JSON.stringify({
419
354
  ok: true,
420
- channel: "omadeus",
355
+ channel: CHANNEL_ID,
421
356
  ignored: true,
422
357
  reason: "unsupported_emoji",
423
358
  emoji,
@@ -425,84 +360,42 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
425
360
  }),
426
361
  },
427
362
  ],
428
- details: {
429
- ok: true,
430
- ignored: true,
431
- channel: "omadeus",
432
- },
363
+ details: { ok: true, ignored: true, channel: CHANNEL_ID },
433
364
  };
434
365
  }
435
366
  const messageId = readReactionMessageId(ctx.params, ctx.toolContext);
436
367
  if (messageId == null) {
437
- return {
438
- isError: true,
439
- content: [
440
- {
441
- type: "text" as const,
442
- text: "Omadeus react requires `messageId` or a current inbound message id (MessageSid).",
443
- },
444
- ],
445
- details: { error: "Missing messageId for reaction." },
446
- };
447
- }
448
- if (!activeTokenManager) {
449
- return {
450
- isError: true,
451
- content: [
452
- {
453
- type: "text" as const,
454
- text: "Omadeus is not connected; cannot react (gateway must be running).",
455
- },
456
- ],
457
- details: { error: "Omadeus not connected." },
458
- };
368
+ return actionError(
369
+ "Omadeus react requires `messageId` or a current inbound message id (MessageSid).",
370
+ "Missing messageId for reaction.",
371
+ );
459
372
  }
460
373
  try {
461
- await addMessageReaction(apiOptsForAccount(), { messageId, emoji });
374
+ await addMessageReaction(apiOpts(), { messageId, emoji });
462
375
  } catch (err) {
463
376
  const msg = err instanceof Error ? err.message : String(err);
464
- return {
465
- isError: true,
466
- content: [{ type: "text" as const, text: msg }],
467
- details: { error: msg },
468
- };
377
+ return actionError(msg);
469
378
  }
470
- return {
471
- content: [
472
- {
473
- type: "text" as const,
474
- text: JSON.stringify({
475
- ok: true,
476
- channel: "omadeus",
477
- messageId,
478
- emoji,
479
- }),
480
- },
481
- ],
482
- details: { ok: true, channel: "omadeus", messageId, emoji },
483
- };
379
+ return actionOk({ messageId, emoji });
484
380
  }
485
- // Return null to fall through to default handler
486
- return null as never;
381
+
382
+ throw new Error(`Unhandled Omadeus action: ${String(ctx.action)}`);
487
383
  },
488
384
  },
489
385
  reload: { configPrefixes: ["channels.omadeus"] },
490
386
  setup: omadeusSetupAdapter,
491
387
  setupWizard: omadeusSetupWizard,
492
388
 
493
- // -------------------------------------------------------------------------
494
- // Config adapter
495
- // -------------------------------------------------------------------------
496
389
  config: {
497
390
  ...omadeusConfigAdapter,
498
- isConfigured: (account) => account.credentialSource !== "none",
391
+ isConfigured: (account) => !isUnconfigured(account),
499
392
  unconfiguredReason: () =>
500
393
  "Omadeus requires email, password, and organizationId. Run: openclaw setup omadeus",
501
394
  describeAccount: (account) => ({
502
395
  accountId: account.accountId,
503
396
  name: account.name,
504
397
  enabled: account.enabled,
505
- configured: account.credentialSource !== "none",
398
+ configured: !isUnconfigured(account),
506
399
  credentialSource: account.credentialSource,
507
400
  baseUrl: account.maestroUrl,
508
401
  }),
@@ -520,13 +413,13 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
520
413
  const id = normalizeOmadeusRoomId(input);
521
414
  if (!id) {
522
415
  const taskIntent = parseTaskChannelTargetIntent(input);
523
- if (!taskIntent || !activeTokenManager) {
416
+ if (!taskIntent || !gatewayState.tokenManager) {
524
417
  return null;
525
418
  }
526
419
  const roomId = await resolveTaskRoomIdByNumber(
527
420
  {
528
421
  maestroUrl: resolveOmadeusAccount({ cfg }).maestroUrl,
529
- tokenManager: activeTokenManager,
422
+ tokenManager: gatewayState.tokenManager,
530
423
  },
531
424
  { nuggetNumber: taskIntent.nuggetNumber },
532
425
  );
@@ -550,18 +443,15 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
550
443
  },
551
444
  },
552
445
 
553
- // -------------------------------------------------------------------------
554
- // Outbound adapter
555
- // -------------------------------------------------------------------------
556
446
  outbound: {
557
447
  deliveryMode: "direct",
558
448
  textChunkLimit: 4000,
559
449
  chunker: (text, limit) => getOmadeusRuntime().channel.text.chunkMarkdownText(text, limit),
560
450
  chunkerMode: "markdown",
561
451
  ...createAttachedChannelResultAdapter({
562
- channel: "omadeus",
452
+ channel: CHANNEL_ID,
563
453
  sendText: async ({ cfg, to, text }) => {
564
- if (!activeJaguar || !activeTokenManager) {
454
+ if (!gatewayState.jaguar || !gatewayState.tokenManager) {
565
455
  throw new Error("Omadeus: not connected. Is the gateway running with Omadeus enabled?");
566
456
  }
567
457
  const deps: OutboundDeps = {
@@ -569,9 +459,9 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
569
459
  maestroUrl: resolveOmadeusAccount({
570
460
  cfg,
571
461
  }).maestroUrl,
572
- tokenManager: activeTokenManager,
462
+ tokenManager: gatewayState.tokenManager,
573
463
  },
574
- jaguarSocket: activeJaguar,
464
+ jaguarSocket: gatewayState.jaguar,
575
465
  };
576
466
  return await sendOmadeusMessage(deps, { to, text });
577
467
  },
@@ -593,9 +483,6 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
593
483
  },
594
484
  },
595
485
 
596
- // -------------------------------------------------------------------------
597
- // Status adapter
598
- // -------------------------------------------------------------------------
599
486
  status: {
600
487
  defaultRuntime: defaultRuntimeState,
601
488
  collectStatusIssues: (accounts): ChannelStatusIssue[] =>
@@ -603,7 +490,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
603
490
  const issues: ChannelStatusIssue[] = [];
604
491
  if (entry.enabled !== false && entry.configured !== true) {
605
492
  issues.push({
606
- channel: "omadeus",
493
+ channel: CHANNEL_ID,
607
494
  accountId: String(entry.accountId ?? DEFAULT_ACCOUNT_ID),
608
495
  kind: "config",
609
496
  message: "Omadeus credentials are missing.",
@@ -626,7 +513,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
626
513
  accountId: account.accountId,
627
514
  name: account.name,
628
515
  enabled: account.enabled,
629
- configured: account.credentialSource !== "none",
516
+ configured: !isUnconfigured(account),
630
517
  runtime,
631
518
  }),
632
519
  baseUrl: account.maestroUrl,
@@ -636,15 +523,12 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
636
523
  }),
637
524
  },
638
525
 
639
- // -------------------------------------------------------------------------
640
- // Gateway adapter — starts sockets on gateway boot
641
- // -------------------------------------------------------------------------
642
526
  gateway: {
643
527
  startAccount: async (ctx) => {
644
528
  const { account, cfg, abortSignal } = ctx;
645
529
  ctx.log?.info(`[omadeus] starting for org ${account.organizationId}`);
646
530
 
647
- if (account.credentialSource === "none") {
531
+ if (isUnconfigured(account)) {
648
532
  ctx.log?.warn("[omadeus] skipping start: credentials not configured");
649
533
  ctx.setStatus({
650
534
  accountId: account.accountId,
@@ -666,8 +550,8 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
666
550
  }
667
551
 
668
552
  const log = ctx.log ?? { info: () => {}, warn: () => {}, error: () => {} };
553
+ let isConnected = false;
669
554
 
670
- // Auth
671
555
  const tokenManager = createTokenManager({
672
556
  casUrl: account.casUrl,
673
557
  maestroUrl: account.maestroUrl,
@@ -697,7 +581,7 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
697
581
  }
698
582
 
699
583
  tokenManager.startAutoRefresh();
700
- activeTokenManager = tokenManager;
584
+ gatewayState.tokenManager = tokenManager;
701
585
 
702
586
  const selfReferenceId = tokenManager.getPayload().referenceId;
703
587
 
@@ -711,9 +595,9 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
711
595
  runtime: ctx.runtime,
712
596
  log,
713
597
  outboundDeps,
598
+ selfReferenceId,
714
599
  });
715
600
 
716
- // Jaguar socket (chat — DMs, nugget/task/project rooms)
717
601
  const jaguar = createJaguarSocketClient({
718
602
  maestroUrl: account.maestroUrl,
719
603
  tokenManager,
@@ -742,17 +626,19 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
742
626
  onOtherEvent: (data) => {
743
627
  log.info(`[jaguar] non-message event: ${JSON.stringify(data).slice(0, 120)}`);
744
628
  },
745
- onConnect: () =>
746
- ctx.setStatus({
747
- accountId: account.accountId,
748
- connected: true,
749
- lastConnectedAt: Date.now(),
750
- }),
751
- onDisconnect: () => ctx.setStatus({ accountId: account.accountId, connected: false }),
629
+ onConnect: () => {
630
+ if (!isConnected) {
631
+ isConnected = true;
632
+ ctx.setStatus({ accountId: account.accountId, connected: true, lastConnectedAt: Date.now() });
633
+ }
634
+ },
635
+ onDisconnect: () => {
636
+ isConnected = false;
637
+ ctx.setStatus({ accountId: account.accountId, connected: false });
638
+ },
752
639
  onError: (err) => ctx.setStatus({ accountId: account.accountId, lastError: err.message }),
753
640
  });
754
641
 
755
- // Dolphin socket (data — tasks, projects, sprints, releases)
756
642
  const dolphin = createDolphinSocketClient({
757
643
  maestroUrl: account.maestroUrl,
758
644
  tokenManager,
@@ -761,13 +647,16 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
761
647
  log.info(`[dolphin] event: ${JSON.stringify(data).slice(0, 120)}`);
762
648
  // TODO: handle task assignment/update events as they are discovered
763
649
  },
764
- onConnect: () =>
765
- ctx.setStatus({
766
- accountId: account.accountId,
767
- connected: true,
768
- lastConnectedAt: Date.now(),
769
- }),
770
- onDisconnect: () => ctx.setStatus({ accountId: account.accountId, connected: false }),
650
+ onConnect: () => {
651
+ if (!isConnected) {
652
+ isConnected = true;
653
+ ctx.setStatus({ accountId: account.accountId, connected: true, lastConnectedAt: Date.now() });
654
+ }
655
+ },
656
+ onDisconnect: () => {
657
+ isConnected = false;
658
+ ctx.setStatus({ accountId: account.accountId, connected: false });
659
+ },
771
660
  onError: (err) => ctx.setStatus({ accountId: account.accountId, lastError: err.message }),
772
661
  });
773
662
 
@@ -776,8 +665,8 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
776
665
 
777
666
  jaguar.connect();
778
667
  dolphin.connect();
779
- activeJaguar = jaguar;
780
- activeDolphin = dolphin;
668
+ gatewayState.jaguar = jaguar;
669
+ gatewayState.dolphin = dolphin;
781
670
 
782
671
  ctx.setStatus({
783
672
  accountId: account.accountId,
@@ -792,9 +681,10 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
792
681
  tokenManager.stopAutoRefresh();
793
682
  jaguar.disconnect();
794
683
  dolphin.disconnect();
795
- activeTokenManager = null;
796
- activeJaguar = null;
797
- activeDolphin = null;
684
+ gatewayState.tokenManager = null;
685
+ gatewayState.jaguar = null;
686
+ gatewayState.dolphin = null;
687
+ lastPersistedToken = null;
798
688
  ctx.setStatus({
799
689
  accountId: account.accountId,
800
690
  running: false,
@@ -802,7 +692,6 @@ export const omadeusPlugin: ChannelPlugin<Account> = {
802
692
  });
803
693
  };
804
694
 
805
- // Keep this account runner alive until the gateway aborts it.
806
695
  await new Promise<void>((resolve) => {
807
696
  if (abortSignal.aborted) {
808
697
  resolve();