From 49b69cda811b00a8cd5e6d53c475763f8676d66f Mon Sep 17 00:00:00 2001 From: mingmxren Date: Sun, 1 Mar 2026 12:17:52 +0800 Subject: [PATCH] feat(session): add scope-aware /new and /session commands # Conflicts: # pkg/agent/loop_test.go --- README.md | 21 +++- README.zh.md | 21 +++- config/config.example.json | 4 + pkg/agent/loop.go | 174 ++++++++++++++++++++++++++------ pkg/agent/loop_test.go | 170 ++++++++++++++++++++++++++++++- pkg/commands/builtin.go | 13 +++ pkg/commands/builtin_test.go | 5 +- pkg/commands/dispatcher.go | 4 +- pkg/commands/dispatcher_test.go | 14 +++ pkg/config/config.go | 16 ++- pkg/config/config_test.go | 38 +++++++ pkg/config/defaults.go | 3 +- 12 files changed, 444 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 84bd8fa44..45a20abff 100644 --- a/README.md +++ b/README.md @@ -338,7 +338,7 @@ picoclaw gateway **4. Telegram command menu (auto-registered at startup)** -PicoClaw now keeps command definitions in one shared registry. On startup, Telegram will automatically register supported bot commands (for example `/start`, `/help`, `/show`, `/list`) so command menu and runtime behavior stay in sync. +PicoClaw now keeps command definitions in one shared registry. On startup, Telegram will automatically register supported bot commands (for example `/start`, `/help`, `/new`, `/session`, `/show`, `/list`) so command menu and runtime behavior stay in sync. If command registration fails (network/API transient errors), the channel still starts and PicoClaw retries registration in the background. @@ -648,6 +648,21 @@ PicoClaw stores data in your configured workspace (default: `~/.picoclaw/workspa └── USER.md # User preferences ``` +### Session Scope and Backlog + +Use `session.dm_scope` to control DM session isolation and `session.backlog_limit` to cap how many sessions are retained per scope: + +```json +{ + "session": { + "dm_scope": "per-channel-peer", + "backlog_limit": 20 + } +} +``` + +`/new` (or `/reset`) starts a fresh active session for the current scope. `/session list` and `/session resume ` operate within that same scope. + ### 🔒 Security Sandbox PicoClaw runs in a sandboxed environment by default. The agent can only access files and execute commands within the configured workspace. @@ -1089,6 +1104,10 @@ picoclaw agent -m "Hello" "model": "anthropic/claude-opus-4-5" } }, + "session": { + "dm_scope": "per-channel-peer", + "backlog_limit": 20 + }, "providers": { "openrouter": { "api_key": "sk-or-v1-xxx" diff --git a/README.zh.md b/README.zh.md index cc958a225..63189837e 100644 --- a/README.zh.md +++ b/README.zh.md @@ -307,7 +307,7 @@ PicoClaw 支持多种聊天平台,使您的 Agent 能够连接到任何地方 ### Telegram 命令注册(启动时自动同步) -PicoClaw 现在使用统一的命令定义来源。启动时会自动将 Telegram 支持的命令(例如 `/start`、`/help`、`/show`、`/list`)注册到 Bot 命令菜单,确保菜单展示与实际行为一致。 +PicoClaw 现在使用统一的命令定义来源。启动时会自动将 Telegram 支持的命令(例如 `/start`、`/help`、`/new`、`/session`、`/show`、`/list`)注册到 Bot 命令菜单,确保菜单展示与实际行为一致。 如果注册因网络或 API 短暂异常失败,不会阻塞 channel 启动;系统会在后台自动重试。 @@ -341,6 +341,21 @@ PicoClaw 将数据存储在您配置的工作区中(默认:`~/.picoclaw/work ``` +### 会话作用域与保留上限 + +使用 `session.dm_scope` 控制私聊会话隔离粒度,使用 `session.backlog_limit` 控制每个作用域保留多少历史会话: + +```json +{ + "session": { + "dm_scope": "per-channel-peer", + "backlog_limit": 20 + } +} +``` + +`/new`(或 `/reset`)会在当前作用域创建新会话;`/session list` 和 `/session resume ` 只在当前作用域内生效。 + ### 心跳 / 周期性任务 (Heartbeat) PicoClaw 可以自动执行周期性任务。在工作区创建 `HEARTBEAT.md` 文件: @@ -680,6 +695,10 @@ picoclaw agent -m "你好" "model": "anthropic/claude-opus-4-5" } }, + "session": { + "dm_scope": "per-channel-peer", + "backlog_limit": 20 + }, "providers": { "openrouter": { "api_key": "sk-or-v1-xxx" diff --git a/config/config.example.json b/config/config.example.json index 55a823009..f1b6c5903 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -9,6 +9,10 @@ "max_tool_iterations": 20 } }, + "session": { + "dm_scope": "per-channel-peer", + "backlog_limit": 20 + }, "model_list": [ { "model_name": "gpt4", diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index a72f95bb1..226fb76ac 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "path/filepath" + "strconv" "strings" "sync" "sync/atomic" @@ -369,12 +370,52 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) return al.processSystemMessage(ctx, msg) } + route, agent, err := al.resolveMessageRoute(msg) + if err != nil { + return "", err + } + // Check for commands - if response, handled := al.handleCommand(ctx, msg); handled { + if response, handled := al.handleCommand(ctx, msg, route, agent); handled { return response, nil } - // Route to determine agent and session key + // Reset message-tool state for this round so we don't skip publishing due to a previous round. + if tool, ok := agent.Tools.Get("message"); ok { + if mt, ok := tool.(tools.ContextualTool); ok { + mt.SetContext(msg.Channel, msg.ChatID) + } + } + + // Resolve active session from the routing scope. + scopeKey := resolveScopeKey(route, msg.SessionKey) + sessionKey, err := agent.Sessions.ResolveActive(scopeKey) + if err != nil { + return "", fmt.Errorf("resolve active session: %w", err) + } + + logger.InfoCF("agent", "Routed message", + map[string]any{ + "agent_id": agent.ID, + "scope_key": scopeKey, + "session_key": sessionKey, + "matched_by": route.MatchedBy, + "route_agent": route.AgentID, + "route_channel": route.Channel, + }) + + return al.runAgentLoop(ctx, agent, processOptions{ + SessionKey: sessionKey, + Channel: msg.Channel, + ChatID: msg.ChatID, + UserMessage: msg.Content, + DefaultResponse: defaultResponse, + EnableSummary: true, + SendResponse: false, + }) +} + +func (al *AgentLoop) resolveMessageRoute(msg bus.InboundMessage) (routing.ResolvedRoute, *AgentInstance, error) { route := al.registry.ResolveRoute(routing.RouteInput{ Channel: msg.Channel, AccountID: msg.Metadata["account_id"], @@ -389,38 +430,17 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) agent = al.registry.GetDefaultAgent() } if agent == nil { - return "", fmt.Errorf("no agent available for route (agent_id=%s)", route.AgentID) + return routing.ResolvedRoute{}, nil, fmt.Errorf("no agent available for route (agent_id=%s)", route.AgentID) } - // Reset message-tool state for this round so we don't skip publishing due to a previous round. - if tool, ok := agent.Tools.Get("message"); ok { - if mt, ok := tool.(tools.ContextualTool); ok { - mt.SetContext(msg.Channel, msg.ChatID) - } + return route, agent, nil +} + +func resolveScopeKey(route routing.ResolvedRoute, msgSessionKey string) string { + if msgSessionKey != "" && strings.HasPrefix(msgSessionKey, "agent:") { + return msgSessionKey } - - // Use routed session key, but honor pre-set agent-scoped keys (for ProcessDirect/cron) - sessionKey := route.SessionKey - if msg.SessionKey != "" && strings.HasPrefix(msg.SessionKey, "agent:") { - sessionKey = msg.SessionKey - } - - logger.InfoCF("agent", "Routed message", - map[string]any{ - "agent_id": agent.ID, - "session_key": sessionKey, - "matched_by": route.MatchedBy, - }) - - return al.runAgentLoop(ctx, agent, processOptions{ - SessionKey: sessionKey, - Channel: msg.Channel, - ChatID: msg.ChatID, - UserMessage: msg.Content, - DefaultResponse: defaultResponse, - EnableSummary: true, - SendResponse: false, - }) + return route.SessionKey } func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMessage) (string, error) { @@ -1235,7 +1255,12 @@ func (al *AgentLoop) estimateTokens(messages []providers.Message) int { return totalChars * 2 / 5 } -func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage) (string, bool) { +func (al *AgentLoop) handleCommand( + ctx context.Context, + msg bus.InboundMessage, + route routing.ResolvedRoute, + agent *AgentInstance, +) (string, bool) { content := strings.TrimSpace(msg.Content) if !strings.HasPrefix(content, "/") { return "", false @@ -1247,9 +1272,94 @@ func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage) } cmd := parts[0] + if at := strings.Index(cmd, "@"); at > 0 { + cmd = cmd[:at] + } args := parts[1:] switch cmd { + case "/new", "/reset": + scopeKey := resolveScopeKey(route, msg.SessionKey) + newSessionKey, err := agent.Sessions.StartNew(scopeKey) + if err != nil { + return fmt.Sprintf("Failed to start new session: %v", err), true + } + + backlogLimit := config.DefaultSessionBacklogLimit + if al.cfg != nil { + backlogLimit = al.cfg.Session.EffectiveBacklogLimit() + } + + pruned, err := agent.Sessions.Prune(scopeKey, backlogLimit) + if err != nil { + return fmt.Sprintf( + "Started new session (%s), but pruning old sessions failed: %v", + newSessionKey, + err, + ), true + } + + if len(pruned) == 0 { + return fmt.Sprintf("Started new session: %s", newSessionKey), true + } + return fmt.Sprintf("Started new session: %s (pruned %d old session(s))", newSessionKey, len(pruned)), true + + case "/session": + if len(args) < 1 { + return "Usage: /session [list|resume ]", true + } + + scopeKey := resolveScopeKey(route, msg.SessionKey) + switch args[0] { + case "list": + list, err := agent.Sessions.List(scopeKey) + if err != nil { + return fmt.Sprintf("Failed to list sessions: %v", err), true + } + if len(list) == 0 { + return "No sessions found for current chat.", true + } + + lines := make([]string, 0, len(list)+1) + lines = append(lines, "Sessions for current chat:") + for _, item := range list { + activeMarker := " " + if item.Active { + activeMarker = "*" + } + updated := "-" + if !item.UpdatedAt.IsZero() { + updated = item.UpdatedAt.Format("2006-01-02 15:04") + } + lines = append(lines, fmt.Sprintf( + "%d. [%s] %s (%d msgs, updated %s)", + item.Ordinal, + activeMarker, + item.SessionKey, + item.MessageCnt, + updated, + )) + } + return strings.Join(lines, "\n"), true + + case "resume": + if len(args) != 2 { + return "Usage: /session resume ", true + } + index, err := strconv.Atoi(args[1]) + if err != nil || index < 1 { + return "Usage: /session resume ", true + } + sessionKey, err := agent.Sessions.Resume(scopeKey, index) + if err != nil { + return fmt.Sprintf("Failed to resume session %d: %v", index, err), true + } + return fmt.Sprintf("Resumed session %d: %s", index, sessionKey), true + + default: + return "Usage: /session [list|resume ]", true + } + case "/show": if len(args) < 1 { return "Usage: /show [model|channel|agents]", true diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index 1034b06e8..d892b0dde 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "path/filepath" - "slices" "testing" "time" @@ -13,6 +12,7 @@ import ( "github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/providers" + "github.com/sipeed/picoclaw/pkg/routing" "github.com/sipeed/picoclaw/pkg/tools" ) @@ -438,6 +438,174 @@ func (h testHelper) executeAndGetResponse(tb testing.TB, ctx context.Context, ms const responseTimeout = 3 * time.Second +func TestProcessMessage_UsesResolvedActiveSession(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "agent-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + cfg := &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + Workspace: tmpDir, + Model: "test-model", + MaxTokens: 4096, + MaxToolIterations: 10, + }, + }, + } + + msgBus := bus.NewMessageBus() + provider := &simpleMockProvider{response: "ok"} + al := NewAgentLoop(cfg, msgBus, provider) + + msg := bus.InboundMessage{ + Channel: "telegram", + SenderID: "user1", + ChatID: "chat1", + Content: "hello", + Peer: bus.Peer{ + Kind: "direct", + ID: "user1", + }, + } + + route := al.registry.ResolveRoute(routing.RouteInput{ + Channel: msg.Channel, + Peer: extractPeer(msg), + }) + scopeKey := route.SessionKey + + defaultAgent := al.registry.GetDefaultAgent() + if defaultAgent == nil { + t.Fatal("No default agent found") + } + + rotated, err := defaultAgent.Sessions.StartNew(scopeKey) + if err != nil { + t.Fatalf("StartNew(%q) failed: %v", scopeKey, err) + } + + helper := testHelper{al: al} + _ = helper.executeAndGetResponse(t, context.Background(), msg) + + if got := len(defaultAgent.Sessions.GetHistory(scopeKey)); got != 0 { + t.Fatalf("expected base scope history len=0, got %d", got) + } + + rotatedHistory := defaultAgent.Sessions.GetHistory(rotated) + if len(rotatedHistory) != 2 { + t.Fatalf("expected rotated history len=2, got %d", len(rotatedHistory)) + } + if rotatedHistory[0].Role != "user" || rotatedHistory[0].Content != "hello" { + t.Fatalf("unexpected first message in rotated session: %+v", rotatedHistory[0]) + } +} + +func TestHandleCommand_NewAndSessionCommands(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "agent-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + cfg := &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + Workspace: tmpDir, + Model: "test-model", + MaxTokens: 4096, + MaxToolIterations: 10, + }, + }, + Session: config.SessionConfig{ + DMScope: "per-channel-peer", + BacklogLimit: 20, + }, + } + + msgBus := bus.NewMessageBus() + provider := &simpleMockProvider{response: "ok"} + al := NewAgentLoop(cfg, msgBus, provider) + helper := testHelper{al: al} + + baseMsg := bus.InboundMessage{ + Channel: "telegram", + SenderID: "user1", + ChatID: "chat1", + Peer: bus.Peer{ + Kind: "direct", + ID: "user1", + }, + } + + route := al.registry.ResolveRoute(routing.RouteInput{ + Channel: baseMsg.Channel, + Peer: extractPeer(baseMsg), + }) + scopeKey := route.SessionKey + defaultAgent := al.registry.GetDefaultAgent() + if defaultAgent == nil { + t.Fatal("No default agent found") + } + + respNew1 := helper.executeAndGetResponse(t, context.Background(), bus.InboundMessage{ + Channel: baseMsg.Channel, + SenderID: baseMsg.SenderID, + ChatID: baseMsg.ChatID, + Content: "/new", + Peer: baseMsg.Peer, + }) + if !strings.Contains(respNew1, scopeKey+"#2") { + t.Fatalf("/new response missing new session key, got: %q", respNew1) + } + + respNew2 := helper.executeAndGetResponse(t, context.Background(), bus.InboundMessage{ + Channel: baseMsg.Channel, + SenderID: baseMsg.SenderID, + ChatID: baseMsg.ChatID, + Content: "/new", + Peer: baseMsg.Peer, + }) + if !strings.Contains(respNew2, scopeKey+"#3") { + t.Fatalf("second /new response missing session #3, got: %q", respNew2) + } + + listResp := helper.executeAndGetResponse(t, context.Background(), bus.InboundMessage{ + Channel: baseMsg.Channel, + SenderID: baseMsg.SenderID, + ChatID: baseMsg.ChatID, + Content: "/session list", + Peer: baseMsg.Peer, + }) + if !strings.Contains(listResp, "1. [*] "+scopeKey+"#3") { + t.Fatalf("/session list response missing active session #3, got:\n%s", listResp) + } + if !strings.Contains(listResp, "3. [ ] "+scopeKey) { + t.Fatalf("/session list response missing base session ordinal, got:\n%s", listResp) + } + + resumeResp := helper.executeAndGetResponse(t, context.Background(), bus.InboundMessage{ + Channel: baseMsg.Channel, + SenderID: baseMsg.SenderID, + ChatID: baseMsg.ChatID, + Content: "/session resume 3", + Peer: baseMsg.Peer, + }) + if !strings.Contains(resumeResp, scopeKey) { + t.Fatalf("/session resume response missing target session key, got: %q", resumeResp) + } + + active, err := defaultAgent.Sessions.ResolveActive(scopeKey) + if err != nil { + t.Fatalf("ResolveActive failed: %v", err) + } + if active != scopeKey { + t.Fatalf("active session = %q, want %q after resume", active, scopeKey) + } +} + // TestToolResult_SilentToolDoesNotSendUserMessage verifies silent tools don't trigger outbound func TestToolResult_SilentToolDoesNotSendUserMessage(t *testing.T) { tmpDir, err := os.MkdirTemp("", "agent-test-*") diff --git a/pkg/commands/builtin.go b/pkg/commands/builtin.go index f9622bb6c..6c3591246 100644 --- a/pkg/commands/builtin.go +++ b/pkg/commands/builtin.go @@ -30,6 +30,19 @@ func BuiltinDefinitions(cfg *config.Config) []Definition { return req.Reply(FormatHelpMessage(defs)) }, }, + { + Name: "new", + Aliases: []string{"reset"}, + Description: "Start a new chat session", + Usage: "/new", + Channels: []string{"telegram", "whatsapp", "whatsapp_native"}, + }, + { + Name: "session", + Description: "Manage chat sessions", + Usage: "/session [list|resume ]", + Channels: []string{"telegram", "whatsapp", "whatsapp_native"}, + }, { Name: "show", Description: "Show current configuration", diff --git a/pkg/commands/builtin_test.go b/pkg/commands/builtin_test.go index f1a302ebd..44c1cca5a 100644 --- a/pkg/commands/builtin_test.go +++ b/pkg/commands/builtin_test.go @@ -8,7 +8,7 @@ func TestBuiltinDefinitions_ContainsTelegramDefaults(t *testing.T) { for _, d := range defs { names[d.Name] = true } - for _, want := range []string{"help", "start", "show", "list"} { + for _, want := range []string{"help", "start", "new", "session", "show", "list"} { if !names[want] { t.Fatalf("missing command %q", want) } @@ -24,6 +24,9 @@ func TestBuiltinDefinitions_WhatsAppOnlyHasBasicCommands(t *testing.T) { if !names["start"] || !names["help"] { t.Fatalf("whatsapp should include start/help, got %+v", names) } + if !names["new"] || !names["session"] { + t.Fatalf("whatsapp should include new/session, got %+v", names) + } if names["show"] || names["list"] { t.Fatalf("whatsapp should not include show/list, got %+v", names) } diff --git a/pkg/commands/dispatcher.go b/pkg/commands/dispatcher.go index 031967018..2d8859760 100644 --- a/pkg/commands/dispatcher.go +++ b/pkg/commands/dispatcher.go @@ -52,7 +52,9 @@ func (d *Dispatcher) Dispatch(ctx context.Context, req Request) Result { continue } if def.Handler == nil { - return Result{Matched: true, Handled: false, Command: def.Name} + // Definition-only command (for menu registration / discovery). + // Let the inbound message continue to the agent loop. + return Result{Matched: false, Handled: false, Command: def.Name} } err := def.Handler(ctx, req) return Result{Matched: true, Handled: true, Command: def.Name, Err: err} diff --git a/pkg/commands/dispatcher_test.go b/pkg/commands/dispatcher_test.go index 44626d323..8590e4860 100644 --- a/pkg/commands/dispatcher_test.go +++ b/pkg/commands/dispatcher_test.go @@ -59,3 +59,17 @@ func TestDispatcher_MatchTelegramMentionSyntax(t *testing.T) { t.Fatalf("dispatch result = %+v, called=%v", res, called) } } + +func TestDispatcher_PassThroughDefinitionWithoutHandler(t *testing.T) { + d := NewDispatcher(NewRegistry([]Definition{ + {Name: "session"}, // menu-only / pass-through definition + })) + + res := d.Dispatch(context.Background(), Request{ + Channel: "telegram", + Text: "/session list", + }) + if res.Matched { + t.Fatalf("expected pass-through unmatched result, got %+v", res) + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index d84772d2b..04e7d69ce 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -78,7 +78,7 @@ func (c Config) MarshalJSON() ([]byte, error) { } // Only include session if not empty - if c.Session.DMScope != "" || len(c.Session.IdentityLinks) > 0 { + if c.Session.DMScope != "" || len(c.Session.IdentityLinks) > 0 || c.Session.BacklogLimit > 0 { aux.Session = &c.Session } @@ -165,6 +165,16 @@ type AgentBinding struct { type SessionConfig struct { DMScope string `json:"dm_scope,omitempty"` IdentityLinks map[string][]string `json:"identity_links,omitempty"` + BacklogLimit int `json:"backlog_limit,omitempty"` +} + +const DefaultSessionBacklogLimit = 20 + +func (s SessionConfig) EffectiveBacklogLimit() int { + if s.BacklogLimit < 1 { + return DefaultSessionBacklogLimit + } + return s.BacklogLimit } type AgentDefaults struct { @@ -609,6 +619,10 @@ func LoadConfig(path string) (*Config, error) { return nil, err } + if cfg.Session.BacklogLimit < 1 { + cfg.Session.BacklogLimit = DefaultSessionBacklogLimit + } + // Migrate legacy channel config fields to new unified structures cfg.migrateChannelConfigs() diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 12fd10b50..fea057c45 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -442,3 +442,41 @@ func TestDefaultConfig_DMScope(t *testing.T) { t.Errorf("Session.DMScope = %q, want 'per-channel-peer'", cfg.Session.DMScope) } } + +func TestDefaultConfig_SessionBacklogLimit(t *testing.T) { + cfg := DefaultConfig() + + if cfg.Session.BacklogLimit != DefaultSessionBacklogLimit { + t.Errorf( + "Session.BacklogLimit = %d, want %d", + cfg.Session.BacklogLimit, + DefaultSessionBacklogLimit, + ) + } +} + +func TestLoadConfig_InvalidBacklogLimitFallsBackToDefault(t *testing.T) { + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, "config.json") + + configJSON := `{ + "agents": {"defaults":{"workspace":"./workspace","model":"gpt4","max_tokens":8192,"max_tool_iterations":20}}, + "session": {"backlog_limit": 0}, + "model_list": [{"model_name":"gpt4","model":"openai/gpt-5.2","api_key":"x"}] +}` + if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil { + t.Fatalf("os.WriteFile() error: %v", err) + } + + cfg, err := LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig() error: %v", err) + } + if cfg.Session.BacklogLimit != DefaultSessionBacklogLimit { + t.Fatalf( + "Session.BacklogLimit = %d, want %d", + cfg.Session.BacklogLimit, + DefaultSessionBacklogLimit, + ) + } +} diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index ebb924859..d8d33fc0a 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -21,7 +21,8 @@ func DefaultConfig() *Config { }, Bindings: []AgentBinding{}, Session: SessionConfig{ - DMScope: "per-channel-peer", + DMScope: "per-channel-peer", + BacklogLimit: DefaultSessionBacklogLimit, }, Channels: ChannelsConfig{ WhatsApp: WhatsAppConfig{