Working Memory & Integration Protocol

How subsystems connect at runtime via SCRATCHPAD.md

How the subsystems actually connect at runtime.

The Problem

We have pieces:

  • Sentiment engine — standalone TS library, 90 tests, extracts mood + maps strategy
  • Memory search — Gemini embeddings on workspace MD, MCP/the vector store for semantic
  • Model router — documented skill, not yet automated
  • HEARTBEAT.md — manual checklist

But no wiring. The cognitive architecture shows arrows between boxes. This document describes what flows through those arrows, and how OpenClaw — which currently has no middleware layer — can host these subsystems without requiring core changes.

Constraint: OpenClaw Is Not Extensible (Yet)

OpenClaw doesn't have a plugin hook for "run this before each response." We can't inject middleware. What we can do:

  1. System prompt injection — SOUL.md, AGENTS.md, and workspace files are loaded each session
  2. Cron/heartbeat preprocessing — tasks that run on schedule and write context files
  3. Skills — tools + instructions loaded when relevant
  4. Session state files — JSON/MD files in workspace that persist between turns

This means integration must be file-mediated. Subsystems write to files; the main session reads those files via injected context.

Architecture: The Scratchpad Pattern

┌──────────────────────────────────────────────────────┐
│              PRE-TURN PIPELINE                       │
│  (runs as cron/heartbeat or session-start script)    │
│                                                      │
│  1. Read recent messages (from session transcript)   │
│  2. Run sentiment-engine → SentimentState            │
│  3. Run memory search → relevant memories            │
│  4. Assemble → write to SCRATCHPAD.md                │
└──────────────────────────────────────────────────────┘
         ▼ (file written to workspace)
┌──────────────────────────────────────────────────────┐
│  workspace/SCRATCHPAD.md                             │
│  ┌────────────────────────────────────────────────┐  │
│  │ ## Current State (auto-generated, do not edit) │  │
│  │ **Mood:** frustrated (0.71) — concise mode     │  │
│  │ **Strategy:** direct, no preamble, action-first│  │
│  │ **Active context:** debugging k8s deployment   │  │
│  │ **Time context:** late night, suggest deferral │  │
│  │ **Relevant memories:** [3 items]               │  │
│  └────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────┘
         ▼ (loaded as project context)
┌──────────────────────────────────────────────────────┐
│  Main session reads SCRATCHPAD.md at turn start      │
│  → adjusts tone, depth, approach accordingly         │
└──────────────────────────────────────────────────────┘

Why a File?

  • OpenClaw injects workspace files into project context automatically
  • No core changes needed — just add SCRATCHPAD.md to the workspace
  • Any subsystem can write to it; the LLM reads it naturally
  • Debugging is trivial — cat SCRATCHPAD.md
  • If the scratchpad is stale (>30 min), ignore it — the LLM can tell

Why Not Just Embed Strategy in SOUL.md?

SOUL.md is static. The whole point of sentiment analysis is dynamic adjustment. "Be concise when I'm frustrated" is a rule; actually detecting frustration in real-time and surfacing it as context is a system.

SCRATCHPAD.md Schema

# SCRATCHPAD.md — Working Memory (auto-generated)
<!-- Updated: 2026-02-06T03:00:00+11:00 -->
<!-- TTL: 30 minutes — ignore if older -->

## Emotional Context
- **Primary:** frustrated (0.71)
- **Secondary:** urgent (0.55)
- **Valence:** -0.4 (negative) | **Arousal:** 0.7 (high)
- **Confidence:** 0.82

## Response Strategy
- **Style:** concise — lead with answer, skip preamble
- **Tone:** direct — no hedging, no "I think maybe"
- **Verbosity:** minimal — they'll ask for more if needed
- **Preamble:** NO

## Active Context
- Working on: k8s deployment debugging (mcp-memory-service)
- Last topic: CUDA container missing libcublas
- Session gap: 2h (short, no context resurrection needed)

## Relevant Memories
- Ray prefers a specific cluster config path (not sudo)
- mcp-memory-service v10.0 deployed on CPU tag (CUDA failed)
- Lab disk at ~87% (improved from 96%)

## Time Context
- Local time: 01:30 AEDT (late night)
- Note: Consider suggesting deferral for non-urgent work

The Assembler: scripts/assemble-scratchpad.ts

A lightweight script that:

  1. Accepts recent messages as input (JSON on stdin or file path)
  2. Runs @stev3/sentiment-engine to get state + strategy
  3. Queries MCP memory for relevant context (top 3-5 hits based on recent topics)
  4. Reads today's daily memory file for active context
  5. Writes SCRATCHPAD.md to workspace
// Pseudocode — not production

import { analyze } from '@stev3/sentiment-engine';
import { mapStrategy } from '@stev3/sentiment-engine';
import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';

interface AssemblerInput {
  messages: Message[];      // recent conversation
  utcOffsetHours: number;   // 11 for AEDT
}

function assembleScratchpad(input: AssemblerInput): string {
  // 1. Sentiment analysis
  const state = analyze(input.messages, {
    utcOffsetHours: input.utcOffsetHours,
  });
  const strategy = mapStrategy(state);

  // 2. Memory search (via the tool proxy or direct)
  const recentTopics = extractTopics(input.messages);
  const memories = searchMemories(recentTopics);

  // 3. Daily context
  const today = new Date().toISOString().slice(0, 10);
  const dailyFile = `memory/${today}.md`;
  const dailyContext = readFileSync(dailyFile, 'utf-8').slice(-2000); // last 2KB

  // 4. Time context
  const localHour = (new Date().getUTCHours() + input.utcOffsetHours) % 24;
  const timeNote = localHour >= 23 || localHour < 7
    ? 'Late night — suggest deferral for non-urgent work'
    : localHour < 10
    ? 'Morning — fresh start, good for complex work'
    : 'Active hours — standard engagement';

  // 5. Assemble
  return formatScratchpad({
    state,
    strategy,
    memories,
    dailyContext,
    timeNote,
    updatedAt: new Date().toISOString(),
  });
}

Trigger Modes

Mode 1: Heartbeat-Driven (MVP)

Every heartbeat (~30min), run the assembler with messages from the last session. Write SCRATCHPAD.md. The main session picks it up as injected context.

Pros: Zero infrastructure, works today Cons: 30-min staleness, heartbeat might not have recent messages

Mode 2: Cron Pre-Process

A cron job that runs every 10 minutes during active hours:

{
  "schedule": { "kind": "every", "everyMs": 600000 },
  "payload": {
    "kind": "agentTurn",
    "message": "Run scripts/assemble-scratchpad.ts with recent session messages. Write output to SCRATCHPAD.md.",
    "timeoutSeconds": 30
  },
  "sessionTarget": "isolated"
}

Pros: More current, isolated from main session Cons: Burns tokens every 10 min, can't access main session messages from isolated session

Mode 3: OpenClaw Event Hook (Future)

If OpenClaw adds pre-turn hooks or middleware:

# hypothetical openclaw config
hooks:
  preTurn:
    - script: scripts/assemble-scratchpad.ts
      input: $RECENT_MESSAGES
      timeout: 5s

Pros: Real-time, no staleness, minimal overhead Cons: Doesn't exist yet

Start with heartbeat. The 30-min staleness is acceptable because:

  • Emotional state changes slowly in most conversations
  • The LLM can detect obvious mood shifts without the scratchpad
  • The scratchpad provides background context, not real-time steering

When OpenClaw supports hooks, migrate to Mode 3 for real-time.

Integration with Other Subsystems

Memory Gardener → Scratchpad

Memory gardener consolidates daily files → MEMORY.md. The scratchpad assembler reads MEMORY.md for relevant memories. They're complementary:

Daily files → [Memory Gardener] → MEMORY.md → [Scratchpad Assembler] → SCRATCHPAD.md
                                                                     Main session reads

Model Router → Scratchpad

The scratchpad could include routing hints:

## Routing Hint
- Content sensitivity: LOW (technical discussion, no PII)
- Task complexity: MEDIUM (debugging, not architecture)
- Suggested model: standard (no need for local-only)

This is informational — the LLM can't actually switch its own model mid-turn. But it could influence tool choices (e.g., "use perplexity for research" vs "search locally first").

Wake Patterns → Scratchpad

Wake patterns (morning brief, deadline whisper) could write their output to SCRATCHPAD.md sections:

## Proactive Alerts
- 📅 Calendar: "Team standup" in 1h 45m
- 📧 Email: 2 unread from the consultancy (flagged important)
- ⚠️ Lab disk: 89% (rising — investigate)

What This Doesn't Solve

  1. Real-time sentiment — The scratchpad is a snapshot. Within a conversation, the LLM still needs to read the room itself. The scratchpad provides the starting point.

  2. Multi-turn strategy shifts — If Ray starts frustrated but calms down, the scratchpad won't update mid-conversation. The LLM must adapt. Consider: should the scratchpad include a "trajectory" note? ("Frustration was 0.8 at start, trending down")

  3. Token cost — SCRATCHPAD.md adds to every turn's context window. Keep it under 500 tokens. The schema above is ~200 tokens.

  4. Feedback loop — How do we know the strategy is working? Options:

    • Ray explicitly says "too verbose" or "that was perfect"
    • Conversation length/resolution speed as proxy
    • A "strategy audit" in dream cycles: review recent sessions, check if scratchpad advice was followed and whether outcomes were good

Open Questions

  1. Message access: Can the assembler actually get recent messages? Heartbeat has conversation history, but cron (isolated) sessions don't. The sessions_history tool might bridge this.

  2. Feedback calibration: The sentiment engine calibrates from message patterns. Should the scratchpad also capture response effectiveness metrics for future calibration?

  3. Multiple humans: In group chats, whose sentiment matters? Should the scratchpad track per-user state?

  4. SCRATCHPAD.md vs JSON: MD is human-readable but messy to parse. JSON is structured but ugly in context injection. Could use frontmatter YAML + prose body.

  5. Staleness indicator: Should the LLM ignore a scratchpad older than N minutes? Or is "last known state" always better than nothing?


Implementation Path

Phase 1: Manual Scratchpad (This Week)

  • Write SCRATCHPAD.md manually during heartbeats
  • Just mood + strategy + time context
  • See if it actually changes my behavior

Phase 2: Sentiment Integration (When sentiment-engine is packaged)

  • npm link @stev3/sentiment-engine in workspace
  • Write scripts/assemble-scratchpad.ts
  • Run from heartbeat

Phase 3: Full Assembly (After memory gardener)

  • Add memory search integration
  • Add wake pattern alerts
  • Add routing hints
  • Target: <500 tokens total

Phase 4: Real-Time (When OpenClaw supports it)

  • Migrate to pre-turn hook
  • Per-turn freshness
  • Feedback loop integration



Created: 2026-02-06 03:00 (Dream Cycle #4) Status: Design. The bridge between standalone libraries and runtime behavior. Key insight: File-mediated integration is the only practical path given OpenClaw's current architecture. The SCRATCHPAD.md pattern turns any subsystem into context injection without core changes.