fix(session): restore thread and legacy compatibility

This commit is contained in:
Hoshina
2026-04-08 00:32:53 +08:00
parent a827d01d7c
commit 296077eabf
18 changed files with 568 additions and 46 deletions
+56 -10
View File
@@ -44,6 +44,7 @@ func AllocateRouteSession(input AllocationInput) Allocation {
func buildSessionScope(input AllocationInput) SessionScope {
inbound := input.Context
includeTopicInChatDimension := shouldPreserveTelegramForumIsolation(input)
scope := SessionScope{
Version: ScopeVersionV1,
AgentID: routing.NormalizeAgentID(input.AgentID),
@@ -73,6 +74,11 @@ func buildSessionScope(input AllocationInput) SessionScope {
if chatID == "" {
continue
}
if includeTopicInChatDimension {
if topicID := strings.TrimSpace(inbound.TopicID); topicID != "" {
chatID = chatID + "/" + topicID
}
}
chatType := strings.ToLower(strings.TrimSpace(inbound.ChatType))
if chatType == "" {
chatType = "direct"
@@ -111,18 +117,16 @@ func buildLegacySessionAliases(input AllocationInput) []string {
inbound := input.Context
if strings.EqualFold(strings.TrimSpace(inbound.ChatType), "direct") {
senderID := CanonicalSessionIdentityID(
inbound.Channel,
inbound.SenderID,
input.SessionPolicy.IdentityLinks,
)
if senderID == "" {
peerIDs := buildLegacyDirectPeerIDs(input)
if len(peerIDs) == 0 {
return uniqueAliases(aliases)
}
aliases = append(
aliases,
BuildLegacyDirectAliases(input.AgentID, inbound.Channel, inbound.Account, senderID)...,
)
for _, peerID := range peerIDs {
aliases = append(
aliases,
BuildLegacyDirectAliases(input.AgentID, inbound.Channel, inbound.Account, peerID)...,
)
}
return uniqueAliases(aliases)
}
@@ -143,6 +147,48 @@ func buildLegacySessionAliases(input AllocationInput) []string {
return uniqueAliases(aliases)
}
func shouldPreserveTelegramForumIsolation(input AllocationInput) bool {
inbound := input.Context
if !strings.EqualFold(strings.TrimSpace(inbound.Channel), "telegram") {
return false
}
if strings.TrimSpace(inbound.TopicID) == "" {
return false
}
for _, dimension := range input.SessionPolicy.Dimensions {
if strings.EqualFold(strings.TrimSpace(dimension), "topic") {
return false
}
}
return true
}
func buildLegacyDirectPeerIDs(input AllocationInput) []string {
inbound := input.Context
peerIDs := make([]string, 0, 3)
rawSenderID := strings.TrimSpace(inbound.SenderID)
if rawSenderID != "" {
peerIDs = append(peerIDs, strings.ToLower(rawSenderID))
}
canonicalSenderID := CanonicalSessionIdentityID(
inbound.Channel,
inbound.SenderID,
input.SessionPolicy.IdentityLinks,
)
if canonicalSenderID != "" {
peerIDs = append(peerIDs, canonicalSenderID)
}
chatID := strings.TrimSpace(inbound.ChatID)
if chatID != "" {
peerIDs = append(peerIDs, strings.ToLower(chatID))
}
return uniqueAliases(peerIDs)
}
func uniqueAliases(aliases []string) []string {
if len(aliases) == 0 {
return nil
+59
View File
@@ -80,6 +80,65 @@ func TestAllocateRouteSession_GroupPeer(t *testing.T) {
}
}
func TestAllocateRouteSession_TelegramForumTopicsRemainIsolatedByDefault(t *testing.T) {
first := AllocateRouteSession(AllocationInput{
AgentID: "main",
Context: bus.InboundContext{
Channel: "telegram",
ChatID: "-1001234567890",
ChatType: "group",
TopicID: "42",
SenderID: "7",
},
SessionPolicy: routing.SessionPolicy{
Dimensions: []string{"chat"},
},
})
second := AllocateRouteSession(AllocationInput{
AgentID: "main",
Context: bus.InboundContext{
Channel: "telegram",
ChatID: "-1001234567890",
ChatType: "group",
TopicID: "99",
SenderID: "7",
},
SessionPolicy: routing.SessionPolicy{
Dimensions: []string{"chat"},
},
})
if first.SessionKey == second.SessionKey {
t.Fatalf("forum topics should not share default session key: %q", first.SessionKey)
}
if got := first.Scope.Values["chat"]; got != "group:-1001234567890/42" {
t.Fatalf("first.Scope.Values[chat] = %q, want %q", got, "group:-1001234567890/42")
}
if got := second.Scope.Values["chat"]; got != "group:-1001234567890/99" {
t.Fatalf("second.Scope.Values[chat] = %q, want %q", got, "group:-1001234567890/99")
}
}
func TestAllocateRouteSession_PicoDirectAliasesIncludeLegacyChatKey(t *testing.T) {
allocation := AllocateRouteSession(AllocationInput{
AgentID: "main",
Context: bus.InboundContext{
Channel: "pico",
Account: "default",
ChatID: "pico:session-123",
ChatType: "direct",
SenderID: "pico-user",
},
SessionPolicy: routing.SessionPolicy{
Dimensions: []string{"sender"},
},
})
if !containsAlias(allocation.SessionAliases, "agent:main:pico:direct:pico:session-123") {
t.Fatalf("SessionAliases = %v, want pico legacy alias", allocation.SessionAliases)
}
}
func TestBuildOpaqueSessionKey_IsStable(t *testing.T) {
first := BuildOpaqueSessionKey("agent:main:direct:user123")
second := BuildOpaqueSessionKey("agent:main:direct:user123")
+7
View File
@@ -84,6 +84,13 @@ func (b *JSONLBackend) EnsureSessionMetadata(sessionKey string, scope *SessionSc
return
}
canonicalMeta, metaErr := metaStore.GetSessionMeta(ctx, sessionKey)
if metaErr != nil {
log.Printf("session: get canonical session metadata: %v", metaErr)
} else if canonicalMeta.Count > 0 || strings.TrimSpace(canonicalMeta.Summary) != "" {
return
}
canonicalHistory, historyErr := b.store.GetHistory(ctx, sessionKey)
if historyErr != nil {
log.Printf("session: get canonical history: %v", historyErr)
+43
View File
@@ -4,8 +4,10 @@ import (
"fmt"
"testing"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/memory"
"github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/routing"
"github.com/sipeed/picoclaw/pkg/session"
)
@@ -239,3 +241,44 @@ func TestJSONLBackend_EnsureSessionMetadata_PromotesLegacyAliasHistory(t *testin
t.Fatalf("promoted summary = %q, want %q", summary, "legacy summary")
}
}
func TestJSONLBackend_EnsureSessionMetadata_PromotesLegacyPicoDirectAliasHistory(t *testing.T) {
b := newBackend(t)
legacyKey := "agent:main:pico:direct:pico:session-123"
b.AddMessage(legacyKey, "user", "legacy pico history")
scope := &session.SessionScope{
Version: session.ScopeVersionV1,
AgentID: "main",
Channel: "pico",
Account: "default",
Dimensions: []string{"sender"},
Values: map[string]string{
"sender": "pico-user",
},
}
allocation := session.AllocateRouteSession(session.AllocationInput{
AgentID: "main",
Context: bus.InboundContext{
Channel: "pico",
Account: "default",
ChatID: "pico:session-123",
ChatType: "direct",
SenderID: "pico-user",
},
SessionPolicy: routing.SessionPolicy{
Dimensions: []string{"sender"},
},
})
b.EnsureSessionMetadata(allocation.SessionKey, scope, allocation.SessionAliases)
if got := b.ResolveSessionKey(legacyKey); got != allocation.SessionKey {
t.Fatalf("ResolveSessionKey() = %q, want %q", got, allocation.SessionKey)
}
history := b.GetHistory(allocation.SessionKey)
if len(history) != 1 || history[0].Content != "legacy pico history" {
t.Fatalf("promoted history = %+v", history)
}
}