Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bdc3124
fix: Zalo Bot Platform API compatibility — struct tags, response pars…
duhd-vnpay Mar 10, 2026
913f8fc
feat: download Zalo CDN photos to local temp files before agent proce…
duhd-vnpay Mar 10, 2026
a95eee9
fix: always convert credentials to []byte for bytea column in channel…
duhd-vnpay Mar 10, 2026
a88536a
feat: Party Mode — multi-persona collaborative discussion engine
duhd-vnpay Mar 10, 2026
2f26d58
fix: auto-clean orphaned tool messages from session history
duhd-vnpay Mar 10, 2026
9466043
fix: route subagent announce through correct agent in delegation context
duhd-vnpay Mar 10, 2026
3d28e2d
feat: team auth — sender_id tracking + fix thinking model prefill
duhd-vnpay Mar 12, 2026
7420834
merge: upstream origin/main — 70+ commits (contacts, activity, skills…
duhd-vnpay Mar 12, 2026
6d6a7c7
Merge branch 'nextlevelbuilder:main' into main
duhd-vnpay Mar 12, 2026
68a9e95
Merge remote-tracking branch 'origin/main'
duhd-vnpay Mar 12, 2026
925a5c1
Merge branch 'main' of https://github.com/duhd-vnpay/goclaw
duhd-vnpay Mar 12, 2026
f26d52a
fix: TypeScript strict mode errors in Party Mode UI
duhd-vnpay Mar 12, 2026
b332024
test: add data-testid attributes to Party Mode components for E2E tes…
duhd-vnpay Mar 13, 2026
d405895
fix: Party Mode — deterministic provider selection + snake_case WS pr…
duhd-vnpay Mar 13, 2026
ba3871e
Merge remote-tracking branch 'origin/main'
duhd-vnpay Mar 13, 2026
787bb01
fix: restore Party Mode session history when selecting old sessions
duhd-vnpay Mar 13, 2026
da3b5d3
fix: prevent nil pointer crash in OpenAI SSE tool call accumulation
duhd-vnpay Mar 13, 2026
65c249e
fix: detect and handle truncated LLM responses to prevent stuck agent…
duhd-vnpay Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions cmd/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/signal"
"path/filepath"
"syscall"
"time"

"github.com/google/uuid"

Expand Down Expand Up @@ -308,6 +309,18 @@ func runGateway() {
}()
}

// Sweep orphan traces left by previous crashes (running > 1h)
if pgStores.Tracing != nil {
go func() {
n, err := pgStores.Tracing.SweepOrphanTraces(context.Background(), time.Hour)
if err != nil {
slog.Warn("orphan trace sweep failed", "error", err)
} else if n > 0 {
slog.Info("orphan trace sweep complete", "swept", n)
}
}()
}

// Redis cache: compiled via build tags. Build with 'go build -tags redis' to enable.
redisClient := initRedisClient(cfg)
defer shutdownRedis(redisClient)
Expand Down Expand Up @@ -818,6 +831,11 @@ func runGateway() {
// Register channels/instances/links/teams RPC methods
wireChannelRPCMethods(server, pgStores, channelMgr, agentRouter, msgBus)

// Register party mode WS RPC methods
if pgStores.Party != nil {
methods.NewPartyMethods(pgStores.Party, pgStores.Agents, providerRegistry, msgBus).Register(server.Router())
}

// Wire channel event subscribers (cache invalidation, pairing, cascade disable)
wireChannelEventSubscribers(msgBus, server, pgStores, channelMgr, instanceLoader, pairingMethods, cfg)

Expand Down
17 changes: 16 additions & 1 deletion cmd/gateway_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi
prov.WithProviderType(p.ProviderType)
registry.Register(prov)
default:
prov := providers.NewOpenAIProvider(p.Name, p.APIKey, p.APIBase, "")
defaultModel := extractDefaultModel(p.Settings)
prov := providers.NewOpenAIProvider(p.Name, p.APIKey, p.APIBase, defaultModel)
prov.WithProviderType(p.ProviderType)
if p.ProviderType == store.ProviderMiniMax {
prov.WithChatPath("/text/chatcompletion_v2")
Expand All @@ -329,3 +330,17 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi
slog.Info("registered provider from DB", "name", p.Name)
}
}

// extractDefaultModel reads default_model from a provider's settings JSONB.
func extractDefaultModel(settings json.RawMessage) string {
if len(settings) == 0 {
return ""
}
var s struct {
DefaultModel string `json:"default_model"`
}
if json.Unmarshal(settings, &s) == nil {
return s.DefaultModel
}
return ""
}
26 changes: 24 additions & 2 deletions internal/agent/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
reminder := "[System] " + strings.Join(parts, "\n\n")
messages = append(messages,
providers.Message{Role: "user", Content: reminder},
providers.Message{Role: "assistant", Content: "I see the task status. Let me handle accordingly."},
// No assistant prefill — thinking models reject it.
)
}
}
Expand Down Expand Up @@ -430,7 +430,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
Tools: toolDefs,
Model: l.model,
Options: map[string]any{
providers.OptMaxTokens: 8192,
providers.OptMaxTokens: l.maxTokens,
providers.OptTemperature: 0.7,
providers.OptSessionKey: req.SessionKey,
providers.OptAgentID: l.agentUUID.String(),
Expand Down Expand Up @@ -506,6 +506,28 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
}
}

// Truncation guard: if response was cut off (max_tokens reached) and has tool calls,
// the tool call arguments are likely incomplete/malformed. Skip execution and ask
// the LLM to re-issue with complete arguments or break into smaller parts.
if resp.FinishReason == "length" && len(resp.ToolCalls) > 0 {
slog.Warn("truncated tool calls detected",
"agent", l.id, "iteration", iteration,
"tool_calls", len(resp.ToolCalls), "max_tokens", l.maxTokens)
messages = append(messages,
providers.Message{Role: "assistant", Content: resp.Content, ToolCalls: resp.ToolCalls,
Thinking: resp.Thinking, RawAssistantContent: resp.RawAssistantContent},
providers.Message{
Role: "user",
Content: "[System] Your response was truncated (max_tokens reached). The last tool call had incomplete arguments. Do NOT re-issue the same large tool call. Instead, break your work into smaller steps or respond with text only.",
},
)
pendingMsgs = append(pendingMsgs,
providers.Message{Role: "assistant", Content: resp.Content, ToolCalls: resp.ToolCalls},
providers.Message{Role: "user", Content: "[System] Response truncated — tool call skipped."},
)
continue
}

if resp.Usage != nil {
totalUsage.PromptTokens += resp.Usage.PromptTokens
totalUsage.CompletionTokens += resp.Usage.CompletionTokens
Expand Down
3 changes: 2 additions & 1 deletion internal/agent/loop_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ func limitHistoryTurns(msgs []providers.Message, limit int) []providers.Message
// - Orphaned tool messages at start of history (after truncation)
// - tool_result without matching tool_use in preceding assistant message
// - assistant with tool_calls but missing tool_results
// sanitizeHistory repairs tool_use/tool_result pairing in session history.
//
// Returns the cleaned messages and the number of messages that were dropped or synthesized.
func sanitizeHistory(msgs []providers.Message) ([]providers.Message, int) {
if len(msgs) == 0 {
Expand All @@ -301,6 +301,7 @@ func sanitizeHistory(msgs []providers.Message) ([]providers.Message, int) {
"tool_call_id", msgs[start].ToolCallID)
dropped++
start++
dropped++
}

if start >= len(msgs) {
Expand Down
3 changes: 3 additions & 0 deletions internal/agent/loop_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Loop struct {
contextWindow int
maxIterations int
maxToolCalls int
maxTokens int
workspace string
workspaceSharing *store.WorkspaceSharingConfig

Expand Down Expand Up @@ -154,6 +155,7 @@ type LoopConfig struct {
ContextWindow int
MaxIterations int
MaxToolCalls int
MaxTokens int
Workspace string
WorkspaceSharing *store.WorkspaceSharingConfig

Expand Down Expand Up @@ -263,6 +265,7 @@ func NewLoop(cfg LoopConfig) *Loop {
contextWindow: cfg.ContextWindow,
maxIterations: cfg.MaxIterations,
maxToolCalls: cfg.MaxToolCalls,
maxTokens: cfg.MaxTokens,
workspace: cfg.Workspace,
workspaceSharing: cfg.WorkspaceSharing,
restrictToWs: cfg.RestrictToWs,
Expand Down
5 changes: 5 additions & 0 deletions internal/agent/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
if maxIter <= 0 {
maxIter = 20
}
maxTokens := ag.ParseMaxTokens()
if maxTokens <= 0 {
maxTokens = 8192
}

// Per-agent config overrides (fallback to global defaults from config.json)
compactionCfg := deps.CompactionCfg
Expand Down Expand Up @@ -357,6 +361,7 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
Model: ag.Model,
ContextWindow: contextWindow,
MaxIterations: maxIter,
MaxTokens: maxTokens,
Workspace: workspace,
RestrictToWs: &restrictVal,
SubagentsCfg: ag.ParseSubagentsConfig(),
Expand Down
63 changes: 46 additions & 17 deletions internal/channels/zalo/zalo.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func (c *Channel) Start(ctx context.Context) error {
if err != nil {
return fmt.Errorf("zalo getMe failed: %w", err)
}
slog.Info("zalo bot connected", "bot_id", info.ID, "bot_name", info.Name)
slog.Info("zalo bot connected", "bot_id", info.ID, "bot_name", info.Label())

c.SetRunning(true)

Expand Down Expand Up @@ -418,29 +418,41 @@ type zaloAPIResponse struct {
}

type zaloBotInfo struct {
ID string `json:"id"`
Name string `json:"name"`
ID string `json:"id"`
Name string `json:"account_name"`
DisplayName string `json:"display_name"`
}

func (b *zaloBotInfo) Label() string {
if b.DisplayName != "" {
return b.DisplayName
}
return b.Name
}

type zaloMessage struct {
MessageID string `json:"message_id"`
Text string `json:"text"`
Photo string `json:"photo"`
PhotoURL string `json:"photo_url"`
Caption string `json:"caption"`
From zaloFrom `json:"from"`
Chat zaloChat `json:"chat"`
Date int64 `json:"date"`
MessageID string `json:"message_id"`
MessageType string `json:"message_type"`
Text string `json:"text"`
Photo string `json:"photo"`
PhotoURL string `json:"photo_url"`
Caption string `json:"caption"`
From zaloFrom `json:"from"`
Chat zaloChat `json:"chat"`
Date int64 `json:"date"`
}

type zaloFrom struct {
ID string `json:"id"`
Username string `json:"username"`
ID string `json:"id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
IsBot bool `json:"is_bot"`
}

type zaloChat struct {
ID string `json:"id"`
Type string `json:"type"`
ID string `json:"id"`
Type string `json:"type"`
ChatType string `json:"chat_type"`
}

type zaloUpdate struct {
Expand Down Expand Up @@ -514,11 +526,28 @@ func (c *Channel) getUpdates(timeout int) ([]zaloUpdate, error) {
return nil, err
}

// Try array first
var updates []zaloUpdate
if err := json.Unmarshal(result, &updates); err != nil {
if err := json.Unmarshal(result, &updates); err == nil {
return updates, nil
}

// Try single object (Zalo Bot Platform returns one update at a time)
var single zaloUpdate
if err := json.Unmarshal(result, &single); err == nil && single.EventName != "" {
slog.Info("zalo update received", "event", single.EventName)
return []zaloUpdate{single}, nil
}

// Try wrapped {"updates": [...]}
var wrapped struct {
Updates []zaloUpdate `json:"updates"`
}
if err := json.Unmarshal(result, &wrapped); err != nil {
slog.Warn("zalo getUpdates unknown format", "raw", string(result[:min(len(result), 500)]))
return nil, fmt.Errorf("unmarshal updates: %w", err)
}
return updates, nil
return wrapped.Updates, nil
}

func (c *Channel) sendMessage(chatID, text string) error {
Expand Down
Loading