From 9c71a444212f20a4cbdcb0f4bbeccd84bb6c3d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E6=99=BA=E8=B6=850668000959?= Date: Tue, 2 Jun 2026 22:44:53 +0800 Subject: [PATCH 1/2] fix(session): skip main-session alias during history promotion The PromoteAliasHistory method previously promoted the first non-empty alias session into a new canonical session. When a user upgraded, the migrated main session contained old messages that were copied into every new Web UI session because agent:main:main is always the first alias. Add isMainSessionAlias() to detect and skip the main session alias during promotion. Fixes #2972. --- pkg/memory/jsonl.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pkg/memory/jsonl.go b/pkg/memory/jsonl.go index a63598f4e..463aec0e0 100644 --- a/pkg/memory/jsonl.go +++ b/pkg/memory/jsonl.go @@ -4,6 +4,8 @@ import ( "bufio" "bytes" "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "hash/fnv" @@ -227,6 +229,11 @@ func (s *JSONLStore) UpsertSessionMeta( // PromoteAliasHistory atomically promotes the first non-empty alias session // into the canonical session when the canonical session is still empty. +// +// Main-session aliases (e.g. "agent:main:main" or its opaque form) are +// skipped during promotion. The main session is a shared global fallback +// and promoting its history into individual sessions would attach stale +// messages to every new Web UI session (issue #2972). func (s *JSONLStore) PromoteAliasHistory( _ context.Context, sessionKey string, @@ -240,6 +247,9 @@ func (s *JSONLStore) PromoteAliasHistory( aliases = normalizeAliases(sessionKey, aliases) for _, alias := range aliases { + if isMainSessionAlias(alias) { + continue + } unlock := s.lockSessionPair(sessionKey, alias) promoted, err := s.promoteAliasHistoryLocked(sessionKey, alias, scope, aliases) unlock() @@ -251,6 +261,30 @@ func (s *JSONLStore) PromoteAliasHistory( return false, nil } +// isMainSessionAlias reports whether alias is the legacy or opaque main-session +// key. The main session ("agent::main") is a shared fallback and should +// not have its history promoted into individual per-channel sessions. +func isMainSessionAlias(alias string) bool { + if alias == "" { + return false + } + // Legacy form: "agent:main:main" (case-insensitive) + if strings.HasPrefix(alias, "agent:") && strings.HasSuffix(alias, ":main") { + return true + } + // Opaque form: "sk_v1_" + SHA256("agent:main:main") + if strings.HasPrefix(alias, "sk_v1_") { + for _, agentID := range []string{"main", "Main", "MAIN"} { + legacy := "agent:" + agentID + ":main" + hash := sha256.Sum256([]byte(legacy)) + if "sk_v1_"+hex.EncodeToString(hash[:]) == alias { + return true + } + } + } + return false +} + // ResolveSessionKey returns the canonical session key for a candidate key. // It short-circuits direct canonical keys when possible, then scans metadata // once to resolve aliases or canonical metadata keys. From 04664ab514a4b0db26be5860bed388b1c9a8e495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E6=99=BA=E8=B6=850668000959?= Date: Wed, 3 Jun 2026 16:46:32 +0800 Subject: [PATCH 2/2] fix(session): tighten main-session alias detection to exact 3-part format Only match agent:X:main, not agent:X:direct:main or agent:X:slack:channel:main. Review feedback from afjcjsbx. --- pkg/memory/jsonl.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/memory/jsonl.go b/pkg/memory/jsonl.go index 463aec0e0..3b2f9ff3f 100644 --- a/pkg/memory/jsonl.go +++ b/pkg/memory/jsonl.go @@ -268,9 +268,13 @@ func isMainSessionAlias(alias string) bool { if alias == "" { return false } - // Legacy form: "agent:main:main" (case-insensitive) + // Legacy form: "agent:main:main" (exactly 3 colon-separated parts) + // Must not match "agent:sales:direct:main" etc. if strings.HasPrefix(alias, "agent:") && strings.HasSuffix(alias, ":main") { - return true + parts := strings.SplitN(alias, ":", 4) + if len(parts) == 3 { + return true + } } // Opaque form: "sk_v1_" + SHA256("agent:main:main") if strings.HasPrefix(alias, "sk_v1_") {