@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.
- package/CHANGELOG.md +19 -0
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +31 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/session/session-storage.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/src/cli/notify-cli.ts +204 -94
- package/src/commands/notify.ts +19 -8
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-registry.ts +862 -75
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +464 -50
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +97 -35
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +431 -130
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +10 -7
- package/src/session/agent-session.ts +44 -24
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +382 -45
- package/src/session/internal/native-publish-outcome.ts +3 -1
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/session-manager.ts +27 -3
- package/src/session/session-storage.ts +72 -5
- package/src/slash-commands/builtin-registry.ts +82 -4
- package/src/task/index.ts +9 -3
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/telegram-send.ts +4 -2
|
@@ -2,10 +2,13 @@ import type { CasRestoreResult } from "../../config/atomic-yaml-patch";
|
|
|
2
2
|
import type { RawSettings, SettingsAtomicPatch, SettingsAtomicReceipt } from "../../config/settings";
|
|
3
3
|
import { isProcessIncarnation, processIncarnation } from "../broker/process-incarnation";
|
|
4
4
|
import {
|
|
5
|
+
getCurrentTelegramActivationMarker,
|
|
5
6
|
getNotificationConfig,
|
|
6
7
|
hasNonBlankValue,
|
|
8
|
+
isProviderEffectivelyEnabled,
|
|
9
|
+
type NotificationProvider,
|
|
7
10
|
type NotificationSettingsReader,
|
|
8
|
-
type
|
|
11
|
+
type ProviderSecretDisposition,
|
|
9
12
|
readTelegramActivationMarkers,
|
|
10
13
|
type TelegramActivationMarker,
|
|
11
14
|
type TelegramActivationMarkers,
|
|
@@ -136,7 +139,7 @@ export async function proposedTelegramIdentity(
|
|
|
136
139
|
|
|
137
140
|
export interface NotificationConfigurationWriter extends NotificationSettingsReader {
|
|
138
141
|
commitAtomicBatch(patches: readonly SettingsAtomicPatch[]): Promise<SettingsAtomicReceipt>;
|
|
139
|
-
commitAtomicBatchWithCurrent
|
|
142
|
+
commitAtomicBatchWithCurrent(
|
|
140
143
|
buildPatches: (
|
|
141
144
|
current: Readonly<RawSettings>,
|
|
142
145
|
) => Promise<readonly SettingsAtomicPatch[]> | readonly SettingsAtomicPatch[],
|
|
@@ -181,16 +184,7 @@ async function commitNotificationBatchWithCurrent(
|
|
|
181
184
|
current: Readonly<RawSettings>,
|
|
182
185
|
) => Promise<readonly SettingsAtomicPatch[]> | readonly SettingsAtomicPatch[],
|
|
183
186
|
): Promise<SettingsAtomicReceipt> {
|
|
184
|
-
|
|
185
|
-
const snapshot = settings.getNotificationSettingsSnapshot();
|
|
186
|
-
const current: RawSettings = {
|
|
187
|
-
notifications: {
|
|
188
|
-
telegram: {
|
|
189
|
-
activation: snapshot.telegram.activation,
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
return settings.commitAtomicBatch(await buildPatches(current));
|
|
187
|
+
return settings.commitAtomicBatchWithCurrent(buildPatches);
|
|
194
188
|
}
|
|
195
189
|
|
|
196
190
|
function sameActivationMarker(left: TelegramActivationMarker, right: TelegramActivationMarker): boolean {
|
|
@@ -231,36 +225,9 @@ export async function clearTelegramActivationMarker(
|
|
|
231
225
|
receipt.discard();
|
|
232
226
|
}
|
|
233
227
|
|
|
234
|
-
export type CompleteNonTelegramAdapter = "discord" | "slack";
|
|
235
|
-
|
|
236
|
-
function completeNonTelegramAdapters(snapshot: NotificationSettingsSnapshot): CompleteNonTelegramAdapter[] {
|
|
237
|
-
if (!snapshot.enabled) return [];
|
|
238
|
-
const adapters: CompleteNonTelegramAdapter[] = [];
|
|
239
|
-
if (
|
|
240
|
-
hasNonBlankValue(snapshot.discord.botToken) &&
|
|
241
|
-
hasNonBlankValue(snapshot.discord.applicationId) &&
|
|
242
|
-
hasNonBlankValue(snapshot.discord.guildId) &&
|
|
243
|
-
hasNonBlankValue(snapshot.discord.parentChannelId)
|
|
244
|
-
) {
|
|
245
|
-
adapters.push("discord");
|
|
246
|
-
}
|
|
247
|
-
if (
|
|
248
|
-
hasNonBlankValue(snapshot.slack.botToken) &&
|
|
249
|
-
hasNonBlankValue(snapshot.slack.appToken) &&
|
|
250
|
-
hasNonBlankValue(snapshot.slack.workspaceId) &&
|
|
251
|
-
hasNonBlankValue(snapshot.slack.channelId)
|
|
252
|
-
) {
|
|
253
|
-
adapters.push("slack");
|
|
254
|
-
}
|
|
255
|
-
return adapters;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
228
|
export type SaveTelegramInactiveAvailability = { available: true };
|
|
259
229
|
|
|
260
|
-
/**
|
|
261
|
-
* Telegram-specific activation markers let Save inactive preserve globally
|
|
262
|
-
* enabled Discord and Slack adapters, so the action is always available.
|
|
263
|
-
*/
|
|
230
|
+
/** Telegram activation markers make Save inactive independent of sibling providers. */
|
|
264
231
|
export function getSaveTelegramInactiveAvailability(
|
|
265
232
|
_settings: NotificationSettingsReader,
|
|
266
233
|
): SaveTelegramInactiveAvailability {
|
|
@@ -269,10 +236,7 @@ export function getSaveTelegramInactiveAvailability(
|
|
|
269
236
|
|
|
270
237
|
export type SaveTelegramInactiveResult = { status: "saved_inactive"; receipt: SettingsAtomicReceipt };
|
|
271
238
|
|
|
272
|
-
/**
|
|
273
|
-
* Atomically persist Telegram credentials with a non-secret inactive marker.
|
|
274
|
-
* Global notifications are disabled only when Telegram is the sole complete adapter.
|
|
275
|
-
*/
|
|
239
|
+
/** Atomically persist Telegram credentials, desired-off intent, and its inactive marker. */
|
|
276
240
|
export async function saveTelegramInactive(input: {
|
|
277
241
|
settings: NotificationConfigurationWriter;
|
|
278
242
|
botToken: string;
|
|
@@ -281,7 +245,6 @@ export async function saveTelegramInactive(input: {
|
|
|
281
245
|
if (!hasNonBlankValue(input.botToken) || !hasNonBlankValue(input.chatId)) {
|
|
282
246
|
throw new TypeError("Saving inactive Telegram configuration requires a non-blank token and chat ID.");
|
|
283
247
|
}
|
|
284
|
-
const snapshot = input.settings.getNotificationSettingsSnapshot();
|
|
285
248
|
const marker = createTelegramActivationMarker({
|
|
286
249
|
botToken: input.botToken,
|
|
287
250
|
chatId: input.chatId,
|
|
@@ -291,15 +254,12 @@ export async function saveTelegramInactive(input: {
|
|
|
291
254
|
const receipt = await commitNotificationBatchWithCurrent(input.settings, current => {
|
|
292
255
|
const markers = activationMarkersFromCurrent(current);
|
|
293
256
|
markers[marker.identity] = marker;
|
|
294
|
-
|
|
257
|
+
return [
|
|
295
258
|
{ path: "notifications.telegram.botToken", op: "set", value: input.botToken },
|
|
296
259
|
{ path: "notifications.telegram.chatId", op: "set", value: input.chatId },
|
|
260
|
+
{ path: "notifications.telegram.enabled", op: "set", value: false },
|
|
297
261
|
{ path: "notifications.telegram.activation", op: "set", value: markers },
|
|
298
262
|
];
|
|
299
|
-
if (completeNonTelegramAdapters(snapshot).length === 0) {
|
|
300
|
-
patches.push({ path: "notifications.enabled", op: "set", value: false });
|
|
301
|
-
}
|
|
302
|
-
return patches;
|
|
303
263
|
});
|
|
304
264
|
return { status: "saved_inactive", receipt };
|
|
305
265
|
}
|
|
@@ -318,25 +278,14 @@ export async function removeTelegramConfiguration(input: {
|
|
|
318
278
|
settings: NotificationConfigurationWriter;
|
|
319
279
|
removal: TelegramRemovalRuntime;
|
|
320
280
|
}): Promise<{ receipt: SettingsAtomicReceipt; globallyDisabled: boolean }> {
|
|
321
|
-
const cfg = getNotificationConfig(input.settings);
|
|
322
|
-
const otherAdapterRemains =
|
|
323
|
-
(hasNonBlankValue(cfg.discord.botToken) &&
|
|
324
|
-
hasNonBlankValue(cfg.discord.applicationId) &&
|
|
325
|
-
hasNonBlankValue(cfg.discord.guildId) &&
|
|
326
|
-
hasNonBlankValue(cfg.discord.parentChannelId)) ||
|
|
327
|
-
(hasNonBlankValue(cfg.slack.botToken) &&
|
|
328
|
-
hasNonBlankValue(cfg.slack.appToken) &&
|
|
329
|
-
hasNonBlankValue(cfg.slack.workspaceId) &&
|
|
330
|
-
hasNonBlankValue(cfg.slack.channelId));
|
|
331
281
|
await input.removal.stopAndUnregister();
|
|
332
|
-
const
|
|
282
|
+
const receipt = await input.settings.commitAtomicBatch([
|
|
333
283
|
{ path: "notifications.telegram.botToken", op: "unset" },
|
|
334
284
|
{ path: "notifications.telegram.chatId", op: "unset" },
|
|
335
285
|
{ path: "notifications.telegram.activation", op: "unset" },
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return { receipt, globallyDisabled: !otherAdapterRemains };
|
|
286
|
+
{ path: "notifications.telegram.enabled", op: "set", value: false },
|
|
287
|
+
]);
|
|
288
|
+
return { receipt, globallyDisabled: false };
|
|
340
289
|
}
|
|
341
290
|
|
|
342
291
|
/** Detailed outcome of checking or reconnecting the Telegram daemon after a durable commit. */
|
|
@@ -365,9 +314,20 @@ export interface TelegramPostCommitActivation {
|
|
|
365
314
|
}
|
|
366
315
|
|
|
367
316
|
export type PostCommitTelegramActivationResult =
|
|
368
|
-
| {
|
|
317
|
+
| {
|
|
318
|
+
status: "activated";
|
|
319
|
+
receipt: SettingsAtomicReceipt;
|
|
320
|
+
reconnect: Exclude<TelegramDaemonReconnectOutcome, "blocked_identity" | "disabled">;
|
|
321
|
+
}
|
|
322
|
+
| {
|
|
323
|
+
status: "activation_failed";
|
|
324
|
+
receipt: SettingsAtomicReceipt;
|
|
325
|
+
reconnect?: TelegramDaemonReconnectOutcome;
|
|
326
|
+
message: string;
|
|
327
|
+
}
|
|
369
328
|
| {
|
|
370
329
|
status: "blocked_identity";
|
|
330
|
+
receipt: SettingsAtomicReceipt;
|
|
371
331
|
message: string;
|
|
372
332
|
restore(): Promise<BlockedTelegramRestoreResult>;
|
|
373
333
|
retainCommitted(): void;
|
|
@@ -433,7 +393,7 @@ async function restoreBlockedConfiguration(input: {
|
|
|
433
393
|
* owner race stops the endpoint, durably marks only that Telegram identity
|
|
434
394
|
* inactive, then exposes an ordered CAS restore/retain choice.
|
|
435
395
|
*/
|
|
436
|
-
|
|
396
|
+
async function reconcileCommittedTelegramConfigurationUnsafe(input: {
|
|
437
397
|
receipt: SettingsAtomicReceipt;
|
|
438
398
|
activation: TelegramPostCommitActivation;
|
|
439
399
|
inactiveMarkerToClear?: TelegramActivationMarker;
|
|
@@ -443,6 +403,7 @@ export async function reconcileCommittedTelegramConfiguration(input: {
|
|
|
443
403
|
await input.activation.controller.enterBlockedRuntime();
|
|
444
404
|
const inactiveReceipt = await input.activation.persistInactive(input.activation.marker);
|
|
445
405
|
return {
|
|
406
|
+
receipt: input.receipt,
|
|
446
407
|
status: "blocked_identity",
|
|
447
408
|
message:
|
|
448
409
|
"Configuration saved inactive; activation blocked; foreign daemon untouched. Current session stopped because Telegram activation was blocked by a foreign daemon.",
|
|
@@ -453,6 +414,15 @@ export async function reconcileCommittedTelegramConfiguration(input: {
|
|
|
453
414
|
},
|
|
454
415
|
};
|
|
455
416
|
}
|
|
417
|
+
if (reconnect === "disabled") {
|
|
418
|
+
await input.activation.controller.enterBlockedRuntime();
|
|
419
|
+
return {
|
|
420
|
+
receipt: input.receipt,
|
|
421
|
+
status: "activation_failed",
|
|
422
|
+
reconnect,
|
|
423
|
+
message: "Configuration and desired intent were saved, but Telegram runtime activation did not become ready.",
|
|
424
|
+
};
|
|
425
|
+
}
|
|
456
426
|
|
|
457
427
|
if (input.inactiveMarkerToClear) {
|
|
458
428
|
try {
|
|
@@ -464,7 +434,32 @@ export async function reconcileCommittedTelegramConfiguration(input: {
|
|
|
464
434
|
}
|
|
465
435
|
await input.activation.controller.clearBlockedRuntime();
|
|
466
436
|
await input.activation.controller.reconcileCurrentSession();
|
|
467
|
-
return { status: "activated", reconnect };
|
|
437
|
+
return { status: "activated", receipt: input.receipt, reconnect };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export async function reconcileCommittedTelegramConfiguration(input: {
|
|
441
|
+
receipt: SettingsAtomicReceipt;
|
|
442
|
+
activation: TelegramPostCommitActivation;
|
|
443
|
+
inactiveMarkerToClear?: TelegramActivationMarker;
|
|
444
|
+
}): Promise<PostCommitTelegramActivationResult> {
|
|
445
|
+
try {
|
|
446
|
+
return await reconcileCommittedTelegramConfigurationUnsafe(input);
|
|
447
|
+
} catch {
|
|
448
|
+
let fenced = false;
|
|
449
|
+
try {
|
|
450
|
+
await input.activation.controller.enterBlockedRuntime();
|
|
451
|
+
fenced = true;
|
|
452
|
+
} catch {
|
|
453
|
+
fenced = false;
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
status: "activation_failed",
|
|
457
|
+
receipt: input.receipt,
|
|
458
|
+
message: fenced
|
|
459
|
+
? "Configuration and desired intent were saved, but post-commit activation or reconciliation failed; the current runtime was fenced."
|
|
460
|
+
: "Configuration and desired intent were saved, but post-commit activation or reconciliation failed and the current runtime could not be fenced.",
|
|
461
|
+
};
|
|
462
|
+
}
|
|
468
463
|
}
|
|
469
464
|
|
|
470
465
|
export type SaveTelegramConfigurationResult =
|
|
@@ -520,6 +515,7 @@ export async function saveTelegramConfiguration(input: {
|
|
|
520
515
|
{ path: "notifications.telegram.botToken", op: "set", value: input.botToken },
|
|
521
516
|
{ path: "notifications.telegram.chatId", op: "set", value: input.chatId },
|
|
522
517
|
{ path: "notifications.enabled", op: "set", value: true },
|
|
518
|
+
{ path: "notifications.telegram.enabled", op: "set", value: true },
|
|
523
519
|
];
|
|
524
520
|
// With post-commit activation, the marker has its own receipt so a blocked
|
|
525
521
|
// rollback can restore configuration without self-conflicting on the marker
|
|
@@ -545,3 +541,310 @@ export async function saveTelegramConfiguration(input: {
|
|
|
545
541
|
inactiveMarkerToClear,
|
|
546
542
|
});
|
|
547
543
|
}
|
|
544
|
+
|
|
545
|
+
export type ProviderSecretMutation = { action: "keep" } | { action: "replace"; value: string } | { action: "remove" };
|
|
546
|
+
|
|
547
|
+
export interface TelegramProviderConfigurationMutation {
|
|
548
|
+
provider: "telegram";
|
|
549
|
+
botToken: ProviderSecretMutation;
|
|
550
|
+
chatId?: string;
|
|
551
|
+
richEnabled?: boolean;
|
|
552
|
+
richDraftEnabled?: boolean;
|
|
553
|
+
streamingEnabled?: boolean;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export interface DiscordProviderConfigurationMutation {
|
|
557
|
+
provider: "discord";
|
|
558
|
+
botToken: ProviderSecretMutation;
|
|
559
|
+
applicationId?: string;
|
|
560
|
+
guildId?: string;
|
|
561
|
+
parentChannelId?: string;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export interface SlackProviderConfigurationMutation {
|
|
565
|
+
provider: "slack";
|
|
566
|
+
botToken: ProviderSecretMutation;
|
|
567
|
+
appToken: ProviderSecretMutation;
|
|
568
|
+
workspaceId?: string;
|
|
569
|
+
channelId?: string;
|
|
570
|
+
authorizedUserId?: string | null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export type NotificationProviderConfigurationMutation =
|
|
574
|
+
| TelegramProviderConfigurationMutation
|
|
575
|
+
| DiscordProviderConfigurationMutation
|
|
576
|
+
| SlackProviderConfigurationMutation;
|
|
577
|
+
|
|
578
|
+
export interface NotificationProviderRuntimeAuthority {
|
|
579
|
+
activate(provider: NotificationProvider): Promise<void>;
|
|
580
|
+
deactivate(provider: NotificationProvider): Promise<void>;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export type NotificationProviderMutationResult =
|
|
584
|
+
| { status: "saved"; receipt: SettingsAtomicReceipt; observerFailed: boolean }
|
|
585
|
+
| { status: "activated"; receipt: SettingsAtomicReceipt; observerFailed: boolean }
|
|
586
|
+
| { status: "activation_failed"; receipt: SettingsAtomicReceipt; observerFailed: boolean }
|
|
587
|
+
| { status: "deactivation_failed"; receipt: SettingsAtomicReceipt; observerFailed: boolean }
|
|
588
|
+
| { status: "commit_failed" };
|
|
589
|
+
|
|
590
|
+
function secretMutationPatches(
|
|
591
|
+
pathName:
|
|
592
|
+
| "notifications.telegram.botToken"
|
|
593
|
+
| "notifications.discord.botToken"
|
|
594
|
+
| "notifications.slack.botToken"
|
|
595
|
+
| "notifications.slack.appToken",
|
|
596
|
+
mutation: ProviderSecretMutation,
|
|
597
|
+
): SettingsAtomicPatch[] {
|
|
598
|
+
if (mutation.action === "keep") return [];
|
|
599
|
+
if (mutation.action === "remove") return [{ path: pathName, op: "unset" }];
|
|
600
|
+
if (!hasNonBlankValue(mutation.value)) throw new TypeError(`A non-blank replacement is required for ${pathName}.`);
|
|
601
|
+
return [{ path: pathName, op: "set", value: mutation.value }];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function optionalSet(pathName: SettingsAtomicPatch["path"], value: unknown): SettingsAtomicPatch[] {
|
|
605
|
+
return value === undefined ? [] : [{ path: pathName, op: "set", value }];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function selectedProviderPatches(mutation: NotificationProviderConfigurationMutation): SettingsAtomicPatch[] {
|
|
609
|
+
if (mutation.provider === "telegram") {
|
|
610
|
+
return [
|
|
611
|
+
...secretMutationPatches("notifications.telegram.botToken", mutation.botToken),
|
|
612
|
+
...optionalSet("notifications.telegram.chatId", mutation.chatId),
|
|
613
|
+
...optionalSet("notifications.telegram.rich.enabled", mutation.richEnabled),
|
|
614
|
+
...optionalSet("notifications.telegram.richDraft.enabled", mutation.richDraftEnabled),
|
|
615
|
+
...optionalSet("notifications.telegram.streaming.enabled", mutation.streamingEnabled),
|
|
616
|
+
];
|
|
617
|
+
}
|
|
618
|
+
if (mutation.provider === "discord") {
|
|
619
|
+
return [
|
|
620
|
+
...secretMutationPatches("notifications.discord.botToken", mutation.botToken),
|
|
621
|
+
...optionalSet("notifications.discord.applicationId", mutation.applicationId),
|
|
622
|
+
...optionalSet("notifications.discord.guildId", mutation.guildId),
|
|
623
|
+
...optionalSet("notifications.discord.parentChannelId", mutation.parentChannelId),
|
|
624
|
+
];
|
|
625
|
+
}
|
|
626
|
+
return [
|
|
627
|
+
...secretMutationPatches("notifications.slack.botToken", mutation.botToken),
|
|
628
|
+
...secretMutationPatches("notifications.slack.appToken", mutation.appToken),
|
|
629
|
+
...optionalSet("notifications.slack.workspaceId", mutation.workspaceId),
|
|
630
|
+
...optionalSet("notifications.slack.channelId", mutation.channelId),
|
|
631
|
+
...(mutation.authorizedUserId === undefined
|
|
632
|
+
? []
|
|
633
|
+
: mutation.authorizedUserId === null
|
|
634
|
+
? [{ path: "notifications.slack.authorizedUserId", op: "unset" } as const]
|
|
635
|
+
: optionalSet("notifications.slack.authorizedUserId", mutation.authorizedUserId)),
|
|
636
|
+
];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function providerDesiredPath(provider: NotificationProvider): SettingsAtomicPatch["path"] {
|
|
640
|
+
if (provider === "telegram") return "notifications.telegram.enabled";
|
|
641
|
+
if (provider === "discord") return "notifications.discord.enabled";
|
|
642
|
+
return "notifications.slack.enabled";
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function mutationRemovesRequiredSecret(mutation: NotificationProviderConfigurationMutation): boolean {
|
|
646
|
+
if (mutation.provider === "telegram" || mutation.provider === "discord")
|
|
647
|
+
return mutation.botToken.action === "remove";
|
|
648
|
+
return mutation.botToken.action === "remove" || mutation.appToken.action === "remove";
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Sole CAS-backed provider configuration authority. It commits one selected-provider
|
|
653
|
+
* batch before observer/runtime effects and never reports rollback after a durable save.
|
|
654
|
+
*/
|
|
655
|
+
export async function mutateNotificationProvider(input: {
|
|
656
|
+
settings: NotificationConfigurationWriter;
|
|
657
|
+
mutation: NotificationProviderConfigurationMutation;
|
|
658
|
+
desiredEnabled?: boolean;
|
|
659
|
+
configureAndActivate?: boolean;
|
|
660
|
+
notifyConfigChanged?: () => Promise<void> | void;
|
|
661
|
+
redact?: boolean;
|
|
662
|
+
runtime?: NotificationProviderRuntimeAuthority;
|
|
663
|
+
signal?: AbortSignal;
|
|
664
|
+
}): Promise<NotificationProviderMutationResult> {
|
|
665
|
+
if (input.signal?.aborted) return { status: "commit_failed" };
|
|
666
|
+
const provider = input.mutation.provider;
|
|
667
|
+
const removesRequiredSecret = mutationRemovesRequiredSecret(input.mutation);
|
|
668
|
+
const desiredEnabled = removesRequiredSecret ? false : input.desiredEnabled;
|
|
669
|
+
const activate = input.configureAndActivate === true && !removesRequiredSecret;
|
|
670
|
+
const patches = selectedProviderPatches(input.mutation);
|
|
671
|
+
if (activate) {
|
|
672
|
+
patches.push(
|
|
673
|
+
{ path: "notifications.enabled", op: "set", value: true },
|
|
674
|
+
{ path: providerDesiredPath(provider), op: "set", value: true },
|
|
675
|
+
);
|
|
676
|
+
} else if (desiredEnabled !== undefined) {
|
|
677
|
+
patches.push({ path: providerDesiredPath(provider), op: "set", value: desiredEnabled });
|
|
678
|
+
}
|
|
679
|
+
if (input.redact !== undefined) patches.push({ path: "notifications.redact", op: "set", value: input.redact });
|
|
680
|
+
let receipt: SettingsAtomicReceipt;
|
|
681
|
+
try {
|
|
682
|
+
receipt = await input.settings.commitAtomicBatch(patches);
|
|
683
|
+
} catch {
|
|
684
|
+
return { status: "commit_failed" };
|
|
685
|
+
}
|
|
686
|
+
let observerFailed = false;
|
|
687
|
+
try {
|
|
688
|
+
await input.notifyConfigChanged?.();
|
|
689
|
+
} catch {
|
|
690
|
+
observerFailed = true;
|
|
691
|
+
}
|
|
692
|
+
if (!input.runtime) return { status: "saved", receipt, observerFailed };
|
|
693
|
+
try {
|
|
694
|
+
if (activate || desiredEnabled === true) {
|
|
695
|
+
await input.runtime.activate(provider);
|
|
696
|
+
return { status: "activated", receipt, observerFailed };
|
|
697
|
+
}
|
|
698
|
+
if (desiredEnabled === false) {
|
|
699
|
+
await input.runtime.deactivate(provider);
|
|
700
|
+
}
|
|
701
|
+
return { status: "saved", receipt, observerFailed };
|
|
702
|
+
} catch {
|
|
703
|
+
return {
|
|
704
|
+
status: activate || desiredEnabled === true ? "activation_failed" : "deactivation_failed",
|
|
705
|
+
receipt,
|
|
706
|
+
observerFailed,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export type NotificationProviderRemovalResult =
|
|
712
|
+
| { status: "removed"; receipt: SettingsAtomicReceipt; observerFailed: boolean }
|
|
713
|
+
| { status: "deactivation_failed"; receipt?: SettingsAtomicReceipt; observerFailed?: boolean }
|
|
714
|
+
| { status: "commit_failed_after_teardown" }
|
|
715
|
+
| { status: "commit_failed" };
|
|
716
|
+
|
|
717
|
+
function providerRemovalPatches(provider: NotificationProvider): SettingsAtomicPatch[] {
|
|
718
|
+
if (provider === "telegram") {
|
|
719
|
+
return [
|
|
720
|
+
{ path: "notifications.telegram.botToken", op: "unset" },
|
|
721
|
+
{ path: "notifications.telegram.chatId", op: "unset" },
|
|
722
|
+
{ path: "notifications.telegram.activation", op: "unset" },
|
|
723
|
+
{ path: "notifications.telegram.enabled", op: "set", value: false },
|
|
724
|
+
];
|
|
725
|
+
}
|
|
726
|
+
if (provider === "discord") {
|
|
727
|
+
return [
|
|
728
|
+
{ path: "notifications.discord.botToken", op: "unset" },
|
|
729
|
+
{ path: "notifications.discord.applicationId", op: "unset" },
|
|
730
|
+
{ path: "notifications.discord.guildId", op: "unset" },
|
|
731
|
+
{ path: "notifications.discord.parentChannelId", op: "unset" },
|
|
732
|
+
{ path: "notifications.discord.enabled", op: "set", value: false },
|
|
733
|
+
];
|
|
734
|
+
}
|
|
735
|
+
return [
|
|
736
|
+
{ path: "notifications.slack.botToken", op: "unset" },
|
|
737
|
+
{ path: "notifications.slack.appToken", op: "unset" },
|
|
738
|
+
{ path: "notifications.slack.workspaceId", op: "unset" },
|
|
739
|
+
{ path: "notifications.slack.channelId", op: "unset" },
|
|
740
|
+
{ path: "notifications.slack.authorizedUserId", op: "unset" },
|
|
741
|
+
{ path: "notifications.slack.enabled", op: "set", value: false },
|
|
742
|
+
];
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Remove only one provider. Telegram keeps teardown-before-delete; chat providers deactivate after commit. */
|
|
746
|
+
export async function removeNotificationProvider(input: {
|
|
747
|
+
settings: NotificationConfigurationWriter;
|
|
748
|
+
provider: NotificationProvider;
|
|
749
|
+
runtime: NotificationProviderRuntimeAuthority;
|
|
750
|
+
notifyConfigChanged?: () => Promise<void> | void;
|
|
751
|
+
}): Promise<NotificationProviderRemovalResult> {
|
|
752
|
+
if (input.provider === "telegram") {
|
|
753
|
+
try {
|
|
754
|
+
await input.runtime.deactivate("telegram");
|
|
755
|
+
} catch {
|
|
756
|
+
return { status: "deactivation_failed" };
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
let receipt: SettingsAtomicReceipt;
|
|
760
|
+
try {
|
|
761
|
+
receipt = await input.settings.commitAtomicBatch(providerRemovalPatches(input.provider));
|
|
762
|
+
} catch {
|
|
763
|
+
return { status: input.provider === "telegram" ? "commit_failed_after_teardown" : "commit_failed" };
|
|
764
|
+
}
|
|
765
|
+
let observerFailed = false;
|
|
766
|
+
try {
|
|
767
|
+
await input.notifyConfigChanged?.();
|
|
768
|
+
} catch {
|
|
769
|
+
observerFailed = true;
|
|
770
|
+
}
|
|
771
|
+
if (input.provider !== "telegram") {
|
|
772
|
+
try {
|
|
773
|
+
await input.runtime.deactivate(input.provider);
|
|
774
|
+
} catch {
|
|
775
|
+
return { status: "deactivation_failed", receipt, observerFailed };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return { status: "removed", receipt, observerFailed };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export type GlobalNotificationMutationResult =
|
|
782
|
+
| { status: "saved"; receipt: SettingsAtomicReceipt; observerFailed: boolean }
|
|
783
|
+
| {
|
|
784
|
+
status: "global_deactivation_partial";
|
|
785
|
+
receipt: SettingsAtomicReceipt;
|
|
786
|
+
failed: readonly NotificationProvider[];
|
|
787
|
+
observerFailed: boolean;
|
|
788
|
+
}
|
|
789
|
+
| {
|
|
790
|
+
status: "global_activation_partial";
|
|
791
|
+
receipt: SettingsAtomicReceipt;
|
|
792
|
+
failed: readonly NotificationProvider[];
|
|
793
|
+
observerFailed: boolean;
|
|
794
|
+
}
|
|
795
|
+
| { status: "commit_failed" };
|
|
796
|
+
|
|
797
|
+
/** Toggle only the global master. Provider configuration and desired intent are preserved. */
|
|
798
|
+
export async function setGlobalNotificationsEnabled(input: {
|
|
799
|
+
settings: NotificationConfigurationWriter;
|
|
800
|
+
enabled: boolean;
|
|
801
|
+
runtime?: NotificationProviderRuntimeAuthority;
|
|
802
|
+
notifyConfigChanged?: () => Promise<void> | void;
|
|
803
|
+
}): Promise<GlobalNotificationMutationResult> {
|
|
804
|
+
let receipt: SettingsAtomicReceipt;
|
|
805
|
+
try {
|
|
806
|
+
receipt = await input.settings.commitAtomicBatch([
|
|
807
|
+
{ path: "notifications.enabled", op: "set", value: input.enabled },
|
|
808
|
+
]);
|
|
809
|
+
} catch {
|
|
810
|
+
return { status: "commit_failed" };
|
|
811
|
+
}
|
|
812
|
+
let observerFailed = false;
|
|
813
|
+
try {
|
|
814
|
+
await input.notifyConfigChanged?.();
|
|
815
|
+
} catch {
|
|
816
|
+
observerFailed = true;
|
|
817
|
+
}
|
|
818
|
+
if (!input.runtime) return { status: "saved", receipt, observerFailed };
|
|
819
|
+
const failed: NotificationProvider[] = [];
|
|
820
|
+
if (input.enabled) {
|
|
821
|
+
const cfg = getNotificationConfig(input.settings);
|
|
822
|
+
for (const provider of ["telegram", "discord", "slack"] as const) {
|
|
823
|
+
if (!isProviderEffectivelyEnabled(cfg, provider)) continue;
|
|
824
|
+
if (provider === "telegram" && getCurrentTelegramActivationMarker(cfg)) continue;
|
|
825
|
+
try {
|
|
826
|
+
await input.runtime.activate(provider);
|
|
827
|
+
} catch {
|
|
828
|
+
failed.push(provider);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return failed.length === 0
|
|
832
|
+
? { status: "saved", receipt, observerFailed }
|
|
833
|
+
: { status: "global_activation_partial", receipt, failed, observerFailed };
|
|
834
|
+
}
|
|
835
|
+
for (const provider of ["telegram", "discord", "slack"] as const) {
|
|
836
|
+
try {
|
|
837
|
+
await input.runtime.deactivate(provider);
|
|
838
|
+
} catch {
|
|
839
|
+
failed.push(provider);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return failed.length === 0
|
|
843
|
+
? { status: "saved", receipt, observerFailed }
|
|
844
|
+
: { status: "global_deactivation_partial", receipt, failed, observerFailed };
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** Safe helper for UI contracts that must retain only the disposition, never replacement material. */
|
|
848
|
+
export function providerSecretDisposition(mutation: ProviderSecretMutation): ProviderSecretDisposition {
|
|
849
|
+
return mutation.action;
|
|
850
|
+
}
|