From 11e1363d2d3ef13562eda13a1a561716fb38caff Mon Sep 17 00:00:00 2001 From: David Rudduck <47308254+davidrudduck@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:39:20 +1000 Subject: [PATCH] feat(hooks): add trigger and channelId to plugin hook agent context (#28623) * feat(hooks): add trigger and channelId to plugin hook agent context Adds `trigger` and `channelId` fields to `PluginHookAgentContext` so plugins can determine what initiated the agent run and which channel it originated from, without session-key parsing or Redis bridging. trigger values: "user", "heartbeat", "cron", "memory" channelId values: "telegram", "discord", "whatsapp", etc. Both fields are threaded through run.ts and attempt.ts hookCtx so all hook phases receive them (before_model_resolve, before_prompt_build, before_agent_start, llm_input, llm_output, agent_end). channelId falls back from messageChannel to messageProvider when the former is not set. followup-runner passes originatingChannel so queued followup runs also carry channel context. * docs(changelog): note hook context parity fix for #28623 --------- Co-authored-by: Vincent Koc --- CHANGELOG.md | 1 + src/agents/pi-embedded-runner/run.ts | 3 +++ src/agents/pi-embedded-runner/run/attempt.ts | 2 ++ src/agents/pi-embedded-runner/run/params.ts | 2 ++ src/auto-reply/reply/agent-runner-execution.ts | 1 + src/auto-reply/reply/agent-runner-memory.ts | 1 + src/auto-reply/reply/followup-runner.ts | 2 ++ src/commands/agent.ts | 1 + src/cron/isolated-agent/run.ts | 1 + src/plugins/types.ts | 4 ++++ 10 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 237f01245fd..03619fa7905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai - CLI/Config validation and routing hardening: dedupe `openclaw config validate` failures to a single authoritative report, expose allowed-values metadata/hints across core Zod and plugin AJV validation (including `--json` fields), sanitize terminal-rendered validation text, and make command-path parsing root-option-aware across preaction/route/lazy registration (including routed `config get/unset` with split root options). Thanks @gumadeiras. - Models/config env propagation: apply `config.env.vars` before implicit provider discovery in models bootstrap so config-scoped credentials are visible to implicit provider resolution paths. (#32295) Thanks @hsiaoa. - Hooks/runtime stability: keep the internal hook handler registry on a `globalThis` singleton so hook registration/dispatch remains consistent when bundling emits duplicate module copies. (#32292) Thanks @Drickon. +- Hooks/plugin context parity: ensure `llm_input` hooks in embedded attempts receive the same `trigger` and `channelId`-aware `hookCtx` used by the other hook phases, preserving channel/trigger-scoped plugin behavior. (#28623) Thanks @davidrudduck and @vincentkoc. - Restart sentinel formatting: avoid duplicate `Reason:` lines when restart message text already matches `stats.reason`, keeping restart notifications concise for users and downstream parsers. (#32083) Thanks @velamints2. - Voice-call/Twilio signature verification: retry signature validation across deterministic URL port variants (with/without port) to handle mixed Twilio signing behavior behind reverse proxies and non-standard ports. (#25140) Thanks @drvoss. - Hooks/webhook ACK compatibility: return `200` (instead of `202`) for successful `/hooks/agent` requests so providers that require `200` (for example Forward Email) accept dispatched agent hook deliveries. (#28204) Thanks @Glucksberg. diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 9d440bda6eb..3c5d5a67f6f 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -263,6 +263,8 @@ export async function runEmbeddedPiAgent( sessionId: params.sessionId, workspaceDir: resolvedWorkspace, messageProvider: params.messageProvider ?? undefined, + trigger: params.trigger, + channelId: params.messageChannel ?? params.messageProvider ?? undefined, }; if (hookRunner?.hasHooks("before_model_resolve")) { try { @@ -715,6 +717,7 @@ export async function runEmbeddedPiAgent( const attempt = await runEmbeddedAttempt({ sessionId: params.sessionId, sessionKey: params.sessionKey, + trigger: params.trigger, messageChannel: params.messageChannel, messageProvider: params.messageProvider, agentAccountId: params.agentAccountId, diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 64a8f2fd2cf..d1b158eee9f 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -1356,6 +1356,8 @@ export async function runEmbeddedAttempt( sessionId: params.sessionId, workspaceDir: params.workspaceDir, messageProvider: params.messageProvider ?? undefined, + trigger: params.trigger, + channelId: params.messageChannel ?? params.messageProvider ?? undefined, }; const hookResult = await resolvePromptBuildHookResult({ prompt: params.prompt, diff --git a/src/agents/pi-embedded-runner/run/params.ts b/src/agents/pi-embedded-runner/run/params.ts index 7362f7fcdc3..647d9dd4a32 100644 --- a/src/agents/pi-embedded-runner/run/params.ts +++ b/src/agents/pi-embedded-runner/run/params.ts @@ -26,6 +26,8 @@ export type RunEmbeddedPiAgentParams = { messageChannel?: string; messageProvider?: string; agentAccountId?: string; + /** What initiated this agent run: "user", "heartbeat", "cron", or "memory". */ + trigger?: string; /** Delivery target (e.g. telegram:group:123:topic:456) for topic/thread routing. */ messageTo?: string; /** Thread/topic identifier for routing replies to the originating thread. */ diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 70d7becf762..ea8c25c1e52 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -295,6 +295,7 @@ export async function runAgentTurnWithFallback(params: { }); return runEmbeddedPiAgent({ ...embeddedContext, + trigger: params.isHeartbeat ? "heartbeat" : "user", groupId: resolveGroupSessionKey(params.sessionCtx)?.id, groupChannel: params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(), diff --git a/src/auto-reply/reply/agent-runner-memory.ts b/src/auto-reply/reply/agent-runner-memory.ts index 985a5e2ee6c..e14946ce8c2 100644 --- a/src/auto-reply/reply/agent-runner-memory.ts +++ b/src/auto-reply/reply/agent-runner-memory.ts @@ -487,6 +487,7 @@ export async function runMemoryFlushIfNeeded(params: { ...embeddedContext, ...senderContext, ...runBaseParams, + trigger: "memory", prompt: resolveMemoryFlushPromptForRun({ prompt: memoryFlushSettings.prompt, cfg: params.cfg, diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index d36080f4443..2a9cf9a550f 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -157,6 +157,8 @@ export function createFollowupRunner(params: { sessionId: queued.run.sessionId, sessionKey: queued.run.sessionKey, agentId: queued.run.agentId, + trigger: "user", + messageChannel: queued.originatingChannel ?? undefined, messageProvider: queued.run.messageProvider, agentAccountId: queued.run.agentAccountId, messageTo: queued.originatingTo, diff --git a/src/commands/agent.ts b/src/commands/agent.ts index b0d3f3f09ce..f1258cb8ced 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -279,6 +279,7 @@ function runAgentAttempt(params: { sessionId: params.sessionId, sessionKey: params.sessionKey, agentId: params.sessionAgentId, + trigger: "user", messageChannel: params.messageChannel, agentAccountId: params.runContext.accountId, messageTo: params.opts.replyTo ?? params.opts.to, diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 623cc6e3eb2..028b2e3ce36 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -490,6 +490,7 @@ export async function runCronIsolatedAgentTurn(params: { sessionId: cronSession.sessionEntry.sessionId, sessionKey: agentSessionKey, agentId, + trigger: "cron", messageChannel, agentAccountId: resolvedDelivery.accountId, sessionFile, diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 2dbb0a23bfc..fb2f645a233 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -340,6 +340,10 @@ export type PluginHookAgentContext = { sessionId?: string; workspaceDir?: string; messageProvider?: string; + /** What initiated this agent run: "user", "heartbeat", "cron", or "memory". */ + trigger?: string; + /** Channel identifier (e.g. "telegram", "discord", "whatsapp"). */ + channelId?: string; }; // before_model_resolve hook