-
-
-
Morning Briefing
-
- {statusLabel}
-
+
+
+
+
+
Morning Briefing
+
+ {statusLabel}
+
+
+ {showScheduleDetails && briefingStatus?.cron && briefingStatus?.timezone && (
+
+ {formatMorningBriefingSchedule(briefingStatus.cron, briefingStatus.timezone)}
+
+ )}
+ {showScheduleDetails && (
+
+ Last generated: {briefingStatus?.lastGeneratedDate ?? '(none)'}
+
+ )}
+ {showLastDelivery && (
+
+ Last delivery:{' '}
+ {lastDelivery
+ .map(entry => {
+ const channel = deliveryChannelLabel[entry.channel] ?? entry.channel;
+ const status = deliveryStatusLabel[entry.status] ?? entry.status;
+ if (entry.status === 'sent') {
+ return `${channel}: ${status}`;
+ }
+ const reason = entry.reason
+ ? (deliveryReasonLabel[entry.reason] ?? entry.reason)
+ : undefined;
+ return reason ? `${channel}: ${status} (${reason})` : `${channel}: ${status}`;
+ })
+ .join(' • ')}
+
+ )}
+
+
{sourceSummaryText}
- {showScheduleDetails && briefingStatus?.cron && briefingStatus?.timezone && (
-
- {formatMorningBriefingSchedule(briefingStatus.cron, briefingStatus.timezone)}
-
- )}
- {showScheduleDetails && (
-
- Last generated: {briefingStatus?.lastGeneratedDate ?? '(none)'}
-
- )}
- {showLastDelivery && (
-
- Last delivery:{' '}
- {lastDelivery
- .map(entry => {
- const channel = deliveryChannelLabel[entry.channel] ?? entry.channel;
- const status = deliveryStatusLabel[entry.status] ?? entry.status;
- if (entry.status === 'sent') {
- return `${channel}: ${status}`;
- }
- const reason = entry.reason
- ? (deliveryReasonLabel[entry.reason] ?? entry.reason)
- : undefined;
- return reason ? `${channel}: ${status} (${reason})` : `${channel}: ${status}`;
- })
- .join(' • ')}
-
- )}
@@ -742,13 +749,6 @@ function MorningBriefingCard({
- {isWarmupState && (
-
- Instance is still warming up. Morning Briefing controls will become available once the
- gateway is fully ready.
-
- )}
-
{!desiredEnabled && controlsEnabled && (
Enable Morning Briefing to get a personalized briefing everyday.
diff --git a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.test.ts b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.test.ts
new file mode 100644
index 000000000..3452fafb1
--- /dev/null
+++ b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.test.ts
@@ -0,0 +1,107 @@
+import { describe, expect, it } from 'vitest';
+import { parseStoredDelivery, resolveDeliveryRoute } from './delivery-utils';
+
+describe('delivery-utils', () => {
+ it('parseStoredDelivery ignores malformed entries and keeps valid ones', () => {
+ const parsed = parseStoredDelivery([
+ null,
+ 123,
+ { channel: 'telegram', status: 'sent', target: '-5055658641' },
+ { channel: 'discord', status: 'unknown' },
+ { channel: 'slack', status: 'failed', reason: 'send_failed', error: 'send failed' },
+ { channel: 'email', status: 'sent' },
+ { channel: 'telegram', status: 'skipped', reason: 'bogus_reason' },
+ ]);
+
+ expect(parsed).toEqual([
+ {
+ channel: 'telegram',
+ status: 'sent',
+ target: '-5055658641',
+ },
+ {
+ channel: 'slack',
+ status: 'failed',
+ reason: 'send_failed',
+ error: 'send failed',
+ },
+ {
+ channel: 'telegram',
+ status: 'skipped',
+ },
+ ]);
+ });
+
+ it('resolveDeliveryRoute infers single discord fallback channel target', () => {
+ const resolution = resolveDeliveryRoute({
+ channel: 'discord',
+ channelsConfig: {
+ discord: {
+ enabled: true,
+ guilds: {
+ 'guild-1': {
+ channels: {
+ '1234567890': { enabled: true },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ expect(resolution).toEqual({
+ configured: true,
+ route: {
+ channel: 'discord',
+ target: 'channel:1234567890',
+ },
+ });
+ });
+
+ it('resolveDeliveryRoute infers single slack fallback channel target', () => {
+ const resolution = resolveDeliveryRoute({
+ channel: 'slack',
+ channelsConfig: {
+ slack: {
+ enabled: true,
+ channels: {
+ C123456: { enabled: true },
+ },
+ },
+ },
+ });
+
+ expect(resolution).toEqual({
+ configured: true,
+ route: {
+ channel: 'slack',
+ target: 'channel:C123456',
+ },
+ });
+ });
+
+ it('resolveDeliveryRoute marks ambiguous fallback when multiple discord channels exist', () => {
+ const resolution = resolveDeliveryRoute({
+ channel: 'discord',
+ channelsConfig: {
+ discord: {
+ enabled: true,
+ guilds: {
+ 'guild-1': {
+ channels: {
+ '123': { enabled: true },
+ '456': { enabled: true },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ expect(resolution).toEqual({
+ configured: true,
+ route: null,
+ skipReason: 'ambiguous_target',
+ });
+ });
+});
diff --git a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
index 965fb1b7a..c77effad0 100644
--- a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
+++ b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
@@ -32,11 +32,17 @@ type DeliveryRoute = {
type DeliveryApi = CommandCapableRuntime & {
config: unknown;
- logger: { warn?: (message: string) => void };
+ logger: { info?: (message: string) => void; warn?: (message: string) => void };
};
type SkipReason = Extract;
+export type DeliveryRouteResolution = {
+ configured: boolean;
+ route: DeliveryRoute | null;
+ skipReason?: SkipReason;
+};
+
function asObject(value: unknown): Record {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record)
@@ -87,14 +93,10 @@ function collectFallbackTargets(
return channels.map(([channelId]) => `channel:${channelId}`);
}
-function resolveDeliveryRoute(params: {
+export function resolveDeliveryRoute(params: {
channel: DeliveryChannel;
channelsConfig: Record;
-}): {
- configured: boolean;
- route: DeliveryRoute | null;
- skipReason?: SkipReason;
-} {
+}): DeliveryRouteResolution {
const rawChannelConfig = asObject(params.channelsConfig[params.channel]);
if (Object.keys(rawChannelConfig).length === 0 || rawChannelConfig.enabled === false) {
return { configured: false, route: null };
@@ -294,6 +296,29 @@ export function formatDeliverySummary(delivery: BriefingDeliveryResult[]): strin
});
}
+export function logDeliveryOutcomeEvents(
+ api: Pick,
+ delivery: BriefingDeliveryResult[]
+): void {
+ for (const entry of delivery) {
+ const reason = entry.reason ?? 'none';
+ const target = entry.target ?? 'none';
+ const eventLine =
+ `event=morning_briefing_delivery_outcome` +
+ ` outcome=${entry.status}` +
+ ` channel=${entry.channel}` +
+ ` reason=${reason}` +
+ ` target=${target}`;
+ api.logger.info?.(eventLine);
+ if (entry.status === 'failed') {
+ const detail = entry.error ?? 'unknown_error';
+ api.logger.warn?.(
+ `event=morning_briefing_delivery_failure channel=${entry.channel} detail=${detail}`
+ );
+ }
+ }
+}
+
export async function deliverBriefingToConfiguredChannels(
api: DeliveryApi,
markdown: string
diff --git a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.lifecycle.test.ts b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.lifecycle.test.ts
index 9d03e0f30..ffe91ed67 100644
--- a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.lifecycle.test.ts
+++ b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.lifecycle.test.ts
@@ -41,6 +41,8 @@ type TestHarness = {
accountId?: string;
message: string;
}>;
+ loggerInfo: ReturnType;
+ loggerWarn: ReturnType;
runCommandWithTimeout: ReturnType;
};
@@ -240,6 +242,8 @@ async function createHarness(options?: {
let statusHttpHandler: ((_req: unknown, res: FakeResponse) => Promise) | null = null;
let enableHttpHandler: ((req: unknown, res: FakeResponse) => Promise) | null = null;
let runHttpHandler: ((_req: unknown, res: FakeResponse) => Promise) | null = null;
+ const loggerInfo = vi.fn();
+ const loggerWarn = vi.fn();
morningBriefingPlugin.register({
runtime: {
@@ -254,7 +258,7 @@ async function createHarness(options?: {
agents: { defaults: { userTimezone: 'America/Chicago' } },
...(options?.omitRuntimeChannelsConfig ? {} : { channels: options?.channelsConfig ?? {} }),
},
- logger: { warn: vi.fn() },
+ logger: { info: loggerInfo, warn: loggerWarn },
registerCommand: (def: { handler: (ctx: { args?: string }) => Promise<{ text: string }> }) => {
commandHandler = def.handler;
},
@@ -286,6 +290,8 @@ async function createHarness(options?: {
runHttpHandler,
cronJobs,
sentMessages,
+ loggerInfo,
+ loggerWarn,
runCommandWithTimeout,
};
}
@@ -877,4 +883,63 @@ describe('morning briefing lifecycle', () => {
expect(sendCalls[0]?.[1]).toMatchObject({ timeoutMs: 120_000 });
expect(sendCalls[1]?.[1]).toMatchObject({ timeoutMs: 120_000 });
});
+
+ it('emits delivery outcome metric logs for sent/skipped/failed results', async () => {
+ const harness = await createHarness({
+ githubAuthReady: true,
+ githubIssues: [
+ {
+ title: 'Delivery observability smoke check',
+ url: 'https://github.com/Kilo-Org/cloud/issues/910',
+ updatedAt: '2026-04-25T00:10:00Z',
+ },
+ ],
+ channelsConfig: {
+ telegram: {
+ enabled: true,
+ defaultTo: '-5055658641',
+ },
+ discord: {
+ enabled: true,
+ },
+ slack: {
+ enabled: true,
+ defaultTo: 'channel:C123',
+ },
+ },
+ messageSendFailures: {
+ slack: 'slack send failed',
+ },
+ });
+
+ const response = new FakeResponse();
+ await harness.runHttpHandler({}, response);
+ expect(response.statusCode).toBe(200);
+
+ const infoMessages = harness.loggerInfo.mock.calls.map(call => String(call[0]));
+ expect(
+ infoMessages.some(message =>
+ message.includes('event=morning_briefing_delivery_outcome outcome=sent channel=telegram')
+ )
+ ).toBe(true);
+ expect(
+ infoMessages.some(message =>
+ message.includes('event=morning_briefing_delivery_outcome outcome=skipped channel=discord')
+ )
+ ).toBe(true);
+ expect(
+ infoMessages.some(message =>
+ message.includes('event=morning_briefing_delivery_outcome outcome=failed channel=slack')
+ )
+ ).toBe(true);
+
+ const warnMessages = harness.loggerWarn.mock.calls.map(call => String(call[0]));
+ expect(
+ warnMessages.some(message =>
+ message.includes(
+ 'event=morning_briefing_delivery_failure channel=slack detail=slack send failed'
+ )
+ )
+ ).toBe(true);
+ });
});
diff --git a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.ts b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.ts
index 45e34ce5a..0a209756d 100644
--- a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.ts
+++ b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.ts
@@ -8,6 +8,7 @@ import {
type BriefingDeliveryResult,
deliverBriefingToConfiguredChannels,
formatDeliverySummary,
+ logDeliveryOutcomeEvents,
parseStoredDelivery,
} from './delivery-utils';
import { CommandExecutionError, runCommand } from './command-utils';
@@ -226,7 +227,7 @@ async function removeDuplicateBriefingCronJobs(
) => Promise<{ stdout: string; stderr: string; code: number | null }>;
};
};
- logger: { warn?: (message: string) => void };
+ logger: { info?: (message: string) => void; warn?: (message: string) => void };
},
canonicalId: string
): Promise {
@@ -266,7 +267,7 @@ function resolveDefaults(api: {
}
function resolveEffectiveTimezone(
- api: { logger: { warn?: (message: string) => void } },
+ api: { logger: { info?: (message: string) => void; warn?: (message: string) => void } },
timezone: string,
context: 'enable' | 'schedule' | 'date'
): string {
@@ -408,7 +409,7 @@ async function ensureCronJob(
) => Promise<{ stdout: string; stderr: string; code: number | null }>;
};
};
- logger: { warn?: (message: string) => void };
+ logger: { info?: (message: string) => void; warn?: (message: string) => void };
},
config: StoredConfig
): Promise<{ cronJobId: string; cron: string; timezone: string }> {
@@ -787,7 +788,7 @@ async function generateBriefing(
};
};
config: unknown;
- logger: { warn?: (message: string) => void };
+ logger: { info?: (message: string) => void; warn?: (message: string) => void };
},
dateKey: string
): Promise<{
@@ -850,6 +851,7 @@ async function generateBriefing(
error: errorText,
}));
}
+ logDeliveryOutcomeEvents(api, delivery);
await patchStoredStatus(paths, {
lastGeneratedDate: dateKey,
@@ -911,7 +913,7 @@ async function resolveDateKeyForOffset(
};
};
pluginConfig?: Record;
- logger: { warn?: (message: string) => void };
+ logger: { info?: (message: string) => void; warn?: (message: string) => void };
},
offset: number
): Promise {
diff --git a/services/kiloclaw/src/routes/platform-morning-briefing.test.ts b/services/kiloclaw/src/routes/platform-morning-briefing.test.ts
index ccb9cff47..86469cf81 100644
--- a/services/kiloclaw/src/routes/platform-morning-briefing.test.ts
+++ b/services/kiloclaw/src/routes/platform-morning-briefing.test.ts
@@ -210,7 +210,7 @@ describe('platform morning-briefing warm-up handling', () => {
expect(runMorningBriefing).toHaveBeenCalledTimes(1);
});
- it('returns warm-up payload for run timeout instead of generic 500', async () => {
+ it('returns timeout code for run timeout instead of generic 500', async () => {
const runMorningBriefing = vi
.fn<() => Promise>()
.mockRejectedValue(
From 668d62b722d70bb06e8c20e804b994462e395908 Mon Sep 17 00:00:00 2001
From: joshavant <830519+joshavant@users.noreply.github.com>
Date: Sat, 25 Apr 2026 01:45:10 -0500
Subject: [PATCH 16/19] refactor(kiloclaw): share morning briefing delivery
constants
Extract delivery channel/status/reason enums into a shared module and reuse them in gateway response schemas and plugin delivery utilities to prevent drift.
---
.../src/delivery-constants.ts | 10 ++++++++++
.../src/delivery-utils.ts | 9 +--------
.../gateway-controller-types.ts | 20 ++++++++-----------
.../morning-briefing-delivery-constants.ts | 10 ++++++++++
4 files changed, 29 insertions(+), 20 deletions(-)
create mode 100644 services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts
create mode 100644 services/kiloclaw/src/durable-objects/morning-briefing-delivery-constants.ts
diff --git a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts
new file mode 100644
index 000000000..04847c8af
--- /dev/null
+++ b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts
@@ -0,0 +1,10 @@
+export const DELIVERY_CHANNELS = ['telegram', 'discord', 'slack'] as const;
+
+export const DELIVERY_STATUSES = ['sent', 'skipped', 'failed'] as const;
+
+export const DELIVERY_REASONS = [
+ 'missing_target',
+ 'ambiguous_target',
+ 'send_failed',
+ 'config_unavailable',
+] as const;
diff --git a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
index c77effad0..810771396 100644
--- a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
+++ b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
@@ -1,18 +1,11 @@
import { formatBriefingMarkdownForMessage } from './briefing-utils';
import { type CommandCapableRuntime, isTimeoutExecutionError, runCommand } from './command-utils';
+import { DELIVERY_CHANNELS, DELIVERY_REASONS, DELIVERY_STATUSES } from './delivery-constants';
-export const DELIVERY_CHANNELS = ['telegram', 'discord', 'slack'] as const;
export type DeliveryChannel = (typeof DELIVERY_CHANNELS)[number];
-export const DELIVERY_STATUSES = ['sent', 'skipped', 'failed'] as const;
export type DeliveryStatus = (typeof DELIVERY_STATUSES)[number];
-export const DELIVERY_REASONS = [
- 'missing_target',
- 'ambiguous_target',
- 'send_failed',
- 'config_unavailable',
-] as const;
export type DeliveryReason = (typeof DELIVERY_REASONS)[number];
export type BriefingDeliveryResult = {
diff --git a/services/kiloclaw/src/durable-objects/gateway-controller-types.ts b/services/kiloclaw/src/durable-objects/gateway-controller-types.ts
index f69c2b281..15fe284a1 100644
--- a/services/kiloclaw/src/durable-objects/gateway-controller-types.ts
+++ b/services/kiloclaw/src/durable-objects/gateway-controller-types.ts
@@ -1,4 +1,9 @@
import { z, type ZodType } from 'zod';
+import {
+ MORNING_BRIEFING_DELIVERY_CHANNELS,
+ MORNING_BRIEFING_DELIVERY_REASONS,
+ MORNING_BRIEFING_DELIVERY_STATUSES,
+} from './morning-briefing-delivery-constants';
export type GatewayProcessStatus = {
state: 'stopped' | 'starting' | 'running' | 'stopping' | 'crashed' | 'shutting_down';
@@ -104,21 +109,12 @@ const MorningBriefingSourceReadinessSchema = z.object({
summary: z.string(),
});
-const DELIVERY_CHANNELS = ['telegram', 'discord', 'slack'] as const;
-const DELIVERY_STATUSES = ['sent', 'skipped', 'failed'] as const;
-const DELIVERY_REASONS = [
- 'missing_target',
- 'ambiguous_target',
- 'send_failed',
- 'config_unavailable',
-] as const;
-
const MorningBriefingDeliverySchema = z.object({
- channel: z.enum(DELIVERY_CHANNELS),
- status: z.enum(DELIVERY_STATUSES),
+ channel: z.enum(MORNING_BRIEFING_DELIVERY_CHANNELS),
+ status: z.enum(MORNING_BRIEFING_DELIVERY_STATUSES),
target: z.string().optional(),
accountId: z.string().optional(),
- reason: z.enum(DELIVERY_REASONS).optional(),
+ reason: z.enum(MORNING_BRIEFING_DELIVERY_REASONS).optional(),
error: z.string().optional(),
});
diff --git a/services/kiloclaw/src/durable-objects/morning-briefing-delivery-constants.ts b/services/kiloclaw/src/durable-objects/morning-briefing-delivery-constants.ts
new file mode 100644
index 000000000..b44e27ddd
--- /dev/null
+++ b/services/kiloclaw/src/durable-objects/morning-briefing-delivery-constants.ts
@@ -0,0 +1,10 @@
+export const MORNING_BRIEFING_DELIVERY_CHANNELS = ['telegram', 'discord', 'slack'] as const;
+
+export const MORNING_BRIEFING_DELIVERY_STATUSES = ['sent', 'skipped', 'failed'] as const;
+
+export const MORNING_BRIEFING_DELIVERY_REASONS = [
+ 'missing_target',
+ 'ambiguous_target',
+ 'send_failed',
+ 'config_unavailable',
+] as const;
From 980531b704801fe16004b7e8c3013bdc3e4afa68 Mon Sep 17 00:00:00 2001
From: joshavant <830519+joshavant@users.noreply.github.com>
Date: Sat, 25 Apr 2026 01:54:11 -0500
Subject: [PATCH 17/19] Revert "refactor(kiloclaw): share morning briefing
delivery constants"
This reverts commit 384cc0d4f18ac7744c3cce27f6effad603248bc1.
---
.../src/delivery-constants.ts | 10 ----------
.../src/delivery-utils.ts | 9 ++++++++-
.../gateway-controller-types.ts | 20 +++++++++++--------
.../morning-briefing-delivery-constants.ts | 10 ----------
4 files changed, 20 insertions(+), 29 deletions(-)
delete mode 100644 services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts
delete mode 100644 services/kiloclaw/src/durable-objects/morning-briefing-delivery-constants.ts
diff --git a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts
deleted file mode 100644
index 04847c8af..000000000
--- a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export const DELIVERY_CHANNELS = ['telegram', 'discord', 'slack'] as const;
-
-export const DELIVERY_STATUSES = ['sent', 'skipped', 'failed'] as const;
-
-export const DELIVERY_REASONS = [
- 'missing_target',
- 'ambiguous_target',
- 'send_failed',
- 'config_unavailable',
-] as const;
diff --git a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
index 810771396..c77effad0 100644
--- a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
+++ b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
@@ -1,11 +1,18 @@
import { formatBriefingMarkdownForMessage } from './briefing-utils';
import { type CommandCapableRuntime, isTimeoutExecutionError, runCommand } from './command-utils';
-import { DELIVERY_CHANNELS, DELIVERY_REASONS, DELIVERY_STATUSES } from './delivery-constants';
+export const DELIVERY_CHANNELS = ['telegram', 'discord', 'slack'] as const;
export type DeliveryChannel = (typeof DELIVERY_CHANNELS)[number];
+export const DELIVERY_STATUSES = ['sent', 'skipped', 'failed'] as const;
export type DeliveryStatus = (typeof DELIVERY_STATUSES)[number];
+export const DELIVERY_REASONS = [
+ 'missing_target',
+ 'ambiguous_target',
+ 'send_failed',
+ 'config_unavailable',
+] as const;
export type DeliveryReason = (typeof DELIVERY_REASONS)[number];
export type BriefingDeliveryResult = {
diff --git a/services/kiloclaw/src/durable-objects/gateway-controller-types.ts b/services/kiloclaw/src/durable-objects/gateway-controller-types.ts
index 15fe284a1..f69c2b281 100644
--- a/services/kiloclaw/src/durable-objects/gateway-controller-types.ts
+++ b/services/kiloclaw/src/durable-objects/gateway-controller-types.ts
@@ -1,9 +1,4 @@
import { z, type ZodType } from 'zod';
-import {
- MORNING_BRIEFING_DELIVERY_CHANNELS,
- MORNING_BRIEFING_DELIVERY_REASONS,
- MORNING_BRIEFING_DELIVERY_STATUSES,
-} from './morning-briefing-delivery-constants';
export type GatewayProcessStatus = {
state: 'stopped' | 'starting' | 'running' | 'stopping' | 'crashed' | 'shutting_down';
@@ -109,12 +104,21 @@ const MorningBriefingSourceReadinessSchema = z.object({
summary: z.string(),
});
+const DELIVERY_CHANNELS = ['telegram', 'discord', 'slack'] as const;
+const DELIVERY_STATUSES = ['sent', 'skipped', 'failed'] as const;
+const DELIVERY_REASONS = [
+ 'missing_target',
+ 'ambiguous_target',
+ 'send_failed',
+ 'config_unavailable',
+] as const;
+
const MorningBriefingDeliverySchema = z.object({
- channel: z.enum(MORNING_BRIEFING_DELIVERY_CHANNELS),
- status: z.enum(MORNING_BRIEFING_DELIVERY_STATUSES),
+ channel: z.enum(DELIVERY_CHANNELS),
+ status: z.enum(DELIVERY_STATUSES),
target: z.string().optional(),
accountId: z.string().optional(),
- reason: z.enum(MORNING_BRIEFING_DELIVERY_REASONS).optional(),
+ reason: z.enum(DELIVERY_REASONS).optional(),
error: z.string().optional(),
});
diff --git a/services/kiloclaw/src/durable-objects/morning-briefing-delivery-constants.ts b/services/kiloclaw/src/durable-objects/morning-briefing-delivery-constants.ts
deleted file mode 100644
index b44e27ddd..000000000
--- a/services/kiloclaw/src/durable-objects/morning-briefing-delivery-constants.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export const MORNING_BRIEFING_DELIVERY_CHANNELS = ['telegram', 'discord', 'slack'] as const;
-
-export const MORNING_BRIEFING_DELIVERY_STATUSES = ['sent', 'skipped', 'failed'] as const;
-
-export const MORNING_BRIEFING_DELIVERY_REASONS = [
- 'missing_target',
- 'ambiguous_target',
- 'send_failed',
- 'config_unavailable',
-] as const;
From dfedfa9be78d6bc05f2da02afc05f87a304885ca Mon Sep 17 00:00:00 2001
From: joshavant <830519+joshavant@users.noreply.github.com>
Date: Mon, 27 Apr 2026 10:26:35 -0500
Subject: [PATCH 18/19] refactor(kiloclaw): dedupe delivery enums via shared
constants
Define morning briefing delivery channel/status/reason enums once and reuse them in both plugin delivery logic and gateway controller response schemas to prevent drift.
---
.../src/delivery-constants.ts | 10 ++++++++++
.../src/delivery-utils.ts | 9 +--------
.../durable-objects/gateway-controller-types.ts | 14 +++++---------
3 files changed, 16 insertions(+), 17 deletions(-)
create mode 100644 services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts
diff --git a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts
new file mode 100644
index 000000000..04847c8af
--- /dev/null
+++ b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-constants.ts
@@ -0,0 +1,10 @@
+export const DELIVERY_CHANNELS = ['telegram', 'discord', 'slack'] as const;
+
+export const DELIVERY_STATUSES = ['sent', 'skipped', 'failed'] as const;
+
+export const DELIVERY_REASONS = [
+ 'missing_target',
+ 'ambiguous_target',
+ 'send_failed',
+ 'config_unavailable',
+] as const;
diff --git a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
index c77effad0..810771396 100644
--- a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
+++ b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/delivery-utils.ts
@@ -1,18 +1,11 @@
import { formatBriefingMarkdownForMessage } from './briefing-utils';
import { type CommandCapableRuntime, isTimeoutExecutionError, runCommand } from './command-utils';
+import { DELIVERY_CHANNELS, DELIVERY_REASONS, DELIVERY_STATUSES } from './delivery-constants';
-export const DELIVERY_CHANNELS = ['telegram', 'discord', 'slack'] as const;
export type DeliveryChannel = (typeof DELIVERY_CHANNELS)[number];
-export const DELIVERY_STATUSES = ['sent', 'skipped', 'failed'] as const;
export type DeliveryStatus = (typeof DELIVERY_STATUSES)[number];
-export const DELIVERY_REASONS = [
- 'missing_target',
- 'ambiguous_target',
- 'send_failed',
- 'config_unavailable',
-] as const;
export type DeliveryReason = (typeof DELIVERY_REASONS)[number];
export type BriefingDeliveryResult = {
diff --git a/services/kiloclaw/src/durable-objects/gateway-controller-types.ts b/services/kiloclaw/src/durable-objects/gateway-controller-types.ts
index f69c2b281..74eb6c496 100644
--- a/services/kiloclaw/src/durable-objects/gateway-controller-types.ts
+++ b/services/kiloclaw/src/durable-objects/gateway-controller-types.ts
@@ -1,4 +1,9 @@
import { z, type ZodType } from 'zod';
+import {
+ DELIVERY_CHANNELS,
+ DELIVERY_REASONS,
+ DELIVERY_STATUSES,
+} from '../../plugins/kiloclaw-morning-briefing/src/delivery-constants';
export type GatewayProcessStatus = {
state: 'stopped' | 'starting' | 'running' | 'stopping' | 'crashed' | 'shutting_down';
@@ -104,15 +109,6 @@ const MorningBriefingSourceReadinessSchema = z.object({
summary: z.string(),
});
-const DELIVERY_CHANNELS = ['telegram', 'discord', 'slack'] as const;
-const DELIVERY_STATUSES = ['sent', 'skipped', 'failed'] as const;
-const DELIVERY_REASONS = [
- 'missing_target',
- 'ambiguous_target',
- 'send_failed',
- 'config_unavailable',
-] as const;
-
const MorningBriefingDeliverySchema = z.object({
channel: z.enum(DELIVERY_CHANNELS),
status: z.enum(DELIVERY_STATUSES),
From 572c9dce61407a869f6bcb4065089330b751cffd Mon Sep 17 00:00:00 2001
From: joshavant <830519+joshavant@users.noreply.github.com>
Date: Mon, 27 Apr 2026 11:23:35 -0500
Subject: [PATCH 19/19] fix(kiloclaw): import delivery channels from constants
module
Resolve plugin pack build failure by importing DELIVERY_CHANNELS directly from delivery-constants instead of delivery-utils, keeping a single source for delivery enum declarations.
---
.../kiloclaw/plugins/kiloclaw-morning-briefing/src/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.ts b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.ts
index 0a209756d..4c1fed24d 100644
--- a/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.ts
+++ b/services/kiloclaw/plugins/kiloclaw-morning-briefing/src/index.ts
@@ -4,13 +4,13 @@ import { Type } from '@sinclair/typebox';
import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
import { buildBriefingMarkdown, offsetDateKey, resolveBriefingPath } from './briefing-utils';
import {
- DELIVERY_CHANNELS,
type BriefingDeliveryResult,
deliverBriefingToConfiguredChannels,
formatDeliverySummary,
logDeliveryOutcomeEvents,
parseStoredDelivery,
} from './delivery-utils';
+import { DELIVERY_CHANNELS } from './delivery-constants';
import { CommandExecutionError, runCommand } from './command-utils';
import {
filterEnabledBriefingJobs,