Files
clawtap/docs/superpowers/plans/2026-03-24-session-id-unification.md
kuannnn 42861ea7fa feat: ClawTap v0.1.0 — initial release
Multi-adapter mobile UI for AI coding assistants.
Supports Claude Code, Codex CLI, and Gemini CLI through one interface.

Features:
- Real-time bidirectional sync via tmux + WebSocket
- Cross-AI review (send one AI's output to another for review)
- Multi-review tabs with minimize/expand
- Push notifications (PWA) with smart session-aware filtering
- Three-channel event system (hooks, file watcher, pane monitor)
- Voice input, image paste, draft persistence
- Terminal-native design (JetBrains Mono, dark theme, pixel art claw)
- CLI with --adapter flag on every command
- Zero-overhead fire-and-forget hooks
2026-03-26 10:40:26 +08:00

469 lines
16 KiB
Markdown

# Session ID Unification Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Eliminate the dual session ID system (internal ID + CLI UUID) and unify on CLI UUID as the single source of truth across the entire codebase.
**Architecture:** 5 phases -- (1) DB schema migration, (2) adapter internals (both Claude + Codex), (3) session manager + permissions + push, (4) server endpoints + frontend, (5) CLI script + cleanup. Each phase builds on the previous.
**Tech Stack:** TypeScript, SQLite (better-sqlite3), tmux, React, WebSocket, Shell (bin/codetap)
**Spec:** `docs/superpowers/specs/2026-03-24-session-id-unification-design.md`
---
## Phase 1: DB Schema
### Task 1: Migrate sessions table -- CLI UUID as primary key
**Files:**
- Modify: `server/db.ts`
- [ ] **Step 1: Update SessionRow interface (line 284)**
Remove `cli_session` field, add `window_name`:
```typescript
export interface SessionRow {
id: string; // CLI UUID (was internal ID)
cwd: string;
window_id: string | null;
window_name: string | null; // tmux window name for debug
adapter: string;
permission_mode: string;
created_at: string;
last_activity: string;
}
```
- [ ] **Step 2: Add schema migration in initDB() (after line 85)**
Detect old `cli_session` column and rebuild table:
```typescript
const hasCliSession = tableInfo.some((c: any) => c.name === 'cli_session');
const hasWindowName = tableInfo.some((c: any) => c.name === 'window_name');
if (hasCliSession && !hasWindowName) {
d.exec(`
CREATE TABLE IF NOT EXISTS sessions_new (
id TEXT PRIMARY KEY,
cwd TEXT NOT NULL,
window_id TEXT,
window_name TEXT,
adapter TEXT DEFAULT 'claude',
permission_mode TEXT DEFAULT 'default',
created_at TEXT DEFAULT (datetime('now')),
last_activity TEXT DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO sessions_new (id, cwd, window_id, window_name, adapter, permission_mode, created_at, last_activity)
SELECT
CASE WHEN cli_session IS NOT NULL AND cli_session != '' AND cli_session != id THEN cli_session ELSE id END,
cwd, window_id, id, adapter, permission_mode, created_at, last_activity
FROM sessions;
DROP TABLE sessions;
ALTER TABLE sessions_new RENAME TO sessions;
CREATE INDEX IF NOT EXISTS idx_sessions_window ON sessions(window_id);
`);
console.log('[db] Migrated sessions table: CLI UUID as primary key');
}
```
- [ ] **Step 3: Update CREATE TABLE for fresh installs (line 20)**
```sql
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
cwd TEXT NOT NULL,
window_id TEXT,
window_name TEXT,
adapter TEXT DEFAULT 'claude',
permission_mode TEXT DEFAULT 'default',
created_at TEXT DEFAULT (datetime('now')),
last_activity TEXT DEFAULT (datetime('now'))
);
```
- [ ] **Step 4: Update prepared statements**
Replace `sessionsUpsert` SQL:
```sql
INSERT INTO sessions (id, cwd, window_id, window_name, adapter)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
cwd = excluded.cwd,
window_id = excluded.window_id,
window_name = excluded.window_name,
last_activity = datetime('now')
```
Replace `sessionsFindByCliSession` with `sessionsGet`:
```typescript
sessionsGet: d.prepare('SELECT * FROM sessions WHERE id = ?'),
```
- [ ] **Step 5: Update sessions operations export (line 308)**
```typescript
export const sessions = {
upsert(id: string, cwd: string, windowId?: string, windowName?: string, adapter?: string): void {
stmts().sessionsUpsert.run(id, cwd, windowId || null, windowName || null, adapter || 'claude');
},
get(id: string): SessionRow | undefined {
return stmts().sessionsGet.get(id) as SessionRow | undefined;
},
findByWindowId(windowId: string): SessionRow | undefined {
return stmts().sessionsFindByWindowId.get(windowId) as SessionRow | undefined;
},
remove(id: string): void { stmts().sessionsRemove.run(id); },
getAll(): SessionRow[] { return stmts().sessionsGetAll.all() as SessionRow[]; },
clearAll(): void { getDB().exec('DELETE FROM sessions'); },
};
```
Key: `upsert` signature changes from `(id, cliSession, cwd, windowId, adapter)` to `(id, cwd, windowId, windowName, adapter)`. `findByCliSession` replaced by `get` (PK lookup).
**IMPORTANT — Backward compatibility:** To allow each task to compile independently, KEEP the old methods as deprecated aliases alongside the new ones:
```typescript
/** @deprecated Use get() instead */
findByCliSession(cliSession: string): SessionRow | undefined {
return this.get(cliSession); // After migration, cli_session IS the id
},
/** @deprecated Use upsert(id, cwd, windowId, windowName, adapter) instead */
upsertLegacy(id: string, cliSession: string, cwd: string, windowId?: string, adapter?: string): void {
// During transition: use cliSession as new id if it looks like a UUID, else use id
const effectiveId = (cliSession && cliSession !== id && cliSession.includes('-')) ? cliSession : id;
this.upsert(effectiveId, cwd, windowId, id, adapter);
},
```
These aliases are removed in Task 8 (cleanup). This allows Tasks 2-5 to compile at each intermediate step.
- [ ] **Step 6: Wrap migration in transaction**
Ensure the migration SQL in Step 2 is wrapped in a transaction:
```typescript
d.transaction(() => {
d.exec(` ... migration SQL ... `);
})();
```
- [ ] **Step 7: Remove old index creation**
At lines 94-98 of db.ts, the `CREATE INDEX idx_sessions_cli ON sessions(cli_session)` must be removed or guarded (column no longer exists after migration).
- [ ] **Step 8: Commit**
```bash
git add server/db.ts
git commit -m "refactor: migrate sessions table -- CLI UUID as primary key"
```
---
## Phase 2: Adapter Internals
### Task 2: Unify Claude adapter to CLI UUID
**Files:**
- Modify: `server/adapters/claude/tmux-adapter.ts`
- Modify: `server/adapters/claude/index.ts`
- Modify: `server/adapters/interface.ts`
- [ ] **Step 1: Remove translation infrastructure**
In `tmux-adapter.ts`:
- Remove `cliToSessionId: Map<string, string>` field (line 88)
- Remove ALL `this.cliToSessionId.set(...)` calls
- Remove `resolveSessionId()` method (lines 912-970)
- Remove `_registerCliUUID()` (lines 797-805) -- also fixes `claude_session` bug
- Remove `_remapCliSession()` (lines 779-785)
- Remove `_removeCliMapping()` (lines 792-793)
In `interface.ts`: remove `resolveSessionId` method from IAdapter base class.
In `claude/index.ts`: remove `resolveSessionId` delegation (line 219).
- [ ] **Step 2: Change sessions Map key to CLI UUID**
In `startSession()`:
- `const sessionId = cliSessionId` (was `windowName`)
- `this.sessions.set(sessionId, ...)` keyed by CLI UUID
- `dbSessions.upsert(sessionId, cwd, windowId, windowName, 'claude')` -- new signature
- Return `{ sessionId }` -- now CLI UUID
In `resumeSession()`:
- `const newSessionId = cliUuid` (was `'claude-${Date.now()}'`)
- Keep `const windowName = 'claude-${Date.now()}'` for tmux display
- `this.sessions.set(newSessionId, ...)` keyed by CLI UUID
- `dbSessions.upsert(newSessionId, cwd, windowId, windowName, 'claude')`
- Return `{ sessionId: newSessionId }`
In `attachSession()`: same pattern -- use CLI UUID as Map key.
In `handleSessionStart()`: use CLI UUID from hook body directly as Map key.
- [ ] **Step 3: Update _findWindowForSession (line 986)**
Replace window-name matching AND the `findByCliSession` fallback (line 994) with DB PK lookup:
```typescript
const dbRow = dbSessions.get(sessionId);
if (dbRow?.window_id) return dbRow.window_id;
```
Remove `windows.find(w => w.name === sessionId)` -- no longer matching by window name.
- [ ] **Step 4: Update all dbSessions.upsert calls to new signature**
Enumerate ALL call sites in this file and update each from `(id, cliSession, cwd, windowId, adapter)` to `(id, cwd, windowId, windowName, adapter)`:
- Line 136 (startSession)
- Line 192 (attachSession)
- Line 239 (resumeSession)
- Line 572 (handleSessionStart)
- Line 946 (inside resolveSessionId -- goes away when method is deleted)
- [ ] **Step 5: Commit**
```bash
git add server/adapters/claude/tmux-adapter.ts server/adapters/claude/index.ts server/adapters/interface.ts
git commit -m "refactor: Claude adapter uses CLI UUID as session key"
```
---
### Task 3: Unify Codex adapter to CLI UUID + _waitForCliUUID
**Files:**
- Modify: `server/adapters/codex/codex-tmux-adapter.ts`
- Modify: `server/adapters/codex/index.ts`
- Modify: `server/adapters/codex/pane-monitor.ts`
- [ ] **Step 1: Remove translation infrastructure**
Same as Claude: remove `cliToSessionId` Map, `resolveSessionId()`, `_removeCliMapping()`.
In `codex/index.ts`: remove `resolveSessionId` delegation.
- [ ] **Step 2: Add _waitForCliUUID method**
New method that polls `session.cliSessionId` every 500ms (max 15s). When UUID discovered: re-key session in Map, upsert DB, remove temp key. On timeout: kill tmux window, remove temp session, throw error.
- [ ] **Step 3: Update startSession**
Store session under temp `windowName` key initially. After `_waitForReady`, call `await this._waitForCliUUID(windowName)` which returns CLI UUID. Return `{ sessionId: cliUUID }`.
- [ ] **Step 4: Update resumeSession + handleSessionStart + _watchForTranscript**
All use CLI UUID as Map key directly. `handleSessionStart` and `_watchForTranscript` set `session.cliSessionId` (which `_waitForCliUUID` polls for).
- [ ] **Step 5: Update all dbSessions.upsert calls to new signature**
Enumerate ALL call sites in this file:
- Line 135 (startSession)
- Line 189 (resumeSession)
- Line 337 (handleSessionStart)
- Line 753 (_watchForTranscript scanOnce lambda)
- Line 861 (_findAndAttachWindow)
- [ ] **Step 6: Commit**
```bash
git add server/adapters/codex/codex-tmux-adapter.ts server/adapters/codex/index.ts server/adapters/codex/pane-monitor.ts
git commit -m "refactor: Codex adapter uses CLI UUID, add _waitForCliUUID"
```
---
## Phase 3: Session Manager
### Task 4: Unify session-manager to CLI UUID
**Files:**
- Modify: `server/session-manager.ts`
- [ ] **Step 1: Remove all resolveSessionId calls**
In `handleQuery`: remove resolution block. `sessionId` from client is CLI UUID.
In `handleReconnect`: remove resolution block. Use `sessionId` directly as `effectiveId`.
Remove all `(adapter as ...).resolveSessionId?.(...)` casts.
- [ ] **Step 2: Simplify sendSessionCreated**
Send single `sessionId` (CLI UUID). Remove `cliSessionId` field.
- [ ] **Step 3: Simplify handleReconnect**
Preserve all 11 steps. Key changes:
- Step 6: add `hasActiveWindow` guard (prevent creating unwanted tmux windows)
- Step 8: use `sessionId` directly for `getMessages()` (CLI UUID = JSONL key)
- Step 11: use `sessionId` directly for `getActiveForParent()`
- Replace `dbSessions.findByCliSession` with `dbSessions.get`
- Remove dynamic `import('./db.js')` -- use static import
- [ ] **Step 4: Simplify triggerPush**
Single `getSession()` call. Use `sessionId` directly for child review check.
- [ ] **Step 5: Simplify session-ended handler**
`sessionId` IS CLI UUID. Remove convoluted DB lookup for `endedCliId`. Use directly for review cascade.
- [ ] **Step 6: Update dbSessions calls**
Replace all `dbSessions.findByCliSession(...)` with `dbSessions.get(...)`.
- [ ] **Step 7: Commit**
```bash
git add server/session-manager.ts
git commit -m "refactor: session-manager uses CLI UUID for broadcast and registration"
```
---
## Phase 4: Server Endpoints + Frontend
### Task 5: Update server/index.ts
**Files:**
- Modify: `server/index.ts`
- [ ] **Step 1: Replace dbSessions.findByCliSession with dbSessions.get**
All `dbSessions.findByCliSession(...)` calls become `dbSessions.get(...)`.
- [ ] **Step 2: Simplify active-sessions client count**
Replace `getClientCount(s.sessionId) || getClientCount(s.cliSessionId)` with `getClientCount(s.sessionId)`.
- [ ] **Step 3: Update review endpoints to use dbSessions.get**
- [ ] **Step 4: Commit**
```bash
git add server/index.ts
git commit -m "refactor: server endpoints use CLI UUID, remove dual-ID lookups"
```
---
### Task 6: Unify frontend
**Files:**
- Modify: `src/hooks/useChat.ts`
- Modify: `src/components/ChatView.tsx`
- Modify: `src/hooks/useSessions.ts`
- Modify: `src/components/SessionsView.tsx`
- [ ] **Step 1: Merge sessionId + cliSessionId in useChat**
Remove `cliSessionId` state. Keep only `sessionId` (CLI UUID). Remove `setCliSessionId(msg.cliSessionId)` from SESSION_CREATED handler. Remove `cliSessionId` from return.
- [ ] **Step 2: Update ChatView**
Remove `cliSessionId` from useChat destructuring. Use `sessionId` for ChatHeader display and review API calls.
- [ ] **Step 3: Update useSessions**
Change `s.cliSessionId` to `s.sessionId` in `activeSessionIds` builder.
- [ ] **Step 4: Update SessionsView**
Change `pending[session.cliSessionId]` to `pending[session.sessionId]` for notification badges.
- [ ] **Step 5: Commit**
```bash
git add src/hooks/useChat.ts src/components/ChatView.tsx src/hooks/useSessions.ts src/components/SessionsView.tsx
git commit -m "refactor: frontend uses single sessionId (CLI UUID)"
```
---
## Phase 5: CLI + Cleanup
### Task 7: Update bin/codetap
**Files:**
- Modify: `bin/codetap`
- [ ] **Step 1: Update get_project_sessions() SQL (line 239)**
Change `SELECT id FROM sessions` to `SELECT window_name FROM sessions` -- returns tmux window names for matching.
- [ ] **Step 2: Update -a listing SQL (line 290)**
Change to `SELECT id, adapter, window_name, cwd FROM sessions WHERE window_name IN (...)`.
- [ ] **Step 3: Update --resume SQL (line 382)**
Change to `WHERE id='${SAFE_ID}' OR window_name='${SAFE_ID}'` -- accepts both CLI UUID and window name (backwards compatible for users who may pass old-style IDs).
- [ ] **Step 4: Commit**
```bash
git add bin/codetap
git commit -m "refactor: bin/codetap uses new DB schema"
```
---
### Task 8: Final cleanup -- remove deprecated aliases, verify all files
**Files:**
- Modify: `server/db.ts`
- Modify: `server/adapters/interface.ts`
- Modify: `tests/e2e-spec.feature`
- [ ] **Step 1: Remove deprecated DB method aliases**
In `server/db.ts`, remove `findByCliSession` and `upsertLegacy` deprecated aliases added in Task 1. Grep to verify zero remaining callers.
- [ ] **Step 2: Deprecate ActiveSessionInfo.cliSessionId**
In `server/adapters/interface.ts`, add `/** @deprecated Use sessionId instead */` comment.
- [ ] **Step 3: Verify no-op files from spec**
These files are listed in the spec as MODIFY but require no code changes (already use generic string params). Verify each with grep:
- `server/push.ts` -- callers now pass CLI UUID. NO CHANGE needed.
- `server/permission-manager.ts` -- callers now pass CLI UUID. NO CHANGE needed.
- `server/types/messages.ts` -- `QueryOptions.sessionId` is generic. NO CHANGE.
- `server/types/adapter.ts` -- `SessionInfo.sessionId` already CLI UUID. NO CHANGE.
- `src/lib/ws.ts`, `src/lib/api.ts`, `src/sw.ts`, `src/App.tsx`, `src/components/FloatingReviewPanel.tsx` -- NO CHANGE.
- [ ] **Step 4: Verify session_stats table**
Confirm `session_stats` table is never written to (no INSERT statements). No migration needed.
- [ ] **Step 5: Update e2e specs**
Remove references to dual-ID system, `resolveSessionId`, `cliSessionId` as separate concept.
- [ ] **Step 6: TypeScript compilation check**
`npx tsc --noEmit` -- zero errors.
- [ ] **Step 7: Commit**
```bash
git add -A
git commit -m "refactor: cleanup -- remove deprecated aliases, verify all files, update e2e specs"
```
---
## Verification
After all tasks:
1. Server starts, migration runs without errors
2. Open historical session from project list -- history loads immediately (no 30s wait)
3. Open session from active list -- connects correctly
4. Send message -- goes to correct tmux window
5. Desktop opens same session -- mobile receives streaming events in real-time
6. Push notification click -- navigates to correct session
7. Cross-AI Review -- create, chat, send-back, end -- all work
8. `bin/codetap -a` -- lists sessions correctly
9. `bin/codetap --resume <UUID>` -- resumes correctly
10. Server restart -- sessions re-discovered, reviews survive