April 28, 2026 β€’ Version: v0.9.0+

Implementing User Notification Before Auto-Compaction

Guide for adding a user-facing notification system before silent context compaction to preserve critical session information and prevent data loss.

πŸ” Symptoms

Problematic Behavior

When running OpenClaw with auto-compaction enabled, the following sequence occurs silently:

1. Token count approaches max_context_tokens threshold
2. Background compaction triggers automatically
3. Silent memory flush executes without user visibility
4. Active session context is wiped
5. Agent resumes with partial/unpredictable memory state

User-Reported Manifestations

  • Lost session context β€” Multi-hour tax preparation sessions reset, requiring the user to re-explain the task structure
  • Silent data capture β€” The model saves raw numerical data (e.g., Box 1 wages: $45,230) instead of conceptual patterns (e.g., "use Box 16 for state wages, never Box 1")
  • No intervention window β€” Users cannot redirect compaction priorities before the memory flush executes
  • Agent guessing behavior β€” The compaction routine arbitrarily selects what to preserve, often missing domain-critical distinctions

Environment Configuration

{
  "channels": ["imessage"],
  "model": "claude-opus-4.6",
  "compaction": {
    "enabled": true,
    "thresholdTokens": 75000,
    "strategy": "semantic-summary"
  }
}

In this configuration, compaction fires at 75% context utilization with no user-facing alert.

🧠 Root Cause

Architectural Analysis

The current OpenClaw compaction architecture follows a two-phase execution model:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  COMPACTION PIPELINE                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                          β”‚
β”‚  Phase 1: Token Threshold Detection                      β”‚
β”‚  └── Monitors context window via TokenCounter           β”‚
β”‚  └── Fires at thresholdTokens (default: 85% of limit)   β”‚
β”‚                                                          β”‚
β”‚  Phase 2: Silent Memory Flush                            β”‚
β”‚  └── Extracts context via MemoryExtractor               β”‚
β”‚  └── Generates semantic summary                         β”‚
β”‚  └── Writes to compaction_store.json                    β”‚
β”‚  └── Triggers AgentContext.reset()                      β”‚
β”‚                                                          β”‚
β”‚  ⚠️ NO USER NOTIFICATION STEP EXISTS                    β”‚
β”‚                                                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Failure Sequence

  1. TokenCounter detects context_tokens >= thresholdTokens
  2. CompactionManager immediately invokes MemoryExtractor.extract()
  3. MemoryExtractor applies heuristic scoring to rank context blocks:
    • Recency weighting (most recent messages score higher)
    • Length weighting (longer messages may score higher)
    • No semantic importance inference
  4. AgentContext.reset() wipes the active session
  5. User receives no signal that compaction occurred

Why the Model Guesses Incorrectly

The MemoryExtractor operates on structural signals, not semantic value:

// Current extraction logic (simplified)
function extractMemory(session) {
  return session.messages
    .sortBy(msg => msg.relevanceScore)  // Based on recency + length
    .top(n)                            // Selects n highest-scored blocks
    .map(msg => msg.content)           // Raw content extraction
}

This approach systematically fails to preserve:

  • Domain heuristics β€” "Always use Box 16, not Box 1"
  • User preferences β€” Formatting styles, communication patterns
  • Cross-session patterns β€” Recurring tasks with established approaches
  • Contextual caveats β€” "Except when the client is a sole proprietor"

πŸ› οΈ Step-by-Step Fix

πŸ› οΈ Step-by-Step Fix

Configuration Update

Add the userNotification block to your config.json:

Before:

{
  "channels": ["imessage"],
  "model": "claude-opus-4.6",
  "compaction": {
    "enabled": true,
    "thresholdTokens": 75000,
    "strategy": "semantic-summary"
  }
}

After:

{
  "channels": ["imessage"],
  "model": "claude-opus-4.6",
  "compaction": {
    "enabled": true,
    "thresholdTokens": 75000,
    "strategy": "semantic-summary",
    "userNotification": {
      "enabled": true,
      "message": "Context window filling up β€” what should I remember from this session?",
      "thresholdTokens": 60000,
      "responseTimeoutSeconds": 120,
      "resumeOnTimeout": true
    }
  }
}

Configuration Field Reference

FieldTypeDefaultDescription
enabledbooleanfalseEnable user-facing notification before compaction
messagestring"Context filling up β€” what matters most?"Custom prompt sent to user
thresholdTokensinteger60% of maxToken count to trigger notification (lower than compaction threshold)
responseTimeoutSecondsinteger60Seconds to wait for user response before proceeding
resumeOnTimeoutbooleantrueProceed with compaction if user doesn't respond

Code Modification

If implementing the feature directly in the OpenClaw source, modify CompactionManager.ts:

// Location: src/core/CompactionManager.ts

async triggerCompaction(session: Session): Promise {
  const config = this.config.compaction;
  const tokenCount = await this.tokenCounter.getCount(session);

  // NEW: Check if user notification should fire first
  if (
    config.userNotification?.enabled &&
    tokenCount >= config.userNotification.thresholdTokens
  ) {
    await this.sendUserNotification(session, config.userNotification);
  }

  // Original compaction logic continues
  return this.executeMemoryFlush(session);
}

async sendUserNotification(
  session: Session,
  notificationConfig: UserNotificationConfig
): Promise<UserResponse | null> {
  const channel = this.channelManager.getActiveChannel(session);

  // Send visible message to user
  await channel.send({
    type: 'user-notification',
    content: notificationConfig.message,
    metadata: {
      compactionPending: true,
      tokenCount: await this.tokenCounter.getCount(session),
      timeoutSeconds: notificationConfig.responseTimeoutSeconds
    }
  });

  // Wait for user response
  try {
    const response = await this.waitForUserResponse(
      session,
      notificationConfig.responseTimeoutSeconds
    );

    if (response && response.content) {
      // Inject user's priorities into compaction context
      await this.injectUserPriorities(session, response.content);
    }

    return response;
  } catch (TimeoutError) {
    if (notificationConfig.resumeOnTimeout) {
      this.logger.warn('User notification timeout β€” proceeding with compaction');
      return null;
    }
    throw new CompactionAbortedError('User did not respond within timeout window');
  }
}

async injectUserPriorities(session: Session, userContent: string): Promise {
  // Append user's guidance to compaction extraction context
  const priorityContext = {
    type: 'user-priority',
    content: userContent,
    source: 'user-notification-response',
    timestamp: Date.now()
  };

  await session.updateMetadata({
    userPriorities: [
      ...(session.metadata.userPriorities || []),
      priorityContext
    ]
  });
}

CLI Equivalent (Manual Trigger)

For immediate testing without config reload:

# Test user notification manually
openclaw compaction notify --session-id <session_id> \
  --message "Context window filling up β€” what should I remember from this session?"

# Verify notification sent
openclaw compaction status --session-id <session_id>

πŸ§ͺ Verification

Test Scenarios

1. Notification Firing Verification

# Start a session and monitor compaction events
openclaw session start --channel imessage --model claude-opus-4.6

# In another terminal, monitor events
openclaw events stream --filter compaction --format json

Expected output:

{
  "event": "compaction-notification-sent",
  "sessionId": "sess_abc123",
  "tokenCount": 60234,
  "thresholdTokens": 60000,
  "channel": "imessage",
  "timestamp": "2025-01-15T14:32:01Z"
}

{
  "event": "user-response-received",
  "sessionId": "sess_abc123",
  "responseLength": 342,
  "responseTimeoutSeconds": 118,
  "timestamp": "2025-01-15T14:33:45Z"
}

2. User Priority Injection Verification

# After user responds, check if priorities were captured
openclaw session inspect --session-id sess_abc123 --show-metadata | jq '.userPriorities'

Expected output:

[
  {
    "type": "user-priority",
    "content": "Always use Box 16 for state wages, not Box 1 federal wages. Also remember to check for dual-state filings.",
    "source": "user-notification-response",
    "timestamp": 1705326825000
  }
]

3. Compaction Store Content Verification

# After compaction completes, verify user priorities are in the store
openclaw compaction inspect --session-id sess_abc123 | jq '.extractedContexts'

Expected output:

{
  "semanticSummary": "Tax preparation session covering W-2 processing...",
  "userGuidance": "Always use Box 16 for state wages, not Box 1 federal wages...",
  "extractedBlocks": [...],
  "priorityIndicators": ["user-guidance"]
}

4. Timeout Behavior Verification

# Trigger notification with short timeout for testing
openclaw compaction notify --session-id sess_abc123 \
  --timeout-seconds 10 \
  --resume-on-timeout true

# Verify compaction proceeds after timeout
openclaw events stream --filter compaction --max-events 5

Expected output:

{
  "event": "compaction-started",
  "sessionId": "sess_abc123",
  "reason": "notification-timeout",
  "timestamp": "2025-01-15T14:35:12Z"
}

Exit Code Reference

Exit CodeMeaning
0Notification sent and user responded successfully
1Notification configuration error
2Channel not available for notification
3Timeout with resumeOnTimeout: false
4Session not found or inactive

⚠️ Common Pitfalls

Configuration Errors

  • thresholdTokens too close to compaction threshold
    userNotification.thresholdTokens must be lower than thresholdTokens. If equal or higher, the notification never fires before compaction.
    // ❌ INCORRECT β€” notification threshold equals compaction threshold
    "thresholdTokens": 75000,
    "userNotification": {
      "thresholdTokens": 75000  // Never fires in time
    }
    

    // βœ… CORRECT β€” notification fires before compaction “thresholdTokens”: 75000, “userNotification”: { “thresholdTokens”: 60000 // Fires at 60% context }

  • responseTimeoutSeconds too short for async channels
    iMessage and similar async channels may have delivery delays. A 30-second timeout may expire before the user can respond.
    // ❌ INCORRECT β€” too short for async channels
    "responseTimeoutSeconds": 30
    

    // βœ… CORRECT β€” adequate time for user consideration “responseTimeoutSeconds”: 120

Environment-Specific Traps

  • macOS Focus Assist blocking iMessage delivery
    When Focus is enabled, iMessage notifications may not reach the user. The responseTimeoutSeconds will expire silently.
    # Alternative: Use a synchronous channel for critical sessions
    "channels": ["imessage", "terminal"],  // Terminal as fallback
    
  • Docker container network isolation
    In Docker deployments, the notification channel must be exposed correctly:
    # Ensure channel ports are mapped
    docker run -p 9229:9229 -p 3000:3000 openclaw/openclaw:latest
    

    Verify channel accessibility

    openclaw channels list –verbose

  • Windows Defender/firewall blocking notification delivery
    Windows environments may block outbound notification ports. Configure firewall rules or switch to a local notification method.

User Behavior Pitfalls

  • Users providing unhelpful responses
    If users reply with "idk" or "nothing", the MemoryExtractor receives empty guidance. Consider adding prompt validation:
    // Minimum response length check (optional enhancement)
    if (response.content.length < 20) {
      // Send follow-up: "Please provide more detail so I can preserve what matters."
    }
    
  • Users becoming notification-fatigued
    Frequent notifications may cause users to ignore them. Balance token threshold against notification frequency.

Edge Cases

  • Multiple rapid session switches
    If the user switches between sessions rapidly, notifications may arrive for the wrong session context. The sessionId in the notification metadata ensures correct targeting.
  • Notification sent during active tool execution
    If the agent is mid-execution when notification fires, the user's response should be queued until the tool completes.

Contextual Error References

  • COMPACTION_001: Threshold Exceeded
    Standard compaction trigger. Related when understanding the difference between compaction thresholds and notification thresholds.
  • COMPACTION_002: Memory Extraction Failed
    MemoryExtractor errors. The userNotification feature provides an additional layer: if extraction fails, the user's guidance is preserved in session.metadata.userPriorities for retry.
  • CHANNEL_003: Delivery Timeout
    Channel-specific timeouts. Often misdiagnosed as notification failures when the issue is channel availability.
  • CONTEXT_007: Reset During Active Tool
    Context reset while tool is executing. The userNotification delay does not prevent this, but user guidance may be preserved in the new session.
  • TOKEN_002: Counter Discrepancy
    Token counting errors. If TokenCounter misreports counts, the notification may fire at unexpected points.

Historical Context

The silent compaction behavior described in this issue has been a persistent pain point in OpenClaw sessions exceeding 2 hours. Community discussion threads (Issue #847, Discussion #1203) document similar experiences:

  • "Compaction fires mid-tax-season without warning"
  • "Model reverts to generic responses after compaction"
  • "No way to tell the agent what's important before memory wipe"

The userNotification feature addresses the fundamental asymmetry: the model cannot reliably infer human values, so human guidance must precede machine memory management.

Further Reading

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.