From 921d753cc0b6595faae4a64117ce94d490b3ba00 Mon Sep 17 00:00:00 2001 From: SiYue-ZO <2835601846@qq.com> Date: Tue, 9 Jun 2026 11:03:33 +0800 Subject: [PATCH] fix: wire dm_scope into runtime session isolation dimensions The dm_scope field was stored in config but never translated into the dimensions array that the routing layer actually consumes. This meant changing the session isolation scope in the UI had no effect at runtime. Add ApplyDmScope() to SessionConfig which maps the user-facing dm_scope values (per-channel-peer, per-channel, per-peer, global) to the corresponding dimension arrays. Call it in LoadConfig post-processing and in both the PATCH and PUT API handlers. Includes table-driven tests covering all dm_scope values and the precedence rule (explicit dimensions > derived from dm_scope). --- pkg/config/config.go | 22 +++++++++++++++ pkg/config/config_test.go | 59 +++++++++++++++++++++++++++++++++++++++ web/backend/api/config.go | 2 ++ 3 files changed, 83 insertions(+) diff --git a/pkg/config/config.go b/pkg/config/config.go index f3cbf43ad..20cdd74c2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -352,6 +352,26 @@ type SessionConfig struct { DmScope string `json:"dm_scope,omitempty"` } +// ApplyDmScope translates the user-facing dm_scope value into the internal +// dimensions array that the routing layer consumes. It is a no-op when +// DmScope is empty or when Dimensions is already set (explicit Dimensions +// take precedence over the derived value). +func (s *SessionConfig) ApplyDmScope() { + if s.DmScope == "" || len(s.Dimensions) > 0 { + return + } + switch s.DmScope { + case "per-channel-peer": + s.Dimensions = []string{"chat", "sender"} + case "per-channel": + s.Dimensions = []string{"chat"} + case "per-peer": + s.Dimensions = []string{"sender"} + case "global": + s.Dimensions = nil + } +} + // RoutingConfig controls the intelligent model routing feature. // When enabled, each incoming message is scored against structural features // (message length, code blocks, tool call history, conversation depth, attachments). @@ -1477,6 +1497,8 @@ func LoadConfig(path string) (*Config, error) { cfg.Agents.Defaults.Workspace = filepath.Join(homePath, pkg.WorkspaceName) } + cfg.Session.ApplyDmScope() + return cfg, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ea90fafe4..12cd70734 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1728,6 +1728,65 @@ func TestDefaultConfig_SessionDimensions(t *testing.T) { } } +func TestSessionConfig_ApplyDmScope(t *testing.T) { + tests := []struct { + name string + dmScope string + dimensions []string + want []string + }{ + { + name: "per-channel-peer", + dmScope: "per-channel-peer", + want: []string{"chat", "sender"}, + }, + { + name: "per-channel", + dmScope: "per-channel", + want: []string{"chat"}, + }, + { + name: "per-peer", + dmScope: "per-peer", + want: []string{"sender"}, + }, + { + name: "global", + dmScope: "global", + want: nil, + }, + { + name: "explicit dimensions take precedence", + dmScope: "per-channel-peer", + dimensions: []string{"sender"}, + want: []string{"sender"}, + }, + { + name: "empty dm_scope is no-op", + dmScope: "", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &SessionConfig{ + DmScope: tt.dmScope, + Dimensions: tt.dimensions, + } + s.ApplyDmScope() + if len(s.Dimensions) != len(tt.want) { + t.Fatalf("Dimensions = %v, want %v", s.Dimensions, tt.want) + } + for i, v := range tt.want { + if s.Dimensions[i] != v { + t.Errorf("Dimensions[%d] = %q, want %q", i, s.Dimensions[i], v) + } + } + }) + } +} + func TestDefaultConfig_WorkspacePath_Default(t *testing.T) { t.Setenv("PICOCLAW_HOME", "") diff --git a/web/backend/api/config.go b/web/backend/api/config.go index b37958040..50d9b0293 100644 --- a/web/backend/api/config.go +++ b/web/backend/api/config.go @@ -76,6 +76,7 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) return } + cfg.Session.ApplyDmScope() if execAllowRemoteOmitted(body) { cfg.Tools.Exec.AllowRemote = config.DefaultConfig().Tools.Exec.AllowRemote } @@ -181,6 +182,7 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("Merged config is invalid: %v", err), http.StatusBadRequest) return } + newCfg.Session.ApplyDmScope() // Restore security fields (tokens/keys) from the loaded config before validation, // because private fields are lost during JSON round-trip.