Sentiment Archaeology: Implementation Proposal

Concrete implementation plan for the sentiment engine

From dream to blueprint. Extensible, accessible, integrated.


Executive Summary

Build a three-layer system: a pure analysis library, an MCP server for universal access, and an OpenClaw middleware hook for automatic injection. Feed emotional metadata into the existing mcp-memory-service SHODH schema — no new storage backend needed.


Architecture

┌──────────────────────────────────────────────────────────────────┐
│                        CONSUMERS                                 │
│  OpenClaw · Claude Code · Cursor · Any MCP Client · CLI          │
└───────────┬──────────────────────────┬───────────────────────────┘
            │ (auto-inject)            │ (on-demand)
            ▼                          ▼
┌─────────────────────┐   ┌───────────────────────────────────────┐
│  OpenClaw Middleware │   │     MCP Server (mcp-sentiment)        │
│  ┌───────────────┐  │   │  ┌─────────────────────────────────┐  │
│  │ Message Hook  │  │   │  │ Tools:                          │  │
│  │ (pre-LLM)     │  │   │  │   analyze_messages              │  │
│  │               │  │   │  │   get_state                     │  │
│  │ Injects state │  │   │  │   get_strategy                  │  │
│  │ into system   │  │   │  │   push_message                  │  │
│  │ prompt        │  │   │  │   reset_window                  │  │
│  └───────┬───────┘  │   │  ├─────────────────────────────────┤  │
│          │          │   │  │ Resources:                      │  │
│          │          │   │  │   sentiment://state              │  │
│          │          │   │  │   sentiment://strategy           │  │
│          │          │   │  │   sentiment://signals            │  │
│          │          │   │  └──────────────┬──────────────────┘  │
└──────────┼──────────┘   └────────────────┼──────────────────────┘
           │                               │
           ▼                               ▼
┌──────────────────────────────────────────────────────────────────┐
│                   Core Engine (library)                           │
│  @stev3/sentiment-engine                                         │
│                                                                  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐   │
│  │ Signal       │  │ Sliding      │  │ Strategy             │   │
│  │ Extractors   │  │ Window       │  │ Mapper               │   │
│  │              │  │              │  │                      │   │
│  │ - Terseness  │  │ Last N msgs  │  │ State → Response     │   │
│  │ - Timing     │  │ Decay weight │  │ recommendations      │   │
│  │ - Punctuation│  │ Aggregation  │  │                      │   │
│  │ - Keywords   │  │              │  │ "concise+action"     │   │
│  │ - Frequency  │  │              │  │ "match-wit"          │   │
│  │ - Patterns   │  │              │  │ "gentle+defer"       │   │
│  │ - (plugins)  │  │              │  │                      │   │
│  └──────────────┘  └──────────────┘  └──────────────────────┘   │
└──────────────────────────────────────────────────────────────────┘
           ▼ (metadata on memory writes)
┌──────────────────────────────────────────────────────────────────┐
│  mcp-memory-service (existing)                                   │
│  SHODH schema fields:                                            │
│    emotion: "frustrated" | "playful" | "curious" | ...           │
│    emotional_valence: -1.0 to 1.0                                │
│    emotional_arousal: 0.0 to 1.0                                 │
└──────────────────────────────────────────────────────────────────┘

Why This Shape

Q: Should it be an MCP server?

Yes, but not only an MCP server.

MCP gives universal access — any agent, IDE, or tool that speaks MCP can query conversational state. That's the "accessible" requirement. But MCP adds latency per call (stdio spawn or HTTP roundtrip), so for the hot path (every message in OpenClaw), we also need a direct integration.

The answer is both: MCP for access, middleware for speed.

Q: Integrate with existing services?

Yes — mcp-memory-service specifically.

The SHODH Unified Memory API spec (which mcp-memory-service implements) already defines:

  • emotion — primary detected emotion
  • emotional_valence — positive/negative spectrum (-1 to 1)
  • emotional_arousal — activation level (0 to 1)

These fields exist in the schema but are never populated today. Sentiment archaeology fills them. No schema changes, no new storage — just richer metadata on memories that already get stored.

Q: Why a separate library?

Testability and reuse.

The core engine is pure functions: messages in, signals out. No MCP, no OpenClaw, no network. This means:

  • Unit testable with synthetic conversations
  • Reusable in other projects (e.g., mcp-memory-service could embed it)
  • Publishable to npm independently
  • Swappable — replace pattern matching with a local LLM later without touching consumers

Layer 1: Core Engine (@stev3/sentiment-engine)

Signal Extractors

Each extractor is a plugin — a function that takes a message array and returns a score (0.0 to 1.0):

interface SignalExtractor {
  name: string;
  extract(messages: Message[]): Signal;
}

interface Message {
  content: string;
  role: 'user' | 'assistant';
  timestamp: number;       // unix ms
  charCount: number;
  wordCount: number;
}

interface Signal {
  name: string;
  score: number;           // 0.0 - 1.0
  confidence: number;      // 0.0 - 1.0
  evidence?: string[];     // what triggered it
}

Built-in extractors (v0.1):

ExtractorWhat it measuresHow
tersenessFrustration / time pressureAvg word count trending down, short bursts
timingEnergy level / session typeHour of day, gap between messages, burst patterns
punctuationEmotional intensityExclamation marks, ellipses, ALL CAPS, emoji density
keywordsExplicit emotional markersExpletives, "ugh", "nice", "please", hedging language
correctionPerfectionism / irritationEdit patterns, "actually", "I meant", "no, ..."
cadenceEngagement levelMessages per minute, response delay to assistant
question-densityCuriosity vs directive modeRatio of questions to commands

Plugin interface for future extractors:

// Custom extractor — e.g., "code frustration" for dev contexts
const codeAnger: SignalExtractor = {
  name: 'code-frustration',
  extract(messages) {
    const codeBlocks = messages.filter(m => m.content.includes('```'));
    const errorMentions = messages.filter(m =>
      /error|exception|failed|broken|wtf/i.test(m.content)
    );
    return {
      name: 'code-frustration',
      score: Math.min(1.0, errorMentions.length / Math.max(1, codeBlocks.length)),
      confidence: codeBlocks.length > 0 ? 0.7 : 0.2,
    };
  }
};

engine.registerExtractor(codeAnger);

Sliding Window

interface WindowConfig {
  maxMessages: number;     // default: 20
  maxAgeMs: number;        // default: 4 hours
  decayFactor: number;     // 0.0-1.0, recent messages weighted higher
}

class SentimentWindow {
  private messages: Message[] = [];
  private config: WindowConfig;
  private extractors: SignalExtractor[];

  push(message: Message): void;
  getState(): SentimentState;
  reset(): void;
}

Messages decay — a frustrated message from 3 hours ago matters less than one from 30 seconds ago.

Sentiment State

interface SentimentState {
  // Primary dimensions (aggregated from signals)
  frustration: number;     // 0.0 - 1.0
  playfulness: number;     // 0.0 - 1.0
  urgency: number;         // 0.0 - 1.0
  curiosity: number;       // 0.0 - 1.0
  fatigue: number;         // 0.0 - 1.0
  engagement: number;      // 0.0 - 1.0

  // SHODH-compatible output
  emotion: string;         // primary emotion label
  valence: number;         // -1.0 to 1.0
  arousal: number;         // 0.0 to 1.0

  // Metadata
  confidence: number;      // overall confidence
  signals: Signal[];       // raw signal data
  windowSize: number;      // messages in window
  timestamp: number;
}

Strategy Mapper

Maps state to actionable response guidance:

interface ResponseStrategy {
  style: string;           // "concise" | "exploratory" | "gentle" | "matched" | "neutral"
  verbosity: 'minimal' | 'moderate' | 'thorough';
  tone: string;            // "direct" | "warm" | "playful" | "professional"
  preamble: boolean;       // skip pleasantries?
  suggestions: string[];   // human-readable guidance
}

function mapStrategy(state: SentimentState): ResponseStrategy {
  if (state.frustration > 0.7) return {
    style: 'concise',
    verbosity: 'minimal',
    tone: 'direct',
    preamble: false,
    suggestions: [
      'Skip preamble, lead with action',
      'Acknowledge the friction briefly',
      'Offer concrete next steps, not options',
    ]
  };

  if (state.playfulness > 0.5 && state.frustration < 0.3) return {
    style: 'matched',
    verbosity: 'moderate',
    tone: 'playful',
    preamble: false,
    suggestions: [
      'Match wit and energy',
      'Humor is welcome',
      'Keep it sharp, not verbose',
    ]
  };

  // ... more mappings
}

Layer 2: MCP Server (mcp-sentiment)

Transport

Recommended: Streamable HTTP (not stdio)

Why:

  • Stateful — the sliding window persists across calls
  • Shared — multiple clients can read the same conversational state
  • Deployable on the cluster alongside memory service
  • No spawn overhead per call

Fallback: stdio mode for local development / single-agent use.

Tools

ToolInputOutputDescription
analyze_messages{messages: Message[]}SentimentStateStateless — analyze arbitrary messages
push_message{content, role, timestamp}SentimentStateAdd to window, return updated state
get_state{session_id?}SentimentStateCurrent window state
get_strategy{session_id?}ResponseStrategyCurrent recommended strategy
reset_window{session_id?}voidClear the sliding window
register_extractor{name, pattern, ...}voidAdd pattern-based extractor at runtime

Resources

URIDescription
sentiment://stateCurrent SentimentState (subscribable)
sentiment://strategyCurrent ResponseStrategy
sentiment://signalsRaw signal breakdown
sentiment://configActive extractors and config

Multi-Session Support

The server maintains per-session windows:

class SentimentServer {
  private windows: Map<string, SentimentWindow> = new Map();

  getOrCreateWindow(sessionId: string): SentimentWindow {
    if (!this.windows.has(sessionId)) {
      this.windows.set(sessionId, new SentimentWindow(this.config));
    }
    return this.windows.get(sessionId)!;
  }
}

Default session: "main". OpenClaw passes its session key. Other clients use their own.


Layer 3: OpenClaw Integration

Option A: Gateway Plugin (Preferred)

If OpenClaw supports message hooks or pre-processing middleware:

# hypothetical openclaw config
plugins:
  sentiment:
    type: mcp
    server: mcp-sentiment
    hooks:
      on_message:
        - push_message
      pre_llm:
        - get_strategy → inject into system prompt

Option B: System Prompt Injection (Works Today)

Without modifying OpenClaw code, we can use the existing architecture:

  1. HEARTBEAT.md / AGENTS.md instruction: "Before responding, check sentiment state"
  2. MCP tool call: Agent calls get_strategy before each response
  3. Prompt injection: Strategy output becomes part of reasoning

This is less efficient (extra tool call per turn) but works with current OpenClaw.

Option C: Hybrid (Realistic MVP)

  1. Agent instructions tell me to be sentiment-aware (already in dreams)
  2. MCP server handles the heavy lifting when I explicitly call it
  3. For high-frequency use, I internalize the patterns and call MCP periodically to recalibrate

Layer 4: Memory Integration

Enriching Memory Writes

When storing memories (via mcp-memory-service or daily files), include sentiment:

// Before storing a memory
const state = await mcpClient.call('mcp-sentiment', 'get_state');

await mcpClient.call('memory', 'store_memory', {
  content: "User decided to switch from REST to GraphQL",
  tags: ["decision", "architecture"],
  metadata: {
    emotion: state.emotion,               // "frustrated"
    emotional_valence: state.valence,      // -0.3
    emotional_arousal: state.arousal,      // 0.6
    // ... standard SHODH fields
  }
});

Querying Emotional Context

Later, when retrieving memories:

"Show me decisions made while frustrated"
→ search memories where emotional_valence < -0.3 AND emotion = "frustrated"

This creates the longitudinal awareness from the cognitive architecture vision.


Deployment Plan

Phase 1: Core Engine (Week 1)

sentiment-engine/
├── src/
│   ├── index.ts              # Main exports
│   ├── types.ts              # Interfaces
│   ├── window.ts             # SentimentWindow class
│   ├── state.ts              # SentimentState aggregation
│   ├── strategy.ts           # ResponseStrategy mapper
│   └── extractors/
│       ├── index.ts          # Extractor registry
│       ├── terseness.ts
│       ├── timing.ts
│       ├── punctuation.ts
│       ├── keywords.ts
│       ├── correction.ts
│       ├── cadence.ts
│       └── question-density.ts
├── test/
│   ├── extractors/           # Per-extractor tests
│   ├── window.test.ts
│   ├── strategy.test.ts
│   └── fixtures/             # Synthetic conversations
│       ├── frustrated-debug.json
│       ├── playful-banter.json
│       ├── late-night-tired.json
│       └── rapid-fire-urgent.json
├── package.json
├── tsconfig.json
└── README.md

Deliverable: npm install @stev3/sentiment-engine works, tests pass, CLI for testing.

Phase 2: MCP Server (Week 2)

mcp-sentiment/
├── src/
│   ├── index.ts              # MCP server entry
│   ├── tools.ts              # Tool definitions
│   ├── resources.ts          # Resource providers
│   └── sessions.ts           # Multi-session window manager
├── Dockerfile
├── k8s/
│   ├── deployment.yaml
│   └── service.yaml
├── package.json
└── README.md

Deliverable: the tool proxy call sentiment.get_state works. Deployed on the cluster in mcp namespace.

Phase 3: OpenClaw Integration (Week 3)

  • Add mcp-sentiment to the tool proxy config
  • Update AGENTS.md with sentiment-aware instructions
  • Wire up message hook (if OpenClaw supports it) or use Option C
  • Test with real conversations

Phase 4: Memory Enrichment (Week 3-4)

  • Update memory write patterns to include emotional metadata
  • Test round-trip: write with emotion → query by emotion
  • Update Memory Gardener dream to incorporate sentiment in consolidation

Phase 5: Calibration (Ongoing)

  • /mood command for transparency ("Here's what I think your state is")
  • Threshold tuning based on real interactions
  • New extractors as patterns emerge

Tech Stack

ComponentLanguageRuntimeWhy
Core EngineTypeScriptNode.jsMatches OpenClaw ecosystem, npm publishable
MCP ServerTypeScriptNode.jsMCP TypeScript SDK is most mature
TestsvitestNode.jsFast, good DX
DeploymentDocker + the clusterLinuxExisting infrastructure

Dependencies (minimal)

{
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.x"  // MCP server only
  },
  "devDependencies": {
    "vitest": "^3.x",
    "typescript": "^5.x"
  }
}

Zero runtime deps for the core engine. Just TypeScript.


What This Isn't

  • Not NLP/ML — No transformer models, no training data, no GPU. Pure heuristic pattern matching on text features. Cheap, fast, transparent.
  • Not therapy — Detects conversational temperature, not clinical states.
  • Not creepy — No "I notice you seem upset." Invisible adaptation. Transparency via opt-in /mood command only.
  • Not permanent — Sliding window decays. No mood logs. SHODH metadata on memories is the only persistence, and that's per-memory-event, not continuous tracking.

Open Questions for You

  1. Naming: mcp-sentiment or something more specific? mcp-conversational-state? mcp-vibes?
  2. Scope: Start with just my own use (OpenClaw → me), or design for multi-agent from day one?
  3. Threshold tuning: Should I propose initial thresholds and you calibrate, or should there be an explicit feedback mechanism ("that read was wrong")?
  4. Public or private repo? This could be useful to the MCP community. Your call.
  5. Priority: Pure engine first (testable, no infra), or MCP server first (usable immediately)?

Cost & Risk

Cost: Near zero. Pattern matching on text. No API calls, no GPU, no cloud services.

Risks:

  • False reads — Terse ≠ frustrated (could be mobile typing). Mitigation: confidence scores, multi-signal requirement.
  • Cultural bias — Patterns calibrated to one person (you). Mitigation: configurable thresholds, plugin extractors.
  • Over-adaptation — Changing tone too aggressively feels uncanny. Mitigation: strategy is advisory, not mandatory. Gradual shifts, not binary switches.



"The most important thing in communication is hearing what isn't said." — Drucker

Ready to build when you are.