refactor(server): explicit module boundaries for the master server - #483
Conversation
Compiler no longer grabs the transport peer. Compile results (diagnostics, inactive regions) are materialized into Session::output and a typed Signal<T> wakes the transport push path; LSPClient subscribes with an RAII connection. Domain-to-domain hooks are now wired centrally in MasterServer::wire(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move header-context resolution/synthesis and the editor context protocol extension (queryContext/currentContext/switchContext, saved-context restore) into a dedicated ContextResolver owned by MasterServer. Compiler and LSPClient now reach the shared logic through a reference, keeping compile-argument resolution and protocol handlers thin. Pure code move, no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Separate the read-only query surface (IndexQuery) from the background indexing scheduler (BackgroundIndexer), introduce a SessionStore that owns the open-document table and buffer sync, and report indexing progress through a signal instead of a borrowed transport peer. Pure code movement with no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xplicit The transports (LSPClient, AgentClient) reached into MasterServer through friend grants. Replace that with an explicit public surface: the composed services, lifecycle state, and init parameters they actually touch move to public, grouped by role, while the genuinely internal wiring stays private. No behavior change — the members and their construction order are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multi-source feature assembly (preamble links merged into document links, the five-step definition chain with its ast_dirty gates) moves out of the LSP handlers into a dedicated routing layer owned by MasterServer. Transports now only translate between the wire protocol and FeatureRouter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cover the incremental-change folding path (range replace, sequential folds, whole-document replace, invalid-range drop) and the session map generation semantics, which prior tests only exercised via full-document sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The detached create() task captured the LSPClient and dereferenced its members after resuming; a socket-mode connection torn down during the handshake window would leave it touching a destroyed client. The token state now lives behind a shared_ptr owned jointly by the client and the task, with an abandoned flag checked after the await. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR splits server responsibilities into dedicated components for signals, sessions, header context resolution, read-side index queries, background indexing, and feature routing, then rewires compiler and service entry points to use them. ChangesServer subsystem decomposition and rewiring
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Compiler
participant Session
participant LSPClient
participant peer
Compiler->>Session: set session.output
Compiler->>LSPClient: on_output.emit(session)
LSPClient->>peer: publish diagnostics
LSPClient->>peer: publish inactive regions
sequenceDiagram
participant LSPClient
participant SessionStore
participant ContextResolver
participant Compiler
LSPClient->>SessionStore: apply_open / apply_change
LSPClient->>ContextResolver: restore_saved_context / switch_context
Compiler->>ContextResolver: fill_header_context_args(...)
ContextResolver-->>Compiler: arguments and directory
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/server/index/query.cpp (1)
103-127: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winUse the merged-index line map for merged-index lookups.
Line 119 computes the lookup offset from the dirty session text, then queries occurrence ranges stored for
merged_index.content(). When the open buffer differs from disk, offsets no longer refer to the same bytes and can resolve the wrong symbol.Proposed fix
- // Fallback to MergedIndex. Position -> offset uses the session text when - // one exists (open but not yet compiled); for closed files the shard's - // own stored content provides the mapping. + // Fallback to MergedIndex. Position -> offset must use the shard's stored + // content because its occurrence ranges are expressed in that buffer. @@ - auto offset = session ? session->line_map().to_offset(position) : map.to_offset(position); + auto offset = map.to_offset(position);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/index/query.cpp` around lines 103 - 127, The merged-index fallback in query handling is mixing session text offsets with occurrences from merged_index.content(), so use the merged-index line map for both offset conversion and range translation in the lookup path. Update the lookup logic in the query function around merged_index.line_starts(), lsp::LineMap, and merged_index.lookup so that position->offset and offset->range are consistently based on the merged index data, not session->line_map() when querying merged_index.
🧹 Nitpick comments (3)
src/server/index/query.h (1)
151-155: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winClarify that
with_sessionskips dirty or unindexed sessions.Because this delegates to
visit_sessions, the callback is also skipped when a Session exists but lacks a currentfile_index/symbolsor isast_dirty.Suggested clarification
- /// The callback is not invoked if no Session exists for that path_id. + /// The callback is not invoked if no queryable Session exists for that path_id + /// (missing, unindexed, or dirty Sessions are skipped by visit_sessions()).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/index/query.h` around lines 151 - 155, Clarify the `with_session` contract in `query.h` so it states that the callback is not only skipped when no `Session` exists, but also when `visit_sessions` filters out a session that is `ast_dirty` or missing a current `file_index`/`symbols`. Update the comment on `with_session` to match the actual behavior delegated through `visit_sessions` and `Session`, so callers know the callback only runs for fully indexed, clean sessions.src/server/compiler/compiler.cpp (1)
436-462: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider unit-testing the newly extracted
format_diagnostics/format_inactive_regions.These are now pure free functions (no
Compilerstate needed) encapsulating the phantom-suffix-line filtering and "inferred compile command" guidance-diagnostic logic — both of which affect what the user sees on every compile. They're easy to unit-test directly with a syntheticCompileOutput/Session, independent of the worker pipeline.Also applies to: 488-509
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/compiler/compiler.cpp` around lines 436 - 462, Cover the newly extracted pure helpers by adding direct unit tests for format_diagnostics and format_inactive_regions, using synthetic CompileOutput and Session inputs instead of Compiler state. Verify the phantom-suffix line filtering behavior, the inferred compile-command guidance insertion rules, and the inactive-region formatting logic so these user-visible paths are exercised independently of the worker pipeline.src/server/context/context_resolver.cpp (1)
1-735: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding unit tests for
ContextResolver.This file implements header-context resolution/synthesis, the multi-config CDB command-selection logic, and the
queryContext/currentContext/switchContextprotocol handlers — all business-critical and non-trivial (chain resolution, preamble/suffix caching, staleness invalidation). The PR's commit summary shows new tests were added only forSessionStore;ContextResolverhas no dedicated unit coverage in this stack. Given it is now a standalone, dependency-injectable class (Workspace&only), it should be straightforward to unit-test against a fake/mockWorkspace.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/context/context_resolver.cpp` around lines 1 - 735, Add dedicated unit tests for ContextResolver, since this change introduces complex header-context resolution/synthesis and context switching behavior without coverage. Create tests around the main entry points fill_header_context_args, resolve_header_context, query_contexts, current_context, and switch_context using a fake Workspace, covering synthesized vs self-contained headers, multi-config host selection, cached context invalidation, and stale epoch handling. Include assertions for command-hash matching, occurrence handling, and persistence/reset behavior so the critical protocol and cache paths stay protected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/server/index/background_indexer.cpp`:
- Around line 422-442: The final completion count in BackgroundIndexer is stale
when files are skipped because `completed` is incremented but
`progress_data.completed` is only refreshed inside the worker lambda. Update
`progress_data.completed` (and emit `on_progress_changed`) after the skip path
in the `for` loop and again before setting `Progress::Stage::End` so the final
report reflects all skipped items. Keep the change localized around
`background_indexer.cpp`, `progress_data`, `completed`, and the
`workers.spawn`/`workers.join` flow.
- Around line 444-461: The run completion path in background_indexer.cpp leaves
queued work stranded when files are added during an active indexing round,
because schedule() exits early on indexing_active and nothing retriggers it
afterward. Update the indexing flow around the run/queue handling in
background_indexer::run and schedule() so that, after indexing_active is cleared
and the current batch finishes, any remaining items in index_queue automatically
schedule another pass. Use the existing index_queue, index_queue_pos,
pending_ids, and indexing_active state to detect pending work and enqueue the
next run without duplicating a currently running round.
In `@src/server/index/query.cpp`:
- Around line 755-771: The persisted-index branch in query.cpp’s symbol lookup
stops at the first matching Definition, unlike the open-session path that
collects all path+line matches and then filters them. Update the loop around
workspace.project_index.symbols and merged_index.lookup so it accumulates every
symbol whose definition resolves to the requested target_line, then apply the
same parameter/label exclusion logic used by the session path before resolving
uniqueness. Ensure resolve_unique() sees all persisted matches instead of an
early return from the first found symbol.
- Around line 36-40: The IndexQuery::is_proj_path_open helper is treating every
open session as enough to suppress the disk shard, even when the session is
dirty or otherwise not queryable. Update this check to mirror the session
filtering used by visit_sessions so only queryable open sessions return true,
and leave dirty/unindexed sessions as false. Use the existing symbols
IndexQuery::is_proj_path_open, visit_sessions, and sessions to align the logic.
In `@src/server/service/agent_client.cpp`:
- Around line 304-306: The session iteration in agent_client’s index query path
dereferences session.symbols and session.file_index without checking whether the
open session has materialized indexes yet. Update the visit_sessions handling in
the relevant agent_client logic (including the later fallback path) to skip
sessions whose symbols/file_index are not available, or use the persisted shard
instead of accessing null/empty in-memory state. Keep the guard close to the
Session access so try_symbol and related lookups only run on fully indexed
sessions.
In `@src/server/service/lsp_client.cpp`:
- Around line 677-687: The detached handshake in lsp_client.cpp is still using
IndexProgressState::reporter after peer teardown, so it must be tied to the peer
lifetime instead of relying on abandoned alone. Update the
index_progress/create() flow to cancel or join the in-flight handshake before
Connection erases peer, or refactor IndexProgressState and the scheduled lambda
so the reporter has a shared cancellation/lifetime handle bound to JsonPeer. Use
the existing symbols index_progress, IndexProgressState::reporter, and the
scheduled create() coroutine to locate the fix.
In `@src/support/signal.h`:
- Around line 94-102: The Signal::emit path leaves state->emitting stuck on
exceptions, so update the emit() implementation to guarantee the flag is cleared
even if a handler throws. Add a scope guard or equivalent try/finally-style
cleanup around the loop in Signal::emit so state->emitting is reset after
invoking all slot.handler calls, and keep the reentrancy assertion behavior
intact.
- Around line 21-23: `Signal` currently allows `connect()`/`disconnect()` to
mutate `state->slots` during `emit()`, which can invalidate the range iteration
in release builds. Update the `Signal` implementation to make reentrant slot
mutation safe by either snapshotting the current slots before invoking handlers
or deferring add/remove operations until emission completes. Use the
`Signal::emit`, `Signal::connect`, and `Signal::disconnect` paths to locate the
fix and ensure the iterator/storage cannot be modified mid-loop.
---
Outside diff comments:
In `@src/server/index/query.cpp`:
- Around line 103-127: The merged-index fallback in query handling is mixing
session text offsets with occurrences from merged_index.content(), so use the
merged-index line map for both offset conversion and range translation in the
lookup path. Update the lookup logic in the query function around
merged_index.line_starts(), lsp::LineMap, and merged_index.lookup so that
position->offset and offset->range are consistently based on the merged index
data, not session->line_map() when querying merged_index.
---
Nitpick comments:
In `@src/server/compiler/compiler.cpp`:
- Around line 436-462: Cover the newly extracted pure helpers by adding direct
unit tests for format_diagnostics and format_inactive_regions, using synthetic
CompileOutput and Session inputs instead of Compiler state. Verify the
phantom-suffix line filtering behavior, the inferred compile-command guidance
insertion rules, and the inactive-region formatting logic so these user-visible
paths are exercised independently of the worker pipeline.
In `@src/server/context/context_resolver.cpp`:
- Around line 1-735: Add dedicated unit tests for ContextResolver, since this
change introduces complex header-context resolution/synthesis and context
switching behavior without coverage. Create tests around the main entry points
fill_header_context_args, resolve_header_context, query_contexts,
current_context, and switch_context using a fake Workspace, covering synthesized
vs self-contained headers, multi-config host selection, cached context
invalidation, and stale epoch handling. Include assertions for command-hash
matching, occurrence handling, and persistence/reset behavior so the critical
protocol and cache paths stay protected.
In `@src/server/index/query.h`:
- Around line 151-155: Clarify the `with_session` contract in `query.h` so it
states that the callback is not only skipped when no `Session` exists, but also
when `visit_sessions` filters out a session that is `ast_dirty` or missing a
current `file_index`/`symbols`. Update the comment on `with_session` to match
the actual behavior delegated through `visit_sessions` and `Session`, so callers
know the callback only runs for fully indexed, clean sessions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 54d5052c-f7b0-4a07-bcf9-07fe073fda12
📒 Files selected for processing (21)
src/server/compiler/compiler.cppsrc/server/compiler/compiler.hsrc/server/context/context_resolver.cppsrc/server/context/context_resolver.hsrc/server/feature/feature_router.cppsrc/server/feature/feature_router.hsrc/server/index/background_indexer.cppsrc/server/index/background_indexer.hsrc/server/index/query.cppsrc/server/index/query.hsrc/server/service/agent_client.cppsrc/server/service/lsp_client.cppsrc/server/service/lsp_client.hsrc/server/service/master_server.cppsrc/server/service/master_server.hsrc/server/session/session.hsrc/server/session/session_store.cppsrc/server/session/session_store.hsrc/support/signal.htests/unit/server/session_store_tests.cpptests/unit/support/signal_tests.cpp
Skipped files bump the local counter without a Report emit, so a subscriber waking up on End could read a stale completed count from the materialized progress state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
Pure refactor of the master server: no behavior change, no LSP semantics change, no error-message change. Function bodies move verbatim (only mechanical
this/reference/namespace adjustments); zero snapshot changes across all test suites.The server core previously concentrated transport wiring, domain logic, and state management inside
MasterServer+LSPClient/AgentClient(viafriendaccess), with components reaching up into the transport through rawJsonPeer*pointers. This PR draws explicit boundaries:Session::output; the compiler emits a typedSignal<T>and the transport subscribes with an RAII connection and pushes. Background-index progress works the same way (state +Signal<>), so no component holds a peer pointer anymore (set_peeris gone). Data lives in state, signals only wake subscribers, and a missed signal is harmless.MasterServer::wire()is the composition root's wiring diagram (pool.on_crash,pool.on_evicted,compiler.on_indexing_needed).friendaccess is gone. Transports drive the server through an explicit public surface, matching the project's struct-style state classes.New module layout
support/signal.h: minimal single-threadedSignal<Args...>with RAIIConnection(unit-tested).SessionStorereplaces theis_open/each_sessioncallback bridges; the index-validity filter moved verbatim to its consumer (IndexQuery::visit_sessions).IndexQueryabsorbs the agent client's three-strategy symbol locator; the server/project path-pool translations converge on two private helpers.FeatureRouterowns the documentLink preamble merge and the five-step definition chain with itsast_dirtygates; transports only translate protocol. New multi-source feature assembly belongs there, never in handlers.context/,index/,session/,feature/do not includeservice/(transport).Testing
SignalandSessionStoresuites).DebugandRelWithDebInfoconfigurations build clean;pixi run formatis idempotent.