fix(agent): set AgentUUID in memoryflush SystemPromptConfig

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.
This commit is contained in:
viettranx committed 2026-04-11 21:22:23 +07:00
1 parent de2bc201fa
commit 89862981e6
2 files changed
+105 -10

No files matched your search

+35 -10
View File
@@ -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{
+70
View File
@@ -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())
}
}