Files
clawtap/docs/superpowers/plans/2026-03-24-remaining-session-fixes.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

12 KiB

Remaining Session Fixes — 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.

Goal: Complete session architecture cleanup: remove pending guessing, remove desktop-discovery, add session API endpoints, update bin/codetap.

Spec: docs/superpowers/specs/2026-03-24-remaining-session-fixes.md


Task 1: Codex handleSessionStart — remove pending matching, add _pendingHookBodies

Files: server/adapters/codex/codex-tmux-adapter.ts

  • Step 1: Add _pendingHookBodies field
private _pendingHookBodies: Map<string, CodexHookBody> = new Map();
  • Step 2: Rewrite handleSessionStart (line 275)

Replace the entire method body:

handleSessionStart(body: CodexHookBody): void {
  const codexUuid = body.session_id;
  if (!codexUuid) return;

  // 1. Already managed
  if (this.sessions.has(codexUuid)) {
    this._applySessionStartBody(codexUuid, body);
    return;
  }

  // 2. Has pending sessions → store hook body, let _watchForTranscript match later
  const hasPending = [...this.sessions.values()].some(s => s._watcherPending);
  if (hasPending) {
    this._pendingHookBodies.set(codexUuid, body);
    return;
  }

  // 3. Not our session → ignore
}

Remove the old pendingSessions.length === 1 block (lines 297-308). Remove the "desktop/unknown origin" block (lines 309-330).

  • Step 3: Update _watchForTranscript to read _pendingHookBodies after rekey

In the scanOnce function, after _rekeyAndRename(sessionId, uuid) succeeds, check for stored hook body:

const hookBody = this._pendingHookBodies.get(uuid);
if (hookBody) {
  this._applySessionStartBody(uuid, hookBody);
  this._pendingHookBodies.delete(uuid);
}
  • Step 4: Add cleanup for _pendingHookBodies

In _startSessionCleanup interval, add a sweep:

// Clean up stale pending hook bodies (older than 60s)
const now = Date.now();
for (const [uuid, body] of this._pendingHookBodies) {
  // Use a timestamp field or just clean up all entries periodically
  this._pendingHookBodies.delete(uuid);
}

Actually simpler: clean up in _pendingHookBodies when adding — if size > 10, delete oldest. Or just clear all entries older than 60s by storing a timestamp alongside.

  • Step 5: Commit
git commit -m "refactor: Codex handleSessionStart uses _pendingHookBodies, no pending guessing"

Task 2: Remove desktop-discovery from both adapters

Files: server/adapters/claude/tmux-adapter.ts, server/adapters/codex/codex-tmux-adapter.ts

  • Step 1: Claude — simplify handleSessionStart (line 512)

Current code (lines 512-539) does:

  1. sessions.has(cliUuid) → update lastActivity → return
  2. List tmux windows → search for w.command.includes('claude') && !sessions.has(w.name) → create session

Remove step 2 entirely. The method becomes:

async handleSessionStart(body: HookBody): Promise<void> {
  const cliUuid = body.session_id;
  if (!cliUuid) return;

  if (this.sessions.has(cliUuid)) {
    this.sessions.get(cliUuid)!.lastActivity = Date.now();
    return;
  }

  // Unknown UUID — not our session, ignore
}

Also remove the await tmuxManager.listWindows() call (no longer needed).

  • Step 2: Codex — verify desktop-discovery already removed in Task 1

Check that Task 1's rewrite of handleSessionStart has no desktop-discovery path.

  • Step 3: Commit
git commit -m "refactor: remove desktop-discovery from both adapters"

Task 3: Add POST /api/sessions/start and /resume endpoints

Files: server/index.ts

  • Step 1: Add POST /api/sessions/start

Place after the existing session endpoints (after DELETE /api/active-sessions/:id):

app.post('/api/sessions/start', authMiddleware, async (req: Request, res: Response) => {
  try {
    const { adapter: adapterName, cwd, model, permissionMode } = req.body;
    if (!cwd) return res.status(400).json({ error: 'cwd required' });

    const adapter = getAdapter(adapterName || DEFAULT_ADAPTER);
    if (!adapter) return res.status(400).json({ error: `Unknown adapter: ${adapterName}` });

    const handle = await adapter.startSession(cwd, { model, permissionMode });

    // Register in sessionAdapterMap so events route correctly
    sessionAdapterMap.set(handle.sessionId, adapterName || DEFAULT_ADAPTER);

    res.json({ sessionId: handle.sessionId });
  } catch (error) {
    res.status(500).json({ error: (error as Error).message });
  }
});

Note: import sessionAdapterMap — check if it's already accessible. It's a module-level variable in session-manager.ts. May need to export a helper function registerSessionAdapter(sessionId, adapterName) from session-manager.

Actually, looking at the code: sessionAdapterMap is defined in session-manager.ts as a module-level const. It's NOT exported. The handleQuery function accesses it directly because it's in the same file.

For server/index.ts to set it, we need either: a) Export sessionAdapterMap from session-manager.ts b) Add a registerSessionAdapter(id, name) helper exported from session-manager.ts c) Have startSession trigger an event that session-manager listens to

Option (b) is cleanest.

  • Step 2: Add POST /api/sessions/resume
app.post('/api/sessions/resume', authMiddleware, async (req: Request, res: Response) => {
  try {
    const { sessionId, adapter: adapterName, cwd } = req.body;
    if (!sessionId) return res.status(400).json({ error: 'sessionId required' });

    // Determine adapter if not provided
    let resolvedAdapterName = adapterName;
    if (!resolvedAdapterName) {
      // Try to detect from JSONL file location
      // ... (use existing findSessionFile logic from each adapter's jsonl-store)
      resolvedAdapterName = DEFAULT_ADAPTER;
    }

    const adapter = getAdapter(resolvedAdapterName);
    if (!adapter) return res.status(400).json({ error: `Unknown adapter: ${resolvedAdapterName}` });

    const handle = await adapter.resumeSession(sessionId, cwd || process.cwd());

    registerSessionAdapter(handle.sessionId, resolvedAdapterName);

    res.json({ sessionId: handle.sessionId });
  } catch (error) {
    res.status(500).json({ error: (error as Error).message });
  }
});
  • Step 3: Export registerSessionAdapter from session-manager.ts
export function registerSessionAdapter(sessionId: string, adapterName: string): void {
  sessionAdapterMap.set(sessionId, adapterName);
}
  • Step 4: Commit
git commit -m "feat: add POST /api/sessions/start and /resume endpoints"

Task 4: Update bin/codetap to use API endpoints

Files: bin/codetap

NOTE: sqlite3 references were already removed in Fix 5. This task replaces direct tmux new-window calls with API calls.

  • Step 1: Add authentication function

Near the top of the script (after the server-running check):

get_auth_token() {
  curl -sk -X POST "https://localhost:$PORT/api/auth/login" \
    -H "Content-Type: application/json" \
    -d "{\"password\":\"$CLAUDE_UI_PASSWORD\"}" 2>/dev/null | \
    python3 -c 'import sys,json; print(json.load(sys.stdin).get("token",""))' 2>/dev/null
}
  • Step 2: Replace new session creation

Find the block that does tmux new-window ... "$COMMAND". Replace with:

AUTH_TOKEN=$(get_auth_token)
if [ -z "$AUTH_TOKEN" ]; then
  echo "Error: Failed to authenticate with CodeTap server"
  exit 1
fi

RESULT=$(curl -sk -X POST "https://localhost:$PORT/api/sessions/start" \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"adapter\":\"$ADAPTER\",\"cwd\":\"$(pwd)\"}")
SESSION_ID=$(echo "$RESULT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("sessionId",""))' 2>/dev/null)

if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "null" ]; then
  echo "Error: Failed to create session"
  echo "$RESULT"
  exit 1
fi

tmux select-window -t "$TMUX_SESSION:$SESSION_ID"
  • Step 3: Replace --resume with API call
AUTH_TOKEN=$(get_auth_token)
RESULT=$(curl -sk -X POST "https://localhost:$PORT/api/sessions/resume" \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"sessionId\":\"$RESUME_ID\",\"adapter\":\"$ADAPTER\",\"cwd\":\"$(pwd)\"}")
SESSION_ID=$(echo "$RESULT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("sessionId",""))' 2>/dev/null)
tmux select-window -t "$TMUX_SESSION:$SESSION_ID"
  • Step 4: Replace --continue

Find most recent tmux window, resume it:

LATEST=$(tmux list-windows -t "$TMUX_SESSION" -F '#{window_activity} #{window_name}' 2>/dev/null | sort -rn | head -1 | awk '{print $2}')
if [ -n "$LATEST" ] && [ "$LATEST" != "main" ]; then
  tmux select-window -t "$TMUX_SESSION:$LATEST"
else
  echo "No active sessions to continue"
  exit 1
fi
  • Step 5: Verify -a listing uses tmux directly

Should already be tmux-based (from Fix 5). Verify no remaining sqlite3 references.

  • Step 6: Commit
git commit -m "refactor: bin/codetap uses API endpoints for session creation"

Self-Review Checklist

Compilation safety

  • Task 1 changes only Codex adapter internals → compiles independently
  • Task 2 simplifies Claude handleSessionStart → compiles independently
  • Task 3 adds new endpoints, needs registerSessionAdapter export → export first, then add endpoints
  • Task 4 is shell script only → no compilation

Codex _watchForTranscript flow after Task 1

1. startSession → temp key in Map, _watcherPending = true
2. pasteToSession → marker + prompt pasted
3. SessionStart hook fires → has pending → stored in _pendingHookBodies
4. _watchForTranscript detects JSONL → reads marker → matches temp key
5. _rekeyAndRename(tempKey, uuid) → rekey + rename
6. Read _pendingHookBodies(uuid) → apply transcript_path, cwd
7. Start JSONL watcher

All steps covered

Claude handleSessionStart after Task 2

handleSessionStart(body):
  sessions.has(uuid) → true → update → return
  → false → ignore

Two lines. Very simple. No matching, no discovery.

bin/codetap after Task 4

  • new: API call → tmux select-window
  • --resume: API call → tmux select-window
  • --continue: tmux list-windows → select most recent
  • -a: tmux list-windows directly
  • No sqlite3 references
  • Requires server running + password (already a requirement)

Edge cases

  • bin/codetap when server is down: API calls fail → script shows error → user knows server needs to be running. This is acceptable since CodeTap server is required for all functionality.
  • Multiple pending sessions with same UUID in _pendingHookBodies: Won't happen — UUIDs are unique per CLI session.
  • _pendingHookBodies grows unbounded: Mitigated by cleanup in _startSessionCleanup (60s sweep).
  • bin/codetap Codex new session — temp key returned: Script does tmux select-window -t codetap:codex-{timestamp}. After rekey, window renamed to UUID. User is already inside — unaffected.

No changes needed

  • server/session-manager.ts — only needs registerSessionAdapter export (Task 3)
  • server/db.ts — no changes
  • Frontend — no changes
  • server/adapters/claude/tmux-manager.ts — no changes

Verification

  1. Server starts cleanly
  2. New Codex session from Web UI → hook stored in _pendingHookBodies → _watchForTranscript matches → rekey
  3. New Claude session from Web UI → works (no matching needed)
  4. bin/codetap new --adapter claude → API call → session created → window selected
  5. bin/codetap new --adapter codex → API call → session created → window selected
  6. bin/codetap --resume UUID → API call → session resumed
  7. bin/codetap -a → lists sessions from tmux
  8. Desktop-started sessions (user runs claude/codex directly) → hooks ignored by CodeTap (expected)