From a94da4fce2f4c11e73d5ae4704038450c0973f7f Mon Sep 17 00:00:00 2001 From: agent Date: Wed, 18 Mar 2026 12:58:45 -0700 Subject: [PATCH 1/3] fix(core): skip Discord hooks for Discord-triggered jobs to prevent duplicate messages When a job is triggered from Discord, the Discord manager already streams all output to the channel in real-time via the onMessage callback. The hooks system was also firing Discord after_run/on_error hooks, causing duplicate messages in the channel. This fix: - Adds triggerType to HookContext so hooks know how a job was triggered - Populates triggerType from job metadata's trigger_type field - Filters out Discord-type hooks when triggerType is "discord" Co-Authored-By: Claude Opus 4.6 --- .../core/src/fleet-manager/job-control.ts | 42 ++++++++++++++++--- packages/core/src/hooks/types.ts | 9 ++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/core/src/fleet-manager/job-control.ts b/packages/core/src/fleet-manager/job-control.ts index f9c2e274..4ab53346 100644 --- a/packages/core/src/fleet-manager/job-control.ts +++ b/packages/core/src/fleet-manager/job-control.ts @@ -509,9 +509,24 @@ export class JobControl { }); // Execute after_run hooks (run for all events) - if (agent.hooks.after_run && agent.hooks.after_run.length > 0) { - logger.debug(`Executing ${agent.hooks.after_run.length} after_run hook(s)`); - const afterRunResult = await hookExecutor.executeHooks(agent.hooks, context, "after_run"); + // Skip Discord hooks when the job was triggered from Discord, since the + // Discord manager already streams output to the channel in real-time. + const filteredAfterRun = + context.triggerType === "discord" + ? (agent.hooks.after_run ?? []).filter((h) => h.type !== "discord") + : agent.hooks.after_run; + if (filteredAfterRun && filteredAfterRun.length > 0) { + if (filteredAfterRun.length !== (agent.hooks.after_run ?? []).length) { + logger.debug( + "Skipping Discord after_run hook(s) for Discord-triggered job to prevent duplicates", + ); + } + logger.debug(`Executing ${filteredAfterRun.length} after_run hook(s)`); + const afterRunResult = await hookExecutor.executeHooks( + { ...agent.hooks, after_run: filteredAfterRun }, + context, + "after_run", + ); if (afterRunResult.shouldFailJob) { logger.warn(`Hook failure with continue_on_error=false detected for job ${jobMetadata.id}`); @@ -519,9 +534,23 @@ export class JobControl { } // Execute on_error hooks (only for failed events) - if (event === "failed" && agent.hooks.on_error && agent.hooks.on_error.length > 0) { - logger.debug(`Executing ${agent.hooks.on_error.length} on_error hook(s)`); - const onErrorResult = await hookExecutor.executeHooks(agent.hooks, context, "on_error"); + // Also skip Discord hooks for Discord-triggered jobs here + const filteredOnError = + context.triggerType === "discord" + ? (agent.hooks.on_error ?? []).filter((h) => h.type !== "discord") + : agent.hooks.on_error; + if (event === "failed" && filteredOnError && filteredOnError.length > 0) { + if (filteredOnError.length !== (agent.hooks.on_error ?? []).length) { + logger.debug( + "Skipping Discord on_error hook(s) for Discord-triggered job to prevent duplicates", + ); + } + logger.debug(`Executing ${filteredOnError.length} on_error hook(s)`); + const onErrorResult = await hookExecutor.executeHooks( + { ...agent.hooks, on_error: filteredOnError }, + context, + "on_error", + ); if (onErrorResult.shouldFailJob) { logger.warn( @@ -556,6 +585,7 @@ export class JobControl { return { event, + triggerType: jobMetadata.trigger_type, job: { id: jobMetadata.id, agentId: agent.qualifiedName, diff --git a/packages/core/src/hooks/types.ts b/packages/core/src/hooks/types.ts index 356423db..fcb76e9e 100644 --- a/packages/core/src/hooks/types.ts +++ b/packages/core/src/hooks/types.ts @@ -149,6 +149,15 @@ export interface HookContext { name?: string; }; + /** + * How the job was triggered (e.g. "manual", "schedule", "discord", "slack") + * + * Used to prevent duplicate notifications - for example, Discord hooks are + * skipped when triggerType is "discord" because the Discord manager already + * streams output to the channel in real-time. + */ + triggerType?: string; + /** * Agent-provided metadata (from metadata.json or configured metadata_file) * From ac9140108f0f56a13b6baa35639852b0245d467d Mon Sep 17 00:00:00 2001 From: agent Date: Wed, 18 Mar 2026 21:36:53 -0700 Subject: [PATCH 2/3] refactor: use TriggerType enum instead of string for triggerType field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses CodeRabbit review feedback — import and use the TriggerType type from job-metadata schema for stronger type safety and IDE autocomplete. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/hooks/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/hooks/types.ts b/packages/core/src/hooks/types.ts index fcb76e9e..44bb42d7 100644 --- a/packages/core/src/hooks/types.ts +++ b/packages/core/src/hooks/types.ts @@ -9,6 +9,8 @@ * This file contains only runtime types not derived from Zod schemas. */ +import type { TriggerType } from "../state/schemas/job-metadata.js"; + // Import config types that are validated by Zod // We import both output types (after defaults) and input types (for construction) import type { @@ -156,7 +158,7 @@ export interface HookContext { * skipped when triggerType is "discord" because the Discord manager already * streams output to the channel in real-time. */ - triggerType?: string; + triggerType?: TriggerType; /** * Agent-provided metadata (from metadata.json or configured metadata_file) From 90237e1d2bef82e32fadc0719d77c1112318c9a8 Mon Sep 17 00:00:00 2001 From: agent Date: Wed, 18 Mar 2026 21:39:57 -0700 Subject: [PATCH 3/3] style: fix import ordering for Biome lint Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/hooks/types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/hooks/types.ts b/packages/core/src/hooks/types.ts index 44bb42d7..c18a611a 100644 --- a/packages/core/src/hooks/types.ts +++ b/packages/core/src/hooks/types.ts @@ -9,8 +9,6 @@ * This file contains only runtime types not derived from Zod schemas. */ -import type { TriggerType } from "../state/schemas/job-metadata.js"; - // Import config types that are validated by Zod // We import both output types (after defaults) and input types (for construction) import type { @@ -28,6 +26,7 @@ import type { WebhookHookConfig, WebhookHookConfigInput, } from "../config/schema.js"; +import type { TriggerType } from "../state/schemas/job-metadata.js"; // Re-export for convenience within the hooks module // Export input types (used by tests and runners)