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

352 lines
12 KiB
Markdown

# 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**
```typescript
private _pendingHookBodies: Map<string, CodexHookBody> = new Map();
```
- [ ] **Step 2: Rewrite handleSessionStart (line 275)**
Replace the entire method body:
```typescript
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:
```typescript
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:
```typescript
// 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**
```bash
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:
```typescript
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**
```bash
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):
```typescript
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**
```typescript
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**
```typescript
export function registerSessionAdapter(sessionId: string, adapterName: string): void {
sessionAdapterMap.set(sessionId, adapterName);
}
```
- [ ] **Step 4: Commit**
```bash
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):
```bash
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:
```bash
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**
```bash
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:
```bash
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**
```bash
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)