Clarify subprocess persistence on session switch, mandatory auto-spawn on /session, and message queueing delegation to ClaudeSubprocess. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-session-process-foundation | 03 | execute | 2 |
|
|
false |
|
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.
<execution_context> @/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md @/home/mikkel/.claude/get-shit-done/templates/summary.md </execution_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 Task 1: Add /new and /session commands to bot.py telegram/bot.py Modify the existing `telegram/bot.py` to add session management commands and wire up the Claude subprocess for message handling.New imports to add:
from telegram.session_manager import SessionManager
from telegram.claude_subprocess import ClaudeSubprocess
Module-level state:
- Create a
SessionManagerinstance (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 insubprocesses['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 [persona]"
- Call
session_manager.create_session(name, persona=persona) - On ValueError (duplicate name): reply "Session '' already exists. Use /session to switch to it."
- On success:
- Auto-switch to new session: call
session_manager.switch_session(name) - Reply "Session '' created." (include persona name if specified: "Session '' created with 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)
- Auto-switch to new session: call
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 '' doesn't exist. Use /new to create it."
- Call
session_manager.switch_session(name)— this marks the PREVIOUS session as "suspended" in metadata (subprocess stays alive insubprocessesdict) - MANDATORY auto-spawn: After switching, check if
subprocesses.get(name)is None ornot subprocesses[name].is_alive. If so, create a new ClaudeSubprocess with session directory and persona, store insubprocesses[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 ''." (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 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 checkis_busy— just callsend_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:
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=Falsefor 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:
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():
app.add_handler(CommandHandler("new", new_session))
app.add_handler(CommandHandler("session", switch_session_cmd))
Update the text message handler to use block=False:
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
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!')
"
/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.
Task 2: Verify session commands work in Telegram
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.
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
Type "approved" if sessions work correctly, or describe any issues found.
1. Bot starts without errors after code changes
2. `/new ` creates session directory with metadata.json and persona.json
3. `/session ` 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
<success_criteria>
/new testcreates session and confirms in Telegram/session testswitches 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>