Sentiment Engine → OpenClaw Integration Plan

Wiring the sentiment engine into OpenClaw

Concrete wiring, not more architecture diagrams.


Summary

Hook the existing @stev3/sentiment-engine (90 tests, ~/code/sentiment-engine/) into OpenClaw using two integration points:

  1. Assembler script — generates SCRATCHPAD.md from recent messages
  2. Bootstrap hook — injects SCRATCHPAD.md into project context every turn

No MCP server needed for MVP. The engine runs directly as a library import.


Integration Architecture

┌─────────────────────────────────────────────────┐
│  Cron job (every 10 min, active hours)          │
│  OR heartbeat task                              │
│                                                 │
│  1. sessions_history → recent messages          │
│  2. @stev3/sentiment-engine → state + strategy  │
│  3. Write SCRATCHPAD.md to workspace            │
└──────────────────┬──────────────────────────────┘
                   │ (file on disk)
┌─────────────────────────────────────────────────┐
│  workspace/SCRATCHPAD.md                        │
│  (~200 tokens, auto-generated)                  │
└──────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────┐
│  Bootstrap hook: sentiment-inject               │
│  Event: agent:bootstrap                         │
│  Action: reads SCRATCHPAD.md, injects into      │
│          bootstrapFiles array                   │
└──────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────┐
│  Main session sees SCRATCHPAD.md as injected    │
│  project context (like SOUL.md, TOOLS.md)       │
└─────────────────────────────────────────────────┘

Component 1: Assembler Script

Location: scripts/assemble-scratchpad.ts

What it does:

  1. Reads recent messages from main session via sessions_history
    • OR: accepts messages as JSON on stdin
    • OR: runs as an isolated cron agentTurn that calls sessions_history itself
  2. Converts to sentiment-engine Message[] format
  3. Calls engine.analyze(messages)SentimentState + ResponseStrategy
  4. Formats SCRATCHPAD.md (~200 tokens)
  5. Writes to workspace

Key decision: How to get messages?

ApproachProsCons
A: Cron agentTurnIsolated, can use sessions_history toolBurns LLM tokens per run, can't import TS lib directly
B: Shell script via execDirect, cheap, no LLM tokensNeeds Node.js, can't call sessions_history API
C: Heartbeat taskHas conversation context30 min staleness, adds to main session tokens
D: Gateway HTTP APICan hit sessions endpointNeeds auth token, more complex

Recommended: Approach B (shell script) + D (HTTP API for history)

The gateway exposes openai-http-api and openresponses-http-api. We can:

  1. Hit the gateway's session history endpoint to get recent messages
  2. Pipe them through the sentiment engine (pure Node.js)
  3. Write SCRATCHPAD.md
  4. Trigger via cron exec or a simple systemd timer

This avoids burning LLM tokens entirely. The sentiment engine is pure heuristics — no model needed.

Alternative: Approach C (heartbeat) for MVP

Simplest path: just add "run the assembler" as a heartbeat task. The main session already has message context. The LLM can call exec to run the assembler script with piped message data.


Component 2: Bootstrap Hook

Location: ~/.openclaw/workspace/hooks/sentiment-inject/

hooks/sentiment-inject/
├── HOOK.md
└── handler.ts

HOOK.md:

---
name: sentiment-inject
description: "Injects SCRATCHPAD.md (sentiment context) into bootstrap files"
metadata:
  openclaw:
    emoji: "🎭"
    events: ["agent:bootstrap"]
---

# Sentiment Inject Hook

Reads SCRATCHPAD.md from workspace and injects it into the bootstrap
files array so the agent sees emotional context as project context.

Only injects if SCRATCHPAD.md exists and is <30 min old.

handler.ts:

import type { HookHandler } from '../../src/hooks/hooks.js';
import fs from 'node:fs/promises';
import path from 'node:path';

const SCRATCHPAD_FILE = 'SCRATCHPAD.md';
const MAX_AGE_MS = 30 * 60 * 1000; // 30 minutes

const handler: HookHandler = async (event) => {
  if (event.type !== 'agent' || event.action !== 'bootstrap') return;

  const workspaceDir = event.context.workspaceDir;
  if (!workspaceDir || !Array.isArray(event.context.bootstrapFiles)) return;

  const scratchpadPath = path.join(workspaceDir, SCRATCHPAD_FILE);

  try {
    const stat = await fs.stat(scratchpadPath);
    const ageMs = Date.now() - stat.mtimeMs;

    // Skip stale scratchpads
    if (ageMs > MAX_AGE_MS) return;

    const content = await fs.readFile(scratchpadPath, 'utf-8');
    if (!content.trim()) return;

    // Inject into bootstrap files
    event.context.bootstrapFiles.push({
      name: SCRATCHPAD_FILE,
      path: scratchpadPath,
      content,
      missing: false,
    });
  } catch {
    // SCRATCHPAD.md doesn't exist yet — that's fine
  }
};

export default handler;

Component 3: SCRATCHPAD.md Format

# SCRATCHPAD.md — Working Memory (auto-generated)
<!-- Updated: 2026-02-06T21:00:00+11:00 | TTL: 30m -->

## Mood
- **Primary:** playful (0.52) | **Secondary:** engaged (0.48)
- **Valence:** +0.3 | **Arousal:** 0.5 | **Confidence:** 0.74

## Strategy
- **Tone:** playful — match wit and energy
- **Verbosity:** moderate
- **Preamble:** no
- **Notes:** Humor welcome, stay helpful, keep it sharp

~100-150 tokens. Cheap.


Implementation Steps

Step 1: Build the assembler script

  • scripts/assemble-scratchpad.ts
  • Import @stev3/sentiment-engine via npm link
  • Accept messages as JSON on stdin
  • Output SCRATCHPAD.md to workspace
  • Test with fixture data from sentiment-engine

Step 2: Create the bootstrap hook

  • hooks/sentiment-inject/HOOK.md + handler.ts
  • Enable via openclaw hooks enable sentiment-inject
  • Test: manually write a SCRATCHPAD.md, restart gateway, verify it appears in context

Step 3: Wire up the trigger

  • MVP: Heartbeat reads recent messages, pipes to assembler via exec
  • v2: Cron shell script hits gateway HTTP API for history, pipes to assembler

Step 4: Test end-to-end

  • Verify SCRATCHPAD.md appears in system prompt
  • Verify stale scratchpads are ignored (>30 min)
  • Verify sentiment adjusts my behavior
  • Calibrate thresholds with real conversations

Open Questions

  1. Hook import path: The handler.ts needs to import HookHandler type. Workspace hooks — what's the expected import path? Need to check if workspace hooks are transpiled or loaded differently.

  2. Message format: sessions_history returns messages in OpenClaw format. Need a mapper to @stev3/sentiment-engine Message[] format.

  3. Gateway HTTP API: Does the local gateway expose session history via HTTP? Need to check openresponses-http-api.md.



Created: 2026-02-06 21:30 Status: Ready to build