Skip to content

feat(client): migrate to SessionHandler for per-session project identity#157

Merged
thebtf merged 5 commits into
mainfrom
feat/session-handler
Apr 14, 2026
Merged

feat(client): migrate to SessionHandler for per-session project identity#157
thebtf merged 5 commits into
mainfrom
feat/session-handler

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Apr 14, 2026

Summary

Migrates the engram client from stateless Handler to muxcore SessionHandler for per-session project identity. Fixes the critical bug where all CC sessions through the daemon shared one project ID.

Architecture doc: .agent/specs/session-architecture/architecture.md
Spec: .agent/specs/session-architecture/spec.md (7 FR, 4 NFR, 4 US)
Depends on: muxcore/v0.18.2 (PR thebtf/mcp-mux#49, merged)

Changes

cmd/engram/main.go

  • engramHandler struct implementing muxcore.SessionHandler + ProjectLifecycle
  • HandleRequest: per-session env lookup, project resolution, gRPC dispatch
  • envOrDefault: session env -> os.Getenv fallback chain
  • resolveProject: cached proxy.ResolveProjectSlug(p.Cwd) per session
  • getOrDialGRPC: connection pool with sync.Map LoadOrStore pattern
  • OnProjectConnect/OnProjectDisconnect: lifecycle logging
  • Simplified main(): no more ResolveProjectSlug(".") or os.Setenv
  • Removed: old mcpHandler, runTranslator, unused imports

cmd/engram/main_test.go (new)

  • 10 unit tests for envOrDefault, parseGRPCAddr, pool key logic, slug caching

go.mod

  • Bump muxcore v0.18.1 -> v0.18.2

What this fixes

Before: all sessions through daemon get project=engram (daemon startup cwd)
After: Session A (cwd: nvmdfs) gets project=nvmdfs, Session B (cwd: engram) gets project=engram

Summary by CodeRabbit

Примечания к выпуску

  • Улучшения производительности

    • Переиспользование gRPC‑соединений между сессиями для снижения накладных расходов.
  • Улучшения надёжности

    • Обработка одиночных JSON‑RPC запросов с централизованным диспетчером.
    • Сессионно‑ориентированная резолюция проекта и конфигурации (приоритет локальных значений).
  • Новые возможности

    • Хуки жизненного цикла проекта: подключение и отключение с сессионным логированием.
  • Тесты

    • Добавлены модульные тесты для конфигурации, парсинга адресов, пула соединений и кэширования.
  • Обновления

    • Обновлён muxcore до версии 0.18.2.
    • Исправлен путь копирования плагина в образ Docker.

thebtf added 3 commits April 14, 2026 21:04
Replace stateless Handler with muxcore SessionHandler. Each CC session
now gets its own project identity from ProjectContext.Cwd instead of
sharing the daemon's startup project.

Changes:
- engramHandler struct with sync.Map pools (gRPC connections, slug cache)
- HandleRequest: per-session env lookup, project resolution, gRPC dispatch
- OnProjectConnect/OnProjectDisconnect lifecycle logging
- envOrDefault: session env → os.Getenv fallback chain
- resolveProject: cached proxy.ResolveProjectSlug per session
- getOrDialGRPC: connection pool with LoadOrStore pattern
- Simplified main(): no more ResolveProjectSlug(".") or os.Setenv
- Removed: mcpHandler, runTranslator, bufio/io imports

Fixes: cross-project contamination where all daemon sessions shared
one project ID (the daemon's startup cwd).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d4a982a5-3c06-47af-8878-2e0a0951acdb

📥 Commits

Reviewing files that changed from the base of the PR and between 03b51c8 and 735f413.

📒 Files selected for processing (1)
  • Dockerfile
✅ Files skipped from review due to trivial changes (1)
  • Dockerfile

Walkthrough

Заменена потоковая per-session stdin/stdout JSON-RPC обработка на muxcore-native SessionHandler; добавлены хуки жизненного цикла проекта, session‑aware разрешение конфигурации/slug, кэш слагов и пул повторно используемых gRPC‑соединений, запросы теперь обрабатываются как одиночные JSON‑RPC payload'ы.

Changes

Cohort / File(s) Summary
SessionHandler & gRPC pooling
cmd/engram/main.go
Убрана модель mcpHandler/stream loop; добавлен engramHandler с методами HandleRequest, OnProjectConnect, OnProjectDisconnect. Введён grpcConns sync.Map и логика pooled gRPC‑соединений (connKey{addr,tlsMode}), перенос создания/переиспользования соединений в handler.
Project-scoped config & slug resolution
cmd/engram/main.go
Добавлена функция envOrDefault(p.Env, ...), отложенное и кэшируемое разрешение project slug через resolveProject(p) (кэш по p.ID), и session‑scoped логирование/хуки ProjectLifecycle.
Tests
cmd/engram/main_test.go
Добавлены unit‑тесты для envOrDefault, parseGRPCAddr, сравнения connKey, нулевого значения engramHandler и проверки извлечения значения из slugCache.
Dependency bump
go.mod
Обновлён github.com/thebtf/mcp-mux/muxcore с v0.18.1v0.18.2.
Docker build path
Dockerfile
Изменён путь COPY для .claude-plugin на plugin/engram/.claude-plugin//app/.claude-plugin/.

Sequence Diagram(s)

sequenceDiagram
    participant Client as JSON‑RPC Client
    participant Handler as engramHandler
    participant Pool as gRPC Conn Pool
    participant GRPC as gRPC Server

    Client->>Handler: HandleRequest(ctx, ProjectContext, request []byte)
    Handler->>Handler: resolveProject(p) / envOrDefault(p.Env, ...)
    Handler->>Pool: Load connKey{addr, tlsMode}
    alt conn exists
        Pool-->>Handler: existing *grpc.ClientConn
    else
        Handler->>GRPC: dialGRPC(addr, tlsMode)
        GRPC-->>Handler: new *grpc.ClientConn
        Handler->>Pool: Store connKey
    end
    Handler->>GRPC: handleJSONRPC(conn, request)
    GRPC-->>Handler: response []byte
    Handler-->>Client: response []byte, error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
В слаге — память, в пуле — мосты,
Запрос пришёл — и путь известен,
SessionHandler встречает с радостью,
Соединенья делят счастье честно,
Пускай код прыгает, как зайка в лесу!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: migrating from Handler to SessionHandler for per-session project identity resolution.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/session-handler

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the engram entry point to implement the muxcore.SessionHandler and muxcore.ProjectLifecycle interfaces, introducing gRPC connection pooling and project slug caching. Feedback highlights a critical need to remove a local file system replace directive in go.mod that would break builds in other environments. Furthermore, the gRPC connection pooling logic requires the API token to be part of the cache key to ensure proper authentication isolation, and the project slug cache should be pruned during disconnection to prevent memory leaks.

Comment thread go.mod Outdated
Comment on lines +64 to +65

replace github.com/thebtf/mcp-mux/muxcore v0.18.2 => D:\Dev\mcp-mux\muxcore
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The local replace directive pointing to an absolute Windows path (D:\Dev\mcp-mux\muxcore) will break builds for other developers and in CI environments. This should be removed before merging.

Comment thread cmd/engram/main.go
Comment on lines +36 to +39
type connKey struct {
addr string
tlsMode string // "custom-ca", "system-tls", "plaintext"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The connKey should include the token because the gRPC connection is authenticated via a tokenInterceptor that is fixed at dial time. Without the token in the key, sessions with different API tokens but the same server URL will incorrectly share the same authenticated connection, leading to potential security issues or incorrect project access.

Suggested change
type connKey struct {
addr string
tlsMode string // "custom-ca", "system-tls", "plaintext"
}
type connKey struct {
addr string
tlsMode string // "custom-ca", "system-tls", "plaintext"
token string
}

Comment thread cmd/engram/main.go
tlsMode = "system-tls"
}

key := connKey{addr: grpcAddr, tlsMode: tlsMode}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Include the token in the connKey to ensure that connections are pooled correctly based on their authentication credentials.

Suggested change
key := connKey{addr: grpcAddr, tlsMode: tlsMode}
key := connKey{addr: grpcAddr, tlsMode: tlsMode, token: token}

Comment thread cmd/engram/main.go
Comment on lines +105 to +107
func (h *engramHandler) OnProjectDisconnect(projectID string) {
fmt.Fprintf(os.Stderr, "[engram] session disconnected: project=%s\n", projectID)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The slugCache should be cleaned up when a project disconnects to prevent an unbounded memory leak as new sessions are created over time. Additionally, logging the resolved slug (if available) provides better diagnostic information than the internal project ID.

Suggested change
func (h *engramHandler) OnProjectDisconnect(projectID string) {
fmt.Fprintf(os.Stderr, "[engram] session disconnected: project=%s\n", projectID)
}
func (h *engramHandler) OnProjectDisconnect(projectID string) {
slug := projectID
if cached, ok := h.slugCache.LoadAndDelete(projectID); ok {
slug = cached.(string)
}
fmt.Fprintf(os.Stderr, "[engram] session disconnected: project=%s\n", slug)
}

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
cmd/engram/main_test.go (2)

47-53: Используйте уникальный ключ для изоляции теста.

os.Unsetenv не восстанавливает состояние окружения после теста. Хотя ключ уникальный (MISSING_KEY_TEST_12345), это хорошая практика — убедиться, что тест изолирован.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/engram/main_test.go` around lines 47 - 53, Test
TestEnvOrDefault_NeitherSet uses os.Unsetenv but doesn't restore the original
environment value; update the test to save the prior value of
"MISSING_KEY_TEST_12345" using os.LookupEnv, then unset for the test and defer
restoring the environment (either os.Setenv with the saved value or os.Unsetenv
if it was absent) so envOrDefault is tested in isolation.

85-104: Тест не проверяет значимую функциональность.

Строка _ = h ничего не проверяет. Комментарий утверждает, что проверяется "usability" handler'а, но фактически тест только сравнивает struct'ы connKey. Рассмотрите возможность удаления неиспользуемого handler'а или добавления реальной проверки.

♻️ Предлагаемый рефакторинг
 func TestGRPCPool_SharedConnection(t *testing.T) {
-	h := &engramHandler{}
-
 	// Two calls with the same URL should return the same connection.
 	// We can't easily test real gRPC connections without a server,
 	// but we can verify the pool key logic.
 	key1 := connKey{addr: "host:37777", tlsMode: "plaintext"}
 	key2 := connKey{addr: "host:37777", tlsMode: "plaintext"}
 	if key1 != key2 {
 		t.Error("identical connKeys should be equal")
 	}

 	key3 := connKey{addr: "other:443", tlsMode: "system-tls"}
 	if key1 == key3 {
 		t.Error("different connKeys should not be equal")
 	}
-
-	// Verify the handler struct is usable (sync.Map initialized by zero value).
-	_ = h
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/engram/main_test.go` around lines 85 - 104, The test currently creates
engramHandler h and then does `_ = h` which asserts nothing; either remove the
unused engramHandler variable or add a real assertion that its zero-valued
sync.Map is usable: replace `_ = h` with a small exercise of engramHandler
(e.g., call an existing method that uses its internal sync.Map or, if no method
exists, add a temporary store/load via the handler's connection map field) to
store a value and read it back to confirm zero-value initialization; reference
the engramHandler and TestGRPCPool_SharedConnection identifiers when making the
change.
cmd/engram/main.go (1)

103-107: Несогласованность логирования: projectID vs cached slug.

OnProjectConnect логирует resolved slug, а OnProjectDisconnect логирует raw projectID. Для согласованности можно использовать cached slug из slugCache:

♻️ Предлагаемый рефакторинг
 // OnProjectDisconnect is called when a CC session disconnects.
 // Implements muxcore.ProjectLifecycle.
 func (h *engramHandler) OnProjectDisconnect(projectID string) {
-	fmt.Fprintf(os.Stderr, "[engram] session disconnected: project=%s\n", projectID)
+	slug := projectID
+	if cached, ok := h.slugCache.Load(projectID); ok {
+		slug = cached.(string)
+	}
+	fmt.Fprintf(os.Stderr, "[engram] session disconnected: project=%s\n", slug)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/engram/main.go` around lines 103 - 107, OnProjectDisconnect currently
logs the raw projectID while OnProjectConnect logs the resolved slug; update
engramHandler.OnProjectDisconnect to look up the cached slug from h.slugCache
(the same cache used by OnProjectConnect) and log that slug for consistency,
falling back to the original projectID if no slug is found; modify the logging
in OnProjectDisconnect to use the cached value and include clear context (e.g.,
"session disconnected: project=<slug|projectID>").
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/engram/main.go`:
- Around line 82-90: When json.Unmarshal into jsonrpcRequest fails, req.ID may
be invalid or partial; update the error branch to explicitly return a
jsonrpcResponse with JSONRPC "2.0", ID set to nil (null in output) and the parse
error (jsonrpcError Code -32700) instead of using req.ID. Locate the
unmarshalling block around json.Unmarshal(request, &req) and change the created
jsonrpcResponse to use an explicit nil ID value rather than req.ID so the
response follows JSON-RPC 2.0 (null id) on parse errors.

In `@go.mod`:
- Around line 64-65: Удалите локальную директиву replace в go.mod, которая
указывает на Windows-путь "D:\Dev\mcp-mux\muxcore" (строка с replace
github.com/thebtf/mcp-mux/muxcore v0.18.2 => D:\Dev\mcp-mux\muxcore), чтобы
сборка CI и другие разработчики не ломались; либо замените её на корректную
версия-модуль (например вернуть на v0.18.2) или удалите строку полностью перед
мержем.

---

Nitpick comments:
In `@cmd/engram/main_test.go`:
- Around line 47-53: Test TestEnvOrDefault_NeitherSet uses os.Unsetenv but
doesn't restore the original environment value; update the test to save the
prior value of "MISSING_KEY_TEST_12345" using os.LookupEnv, then unset for the
test and defer restoring the environment (either os.Setenv with the saved value
or os.Unsetenv if it was absent) so envOrDefault is tested in isolation.
- Around line 85-104: The test currently creates engramHandler h and then does
`_ = h` which asserts nothing; either remove the unused engramHandler variable
or add a real assertion that its zero-valued sync.Map is usable: replace `_ = h`
with a small exercise of engramHandler (e.g., call an existing method that uses
its internal sync.Map or, if no method exists, add a temporary store/load via
the handler's connection map field) to store a value and read it back to confirm
zero-value initialization; reference the engramHandler and
TestGRPCPool_SharedConnection identifiers when making the change.

In `@cmd/engram/main.go`:
- Around line 103-107: OnProjectDisconnect currently logs the raw projectID
while OnProjectConnect logs the resolved slug; update
engramHandler.OnProjectDisconnect to look up the cached slug from h.slugCache
(the same cache used by OnProjectConnect) and log that slug for consistency,
falling back to the original projectID if no slug is found; modify the logging
in OnProjectDisconnect to use the cached value and include clear context (e.g.,
"session disconnected: project=<slug|projectID>").
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83dd43aa-0b1b-4d1c-876d-935f38ea9011

📥 Commits

Reviewing files that changed from the base of the PR and between 880d567 and 62a0f6b.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • cmd/engram/main.go
  • cmd/engram/main_test.go
  • go.mod

Comment thread cmd/engram/main.go
Comment on lines +82 to +90
var req jsonrpcRequest
if err := json.Unmarshal(request, &req); err != nil {
resp := jsonrpcResponse{
JSONRPC: "2.0",
ID: req.ID,
Error: &jsonrpcError{Code: -32700, Message: "Parse error"},
}
return json.Marshal(resp)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

При ошибке парсинга req.ID может быть невалидным.

Если json.Unmarshal завершается с ошибкой, req.ID может остаться nil или содержать частичные данные. По спецификации JSON-RPC 2.0, при ошибке парсинга id должен быть null.

🐛 Предлагаемое исправление
 	var req jsonrpcRequest
 	if err := json.Unmarshal(request, &req); err != nil {
 		resp := jsonrpcResponse{
 			JSONRPC: "2.0",
-			ID:      req.ID,
+			ID:      nil, // JSON-RPC spec: id MUST be null for parse errors
 			Error:   &jsonrpcError{Code: -32700, Message: "Parse error"},
 		}
 		return json.Marshal(resp)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var req jsonrpcRequest
if err := json.Unmarshal(request, &req); err != nil {
resp := jsonrpcResponse{
JSONRPC: "2.0",
ID: req.ID,
Error: &jsonrpcError{Code: -32700, Message: "Parse error"},
}
return json.Marshal(resp)
}
var req jsonrpcRequest
if err := json.Unmarshal(request, &req); err != nil {
resp := jsonrpcResponse{
JSONRPC: "2.0",
ID: nil, // JSON-RPC spec: id MUST be null for parse errors
Error: &jsonrpcError{Code: -32700, Message: "Parse error"},
}
return json.Marshal(resp)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/engram/main.go` around lines 82 - 90, When json.Unmarshal into
jsonrpcRequest fails, req.ID may be invalid or partial; update the error branch
to explicitly return a jsonrpcResponse with JSONRPC "2.0", ID set to nil (null
in output) and the parse error (jsonrpcError Code -32700) instead of using
req.ID. Locate the unmarshalling block around json.Unmarshal(request, &req) and
change the created jsonrpcResponse to use an explicit nil ID value rather than
req.ID so the response follows JSON-RPC 2.0 (null id) on parse errors.

Comment thread go.mod Outdated
@thebtf thebtf merged commit 6aa4720 into main Apr 14, 2026
2 checks passed
@thebtf thebtf deleted the feat/session-handler branch April 14, 2026 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant