Files
claude-mem/docs/reports/issue-591-openrouter-memorysessionid-capture.md
T
Alex Newman 2659ec3231 fix: Claude Code 2.1.1 compatibility + log-level audit + path validation fixes (#614)
* Refactor CLAUDE.md and related files for December 2025 updates

- Updated CLAUDE.md in src/services/worker with new entries for December 2025, including changes to Search.ts, GeminiAgent.ts, SDKAgent.ts, and SessionManager.ts.
- Revised CLAUDE.md in src/shared to reflect updates and new entries for December 2025, including paths.ts and worker-utils.ts.
- Modified hook-constants.ts to clarify exit codes and their behaviors.
- Added comprehensive hooks reference documentation for Claude Code, detailing usage, events, and examples.
- Created initial CLAUDE.md files in various directories to track recent activity.

* fix: Merge user-message-hook output into context-hook hookSpecificOutput

- Add footer message to additionalContext in context-hook.ts
- Remove user-message-hook from SessionStart hooks array
- Fixes issue where stderr+exit(1) approach was silently discarded

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update logs and documentation for recent plugin and worker service changes

- Added detailed logs for worker service activities from Dec 10, 2025 to Jan 7, 2026, including initialization patterns, cleanup confirmations, and diagnostic logging.
- Updated plugin documentation with recent activities, including plugin synchronization and configuration changes from Dec 3, 2025 to Jan 7, 2026.
- Enhanced the context hook and worker service logs to reflect improvements and fixes in the plugin architecture.
- Documented the migration and verification processes for the Claude memory system and its integration with the marketplace.

* Refactor hooks architecture and remove deprecated user-message-hook

- Updated hook configurations in CLAUDE.md and hooks.json to reflect changes in session start behavior.
- Removed user-message-hook functionality as it is no longer utilized in Claude Code 2.1.0; context is now injected silently.
- Enhanced context-hook to handle session context injection without user-visible messages.
- Cleaned up documentation across multiple files to align with the new hook structure and removed references to obsolete hooks.
- Adjusted timing and command execution for hooks to improve performance and reliability.

* fix: Address PR #610 review issues

- Replace USER_MESSAGE_ONLY test with BLOCKING_ERROR test in hook-constants.test.ts
- Standardize Claude Code 2.1.0 note wording across all three documentation files
- Exclude deprecated user-message-hook.ts from logger-usage-standards test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Remove hardcoded fake token counts from context injection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Address PR #610 review issues by fixing test files, standardizing documentation notes, and verifying code quality improvements.

* fix: Add path validation to CLAUDE.md distribution to prevent invalid directory creation

- Add isValidPathForClaudeMd() function to reject invalid paths:
  - Tilde paths (~) that Node.js doesn't expand
  - URLs (http://, https://)
  - Paths with spaces (likely command text or PR references)
  - Paths with # (GitHub issue/PR references)
  - Relative paths that escape project boundary

- Integrate validation in updateFolderClaudeMdFiles loop
- Add 6 unit tests for path validation
- Update .gitignore to prevent accidental commit of malformed directories
- Clean up existing invalid directories (~/, PR #610..., git diff..., https:)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: Implement path validation in CLAUDE.md generation to prevent invalid directory creation

- Added `isValidPathForClaudeMd()` function to validate file paths in `src/utils/claude-md-utils.ts`.
- Integrated path validation in `updateFolderClaudeMdFiles` to skip invalid paths.
- Added 6 new unit tests in `tests/utils/claude-md-utils.test.ts` to cover various rejection cases.
- Updated `.gitignore` to prevent tracking of invalid directories.
- Cleaned up existing invalid directories in the repository.

* feat: Promote critical WARN logs to ERROR level across codebase

Comprehensive log-level audit promoting 38+ WARN messages to ERROR for
improved debugging and incident response:

- Parser: observation type errors, data contamination
- SDK/Agents: empty init responses (Gemini, OpenRouter)
- Worker/Queue: session recovery, auto-recovery failures
- Chroma: sync failures, search failures (now treated as critical)
- SQLite: search failures (primary data store)
- Session/Generator: failures, missing context
- Infrastructure: shutdown, process management failures
- File Operations: CLAUDE.md updates, config reads
- Branch Management: recovery checkout failures

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: Address PR #614 review issues

- Remove incorrectly tracked tilde-prefixed files from git
- Fix absolute path validation to check projectRoot boundaries
- Add test coverage for absolute path validation edge cases

Closes review issues:
- Issue 1: ~/ prefixed files removed from tracking
- Issue 3: Absolute paths now validated against projectRoot
- Issue 4: Added 3 new test cases for absolute path scenarios

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* build assets and context

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 23:34:20 -05:00

14 KiB

Issue #591: OpenRouter Agent Fails to Capture memorySessionId for Empty Prompt History Sessions

Report Date: 2026-01-07 Issue: #591 Reporter: cjdrilke Environment: claude-mem 9.0.0, Provider: openrouter, Model: xiaomi/mimo-v2-flash:free, Platform: linux


1. Executive Summary

This issue describes a critical failure in the OpenRouter agent where it cannot store observations for sessions that have an empty prompt history (prompt_counter = 0). The error message "Cannot store observations: memorySessionId not yet captured" indicates that the memorySessionId is null when processAgentResponse() attempts to store observations.

Key Finding: Unlike the Claude SDK Agent which captures memorySessionId from SDK response messages, the OpenRouter Agent has no mechanism to capture or generate a memorySessionId. This is a fundamental architectural gap that causes all OpenRouter sessions to fail on their first observation.

Severity: Critical Priority: P1 Impact: All new OpenRouter sessions fail to store observations


2. Problem Analysis

2.1 Error Manifestation

Error: Cannot store observations: memorySessionId not yet captured

This error originates from ResponseProcessor.ts line 73-75:

// CRITICAL: Must use memorySessionId (not contentSessionId) for FK constraint
if (!session.memorySessionId) {
  throw new Error('Cannot store observations: memorySessionId not yet captured');
}

2.2 Affected Code Path

  1. OpenRouter session starts via OpenRouterAgent.startSession()
  2. Session is initialized with memorySessionId: null
  3. OpenRouter API is queried and returns a response
  4. processAgentResponse() is called with the response
  5. memorySessionId is still null - no capture mechanism exists
  6. Error thrown, observations not stored

2.3 Comparison with SDK Agent

The Claude SDK Agent successfully captures memorySessionId at SDKAgent.ts lines 120-141:

// Capture memory session ID from first SDK message (any type has session_id)
if (!session.memorySessionId && message.session_id) {
  session.memorySessionId = message.session_id;
  // Persist to database for cross-restart recovery
  this.dbManager.getSessionStore().updateMemorySessionId(
    session.sessionDbId,
    message.session_id
  );
  // ... verification logging ...
}

The OpenRouter Agent has no equivalent capture mechanism.


3. Technical Details

3.1 Session ID Architecture

Claude-mem uses a dual session ID system (documented in docs/SESSION_ID_ARCHITECTURE.md):

ID Purpose Source
contentSessionId User's Claude Code conversation ID Hook system
memorySessionId Memory agent's internal session for resume SDK response

3.2 Session Initialization Flow

1. Hook creates session
   createSDKSession(contentSessionId, project, prompt)

   Database state:
   ├─ content_session_id: "user-session-123"
   └─ memory_session_id: NULL (not yet captured)

2. SessionManager.initializeSession() creates ActiveSession:
   session = {
     sessionDbId: number,
     contentSessionId: "user-session-123",
     memorySessionId: null,  // ← Critical: starts as null
     ...
   }

3.3 OpenRouter Response Format

OpenRouter uses an OpenAI-compatible API response format:

interface OpenRouterResponse {
  choices?: Array<{
    message?: {
      role?: string;
      content?: string;
    };
    finish_reason?: string;
  }>;
  usage?: {
    prompt_tokens?: number;
    completion_tokens?: number;
    total_tokens?: number;
  };
  error?: {
    message?: string;
    code?: string;
  };
}

Critical Gap: This response format does NOT include a session_id field. OpenRouter is a stateless API that does not maintain server-side session state.

3.4 Root Cause in OpenRouterAgent.ts

In OpenRouterAgent.startSession() (lines 85-133), the init response is processed:

const initResponse = await this.queryOpenRouterMultiTurn(session.conversationHistory, apiKey, model, siteUrl, appName);

if (initResponse.content) {
  // Add response to conversation history
  session.conversationHistory.push({ role: 'assistant', content: initResponse.content });

  // ... token tracking ...

  // Process response using shared ResponseProcessor (no original timestamp for init - not from queue)
  await processAgentResponse(
    initResponse.content,
    session,  // ← memorySessionId is still null here
    this.dbManager,
    this.sessionManager,
    worker,
    tokensUsed,
    null,
    'OpenRouter',
    undefined
  );
}

No memorySessionId capture occurs between session initialization and calling processAgentResponse().


4. Impact Assessment

4.1 Direct Impact

  • All OpenRouter sessions fail when prompt_counter = 0 (new sessions)
  • No observations are stored for OpenRouter-based memory extraction
  • Error prevents any memory from being captured via OpenRouter

4.2 Scope of Impact

Affected Not Affected
All OpenRouter providers Claude SDK Agent
All OpenRouter models Gemini Agent (if implemented differently)
New sessions (prompt_counter = 0) Potentially resumed sessions*

*Note: Resumed sessions may work if they were previously processed by Claude SDK and have a captured memorySessionId from a fallback.

4.3 User Experience

Users configuring OpenRouter as their provider will:

  1. See successful API calls to OpenRouter
  2. Receive no stored observations
  3. See error messages in logs about memorySessionId not captured
  4. Have an empty memory database despite apparent processing

5. Root Cause Analysis

5.1 Primary Root Cause

The OpenRouter Agent was implemented without a mechanism to generate or capture memorySessionId.

Unlike the Claude SDK which returns a session_id in its response messages, OpenRouter's OpenAI-compatible API is stateless and does not provide session identifiers.

5.2 Contributing Factors

  1. Architectural Mismatch: The memorySessionId concept was designed around the Claude SDK's session management, which OpenRouter does not have.

  2. Missing Initialization Logic: Neither the OpenRouter agent nor the ResponseProcessor generates a memorySessionId when one is not provided by the API.

  3. Shared ResponseProcessor Assumption: ResponseProcessor.ts assumes memorySessionId is always captured before it is called, which is true for Claude SDK but not for OpenRouter.

5.3 Why It Worked Before (Speculation)

This may have been masked if:

  • OpenRouter fallback to Claude SDK triggered before the bug manifested
  • Initial testing used existing sessions with previously captured memorySessionId
  • The feature was added without comprehensive test coverage for new sessions

Since OpenRouter is stateless, generate a unique memorySessionId when starting an OpenRouter session:

Location: OpenRouterAgent.ts in startSession() method, after session initialization

async startSession(session: ActiveSession, worker?: WorkerRef): Promise<void> {
  try {
    // Generate memorySessionId for stateless providers (OpenRouter doesn't have session tracking)
    if (!session.memorySessionId) {
      const generatedMemorySessionId = `openrouter-${session.contentSessionId}-${Date.now()}`;
      session.memorySessionId = generatedMemorySessionId;

      // Persist to database
      this.dbManager.getSessionStore().updateMemorySessionId(
        session.sessionDbId,
        generatedMemorySessionId
      );

      logger.info('SESSION', `MEMORY_ID_GENERATED | sessionDbId=${session.sessionDbId} | memorySessionId=${generatedMemorySessionId} | provider=OpenRouter`, {
        sessionId: session.sessionDbId,
        memorySessionId: generatedMemorySessionId
      });
    }

    // ... rest of existing code ...
  }
}

Pros:

  • Minimal code changes
  • Follows existing patterns
  • Works with stateless APIs
  • Maintains FK integrity

Cons:

  • Memory session ID format differs from Claude SDK
  • No resume capability (OpenRouter is stateless anyway)

6.2 Solution B: Use contentSessionId as memorySessionId for Stateless Providers

For stateless providers, use the contentSessionId directly as the memorySessionId:

if (!session.memorySessionId) {
  session.memorySessionId = session.contentSessionId;
  this.dbManager.getSessionStore().updateMemorySessionId(
    session.sessionDbId,
    session.contentSessionId
  );
}

Pros:

  • Simpler approach
  • No additional ID generation

Cons:

  • Violates the architectural principle that memorySessionId should differ from contentSessionId
  • Could cause issues with session isolation (see SESSION_ID_ARCHITECTURE.md warnings)

6.3 Solution C: Allow null memorySessionId with Auto-Generation in ResponseProcessor

Modify ResponseProcessor.ts to generate a memorySessionId if one is not present:

// In processAgentResponse():
if (!session.memorySessionId) {
  const generatedId = `auto-${session.contentSessionId}-${Date.now()}`;
  session.memorySessionId = generatedId;
  // Persist to database
  dbManager.getSessionStore().updateMemorySessionId(session.sessionDbId, generatedId);
  logger.info('DB', `AUTO_GENERATED_MEMORY_ID | sessionDbId=${session.sessionDbId} | memorySessionId=${generatedId}`);
}

Pros:

  • Works for any agent type
  • Single point of fix

Cons:

  • ResponseProcessor takes on responsibilities it shouldn't have
  • Less explicit about provider behavior

Solution A is recommended because:

  1. It explicitly handles the stateless nature of OpenRouter
  2. It follows the existing pattern established by Claude SDK Agent
  3. It keeps the memorySessionId generation in the agent where provider-specific logic belongs
  4. It maintains clear separation of concerns

7. Priority/Severity Assessment

7.1 Severity Matrix

Factor Assessment
Data Loss High - All observations lost for OpenRouter sessions
Functionality Complete - OpenRouter provider is non-functional
Workaround Exists - Use Claude SDK or Gemini providers
Affected Users Subset - Only OpenRouter users
Regression Unknown - May be present since OpenRouter was added

7.2 Priority Assignment

Priority: P1 (High)

Rationale:

  • Complete feature failure for affected configuration
  • Users who choose OpenRouter are completely blocked
  • Fix is straightforward with low regression risk
Action Timeline
Hotfix development 1-2 hours
Testing 1 hour
Code review 30 minutes
Release Same day

8. Testing Recommendations

8.1 Unit Tests to Add

// tests/worker/openrouter-agent.test.ts

describe('OpenRouterAgent memorySessionId handling', () => {
  it('should generate memorySessionId when session has none', async () => {
    const session = createMockSession({
      memorySessionId: null,
      contentSessionId: 'test-content-123'
    });

    await openRouterAgent.startSession(session, mockWorker);

    expect(session.memorySessionId).not.toBeNull();
    expect(session.memorySessionId).toContain('openrouter-');
  });

  it('should persist generated memorySessionId to database', async () => {
    const session = createMockSession({ memorySessionId: null });

    await openRouterAgent.startSession(session, mockWorker);

    expect(mockDbManager.getSessionStore().updateMemorySessionId)
      .toHaveBeenCalledWith(session.sessionDbId, expect.any(String));
  });

  it('should not regenerate memorySessionId if already present', async () => {
    const existingId = 'existing-memory-id';
    const session = createMockSession({ memorySessionId: existingId });

    await openRouterAgent.startSession(session, mockWorker);

    expect(session.memorySessionId).toBe(existingId);
  });
});

8.2 Integration Tests to Add

describe('OpenRouter end-to-end observation storage', () => {
  it('should successfully store observations for new OpenRouter sessions', async () => {
    // Create new session via hook
    const sessionDbId = createSDKSession(db, 'content-123', 'test-project', 'test prompt');

    // Initialize and start OpenRouter agent
    const session = sessionManager.initializeSession(sessionDbId);
    await openRouterAgent.startSession(session, mockWorker);

    // Verify observations were stored
    const observations = db.prepare('SELECT * FROM observations WHERE memory_session_id = ?')
      .all(session.memorySessionId);
    expect(observations.length).toBeGreaterThan(0);
  });
});

File Relevance
src/services/worker/OpenRouterAgent.ts Primary fix location
src/services/worker/agents/ResponseProcessor.ts Error origin (line 73-75)
src/services/worker/SessionManager.ts Session initialization
src/services/worker/SDKAgent.ts Reference implementation for memorySessionId capture
src/services/sqlite/sessions/create.ts Database session creation
docs/SESSION_ID_ARCHITECTURE.md Architecture documentation
tests/worker/agents/response-processor.test.ts Existing test coverage

10. Conclusion

Issue #591 is a critical bug that renders the OpenRouter provider non-functional for new sessions. The root cause is a missing memorySessionId capture mechanism specific to stateless providers like OpenRouter.

The recommended fix is to generate a unique memorySessionId in OpenRouterAgent.startSession() before calling processAgentResponse(). This fix is straightforward, follows existing patterns, and carries low regression risk.

Immediate Action Required: Implement Solution A and release a hotfix.