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 emotionemotional_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):
| Extractor | What it measures | How |
|---|---|---|
terseness | Frustration / time pressure | Avg word count trending down, short bursts |
timing | Energy level / session type | Hour of day, gap between messages, burst patterns |
punctuation | Emotional intensity | Exclamation marks, ellipses, ALL CAPS, emoji density |
keywords | Explicit emotional markers | Expletives, "ugh", "nice", "please", hedging language |
correction | Perfectionism / irritation | Edit patterns, "actually", "I meant", "no, ..." |
cadence | Engagement level | Messages per minute, response delay to assistant |
question-density | Curiosity vs directive mode | Ratio 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
| Tool | Input | Output | Description |
|---|---|---|---|
analyze_messages | {messages: Message[]} | SentimentState | Stateless — analyze arbitrary messages |
push_message | {content, role, timestamp} | SentimentState | Add to window, return updated state |
get_state | {session_id?} | SentimentState | Current window state |
get_strategy | {session_id?} | ResponseStrategy | Current recommended strategy |
reset_window | {session_id?} | void | Clear the sliding window |
register_extractor | {name, pattern, ...} | void | Add pattern-based extractor at runtime |
Resources
| URI | Description |
|---|---|
sentiment://state | Current SentimentState (subscribable) |
sentiment://strategy | Current ResponseStrategy |
sentiment://signals | Raw signal breakdown |
sentiment://config | Active 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:
- HEARTBEAT.md / AGENTS.md instruction: "Before responding, check sentiment state"
- MCP tool call: Agent calls
get_strategybefore each response - 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)
- Agent instructions tell me to be sentiment-aware (already in dreams)
- MCP server handles the heavy lifting when I explicitly call it
- 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-sentimentto 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)
/moodcommand for transparency ("Here's what I think your state is")- Threshold tuning based on real interactions
- New extractors as patterns emerge
Tech Stack
| Component | Language | Runtime | Why |
|---|---|---|---|
| Core Engine | TypeScript | Node.js | Matches OpenClaw ecosystem, npm publishable |
| MCP Server | TypeScript | Node.js | MCP TypeScript SDK is most mature |
| Tests | vitest | Node.js | Fast, good DX |
| Deployment | Docker + the cluster | Linux | Existing 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
/moodcommand 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
- Naming:
mcp-sentimentor something more specific?mcp-conversational-state?mcp-vibes? - Scope: Start with just my own use (OpenClaw → me), or design for multi-agent from day one?
- Threshold tuning: Should I propose initial thresholds and you calibrate, or should there be an explicit feedback mechanism ("that read was wrong")?
- Public or private repo? This could be useful to the MCP community. Your call.
- 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.
Related Dreams
- Sentiment Archaeology — The original dream
- Working Memory Protocol — How sentiment integrates via SCRATCHPAD.md
- Cognitive Architecture — The broader vision this enables
- Sentiment → OpenClaw Integration — Concrete wiring plan
"The most important thing in communication is hearing what isn't said." — Drucker
Ready to build when you are.