---
phase: 01-session-process-foundation
plan: 03
type: execute
wave: 2
depends_on: ["01-01", "01-02"]
files_modified:
  - telegram/bot.py
autonomous: false

must_haves:
  truths:
    - "User sends /new myproject in Telegram and receives confirmation that session was created"
    - "User sends /new myproject brainstorm and session is created with brainstorm persona"
    - "User sends /session myproject in Telegram and active session switches"
    - "User sends plain text with no active session and gets prompted to create one"
    - "User sends plain text with active session and message is routed to ClaudeSubprocess"
    - "/new with duplicate name returns friendly error, not crash"
    - "No zombie processes after switching sessions"
  artifacts:
    - path: "telegram/bot.py"
      provides: "Bot with /new and /session commands wired to session manager and subprocess"
      contains: "SessionManager"
  key_links:
    - from: "telegram/bot.py"
      to: "telegram/session_manager.py"
      via: "SessionManager import and method calls"
      pattern: "from telegram\\.session_manager import|SessionManager"
    - from: "telegram/bot.py"
      to: "telegram/claude_subprocess.py"
      via: "ClaudeSubprocess import for process spawning"
      pattern: "from telegram\\.claude_subprocess import|ClaudeSubprocess"
    - from: "telegram/bot.py /new handler"
      to: "SessionManager.create_session"
      via: "direct method call"
      pattern: "create_session"
    - from: "telegram/bot.py /session handler"
      to: "SessionManager.switch_session"
      via: "direct method call"
      pattern: "switch_session"
---

<objective>
Wire the session manager and Claude subprocess modules into the existing Telegram bot, adding `/new` and `/session` commands and routing plain text messages to the active session's Claude Code subprocess.

Purpose: This plan connects the foundation pieces (Plans 01 and 02) to the existing Telegram bot. After this plan, users can create sessions, switch between them, and send messages that spawn Claude Code subprocesses. This completes Phase 1's core goal: path-based sessions with subprocess management.

Output: Updated `telegram/bot.py` with new command handlers and session-aware message routing.
</objective>

<execution_context>
@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md
@/home/mikkel/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-session-process-foundation/01-CONTEXT.md
@.planning/phases/01-session-process-foundation/01-RESEARCH.md
@.planning/phases/01-session-process-foundation/01-01-SUMMARY.md
@.planning/phases/01-session-process-foundation/01-02-SUMMARY.md
@telegram/bot.py
@telegram/session_manager.py
@telegram/claude_subprocess.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add /new and /session commands to bot.py</name>
  <files>telegram/bot.py</files>
  <action>
Modify the existing `telegram/bot.py` to add session management commands and wire up the Claude subprocess for message handling.

**New imports to add:**
```python
from telegram.session_manager import SessionManager
from telegram.claude_subprocess import ClaudeSubprocess
```

**Module-level state:**
- Create a `SessionManager` instance (singleton for the bot process)
- Create a dict `subprocesses: dict[str, ClaudeSubprocess]` to track subprocess per session — **CRITICAL: this dict persists ALL session subprocesses, not just the active one. When switching from session A to B, the subprocess for A stays alive in `subprocesses['A']` with status "suspended" via SessionManager. This implements the locked decision: "Switching sessions suspends (not kills) the current process — it stays alive. No limit on concurrent live Claude Code processes."**

**New command: `/new <name> [persona]`**

Handler: `async def new_session(update, context)`
- Extract name from `context.args[0]` (required)
- Extract persona from `context.args[1]` if provided (optional)
- Validate: if no args, reply with usage: "Usage: /new <name> [persona]"
- Call `session_manager.create_session(name, persona=persona)`
- On ValueError (duplicate name): reply "Session '<name>' already exists. Use /session <name> to switch to it."
- On success:
  - Auto-switch to new session: call `session_manager.switch_session(name)`
  - Reply "Session '<name>' created." (include persona name if specified: "Session '<name>' created with persona '<persona>'.")
  - Do NOT auto-spawn subprocess yet (spawn happens on first message, per CONTEXT.md: "Switching to a session with no running process auto-spawns Claude Code immediately" -- but creating is not switching. Actually, /new does auto-switch, so it should auto-spawn. However, we can defer spawn to first message for simplicity. Let Claude handle this: spawn subprocess immediately after creation since we auto-switch.)
  - Spawn a ClaudeSubprocess for the new session (but don't send a message yet -- it's just ready)

**New command: `/session <name>`**

Handler: `async def switch_session_cmd(update, context)`
- Extract name from `context.args[0]` (required)
- Validate: if no args, reply with usage and list available sessions
- If session doesn't exist: reply "Session '<name>' doesn't exist. Use /new <name> to create it."
- Call `session_manager.switch_session(name)` — this marks the PREVIOUS session as "suspended" in metadata (subprocess stays alive in `subprocesses` dict)
- **MANDATORY auto-spawn:** After switching, check if `subprocesses.get(name)` is None or `not subprocesses[name].is_alive`. If so, create a new ClaudeSubprocess with session directory and persona, store in `subprocesses[name]`. This implements the locked decision: "Switching to a session with no running process auto-spawns Claude Code immediately." The subprocess idles waiting for first message.
- Reply "Switched to session '<name>'." (include persona if set)

**Modified message handler: `handle_message`**

Replace the current implementation (which saves to inbox) with session-aware routing:
- If no active session: reply "No active session. Use /new <name> to start one."
- If active session exists:
  - Get or create ClaudeSubprocess for session (auto-spawn if not alive)
  - Call `await subprocess.send_message(update.message.text)` — **Note: ClaudeSubprocess handles internal message queueing via asyncio.Queue (Plan 02 design). If the subprocess is busy processing a previous message, `send_message()` queues the message and it will be sent after the current response completes. The bot handler does NOT need to check `is_busy` — just call `send_message()` and the subprocess manages the queue.**
  - The on_output callback will need to send messages back to the chat. For Phase 1, create callbacks that send to the Telegram chat:
    ```python
    async def on_output(text):
        await update.message.reply_text(text)
    ```
  - Note: For Phase 1, basic output routing is needed. Full Telegram integration (typing indicators, message batching, code block handling) comes in Phase 2.
  - Register handler with `block=False` for non-blocking execution (per research: prevents blocking Telegram event loop)

**Subprocess callback factory:**

Create a helper function that generates callbacks bound to a specific chat_id and bot instance:
```python
def make_callbacks(bot, chat_id):
    async def on_output(text):
        # Truncate to Telegram limit
        if len(text) > 4000:
            text = text[:4000] + "\n... (truncated)"
        await bot.send_message(chat_id=chat_id, text=text)

    async def on_error(error):
        await bot.send_message(chat_id=chat_id, text=f"Error: {error}")

    async def on_complete():
        pass  # Phase 2 will add typing indicator cleanup

    async def on_status(status):
        await bot.send_message(chat_id=chat_id, text=f"[{status}]")

    return on_output, on_error, on_complete, on_status
```

**Important: callback async handling.** The ClaudeSubprocess callbacks may be called from a background task. Since `bot.send_message` is async, the callbacks need to be async and properly awaited. The subprocess module should use `asyncio.ensure_future()` or check if callbacks are coroutines.

Actually, reconsider: the subprocess stream reader runs in a background asyncio.Task. Callbacks called from there ARE in the event loop already. So async callbacks work naturally. The subprocess module should `await callback(text)` if the callback is a coroutine, or call `asyncio.create_task(callback(text))` to not block the reader.

**Handler registration updates:**

Add to `main()`:
```python
app.add_handler(CommandHandler("new", new_session))
app.add_handler(CommandHandler("session", switch_session_cmd))
```

Update the text message handler to use `block=False`:
```python
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message, block=False))
```

**Update /help text:** Add /new and /session to the help output.

**Keep all existing commands working.** Do not remove or modify `/status`, `/pbs`, `/backups`, etc. Only modify `handle_message` and add new handlers.

**Authorization:** All new handlers must check `is_authorized(update.effective_user.id)` as the first line, following existing pattern.

**DO NOT:**
- Remove or break existing command handlers
- Implement typing indicators (Phase 2)
- Implement message batching or code block handling (Phase 2)
- Implement idle timeout (Phase 3)
- Make the bot.py import path different from existing pattern
  </action>
  <verify>
```bash
cd ~/homelab
source ~/venv/bin/activate

# Verify bot.py loads without import errors
python3 -c "
import sys
sys.path.insert(0, '.')
# Just verify imports work - don't run the bot
from telegram.session_manager import SessionManager
from telegram.claude_subprocess import ClaudeSubprocess
print('Imports successful')
"

# Verify new handlers are registered in the code
python3 -c "
source = open('telegram/bot.py').read()
assert 'CommandHandler(\"new\"' in source or 'CommandHandler(\"new\",' in source, 'Missing /new handler registration'
assert 'CommandHandler(\"session\"' in source or 'CommandHandler(\"session\",' in source, 'Missing /session handler registration'
assert 'SessionManager' in source, 'Missing SessionManager usage'
assert 'ClaudeSubprocess' in source, 'Missing ClaudeSubprocess usage'
assert 'block=False' in source, 'Missing block=False for non-blocking handler'
assert 'is_authorized' in source, 'Authorization checks must be present'
print('Handler registration and imports verified!')
"

# Verify existing handlers still present
python3 -c "
source = open('telegram/bot.py').read()
existing = ['status', 'pbs', 'backups', 'beszel', 'kuma', 'ping', 'help']
for cmd in existing:
    assert f'CommandHandler(\"{cmd}\"' in source or f'CommandHandler(\"{cmd}\",' in source, f'Existing /{cmd} handler missing!'
print('All existing handlers preserved!')
"
```
  </verify>
  <done>/new and /session commands are registered in bot.py, SessionManager and ClaudeSubprocess are wired in, plain text messages route to active session's subprocess (or prompt if no session), all existing bot commands still work, handlers use block=False for non-blocking execution.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 2: Verify session commands work in Telegram</name>
  <what-built>
Session management commands (/new, /session) integrated into the Telegram bot, with Claude Code subprocess spawning and basic message routing. The bot should handle session creation, switching, and message forwarding to Claude Code.
  </what-built>
  <how-to-verify>
1. Restart the Telegram bot service:
   ```
   systemctl --user restart telegram-bot.service
   systemctl --user status telegram-bot.service
   ```
2. In Telegram, send `/new test-session` -- expect confirmation "Session 'test-session' created."
3. Send a plain text message like "Hello, what can you do?" -- expect Claude's response back in Telegram
4. Send `/new second brainstorm` -- expect "Session 'second' created with persona 'brainstorm'."
5. Send `/session test-session` -- expect "Switched to session 'test-session'."
6. Send another message -- should go to test-session's Claude (different context from second)
7. Check for zombie processes: `ps aux | grep defunct` -- should be empty
8. Check session directories exist: `ls ~/telegram/sessions/` -- should show test-session and second
9. Send `/new test-session` (duplicate) -- expect friendly error, not crash
10. Send a message without active session (if possible to test): should get "No active session" prompt
  </how-to-verify>
  <resume-signal>Type "approved" if sessions work correctly, or describe any issues found.</resume-signal>
</task>

</tasks>

<verification>
1. Bot starts without errors after code changes
2. `/new <name>` creates session directory with metadata.json and persona.json
3. `/session <name>` switches active session
4. Plain text messages route to active session's Claude Code subprocess
5. Claude Code responses come back to Telegram chat
6. All existing commands (/status, /pbs, /backups, etc.) still work
7. No zombie processes after session switches
8. Duplicate /new returns friendly error
9. Message with no active session prompts user
</verification>

<success_criteria>
- `/new test` creates session and confirms in Telegram
- `/session test` switches session and confirms
- Plain text messages trigger Claude Code and response appears in Telegram
- Existing bot commands remain functional
- No zombie processes (verified via `ps aux | grep defunct`)
- Bot service runs stably under systemd
</success_criteria>

<output>
After completion, create `.planning/phases/01-session-process-foundation/01-03-SUMMARY.md`
</output>
