Skip to main content
ClaudeWave
Skill1k repo starsupdated 4d ago

create-adapter

The create-adapter skill provides a guide for implementing performance-optimized adapter integrations in Sidecar, a system that syncs conversation history from various Claude clients and IDEs. Use this when building a new adapter to connect an unsupported tool, requiring knowledge of the required interface, performance bottlenecks in the watch-event hot path, caching strategies, and reference implementations to avoid CPU and file descriptor spikes during active sessions.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/marcus/sidecar /tmp/create-adapter && cp -r /tmp/create-adapter/.claude/skills/create-adapter ~/.claude/skills/create-adapter
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Create Adapter

## Why Performance Matters

Adapters are the largest performance risk in Sidecar. Conversations refresh on watch events in a hot path that runs continuously during active sessions:

```
watch event -> coalescer -> session refresh -> adapter.Sessions() -> metadata parsing
```

If an adapter does full directory scans and full-file reparses on every change, CPU and FD usage spike quickly.

## Reference Adapters

Study these before writing a new adapter:
- `internal/adapter/claudecode` - Incremental JSONL parsing, targeted refresh
- `internal/adapter/codex` - Directory cache, two-pass metadata parsing, global watch scope
- `internal/adapter/cursor` - SQLite/WAL-aware cache invalidation, FD-safe DB access
- `internal/adapter/pi` - Global scope, JSONL, CWD-based filtering, session classification, message prefix stripping

## Required Interface

All adapters implement `adapter.Adapter`:

```go
type Adapter interface {
    ID() string
    Name() string
    Icon() string
    Detect(projectRoot string) (bool, error)
    Capabilities() CapabilitySet
    Sessions(projectRoot string) ([]Session, error)
    Messages(sessionID string) ([]Message, error)
    Usage(sessionID string) (*UsageStats, error)
    Watch(projectRoot string) (<-chan Event, io.Closer, error)
}
```

### Required Session Fields

Every session from `Sessions()` must set:
- `ID`, `Name`
- `AdapterID`, `AdapterName`, `AdapterIcon`
- `CreatedAt`, `UpdatedAt`
- `MessageCount`, `FileSize`

`FileSize` is used for dynamic debounce and huge-session auto-reload protection.

### Path and Watch Strategy

Set `Session.Path` only when Sidecar should use tiered file watching for that adapter:
- **File-based append-only** (JSONL/log): set `Path` to absolute file path — this opts into TieredWatcher with HOT/COLD/FROZEN tiers
- **DB/WAL adapters** (Cursor, Warp, Kiro): prefer adapter-specific `Watch()` with WAL-aware invalidation; do not set `Path` unless tiered watching covers your write surface

**FROZEN tier**: File-based sessions with `Path` set automatically benefit from the FROZEN tier. Sessions unchanged for 24 hours (`FrozenThreshold`) are excluded from cold polling entirely — zero syscalls. They unfreeze when promoted to HOT (e.g., user selects the session). This is critical for adapters with thousands of session files; without it, `pollColdSessions()` does one `os.Stat()` per file every 30 seconds.

## Performance Standards

### 1) Cache metadata and messages aggressively

Minimum cache keys:
- Metadata: `path + size + modTime`
- Messages: `path + size + modTime`
- SQLite/WAL: include WAL size+mtime in the key

Use bounded LRU behavior. Prune stale paths.

### 2) Incremental parsing for append-only formats

For JSONL/event-log adapters:
- Cache last parsed byte offset
- Parse only appended bytes
- Fall back to full parse on shrink/rotation/corruption
- Preserve immutable head metadata from prior parse

### 3) Two-pass metadata for large files

When incremental metadata parse is impractical:
- Head pass: ID, CWD, first user message, first timestamp
- Tail pass: latest timestamp, token totals
- Skip middle of large files

### 4) Avoid repeated expensive path work

Resolve project path once per `Sessions()` call (`Abs`/`EvalSymlinks`), reuse for all matches.

### 5) Return defensive copies from caches

Never return cache-owned slices/maps directly. Copy message/session structures to avoid mutation bugs.

### 6) Keep DB access FD-safe

For SQLite adapters:
- Open read-only (`mode=ro`)
- `SetMaxOpenConns(1)`, `SetMaxIdleConns(0)`
- Close rows and DB handles promptly
- Avoid multiple DB connections per `Messages()` call

## Watching and FD Management

### 1) Prefer directory-level watches
Do not watch per-session files when directory-level watch gives equivalent signals.

### 2) Implement watch scope
If adapter watches a global path (same location regardless of worktree):
```go
func (a *Adapter) WatchScope() adapter.WatchScope {
    return adapter.WatchScopeGlobal
}
```
This prevents duplicate watchers across worktrees.

### 3) Always emit SessionID when known
Watch events should include session ID for targeted refresh (avoids full reloads).

### 4) Debounce and non-blocking sends
- Debounce bursty write events
- Use buffered channels
- Non-blocking sends: `select { case ch <- evt: default: }`

### 5) Leverage FROZEN tier for file-based adapters
File-based adapters that set `Session.Path` get TieredWatcher's three-tier system (HOT → COLD → FROZEN). Sessions unchanged for 24h are frozen and cost zero polling overhead. This is the primary defense against CPU spikes with thousands of session files. If your adapter has file-based sessions, always set `Path` — the FROZEN tier scales automatically.

### 6) Ensure cleanup
All watcher paths must close cleanly on plugin stop. No goroutine or FD leaks.

## Message and Content Rendering

Adapters must provide rich structured content for Conversation Flow UI.

### Required message mapping
Map source records to:
- `Message.Role`, `Message.Content`, `Message.ContentBlocks`
- `Message.ToolUses` (legacy compatibility)
- `Message.ThinkingBlocks` (if available)
- `Message.Model` when available

### Tool linking rule
Use consistent `ToolUseID` for `tool_use` and `tool_result` blocks. If incremental parsing is used, preserve pending tool-link state across cache updates.

## Optional Interfaces

### TargetedRefresher
```go
type TargetedRefresher interface {
    SessionByID(sessionID string) (*Session, error)
}
```
Reduces refresh from O(N sessions) to O(1). Implement when adapter can resolve a session directly.

### ProjectDiscoverer
Implement when source format allows discovery of sessions beyond current git worktrees.

## Error Handling

- `Detect()`: return `(false, nil)` for missing data directories
- `Sessions()`: skip corrupt/unreadable entries and continue; hard-fail only on systemic errors
- `Messages()`: return `nil, nil` for missing session files; fail on parse errors
- `Watch()`: return