a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
599 lines
18 KiB
Markdown
599 lines
18 KiB
Markdown
# Server Service Split Plan
|
||
|
||
Status: Audit-based plan
|
||
|
||
Scope: `src/server*.rs` and `src/server/**/*.rs` in the current shared-server architecture.
|
||
|
||
This document audits the current server stack and proposes an incremental split into five in-process services:
|
||
|
||
- session
|
||
- client
|
||
- swarm
|
||
- debug
|
||
- maintenance
|
||
|
||
The intent is to improve ownership boundaries and reduce argument fanout without changing the single-process runtime model.
|
||
|
||
See also:
|
||
|
||
- [`SERVER_ARCHITECTURE.md`](./SERVER_ARCHITECTURE.md)
|
||
- [`SWARM_ARCHITECTURE.md`](./SWARM_ARCHITECTURE.md)
|
||
- [`UNIFIED_SELFDEV_SERVER_PLAN.md`](./UNIFIED_SELFDEV_SERVER_PLAN.md)
|
||
|
||
## Executive Summary
|
||
|
||
Today the server is already logically split by file, but not by ownership boundary.
|
||
The dominant pattern is:
|
||
|
||
- `Server` owns nearly all shared state in one struct.
|
||
- `ServerRuntime` clones that full state bag into connection handlers.
|
||
- `handle_client()` and `handle_debug_client()` receive very wide dependency lists.
|
||
- maintenance loops in `server.rs` mutate the same raw maps used by client, session, swarm, and debug paths.
|
||
|
||
That means the main extraction seam is **not** transport or process boundaries. The main seam is introducing **service-owned state + service APIs** inside the existing process.
|
||
|
||
The safest path is:
|
||
|
||
1. keep one server process
|
||
2. keep current modules and behavior
|
||
3. introduce service handle structs around existing state
|
||
4. move mutation behind service methods
|
||
5. reduce `handle_client()` and `handle_debug_client()` to a few service/context arguments
|
||
|
||
Do **not** start with crates, traits, or IPC splits. The code is not ready for that yet, and the current pain is mostly ownership fanout, not runtime topology.
|
||
|
||
## Current Stack Audit
|
||
|
||
### Top-level runtime shape
|
||
|
||
Current runtime flow:
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Server[server.rs::Server] --> Runtime[server/runtime.rs::ServerRuntime]
|
||
Runtime --> MainAccept[main socket accept loop]
|
||
Runtime --> DebugAccept[debug socket accept loop]
|
||
Runtime --> GatewayAccept[gateway accept loop]
|
||
|
||
MainAccept --> ClientLifecycle[client_lifecycle.rs::handle_client]
|
||
DebugAccept --> DebugRouter[debug.rs::handle_debug_client]
|
||
GatewayAccept --> ClientLifecycle
|
||
|
||
Server --> Maintenance[reload, bus monitor, idle timeout, registry, memory, ambient]
|
||
ClientLifecycle --> SessionModules[session/actions/provider/session-state handlers]
|
||
ClientLifecycle --> SwarmModules[comm_* and swarm handlers]
|
||
DebugRouter --> DebugModules[debug_* handlers]
|
||
```
|
||
|
||
### Shared state concentration
|
||
|
||
`src/server.rs` owns one large `Server` struct with state spanning all concerns, including:
|
||
|
||
- sessions and default session id
|
||
- client count and client connection map
|
||
- swarm membership, plans, shared context, coordinator map
|
||
- file touch tracking and reverse indexes
|
||
- channel subscriptions and reverse indexes
|
||
- debug client routing and debug jobs
|
||
- swarm event history and event bus
|
||
- ambient runner, shared MCP pool
|
||
- shutdown signals and soft interrupt queues
|
||
- await-members runtime
|
||
|
||
This is a service container in practice, but it is represented as one broad state owner.
|
||
|
||
### Existing positive seams
|
||
|
||
The code already contains a few useful seams we should preserve:
|
||
|
||
- `runtime.rs` already isolates accept-loop orchestration from bootstrap.
|
||
- `state.rs` already centralizes shared types and delivery helpers.
|
||
- `swarm.rs` is already the closest thing to a stateful domain service.
|
||
- `reload.rs` is already separate from bootstrap, even though `server.rs` still owns most maintenance wiring.
|
||
- `debug_*` modules are already split by debug command domain.
|
||
|
||
These are good extraction points. The plan below leans on them instead of fighting them.
|
||
|
||
## Module Heat Map
|
||
|
||
Largest server-side modules at the time of audit:
|
||
|
||
| File | Lines | Primary concern today | Future service |
|
||
|---|---:|---|---|
|
||
| `src/server/client_lifecycle.rs` | 1767 | client request loop and router | client |
|
||
| `src/server/client_comm.rs` | 1492 | swarm communication requests | swarm |
|
||
| `src/server/client_actions.rs` | 1249 | session-local actions | session |
|
||
| `src/server/swarm.rs` | 1202 | swarm state mutation and fanout | swarm |
|
||
| `src/server/comm_control.rs` | 1183 | swarm control / await-members / client debug bridge | swarm + debug |
|
||
| `src/server/client_session.rs` | 1091 | subscribe, resume, clear, reload | session + client boundary |
|
||
| `src/server/comm_session.rs` | 987 | spawn/stop session flows | session + swarm boundary |
|
||
| `src/server/debug.rs` | 980 | debug socket command router | debug |
|
||
| `src/server/reload.rs` | 826 | reload and graceful shutdown | maintenance |
|
||
| `src/server/debug_server_state.rs` | 748 | debug snapshots across all stores | debug |
|
||
|
||
Interpretation:
|
||
|
||
- The architecture is not blocked on missing modules.
|
||
- It is blocked on **cross-service state access** and **router width**.
|
||
|
||
## Where Coupling Is Highest
|
||
|
||
### 1. `ServerRuntime` is a full-state courier
|
||
|
||
`runtime.rs` clones almost every shared field into the runtime and forwards them into:
|
||
|
||
- main client handling
|
||
- debug client handling
|
||
- gateway client handling
|
||
|
||
This makes transport code depend on internal service storage details.
|
||
|
||
### 2. `handle_client()` is both connection loop and application router
|
||
|
||
`client_lifecycle.rs::handle_client()` currently combines:
|
||
|
||
- stream read loop
|
||
- per-connection state
|
||
- session attach / resume / clear
|
||
- provider control
|
||
- swarm communication dispatch
|
||
- debug bridge requests
|
||
- message processing lifecycle
|
||
- disconnect cleanup
|
||
|
||
That is the clearest signal that client, session, swarm, and debug responsibilities are crossing in one place.
|
||
|
||
### 3. session flows directly mutate swarm state
|
||
|
||
`client_session.rs` does real session work, but also directly touches:
|
||
|
||
- swarm member registration
|
||
- channel subscription cleanup
|
||
- plan participant rename/removal
|
||
- status updates
|
||
- event sender registration
|
||
- interrupt queue rename/removal
|
||
|
||
That makes session lifecycle hard to extract cleanly because it owns both agent state and swarm membership side effects.
|
||
|
||
### 4. maintenance loops reach into domain maps directly
|
||
|
||
`server.rs` maintenance tasks currently touch shared state directly for:
|
||
|
||
- reload handling
|
||
- background task wakeup / notification delivery
|
||
- bus monitoring and file touch conflict detection
|
||
- idle timeout
|
||
- runtime memory logging
|
||
- registry publishing
|
||
- ambient scheduling
|
||
|
||
This makes background jobs depend on storage layout instead of service APIs.
|
||
|
||
### 5. debug paths bypass future boundaries
|
||
|
||
`debug.rs` and `debug_*` modules inspect or mutate many raw stores directly.
|
||
That is fine for now, but it will block extraction unless debug becomes a consumer of service snapshots and public mutation methods.
|
||
|
||
## Proposed Service Split
|
||
|
||
The target split is still one process and one Tokio runtime.
|
||
The change is ownership and APIs, not deployment.
|
||
|
||
### 1. Session Service
|
||
|
||
**Owns:**
|
||
|
||
- `sessions`
|
||
- `session_id` default/global session tracking
|
||
- `shutdown_signals`
|
||
- `soft_interrupt_queues`
|
||
- session event sender registration and fanout
|
||
- session-local agent actions and provider/session mutation
|
||
- headless session creation primitives
|
||
|
||
**Primary modules after split:**
|
||
|
||
- `state.rs` delivery pieces
|
||
- `client_session.rs` session-only parts
|
||
- `client_actions.rs`
|
||
- `provider_control.rs`
|
||
- `headless.rs`
|
||
- parts of `reload.rs` for graceful shutdown helpers
|
||
|
||
**Public API examples:**
|
||
|
||
- `attach_client(...)`
|
||
- `resume_session(...)`
|
||
- `clear_session(...)`
|
||
- `spawn_headless_session(...)`
|
||
- `queue_soft_interrupt(...)`
|
||
- `fanout_session_event(...)`
|
||
- `rename_session(...)`
|
||
- `shutdown_session(...)`
|
||
- `session_snapshot(...)`
|
||
|
||
**Boundary rule:** session service should not directly own swarm membership rules.
|
||
It can expose lifecycle events or return session metadata that another layer uses to update swarm state.
|
||
|
||
### 2. Client Service
|
||
|
||
**Owns:**
|
||
|
||
- socket, debug socket, gateway transport accept loops
|
||
- client connection registry
|
||
- client count / attachment count
|
||
- connection-scoped state and request routing
|
||
- subscribe / reconnect orchestration across services
|
||
- client API wrappers
|
||
|
||
**Primary modules after split:**
|
||
|
||
- `runtime.rs`
|
||
- `socket.rs`
|
||
- `client_api.rs`
|
||
- `client_lifecycle.rs` connection loop and router only
|
||
- `client_disconnect_cleanup.rs`
|
||
- client-facing parts of `client_state.rs`
|
||
|
||
**Public API examples:**
|
||
|
||
- `spawn_accept_loops(...)`
|
||
- `run_client_connection(stream)`
|
||
- `register_connection(...)`
|
||
- `cleanup_connection(...)`
|
||
- `connected_clients_snapshot()`
|
||
|
||
**Boundary rule:** client service routes requests, but does not own business state for sessions, swarms, or debug jobs.
|
||
|
||
### 3. Swarm Service
|
||
|
||
**Owns:**
|
||
|
||
- `swarm_members`
|
||
- `swarms_by_id`
|
||
- `shared_context`
|
||
- `swarm_plans`
|
||
- `swarm_coordinators`
|
||
- channel subscriptions and reverse indexes
|
||
- swarm event history and event broadcast
|
||
- file touch tracking and reverse indexes
|
||
- await-members runtime
|
||
- status broadcasting, plan broadcasting, conflict notifications
|
||
|
||
**Primary modules after split:**
|
||
|
||
- `swarm.rs`
|
||
- `client_comm.rs`
|
||
- `comm_plan.rs`
|
||
- `comm_control.rs` swarm portions
|
||
- `comm_session.rs` swarm coordination portions
|
||
- `comm_sync.rs`
|
||
- file-touch portions of `server.rs::monitor_bus`
|
||
- `await_members_state.rs`
|
||
|
||
**Public API examples:**
|
||
|
||
- `join_swarm(...)`
|
||
- `leave_swarm(...)`
|
||
- `set_member_status(...)`
|
||
- `assign_role(...)`
|
||
- `update_plan(...)`
|
||
- `subscribe_channel(...)`
|
||
- `publish_notification(...)`
|
||
- `record_file_touch(...)`
|
||
- `detect_conflicts(...)`
|
||
- `await_members(...)`
|
||
- `snapshot_swarm(...)`
|
||
|
||
**Boundary rule:** swarm service can request message delivery through the session service, but should not reach into raw session maps.
|
||
|
||
### 4. Debug Service
|
||
|
||
**Owns:**
|
||
|
||
- debug socket request router
|
||
- client debug bridge state
|
||
- debug job registry
|
||
- testers and debug command execution helpers
|
||
- server and swarm snapshots for inspection
|
||
|
||
**Primary modules after split:**
|
||
|
||
- `debug.rs`
|
||
- `debug_command_exec.rs`
|
||
- `debug_events.rs`
|
||
- `debug_help.rs`
|
||
- `debug_jobs.rs`
|
||
- `debug_server_state.rs`
|
||
- `debug_session_admin.rs`
|
||
- `debug_swarm_read.rs`
|
||
- `debug_swarm_write.rs`
|
||
- `debug_testers.rs`
|
||
- `debug_ambient.rs`
|
||
|
||
**Public API examples:**
|
||
|
||
- `run_debug_connection(stream)`
|
||
- `submit_debug_job(...)`
|
||
- `server_snapshot()`
|
||
- `swarm_snapshot(...)`
|
||
- `route_transcript_injection(...)`
|
||
|
||
**Boundary rule:** debug service should read snapshots from other services and mutate them only through explicit service methods.
|
||
It should not be a privileged backdoor around normal APIs except where intentionally documented.
|
||
|
||
### 5. Maintenance Service
|
||
|
||
**Owns:**
|
||
|
||
- reload monitor and reload-state plumbing
|
||
- registry publish / cleanup background tasks
|
||
- idle timeout monitor
|
||
- runtime memory logging loop
|
||
- embedding preload and idle unload
|
||
- ambient loop startup/wiring
|
||
- background task completion delivery orchestration
|
||
- bus subscription loops that translate infra events into service calls
|
||
|
||
**Primary modules after split:**
|
||
|
||
- `reload.rs`
|
||
- `reload_state.rs`
|
||
- background-task delivery logic from `server.rs`
|
||
- registry and idle-timeout pieces from `server.rs`
|
||
- runtime memory logging pieces from `server.rs`
|
||
- `monitor_bus()` after it is narrowed to service calls
|
||
|
||
**Public API examples:**
|
||
|
||
- `start_background_loops(...)`
|
||
- `handle_reload_signal(...)`
|
||
- `deliver_background_task_completion(...)`
|
||
- `publish_registry_metadata(...)`
|
||
- `run_idle_monitor(...)`
|
||
- `run_bus_monitor(...)`
|
||
|
||
**Boundary rule:** maintenance service should orchestrate services, not own their domain maps.
|
||
|
||
## Recommended Dependency Direction
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Client[Client Service] --> Session[Session Service]
|
||
Client --> Swarm[Swarm Service]
|
||
Client --> Debug[Debug Service]
|
||
|
||
Swarm --> Session
|
||
Debug --> Session
|
||
Debug --> Swarm
|
||
Maintenance --> Session
|
||
Maintenance --> Swarm
|
||
Maintenance --> Client
|
||
```
|
||
|
||
Rules:
|
||
|
||
- `Server` becomes bootstrap and wiring only.
|
||
- `ServerRuntime` becomes transport runtime only.
|
||
- session and swarm are the main domain services.
|
||
- debug and maintenance depend on domain services, not the other way around.
|
||
|
||
## Concrete Extraction Seams
|
||
|
||
### Seam A: turn `state.rs` into the session-delivery foundation
|
||
|
||
`state.rs` already contains the best low-risk shared seam:
|
||
|
||
- session event sender registration
|
||
- session event fanout
|
||
- soft interrupt queue registration and enqueue
|
||
|
||
Make this the initial backbone of the session service instead of leaving it as generic helpers.
|
||
|
||
Why this is safe:
|
||
|
||
- logic is already centralized
|
||
- heavily reused by swarm, debug, and maintenance
|
||
- extraction reduces duplication of `SessionAgents` and queue plumbing without changing behavior
|
||
|
||
### Seam B: separate connection routing from business handlers
|
||
|
||
Split `client_lifecycle.rs` into:
|
||
|
||
- `ClientConnection` or `ClientLoop` for stream handling and per-client state
|
||
- `ClientRequestRouter` for mapping `Request` variants to service calls
|
||
|
||
The router should depend on `SessionService`, `SwarmService`, and `DebugService`, not raw `Arc<RwLock<HashMap<...>>>` fields.
|
||
|
||
Why this is safe:
|
||
|
||
- no protocol change
|
||
- no state ownership change yet
|
||
- mostly signature narrowing and file movement
|
||
|
||
### Seam C: move swarm membership side effects out of session lifecycle code
|
||
|
||
Today subscribe/resume/clear paths do both session and swarm work.
|
||
That should become:
|
||
|
||
- session service: attach/resume/rename session
|
||
- swarm service: join/update/leave member state
|
||
- client service: orchestrate the sequence for a request
|
||
|
||
This is likely the most important semantic seam for future maintainability.
|
||
|
||
Why this is safe:
|
||
|
||
- it clarifies ownership without changing the shared-server model
|
||
- it removes the hardest cross-domain coupling first
|
||
|
||
### Seam D: make maintenance loops call service APIs only
|
||
|
||
`monitor_bus()`, reload orchestration, idle timeout, and background-task wakeup should stop mutating shared maps directly.
|
||
They should call:
|
||
|
||
- `session_service.queue_soft_interrupt(...)`
|
||
- `session_service.fanout_session_event(...)`
|
||
- `swarm_service.record_file_touch(...)`
|
||
- `swarm_service.broadcast_status(...)`
|
||
- `swarm_service.detect_conflicts(...)`
|
||
|
||
Why this is safe:
|
||
|
||
- behavior stays the same
|
||
- background logic becomes testable in isolation
|
||
- future refactors no longer require editing `server.rs`
|
||
|
||
### Seam E: make debug consume snapshots, not storage
|
||
|
||
The debug stack currently knows too much about internal maps.
|
||
Introduce service snapshot methods so debug code reads pre-shaped data:
|
||
|
||
- `session_service.snapshot_sessions()`
|
||
- `client_service.snapshot_connections()`
|
||
- `swarm_service.snapshot_state()`
|
||
- `maintenance_service.snapshot_runtime_health()`
|
||
|
||
Why this is safe:
|
||
|
||
- debug stays powerful
|
||
- domain internals become easier to change
|
||
- read-only inspection stops blocking storage changes
|
||
|
||
## First Safe Moves
|
||
|
||
These are the first changes I would recommend landing in order.
|
||
|
||
### Move 1: docs and ownership rules
|
||
|
||
Land this plan and treat it as the contract for future refactors.
|
||
|
||
**Why first:** it prevents accidental partial extractions that worsen coupling.
|
||
|
||
### Move 2: introduce service handle structs with zero behavior change
|
||
|
||
Add thin wrappers such as:
|
||
|
||
- `SessionServiceHandle`
|
||
- `ClientServiceHandle`
|
||
- `SwarmServiceHandle`
|
||
- `DebugServiceHandle`
|
||
- `MaintenanceServiceHandle`
|
||
|
||
Initially these can just wrap the current `Arc` fields.
|
||
No logic movement is required yet.
|
||
|
||
**Payoff:** stops the spread of 20+ argument lists.
|
||
|
||
### Move 3: change `ServerRuntime` to hold service handles, not raw maps
|
||
|
||
`runtime.rs` is the cleanest place to narrow dependencies because it already acts as the server’s execution runtime.
|
||
|
||
**Payoff:** connection accept code no longer needs to know the storage layout of every subsystem.
|
||
|
||
### Move 4: change `handle_client()` and `handle_debug_client()` signatures
|
||
|
||
Replace wide argument lists with a few typed contexts:
|
||
|
||
- `ClientRequestContext`
|
||
- `DebugRequestContext`
|
||
- service handles
|
||
|
||
**Payoff:** largest readability win with limited behavioral risk.
|
||
|
||
### Move 5: extract swarm membership orchestration from `client_session.rs`
|
||
|
||
Create explicit swarm membership methods and have client/session flows call them.
|
||
|
||
**Payoff:** this is the first real domain split and removes one of the biggest architecture knots.
|
||
|
||
### Move 6: move `monitor_bus()` behind the swarm/session API boundary
|
||
|
||
Keep behavior, but stop direct map access from the maintenance loop.
|
||
|
||
**Payoff:** background infrastructure becomes modular and easier to test.
|
||
|
||
## Moves To Avoid Early
|
||
|
||
Avoid these until the service-handle layer exists:
|
||
|
||
- splitting into separate processes
|
||
- creating new crates for each service
|
||
- introducing async traits for every domain call
|
||
- changing the on-the-wire protocol
|
||
- changing session persistence format
|
||
- merging debug and normal sockets into one transport path as part of the refactor
|
||
|
||
These are higher-risk and do not solve the present problem as directly as state/API narrowing.
|
||
|
||
## Suggested File Landing Plan
|
||
|
||
### Phase 1: no behavior change
|
||
|
||
- add service handle types
|
||
- make `Server` store those handles or construct them centrally
|
||
- thread handles through `runtime.rs`
|
||
- narrow `handle_client()` and `handle_debug_client()` inputs
|
||
|
||
### Phase 2: move ownership boundaries
|
||
|
||
- move session delivery helpers under session service
|
||
- move swarm membership/status/channel/plan mutation fully under swarm service
|
||
- move debug readers to service snapshots
|
||
- move maintenance loops to service APIs
|
||
|
||
### Phase 3: clean module layout
|
||
|
||
Possible end-state layout:
|
||
|
||
```text
|
||
src/server/
|
||
bootstrap.rs # current server.rs bootstrap pieces
|
||
runtime.rs # accept loops and transport runtime
|
||
services/
|
||
session.rs
|
||
client.rs
|
||
swarm.rs
|
||
debug.rs
|
||
maintenance.rs
|
||
session/
|
||
actions.rs
|
||
lifecycle.rs
|
||
provider.rs
|
||
delivery.rs
|
||
swarm/
|
||
comm.rs
|
||
plan.rs
|
||
control.rs
|
||
sync.rs
|
||
state.rs
|
||
debug/
|
||
router.rs
|
||
jobs.rs
|
||
snapshots.rs
|
||
testers.rs
|
||
maintenance/
|
||
reload.rs
|
||
bus.rs
|
||
idle.rs
|
||
memory.rs
|
||
registry.rs
|
||
```
|
||
|
||
This can be reached gradually. It does not need to happen in one PR.
|
||
|
||
## Decision Record
|
||
|
||
### Recommended first code extraction
|
||
|
||
If one tiny extraction is desired after docs, the safest one is:
|
||
|
||
- introduce **service handle structs only**, with no behavior change
|
||
|
||
That is the highest-leverage low-risk move because it narrows dependency surfaces immediately and creates a place to move methods later.
|
||
|
||
### Recommended non-goal for now
|
||
|
||
Do not split the server into separate OS services. The current architecture benefits from shared MCP pool, shared embedding lifecycle, shared reload handling, and shared in-memory coordination. The code should first be made modular **inside** the existing process.
|