Files
clawtap/docs/superpowers/plans/2026-03-24-codex-uuid-discovery-fix.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

17 KiB

Codex UUID Discovery Fix + Session Architecture Cleanup — 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: Fix Codex startSession deadlock, replace guess-based matching with JSONL marker matching, kill tmux windows on shutdown, remove DB sessions table.

Architecture: 6 tasks: (1) remove _waitForCliUUID + add marker matching in Codex adapter, (2) inject marker in session-manager + index.ts, (3) filter marker in frontend, (4) shutdown kills tmux + remove _findAndAttachWindow, (5) remove DB sessions table, (6) simplify handleReconnect + review endpoints.

Spec: docs/superpowers/specs/2026-03-24-codex-uuid-discovery-fix.md


Task 1: Codex adapter — remove deadlock, add marker matching

Files:

  • Modify: server/adapters/codex/codex-tmux-adapter.ts

  • Step 1: Delete _waitForCliUUID method

Remove the entire method. Also remove the call to it in startSession() and the renameWindow call after it. startSession now returns temp key immediately:

return { sessionId: tempName };
  • Step 2: Delete _rekeySession method (if still exists from earlier)

This was added in a previous fix. Will be replaced by _rekeyAndRename.

  • Step 3: Add _rekeyAndRename method
private async _rekeyAndRename(tempKey: string, cliUuid: string): Promise<void> {
  const session = this.sessions.get(tempKey);
  if (!session) return;
  session.cliSessionId = cliUuid;
  session._watcherPending = false;
  this.sessions.delete(tempKey);
  this.sessions.set(cliUuid, session);
  await tmuxManager.renameWindow(session.windowId, cliUuid);
  if (session.monitor) {
    (session.monitor as any).sessionId = cliUuid;
  }
}

Note: NO dbSessions calls here — DB sessions table will be removed in Task 5.

  • Step 4: Add _matchByTranscriptMarker method

Reads JSONL at given path, finds [CODETAP_REF:xxx] in first user message, returns xxx if it's a key in this.sessions:

private _matchByTranscriptMarker(transcriptPath: string): string | null {
  try {
    const content = readFileSync(transcriptPath, 'utf8');
    const lines = content.split('\n').filter(Boolean);
    for (const line of lines) {
      try {
        const entry = JSON.parse(line);
        // Check for user message content containing marker
        // NOTE: Read codex transcript-parser.ts to get correct field names
        const text = this._extractTextFromEntry(entry);
        if (text) {
          const match = text.match(/\[CODETAP_REF:([^\]]+)\]/);
          if (match && this.sessions.has(match[1])) return match[1];
        }
      } catch {}
    }
  } catch {}
  return null;
}

Add a helper _extractTextFromEntry that handles the Codex JSONL format (check transcript-parser.ts for the actual field structure).

  • Step 5: Rewrite handleSessionStart matching

Replace pendingSessions.length === 1 logic:

1. Direct lookup: this.sessions.has(codexUuid) → already managed → update state, return
2. Marker matching: _matchByTranscriptMarker(body.transcript_path) → found tempKey → _rekeyAndRename(tempKey, codexUuid), start watcher, return
3. Fallback pending scan: pendingSessions.length === 1 → legacy (kept for sessions without marker)
4. No match: create session entry for this UUID (desktop/unknown origin)

In step 4, do NOT call _findAndAttachWindow (will be deleted in Task 4). Instead, try matching by tmux window name:

const windows = await tmuxManager.listWindows();
const match = windows.find(w => w.name === codexUuid);
if (match) {
  session.windowId = match.id;
  this._startMonitor(codexUuid, match.id);
}
  • Step 6: Update _watchForTranscript with marker verification

In scanOnce, after finding a JSONL file candidate, verify it belongs to this session:

const firstChunk = readFileSync(fullPath, 'utf8').slice(0, 2000);
if (!firstChunk.includes(`CODETAP_REF:${sessionId}`)) continue;

When match confirmed, call _rekeyAndRename(sessionId, uuid).

  • Step 7: Commit
git add server/adapters/codex/codex-tmux-adapter.ts
git commit -m "fix: remove _waitForCliUUID deadlock, add marker-based matching"

Task 2: Inject CODETAP_REF marker in session-manager + index.ts

Files:

  • Modify: server/session-manager.ts

  • Modify: server/index.ts

  • Step 1: Inject marker in handleQuery for new sessions

In handleQuery(), after startSession returns and before sendMessage:

let messageText = prompt;
if (!sessionId) {
  // New session — prepend marker for Codex UUID matching
  messageText = `[CODETAP_REF:${handle.sessionId}]\n${prompt}`;
}
await adapter.sendMessage(handle.sessionId, messageText, { clientId: conn.clientId });

For Claude, handle.sessionId is already a UUID — marker is harmless, just filtered out in ChatView.

  • Step 2: Inject marker in POST /api/reviews context
if (context) {
  const markerContext = `[CODETAP_REF:${handle.sessionId}]\n${context}`;
  await adapter.pasteToSession(handle.sessionId, markerContext);
}
  • Step 3: Commit
git add server/session-manager.ts server/index.ts
git commit -m "feat: inject CODETAP_REF marker in first message for UUID matching"

Task 3: Filter marker in frontend

Files:

  • Modify: src/lib/content-utils.ts

  • Modify: src/hooks/useChat.ts

  • Step 1: Add stripMarker to content-utils.ts

const CODETAP_REF_REGEX = /^\[CODETAP_REF:[^\]]+\]\n?/;

export function stripMarker(text: string): string {
  return text.replace(CODETAP_REF_REGEX, '');
}
  • Step 2: Strip marker in convertMessages (useChat.ts)

Import stripMarker from @/lib/content-utils. In convertMessages(), when processing user messages, strip marker from text content blocks:

if (msg.role === 'user') {
  const content = typeof msg.content === 'string'
    ? [{ type: 'text', text: stripMarker(msg.content) }]
    : (msg.content || []).map((b: any) =>
        b.type === 'text' ? { ...b, text: stripMarker(b.text || '') } : b
      );
  // ... rest of processing
}
  • Step 3: Commit
git add src/lib/content-utils.ts src/hooks/useChat.ts
git commit -m "feat: strip CODETAP_REF marker from user messages in ChatView"

Task 4: Shutdown kills tmux + remove _findAndAttachWindow

Files:

  • Modify: server/adapters/claude/tmux-adapter.ts

  • Modify: server/adapters/codex/codex-tmux-adapter.ts

  • Step 1: Codex destroy() kills tmux session

Claude's destroy() already has tmuxManager.killSession() (added in earlier task). Add the same to Codex's destroy():

await tmuxManager.killSession();

Note: Both adapters share the same tmuxManager. Calling killSession() twice is harmless (catch block swallows error).

  • Step 3: Delete _findAndAttachWindow from Codex adapter

Remove the entire _findAndAttachWindow method. Remove all call sites (in handleSessionStart fallback path).

  • Step 4: Commit
git add server/adapters/claude/tmux-adapter.ts server/adapters/codex/codex-tmux-adapter.ts
git commit -m "feat: shutdown kills tmux windows, remove _findAndAttachWindow"

Task 5: Remove DB sessions table

Files:

  • Modify: server/db.ts

  • Modify: server/adapters/claude/tmux-adapter.ts

  • Modify: server/adapters/codex/codex-tmux-adapter.ts

  • Modify: server/session-manager.ts

  • Modify: server/index.ts

  • Modify: bin/codetap

  • Step 1: Add parent_adapter to session_reviews table

In server/db.ts, add parent_adapter TEXT NOT NULL DEFAULT 'claude' to the session_reviews CREATE TABLE.

Add migration: if parent_adapter column doesn't exist, add it:

ALTER TABLE session_reviews ADD COLUMN parent_adapter TEXT NOT NULL DEFAULT 'claude';

Update SessionReviewRow interface to include parent_adapter: string.

Update sessionReviews.create() to accept and store parentAdapter.

Update POST /api/reviews in server/index.ts to pass the parent's adapter name when creating a review.

  • Step 2: Remove sessions table from db.ts

Delete:

  • CREATE TABLE IF NOT EXISTS sessions from initDB
  • Old schema detection/drop logic (PRAGMA table_info, hasOldColumns)
  • sessionsUpsert, sessionsGet, sessionsFindByWindowId, sessionsRemove, sessionsGetAll prepared statements from PreparedStatements interface and stmts() function
  • SessionRow interface
  • sessions export object
  • CREATE INDEX idx_sessions_window

Keep: everything related to session_reviews.

  • Step 2: Remove all dbSessions calls from Claude adapter

Grep for dbSessions in tmux-adapter.ts. Remove every call (9 sites):

  • dbSessions.upsert(...) in startSession (line 131), attachSession (line 181), resumeSession (line 224), handleSessionStart (line 547)
  • dbSessions.remove(...) in handleSessionEnd (line 500), cleanup loop (line 707)
  • dbSessions.get(...) in handleSessionStart (line 532), _findWindowForSession (line 875)

Also remove the import { sessions as dbSessions } from '../../db.js' line.

NOTE: _findWindowForSession currently does dbSessions.get(sessionId) first, then falls back to windows.find(w => w.name === sessionId). After removing DB, keep ONLY the window name matching fallback.

  • Step 3: Remove all dbSessions calls from Codex adapter

Same pattern. Grep and remove all dbSessions.* calls. Remove import.

  • Step 4: Remove dbSessions from session-manager.ts

Remove:

  • import { sessions as dbSessions } from './db.js' (keep sessionReviews import)

  • dbSessions.get(sessionId) in handleReconnect (cwd lookup — no longer needed)

  • dbSessions.get(review.parent_cli_session_id) in review restoration (Task 6 changes this)

  • Step 5: Remove dbSessions from index.ts

Remove:

  • dbSessions.clearAll() from shutdown handler

  • dbSessions.get(parentCliSessionId) from review endpoints (Task 6 changes these)

  • Import of sessions as dbSessions

  • Step 6: Update bin/codetap

Remove all SQL queries that reference the sessions table:

  • get_project_sessions() function — queries sessions by cwd
  • -a listing block — queries sessions by window name
  • --resume block — queries sessions by id

These CLI features will stop working without the DB. Options: a) Remove these features from bin/codetap (they depend on DB) b) Use tmux list-windows directly instead of DB queries

Recommended: option (b) — replace DB queries with tmux-based queries:

get_project_sessions():

tmux list-windows -t codetap -F '#{window_name}\t#{pane_current_path}' 2>/dev/null | \
  awk -F'\t' -v cwd="$CWD" '$2 == cwd { print $1 }'

-a listing:

tmux list-windows -t codetap -F '#{window_name}\t#{pane_current_command}\t#{pane_current_path}' 2>/dev/null
# window_name = UUID, pane_current_command = "claude" or "codex" (adapter detection)

--resume:

# Check if UUID exists as a tmux window name
tmux list-windows -t codetap -F '#{window_name}' 2>/dev/null | grep -q "^${RESUME_ID}$"
# Detect adapter from pane command
ADAPTER=$(tmux display -t "codetap:${RESUME_ID}" -p '#{pane_current_command}' 2>/dev/null)
  • Step 7: TypeScript compilation check

npx tsc --noEmit — zero errors.

  • Step 8: Commit
git add server/db.ts server/adapters/claude/tmux-adapter.ts server/adapters/codex/codex-tmux-adapter.ts server/session-manager.ts server/index.ts bin/codetap
git commit -m "refactor: remove DB sessions table — in-memory Map is sole source of truth"

Task 6: Simplify handleReconnect + review endpoints use Map for cwd

Files:

  • Modify: server/session-manager.ts

  • Modify: server/index.ts

  • Step 1: Simplify handleReconnect

Remove the entire hasActiveWindow + resumeSession block:

// BEFORE:
if (!adapter.getSession(sessionId)) {
  const hasWindow = await adapter.hasActiveWindow(sessionId);
  if (hasWindow) {
    const dbRow = dbSessions.get(sessionId);
    try { await adapter.resumeSession(sessionId, dbRow?.cwd || ''); } catch {}
  }
}

// AFTER:
// (deleted — handleReconnect only loads history, handleQuery builds windows)
  • Step 2: Review endpoints use adapter.getSession for cwd

In POST /api/reviews:

// BEFORE:
const parentRow = dbSessions.get(parentCliSessionId);
const cwd = parentRow?.cwd || process.cwd();

// AFTER:
const parentSession = adapter.getSession(parentCliSessionId) as { cwd?: string } | null;
const cwd = parentSession?.cwd || process.cwd();

In POST /api/reviews/:id/send-back and DELETE /api/reviews/🆔

// BEFORE:
const parentRow = dbSessions.get(review.parent_cli_session_id);
const parentAdapter = getAdapter(parentRow?.adapter || DEFAULT_ADAPTER);

// AFTER: parent_adapter is now stored in session_reviews
const parentAdapter = getAdapter(review.parent_adapter);

No need to iterate adapters — parent_adapter is directly in the review row.

In handleReconnect review restoration:

// BEFORE:
const parentRow = dbSessions.get(review.parent_cli_session_id);
const cwd = parentRow?.cwd || '';
await childAdapterObj.resumeSession(review.child_cli_session_id, cwd);

// AFTER: don't call resumeSession — if server didn't restart, child is still in Map.
// If server restarted, windows are dead, review should be marked ended.
if (!childAdapterObj.getSession(review.child_cli_session_id)) {
  // Child session gone (server restarted + windows killed) → mark review ended
  sessionReviews.endReview(review.id);
  continue;
}
// Child still alive → just send REVIEW_STARTED event, child's useChat reconnects itself
  • Step 3: Commit
git add server/session-manager.ts server/index.ts
git commit -m "refactor: handleReconnect simplified, review endpoints use in-memory Map"

Self-Review Checklist

Compilation Safety

  • Task 1 removes _waitForCliUUID calls → startSession returns temp key → compiles
  • Task 5 removes dbSessions → all callers must be updated in same task → grep to verify zero remaining references
  • Task 6 depends on Task 5 (dbSessions already removed) → correct ordering

Codex UUID Discovery Flow (after fix)

1. startSession → return tempKey immediately
2. handleQuery/reviews → paste [CODETAP_REF:tempKey] + prompt
3. Codex processes → SessionStart hook fires (or JSONL appears)
4. handleSessionStart → read JSONL → find CODETAP_REF:tempKey → match
5. _rekeyAndRename(tempKey, uuid) → Map re-key + tmux rename
6. From now on: session ID = UUID = tmux window name

handleReconnect Flow (after fix)

User clicks session → RECONNECT
1. registerClient(conn, sessionId)
2. load JSONL history → HISTORY_LOAD
3. replay pending state
4. NO resumeSession, NO cwd lookup, NO DB query
5. If user sends message → handleQuery → resumeSession → builds tmux window

Review Endpoints (after fix)

  • POST /api/reviews: cwd from adapter.getSession(parentId).cwd (parent is active, must be in Map)
  • send-back: find parent adapter by iterating getAllAdapters(), check adapter.getSession(reviewParentId)
  • delete: same pattern

bin/codetap (after fix)

  • -a listing: tmux list-windows directly instead of DB query
  • --resume: tmux list-windows to find window by name (= UUID)
  • get_project_sessions: tmux list-windows + filter by pane_current_path

Things NOT changed

  • session_reviews DB table — stays (Cross-AI Review needs it)
  • In-memory sessions Map — stays (runtime state store)
  • JSONL files — untouched (historical record)
  • Push notifications — untouched
  • Permission manager — untouched

Issues Found in Self-Review (all addressed in plan)

  • Claude's destroy() already has killSession() — only Codex needs it added (Task 4 updated)
  • Claude's _findWindowForSession has DB-first check — keep only name-matching fallback (Task 5 Step 2 noted)
  • Review restoration in handleReconnect: don't call resumeSession, just check if child exists or mark ended (Task 6 Step 2 updated)
  • send-back/delete review endpoints: added parent_adapter column to session_reviews — direct lookup, no iteration (Task 5 Step 1)
  • bin/codetap --resume detects adapter from pane_current_command (Task 5 Step 6 updated)
  • dbSessions has 26 call sites across 4 files — all enumerated in Task 5

Verification

  1. Server starts without sessions table in DB
  2. New Claude session from Web UI → works (marker injected, UUID known immediately)
  3. New Codex session from Web UI → works (marker injected, UUID discovered via hook, tmux renamed)
  4. Cross-AI Review → works (marker in context, child session matched, floating panel opens)
  5. Server shutdown → all tmux windows killed
  6. Server restart → clean state, no stale windows
  7. bin/codetap -a → lists sessions from tmux directly
  8. bin/codetap --resume <UUID> → works
  9. Historical session click → loads history (no 30s wait, no resumeSession)
  10. ChatView shows no [CODETAP_REF:...] markers