Files
claude-mem/docs/context/platform-integration-guide.md
T
Alex Newman 1491123706 feat(ProcessManager): add Bun availability check and improve PID file validation
- Implemented a method to check if Bun is available in the system PATH.
- Updated the startWithBun method to return an error if Bun is not found.
- Enhanced PID file parsing to validate required fields and their types.
- Cleaned up stale PID files if the process is no longer alive.

fix(SettingsRoutes): clear port cache after updating settings

- Added a call to clearPortCache after writing updated settings to ensure the application uses the latest configuration.
2025-12-12 17:48:41 -05:00

34 KiB

Platform Integration Guide - Claude-Mem Worker Service

Version: 7.0.0 (December 2025) Target Audience: Developers building claude-mem integrations (VSCode extensions, IDE plugins, CLI tools) Purpose: Complete reference for integrating with the claude-mem worker service without requiring access to the knowledge base


Table of Contents

  1. Quick Reference
  2. Worker Architecture
  3. API Reference
  4. Data Models
  5. Integration Patterns
  6. Error Handling & Resilience
  7. Development Workflow
  8. Testing Strategy
  9. Code Examples

Quick Reference

Worker Service Basics

const WORKER_BASE_URL = 'http://localhost:37777';
const DEFAULT_PORT = 37777; // Override with CLAUDE_MEM_WORKER_PORT

Most Common Operations

// Health check
GET /api/health

// Create/get session and queue observation
POST /api/sessions/observations
Body: { claudeSessionId, tool_name, tool_input, tool_response, cwd }

// Queue summary
POST /api/sessions/summarize
Body: { claudeSessionId, last_user_message, last_assistant_message }

// Complete session
POST /api/sessions/complete
Body: { claudeSessionId }

// Search observations
GET /api/search?query=authentication&type=observations&format=index&limit=20

// Get recent context for project
GET /api/context/recent?project=my-project&limit=3

Environment Variables

CLAUDE_MEM_MODEL=claude-haiku-4-5           # Model for observations/summaries
CLAUDE_MEM_CONTEXT_OBSERVATIONS=50          # Observations injected at SessionStart
CLAUDE_MEM_WORKER_PORT=37777                # Worker service port
CLAUDE_MEM_PYTHON_VERSION=3.13              # Python version for chroma-mcp

Build Commands (Local Development)

npm run build                 # Compile TypeScript (hooks + worker)
npm run sync-marketplace      # Copy to ~/.claude/plugins
npm run worker:restart        # Restart Bun worker
npm run worker:logs           # View worker logs
bun list                      # Check worker status

Worker Architecture

Request Flow

Platform Hook/Extension
  → HTTP Request to Worker (localhost:37777)
    → Route Handler (SessionRoutes/DataRoutes/SearchRoutes/etc.)
      → Domain Service (SessionManager/SearchManager/DatabaseManager)
        → Database (SQLite3 + Chroma vector DB)
          → SSE Broadcast (real-time UI updates)

Domain Services

DatabaseManager - SQLite connection management, initialization SessionManager - Event-driven session lifecycle, message queues SearchManager - Search orchestration (FTS5 + Chroma) SSEBroadcaster - Server-Sent Events for real-time updates SDKAgent - Claude Agent SDK for generating observations/summaries PaginationHelper - Query pagination utilities SettingsManager - User settings CRUD FormattingService - Result formatting (index vs full) TimelineService - Unified timeline generation

Route Organization

ViewerRoutes - Health check, viewer UI, SSE stream SessionRoutes - Session lifecycle (init, observations, summarize, complete) DataRoutes - Data retrieval (observations, summaries, prompts, stats) SearchRoutes - All search operations (unified search, timeline, semantic shortcuts) SettingsRoutes - User settings, MCP toggle, branch switching


API Reference

Session Lifecycle (SessionRoutes)

Create/Get Session + Queue Observation (New API)

POST /api/sessions/observations
Content-Type: application/json

{
  "claudeSessionId": "abc123",      // Claude session identifier (string)
  "tool_name": "Bash",
  "tool_input": { "command": "ls" },
  "tool_response": { "stdout": "..." },
  "cwd": "/path/to/project"
}

Response: { "status": "queued" } | { "status": "skipped", "reason": "private" }

Privacy Check: Skips if user prompt was entirely wrapped in <private> tags. Tag Stripping: Removes <private> and <claude-mem-context> tags before storage. Auto-Start: Ensures SDK agent generator is running to process the queue.

Queue Summary (New API)

POST /api/sessions/summarize
Content-Type: application/json

{
  "claudeSessionId": "abc123",
  "last_user_message": "User's message",
  "last_assistant_message": "Assistant's response"
}

Response: { "status": "queued" } | { "status": "skipped", "reason": "private" }

Complete Session (New API)

POST /api/sessions/complete
Content-Type: application/json

{
  "claudeSessionId": "abc123"
}

Response: { "success": true } | { "success": true, "message": "No active session found" }

Effect: Stops SDK agent, marks session complete, broadcasts status change.

Legacy Endpoints (Still Supported)

# Initialize session (legacy, uses sessionDbId)
POST /sessions/:sessionDbId/init
Body: { userPrompt, promptNumber }

# Queue observations (legacy)
POST /sessions/:sessionDbId/observations
Body: { tool_name, tool_input, tool_response, prompt_number, cwd }

# Queue summary (legacy)
POST /sessions/:sessionDbId/summarize
Body: { last_user_message, last_assistant_message }

# Complete session (legacy)
POST /sessions/:sessionDbId/complete

Note: New integrations should use /api/sessions/* endpoints with claudeSessionId.


Data Retrieval (DataRoutes)

Get Paginated Observations

GET /api/observations?offset=0&limit=20&project=my-project

Response: {
  "items": [...],
  "hasMore": boolean,
  "offset": number,
  "limit": number
}

Get Paginated Summaries

GET /api/summaries?offset=0&limit=20&project=my-project

Get Paginated User Prompts

GET /api/prompts?offset=0&limit=20&project=my-project

Get by ID

GET /api/observation/:id
GET /api/session/:id
GET /api/prompt/:id

Response: {...entity...} | 404 Not Found

Get Database Stats

GET /api/stats

Response: {
  "worker": {
    "version": "7.0.0",
    "uptime": 12345,
    "activeSessions": 2,
    "sseClients": 1,
    "port": 37777
  },
  "database": {
    "path": "~/.claude-mem/claude-mem.db",
    "size": 1048576,
    "observations": 500,
    "sessions": 50,
    "summaries": 25
  }
}

Get Projects List

GET /api/projects

Response: { "projects": ["claude-mem", "other-project", ...] }

Get Processing Status

GET /api/processing-status

Response: { "isProcessing": boolean, "queueDepth": number }

Search Operations (SearchRoutes)

GET /api/search?query=authentication&type=observations&format=index&limit=20

Parameters:
- query: Search query text (optional, omit for filter-only)
- type: "observations" | "sessions" | "prompts" (default: all)
- format: "index" | "full" (default: "index")
- limit: Number of results (default: 20)
- project: Filter by project name
- obs_type: Filter by observation type (discovery, decision, bugfix, feature, refactor)
- concepts: Filter by concepts (comma-separated)
- files: Filter by file paths (comma-separated)
- dateStart: ISO timestamp (filter start)
- dateEnd: ISO timestamp (filter end)

Response: {
  "observations": [...],
  "sessions": [...],
  "prompts": [...]
}

Format Options:

  • index: Minimal fields for list display (id, title, preview)
  • full: Complete entity with all fields

Unified Timeline

GET /api/timeline?anchor=123&depth_before=10&depth_after=10&project=my-project

Parameters:
- anchor: Anchor point (observation ID, "S123" for session, or ISO timestamp)
- depth_before: Records before anchor (default: 10)
- depth_after: Records after anchor (default: 10)
- project: Filter by project

Response: [
  { "type": "observation", "id": 120, "created_at_epoch": ..., ... },
  { "type": "session", "id": 5, "created_at_epoch": ..., ... },
  { "type": "observation", "id": 123, "created_at_epoch": ..., ... },
  ...
]

Semantic Shortcuts

# Find decision observations
GET /api/decisions?format=index&limit=20

# Find change-related observations
GET /api/changes?format=index&limit=20

# Find "how it works" explanations
GET /api/how-it-works?format=index&limit=20

Search by Concept

GET /api/search/by-concept?concept=discovery&format=index&limit=10&project=my-project

Search by File Path

GET /api/search/by-file?filePath=src/services/worker-service.ts&format=index&limit=10

Search by Type

GET /api/search/by-type?type=bugfix&format=index&limit=10

Get Recent Context

GET /api/context/recent?project=my-project&limit=3

Response: {
  "summaries": [...],
  "observations": [...]
}

Context Preview (for Settings UI)

GET /api/context/preview?project=my-project

Response: Plain text with ANSI colors (for terminal display)

Context Injection (for Hooks)

GET /api/context/inject?project=my-project&colors=true

Response: Pre-formatted context string ready for display

Settings & Configuration (SettingsRoutes)

Get/Update User Settings

GET /api/settings
Response: { "sidebarOpen": boolean, "selectedProject": string | null }

POST /api/settings
Body: { "sidebarOpen": true, "selectedProject": "my-project" }
Response: { "success": true }

MCP Server Status/Toggle

GET /api/mcp/status
Response: { "enabled": boolean }

POST /api/mcp/toggle
Body: { "enabled": true }
Response: { "success": true, "enabled": boolean }

Git Branch Operations

GET /api/branch/status
Response: { "current": "main", "remote": "origin/main", "ahead": 0, "behind": 0 }

POST /api/branch/switch
Body: { "branch": "feature/new-feature" }
Response: { "success": true }

POST /api/branch/update
Response: { "success": true, "updated": boolean }

Viewer & Real-Time Updates (ViewerRoutes)

Health Check

GET /api/health

Response: { "status": "ok" }

Viewer UI

GET /

Response: HTML (React app)

SSE Stream

GET /stream

Response: Server-Sent Events stream

Event Types:
- processing_status: { type, isProcessing, queueDepth }
- session_started: { type, sessionDbId, project }
- observation_queued: { type, sessionDbId }
- summarize_queued: { type }
- observation_created: { type, observation }
- summary_created: { type, summary }
- new_prompt: { type, id, claude_session_id, project, prompt_number, prompt_text, created_at_epoch }

Data Models

Active Session (In-Memory)

interface ActiveSession {
  sessionDbId: number;                  // Database ID (numeric)
  claudeSessionId: string;              // Claude session identifier (string)
  sdkSessionId: string | null;          // SDK session ID
  project: string;                      // Project name
  userPrompt: string;                   // Current user prompt text
  pendingMessages: PendingMessage[];    // Queue of pending operations
  abortController: AbortController;     // For cancellation
  generatorPromise: Promise<void> | null; // SDK agent promise
  lastPromptNumber: number;             // Last processed prompt number
  startTime: number;                    // Session start timestamp
  cumulativeInputTokens: number;        // Total input tokens
  cumulativeOutputTokens: number;       // Total output tokens
}

interface PendingMessage {
  type: 'observation' | 'summarize';
  tool_name?: string;
  tool_input?: any;
  tool_response?: any;
  prompt_number?: number;
  cwd?: string;
  last_user_message?: string;
  last_assistant_message?: string;
}

Database Entities

// SDK Session (stored in sdk_sessions table)
interface SDKSessionRow {
  id: number;
  claude_session_id: string;
  sdk_session_id: string;
  project: string;
  user_prompt: string;
  created_at_epoch: number;
  completed_at_epoch?: number;
}

// Observation (stored in observations table)
interface ObservationRow {
  id: number;
  sdk_session_id: string;
  title: string;
  subtitle?: string;
  summary: string;
  facts: string;           // JSON array of fact strings
  concepts: string;        // JSON array of concept strings
  files_touched: string;   // JSON array of file paths
  obs_type: string;        // discovery, decision, bugfix, feature, refactor
  project: string;
  created_at_epoch: number;
  prompt_number: number;
}

// Session Summary (stored in session_summaries table)
interface SessionSummaryRow {
  id: number;
  sdk_session_id: string;
  summary_text: string;
  facts: string;           // JSON array
  concepts: string;        // JSON array
  files_touched: string;   // JSON array
  project: string;
  created_at_epoch: number;
}

// User Prompt (stored in user_prompts table)
interface UserPromptRow {
  id: number;
  claude_session_id: string;
  sdk_session_id: string;
  project: string;
  prompt_number: number;
  prompt_text: string;
  created_at_epoch: number;
}

Search Results

interface ObservationSearchResult {
  id: number;
  title: string;
  subtitle?: string;
  summary: string;
  facts: string[];         // Parsed from JSON
  concepts: string[];      // Parsed from JSON
  files_touched: string[]; // Parsed from JSON
  obs_type: string;
  project: string;
  created_at_epoch: number;
  prompt_number: number;
  rank?: number;           // FTS5 rank score
}

interface SessionSummarySearchResult {
  id: number;
  summary_text: string;
  facts: string[];
  concepts: string[];
  files_touched: string[];
  project: string;
  created_at_epoch: number;
  rank?: number;
}

interface UserPromptSearchResult {
  id: number;
  claude_session_id: string;
  project: string;
  prompt_number: number;
  prompt_text: string;
  created_at_epoch: number;
  rank?: number;
}

Timeline Item

interface TimelineItem {
  type: 'observation' | 'session' | 'prompt';
  id: number;
  created_at_epoch: number;
  // Entity-specific fields based on type
}

Integration Patterns

Mapping Claude Code Hooks to Worker API

SessionStart Hook

// Not needed for new API - sessions are auto-created on first observation

UserPromptSubmit Hook

// No API call needed - user_prompt is captured by first observation in the prompt

PostToolUse Hook

async function onPostToolUse(context: HookContext) {
  const { session_id, tool_name, tool_input, tool_result, cwd } = context;

  const response = await fetch('http://localhost:37777/api/sessions/observations', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      claudeSessionId: session_id,
      tool_name,
      tool_input,
      tool_response: tool_result,
      cwd
    })
  });

  const result = await response.json();
  // result.status === 'queued' | 'skipped'
}

Summary Hook

async function onSummary(context: HookContext) {
  const { session_id, last_user_message, last_assistant_message } = context;

  await fetch('http://localhost:37777/api/sessions/summarize', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      claudeSessionId: session_id,
      last_user_message,
      last_assistant_message
    })
  });
}

SessionEnd Hook

async function onSessionEnd(context: HookContext) {
  const { session_id } = context;

  await fetch('http://localhost:37777/api/sessions/complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      claudeSessionId: session_id
    })
  });
}

VSCode Extension Integration

Language Model Tool Registration

import * as vscode from 'vscode';

interface SearchTool extends vscode.LanguageModelChatTool {
  invoke(
    options: vscode.LanguageModelToolInvocationOptions<{ query: string }>,
    token: vscode.CancellationToken
  ): vscode.ProviderResult<vscode.LanguageModelToolResult>;
}

const searchTool: SearchTool = {
  invoke: async (options, token) => {
    const { query } = options.input;

    try {
      const response = await fetch(
        `http://localhost:37777/api/search?query=${encodeURIComponent(query)}&format=index&limit=10`
      );

      if (!response.ok) {
        throw new Error(`Search failed: ${response.statusText}`);
      }

      const results = await response.json();

      // Format results for language model
      return new vscode.LanguageModelToolResult([
        new vscode.LanguageModelTextPart(JSON.stringify(results, null, 2))
      ]);
    } catch (error) {
      return new vscode.LanguageModelToolResult([
        new vscode.LanguageModelTextPart(`Error: ${error.message}`)
      ]);
    }
  }
};

// Register tool
vscode.lm.registerTool('claude-mem-search', searchTool);

Chat Participant Implementation

const participant = vscode.chat.createChatParticipant('claude-mem', async (request, context, stream, token) => {
  const claudeSessionId = context.session.id;

  // First message in conversation - no initialization needed
  // Session is auto-created on first observation

  // Process user message
  stream.markdown(`Searching memory for: ${request.prompt}\n\n`);

  const response = await fetch(
    `http://localhost:37777/api/search?query=${encodeURIComponent(request.prompt)}&format=index&limit=5`
  );

  const results = await response.json();

  if (results.observations?.length > 0) {
    stream.markdown('**Found observations:**\n');
    for (const obs of results.observations) {
      stream.markdown(`- ${obs.title} (${obs.project})\n`);
    }
  }

  return { metadata: { command: 'search' } };
});

Error Handling & Resilience

Connection Failures

async function callWorkerWithFallback<T>(
  endpoint: string,
  options?: RequestInit
): Promise<T | null> {
  try {
    const response = await fetch(`http://localhost:37777${endpoint}`, {
      ...options,
      signal: AbortSignal.timeout(5000) // 5s timeout
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return await response.json();
  } catch (error) {
    console.error(`Worker unavailable (${endpoint}):`, error);
    return null; // Graceful degradation
  }
}

Retry Logic with Exponential Backoff

async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 100
): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;

      const delay = baseDelay * Math.pow(2, attempt);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  throw new Error('Max retries exceeded');
}

Worker Health Check

async function isWorkerHealthy(): Promise<boolean> {
  try {
    const response = await fetch('http://localhost:37777/api/health', {
      signal: AbortSignal.timeout(2000)
    });
    return response.ok;
  } catch {
    return false;
  }
}

Privacy Tag Handling

The worker automatically strips privacy tags before storage:

  • <private>content</private> - User-level privacy control
  • <claude-mem-context>content</claude-mem-context> - System-level tag (prevents recursive storage)

Privacy Check: Observations/summaries are skipped if the entire user prompt was wrapped in <private> tags.

Custom Error Classes

class WorkerUnavailableError extends Error {
  constructor() {
    super('Claude-mem worker is not running or unreachable');
    this.name = 'WorkerUnavailableError';
  }
}

class WorkerTimeoutError extends Error {
  constructor(endpoint: string) {
    super(`Worker request timed out: ${endpoint}`);
    this.name = 'WorkerTimeoutError';
  }
}

SSE Stream Error Handling

function connectToSSE(onEvent: (event: any) => void) {
  const eventSource = new EventSource('http://localhost:37777/stream');

  eventSource.onmessage = (event) => {
    try {
      const data = JSON.parse(event.data);
      onEvent(data);
    } catch (error) {
      console.error('SSE parse error:', error);
    }
  };

  eventSource.onerror = (error) => {
    console.error('SSE connection error:', error);
    eventSource.close();

    // Reconnect after 5 seconds
    setTimeout(() => connectToSSE(onEvent), 5000);
  };

  return eventSource;
}

Development Workflow

vscode-extension/
├── src/
│   ├── extension.ts              # Extension entry point
│   ├── services/
│   │   ├── WorkerClient.ts       # HTTP client for worker
│   │   └── MemoryManager.ts      # High-level memory operations
│   ├── chat/
│   │   └── participant.ts        # Chat participant implementation
│   └── tools/
│       ├── search.ts             # Search language model tool
│       └── context.ts            # Context injection tool
├── package.json
├── tsconfig.json
└── README.md

Build Configuration (esbuild)

// build.js
const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ['src/extension.ts'],
  bundle: true,
  outfile: 'dist/extension.js',
  external: ['vscode'],
  format: 'cjs',
  platform: 'node',
  target: 'node18',
  sourcemap: true
}).catch(() => process.exit(1));

package.json (VSCode Extension)

{
  "name": "claude-mem-vscode",
  "displayName": "Claude-Mem",
  "version": "1.0.0",
  "engines": {
    "vscode": "^1.95.0"
  },
  "activationEvents": [
    "onStartupFinished"
  ],
  "main": "./dist/extension.js",
  "contributes": {
    "chatParticipants": [
      {
        "id": "claude-mem",
        "name": "memory",
        "description": "Search your persistent memory"
      }
    ],
    "languageModelTools": [
      {
        "name": "claude-mem-search",
        "displayName": "Search Memory",
        "description": "Search persistent memory for observations, sessions, and prompts"
      }
    ]
  },
  "scripts": {
    "build": "node build.js",
    "watch": "node build.js --watch",
    "package": "vsce package"
  },
  "devDependencies": {
    "@types/vscode": "^1.95.0",
    "esbuild": "^0.19.0",
    "typescript": "^5.3.0"
  }
}

Local Testing Loop

# Terminal 1: Watch build
npm run watch

# Terminal 2: Check worker status
bun list
bun logs claude-mem-worker

# Terminal 3: Test API manually
curl http://localhost:37777/api/health
curl "http://localhost:37777/api/search?query=test&limit=5"

# VSCode: Press F5 to launch extension host

Debug Configuration (.vscode/launch.json)

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run Extension",
      "type": "extensionHost",
      "request": "launch",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}"
      ],
      "outFiles": [
        "${workspaceFolder}/dist/**/*.js"
      ],
      "preLaunchTask": "npm: build"
    }
  ]
}

Testing Strategy

Unit Tests (Worker Client)

import { describe, it, expect } from 'vitest';
import { WorkerClient } from '../src/services/WorkerClient';

describe('WorkerClient', () => {
  it('should check worker health', async () => {
    const client = new WorkerClient();
    const healthy = await client.isHealthy();
    expect(healthy).toBe(true);
  });

  it('should queue observation', async () => {
    const client = new WorkerClient();
    const result = await client.queueObservation({
      claudeSessionId: 'test-123',
      tool_name: 'Bash',
      tool_input: { command: 'ls' },
      tool_response: { stdout: 'file1.txt' },
      cwd: '/tmp'
    });
    expect(result.status).toBe('queued');
  });

  it('should search observations', async () => {
    const client = new WorkerClient();
    const results = await client.search({ query: 'test', limit: 5 });
    expect(results).toHaveProperty('observations');
  });
});

Integration Tests (With Worker Spawning)

import { spawn } from 'child_process';
import { describe, it, expect, beforeAll, afterAll } from 'vitest';

describe('Worker Integration', () => {
  let workerProcess: ReturnType<typeof spawn>;

  beforeAll(async () => {
    // Start worker process
    workerProcess = spawn('node', ['dist/worker-service.js'], {
      env: { ...process.env, CLAUDE_MEM_WORKER_PORT: '37778' }
    });

    // Wait for worker to be ready
    await new Promise(resolve => setTimeout(resolve, 2000));
  });

  afterAll(() => {
    workerProcess.kill();
  });

  it('should respond to health check', async () => {
    const response = await fetch('http://localhost:37778/api/health');
    expect(response.ok).toBe(true);
  });
});

Manual Testing Checklist

Phase 1: Connection & Health

  • Worker starts successfully (bun list)
  • Health endpoint responds (curl http://localhost:37777/api/health)
  • SSE stream connects (curl http://localhost:37777/stream)

Phase 2: Session Lifecycle

  • Queue observation creates session
  • Observation appears in database
  • Privacy tags are stripped
  • Private prompts are skipped
  • Queue summary creates summary
  • Complete session stops processing

Phase 3: Search & Retrieval

  • Search observations by query
  • Search sessions by query
  • Search prompts by query
  • Get recent context for project
  • Get timeline around observation
  • Semantic shortcuts (decisions, changes, how-it-works)

Phase 4: Real-Time Updates

  • SSE broadcasts processing status
  • SSE broadcasts new observations
  • SSE broadcasts new summaries
  • SSE broadcasts new prompts

Phase 5: Error Handling

  • Graceful degradation when worker unavailable
  • Timeout handling for slow requests
  • Retry logic for transient failures

Code Examples

Complete WorkerClient Implementation

export class WorkerClient {
  private baseUrl: string;

  constructor(port: number = 37777) {
    this.baseUrl = `http://localhost:${port}`;
  }

  async isHealthy(): Promise<boolean> {
    try {
      const response = await fetch(`${this.baseUrl}/api/health`, {
        signal: AbortSignal.timeout(2000)
      });
      return response.ok;
    } catch {
      return false;
    }
  }

  async queueObservation(data: {
    claudeSessionId: string;
    tool_name: string;
    tool_input: any;
    tool_response: any;
    cwd?: string;
  }): Promise<{ status: string; reason?: string }> {
    const response = await fetch(`${this.baseUrl}/api/sessions/observations`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
      signal: AbortSignal.timeout(5000)
    });

    if (!response.ok) {
      throw new Error(`Failed to queue observation: ${response.statusText}`);
    }

    return await response.json();
  }

  async queueSummarize(data: {
    claudeSessionId: string;
    last_user_message?: string;
    last_assistant_message?: string;
  }): Promise<{ status: string; reason?: string }> {
    const response = await fetch(`${this.baseUrl}/api/sessions/summarize`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
      signal: AbortSignal.timeout(5000)
    });

    if (!response.ok) {
      throw new Error(`Failed to queue summary: ${response.statusText}`);
    }

    return await response.json();
  }

  async completeSession(claudeSessionId: string): Promise<void> {
    const response = await fetch(`${this.baseUrl}/api/sessions/complete`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ claudeSessionId }),
      signal: AbortSignal.timeout(5000)
    });

    if (!response.ok) {
      throw new Error(`Failed to complete session: ${response.statusText}`);
    }
  }

  async search(params: {
    query?: string;
    type?: 'observations' | 'sessions' | 'prompts';
    format?: 'index' | 'full';
    limit?: number;
    project?: string;
    obs_type?: string | string[];
    concepts?: string | string[];
    files?: string | string[];
    dateStart?: string;
    dateEnd?: string;
  }): Promise<any> {
    const queryString = new URLSearchParams(
      Object.entries(params)
        .filter(([_, v]) => v !== undefined)
        .map(([k, v]) => [k, Array.isArray(v) ? v.join(',') : String(v)])
    ).toString();

    const response = await fetch(
      `${this.baseUrl}/api/search?${queryString}`,
      { signal: AbortSignal.timeout(10000) }
    );

    if (!response.ok) {
      throw new Error(`Search failed: ${response.statusText}`);
    }

    return await response.json();
  }

  async getRecentContext(project: string, limit: number = 3): Promise<any> {
    const response = await fetch(
      `${this.baseUrl}/api/context/recent?project=${encodeURIComponent(project)}&limit=${limit}`,
      { signal: AbortSignal.timeout(10000) }
    );

    if (!response.ok) {
      throw new Error(`Get recent context failed: ${response.statusText}`);
    }

    return await response.json();
  }

  async getTimeline(params: {
    anchor: number | string;
    depth_before?: number;
    depth_after?: number;
    project?: string;
  }): Promise<any> {
    const queryString = new URLSearchParams(
      Object.entries(params)
        .filter(([_, v]) => v !== undefined)
        .map(([k, v]) => [k, String(v)])
    ).toString();

    const response = await fetch(
      `${this.baseUrl}/api/timeline?${queryString}`,
      { signal: AbortSignal.timeout(10000) }
    );

    if (!response.ok) {
      throw new Error(`Get timeline failed: ${response.statusText}`);
    }

    return await response.json();
  }

  connectSSE(onEvent: (event: any) => void): EventSource {
    const eventSource = new EventSource(`${this.baseUrl}/stream`);

    eventSource.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        onEvent(data);
      } catch (error) {
        console.error('SSE parse error:', error);
      }
    };

    eventSource.onerror = (error) => {
      console.error('SSE connection error:', error);
    };

    return eventSource;
  }
}

Search Language Model Tool

import * as vscode from 'vscode';
import { WorkerClient } from './WorkerClient';

export function registerSearchTool(context: vscode.ExtensionContext) {
  const client = new WorkerClient();

  const searchTool = vscode.lm.registerTool('claude-mem-search', {
    description: 'Search persistent memory for observations, sessions, and prompts',
    inputSchema: {
      type: 'object',
      properties: {
        query: {
          type: 'string',
          description: 'Search query text'
        },
        type: {
          type: 'string',
          enum: ['observations', 'sessions', 'prompts'],
          description: 'Type of results to return'
        },
        limit: {
          type: 'number',
          description: 'Maximum number of results',
          default: 10
        }
      },
      required: ['query']
    },
    invoke: async (options, token) => {
      const { query, type, limit = 10 } = options.input;

      try {
        const results = await client.search({
          query,
          type,
          format: 'index',
          limit
        });

        // Format results for language model
        let formatted = '';

        if (results.observations?.length > 0) {
          formatted += '## Observations\n\n';
          for (const obs of results.observations) {
            formatted += `- **${obs.title}** (${obs.project})\n`;
            formatted += `  ${obs.summary}\n`;
            if (obs.concepts?.length > 0) {
              formatted += `  Concepts: ${obs.concepts.join(', ')}\n`;
            }
            formatted += '\n';
          }
        }

        if (results.sessions?.length > 0) {
          formatted += '## Sessions\n\n';
          for (const session of results.sessions) {
            formatted += `- ${session.summary_text.substring(0, 100)}...\n\n`;
          }
        }

        return new vscode.LanguageModelToolResult([
          new vscode.LanguageModelTextPart(formatted)
        ]);
      } catch (error) {
        return new vscode.LanguageModelToolResult([
          new vscode.LanguageModelTextPart(`Error: ${error.message}`)
        ]);
      }
    }
  });

  context.subscriptions.push(searchTool);
}

Critical Implementation Notes

sessionDbId vs claudeSessionId

IMPORTANT: Use claudeSessionId (string) for new API endpoints, not sessionDbId (number).

  • sessionDbId - Numeric database ID (legacy endpoints only)
  • claudeSessionId - String identifier from Claude platform (new endpoints)

JSON String Fields

Fields like facts, concepts, and files_touched are stored as JSON strings and require parsing:

const observation = await client.getObservationById(123);
const facts = JSON.parse(observation.facts); // string[] array
const concepts = JSON.parse(observation.concepts); // string[] array

Timestamps

All created_at_epoch fields are in milliseconds, not seconds:

const date = new Date(observation.created_at_epoch); // ✅ Correct
const date = new Date(observation.created_at_epoch * 1000); // ❌ Wrong (already in ms)

Asynchronous Processing

Workers process observations/summaries asynchronously. Results appear in the database 1-2 seconds after queuing. Use SSE events for real-time notifications.

Privacy Tags

Always wrap sensitive content in <private> tags to prevent storage:

const userMessage = '<private>API key: sk-1234567890</private>';
// This observation will be skipped (entire prompt is private)

Additional Resources


End of Platform Integration Guide