@gajae-code/coding-agent 0.12.6 → 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 +22 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- 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/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +8 -10
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
- package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
- 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 -3
- 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 +9 -4
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/host.d.ts +2 -23
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
- 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/artifacts.d.ts +5 -8
- package/dist/types/session/client-bridge.d.ts +0 -14
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -4
- 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/messages.d.ts +1 -4
- package/dist/types/session/session-manager.d.ts +7 -6
- package/dist/types/session/session-storage.d.ts +2 -0
- package/dist/types/session/streaming-output.d.ts +0 -32
- package/dist/types/task/receipt.d.ts +1 -2
- package/dist/types/task/types.d.ts +43 -178
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +0 -3
- package/dist/types/tools/output-meta.d.ts +5 -37
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/dist/types/tools/tool-result.d.ts +0 -1
- 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/async/job-manager.ts +430 -46
- 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/cursor.ts +13 -20
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +151 -425
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
- package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
- package/src/gjc-runtime/workflow-manifest.ts +0 -12
- package/src/internal-urls/agent-protocol.ts +38 -98
- package/src/internal-urls/artifact-protocol.ts +17 -28
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/registry-helpers.ts +0 -62
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/acp/acp-agent.ts +0 -24
- package/src/modes/acp/acp-client-bridge.ts +11 -154
- 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/prompts/tools/bash.md +2 -2
- package/src/sdk/acp/adapter.ts +9 -200
- 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 -8
- 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 +105 -359
- 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 +9 -4
- 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/host.ts +9 -187
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/host/reverse-leases.ts +42 -249
- 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 +59 -202
- package/src/session/artifacts.ts +74 -211
- package/src/session/client-bridge.ts +0 -15
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +362 -124
- package/src/session/internal/native-publish-outcome.ts +3 -1
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/messages.ts +7 -32
- package/src/session/session-manager.ts +48 -15
- package/src/session/session-storage.ts +72 -5
- package/src/session/streaming-output.ts +40 -198
- package/src/slash-commands/builtin-registry.ts +82 -4
- package/src/task/index.ts +237 -65
- package/src/task/receipt.ts +2 -4
- package/src/task/render.ts +0 -30
- package/src/task/types.ts +10 -34
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +239 -818
- package/src/tools/eval.ts +4 -99
- package/src/tools/fetch.ts +15 -77
- package/src/tools/find.ts +3 -7
- package/src/tools/output-meta.ts +114 -696
- package/src/tools/search.ts +4 -9
- package/src/tools/subagent.ts +97 -75
- package/src/tools/telegram-send.ts +4 -2
- package/src/tools/todo-write.ts +47 -0
- package/src/tools/tool-result.ts +0 -1
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
- package/dist/types/utils/artifact-id.d.ts +0 -2
- package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
- package/src/utils/artifact-id.ts +0 -6
|
@@ -20,14 +20,25 @@ import type { Settings } from "../../config/settings";
|
|
|
20
20
|
import { isProcessIncarnation, processIncarnation } from "../broker/process-incarnation";
|
|
21
21
|
import {
|
|
22
22
|
getNotificationConfig,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
hasAnyCompleteProvider,
|
|
24
|
+
hasAnyEffectivelyEnabledProvider,
|
|
25
|
+
isDiscordComplete,
|
|
26
|
+
isProviderEffectivelyEnabled,
|
|
27
|
+
isSlackComplete,
|
|
28
|
+
isTelegramComplete,
|
|
26
29
|
maskToken,
|
|
27
30
|
type NotificationConfig,
|
|
31
|
+
type NotificationProvider,
|
|
32
|
+
type NotificationRuntime,
|
|
33
|
+
type ProviderResolution,
|
|
34
|
+
resolveNotificationProvider,
|
|
28
35
|
tokenFingerprint,
|
|
29
36
|
} from "./config";
|
|
30
37
|
import { type DaemonPaths, daemonPaths, HEARTBEAT_TTL_MS } from "./daemon-paths";
|
|
38
|
+
import { DiscordLiveProvider } from "./discord-live-provider";
|
|
39
|
+
import type { DiscordDiagnosticProvider } from "./discord-provider";
|
|
40
|
+
import { SlackLiveProvider } from "./slack-live-provider";
|
|
41
|
+
import type { SlackDiagnosticProvider } from "./slack-provider";
|
|
31
42
|
import { type OwnerFreshnessSnapshot, readOwnerFreshnessSnapshot, type TelegramDaemonFs } from "./telegram-daemon";
|
|
32
43
|
import { DAEMON_GENERATION } from "./telegram-daemon-contract";
|
|
33
44
|
|
|
@@ -52,6 +63,18 @@ export function sanitizeDiagnostic(text: string, token?: string): string {
|
|
|
52
63
|
return out.replace(TELEGRAM_TOKEN_PATTERN, "<redacted>");
|
|
53
64
|
}
|
|
54
65
|
|
|
66
|
+
function sanitizeProviderDiagnostic(text: string, cfg: NotificationConfig, provider: NotificationProvider): string {
|
|
67
|
+
const secrets =
|
|
68
|
+
provider === "telegram"
|
|
69
|
+
? [cfg.botToken]
|
|
70
|
+
: provider === "discord"
|
|
71
|
+
? [cfg.discord.botToken]
|
|
72
|
+
: [cfg.slack.botToken, cfg.slack.appToken];
|
|
73
|
+
let detail = text;
|
|
74
|
+
for (const secret of secrets) detail = sanitizeDiagnostic(detail, secret);
|
|
75
|
+
return detail;
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
/** Identity evidence required to remove precisely the endpoint that was inspected. */
|
|
56
79
|
export interface NotificationEndpointFileIdentity {
|
|
57
80
|
dev: bigint;
|
|
@@ -161,6 +184,9 @@ export interface NotificationServiceDeps {
|
|
|
161
184
|
pidAlive?: (pid: number) => boolean;
|
|
162
185
|
fetchImpl?: typeof fetch;
|
|
163
186
|
apiBase?: string;
|
|
187
|
+
createDiscordDiagnostic?: (config: { applicationId: string; botToken: string }) => DiscordDiagnosticProvider;
|
|
188
|
+
createSlackDiagnostic?: (config: { appToken: string; botToken: string }) => SlackDiagnosticProvider;
|
|
189
|
+
providerRuntimeStatus?: (provider: NotificationProvider) => Promise<NotificationRuntime> | NotificationRuntime;
|
|
164
190
|
}
|
|
165
191
|
|
|
166
192
|
function defaultPidAlive(pid: number): boolean {
|
|
@@ -187,6 +213,12 @@ export interface AdapterConfigView {
|
|
|
187
213
|
botTokenMasked: string;
|
|
188
214
|
channel: string | undefined;
|
|
189
215
|
configured: boolean;
|
|
216
|
+
quarantined: boolean;
|
|
217
|
+
desiredEnabled: boolean;
|
|
218
|
+
desiredSource: ProviderResolution["desiredSource"];
|
|
219
|
+
effectiveEnabled: boolean;
|
|
220
|
+
issues: ProviderResolution["issues"];
|
|
221
|
+
runtime?: NotificationRuntime;
|
|
190
222
|
}
|
|
191
223
|
|
|
192
224
|
export interface NotificationStatusReport {
|
|
@@ -194,13 +226,30 @@ export interface NotificationStatusReport {
|
|
|
194
226
|
redact: boolean;
|
|
195
227
|
verbosity: "lean" | "verbose";
|
|
196
228
|
globallyConfigured: boolean;
|
|
229
|
+
anyProviderComplete: boolean;
|
|
230
|
+
anyProviderEffective: boolean;
|
|
197
231
|
telegram: AdapterConfigView & { tokenFingerprint: string | undefined };
|
|
198
232
|
discord: AdapterConfigView;
|
|
199
233
|
slack: AdapterConfigView;
|
|
200
234
|
}
|
|
201
235
|
|
|
202
|
-
function
|
|
203
|
-
|
|
236
|
+
function adapterView(
|
|
237
|
+
cfg: NotificationConfig,
|
|
238
|
+
provider: NotificationProvider,
|
|
239
|
+
token: string | undefined,
|
|
240
|
+
channel: string | undefined,
|
|
241
|
+
): AdapterConfigView {
|
|
242
|
+
const resolution = resolveNotificationProvider(cfg, provider);
|
|
243
|
+
return {
|
|
244
|
+
botTokenMasked: maskToken(token),
|
|
245
|
+
channel,
|
|
246
|
+
configured: resolution.configured,
|
|
247
|
+
quarantined: resolution.quarantined,
|
|
248
|
+
desiredEnabled: resolution.desiredEnabled,
|
|
249
|
+
desiredSource: resolution.desiredSource,
|
|
250
|
+
effectiveEnabled: resolution.effectiveEnabled,
|
|
251
|
+
issues: resolution.issues,
|
|
252
|
+
};
|
|
204
253
|
}
|
|
205
254
|
|
|
206
255
|
/** Build a secret-safe structured status snapshot from settings. */
|
|
@@ -210,43 +259,40 @@ export function buildNotificationStatusReport(settings: Settings): NotificationS
|
|
|
210
259
|
enabled: cfg.enabled,
|
|
211
260
|
redact: cfg.redact,
|
|
212
261
|
verbosity: cfg.verbosity,
|
|
213
|
-
globallyConfigured:
|
|
262
|
+
globallyConfigured: cfg.enabled && hasAnyCompleteProvider(cfg),
|
|
263
|
+
anyProviderComplete: hasAnyCompleteProvider(cfg),
|
|
264
|
+
anyProviderEffective: hasAnyEffectivelyEnabledProvider(cfg),
|
|
214
265
|
telegram: {
|
|
215
|
-
|
|
216
|
-
channel: cfg.chatId,
|
|
217
|
-
configured: isTelegramConfigured(cfg),
|
|
266
|
+
...adapterView(cfg, "telegram", cfg.botToken, cfg.chatId),
|
|
218
267
|
tokenFingerprint: cfg.botToken?.trim() ? tokenFingerprint(cfg.botToken) : undefined,
|
|
219
268
|
},
|
|
220
|
-
discord:
|
|
221
|
-
|
|
222
|
-
channel: cfg.discord.parentChannelId,
|
|
223
|
-
configured: isDiscordConfigured(cfg),
|
|
224
|
-
},
|
|
225
|
-
slack: {
|
|
226
|
-
botTokenMasked: maskToken(cfg.slack.botToken),
|
|
227
|
-
channel: cfg.slack.channelId,
|
|
228
|
-
configured: adapterConfigured(cfg.slack.botToken, cfg.slack.channelId),
|
|
229
|
-
},
|
|
269
|
+
discord: adapterView(cfg, "discord", cfg.discord.botToken, cfg.discord.parentChannelId),
|
|
270
|
+
slack: adapterView(cfg, "slack", cfg.slack.botToken, cfg.slack.channelId),
|
|
230
271
|
};
|
|
231
272
|
}
|
|
232
273
|
|
|
233
274
|
/** Render a status report as human-readable lines (no secrets). */
|
|
234
275
|
export function formatNotificationStatusReport(report: NotificationStatusReport): string {
|
|
235
|
-
const yesNo = (
|
|
276
|
+
const yesNo = (value: boolean): string => (value ? "yes" : "no");
|
|
277
|
+
const provider = (name: NotificationProvider, view: AdapterConfigView): string[] => [
|
|
278
|
+
` ${name}.configured: ${yesNo(view.configured)}`,
|
|
279
|
+
` ${name}.needsRepair: ${yesNo(view.quarantined)}`,
|
|
280
|
+
` ${name}.desired: ${view.desiredEnabled ? "on" : "off"} (${view.desiredSource})`,
|
|
281
|
+
` ${name}.effective: ${yesNo(view.effectiveEnabled)}`,
|
|
282
|
+
` ${name}.botToken: ${view.botTokenMasked}`,
|
|
283
|
+
` ${name}.destination: ${view.channel ?? "(unset)"}`,
|
|
284
|
+
];
|
|
236
285
|
return [
|
|
237
286
|
"Notifications",
|
|
238
287
|
` enabled: ${report.enabled}`,
|
|
239
|
-
`
|
|
288
|
+
` any provider effective: ${yesNo(report.anyProviderEffective)}`,
|
|
289
|
+
` any provider complete: ${yesNo(report.anyProviderComplete)}`,
|
|
240
290
|
` redact: ${report.redact}`,
|
|
241
291
|
` verbosity: ${report.verbosity}`,
|
|
242
|
-
|
|
243
|
-
` telegram.chatId: ${report.telegram.channel ?? "(unset)"}`,
|
|
292
|
+
...provider("telegram", report.telegram),
|
|
244
293
|
` telegram.fingerprint: ${report.telegram.tokenFingerprint ?? "(unset)"}`,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
` discord.parentChannelId: ${report.discord.channel ?? "(unset)"}`,
|
|
248
|
-
` slack.botToken: ${report.slack.botTokenMasked}`,
|
|
249
|
-
` slack.channelId: ${report.slack.channel ?? "(unset)"}`,
|
|
294
|
+
...provider("discord", report.discord),
|
|
295
|
+
...provider("slack", report.slack),
|
|
250
296
|
].join("\n");
|
|
251
297
|
}
|
|
252
298
|
|
|
@@ -508,6 +554,8 @@ export interface EndpointHealth {
|
|
|
508
554
|
export interface NotificationHealthReport {
|
|
509
555
|
overall: HealthLevel;
|
|
510
556
|
configured: boolean;
|
|
557
|
+
provider?: NotificationProvider;
|
|
558
|
+
resolution?: ProviderResolution;
|
|
511
559
|
checks: HealthCheck[];
|
|
512
560
|
daemon: DaemonHealth;
|
|
513
561
|
endpoints: EndpointHealth;
|
|
@@ -518,20 +566,25 @@ export interface HealthOptions {
|
|
|
518
566
|
settings: Settings;
|
|
519
567
|
stateRoot?: string;
|
|
520
568
|
deps?: NotificationServiceDeps;
|
|
521
|
-
|
|
569
|
+
provider?: NotificationProvider;
|
|
570
|
+
/** When true, use the selected provider's REST-only diagnostic path. */
|
|
522
571
|
probe?: boolean;
|
|
572
|
+
signal?: AbortSignal;
|
|
523
573
|
}
|
|
524
574
|
|
|
525
575
|
async function probeTelegramReachability(
|
|
526
576
|
fetchImpl: typeof fetch,
|
|
527
577
|
apiBase: string,
|
|
528
578
|
token: string,
|
|
579
|
+
signal?: AbortSignal,
|
|
529
580
|
): Promise<{ ok: boolean; detail: string }> {
|
|
581
|
+
if (signal?.aborted) return { ok: false, detail: "Telegram probe cancelled." };
|
|
530
582
|
try {
|
|
531
583
|
const response = await fetchImpl(`${apiBase.replace(/\/$/, "")}/bot${token}/getMe`, {
|
|
532
584
|
method: "POST",
|
|
533
585
|
headers: { "content-type": "application/json" },
|
|
534
586
|
body: "{}",
|
|
587
|
+
signal,
|
|
535
588
|
});
|
|
536
589
|
const payload = (await response.json().catch(() => undefined)) as
|
|
537
590
|
| { ok?: boolean; description?: string; result?: { username?: string } }
|
|
@@ -544,8 +597,78 @@ async function probeTelegramReachability(
|
|
|
544
597
|
ok: false,
|
|
545
598
|
detail: sanitizeDiagnostic(payload?.description ?? `Telegram getMe failed (HTTP ${response.status})`, token),
|
|
546
599
|
};
|
|
547
|
-
} catch (
|
|
548
|
-
return { ok: false, detail:
|
|
600
|
+
} catch (error) {
|
|
601
|
+
if (signal?.aborted) return { ok: false, detail: "Telegram probe cancelled." };
|
|
602
|
+
return { ok: false, detail: sanitizeDiagnostic(error instanceof Error ? error.message : "network error", token) };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function effectiveProviders(cfg: NotificationConfig): NotificationProvider[] {
|
|
607
|
+
return (["telegram", "discord", "slack"] as const).filter(provider => isProviderEffectivelyEnabled(cfg, provider));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function selectNotificationProvider(
|
|
611
|
+
cfg: NotificationConfig,
|
|
612
|
+
requested: NotificationProvider | undefined,
|
|
613
|
+
): NotificationProvider | undefined {
|
|
614
|
+
if (requested) return requested;
|
|
615
|
+
const effective = effectiveProviders(cfg);
|
|
616
|
+
return effective.length === 1 ? effective[0] : undefined;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function probeSelectedProvider(
|
|
620
|
+
cfg: NotificationConfig,
|
|
621
|
+
provider: NotificationProvider,
|
|
622
|
+
deps: NotificationServiceDeps,
|
|
623
|
+
signal?: AbortSignal,
|
|
624
|
+
): Promise<{ ok: boolean; detail: string }> {
|
|
625
|
+
try {
|
|
626
|
+
let result: { ok: boolean; detail: string };
|
|
627
|
+
if (provider === "telegram" && isTelegramComplete(cfg)) {
|
|
628
|
+
result = await probeTelegramReachability(
|
|
629
|
+
deps.fetchImpl ?? globalThis.fetch,
|
|
630
|
+
deps.apiBase ?? DEFAULT_API_BASE,
|
|
631
|
+
cfg.botToken,
|
|
632
|
+
signal,
|
|
633
|
+
);
|
|
634
|
+
} else if (provider === "discord" && isDiscordComplete(cfg)) {
|
|
635
|
+
const adapter =
|
|
636
|
+
deps.createDiscordDiagnostic?.({
|
|
637
|
+
applicationId: cfg.discord.applicationId,
|
|
638
|
+
botToken: cfg.discord.botToken,
|
|
639
|
+
}) ??
|
|
640
|
+
new DiscordLiveProvider({
|
|
641
|
+
applicationId: cfg.discord.applicationId,
|
|
642
|
+
botToken: cfg.discord.botToken,
|
|
643
|
+
fetchImpl: deps.fetchImpl,
|
|
644
|
+
});
|
|
645
|
+
result = await adapter.probeConfiguration(signal);
|
|
646
|
+
} else if (provider === "slack" && isSlackComplete(cfg)) {
|
|
647
|
+
const adapter =
|
|
648
|
+
deps.createSlackDiagnostic?.({ appToken: cfg.slack.appToken, botToken: cfg.slack.botToken }) ??
|
|
649
|
+
new SlackLiveProvider({
|
|
650
|
+
appToken: cfg.slack.appToken,
|
|
651
|
+
botToken: cfg.slack.botToken,
|
|
652
|
+
fetch: deps.fetchImpl,
|
|
653
|
+
});
|
|
654
|
+
const probe = await adapter.probeConfiguration(signal);
|
|
655
|
+
result =
|
|
656
|
+
probe.ok && (!probe.teamId || probe.teamId !== cfg.slack.workspaceId)
|
|
657
|
+
? { ok: false, detail: "Slack workspace identity does not match the configured workspace ID." }
|
|
658
|
+
: probe;
|
|
659
|
+
} else {
|
|
660
|
+
result = { ok: false, detail: `${provider} configuration is unavailable.` };
|
|
661
|
+
}
|
|
662
|
+
return { ...result, detail: sanitizeProviderDiagnostic(result.detail, cfg, provider) };
|
|
663
|
+
} catch (error) {
|
|
664
|
+
return {
|
|
665
|
+
ok: false,
|
|
666
|
+
detail: sanitizeProviderDiagnostic(
|
|
667
|
+
error instanceof Error ? error.message : `${provider} diagnostic failed.`,
|
|
668
|
+
cfg,
|
|
669
|
+
provider,
|
|
670
|
+
),
|
|
671
|
+
};
|
|
549
672
|
}
|
|
550
673
|
}
|
|
551
674
|
|
|
@@ -563,11 +686,25 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
563
686
|
const stateRoot = opts.stateRoot ?? defaultStateRoot();
|
|
564
687
|
|
|
565
688
|
const cfg: NotificationConfig = getNotificationConfig(opts.settings);
|
|
566
|
-
const
|
|
567
|
-
const
|
|
689
|
+
const provider = selectNotificationProvider(cfg, opts.provider);
|
|
690
|
+
const resolution = provider ? resolveNotificationProvider(cfg, provider) : undefined;
|
|
691
|
+
const configured = resolution?.configured ?? hasAnyCompleteProvider(cfg);
|
|
692
|
+
const telegramConfigured = isTelegramComplete(cfg);
|
|
568
693
|
const checks: HealthCheck[] = [];
|
|
569
694
|
|
|
570
|
-
if (
|
|
695
|
+
if (provider && resolution?.quarantined) {
|
|
696
|
+
checks.push({ name: "config", level: "error", detail: `${provider} configuration needs repair` });
|
|
697
|
+
} else if (provider && !resolution?.configured) {
|
|
698
|
+
checks.push({ name: "config", level: "warn", detail: `${provider} is not fully configured` });
|
|
699
|
+
} else if (provider && !resolution?.desiredEnabled) {
|
|
700
|
+
checks.push({ name: "intent", level: "warn", detail: `${provider} desired intent is off` });
|
|
701
|
+
} else if (provider && !resolution?.effectiveEnabled) {
|
|
702
|
+
checks.push({
|
|
703
|
+
name: "config",
|
|
704
|
+
level: "warn",
|
|
705
|
+
detail: `${provider} is not effective while the global master is off`,
|
|
706
|
+
});
|
|
707
|
+
} else if (!cfg.enabled) {
|
|
571
708
|
checks.push({
|
|
572
709
|
name: "config",
|
|
573
710
|
level: "warn",
|
|
@@ -575,8 +712,24 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
575
712
|
});
|
|
576
713
|
} else if (!configured) {
|
|
577
714
|
checks.push({ name: "config", level: "warn", detail: "no notification adapter is fully configured" });
|
|
715
|
+
} else if (!provider && effectiveProviders(cfg).length === 0) {
|
|
716
|
+
checks.push({
|
|
717
|
+
name: "config",
|
|
718
|
+
level: "warn",
|
|
719
|
+
detail: "no configured provider currently has desired intent enabled",
|
|
720
|
+
});
|
|
721
|
+
} else if (!provider && effectiveProviders(cfg).length > 1) {
|
|
722
|
+
checks.push({
|
|
723
|
+
name: "selection",
|
|
724
|
+
level: "warn",
|
|
725
|
+
detail: "select a provider because multiple providers are effective",
|
|
726
|
+
});
|
|
578
727
|
} else {
|
|
579
|
-
checks.push({
|
|
728
|
+
checks.push({
|
|
729
|
+
name: "config",
|
|
730
|
+
level: "ok",
|
|
731
|
+
detail: provider ? `${provider} is effective` : "enabled with one configured adapter",
|
|
732
|
+
});
|
|
580
733
|
}
|
|
581
734
|
|
|
582
735
|
// Daemon ownership state (offline; corrupt or unreadable state degrades to a
|
|
@@ -612,26 +765,28 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
612
765
|
currentGeneration: DAEMON_GENERATION,
|
|
613
766
|
generationRelation: daemonGenerationRelation(state),
|
|
614
767
|
};
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
768
|
+
if (!provider || provider === "telegram") {
|
|
769
|
+
if (daemonStateUnreadable) {
|
|
770
|
+
checks.push({ name: "daemon", level: "warn", detail: "daemon ownership record is corrupt or unreadable" });
|
|
771
|
+
} else if (!state) {
|
|
772
|
+
checks.push({ name: "daemon", level: "ok", detail: "no daemon ownership record (none running)" });
|
|
773
|
+
} else if (!daemon.alive) {
|
|
774
|
+
checks.push({
|
|
775
|
+
name: "daemon",
|
|
776
|
+
level: "warn",
|
|
777
|
+
detail: `daemon owner pid ${daemon.pid} is not alive; run recovery to clear the stale lock`,
|
|
778
|
+
});
|
|
779
|
+
} else if (!daemon.heartbeatFresh) {
|
|
780
|
+
checks.push({ name: "daemon", level: "warn", detail: `daemon pid ${daemon.pid} heartbeat is stale` });
|
|
781
|
+
} else if (telegramConfigured && !daemon.identityMatches) {
|
|
782
|
+
checks.push({
|
|
783
|
+
name: "daemon",
|
|
784
|
+
level: "warn",
|
|
785
|
+
detail: "a live daemon owns a different bot token or chat id",
|
|
786
|
+
});
|
|
787
|
+
} else {
|
|
788
|
+
checks.push({ name: "daemon", level: "ok", detail: `daemon pid ${daemon.pid} alive with a fresh heartbeat` });
|
|
789
|
+
}
|
|
635
790
|
}
|
|
636
791
|
|
|
637
792
|
// Per-session endpoint discovery files.
|
|
@@ -667,54 +822,68 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
667
822
|
unknown: unknownEndpoints,
|
|
668
823
|
unreadable,
|
|
669
824
|
};
|
|
670
|
-
if (
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
825
|
+
if (!provider || provider === "telegram") {
|
|
826
|
+
if (dead > 0 || unreadable > 0) {
|
|
827
|
+
checks.push({
|
|
828
|
+
name: "endpoints",
|
|
829
|
+
level: "warn",
|
|
830
|
+
detail: `${dead} dead / ${unreadable} unreadable of ${endpoints.total} endpoint file(s); run recovery`,
|
|
831
|
+
});
|
|
832
|
+
} else {
|
|
833
|
+
checks.push({
|
|
834
|
+
name: "endpoints",
|
|
835
|
+
level: "ok",
|
|
836
|
+
detail: `${live} live, ${unknownEndpoints} unverified endpoint file(s)`,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
if (
|
|
840
|
+
telegramConfigured &&
|
|
841
|
+
daemon.present &&
|
|
842
|
+
daemon.alive &&
|
|
843
|
+
daemon.heartbeatFresh &&
|
|
844
|
+
daemon.identityMatches &&
|
|
845
|
+
!daemon.stopped &&
|
|
846
|
+
endpoints.total === 0
|
|
847
|
+
) {
|
|
848
|
+
checks.push({
|
|
849
|
+
name: "local_endpoint",
|
|
850
|
+
level: "warn",
|
|
851
|
+
detail:
|
|
852
|
+
"No local notification endpoint for this working directory. In this GJC terminal run /notify on; if it does not report notifications enabled, start a new local GJC session. Do not re-pair Telegram.",
|
|
853
|
+
});
|
|
854
|
+
}
|
|
698
855
|
}
|
|
699
856
|
|
|
700
|
-
// Optional
|
|
857
|
+
// Optional selected REST-only reachability probe.
|
|
701
858
|
let reachability = { probed: false, ok: false, detail: "not probed" };
|
|
702
|
-
if (opts.probe
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
859
|
+
if (opts.probe) {
|
|
860
|
+
if (!provider) {
|
|
861
|
+
reachability = { probed: false, ok: false, detail: "provider selection required" };
|
|
862
|
+
checks.push({ name: "reachability", level: "error", detail: "provider selection required" });
|
|
863
|
+
} else if (!resolution?.configured || resolution.quarantined) {
|
|
864
|
+
reachability = { probed: false, ok: false, detail: `${provider} is unavailable` };
|
|
865
|
+
checks.push({ name: "reachability", level: "error", detail: `${provider} is unavailable` });
|
|
866
|
+
} else {
|
|
867
|
+
const result = await probeSelectedProvider(cfg, provider, deps, opts.signal);
|
|
868
|
+
reachability = { probed: true, ...result };
|
|
869
|
+
checks.push({
|
|
870
|
+
name: "reachability",
|
|
871
|
+
level: result.ok ? "ok" : "error",
|
|
872
|
+
detail: `${provider}: ${result.detail}`,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
714
875
|
}
|
|
715
876
|
|
|
716
877
|
const overall = checks.reduce<HealthLevel>((acc, check) => worst(acc, check.level), "ok");
|
|
717
|
-
return {
|
|
878
|
+
return {
|
|
879
|
+
overall,
|
|
880
|
+
configured,
|
|
881
|
+
...(provider ? { provider, resolution } : {}),
|
|
882
|
+
checks,
|
|
883
|
+
daemon,
|
|
884
|
+
endpoints,
|
|
885
|
+
reachability,
|
|
886
|
+
};
|
|
718
887
|
}
|
|
719
888
|
|
|
720
889
|
/** Render a health report as human-readable lines (no secrets). */
|
|
@@ -731,66 +900,198 @@ export function formatNotificationHealthReport(report: NotificationHealthReport)
|
|
|
731
900
|
|
|
732
901
|
export interface NotificationTestResult {
|
|
733
902
|
ok: boolean;
|
|
734
|
-
adapter
|
|
735
|
-
|
|
903
|
+
adapter?: NotificationProvider;
|
|
904
|
+
destination?: string;
|
|
736
905
|
detail: string;
|
|
906
|
+
uncertain?: boolean;
|
|
737
907
|
}
|
|
738
908
|
|
|
739
909
|
export interface TestOptions {
|
|
740
910
|
settings: Settings;
|
|
741
911
|
deps?: NotificationServiceDeps;
|
|
912
|
+
provider?: NotificationProvider;
|
|
742
913
|
text?: string;
|
|
914
|
+
signal?: AbortSignal;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async function selectedProviderRuntimeReady(
|
|
918
|
+
provider: NotificationProvider,
|
|
919
|
+
deps: NotificationServiceDeps,
|
|
920
|
+
): Promise<boolean> {
|
|
921
|
+
if (!deps.providerRuntimeStatus) return false;
|
|
922
|
+
const status = await deps.providerRuntimeStatus(provider);
|
|
923
|
+
return status === "ready" || status === "attached";
|
|
743
924
|
}
|
|
744
925
|
|
|
745
|
-
/** Send a one-off test
|
|
926
|
+
/** Send a one-off test through exactly one durable, effective provider. */
|
|
746
927
|
export async function sendNotificationTest(opts: TestOptions): Promise<NotificationTestResult> {
|
|
747
928
|
const deps = opts.deps ?? {};
|
|
748
929
|
const cfg = getNotificationConfig(opts.settings);
|
|
749
|
-
|
|
930
|
+
const provider = selectNotificationProvider(cfg, opts.provider);
|
|
931
|
+
if (!provider) {
|
|
750
932
|
return {
|
|
751
933
|
ok: false,
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
934
|
+
detail:
|
|
935
|
+
effectiveProviders(cfg).length === 0
|
|
936
|
+
? "No notification provider is effective. Configure and enable one provider first."
|
|
937
|
+
: "Multiple notification providers are effective; select one with --provider.",
|
|
755
938
|
};
|
|
756
939
|
}
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
const text = opts.text ?? "GJC notifications test message. If you can read this, delivery works.";
|
|
760
|
-
try {
|
|
761
|
-
const response = await fetchImpl(`${apiBase}/bot${cfg.botToken}/sendMessage`, {
|
|
762
|
-
method: "POST",
|
|
763
|
-
headers: { "content-type": "application/json" },
|
|
764
|
-
body: JSON.stringify({ chat_id: cfg.chatId, text }),
|
|
765
|
-
});
|
|
766
|
-
const payload = (await response.json().catch(() => undefined)) as
|
|
767
|
-
| { ok?: boolean; description?: string }
|
|
768
|
-
| undefined;
|
|
769
|
-
if (response.ok && payload?.ok) {
|
|
770
|
-
return { ok: true, adapter: "telegram", chatId: cfg.chatId, detail: `delivered to chat ${cfg.chatId}` };
|
|
771
|
-
}
|
|
940
|
+
const resolution = resolveNotificationProvider(cfg, provider);
|
|
941
|
+
if (!resolution.effectiveEnabled) {
|
|
772
942
|
return {
|
|
773
943
|
ok: false,
|
|
774
|
-
adapter:
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
cfg.botToken,
|
|
779
|
-
),
|
|
944
|
+
adapter: provider,
|
|
945
|
+
detail: resolution.quarantined
|
|
946
|
+
? `${provider} configuration needs repair.`
|
|
947
|
+
: `${provider} is unavailable because configuration, desired intent, or the global master is off.`,
|
|
780
948
|
};
|
|
781
|
-
}
|
|
949
|
+
}
|
|
950
|
+
try {
|
|
951
|
+
if (!(await selectedProviderRuntimeReady(provider, deps))) {
|
|
952
|
+
return { ok: false, adapter: provider, detail: `${provider} runtime is not ready or attached.` };
|
|
953
|
+
}
|
|
954
|
+
} catch (error) {
|
|
782
955
|
return {
|
|
783
956
|
ok: false,
|
|
784
|
-
adapter:
|
|
785
|
-
|
|
786
|
-
|
|
957
|
+
adapter: provider,
|
|
958
|
+
detail: sanitizeProviderDiagnostic(
|
|
959
|
+
error instanceof Error ? error.message : `${provider} runtime readiness check failed.`,
|
|
960
|
+
cfg,
|
|
961
|
+
provider,
|
|
962
|
+
),
|
|
787
963
|
};
|
|
788
964
|
}
|
|
965
|
+
if (opts.signal?.aborted)
|
|
966
|
+
return { ok: false, adapter: provider, detail: `${provider} notification test cancelled.` };
|
|
967
|
+
const text = opts.text ?? "GJC notifications test message. If you can read this, delivery works.";
|
|
968
|
+
if (provider === "telegram" && isTelegramComplete(cfg)) {
|
|
969
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
970
|
+
const apiBase = (deps.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
971
|
+
try {
|
|
972
|
+
const response = await fetchImpl(`${apiBase}/bot${cfg.botToken}/sendMessage`, {
|
|
973
|
+
method: "POST",
|
|
974
|
+
headers: { "content-type": "application/json" },
|
|
975
|
+
body: JSON.stringify({ chat_id: cfg.chatId, text }),
|
|
976
|
+
signal: opts.signal,
|
|
977
|
+
});
|
|
978
|
+
const payload = (await response.json().catch(() => undefined)) as
|
|
979
|
+
| { ok?: boolean; description?: string; result?: { message_id?: unknown } }
|
|
980
|
+
| undefined;
|
|
981
|
+
const messageId = payload?.result?.message_id;
|
|
982
|
+
if (response.ok && payload?.ok === true && typeof messageId === "number" && Number.isSafeInteger(messageId)) {
|
|
983
|
+
return {
|
|
984
|
+
ok: true,
|
|
985
|
+
adapter: "telegram",
|
|
986
|
+
destination: cfg.chatId,
|
|
987
|
+
detail: `delivered to chat ${cfg.chatId}`,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
const uncertain = response.ok && payload?.ok !== false;
|
|
991
|
+
return {
|
|
992
|
+
ok: false,
|
|
993
|
+
adapter: "telegram",
|
|
994
|
+
destination: cfg.chatId,
|
|
995
|
+
...(uncertain ? { uncertain: true } : {}),
|
|
996
|
+
detail: sanitizeDiagnostic(
|
|
997
|
+
uncertain
|
|
998
|
+
? "Telegram may have accepted the message but returned no usable message receipt."
|
|
999
|
+
: (payload?.description ?? `Telegram sendMessage failed (HTTP ${response.status})`),
|
|
1000
|
+
cfg.botToken,
|
|
1001
|
+
),
|
|
1002
|
+
};
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
return {
|
|
1005
|
+
ok: false,
|
|
1006
|
+
adapter: "telegram",
|
|
1007
|
+
destination: cfg.chatId,
|
|
1008
|
+
uncertain: true,
|
|
1009
|
+
detail: opts.signal?.aborted
|
|
1010
|
+
? "Telegram notification delivery is uncertain because cancellation raced with dispatch."
|
|
1011
|
+
: sanitizeDiagnostic(error instanceof Error ? error.message : "network error", cfg.botToken),
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (provider === "discord" && isDiscordComplete(cfg)) {
|
|
1016
|
+
try {
|
|
1017
|
+
const adapter =
|
|
1018
|
+
deps.createDiscordDiagnostic?.({
|
|
1019
|
+
applicationId: cfg.discord.applicationId,
|
|
1020
|
+
botToken: cfg.discord.botToken,
|
|
1021
|
+
}) ??
|
|
1022
|
+
new DiscordLiveProvider({
|
|
1023
|
+
applicationId: cfg.discord.applicationId,
|
|
1024
|
+
botToken: cfg.discord.botToken,
|
|
1025
|
+
fetchImpl: deps.fetchImpl,
|
|
1026
|
+
});
|
|
1027
|
+
const result = await adapter.sendOneShotTest({
|
|
1028
|
+
channelId: cfg.discord.parentChannelId,
|
|
1029
|
+
message: text,
|
|
1030
|
+
signal: opts.signal,
|
|
1031
|
+
});
|
|
1032
|
+
return {
|
|
1033
|
+
ok: result.ok,
|
|
1034
|
+
adapter: "discord",
|
|
1035
|
+
destination: cfg.discord.parentChannelId,
|
|
1036
|
+
detail: sanitizeProviderDiagnostic(result.detail, cfg, "discord"),
|
|
1037
|
+
...(result.uncertain ? { uncertain: true } : {}),
|
|
1038
|
+
};
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
return {
|
|
1041
|
+
ok: false,
|
|
1042
|
+
adapter: "discord",
|
|
1043
|
+
destination: cfg.discord.parentChannelId,
|
|
1044
|
+
uncertain: true,
|
|
1045
|
+
detail: sanitizeProviderDiagnostic(
|
|
1046
|
+
error instanceof Error ? error.message : "Discord notification delivery failed.",
|
|
1047
|
+
cfg,
|
|
1048
|
+
"discord",
|
|
1049
|
+
),
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (provider === "slack" && isSlackComplete(cfg)) {
|
|
1054
|
+
try {
|
|
1055
|
+
const adapter =
|
|
1056
|
+
deps.createSlackDiagnostic?.({ appToken: cfg.slack.appToken, botToken: cfg.slack.botToken }) ??
|
|
1057
|
+
new SlackLiveProvider({
|
|
1058
|
+
appToken: cfg.slack.appToken,
|
|
1059
|
+
botToken: cfg.slack.botToken,
|
|
1060
|
+
fetch: deps.fetchImpl,
|
|
1061
|
+
});
|
|
1062
|
+
const result = await adapter.sendOneShotTest({
|
|
1063
|
+
channel: cfg.slack.channelId,
|
|
1064
|
+
message: text,
|
|
1065
|
+
idempotencyKey: crypto.randomUUID(),
|
|
1066
|
+
signal: opts.signal,
|
|
1067
|
+
});
|
|
1068
|
+
return {
|
|
1069
|
+
ok: result.ok,
|
|
1070
|
+
adapter: "slack",
|
|
1071
|
+
destination: cfg.slack.channelId,
|
|
1072
|
+
detail: sanitizeProviderDiagnostic(result.detail, cfg, "slack"),
|
|
1073
|
+
...(result.uncertain ? { uncertain: true } : {}),
|
|
1074
|
+
};
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
return {
|
|
1077
|
+
ok: false,
|
|
1078
|
+
adapter: "slack",
|
|
1079
|
+
destination: cfg.slack.channelId,
|
|
1080
|
+
uncertain: true,
|
|
1081
|
+
detail: sanitizeProviderDiagnostic(
|
|
1082
|
+
error instanceof Error ? error.message : "Slack notification delivery failed.",
|
|
1083
|
+
cfg,
|
|
1084
|
+
"slack",
|
|
1085
|
+
),
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return { ok: false, adapter: provider, detail: `${provider} configuration is unavailable.` };
|
|
789
1090
|
}
|
|
790
1091
|
|
|
791
1092
|
/** Render a test result as a single human-readable line (no secrets). */
|
|
792
1093
|
export function formatNotificationTestResult(result: NotificationTestResult): string {
|
|
793
|
-
return `Notification test (${result.adapter}): ${result.ok ? "OK" : "FAILED"} — ${result.detail}`;
|
|
1094
|
+
return `Notification test (${result.adapter ?? "unselected"}): ${result.ok ? "OK" : result.uncertain ? "UNCERTAIN" : "FAILED"} — ${result.detail}`;
|
|
794
1095
|
}
|
|
795
1096
|
|
|
796
1097
|
// --- recovery -----------------------------------------------------------
|