From 89862981e6ce96641d02e7c5f52dca410c8ddde5 Mon Sep 17 00:00:00 2001 From: viettranx Date: Sat, 11 Apr 2026 11:16:54 +0700 Subject: [PATCH] fix(agent): set AgentUUID in memoryflush SystemPromptConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit memoryflush.go constructed SystemPromptConfig without AgentUUID, while loop_history.go set both AgentID (agent_key) and AgentUUID. buildRuntimeSection renders below the cache boundary so the omission did not bust the prompt cache — but the runtime identity line dropped the UUID during compaction flush turns. Extract a pure helper buildMemoryFlushPromptConfig so tests can assert the config shape without a full Loop fixture (M7 mitigation from red-team review). Phase 1 Fix A of agent identity hardening. --- internal/agent/memoryflush.go | 45 ++++++++++++++----- internal/agent/memoryflush_test.go | 70 ++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 internal/agent/memoryflush_test.go diff --git a/internal/agent/memoryflush.go b/internal/agent/memoryflush.go index 7faa8a8a..b2016502 100644 --- a/internal/agent/memoryflush.go +++ b/internal/agent/memoryflush.go @@ -67,6 +67,28 @@ func ResolveMemoryFlushSettings(compaction *config.CompactionConfig) *MemoryFlus return settings } +// buildMemoryFlushPromptConfig returns the SystemPromptConfig used by the +// memory flush turn. Extracted as a pure function so tests can assert the +// config shape (specifically, that AgentUUID is populated) without building +// a full Loop fixture. See Phase 1 Fix A and M7 mitigation. +func buildMemoryFlushPromptConfig( + agentID, agentUUID, model, workspace string, + toolNames []string, + hasMemory bool, + providerType string, +) SystemPromptConfig { + return SystemPromptConfig{ + AgentID: agentID, + AgentUUID: agentUUID, + Model: model, + Workspace: workspace, + Mode: PromptMinimal, + ToolNames: toolNames, + HasMemory: hasMemory, + ProviderType: providerType, + } +} + // shouldRunMemoryFlush checks whether a memory flush should run before compaction. // Flush always runs when compaction triggers (called inside maybeSummarize), // gated only by enabled/memory checks and a dedup guard per compaction cycle. @@ -108,16 +130,19 @@ func (l *Loop) runMemoryFlush(ctx context.Context, sessionKey string, settings * flushPrompt := strings.ReplaceAll(settings.Prompt, "YYYY-MM-DD", today) flushSystemPrompt := strings.ReplaceAll(settings.SystemPrompt, "YYYY-MM-DD", today) - // System prompt: combine agent's normal system prompt context with flush system prompt - systemPrompt := BuildSystemPrompt(SystemPromptConfig{ - AgentID: l.id, - Model: l.model, - Workspace: l.workspace, - Mode: PromptMinimal, - ToolNames: l.filteredToolNames(), - HasMemory: l.hasMemory, - ProviderType: providerTypeOf(l.provider), - }) + // System prompt: combine agent's normal system prompt context with flush system prompt. + // Config construction extracted to buildMemoryFlushPromptConfig for testability — the + // AgentUUID field here mirrors loop_history.go:199-201 and must stay in sync + // (regression: missing AgentUUID caused identity drift in DomainEvents — see PR #826). + systemPrompt := BuildSystemPrompt(buildMemoryFlushPromptConfig( + l.id, + l.agentUUID.String(), + l.model, + l.workspace, + l.filteredToolNames(), + l.hasMemory, + providerTypeOf(l.provider), + )) systemPrompt += "\n\n" + flushSystemPrompt messages = append(messages, providers.Message{ diff --git a/internal/agent/memoryflush_test.go b/internal/agent/memoryflush_test.go new file mode 100644 index 00000000..ff37897a --- /dev/null +++ b/internal/agent/memoryflush_test.go @@ -0,0 +1,70 @@ +package agent + +import ( + "testing" + + "github.com/google/uuid" +) + +// TestBuildMemoryFlushPromptConfig_AgentUUIDPopulated asserts Fix A — the +// SystemPromptConfig returned by buildMemoryFlushPromptConfig carries the +// AgentUUID. Historically missing; loop_history.go set it but memoryflush.go +// did not, leading to identity drift (see PR #826 / brainstorm trap zone 2). +func TestBuildMemoryFlushPromptConfig_AgentUUIDPopulated(t *testing.T) { + u := uuid.New() + cfg := buildMemoryFlushPromptConfig( + "test-agent", + u.String(), + "claude-opus", + "/workspace", + []string{"read_file", "write_file"}, + true, + "anthropic", + ) + + if cfg.AgentUUID != u.String() { + t.Errorf("AgentUUID = %q, want %q", cfg.AgentUUID, u.String()) + } + if cfg.AgentID != "test-agent" { + t.Errorf("AgentID = %q, want %q", cfg.AgentID, "test-agent") + } + if cfg.Model != "claude-opus" { + t.Errorf("Model = %q, want %q", cfg.Model, "claude-opus") + } + if cfg.Workspace != "/workspace" { + t.Errorf("Workspace = %q, want %q", cfg.Workspace, "/workspace") + } + if cfg.Mode != PromptMinimal { + t.Errorf("Mode = %q, want %q", cfg.Mode, PromptMinimal) + } + if !cfg.HasMemory { + t.Error("HasMemory should be true") + } + if cfg.ProviderType != "anthropic" { + t.Errorf("ProviderType = %q, want %q", cfg.ProviderType, "anthropic") + } + if len(cfg.ToolNames) != 2 { + t.Errorf("ToolNames len = %d, want 2", len(cfg.ToolNames)) + } +} + +// TestBuildMemoryFlushPromptConfig_EmptyAgentUUID guards against a regression where +// a Loop with uuid.Nil (zero value) would still produce a non-empty AgentUUID string. +// The .String() of uuid.Nil is "00000000-0000-0000-0000-000000000000" — not empty, +// but it MUST be identifiable downstream so the publish-time observer in eventbus +// can still warn on it after Fix C wires validateAgentID into bus.Publish. +func TestBuildMemoryFlushPromptConfig_ZeroUUIDStringified(t *testing.T) { + cfg := buildMemoryFlushPromptConfig( + "zero-agent", + uuid.Nil.String(), + "m", + "/w", + nil, + false, + "p", + ) + + if cfg.AgentUUID != uuid.Nil.String() { + t.Errorf("AgentUUID = %q, want %q", cfg.AgentUUID, uuid.Nil.String()) + } +}