@gajae-code/coding-agent 0.12.7 → 0.12.8

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 (95) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/types/commands/notify.d.ts +4 -0
  3. package/dist/types/config/file-lock.d.ts +3 -0
  4. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  5. package/dist/types/config/model-registry.d.ts +2 -0
  6. package/dist/types/config/resolve-config-value.d.ts +2 -1
  7. package/dist/types/config/settings-schema.d.ts +17 -1
  8. package/dist/types/main.d.ts +1 -0
  9. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  10. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  11. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  12. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  13. package/dist/types/sdk/broker/broker.d.ts +5 -1
  14. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  15. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  16. package/dist/types/sdk/bus/config.d.ts +75 -113
  17. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  18. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  19. package/dist/types/sdk/bus/index.d.ts +1 -1
  20. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  21. package/dist/types/sdk/bus/notification-service.d.ts +31 -4
  22. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  23. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  24. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  25. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  26. package/dist/types/sdk/client/discovery.d.ts +2 -1
  27. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  28. package/dist/types/sdk/index.d.ts +1 -0
  29. package/dist/types/sdk/providers.d.ts +22 -0
  30. package/dist/types/sdk/session-directory.d.ts +1 -1
  31. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  32. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  33. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
  34. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  35. package/dist/types/session/session-manager.d.ts +3 -1
  36. package/dist/types/session/session-storage.d.ts +2 -0
  37. package/package.json +10 -8
  38. package/scripts/build-sdk-package-smoke.ts +51 -0
  39. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  40. package/src/cli/notify-cli.ts +204 -94
  41. package/src/commands/notify.ts +19 -8
  42. package/src/config/file-lock.ts +6 -0
  43. package/src/config/model-discovery-manager.ts +50 -8
  44. package/src/config/model-registry.ts +862 -75
  45. package/src/config/resolve-config-value.ts +10 -8
  46. package/src/config/settings-schema.ts +25 -15
  47. package/src/config/settings.ts +15 -15
  48. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  49. package/src/internal-urls/docs-index.generated.ts +6 -6
  50. package/src/main.ts +43 -7
  51. package/src/modes/DESIGN.md +11 -11
  52. package/src/modes/components/hook-selector.ts +155 -44
  53. package/src/modes/components/model-selector.ts +242 -49
  54. package/src/modes/components/notifications-settings-editor.ts +631 -85
  55. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  56. package/src/modes/controllers/selector-controller.ts +464 -50
  57. package/src/sdk/broker/broker.ts +54 -10
  58. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  59. package/src/sdk/broker/lifecycle.ts +47 -50
  60. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  61. package/src/sdk/bus/chat-daemon-control.ts +35 -6
  62. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  63. package/src/sdk/bus/config.ts +534 -286
  64. package/src/sdk/bus/discord-live-provider.ts +57 -2
  65. package/src/sdk/bus/discord-provider.ts +21 -0
  66. package/src/sdk/bus/index.ts +97 -35
  67. package/src/sdk/bus/notification-orchestration.ts +371 -68
  68. package/src/sdk/bus/notification-service.ts +431 -130
  69. package/src/sdk/bus/session-control.ts +119 -44
  70. package/src/sdk/bus/slack-live-provider.ts +78 -2
  71. package/src/sdk/bus/slack-provider.ts +24 -0
  72. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  73. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  74. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  75. package/src/sdk/bus/telegram-daemon.ts +23 -15
  76. package/src/sdk/client/discovery.ts +10 -4
  77. package/src/sdk/host/query/handlers.ts +20 -1
  78. package/src/sdk/index.ts +1 -0
  79. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  80. package/src/sdk/protocol/operation-registry.ts +5 -2
  81. package/src/sdk/providers.ts +51 -0
  82. package/src/sdk/session-directory.ts +1 -0
  83. package/src/sdk/session.ts +10 -7
  84. package/src/session/agent-session.ts +44 -24
  85. package/src/session/internal/managed-session-scope.ts +981 -450
  86. package/src/session/internal/managed-session-storage.ts +382 -45
  87. package/src/session/internal/native-publish-outcome.ts +3 -1
  88. package/src/session/internal/session-open-errors.ts +11 -0
  89. package/src/session/session-manager.ts +27 -3
  90. package/src/session/session-storage.ts +72 -5
  91. package/src/slash-commands/builtin-registry.ts +82 -4
  92. package/src/task/index.ts +9 -3
  93. package/src/tools/ask.ts +1 -1
  94. package/src/tools/bash.ts +1 -1
  95. package/src/tools/telegram-send.ts +4 -2
@@ -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";
@@ -234,6 +248,7 @@ type TelegramDaemonStopResult = {
234
248
 
235
249
  export interface NotificationsEditorOperationDependencies {
236
250
  getNotificationConfig: typeof getNotificationConfig;
251
+ getCurrentTelegramActivationMarker: typeof getCurrentTelegramActivationMarker;
237
252
  maskToken: typeof maskToken;
238
253
  buildNotificationStatusReport: typeof buildNotificationStatusReport;
239
254
  checkNotificationHealth: typeof checkNotificationHealth;
@@ -258,10 +273,12 @@ export interface NotificationsEditorOperationDependencies {
258
273
  reloadTelegramDaemon(settings: Settings): Promise<{ ok: boolean; message: string }>;
259
274
  restartTelegramDaemon(settings: Settings): Promise<{ ok: boolean; message: string }>;
260
275
  stopTelegramDaemon(settings: Settings): Promise<TelegramDaemonStopResult>;
276
+ providerRuntime?: NotificationProviderRuntimeAuthority;
261
277
  }
262
278
 
263
279
  const notificationEditorOperationDependencies: NotificationsEditorOperationDependencies = {
264
280
  getNotificationConfig,
281
+ getCurrentTelegramActivationMarker,
265
282
  maskToken,
266
283
  buildNotificationStatusReport,
267
284
  checkNotificationHealth,
@@ -298,9 +315,9 @@ function unavailableNotificationSessionStatus(): NotificationSessionStatus {
298
315
  return {
299
316
  eligible: false,
300
317
  locallyEnabled: true,
301
- effectiveEnabled: false,
318
+ genericSessionEnabled: false,
319
+ genericEligibilitySource: "none",
302
320
  running: false,
303
- environment: "off",
304
321
  };
305
322
  }
306
323
 
@@ -328,6 +345,10 @@ export function createNotificationsEditorOperations(
328
345
  ): NotificationsEditorOperations {
329
346
  const services = { ...notificationEditorOperationDependencies, ...overrides };
330
347
  const drafts = new WeakMap<PreparedTelegramConfiguration, string>();
348
+ const providerDrafts = new WeakMap<
349
+ PreparedNotificationProviderConfiguration,
350
+ { botToken?: string; appToken?: string }
351
+ >();
331
352
  const sessionContext = () => ({ sessionManager: ctx.sessionManager });
332
353
  const notifyAfterDurableCommit = async (): Promise<void> => {
333
354
  await ctx.notifyConfigChanged?.();
@@ -340,12 +361,52 @@ export function createNotificationsEditorOperations(
340
361
  });
341
362
  const telegramSetupPreflight = async (): Promise<TelegramSetupPreflight> =>
342
363
  await services.resolveTelegramSetupPreflight(ctx.settings);
364
+ const providerRuntime: NotificationProviderRuntimeAuthority = services.providerRuntime ?? {
365
+ activate: async provider => {
366
+ if (provider === "telegram") {
367
+ const result = await reconnect();
368
+ if (result === "blocked_identity" || result === "disabled") {
369
+ throw new Error("Telegram activation failed after the durable save.");
370
+ }
371
+ await ctx.session.notificationSessionController?.reconcileCurrentSession(sessionContext());
372
+ return;
373
+ }
374
+ const result = await new ChatDaemonController(ctx.settings, provider).ensure();
375
+ if (result === "disabled") throw new Error(`${provider} activation failed after the durable save.`);
376
+ },
377
+ deactivate: async provider => {
378
+ if (provider === "telegram") {
379
+ const result = await services.stopTelegramDaemon(ctx.settings);
380
+ if (!result.ok) throw new Error(result.message);
381
+ await ctx.session.notificationSessionController?.reconcileCurrentSession(sessionContext());
382
+ return;
383
+ }
384
+ const result = await new ChatDaemonController(ctx.settings, provider).stop();
385
+ if (!result.ok) throw new Error(result.message);
386
+ },
387
+ };
343
388
 
344
389
  return {
345
390
  loadState: async () => {
346
391
  const config = services.getNotificationConfig(ctx.settings);
392
+ const status = services.buildNotificationStatusReport(ctx.settings);
393
+ const runtime = async (provider: NotificationProvider) => {
394
+ const daemon =
395
+ provider === "telegram"
396
+ ? await new TelegramDaemonController(ctx.settings).status()
397
+ : await new ChatDaemonController(ctx.settings, provider).status();
398
+ return daemon.health === "running" ? "ready" : daemon.health === "not_configured" ? "inactive" : "failed";
399
+ };
400
+ const [telegramRuntime, discordRuntime, slackRuntime] = await Promise.all([
401
+ runtime("telegram"),
402
+ runtime("discord"),
403
+ runtime("slack"),
404
+ ]);
405
+ status.telegram.runtime = telegramRuntime;
406
+ status.discord.runtime = discordRuntime;
407
+ status.slack.runtime = slackRuntime;
347
408
  return {
348
- status: services.buildNotificationStatusReport(ctx.settings),
409
+ status,
349
410
  session:
350
411
  ctx.session.notificationSessionController?.query(sessionContext()) ??
351
412
  unavailableNotificationSessionStatus(),
@@ -362,13 +423,14 @@ export function createNotificationsEditorOperations(
362
423
  };
363
424
  },
364
425
 
365
- refreshHealth: async ({ probe, signal }) => {
426
+ refreshHealth: async ({ probe, provider, signal }) => {
366
427
  if (signal?.aborted) throw new Error("Notification health refresh cancelled.");
367
428
  try {
368
429
  const input: Parameters<typeof checkNotificationHealth>[0] & { signal?: AbortSignal } = {
369
430
  settings: ctx.settings,
370
431
  stateRoot: path.join(ctx.sessionManager.getCwd(), ".gjc", "state"),
371
432
  probe,
433
+ provider,
372
434
  signal,
373
435
  };
374
436
  const report = await services.checkNotificationHealth(input);
@@ -390,9 +452,21 @@ export function createNotificationsEditorOperations(
390
452
  }
391
453
  },
392
454
 
393
- sendTest: async () => {
455
+ sendTest: async provider => {
394
456
  try {
395
- const result = await services.sendNotificationTest({ settings: ctx.settings });
457
+ const result = await services.sendNotificationTest({
458
+ settings: ctx.settings,
459
+ provider,
460
+ deps: {
461
+ providerRuntimeStatus: async selected => {
462
+ const status =
463
+ selected === "telegram"
464
+ ? await new TelegramDaemonController(ctx.settings).status()
465
+ : await new ChatDaemonController(ctx.settings, selected).status();
466
+ return status.health === "running" ? "ready" : "inactive";
467
+ },
468
+ },
469
+ });
396
470
  return {
397
471
  ...result,
398
472
  detail: services.sanitizeDiagnostic(
@@ -531,12 +605,14 @@ export function createNotificationsEditorOperations(
531
605
  commitConfigure: async draft => {
532
606
  const token = drafts.get(draft);
533
607
  if (!token) throw new Error("The Telegram setup draft expired. Re-enter the masked bot token.");
608
+ let receipt: SettingsAtomicReceipt | undefined;
534
609
  try {
535
610
  const inactiveMarkerToClear = observedTelegramActivationMarker(ctx.settings, token, draft.chatId);
536
- const receipt = await ctx.settings.commitAtomicBatch([
611
+ receipt = await ctx.settings.commitAtomicBatch([
537
612
  { path: "notifications.enabled", op: "set", value: true },
538
613
  { path: "notifications.telegram.botToken", op: "set", value: token },
539
614
  { path: "notifications.telegram.chatId", op: "set", value: draft.chatId },
615
+ { path: "notifications.telegram.enabled", op: "set", value: true },
540
616
  { path: "notifications.telegram.rich.enabled", op: "set", value: draft.richEnabled },
541
617
  { path: "notifications.telegram.richDraft.enabled", op: "set", value: draft.richDraftEnabled },
542
618
  { path: "notifications.telegram.streaming.enabled", op: "set", value: draft.streamingEnabled },
@@ -571,12 +647,20 @@ export function createNotificationsEditorOperations(
571
647
  marker: activationMarker,
572
648
  },
573
649
  });
574
- await notifyAfterDurableCommit();
650
+ let observerFailed = false;
651
+ try {
652
+ await notifyAfterDurableCommit();
653
+ } catch {
654
+ observerFailed = true;
655
+ }
575
656
  if (activation.status === "blocked_identity") {
576
657
  return {
577
658
  status: "blocked_identity" as const,
578
659
  receipt,
579
- message: services.sanitizeDiagnostic(activation.message, token),
660
+ message: services.sanitizeDiagnostic(
661
+ `${activation.message}${observerFailed ? " The settings observer also failed after the durable commit." : ""}`,
662
+ token,
663
+ ),
580
664
  restore: async () => {
581
665
  const restored = await activation.restore();
582
666
  if (restored.status === "restored" || restored.status === "still_blocked") {
@@ -587,12 +671,36 @@ export function createNotificationsEditorOperations(
587
671
  retainCommitted: () => activation.retainCommitted(),
588
672
  };
589
673
  }
674
+ if (activation.status === "activation_failed") {
675
+ return {
676
+ status: "activation_failed" as const,
677
+ receipt,
678
+ message: services.sanitizeDiagnostic(
679
+ `${activation.message}${observerFailed ? " The settings observer also failed after the durable commit." : ""}`,
680
+ token,
681
+ ),
682
+ };
683
+ }
684
+ if (observerFailed) {
685
+ return {
686
+ status: "observer_failed" as const,
687
+ receipt,
688
+ message: "Telegram configuration was saved and activated, but the settings observer failed.",
689
+ };
690
+ }
590
691
  return {
591
692
  status: "saved" as const,
592
693
  receipt,
593
694
  message: services.sanitizeDiagnostic("Telegram configuration saved and reconciled.", token),
594
695
  };
595
696
  } catch (error) {
697
+ if (receipt) {
698
+ return {
699
+ status: "activation_failed" as const,
700
+ receipt,
701
+ message: "Telegram configuration was saved, but post-commit activation failed.",
702
+ };
703
+ }
596
704
  throw notificationOperationError(services, error, token);
597
705
  }
598
706
  },
@@ -607,7 +715,15 @@ export function createNotificationsEditorOperations(
607
715
  chatId: draft.chatId,
608
716
  });
609
717
  drafts.delete(draft);
610
- await notifyAfterDurableCommit();
718
+ try {
719
+ await notifyAfterDurableCommit();
720
+ } catch {
721
+ return {
722
+ status: "observer_failed" as const,
723
+ receipt: result.receipt,
724
+ message: "Telegram configuration was saved inactive, but the settings observer failed.",
725
+ };
726
+ }
611
727
  return {
612
728
  status: "saved_inactive" as const,
613
729
  receipt: result.receipt,
@@ -622,28 +738,289 @@ export function createNotificationsEditorOperations(
622
738
  drafts.delete(draft);
623
739
  },
624
740
 
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);
741
+ prepareProviderConfiguration: async input => {
742
+ const cfg = services.getNotificationConfig(ctx.settings);
743
+ const consumeSecret = (action: typeof input.botToken): string | undefined => {
744
+ if (!action.value) return undefined;
745
+ const value = action.value.consume();
746
+ return action.action === "replace" ? value : undefined;
747
+ };
748
+ const botToken = consumeSecret(input.botToken);
749
+ const appToken = input.appToken ? consumeSecret(input.appToken) : undefined;
750
+ if (input.botToken.action === "replace" && !botToken?.trim()) {
751
+ throw new Error("A non-blank bot token replacement is required.");
752
+ }
753
+ if (input.provider === "slack" && input.appToken?.action === "replace" && !appToken?.trim()) {
754
+ throw new Error("A non-blank Slack app token replacement is required.");
634
755
  }
756
+ if (input.provider === "discord") {
757
+ const applicationId = input.applicationId?.trim() || undefined;
758
+ const guildId = input.guildId?.trim() || undefined;
759
+ const parentChannelId = input.parentChannelId?.trim() || undefined;
760
+ const resolvedApplicationId = applicationId ?? cfg.discord.applicationId;
761
+ const resolvedGuildId = guildId ?? cfg.discord.guildId;
762
+ const resolvedParentChannelId = parentChannelId ?? cfg.discord.parentChannelId;
763
+ const removesSecret = input.botToken.action === "remove";
764
+ if (!removesSecret && (!resolvedApplicationId || !resolvedGuildId || !resolvedParentChannelId)) {
765
+ throw new Error("Discord application, guild, and parent channel IDs are required.");
766
+ }
767
+ if (!removesSecret && input.botToken.action === "keep" && !cfg.discord.botToken) {
768
+ throw new Error("Discord has no stored bot token to keep.");
769
+ }
770
+ const draft: PreparedNotificationProviderConfiguration = {
771
+ provider: "discord",
772
+ botTokenDisposition: input.botToken.action,
773
+ botTokenMask: maskToken(botToken ?? cfg.discord.botToken),
774
+ ...(applicationId === undefined ? {} : { applicationId }),
775
+ ...(guildId === undefined ? {} : { guildId }),
776
+ ...(parentChannelId === undefined ? {} : { parentChannelId }),
777
+ applicationIdDisplay: resolvedApplicationId,
778
+ guildIdDisplay: resolvedGuildId,
779
+ parentChannelIdDisplay: resolvedParentChannelId,
780
+ };
781
+ providerDrafts.set(draft, { ...(botToken === undefined ? {} : { botToken }) });
782
+ return draft;
783
+ }
784
+ const workspaceId = input.workspaceId?.trim() || undefined;
785
+ const channelId = input.channelId?.trim() || undefined;
786
+ const authorizedUserId = input.authorizedUserId?.trim() || undefined;
787
+ const resolvedWorkspaceId = workspaceId ?? cfg.slack.workspaceId;
788
+ const resolvedChannelId = channelId ?? cfg.slack.channelId;
789
+ const resolvedAuthorizedUserId = authorizedUserId ?? cfg.slack.authorizedUserId;
790
+ const appDisposition = input.appToken?.action ?? "keep";
791
+ const removesSecret = input.botToken.action === "remove" || appDisposition === "remove";
792
+ if (!removesSecret && (!resolvedWorkspaceId || !resolvedChannelId)) {
793
+ throw new Error("Slack workspace and channel IDs are required.");
794
+ }
795
+ if (!removesSecret && input.botToken.action === "keep" && !cfg.slack.botToken) {
796
+ throw new Error("Slack has no stored bot token to keep.");
797
+ }
798
+ if (!removesSecret && appDisposition === "keep" && !cfg.slack.appToken) {
799
+ throw new Error("Slack has no stored app token to keep.");
800
+ }
801
+ const draft: PreparedNotificationProviderConfiguration = {
802
+ provider: "slack",
803
+ botTokenDisposition: input.botToken.action,
804
+ botTokenMask: maskToken(botToken ?? cfg.slack.botToken),
805
+ appTokenDisposition: appDisposition,
806
+ appTokenMask: maskToken(appToken ?? cfg.slack.appToken),
807
+ ...(workspaceId === undefined ? {} : { workspaceId }),
808
+ ...(channelId === undefined ? {} : { channelId }),
809
+ ...(authorizedUserId === undefined ? {} : { authorizedUserId }),
810
+ workspaceIdDisplay: resolvedWorkspaceId,
811
+ channelIdDisplay: resolvedChannelId,
812
+ authorizedUserIdDisplay: resolvedAuthorizedUserId,
813
+ };
814
+ providerDrafts.set(draft, {
815
+ ...(botToken === undefined ? {} : { botToken }),
816
+ ...(appToken === undefined ? {} : { appToken }),
817
+ });
818
+ return draft;
635
819
  },
636
820
 
637
- disableGlobally: async () => {
821
+ commitProviderConfiguration: async draft => {
822
+ const secrets = providerDrafts.get(draft) ?? {};
638
823
  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);
824
+ let mutation: NotificationProviderConfigurationMutation;
825
+ if (draft.provider === "discord") {
826
+ mutation = {
827
+ provider: "discord",
828
+ botToken:
829
+ draft.botTokenDisposition === "replace"
830
+ ? { action: "replace", value: secrets.botToken ?? "" }
831
+ : { action: draft.botTokenDisposition },
832
+ applicationId: draft.applicationId,
833
+ guildId: draft.guildId,
834
+ parentChannelId: draft.parentChannelId,
835
+ };
836
+ } else {
837
+ const appDisposition = draft.appTokenDisposition ?? "keep";
838
+ mutation = {
839
+ provider: "slack",
840
+ botToken:
841
+ draft.botTokenDisposition === "replace"
842
+ ? { action: "replace", value: secrets.botToken ?? "" }
843
+ : { action: draft.botTokenDisposition },
844
+ appToken:
845
+ appDisposition === "replace"
846
+ ? { action: "replace", value: secrets.appToken ?? "" }
847
+ : { action: appDisposition },
848
+ workspaceId: draft.workspaceId,
849
+ channelId: draft.channelId,
850
+ authorizedUserId: draft.authorizedUserId,
851
+ };
852
+ }
853
+ const removesSecret = draft.botTokenDisposition === "remove" || draft.appTokenDisposition === "remove";
854
+ const result = await mutateNotificationProvider({
855
+ settings: ctx.settings,
856
+ mutation,
857
+ configureAndActivate: !removesSecret,
858
+ ...(removesSecret ? { desiredEnabled: false } : {}),
859
+ notifyConfigChanged: notifyAfterDurableCommit,
860
+ runtime: providerRuntime,
861
+ });
862
+ const outcome =
863
+ result.status === "commit_failed"
864
+ ? "failed"
865
+ : result.status === "activation_failed" ||
866
+ result.status === "deactivation_failed" ||
867
+ result.observerFailed
868
+ ? "degraded"
869
+ : "success";
870
+ const observerSuffix =
871
+ result.status !== "commit_failed" && result.observerFailed
872
+ ? " The settings observer also failed after the durable commit."
873
+ : "";
874
+ return {
875
+ ...(result.status === "commit_failed" ? {} : { receipt: result.receipt }),
876
+ outcome,
877
+ message:
878
+ (result.status === "activated"
879
+ ? `${draft.provider} configuration saved and activated.`
880
+ : result.status === "activation_failed"
881
+ ? `${draft.provider} configuration and desired intent were saved, but runtime activation failed.`
882
+ : result.status === "deactivation_failed"
883
+ ? `${draft.provider} desired-off configuration was saved, but runtime deactivation failed.`
884
+ : result.status === "commit_failed"
885
+ ? `${draft.provider} configuration was not saved because the CAS commit failed.`
886
+ : `${draft.provider} configuration saved.`) + observerSuffix,
887
+ };
888
+ } finally {
889
+ providerDrafts.delete(draft);
890
+ }
891
+ },
892
+
893
+ discardProviderConfiguration: draft => {
894
+ providerDrafts.delete(draft);
895
+ },
896
+
897
+ setProviderDesired: async (provider, enabled) => {
898
+ if (provider === "telegram" && enabled) {
899
+ const config = services.getNotificationConfig(ctx.settings);
900
+ if (services.getCurrentTelegramActivationMarker(config)) {
901
+ return {
902
+ outcome: "failed",
903
+ message:
904
+ "Telegram remains inactive because its exact activation marker must be restored or cleared after safe owner readiness.",
905
+ };
906
+ }
646
907
  }
908
+ const mutation: NotificationProviderConfigurationMutation =
909
+ provider === "telegram"
910
+ ? { provider, botToken: { action: "keep" } }
911
+ : provider === "discord"
912
+ ? { provider, botToken: { action: "keep" } }
913
+ : { provider, botToken: { action: "keep" }, appToken: { action: "keep" } };
914
+ const result = await mutateNotificationProvider({
915
+ settings: ctx.settings,
916
+ mutation,
917
+ desiredEnabled: enabled,
918
+ notifyConfigChanged: notifyAfterDurableCommit,
919
+ runtime: providerRuntime,
920
+ });
921
+ const outcome =
922
+ result.status === "commit_failed"
923
+ ? "failed"
924
+ : result.status === "activation_failed" ||
925
+ result.status === "deactivation_failed" ||
926
+ result.observerFailed
927
+ ? "degraded"
928
+ : "success";
929
+ return {
930
+ ...(result.status === "commit_failed" ? {} : { receipt: result.receipt }),
931
+ outcome,
932
+ message:
933
+ (result.status === "activation_failed"
934
+ ? `${provider} desired intent was saved, but activation failed.`
935
+ : result.status === "deactivation_failed"
936
+ ? `${provider} desired-off intent was saved, but deactivation failed.`
937
+ : result.status === "commit_failed"
938
+ ? `${provider} desired intent was not saved because the CAS commit failed.`
939
+ : `${provider} desired intent ${enabled ? "enabled" : "disabled"}.`) +
940
+ (result.status !== "commit_failed" && result.observerFailed
941
+ ? " The settings observer also failed after the durable commit."
942
+ : ""),
943
+ };
944
+ },
945
+
946
+ removeProvider: async provider => {
947
+ if (provider === "telegram") {
948
+ return { message: "Use the Telegram removal action to preserve root-registration fencing." };
949
+ }
950
+ const result = await removeNotificationProvider({
951
+ settings: ctx.settings,
952
+ provider,
953
+ runtime: providerRuntime,
954
+ notifyConfigChanged: notifyAfterDurableCommit,
955
+ });
956
+ const degraded =
957
+ result.status === "deactivation_failed" || ("observerFailed" in result && result.observerFailed);
958
+ return {
959
+ ...("receipt" in result && result.receipt ? { receipt: result.receipt } : {}),
960
+ outcome:
961
+ result.status === "commit_failed" || result.status === "commit_failed_after_teardown"
962
+ ? "failed"
963
+ : degraded
964
+ ? "degraded"
965
+ : "success",
966
+ message:
967
+ (result.status === "removed"
968
+ ? `${provider} configuration removed.`
969
+ : result.status === "deactivation_failed"
970
+ ? `${provider} configuration was removed, but runtime deactivation failed.`
971
+ : `${provider} configuration removal failed.`) +
972
+ ("observerFailed" in result && result.observerFailed
973
+ ? " The settings observer also failed after the durable commit."
974
+ : ""),
975
+ };
976
+ },
977
+
978
+ enableGlobally: async () => {
979
+ const result = await setGlobalNotificationsEnabled({
980
+ settings: ctx.settings,
981
+ enabled: true,
982
+ notifyConfigChanged: notifyAfterDurableCommit,
983
+ runtime: providerRuntime,
984
+ });
985
+ const degraded =
986
+ result.status === "global_activation_partial" || ("observerFailed" in result && result.observerFailed);
987
+ return {
988
+ ...(result.status === "commit_failed" ? {} : { receipt: result.receipt }),
989
+ outcome: result.status === "commit_failed" ? "failed" : degraded ? "degraded" : "success",
990
+ message:
991
+ (result.status === "global_activation_partial"
992
+ ? `Global notifications were enabled, but activation failed for ${result.failed.join(", ")}.`
993
+ : result.status === "commit_failed"
994
+ ? "Global notifications were not enabled because the CAS commit failed."
995
+ : "Global notifications enabled using stored provider intent.") +
996
+ (result.status !== "commit_failed" && result.observerFailed
997
+ ? " The settings observer also failed after the durable commit."
998
+ : ""),
999
+ };
1000
+ },
1001
+
1002
+ disableGlobally: async () => {
1003
+ const result = await setGlobalNotificationsEnabled({
1004
+ settings: ctx.settings,
1005
+ enabled: false,
1006
+ notifyConfigChanged: notifyAfterDurableCommit,
1007
+ runtime: providerRuntime,
1008
+ });
1009
+ const degraded =
1010
+ result.status === "global_deactivation_partial" || ("observerFailed" in result && result.observerFailed);
1011
+ return {
1012
+ ...(result.status === "commit_failed" ? {} : { receipt: result.receipt }),
1013
+ outcome: result.status === "commit_failed" ? "failed" : degraded ? "degraded" : "success",
1014
+ message:
1015
+ (result.status === "global_deactivation_partial"
1016
+ ? `Global notifications were disabled, but teardown failed for ${result.failed.join(", ")}.`
1017
+ : result.status === "commit_failed"
1018
+ ? "Global notifications were not disabled because the CAS commit failed."
1019
+ : "Global notifications disabled; provider configuration and desired intent were preserved.") +
1020
+ (result.status !== "commit_failed" && result.observerFailed
1021
+ ? " The settings observer also failed after the durable commit."
1022
+ : ""),
1023
+ };
647
1024
  },
648
1025
 
649
1026
  removeTelegram: async () => {
@@ -676,17 +1053,27 @@ export function createNotificationsEditorOperations(
676
1053
  },
677
1054
  },
678
1055
  });
1056
+ const postCommitFailures: string[] = [];
679
1057
  if (runtimePrepared && controller) {
680
- await controller.clearBlockedRuntime(sessionContext());
681
- await controller.reconcileCurrentSession(sessionContext());
1058
+ try {
1059
+ await controller.clearBlockedRuntime(sessionContext());
1060
+ await controller.reconcileCurrentSession(sessionContext());
1061
+ } catch {
1062
+ postCommitFailures.push("current-session reconciliation failed");
1063
+ }
1064
+ }
1065
+ try {
1066
+ await notifyAfterDurableCommit();
1067
+ } catch {
1068
+ postCommitFailures.push("settings observer failed");
682
1069
  }
683
- await notifyAfterDurableCommit();
684
1070
  return {
685
1071
  receipt: result.receipt,
1072
+ outcome: postCommitFailures.length === 0 ? "success" : "degraded",
686
1073
  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.",
1074
+ message: `Telegram configuration removed without changing the global master.${
1075
+ postCommitFailures.length === 0 ? "" : ` Post-commit ${postCommitFailures.join(" and ")}.`
1076
+ }`,
690
1077
  };
691
1078
  } catch (error) {
692
1079
  if (runtimePrepared) {
@@ -715,7 +1102,10 @@ export function createNotificationsEditorOperations(
715
1102
  try {
716
1103
  const before = services.getNotificationConfig(ctx.settings);
717
1104
  const disablingToolActivity =
718
- isTelegramConfigured(before) && before.toolActivity.enabled && !preferences.toolActivityEnabled;
1105
+ isProviderEffectivelyEnabled(before, "telegram") &&
1106
+ isTelegramComplete(before) &&
1107
+ before.toolActivity.enabled &&
1108
+ !preferences.toolActivityEnabled;
719
1109
  if (disablingToolActivity) {
720
1110
  const stopped = await services.stopTelegramDaemon(ctx.settings);
721
1111
  if (!stopped.ok)
@@ -725,7 +1115,7 @@ export function createNotificationsEditorOperations(
725
1115
  daemonWasRunningForDisable = stopped.before?.health === "running";
726
1116
  }
727
1117
 
728
- let receipt: Awaited<ReturnType<typeof ctx.settings.commitAtomicBatch>>;
1118
+ let receipt: SettingsAtomicReceipt;
729
1119
  try {
730
1120
  receipt = await ctx.settings.commitAtomicBatch([
731
1121
  { path: "notifications.redact", op: "set", value: preferences.redact },
@@ -758,16 +1148,31 @@ export function createNotificationsEditorOperations(
758
1148
  throw error;
759
1149
  }
760
1150
 
1151
+ const postCommitFailures: string[] = [];
761
1152
  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}`);
1153
+ if (isProviderEffectivelyEnabled(config, "telegram") && isTelegramComplete(config)) {
1154
+ try {
1155
+ const reload = daemonWasRunningForDisable
1156
+ ? await services.restartTelegramDaemon(ctx.settings)
1157
+ : await services.reloadTelegramDaemon(ctx.settings);
1158
+ if (!reload.ok) postCommitFailures.push(`daemon reload failed: ${reload.message}`);
1159
+ } catch {
1160
+ postCommitFailures.push("daemon reload failed");
1161
+ }
1162
+ }
1163
+ try {
1164
+ await notifyAfterDurableCommit();
1165
+ } catch {
1166
+ postCommitFailures.push("settings observer failed");
768
1167
  }
769
- await notifyAfterDurableCommit();
770
- return { receipt, message: "Notification preferences saved atomically." };
1168
+ return {
1169
+ receipt,
1170
+ outcome: postCommitFailures.length === 0 ? "success" : "degraded",
1171
+ message:
1172
+ postCommitFailures.length === 0
1173
+ ? "Notification preferences saved atomically."
1174
+ : `Notification preferences were saved, but post-commit ${postCommitFailures.join(" and ")}.`,
1175
+ };
771
1176
  } catch (error) {
772
1177
  throw notificationOperationError(services, error, services.getNotificationConfig(ctx.settings).botToken);
773
1178
  }
@@ -2544,6 +2949,9 @@ export class SelectorController {
2544
2949
  this.ctx.showStatus(`Logging in to ${providerId}…`);
2545
2950
  const manualInput = this.ctx.oauthManualInput;
2546
2951
  const useManualInput = CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider);
2952
+ if (providerId === "opencodex") {
2953
+ this.ctx.showStatus("Checking the local OpenCodex proxy…");
2954
+ }
2547
2955
  try {
2548
2956
  await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
2549
2957
  onAuth: (info: { url: string; instructions?: string }) => {
@@ -2592,11 +3000,17 @@ export class SelectorController {
2592
3000
  });
2593
3001
  await this.ctx.session.modelRegistry.refresh();
2594
3002
  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);
3003
+ const successMessage =
3004
+ providerId === "opencodex"
3005
+ ? `${theme.status.success} OpenCodex proxy status checked`
3006
+ : `${theme.status.success} Successfully logged in to ${providerId}`;
3007
+ this.ctx.chatContainer.addChild(new Text(theme.fg("success", successMessage), 1, 0));
3008
+ if (providerId !== "opencodex") {
3009
+ this.ctx.chatContainer.addChild(
3010
+ new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0),
3011
+ );
3012
+ await this.#handlePostLoginModelProfileRecommendation(providerId);
3013
+ }
2600
3014
  this.ctx.ui.requestRender();
2601
3015
  } catch (error: unknown) {
2602
3016
  this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);