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
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
_waitForCliUUIDmethod
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
_rekeySessionmethod (if still exists from earlier)
This was added in a previous fix. Will be replaced by _rekeyAndRename.
- Step 3: Add
_rekeyAndRenamemethod
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
_matchByTranscriptMarkermethod
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
handleSessionStartmatching
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
_watchForTranscriptwith 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 sessionsfrom initDB- Old schema detection/drop logic (
PRAGMA table_info,hasOldColumns) sessionsUpsert,sessionsGet,sessionsFindByWindowId,sessionsRemove,sessionsGetAllprepared statements fromPreparedStatementsinterface andstmts()functionSessionRowinterfacesessionsexport objectCREATE 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'(keepsessionReviewsimport) -
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-alisting block — queries sessions by window name--resumeblock — 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
_waitForCliUUIDcalls →startSessionreturns 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:
cwdfromadapter.getSession(parentId).cwd(parent is active, must be in Map) - send-back: find parent adapter by iterating
getAllAdapters(), checkadapter.getSession(reviewParentId) - delete: same pattern
bin/codetap (after fix)
-alisting:tmux list-windowsdirectly instead of DB query--resume:tmux list-windowsto find window by name (= UUID)get_project_sessions:tmux list-windows+ filter bypane_current_path
Things NOT changed
session_reviewsDB table — stays (Cross-AI Review needs it)- In-memory
sessionsMap — 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 haskillSession()— only Codex needs it added (Task 4 updated) - Claude's
_findWindowForSessionhas 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/deletereview endpoints: addedparent_adaptercolumn tosession_reviews— direct lookup, no iteration (Task 5 Step 1)bin/codetap --resumedetects adapter frompane_current_command(Task 5 Step 6 updated)- dbSessions has 26 call sites across 4 files — all enumerated in Task 5
Verification
- Server starts without sessions table in DB
- New Claude session from Web UI → works (marker injected, UUID known immediately)
- New Codex session from Web UI → works (marker injected, UUID discovered via hook, tmux renamed)
- Cross-AI Review → works (marker in context, child session matched, floating panel opens)
- Server shutdown → all tmux windows killed
- Server restart → clean state, no stale windows
bin/codetap -a→ lists sessions from tmux directlybin/codetap --resume <UUID>→ works- Historical session click → loads history (no 30s wait, no resumeSession)
- ChatView shows no
[CODETAP_REF:...]markers