feat: structured compaction summary with identifier preservation

Compaction summaries were too generic ("provide a concise summary"),
causing loss of task progress, decisions, and identifier corruption
after summarization.

Port from OpenClaw TS (compaction.ts):
- Structured MUST PRESERVE sections: active tasks, progress, last
  request, decisions, TODOs, commitments
- Identifier preservation: preserve UUIDs, hashes, URLs, file names
  exactly as written (no shortening/reconstruction)
- Prioritize recent context over older history
- Shared prompt constant used by both mid-loop and background
  compaction paths
This commit is contained in:
viettranx committed 2026-03-31 09:32:38 +07:00
1 parent ab5bad11ff
commit 0f6cebc783
2 files changed
+30 -6

No files matched your search

+25 -1
View File
@@ -10,6 +10,30 @@ import (
"github.com/nextlevelbuilder/goclaw/internal/providers"
)
// compactionSummaryPrompt is the structured summarization instruction used by both
// mid-loop compaction and background summarization. Matching OpenClaw TS compaction.ts
// MERGE_SUMMARIES_INSTRUCTIONS + IDENTIFIER_PRESERVATION_INSTRUCTIONS.
const compactionSummaryPrompt = `Summarize this conversation concisely for the AI agent to resume work.
MUST PRESERVE:
- Active tasks and their current status (in-progress, blocked, pending)
- Batch operation progress (e.g., "5/17 items completed")
- The last thing the user requested and what was being done about it
- Decisions made and their rationale
- TODOs, open questions, and constraints
- Any commitments or follow-ups promised
IDENTIFIER PRESERVATION:
Preserve all opaque identifiers exactly as written (no shortening or reconstruction),
including UUIDs, hashes, IDs, tokens, API keys, hostnames, IPs, ports, URLs, and file names.
PRIORITIZE recent context over older history. The agent needs to know
what it was doing, not just what was discussed.
Conversation to summarize:
`
// compactMessagesInPlace summarizes the first ~70% of messages into a condensed
// summary, keeping the last ~30% intact. Operates purely on the local messages
// slice — no session state touched, no locks needed.
@@ -63,7 +87,7 @@ func (l *Loop) compactMessagesInPlace(ctx context.Context, messages []providers.
resp, err := l.provider.Chat(sctx, providers.ChatRequest{
Messages: []providers.Message{{
Role: "user",
Content: "Provide a concise summary of this conversation, preserving key findings, data, and context:\n\n" + sb.String(),
Content: compactionSummaryPrompt + sb.String(),
}},
Model: l.model,
Options: map[string]any{"max_tokens": 1024, "temperature": 0.3},
+5 -5
View File
@@ -617,14 +617,14 @@ func (l *Loop) maybeSummarize(ctx context.Context, sessionKey string) {
}
var prompt strings.Builder
prompt.WriteString("Provide a concise summary of this conversation, preserving key context:\n")
prompt.WriteString(compactionSummaryPrompt)
if len(mediaKinds) > 0 {
// Deduplicate and count media types for a compact note.
counts := make(map[string]int)
for _, k := range mediaKinds {
counts[k]++
}
prompt.WriteString("\nNote: user shared media files (")
prompt.WriteString("Note: user shared media files (")
first := true
for k, n := range counts {
if !first {
@@ -633,12 +633,12 @@ func (l *Loop) maybeSummarize(ctx context.Context, sessionKey string) {
prompt.WriteString(fmt.Sprintf("%d %s(s)", n, k))
first = false
}
prompt.WriteString(") which are no longer in context. Mention briefly if relevant.\n")
prompt.WriteString(") which are no longer in context. Mention briefly if relevant.\n\n")
}
if summary != "" {
prompt.WriteString("Existing context: " + summary + "\n")
prompt.WriteString("Existing context: " + summary + "\n\n")
}
prompt.WriteString("\n" + sb.String())
prompt.WriteString(sb.String())
resp, err := l.provider.Chat(sctx, providers.ChatRequest{
Messages: []providers.Message{{Role: "user", Content: prompt.String()}},