feat(plugin): OpenClaw conversation turn hooks for memory capture#27
feat(plugin): OpenClaw conversation turn hooks for memory capture#27ehfazrezwan wants to merge 14 commits intodevfrom
Conversation
…fest Define the interface all NeuralScape extensions must implement: - ExtensionManifest (Pydantic model with name, version, description, hooks) - NeuralscapeExtension (runtime-checkable Protocol with startup, shutdown, on_event, and get_routes methods)
Define canonical event types (conversation_turn, session_start, session_end, memory_stored, compile_requested) as an enum with corresponding Pydantic payload models for type-safe event dispatch.
ExtensionRegistry handles: - Auto-discovery from extensions/ subdirectories - Explicit registration via NEURALSCAPE_EXTENSIONS env var - Startup/shutdown lifecycle with graceful failure handling - Route mounting at /v1/extensions/<name>/ - Event broadcasting to extensions matching manifest.hooks
Integrate ExtensionRegistry into main.py: - Discover and start extensions during lifespan startup - Mount extension routes onto the app - Shutdown extensions during lifespan teardown - Add GET /v1/extensions endpoint to list registered extensions - Add POST /v1/extensions/events endpoint for external event posting
Cover the extension protocol, step-by-step creation guide, lifecycle (discovery → startup → events → shutdown), route mounting, event listening, configuration via env vars, and a skeleton example.
30 tests covering registration, lifecycle (startup/shutdown), event dispatch, route mounting, discovery (local + env var), graceful failure handling, /v1/extensions listing endpoint, event schemas, and protocol compliance.
Add isHeartbeat(), isNoReply(), and isSystemMessage() helpers for filtering trivial conversation turns in OpenClaw integration.
Captures completed conversation turns and sends them to NeuralScape's conversation-compiler flush endpoint. Accepts both direct invocation and OpenClaw InternalHookEvent formats via stdin. Filters heartbeats, NO_REPLY responses, system messages, and short trivial exchanges.
Triggers NeuralScape's conversation-compiler compile endpoint on session end to consolidate captured turns into durable memories. Skips sessions with fewer than 2 messages.
Add OpenClaw hook manifest for message:sent and session:end events. Update esbuild entry points to include conversation-turn and session-summary scripts.
Add README covering Claude Code and OpenClaw hook setup, environment variables, managed hook installation, stdin payload formats, filtering behavior, and verification steps.
There was a problem hiding this comment.
Pull request overview
Adds OpenClaw hook support to the neuralscape-plugin so completed conversation turns can be flushed to NeuralScape’s conversation-compiler, and an end-of-session hook can trigger compilation into durable memories.
Changes:
- Add
conversation-turnandsession-summaryhook handlers that POST toconversation-compiler/flushandconversation-compiler/compile. - Add message filtering helpers (
isHeartbeat,isNoReply,isSystemMessage) to reduce noisy captures. - Add OpenClaw hooks manifest and README documentation for setup + stdin payload formats.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| neuralscape-plugin/src/utils.ts | Adds filtering helper functions for heartbeat / no-reply / system messages. |
| neuralscape-plugin/src/conversation-turn.ts | New hook handler to normalize/filter turns and flush them to NeuralScape. |
| neuralscape-plugin/src/session-summary.ts | New hook handler to trigger end-of-session compilation by date/user. |
| neuralscape-plugin/hooks/openclaw-hooks.json | Declares OpenClaw message:sent and session:end command hooks. |
| neuralscape-plugin/esbuild.config.js | Bundles new hook entrypoints into scripts/. |
| neuralscape-plugin/README.md | Documents OpenClaw + Claude Code integration, payloads, and verification steps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (isHeartbeat(userMessage)) return true; | ||
| if (isNoReply(assistantResponse)) return true; | ||
| if (isSystemMessage(userMessage)) return true; | ||
| if (assistantResponse.length < MIN_RESPONSE_LENGTH) return true; |
There was a problem hiding this comment.
assistantResponse.length is used for the short-response filter, so a response with lots of whitespace can bypass the minimum-length check and be flushed even if the meaningful content is very short. Consider using assistantResponse.trim().length (and/or normalizing earlier) so the filter matches the intent of “responses under N characters”.
| if (isHeartbeat(userMessage)) return true; | |
| if (isNoReply(assistantResponse)) return true; | |
| if (isSystemMessage(userMessage)) return true; | |
| if (assistantResponse.length < MIN_RESPONSE_LENGTH) return true; | |
| const trimmedAssistantResponse = assistantResponse.trim(); | |
| if (isHeartbeat(userMessage)) return true; | |
| if (isNoReply(assistantResponse)) return true; | |
| if (isSystemMessage(userMessage)) return true; | |
| if (trimmedAssistantResponse.length < MIN_RESPONSE_LENGTH) return true; |
| const date = | ||
| raw.date || | ||
| (raw.timestamp | ||
| ? new Date(raw.timestamp).toISOString().split("T")[0] |
There was a problem hiding this comment.
new Date(raw.timestamp).toISOString() will throw a RangeError if raw.timestamp is present but not a valid date string. Guard against invalid timestamps (e.g., check isNaN(date.getTime()) / Number.isNaN(Date.parse(...))) and fall back to new Date() or the provided date instead.
| const date = | |
| raw.date || | |
| (raw.timestamp | |
| ? new Date(raw.timestamp).toISOString().split("T")[0] | |
| const timestampDate = raw.timestamp ? new Date(raw.timestamp) : null; | |
| const date = | |
| raw.date || | |
| (timestampDate && !Number.isNaN(timestampDate.getTime()) | |
| ? timestampDate.toISOString().split("T")[0] |
| function shouldSkipTurn(userMessage: string, assistantResponse: string): boolean { | ||
| if (isHeartbeat(userMessage)) return true; | ||
| if (isNoReply(assistantResponse)) return true; | ||
| if (isSystemMessage(userMessage)) return true; |
There was a problem hiding this comment.
isSystemMessage() is only applied to userMessage, but for a message:sent hook the system-like markers (e.g. [system], [internal]) are likely to appear in the assistant payload (assistantResponse / context.content). Consider checking assistantResponse as well (or instead) so system-prefixed assistant messages are actually filtered out.
| if (isSystemMessage(userMessage)) return true; | |
| if (isSystemMessage(userMessage)) return true; | |
| if (isSystemMessage(assistantResponse)) return true; |
…istant for system messages Trim whitespace before checking MIN_RESPONSE_LENGTH so padded responses are correctly filtered. Also skip turns where the assistant response is itself a system message.
Parse the timestamp into a Date first and check for NaN before calling toISOString(), falling back to current date on invalid input.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Summary
conversation-turnhook that captures completed OpenClaw conversation turns and sends them to NeuralScape'sconversation-compiler/flushendpoint for automatic memory extractionsession-summaryhook that triggers end-of-session compilation viaconversation-compiler/compileisHeartbeat(),isNoReply(), andisSystemMessage()filtering helpers to skip trivial exchanges (heartbeat polls, NO_REPLY responses, system messages, short responses)openclaw-hooks.jsonmanifest formessage:sentandsession:endeventsTest plan
cd neuralscape-plugin && npm run build