Files
cc-connect/docs/plans/2026-03-12-multi-workspace-design.md
T
2026-06-02 23:14:41 +08:00

106 lines
4.7 KiB
Markdown

# 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
```toml
[[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_dir` is the parent directory where workspaces live. Replaces `work_dir` on the agent.
- Agent config has no `work_dir` — resolved per-channel at runtime.
## Workspace Resolution Flow
When a message arrives in a channel:
1. **Check bindings** — look up `workspace_bindings.json` for an existing channel-to-workspace mapping.
2. **Convention match** — if no binding, check if `<base_dir>/<channel-name>/` exists. If yes, auto-bind and confirm:
> "Found `~/workspace/model-profiler` matching this channel. Binding workspace and starting session... Ready."
3. **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/repo` to `~/workspace/repo-name` and bind to this channel. OK?"
4. **Clone and bind** — on confirmation, clone the repo, save the binding, spawn agent subprocess. Explicit feedback throughout:
> "Cloning `github.com/org/repo` to `~/workspace/repo-name`..."
> "Clone complete. Binding workspace to this channel... Ready."
### Binding Storage
Persisted in `~/.cc-connect/workspace_bindings.json`:
```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
1. **Spawn on first message** — start a Claude Code subprocess with `work_dir` set to the resolved workspace.
2. **Resume on subsequent messages** — reuse the running subprocess with saved session ID.
3. **Idle reap** — background goroutine checks `lastActivity` every minute. Subprocesses idle >15 minutes are stopped. Session ID is preserved so the next message transparently restarts.
4. **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:
1. **Extract channel ID** from the message's session key (`slack:channelID:userID`).
2. **Resolve workspace** — look up binding, convention match, or trigger init flow.
3. **If no workspace resolved** (init flow in progress) — handle the init conversation directly, don't forward to any agent.
4. **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/foo` no 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.