Files
goclaw/cmd/gateway_system_config_sync.go
viettranx 258e378593 feat(memory): add chunk overlap support and clean up per-agent config
- Add ChunkOverlap to MemoryConfig (default 200) for context continuity
  at chunk boundaries during semantic search
- Implement overlap logic in ChunkText() with safety clamp at maxChunkLen/2
- Wire per-agent chunk_len/chunk_overlap overrides at IndexDocument time
  via RunContext, falling back to global PGMemoryConfig defaults
- Add sync.RWMutex to PGMemoryStore for thread-safe config updates
- Persist chunk settings via system_configs (embedding.max_chunk_len,
  embedding.chunk_overlap) with runtime refresh on config change
- Remove dead per-agent embedding_provider/embedding_model fields from
  agent Memory UI (system uses single global provider)
- Show Memory section always-visible in agent config (no toggle needed)
- Add chunk_overlap fields to System Settings modal and Config AI Defaults
- Update i18n (en/vi/zh) for all new and removed keys
2026-03-25 18:24:36 +07:00

133 lines
4.2 KiB
Go

package cmd
import (
"context"
"fmt"
"log/slog"
"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// syncSystemConfigs upserts non-secret config values into the system_configs table.
// Seeds per-tenant entries for all known tenants using real tenant IDs.
// When onlyMissing is true, existing keys are preserved (used at startup).
// When onlyMissing is false, all keys are upserted (used after config.apply/patch).
func syncSystemConfigs(sc store.SystemConfigStore, ts store.TenantStore, cfg *config.Config, onlyMissing bool) {
if sc == nil {
return
}
// Enumerate tenants and seed each one
if ts != nil {
tenants, err := ts.ListTenants(context.Background())
if err != nil {
slog.Warn("failed to list tenants for system config seed", "error", err)
// Fall back to master tenant only
masterCtx := store.WithTenantID(context.Background(), store.MasterTenantID)
seedConfigForContext(masterCtx, sc, cfg, onlyMissing)
return
}
if len(tenants) == 0 {
// No tenants yet (fresh install before onboard) → seed master tenant
masterCtx := store.WithTenantID(context.Background(), store.MasterTenantID)
seedConfigForContext(masterCtx, sc, cfg, onlyMissing)
return
}
for _, t := range tenants {
tenantCtx := store.WithTenantID(context.Background(), t.ID)
seedConfigForContext(tenantCtx, sc, cfg, onlyMissing)
}
} else {
// No tenant store → seed master tenant
masterCtx := store.WithTenantID(context.Background(), store.MasterTenantID)
seedConfigForContext(masterCtx, sc, cfg, onlyMissing)
}
}
// seedConfigForContext writes config keys for a specific tenant context.
func seedConfigForContext(ctx context.Context, sc store.SystemConfigStore, cfg *config.Config, onlyMissing bool) {
var existing map[string]string
if onlyMissing {
var err error
existing, err = sc.List(ctx)
if err != nil {
slog.Warn("failed to list system_configs for seed", "error", err)
return
}
}
set := func(key, val string) {
if val == "" {
return
}
if onlyMissing {
if _, ok := existing[key]; ok {
return
}
}
if err := sc.Set(ctx, key, val); err != nil {
slog.Warn("failed to sync system config", "key", key, "error", err)
}
}
setInt := func(key string, val int) {
if val != 0 {
set(key, fmt.Sprintf("%d", val))
}
}
setBool := func(key string, val *bool) {
if val != nil {
set(key, fmt.Sprintf("%t", *val))
}
}
// Embedding
if m := cfg.Agents.Defaults.Memory; m != nil {
set("embedding.provider", m.EmbeddingProvider)
set("embedding.model", m.EmbeddingModel)
setInt("embedding.max_chunk_len", m.MaxChunkLen)
setInt("embedding.chunk_overlap", m.ChunkOverlap)
}
// Agent defaults
set("agent.default_provider", cfg.Agents.Defaults.Provider)
set("agent.default_model", cfg.Agents.Defaults.Model)
setInt("agent.context_window", cfg.Agents.Defaults.ContextWindow)
setInt("agent.max_tool_iterations", cfg.Agents.Defaults.MaxToolIterations)
// Gateway behavior (host/port are infra — env/file only, not DB)
setInt("gateway.rate_limit_rpm", cfg.Gateway.RateLimitRPM)
setInt("gateway.max_message_chars", cfg.Gateway.MaxMessageChars)
set("gateway.injection_action", cfg.Gateway.InjectionAction)
setInt("gateway.inbound_debounce_ms", cfg.Gateway.InboundDebounceMs)
setBool("gateway.block_reply", cfg.Gateway.BlockReply)
setBool("gateway.tool_status", cfg.Gateway.ToolStatus)
setInt("gateway.task_recovery_interval_sec", cfg.Gateway.TaskRecoveryIntervalSec)
// Tools
set("tools.profile", cfg.Tools.Profile)
setInt("tools.rate_limit_per_hour", cfg.Tools.RateLimitPerHour)
setBool("tools.scrub_credentials", cfg.Tools.ScrubCredentials)
// TTS
set("tts.provider", cfg.Tts.Provider)
set("tts.auto", cfg.Tts.Auto)
set("tts.mode", cfg.Tts.Mode)
setInt("tts.max_length", cfg.Tts.MaxLength)
// Cron
setInt("cron.max_retries", cfg.Cron.MaxRetries)
set("cron.default_timezone", cfg.Cron.DefaultTimezone)
// Pending message compaction
if pc := cfg.Channels.PendingCompaction; pc != nil {
setInt("compaction.threshold", pc.Threshold)
setInt("compaction.keep_recent", pc.KeepRecent)
setInt("compaction.max_tokens", pc.MaxTokens)
set("compaction.provider", pc.Provider)
set("compaction.model", pc.Model)
}
}