Skip to content

feat: bridge message lifecycle hooks to workspace hook system#16618

Open
DarlingtonDeveloper wants to merge 3 commits intoopenclaw:mainfrom
DarlingtonDeveloper:feat/message-lifecycle-hooks
Open

feat: bridge message lifecycle hooks to workspace hook system#16618
DarlingtonDeveloper wants to merge 3 commits intoopenclaw:mainfrom
DarlingtonDeveloper:feat/message-lifecycle-hooks

Conversation

@DarlingtonDeveloper
Copy link
Contributor

@DarlingtonDeveloper DarlingtonDeveloper commented Feb 14, 2026

Summary

Bridges three message lifecycle events to the internal workspace hook system via triggerInternalHook, enabling workspace hooks (HOOK.md-based) to receive per-message events.

Events Added

Event When Modifying
message:received Inbound message parsed No
message:before Before agent processes Yes (prependContext, systemPrompt)
message:sent After response sent No

Motivation

Workspace hooks currently only receive command:new, command:stop, and gateway:startup. Per-message events are dispatched through the plugin hook runner but never bridged to workspace hooks, making them inaccessible without building a full plugin.

This enables use cases like:

  • Real-time transcript publishing to NATS/message buses
  • Context injection from external knowledge stores
  • Sentiment detection and adaptive prompting
  • Slack thread context injection (eliminates tool call round-trip)

Implementation

  • Added "message" to InternalHookEventType union
  • Added InternalHookResult interface with prependContext and systemPrompt fields
  • Modified triggerInternalHook to collect and merge handler results (prependContext concatenated with \n\n, systemPrompt last-wins)
  • Three triggerInternalHook calls added to the inbound message handling path:
    • dispatch-from-config.ts: fire-and-forget message:received for channel messages
    • attempt.ts: awaited message:received, modifying message:before, fire-and-forget message:sent for agent runner
  • message:before follows the same modifying pattern as before_agent_start in the embedded runner
  • No loader changes needed — src/hooks/loader.ts already registers handlers for arbitrary event strings from HOOK.md metadata

Test Plan

  • Added 6 new unit tests for result-merging behavior in internal-hooks.test.ts
  • pnpm build passes (pre-existing type errors in discord/memory modules unchanged)
  • pnpm test:fast — all 4247 tests pass, 0 regressions
  • Linter: 0 warnings, 0 errors

Closes #7067, #15442, #14735, #12914, #12867, #8807
Partially addresses #7724, #11485, #10502

Greptile Summary

This PR bridges three message lifecycle events (message:received, message:before, message:sent) from the agent runtime into the workspace hook system, enabling HOOK.md-based workspace hooks to observe and modify per-message events. The implementation adds result-merging capabilities to triggerInternalHook for modifying hooks, properly handles prependContext concatenation and last-wins systemPrompt merging, and includes comprehensive test coverage.

The core changes are well-structured and follow existing patterns from the plugin hook system. The message:before hook correctly applies both prependContext and systemPrompt modifications to the session. Test coverage is thorough with 6 new tests covering result merging behavior.

One issue was previously flagged but has since been resolved - the systemPrompt field from message:before is now properly applied via applySystemPromptOverrideToSession (attempt.ts:916-921).

Confidence Score: 4/5

  • Safe to merge with one minor data mapping issue in the embedded runner path
  • The implementation is solid with proper type safety, comprehensive tests (all 4247 passing), and follows existing patterns. The main concern is the incorrect from field mapping in attempt.ts which uses messageTo instead of sender info, though this doesn't affect core functionality since the field is only used for hook context. The systemPrompt application that was previously flagged has been correctly implemented.
  • src/agents/pi-embedded-runner/run/attempt.ts requires attention for sender info mapping

Last reviewed commit: 1a6da0d

@jheeanny
Copy link

Context protection is crucial. We use aggressive pruning (keepLastAssistants=1) and memoryFlush at 60K. How are you measuring impact?

@vincentkoc
Copy link
Member

Duplicate of #12584 and missing tests passing and docs changes

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +910 to +915
if (messageBeforeResult?.prependContext) {
effectivePrompt = `${messageBeforeResult.prependContext}\n\n${effectivePrompt}`;
log.debug(
`message:before hook prepended context (${messageBeforeResult.prependContext.length} chars)`,
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

systemPrompt field from messageBeforeResult is not used. Only prependContext is checked and applied, but InternalHookResult also includes systemPrompt which is merged in triggerInternalHook. Should apply systemPrompt if present, similar to how the plugin before_agent_start hook uses it (line 883-887).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 910:915

Comment:
`systemPrompt` field from `messageBeforeResult` is not used. Only `prependContext` is checked and applied, but `InternalHookResult` also includes `systemPrompt` which is merged in `triggerInternalHook`. Should apply `systemPrompt` if present, similar to how the plugin `before_agent_start` hook uses it (line 883-887).

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +847 to +852
from: "",
content: typeof params.prompt === "string" ? params.prompt : "",
channel: (params.messageChannel ?? params.messageProvider ?? "").toLowerCase(),
senderId: undefined,
senderName: undefined,
commandSource: (params.messageChannel ?? params.messageProvider ?? "").toLowerCase(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sender info hardcoded as empty instead of using available params. params.senderId, params.senderName exist but are set to from: "", senderId: undefined, senderName: undefined. Compare with dispatch-from-config.ts:204-215 which properly uses ctx.From, ctx.SenderId, ctx.SenderName.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 847:852

Comment:
Sender info hardcoded as empty instead of using available params. `params.senderId`, `params.senderName` exist but are set to `from: ""`, `senderId: undefined`, `senderName: undefined`. Compare with `dispatch-from-config.ts:204-215` which properly uses `ctx.From`, `ctx.SenderId`, `ctx.SenderName`.

How can I resolve this? If you propose a fix, please make it concise.

@DarlingtonDeveloper
Copy link
Contributor Author

DarlingtonDeveloper commented Feb 14, 2026

Good question. We're running a multi-agent swarm where context pressure is a real constraint, the orchestrator runs a long-lived WhatsApp/Slack sessions that accumulate fast.

Current approach:

  • Compaction mode safeguard — OpenClaw triggers compaction automatically when context gets heavy
  • Proactive monitoring — heartbeat checks session_status, flushes state to memory files at 70% context usage, alerts at 80%
  • Memory architecture — daily logs (memory/YYYY-MM-DD.md) for raw state, curated MEMORY.md for long-term recall. Post-compaction, the agent reads these to recover context.
  • Worker isolation — heavy work spawns sub-agents with focused briefings, not the main session. Keeps the orchestrator's context lean.

Impact measurement is mostly qualitative right now — did the agent lose track of what it was doing post-compaction? Did it recover from memory files correctly? We've had 6 compactions today and the main failure mode is losing in-flight conversation state during the ~30s compaction window.

The hooks in this PR are partly motivated by this — message:before lets us inject context from an external knowledge store at message time rather than keeping everything in the session window. Moves knowledge out of context and into infrastructure.

@jheeanny
Copy link

ignoreTLS support in web‑fetch is needed for internal HTTPS services with self‑signed certs. We use a local filemanager with self‑signed SSL and had to disable verification globally. Does this flag apply only to fetch, or also to browser automation?

@DarlingtonDeveloper
Copy link
Contributor Author

DarlingtonDeveloper commented Feb 14, 2026

Re: duplicate of #12584 — I don't think these are duplicates. Different hook systems.

#12584 wires message events through the plugin hook runner (typedHooks/createHookRunner). This PR bridges them to the internal workspace hook system via triggerInternalHook/createInternalHookEvent.

Plugin hooks and workspace hooks (HOOK.md-based) are separate dispatch paths. A workspace hook registered for message:received won't fire from #12584's changes — it needs the triggerInternalHook bridge that this PR adds. This is exactly the gap described in #7067.

The two PRs are complementary, not competing. Happy to coordinate if there's a preferred approach to unifying them.

Re: tests and docs, that's a fair point, will add those.

@openclaw-barnacle openclaw-barnacle bot added docs Improvements or additions to documentation agents Agent runtime and tooling size: M labels Feb 15, 2026
@steipete steipete closed this Feb 16, 2026
@steipete steipete reopened this Feb 17, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

"received",
params.sessionKey || params.sessionId || "",
{
from: params.messageTo ?? "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from field uses messageTo which is the delivery target, not the sender. Should use sender info params like senderId/senderName/senderE164 or create a from field from available sender params.

Compare with dispatch-from-config.ts:204 which correctly uses ctx.From.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 847:847

Comment:
`from` field uses `messageTo` which is the delivery target, not the sender. Should use sender info params like `senderId`/`senderName`/`senderE164` or create a `from` field from available sender params.

Compare with dispatch-from-config.ts:204 which correctly uses `ctx.From`.

How can I resolve this? If you propose a fix, please make it concise.

DarlingtonDeveloper and others added 3 commits February 24, 2026 01:45
Add message:received, message:before, and message:sent internal hook
events so workspace hooks (HOOK.md) can react to individual messages
without being a full plugin. Extends triggerInternalHook to collect
and merge handler results (prependContext concatenated, systemPrompt
last-wins).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add tests for message:received, message:before, and message:sent hook
events covering event shape, result merging, and no-op behavior.
Document the three new message lifecycle events in hooks docs (EN + zh-CN)
with context field tables, return value semantics, and example handlers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…fields

Address review feedback:
- Wire messageBeforeResult.systemPrompt through applySystemPromptOverrideToSession
- Use params.senderId/senderName/messageTo in message:received context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@DarlingtonDeveloper DarlingtonDeveloper force-pushed the feat/message-lifecycle-hooks branch from 1a6da0d to 1db1c82 Compare February 24, 2026 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling docs Improvements or additions to documentation size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Bridge message_received to internal hooks system (workspace hooks)

4 participants