4.7 KiB
Multi-Workspace Feature Design
Overview
Enable a single cc-connect bot (one Slack token) to serve multiple workspaces, with the channel determining which Claude Code working directory and session to use.
Config
[[projects]]
name = "claude"
mode = "multi-workspace"
base_dir = "~/workspace"
[projects.agent]
type = "claudecode"
permission_mode = "yolo"
[[projects.platforms]]
type = "slack"
bot_token = "xoxb-..."
app_token = "xapp-..."
mode = "multi-workspace"enables the feature. Omitting or"single"preserves current behavior.base_diris the parent directory where workspaces live. Replaceswork_diron the agent.- Agent config has no
work_dir— resolved per-channel at runtime.
Workspace Resolution Flow
When a message arrives in a channel:
- Check bindings — look up
workspace_bindings.jsonfor an existing channel-to-workspace mapping. - Convention match — if no binding, check if
<base_dir>/<channel-name>/exists. If yes, auto-bind and confirm:"Found
~/workspace/model-profilermatching this channel. Binding workspace and starting session... Ready." - Ask for repo — if no match, reply:
"No workspace found for this channel. What repo should I clone?" User provides URL, bot confirms: "I'll clone
org/repoto~/workspace/repo-nameand bind to this channel. OK?" - Clone and bind — on confirmation, clone the repo, save the binding, spawn agent subprocess. Explicit feedback throughout:
"Cloning
github.com/org/repoto~/workspace/repo-name..." "Clone complete. Binding workspace to this channel... Ready."
Binding Storage
Persisted in ~/.cc-connect/workspace_bindings.json:
{
"project:claude": {
"C0AKYKUF75K": {
"channel_name": "model-profiler",
"workspace": "/home/leigh/workspace/model-profiler",
"bound_at": "2026-03-12T10:00:00Z"
}
}
}
Agent Subprocess Management
Engine maintains workspaceAgents map[string]*workspaceState keyed by workspace path. Each workspaceState holds the agent subprocess, its SessionManager, and a lastActivity timestamp.
Lifecycle
- Spawn on first message — start a Claude Code subprocess with
work_dirset to the resolved workspace. - Resume on subsequent messages — reuse the running subprocess with saved session ID.
- Idle reap — background goroutine checks
lastActivityevery minute. Subprocesses idle >15 minutes are stopped. Session ID is preserved so the next message transparently restarts. - Graceful shutdown — on bot shutdown, stop all subprocesses cleanly.
Session Management
Each workspace gets its own SessionManager instance with a separate JSON file (same naming scheme as today: project_hash.json). Named sessions within a workspace work exactly as they do now.
Message Routing Changes
In Engine.handleMessage, the multi-workspace path inserts before the existing flow:
- Extract channel ID from the message's session key (
slack:channelID:userID). - Resolve workspace — look up binding, convention match, or trigger init flow.
- If no workspace resolved (init flow in progress) — handle the init conversation directly, don't forward to any agent.
- If workspace resolved — get or spawn the agent subprocess for that workspace, then continue with existing message processing.
interactiveStates gets keyed by workspace+sessionKey (rather than just sessionKey) so the same user in different channels hits different agent processes.
New Commands
/workspace— show current channel's bound workspace/workspace init <url>— clone and bind/workspace unbind— remove binding/workspace list— show all bindings
Existing commands (/sessions, /model, etc.) work per-workspace.
Error Handling & Edge Cases
- Unbound channel, bot mentioned — bot asks for repo URL. No agent forwarding until binding is established.
- Clone fails (bad URL, auth, disk) — bot reports error and asks user to try again. No partial binding saved.
- Workspace directory deleted externally — on next message, bot detects missing directory, removes the binding, re-enters init flow: "Workspace
~/workspace/foono longer exists. What repo should I clone?" - Agent subprocess crashes — restart on next message using saved session ID (same as current behavior).
- Bot in unwanted channel — without binding or matching directory, it just asks for a repo. User can ignore or remove the bot.
Architecture: Approach 1 (Engine-level multiplexing)
The Engine itself handles multi-workspace routing. No new meta-engine or wrapper layers. The multi-workspace logic is gated behind the mode config field, so single-workspace projects are completely unaffected.