@gajae-code/coding-agent 0.12.7 → 0.12.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/types/cli/notify-cli.d.ts +1 -0
  3. package/dist/types/commands/notify.d.ts +4 -0
  4. package/dist/types/commands/sdk.d.ts +23 -0
  5. package/dist/types/config/file-lock.d.ts +3 -0
  6. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  7. package/dist/types/config/model-registry.d.ts +2 -0
  8. package/dist/types/config/resolve-config-value.d.ts +2 -1
  9. package/dist/types/config/settings-schema.d.ts +17 -1
  10. package/dist/types/discovery/agents-md.d.ts +11 -1
  11. package/dist/types/main.d.ts +1 -0
  12. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  13. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  14. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  15. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  16. package/dist/types/modes/types.d.ts +6 -0
  17. package/dist/types/sdk/broker/broker.d.ts +5 -1
  18. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  19. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  20. package/dist/types/sdk/bus/config.d.ts +75 -113
  21. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  22. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  23. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  24. package/dist/types/sdk/bus/index.d.ts +1 -1
  25. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  26. package/dist/types/sdk/bus/notification-service.d.ts +54 -4
  27. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  28. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  29. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  31. package/dist/types/sdk/client/discovery.d.ts +2 -1
  32. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  33. package/dist/types/sdk/index.d.ts +1 -0
  34. package/dist/types/sdk/providers.d.ts +22 -0
  35. package/dist/types/sdk/session-directory.d.ts +1 -1
  36. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  37. package/dist/types/session/internal/managed-session-storage.d.ts +19 -1
  38. package/dist/types/session/internal/native-publish-outcome.d.ts +3 -2
  39. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  40. package/dist/types/session/session-manager.d.ts +3 -1
  41. package/dist/types/session/session-storage.d.ts +2 -0
  42. package/dist/types/system-prompt.d.ts +12 -1
  43. package/dist/types/tools/composer-bash-policy.d.ts +9 -1
  44. package/package.json +10 -8
  45. package/scripts/build-sdk-package-smoke.ts +51 -0
  46. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  47. package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
  48. package/src/cli/notify-cli.ts +215 -99
  49. package/src/commands/notify.ts +19 -8
  50. package/src/commands/sdk.ts +54 -0
  51. package/src/config/file-lock.ts +6 -0
  52. package/src/config/model-discovery-manager.ts +50 -8
  53. package/src/config/model-profiles.ts +8 -0
  54. package/src/config/model-registry.ts +866 -76
  55. package/src/config/resolve-config-value.ts +10 -8
  56. package/src/config/settings-schema.ts +25 -15
  57. package/src/config/settings.ts +15 -15
  58. package/src/discovery/agents-md.ts +76 -19
  59. package/src/gjc-runtime/psmux-detect.ts +12 -16
  60. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  61. package/src/gjc-runtime/tmux-sessions.ts +8 -1
  62. package/src/internal-urls/docs-index.generated.ts +10 -9
  63. package/src/main.ts +43 -7
  64. package/src/modes/DESIGN.md +11 -11
  65. package/src/modes/components/extensions/inspector-panel.ts +9 -5
  66. package/src/modes/components/hook-selector.ts +155 -44
  67. package/src/modes/components/model-selector.ts +242 -49
  68. package/src/modes/components/notifications-settings-editor.ts +631 -85
  69. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  70. package/src/modes/controllers/selector-controller.ts +525 -93
  71. package/src/modes/print-mode.ts +3 -0
  72. package/src/modes/types.ts +6 -0
  73. package/src/prompts/system/system-prompt.md +1 -0
  74. package/src/sdk/broker/broker.ts +54 -10
  75. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  76. package/src/sdk/broker/lifecycle.ts +47 -50
  77. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  78. package/src/sdk/bus/chat-daemon-control.ts +35 -6
  79. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  80. package/src/sdk/bus/config.ts +534 -286
  81. package/src/sdk/bus/daemon-paths.ts +2 -0
  82. package/src/sdk/bus/discord-live-provider.ts +57 -2
  83. package/src/sdk/bus/discord-provider.ts +21 -0
  84. package/src/sdk/bus/index.ts +97 -35
  85. package/src/sdk/bus/notification-orchestration.ts +371 -68
  86. package/src/sdk/bus/notification-service.ts +571 -137
  87. package/src/sdk/bus/session-control.ts +119 -44
  88. package/src/sdk/bus/slack-live-provider.ts +78 -2
  89. package/src/sdk/bus/slack-provider.ts +24 -0
  90. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  91. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  92. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  93. package/src/sdk/bus/telegram-daemon.ts +23 -15
  94. package/src/sdk/client/discovery.ts +10 -4
  95. package/src/sdk/host/query/handlers.ts +20 -1
  96. package/src/sdk/index.ts +1 -0
  97. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  98. package/src/sdk/protocol/operation-registry.ts +5 -2
  99. package/src/sdk/providers.ts +51 -0
  100. package/src/sdk/session-directory.ts +1 -0
  101. package/src/sdk/session.ts +34 -14
  102. package/src/session/agent-session.ts +109 -37
  103. package/src/session/internal/managed-session-scope.ts +981 -450
  104. package/src/session/internal/managed-session-storage.ts +418 -47
  105. package/src/session/internal/native-publish-outcome.ts +15 -3
  106. package/src/session/internal/session-open-errors.ts +11 -0
  107. package/src/session/session-manager.ts +27 -3
  108. package/src/session/session-storage.ts +72 -5
  109. package/src/slash-commands/builtin-registry.ts +117 -15
  110. package/src/system-prompt.ts +28 -15
  111. package/src/task/index.ts +9 -3
  112. package/src/tools/ask.ts +1 -1
  113. package/src/tools/bash.ts +4 -2
  114. package/src/tools/composer-bash-policy.ts +21 -5
  115. package/src/tools/telegram-send.ts +4 -2
  116. package/src/utils/command-args.ts +19 -30
  117. package/src/utils/lang-from-path.ts +2 -4
@@ -19,7 +19,7 @@ import { GJC_MODEL_ASSIGNMENT_TARGETS, type GjcModelAssignmentTargetId } from ".
19
19
  import { formatModelSelectorValue } from "../../config/model-resolver";
20
20
  import { selectorHead } from "../../config/model-selector-value";
21
21
  import type { ModelProfileConfig } from "../../config/models-config-schema";
22
- import { type Settings, settings } from "../../config/settings";
22
+ import { type Settings, type SettingsAtomicReceipt, settings } from "../../config/settings";
23
23
  import { DebugSelectorComponent } from "../../debug";
24
24
  import { disableProvider, enableProvider } from "../../discovery";
25
25
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
@@ -50,16 +50,29 @@ import {
50
50
  stopInteractiveActivityIndicator,
51
51
  suspendInteractiveActivityIndicator,
52
52
  } from "../../modes/types";
53
- import { getNotificationConfig, isTelegramConfigured, maskToken } from "../../sdk/bus/config";
53
+ import { ChatDaemonController } from "../../sdk/bus/chat-daemon-control";
54
+ import {
55
+ getCurrentTelegramActivationMarker,
56
+ getNotificationConfig,
57
+ isProviderEffectivelyEnabled,
58
+ isTelegramComplete,
59
+ maskToken,
60
+ type NotificationProvider,
61
+ } from "../../sdk/bus/config";
54
62
  import {
55
63
  clearTelegramActivationMarker,
56
64
  createTelegramActivationMarker,
65
+ mutateNotificationProvider,
66
+ type NotificationProviderConfigurationMutation,
67
+ type NotificationProviderRuntimeAuthority,
57
68
  observedTelegramActivationMarker,
58
69
  persistTelegramActivationMarker,
59
70
  proposedTelegramIdentity,
60
71
  reconcileCommittedTelegramConfiguration,
72
+ removeNotificationProvider,
61
73
  removeTelegramConfiguration,
62
74
  saveTelegramInactive,
75
+ setGlobalNotificationsEnabled,
63
76
  } from "../../sdk/bus/notification-orchestration";
64
77
  import {
65
78
  buildNotificationStatusReport,
@@ -141,6 +154,7 @@ import { JobsOverlayComponent } from "../components/jobs-overlay";
141
154
  import { ModelSelectorComponent } from "../components/model-selector";
142
155
  import type {
143
156
  NotificationsEditorOperations,
157
+ PreparedNotificationProviderConfiguration,
144
158
  PreparedTelegramConfiguration,
145
159
  } from "../components/notifications-settings-editor";
146
160
  import { OAuthSelectorComponent } from "../components/oauth-selector";
@@ -187,6 +201,11 @@ const CALLBACK_SERVER_PROVIDERS = new Set<string>([
187
201
 
188
202
  const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
189
203
 
204
+ /** Providers whose authorization server can display a code for `/login <provider> --manual`. */
205
+ const MANUAL_CODE_PROVIDERS = new Set<string>(["anthropic"]);
206
+
207
+ const MANUAL_CODE_LOGIN_TIP = "Tip: You can complete pairing with /login <code>.";
208
+
190
209
  function isThemePreviewSuperseded(result: { success: boolean; error?: string }): boolean {
191
210
  return !result.success && result.error?.includes("superseded by a newer request") === true;
192
211
  }
@@ -234,6 +253,7 @@ type TelegramDaemonStopResult = {
234
253
 
235
254
  export interface NotificationsEditorOperationDependencies {
236
255
  getNotificationConfig: typeof getNotificationConfig;
256
+ getCurrentTelegramActivationMarker: typeof getCurrentTelegramActivationMarker;
237
257
  maskToken: typeof maskToken;
238
258
  buildNotificationStatusReport: typeof buildNotificationStatusReport;
239
259
  checkNotificationHealth: typeof checkNotificationHealth;
@@ -258,10 +278,12 @@ export interface NotificationsEditorOperationDependencies {
258
278
  reloadTelegramDaemon(settings: Settings): Promise<{ ok: boolean; message: string }>;
259
279
  restartTelegramDaemon(settings: Settings): Promise<{ ok: boolean; message: string }>;
260
280
  stopTelegramDaemon(settings: Settings): Promise<TelegramDaemonStopResult>;
281
+ providerRuntime?: NotificationProviderRuntimeAuthority;
261
282
  }
262
283
 
263
284
  const notificationEditorOperationDependencies: NotificationsEditorOperationDependencies = {
264
285
  getNotificationConfig,
286
+ getCurrentTelegramActivationMarker,
265
287
  maskToken,
266
288
  buildNotificationStatusReport,
267
289
  checkNotificationHealth,
@@ -298,9 +320,9 @@ function unavailableNotificationSessionStatus(): NotificationSessionStatus {
298
320
  return {
299
321
  eligible: false,
300
322
  locallyEnabled: true,
301
- effectiveEnabled: false,
323
+ genericSessionEnabled: false,
324
+ genericEligibilitySource: "none",
302
325
  running: false,
303
- environment: "off",
304
326
  };
305
327
  }
306
328
 
@@ -328,6 +350,10 @@ export function createNotificationsEditorOperations(
328
350
  ): NotificationsEditorOperations {
329
351
  const services = { ...notificationEditorOperationDependencies, ...overrides };
330
352
  const drafts = new WeakMap<PreparedTelegramConfiguration, string>();
353
+ const providerDrafts = new WeakMap<
354
+ PreparedNotificationProviderConfiguration,
355
+ { botToken?: string; appToken?: string }
356
+ >();
331
357
  const sessionContext = () => ({ sessionManager: ctx.sessionManager });
332
358
  const notifyAfterDurableCommit = async (): Promise<void> => {
333
359
  await ctx.notifyConfigChanged?.();
@@ -340,12 +366,52 @@ export function createNotificationsEditorOperations(
340
366
  });
341
367
  const telegramSetupPreflight = async (): Promise<TelegramSetupPreflight> =>
342
368
  await services.resolveTelegramSetupPreflight(ctx.settings);
369
+ const providerRuntime: NotificationProviderRuntimeAuthority = services.providerRuntime ?? {
370
+ activate: async provider => {
371
+ if (provider === "telegram") {
372
+ const result = await reconnect();
373
+ if (result === "blocked_identity" || result === "disabled") {
374
+ throw new Error("Telegram activation failed after the durable save.");
375
+ }
376
+ await ctx.session.notificationSessionController?.reconcileCurrentSession(sessionContext());
377
+ return;
378
+ }
379
+ const result = await new ChatDaemonController(ctx.settings, provider).ensure();
380
+ if (result === "disabled") throw new Error(`${provider} activation failed after the durable save.`);
381
+ },
382
+ deactivate: async provider => {
383
+ if (provider === "telegram") {
384
+ const result = await services.stopTelegramDaemon(ctx.settings);
385
+ if (!result.ok) throw new Error(result.message);
386
+ await ctx.session.notificationSessionController?.reconcileCurrentSession(sessionContext());
387
+ return;
388
+ }
389
+ const result = await new ChatDaemonController(ctx.settings, provider).stop();
390
+ if (!result.ok) throw new Error(result.message);
391
+ },
392
+ };
343
393
 
344
394
  return {
345
395
  loadState: async () => {
346
396
  const config = services.getNotificationConfig(ctx.settings);
397
+ const status = services.buildNotificationStatusReport(ctx.settings);
398
+ const runtime = async (provider: NotificationProvider) => {
399
+ const daemon =
400
+ provider === "telegram"
401
+ ? await new TelegramDaemonController(ctx.settings).status()
402
+ : await new ChatDaemonController(ctx.settings, provider).status();
403
+ return daemon.health === "running" ? "ready" : daemon.health === "not_configured" ? "inactive" : "failed";
404
+ };
405
+ const [telegramRuntime, discordRuntime, slackRuntime] = await Promise.all([
406
+ runtime("telegram"),
407
+ runtime("discord"),
408
+ runtime("slack"),
409
+ ]);
410
+ status.telegram.runtime = telegramRuntime;
411
+ status.discord.runtime = discordRuntime;
412
+ status.slack.runtime = slackRuntime;
347
413
  return {
348
- status: services.buildNotificationStatusReport(ctx.settings),
414
+ status,
349
415
  session:
350
416
  ctx.session.notificationSessionController?.query(sessionContext()) ??
351
417
  unavailableNotificationSessionStatus(),
@@ -362,13 +428,14 @@ export function createNotificationsEditorOperations(
362
428
  };
363
429
  },
364
430
 
365
- refreshHealth: async ({ probe, signal }) => {
431
+ refreshHealth: async ({ probe, provider, signal }) => {
366
432
  if (signal?.aborted) throw new Error("Notification health refresh cancelled.");
367
433
  try {
368
434
  const input: Parameters<typeof checkNotificationHealth>[0] & { signal?: AbortSignal } = {
369
435
  settings: ctx.settings,
370
436
  stateRoot: path.join(ctx.sessionManager.getCwd(), ".gjc", "state"),
371
437
  probe,
438
+ provider,
372
439
  signal,
373
440
  };
374
441
  const report = await services.checkNotificationHealth(input);
@@ -390,9 +457,21 @@ export function createNotificationsEditorOperations(
390
457
  }
391
458
  },
392
459
 
393
- sendTest: async () => {
460
+ sendTest: async provider => {
394
461
  try {
395
- const result = await services.sendNotificationTest({ settings: ctx.settings });
462
+ const result = await services.sendNotificationTest({
463
+ settings: ctx.settings,
464
+ provider,
465
+ deps: {
466
+ providerRuntimeStatus: async selected => {
467
+ const status =
468
+ selected === "telegram"
469
+ ? await new TelegramDaemonController(ctx.settings).status()
470
+ : await new ChatDaemonController(ctx.settings, selected).status();
471
+ return status.health === "running" ? "ready" : "inactive";
472
+ },
473
+ },
474
+ });
396
475
  return {
397
476
  ...result,
398
477
  detail: services.sanitizeDiagnostic(
@@ -531,12 +610,14 @@ export function createNotificationsEditorOperations(
531
610
  commitConfigure: async draft => {
532
611
  const token = drafts.get(draft);
533
612
  if (!token) throw new Error("The Telegram setup draft expired. Re-enter the masked bot token.");
613
+ let receipt: SettingsAtomicReceipt | undefined;
534
614
  try {
535
615
  const inactiveMarkerToClear = observedTelegramActivationMarker(ctx.settings, token, draft.chatId);
536
- const receipt = await ctx.settings.commitAtomicBatch([
616
+ receipt = await ctx.settings.commitAtomicBatch([
537
617
  { path: "notifications.enabled", op: "set", value: true },
538
618
  { path: "notifications.telegram.botToken", op: "set", value: token },
539
619
  { path: "notifications.telegram.chatId", op: "set", value: draft.chatId },
620
+ { path: "notifications.telegram.enabled", op: "set", value: true },
540
621
  { path: "notifications.telegram.rich.enabled", op: "set", value: draft.richEnabled },
541
622
  { path: "notifications.telegram.richDraft.enabled", op: "set", value: draft.richDraftEnabled },
542
623
  { path: "notifications.telegram.streaming.enabled", op: "set", value: draft.streamingEnabled },
@@ -571,12 +652,20 @@ export function createNotificationsEditorOperations(
571
652
  marker: activationMarker,
572
653
  },
573
654
  });
574
- await notifyAfterDurableCommit();
655
+ let observerFailed = false;
656
+ try {
657
+ await notifyAfterDurableCommit();
658
+ } catch {
659
+ observerFailed = true;
660
+ }
575
661
  if (activation.status === "blocked_identity") {
576
662
  return {
577
663
  status: "blocked_identity" as const,
578
664
  receipt,
579
- message: services.sanitizeDiagnostic(activation.message, token),
665
+ message: services.sanitizeDiagnostic(
666
+ `${activation.message}${observerFailed ? " The settings observer also failed after the durable commit." : ""}`,
667
+ token,
668
+ ),
580
669
  restore: async () => {
581
670
  const restored = await activation.restore();
582
671
  if (restored.status === "restored" || restored.status === "still_blocked") {
@@ -587,12 +676,36 @@ export function createNotificationsEditorOperations(
587
676
  retainCommitted: () => activation.retainCommitted(),
588
677
  };
589
678
  }
679
+ if (activation.status === "activation_failed") {
680
+ return {
681
+ status: "activation_failed" as const,
682
+ receipt,
683
+ message: services.sanitizeDiagnostic(
684
+ `${activation.message}${observerFailed ? " The settings observer also failed after the durable commit." : ""}`,
685
+ token,
686
+ ),
687
+ };
688
+ }
689
+ if (observerFailed) {
690
+ return {
691
+ status: "observer_failed" as const,
692
+ receipt,
693
+ message: "Telegram configuration was saved and activated, but the settings observer failed.",
694
+ };
695
+ }
590
696
  return {
591
697
  status: "saved" as const,
592
698
  receipt,
593
699
  message: services.sanitizeDiagnostic("Telegram configuration saved and reconciled.", token),
594
700
  };
595
701
  } catch (error) {
702
+ if (receipt) {
703
+ return {
704
+ status: "activation_failed" as const,
705
+ receipt,
706
+ message: "Telegram configuration was saved, but post-commit activation failed.",
707
+ };
708
+ }
596
709
  throw notificationOperationError(services, error, token);
597
710
  }
598
711
  },
@@ -607,7 +720,15 @@ export function createNotificationsEditorOperations(
607
720
  chatId: draft.chatId,
608
721
  });
609
722
  drafts.delete(draft);
610
- await notifyAfterDurableCommit();
723
+ try {
724
+ await notifyAfterDurableCommit();
725
+ } catch {
726
+ return {
727
+ status: "observer_failed" as const,
728
+ receipt: result.receipt,
729
+ message: "Telegram configuration was saved inactive, but the settings observer failed.",
730
+ };
731
+ }
611
732
  return {
612
733
  status: "saved_inactive" as const,
613
734
  receipt: result.receipt,
@@ -622,28 +743,289 @@ export function createNotificationsEditorOperations(
622
743
  drafts.delete(draft);
623
744
  },
624
745
 
625
- enableGlobally: async () => {
626
- try {
627
- const receipt = await ctx.settings.commitAtomicBatch([
628
- { path: "notifications.enabled", op: "set", value: true },
629
- ]);
630
- await notifyAfterDurableCommit();
631
- return { receipt, message: "Global notifications enabled using stored configuration." };
632
- } catch (error) {
633
- throw notificationOperationError(services, error, services.getNotificationConfig(ctx.settings).botToken);
746
+ prepareProviderConfiguration: async input => {
747
+ const cfg = services.getNotificationConfig(ctx.settings);
748
+ const consumeSecret = (action: typeof input.botToken): string | undefined => {
749
+ if (!action.value) return undefined;
750
+ const value = action.value.consume();
751
+ return action.action === "replace" ? value : undefined;
752
+ };
753
+ const botToken = consumeSecret(input.botToken);
754
+ const appToken = input.appToken ? consumeSecret(input.appToken) : undefined;
755
+ if (input.botToken.action === "replace" && !botToken?.trim()) {
756
+ throw new Error("A non-blank bot token replacement is required.");
634
757
  }
758
+ if (input.provider === "slack" && input.appToken?.action === "replace" && !appToken?.trim()) {
759
+ throw new Error("A non-blank Slack app token replacement is required.");
760
+ }
761
+ if (input.provider === "discord") {
762
+ const applicationId = input.applicationId?.trim() || undefined;
763
+ const guildId = input.guildId?.trim() || undefined;
764
+ const parentChannelId = input.parentChannelId?.trim() || undefined;
765
+ const resolvedApplicationId = applicationId ?? cfg.discord.applicationId;
766
+ const resolvedGuildId = guildId ?? cfg.discord.guildId;
767
+ const resolvedParentChannelId = parentChannelId ?? cfg.discord.parentChannelId;
768
+ const removesSecret = input.botToken.action === "remove";
769
+ if (!removesSecret && (!resolvedApplicationId || !resolvedGuildId || !resolvedParentChannelId)) {
770
+ throw new Error("Discord application, guild, and parent channel IDs are required.");
771
+ }
772
+ if (!removesSecret && input.botToken.action === "keep" && !cfg.discord.botToken) {
773
+ throw new Error("Discord has no stored bot token to keep.");
774
+ }
775
+ const draft: PreparedNotificationProviderConfiguration = {
776
+ provider: "discord",
777
+ botTokenDisposition: input.botToken.action,
778
+ botTokenMask: maskToken(botToken ?? cfg.discord.botToken),
779
+ ...(applicationId === undefined ? {} : { applicationId }),
780
+ ...(guildId === undefined ? {} : { guildId }),
781
+ ...(parentChannelId === undefined ? {} : { parentChannelId }),
782
+ applicationIdDisplay: resolvedApplicationId,
783
+ guildIdDisplay: resolvedGuildId,
784
+ parentChannelIdDisplay: resolvedParentChannelId,
785
+ };
786
+ providerDrafts.set(draft, { ...(botToken === undefined ? {} : { botToken }) });
787
+ return draft;
788
+ }
789
+ const workspaceId = input.workspaceId?.trim() || undefined;
790
+ const channelId = input.channelId?.trim() || undefined;
791
+ const authorizedUserId = input.authorizedUserId?.trim() || undefined;
792
+ const resolvedWorkspaceId = workspaceId ?? cfg.slack.workspaceId;
793
+ const resolvedChannelId = channelId ?? cfg.slack.channelId;
794
+ const resolvedAuthorizedUserId = authorizedUserId ?? cfg.slack.authorizedUserId;
795
+ const appDisposition = input.appToken?.action ?? "keep";
796
+ const removesSecret = input.botToken.action === "remove" || appDisposition === "remove";
797
+ if (!removesSecret && (!resolvedWorkspaceId || !resolvedChannelId)) {
798
+ throw new Error("Slack workspace and channel IDs are required.");
799
+ }
800
+ if (!removesSecret && input.botToken.action === "keep" && !cfg.slack.botToken) {
801
+ throw new Error("Slack has no stored bot token to keep.");
802
+ }
803
+ if (!removesSecret && appDisposition === "keep" && !cfg.slack.appToken) {
804
+ throw new Error("Slack has no stored app token to keep.");
805
+ }
806
+ const draft: PreparedNotificationProviderConfiguration = {
807
+ provider: "slack",
808
+ botTokenDisposition: input.botToken.action,
809
+ botTokenMask: maskToken(botToken ?? cfg.slack.botToken),
810
+ appTokenDisposition: appDisposition,
811
+ appTokenMask: maskToken(appToken ?? cfg.slack.appToken),
812
+ ...(workspaceId === undefined ? {} : { workspaceId }),
813
+ ...(channelId === undefined ? {} : { channelId }),
814
+ ...(authorizedUserId === undefined ? {} : { authorizedUserId }),
815
+ workspaceIdDisplay: resolvedWorkspaceId,
816
+ channelIdDisplay: resolvedChannelId,
817
+ authorizedUserIdDisplay: resolvedAuthorizedUserId,
818
+ };
819
+ providerDrafts.set(draft, {
820
+ ...(botToken === undefined ? {} : { botToken }),
821
+ ...(appToken === undefined ? {} : { appToken }),
822
+ });
823
+ return draft;
635
824
  },
636
825
 
637
- disableGlobally: async () => {
826
+ commitProviderConfiguration: async draft => {
827
+ const secrets = providerDrafts.get(draft) ?? {};
638
828
  try {
639
- const receipt = await ctx.settings.commitAtomicBatch([
640
- { path: "notifications.enabled", op: "set", value: false },
641
- ]);
642
- await notifyAfterDurableCommit();
643
- return { receipt, message: "Global notifications disabled." };
644
- } catch (error) {
645
- throw notificationOperationError(services, error, services.getNotificationConfig(ctx.settings).botToken);
829
+ let mutation: NotificationProviderConfigurationMutation;
830
+ if (draft.provider === "discord") {
831
+ mutation = {
832
+ provider: "discord",
833
+ botToken:
834
+ draft.botTokenDisposition === "replace"
835
+ ? { action: "replace", value: secrets.botToken ?? "" }
836
+ : { action: draft.botTokenDisposition },
837
+ applicationId: draft.applicationId,
838
+ guildId: draft.guildId,
839
+ parentChannelId: draft.parentChannelId,
840
+ };
841
+ } else {
842
+ const appDisposition = draft.appTokenDisposition ?? "keep";
843
+ mutation = {
844
+ provider: "slack",
845
+ botToken:
846
+ draft.botTokenDisposition === "replace"
847
+ ? { action: "replace", value: secrets.botToken ?? "" }
848
+ : { action: draft.botTokenDisposition },
849
+ appToken:
850
+ appDisposition === "replace"
851
+ ? { action: "replace", value: secrets.appToken ?? "" }
852
+ : { action: appDisposition },
853
+ workspaceId: draft.workspaceId,
854
+ channelId: draft.channelId,
855
+ authorizedUserId: draft.authorizedUserId,
856
+ };
857
+ }
858
+ const removesSecret = draft.botTokenDisposition === "remove" || draft.appTokenDisposition === "remove";
859
+ const result = await mutateNotificationProvider({
860
+ settings: ctx.settings,
861
+ mutation,
862
+ configureAndActivate: !removesSecret,
863
+ ...(removesSecret ? { desiredEnabled: false } : {}),
864
+ notifyConfigChanged: notifyAfterDurableCommit,
865
+ runtime: providerRuntime,
866
+ });
867
+ const outcome =
868
+ result.status === "commit_failed"
869
+ ? "failed"
870
+ : result.status === "activation_failed" ||
871
+ result.status === "deactivation_failed" ||
872
+ result.observerFailed
873
+ ? "degraded"
874
+ : "success";
875
+ const observerSuffix =
876
+ result.status !== "commit_failed" && result.observerFailed
877
+ ? " The settings observer also failed after the durable commit."
878
+ : "";
879
+ return {
880
+ ...(result.status === "commit_failed" ? {} : { receipt: result.receipt }),
881
+ outcome,
882
+ message:
883
+ (result.status === "activated"
884
+ ? `${draft.provider} configuration saved and activated.`
885
+ : result.status === "activation_failed"
886
+ ? `${draft.provider} configuration and desired intent were saved, but runtime activation failed.`
887
+ : result.status === "deactivation_failed"
888
+ ? `${draft.provider} desired-off configuration was saved, but runtime deactivation failed.`
889
+ : result.status === "commit_failed"
890
+ ? `${draft.provider} configuration was not saved because the CAS commit failed.`
891
+ : `${draft.provider} configuration saved.`) + observerSuffix,
892
+ };
893
+ } finally {
894
+ providerDrafts.delete(draft);
895
+ }
896
+ },
897
+
898
+ discardProviderConfiguration: draft => {
899
+ providerDrafts.delete(draft);
900
+ },
901
+
902
+ setProviderDesired: async (provider, enabled) => {
903
+ if (provider === "telegram" && enabled) {
904
+ const config = services.getNotificationConfig(ctx.settings);
905
+ if (services.getCurrentTelegramActivationMarker(config)) {
906
+ return {
907
+ outcome: "failed",
908
+ message:
909
+ "Telegram remains inactive because its exact activation marker must be restored or cleared after safe owner readiness.",
910
+ };
911
+ }
912
+ }
913
+ const mutation: NotificationProviderConfigurationMutation =
914
+ provider === "telegram"
915
+ ? { provider, botToken: { action: "keep" } }
916
+ : provider === "discord"
917
+ ? { provider, botToken: { action: "keep" } }
918
+ : { provider, botToken: { action: "keep" }, appToken: { action: "keep" } };
919
+ const result = await mutateNotificationProvider({
920
+ settings: ctx.settings,
921
+ mutation,
922
+ desiredEnabled: enabled,
923
+ notifyConfigChanged: notifyAfterDurableCommit,
924
+ runtime: providerRuntime,
925
+ });
926
+ const outcome =
927
+ result.status === "commit_failed"
928
+ ? "failed"
929
+ : result.status === "activation_failed" ||
930
+ result.status === "deactivation_failed" ||
931
+ result.observerFailed
932
+ ? "degraded"
933
+ : "success";
934
+ return {
935
+ ...(result.status === "commit_failed" ? {} : { receipt: result.receipt }),
936
+ outcome,
937
+ message:
938
+ (result.status === "activation_failed"
939
+ ? `${provider} desired intent was saved, but activation failed.`
940
+ : result.status === "deactivation_failed"
941
+ ? `${provider} desired-off intent was saved, but deactivation failed.`
942
+ : result.status === "commit_failed"
943
+ ? `${provider} desired intent was not saved because the CAS commit failed.`
944
+ : `${provider} desired intent ${enabled ? "enabled" : "disabled"}.`) +
945
+ (result.status !== "commit_failed" && result.observerFailed
946
+ ? " The settings observer also failed after the durable commit."
947
+ : ""),
948
+ };
949
+ },
950
+
951
+ removeProvider: async provider => {
952
+ if (provider === "telegram") {
953
+ return { message: "Use the Telegram removal action to preserve root-registration fencing." };
646
954
  }
955
+ const result = await removeNotificationProvider({
956
+ settings: ctx.settings,
957
+ provider,
958
+ runtime: providerRuntime,
959
+ notifyConfigChanged: notifyAfterDurableCommit,
960
+ });
961
+ const degraded =
962
+ result.status === "deactivation_failed" || ("observerFailed" in result && result.observerFailed);
963
+ return {
964
+ ...("receipt" in result && result.receipt ? { receipt: result.receipt } : {}),
965
+ outcome:
966
+ result.status === "commit_failed" || result.status === "commit_failed_after_teardown"
967
+ ? "failed"
968
+ : degraded
969
+ ? "degraded"
970
+ : "success",
971
+ message:
972
+ (result.status === "removed"
973
+ ? `${provider} configuration removed.`
974
+ : result.status === "deactivation_failed"
975
+ ? `${provider} configuration was removed, but runtime deactivation failed.`
976
+ : `${provider} configuration removal failed.`) +
977
+ ("observerFailed" in result && result.observerFailed
978
+ ? " The settings observer also failed after the durable commit."
979
+ : ""),
980
+ };
981
+ },
982
+
983
+ enableGlobally: async () => {
984
+ const result = await setGlobalNotificationsEnabled({
985
+ settings: ctx.settings,
986
+ enabled: true,
987
+ notifyConfigChanged: notifyAfterDurableCommit,
988
+ runtime: providerRuntime,
989
+ });
990
+ const degraded =
991
+ result.status === "global_activation_partial" || ("observerFailed" in result && result.observerFailed);
992
+ return {
993
+ ...(result.status === "commit_failed" ? {} : { receipt: result.receipt }),
994
+ outcome: result.status === "commit_failed" ? "failed" : degraded ? "degraded" : "success",
995
+ message:
996
+ (result.status === "global_activation_partial"
997
+ ? `Global notifications were enabled, but activation failed for ${result.failed.join(", ")}.`
998
+ : result.status === "commit_failed"
999
+ ? "Global notifications were not enabled because the CAS commit failed."
1000
+ : "Global notifications enabled using stored provider intent.") +
1001
+ (result.status !== "commit_failed" && result.observerFailed
1002
+ ? " The settings observer also failed after the durable commit."
1003
+ : ""),
1004
+ };
1005
+ },
1006
+
1007
+ disableGlobally: async () => {
1008
+ const result = await setGlobalNotificationsEnabled({
1009
+ settings: ctx.settings,
1010
+ enabled: false,
1011
+ notifyConfigChanged: notifyAfterDurableCommit,
1012
+ runtime: providerRuntime,
1013
+ });
1014
+ const degraded =
1015
+ result.status === "global_deactivation_partial" || ("observerFailed" in result && result.observerFailed);
1016
+ return {
1017
+ ...(result.status === "commit_failed" ? {} : { receipt: result.receipt }),
1018
+ outcome: result.status === "commit_failed" ? "failed" : degraded ? "degraded" : "success",
1019
+ message:
1020
+ (result.status === "global_deactivation_partial"
1021
+ ? `Global notifications were disabled, but teardown failed for ${result.failed.join(", ")}.`
1022
+ : result.status === "commit_failed"
1023
+ ? "Global notifications were not disabled because the CAS commit failed."
1024
+ : "Global notifications disabled; provider configuration and desired intent were preserved.") +
1025
+ (result.status !== "commit_failed" && result.observerFailed
1026
+ ? " The settings observer also failed after the durable commit."
1027
+ : ""),
1028
+ };
647
1029
  },
648
1030
 
649
1031
  removeTelegram: async () => {
@@ -676,17 +1058,27 @@ export function createNotificationsEditorOperations(
676
1058
  },
677
1059
  },
678
1060
  });
1061
+ const postCommitFailures: string[] = [];
679
1062
  if (runtimePrepared && controller) {
680
- await controller.clearBlockedRuntime(sessionContext());
681
- await controller.reconcileCurrentSession(sessionContext());
1063
+ try {
1064
+ await controller.clearBlockedRuntime(sessionContext());
1065
+ await controller.reconcileCurrentSession(sessionContext());
1066
+ } catch {
1067
+ postCommitFailures.push("current-session reconciliation failed");
1068
+ }
1069
+ }
1070
+ try {
1071
+ await notifyAfterDurableCommit();
1072
+ } catch {
1073
+ postCommitFailures.push("settings observer failed");
682
1074
  }
683
- await notifyAfterDurableCommit();
684
1075
  return {
685
1076
  receipt: result.receipt,
1077
+ outcome: postCommitFailures.length === 0 ? "success" : "degraded",
686
1078
  globallyDisabled: result.globallyDisabled,
687
- message: result.globallyDisabled
688
- ? "Telegram configuration removed and global notifications disabled."
689
- : "Telegram configuration removed; Discord or Slack configuration was preserved.",
1079
+ message: `Telegram configuration removed without changing the global master.${
1080
+ postCommitFailures.length === 0 ? "" : ` Post-commit ${postCommitFailures.join(" and ")}.`
1081
+ }`,
690
1082
  };
691
1083
  } catch (error) {
692
1084
  if (runtimePrepared) {
@@ -715,7 +1107,10 @@ export function createNotificationsEditorOperations(
715
1107
  try {
716
1108
  const before = services.getNotificationConfig(ctx.settings);
717
1109
  const disablingToolActivity =
718
- isTelegramConfigured(before) && before.toolActivity.enabled && !preferences.toolActivityEnabled;
1110
+ isProviderEffectivelyEnabled(before, "telegram") &&
1111
+ isTelegramComplete(before) &&
1112
+ before.toolActivity.enabled &&
1113
+ !preferences.toolActivityEnabled;
719
1114
  if (disablingToolActivity) {
720
1115
  const stopped = await services.stopTelegramDaemon(ctx.settings);
721
1116
  if (!stopped.ok)
@@ -725,7 +1120,7 @@ export function createNotificationsEditorOperations(
725
1120
  daemonWasRunningForDisable = stopped.before?.health === "running";
726
1121
  }
727
1122
 
728
- let receipt: Awaited<ReturnType<typeof ctx.settings.commitAtomicBatch>>;
1123
+ let receipt: SettingsAtomicReceipt;
729
1124
  try {
730
1125
  receipt = await ctx.settings.commitAtomicBatch([
731
1126
  { path: "notifications.redact", op: "set", value: preferences.redact },
@@ -758,16 +1153,31 @@ export function createNotificationsEditorOperations(
758
1153
  throw error;
759
1154
  }
760
1155
 
1156
+ const postCommitFailures: string[] = [];
761
1157
  const config = services.getNotificationConfig(ctx.settings);
762
- if (isTelegramConfigured(config)) {
763
- const reload = daemonWasRunningForDisable
764
- ? await services.restartTelegramDaemon(ctx.settings)
765
- : await services.reloadTelegramDaemon(ctx.settings);
766
- if (!reload.ok)
767
- throw new Error(`Notification preferences were saved, but daemon reload failed: ${reload.message}`);
1158
+ if (isProviderEffectivelyEnabled(config, "telegram") && isTelegramComplete(config)) {
1159
+ try {
1160
+ const reload = daemonWasRunningForDisable
1161
+ ? await services.restartTelegramDaemon(ctx.settings)
1162
+ : await services.reloadTelegramDaemon(ctx.settings);
1163
+ if (!reload.ok) postCommitFailures.push(`daemon reload failed: ${reload.message}`);
1164
+ } catch {
1165
+ postCommitFailures.push("daemon reload failed");
1166
+ }
768
1167
  }
769
- await notifyAfterDurableCommit();
770
- return { receipt, message: "Notification preferences saved atomically." };
1168
+ try {
1169
+ await notifyAfterDurableCommit();
1170
+ } catch {
1171
+ postCommitFailures.push("settings observer failed");
1172
+ }
1173
+ return {
1174
+ receipt,
1175
+ outcome: postCommitFailures.length === 0 ? "success" : "degraded",
1176
+ message:
1177
+ postCommitFailures.length === 0
1178
+ ? "Notification preferences saved atomically."
1179
+ : `Notification preferences were saved, but post-commit ${postCommitFailures.join(" and ")}.`,
1180
+ };
771
1181
  } catch (error) {
772
1182
  throw notificationOperationError(services, error, services.getNotificationConfig(ctx.settings).botToken);
773
1183
  }
@@ -2540,63 +2950,85 @@ export class SelectorController {
2540
2950
  });
2541
2951
  }
2542
2952
 
2543
- async #handleOAuthLogin(providerId: string): Promise<void> {
2953
+ async #handleOAuthLogin(providerId: string, options?: OAuthSelectorOptions): Promise<void> {
2954
+ const manualCode = options?.manualCode === true;
2955
+ if (manualCode && !MANUAL_CODE_PROVIDERS.has(providerId)) {
2956
+ this.ctx.showError(
2957
+ `${providerId} has no paste-a-code login. Supported: ${[...MANUAL_CODE_PROVIDERS].join(", ")}.`,
2958
+ );
2959
+ return;
2960
+ }
2544
2961
  this.ctx.showStatus(`Logging in to ${providerId}…`);
2545
2962
  const manualInput = this.ctx.oauthManualInput;
2546
2963
  const useManualInput = CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider);
2964
+ if (providerId === "opencodex") {
2965
+ this.ctx.showStatus("Checking the local OpenCodex proxy…");
2966
+ }
2547
2967
  try {
2548
- await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
2549
- onAuth: (info: { url: string; instructions?: string }) => {
2550
- this.ctx.chatContainer.addChild(new Spacer(1));
2551
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
2552
- const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
2553
- this.ctx.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
2554
- if (info.instructions) {
2968
+ await this.ctx.session.modelRegistry.authStorage.login(
2969
+ providerId as OAuthProvider,
2970
+ {
2971
+ onAuth: (info: { url: string; instructions?: string }) => {
2555
2972
  this.ctx.chatContainer.addChild(new Spacer(1));
2556
- this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
2557
- }
2558
- if (useManualInput) {
2973
+ this.ctx.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
2974
+ const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
2975
+ this.ctx.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
2976
+ if (info.instructions) {
2977
+ this.ctx.chatContainer.addChild(new Spacer(1));
2978
+ this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
2979
+ }
2980
+ if (useManualInput) {
2981
+ this.ctx.chatContainer.addChild(new Spacer(1));
2982
+ this.ctx.chatContainer.addChild(
2983
+ new Text(theme.fg("dim", manualCode ? MANUAL_CODE_LOGIN_TIP : MANUAL_LOGIN_TIP), 1, 0),
2984
+ );
2985
+ }
2986
+ this.ctx.ui.requestRender();
2987
+ this.ctx.openInBrowser(info.url);
2988
+ },
2989
+ onPrompt: async (prompt: { message: string; placeholder?: string }) => {
2559
2990
  this.ctx.chatContainer.addChild(new Spacer(1));
2560
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", MANUAL_LOGIN_TIP), 1, 0));
2561
- }
2562
- this.ctx.ui.requestRender();
2563
- this.ctx.openInBrowser(info.url);
2564
- },
2565
- onPrompt: async (prompt: { message: string; placeholder?: string }) => {
2566
- this.ctx.chatContainer.addChild(new Spacer(1));
2567
- this.ctx.chatContainer.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
2568
- if (prompt.placeholder) {
2569
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
2570
- }
2571
- this.ctx.ui.requestRender();
2572
- const { promise, resolve } = Promise.withResolvers<string>();
2573
- const codeInput = new Input();
2574
- codeInput.onSubmit = () => {
2575
- const code = codeInput.getValue();
2991
+ this.ctx.chatContainer.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
2992
+ if (prompt.placeholder) {
2993
+ this.ctx.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
2994
+ }
2995
+ this.ctx.ui.requestRender();
2996
+ const { promise, resolve } = Promise.withResolvers<string>();
2997
+ const codeInput = new Input();
2998
+ codeInput.onSubmit = () => {
2999
+ const code = codeInput.getValue();
3000
+ this.ctx.editorContainer.clear();
3001
+ this.ctx.editorContainer.addChild(this.ctx.editor);
3002
+ this.ctx.ui.setFocus(this.ctx.editor);
3003
+ resolve(code);
3004
+ };
2576
3005
  this.ctx.editorContainer.clear();
2577
- this.ctx.editorContainer.addChild(this.ctx.editor);
2578
- this.ctx.ui.setFocus(this.ctx.editor);
2579
- resolve(code);
2580
- };
2581
- this.ctx.editorContainer.clear();
2582
- this.ctx.editorContainer.addChild(codeInput);
2583
- this.ctx.ui.setFocus(codeInput);
2584
- this.ctx.ui.requestRender();
2585
- return promise;
2586
- },
2587
- onProgress: (message: string) => {
2588
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
2589
- this.ctx.ui.requestRender();
3006
+ this.ctx.editorContainer.addChild(codeInput);
3007
+ this.ctx.ui.setFocus(codeInput);
3008
+ this.ctx.ui.requestRender();
3009
+ return promise;
3010
+ },
3011
+ onProgress: (message: string) => {
3012
+ this.ctx.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
3013
+ this.ctx.ui.requestRender();
3014
+ },
3015
+ onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
2590
3016
  },
2591
- onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
2592
- });
3017
+ { manualCode },
3018
+ );
2593
3019
  await this.ctx.session.modelRegistry.refresh();
2594
3020
  this.ctx.chatContainer.addChild(new Spacer(1));
2595
- this.ctx.chatContainer.addChild(
2596
- new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
2597
- );
2598
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
2599
- await this.#handlePostLoginModelProfileRecommendation(providerId);
3021
+ const successMessage =
3022
+ providerId === "opencodex"
3023
+ ? `${theme.status.success} OpenCodex proxy status checked`
3024
+ : `${theme.status.success} Successfully logged in to ${providerId}`;
3025
+ this.ctx.chatContainer.addChild(new Text(theme.fg("success", successMessage), 1, 0));
3026
+ if (providerId !== "opencodex") {
3027
+ this.ctx.chatContainer.addChild(
3028
+ new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0),
3029
+ );
3030
+ await this.#handlePostLoginModelProfileRecommendation(providerId);
3031
+ }
2600
3032
  this.ctx.ui.requestRender();
2601
3033
  } catch (error: unknown) {
2602
3034
  this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -2636,7 +3068,7 @@ export class SelectorController {
2636
3068
  return;
2637
3069
  }
2638
3070
  if (mode === "login") {
2639
- await this.#handleOAuthLogin(providerId);
3071
+ await this.#handleOAuthLogin(providerId, options);
2640
3072
  } else {
2641
3073
  await this.#handleOAuthLogout(providerId);
2642
3074
  }