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).
This commit is contained in:
SiYue-ZO
2026-06-09 11:03:33 +08:00
parent 0bbd8f081e
commit 921d753cc0
3 changed files with 83 additions and 0 deletions
+22
View File
@@ -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
}
+59
View File
@@ -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", "")
+2
View File
@@ -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.