From 48335d7797efecdd51945486c867f10407436fc0 Mon Sep 17 00:00:00 2001 From: viettranx Date: Wed, 15 Apr 2026 11:24:49 +0700 Subject: [PATCH] feat(pruning)!: faithful port of TS context pruning + backfill migration Port goclaw context pruning to match upstream TS design in openclaw/src/agents/pi-hooks/context-pruning/: - Opt-in default: prune only when mode="cache-ttl" (was opt-out) - Remove Pass 0 per-result 30% guard (duplicated Pass 1 with different suffix, caused wobble) - Dedupe double prune call per iteration: PruneStage owns the single entry point; loop_history only runs limitHistoryTurns + sanitizeHistory - Add cache-TTL gate for Anthropic prompt cache: skip prune while cache is live, scoped per-session via sync.Map - Add context.pruned event emission for observability - Configurable TTL as Go duration string ("5m", "30s") BREAKING CHANGE: context pruning now opt-in. Add contextPruning.mode: "cache-ttl" to config.agents.defaults to restore. Migration 51 / SQLite v19 backfills mode="cache-ttl" for agents with existing custom context_pruning config missing the mode field, so previously-configured agents keep pruning after the opt-in flip. NULL configs stay NULL (new opt-in default applies). Web UI adds Cache TTL input + toggle wiring mode to cache-ttl/off. --- CHANGELOG.md | 29 +++ docs/01-agent-loop.md | 47 +++-- docs/17-changelog.md | 34 ++++ internal/agent/loop_history.go | 6 +- internal/agent/loop_history_test.go | 9 +- internal/agent/loop_pipeline_adapter.go | 15 ++ internal/agent/loop_pipeline_callbacks.go | 27 ++- internal/agent/loop_types.go | 8 + internal/agent/pruning.go | 60 ++---- internal/agent/pruning_integration_test.go | 62 ++++++ internal/agent/pruning_test.go | 154 +++++++++++++-- internal/config/config.go | 11 +- internal/eventbus/event_types.go | 16 ++ internal/pipeline/deps.go | 18 +- internal/pipeline/prune_stage.go | 110 ++++++++++- internal/pipeline/stages_test.go | 180 ++++++++++++++++-- internal/store/sqlitestore/schema.go | 14 +- internal/upgrade/version.go | 2 +- ...051_backfill_context_pruning_mode.down.sql | 4 + ...00051_backfill_context_pruning_mode.up.sql | 12 ++ ui/web/src/i18n/locales/en/agents.json | 3 +- ui/web/src/i18n/locales/vi/agents.json | 3 +- ui/web/src/i18n/locales/zh/agents.json | 3 +- .../agent-advanced-state-utils.ts | 4 +- .../context-pruning-section.tsx | 13 ++ ui/web/src/types/agent.ts | 1 + 26 files changed, 730 insertions(+), 115 deletions(-) create mode 100644 internal/agent/pruning_integration_test.go create mode 100644 migrations/000051_backfill_context_pruning_mode.down.sql create mode 100644 migrations/000051_backfill_context_pruning_mode.up.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b03c2a0..f640b6a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ All notable changes to GoClaw are documented here. For full documentation, see [docs.goclaw.sh](https://docs.goclaw.sh). +## Unreleased + +### Breaking Changes + +- **Context pruning now opt-in.** Previously tool-result trimming ran by default + for all providers; now requires explicit `contextPruning.mode: "cache-ttl"` in + `config.agents.defaults` to enable. Matches upstream TS design and prevents + silent prompt-cache invalidation on Anthropic. + + Migration — add to `config.json5`: + ```json5 + agents: { + defaults: { + contextPruning: { mode: "cache-ttl" } + } + } + ``` + +### Improvements + +- **Context pruning cleanup.** Removed redundant Pass 0 (per-result 30% guard), + deduplicated double prune call per iteration, added SanitizeHistory to + PruneStage for broken tool_use/tool_result pair cleanup. +- **Context pruning config backfill (migration).** Agents with existing custom + `context_pruning` config (e.g., `softTrimRatio`, `keepLastAssistants`) but + missing a `mode` field get auto-backfilled with `mode: "cache-ttl"` to + preserve their intent after the opt-in flip. Rows with NULL config stay + NULL (new opt-in default applies). PG migration 51; SQLite schema v19. + ## Project Status ### Implemented & Tested in Production diff --git a/docs/01-agent-loop.md b/docs/01-agent-loop.md index fea3a1e5..f7e24a4d 100644 --- a/docs/01-agent-loop.md +++ b/docs/01-agent-loop.md @@ -59,15 +59,16 @@ Finalize (runs once, uses background context if cancelled) - Resolve workspace + context files dynamically - Build system prompt (15+ sections) - Inject conversation summary if exists -- Run history pipeline (limitHistoryTurns → pruneContextMessages → sanitizeHistory) +- Run history pipeline (limitHistoryTurns → sanitizeHistory) - Filter tools through PolicyEngine (RBAC) - Call LLM, record span with token counts - Emit `chunk` events (streaming) or single response -**PruneStage** +**PruneStage** (opt-in via `contextPruning.mode: "cache-ttl"`) - Estimate token ratio vs context window -- If >= 30%, run soft trim pass (keep first/last 1500 chars, replace middle with "...") +- If >= 25%, run soft trim pass (keep first/last 3000 chars, replace middle with "...") - If >= 50%, run hard clear pass (replace with placeholder) +- Run sanitizeHistory to fix broken tool_use/tool_result pairs after prune - Trigger memory flush (synchronous) if compaction threshold exceeded **ToolStage** @@ -183,7 +184,7 @@ flowchart TD subgraph PH3["Phase 3: Build Messages"] P3A[Build system prompt - 15+ sections] --> P3B[Inject conversation summary if present] - P3B --> P3C["History pipeline: limitHistoryTurns --> pruneContextMessages --> sanitizeHistory"] + P3B --> P3C["History pipeline: limitHistoryTurns --> sanitizeHistory"] P3C --> P3D[Append current user message] P3D --> P3E[Buffer user message locally - deferred write] end @@ -398,25 +399,20 @@ All security events use the `slog.Warn("security.injection_detected")` conventio ## 5. History Pipeline -The history pipeline prepares conversation history before sending it to the LLM. It runs in three sequential stages. +The history pipeline prepares conversation history before sending it to the LLM. It runs in two sequential stages. Context pruning is handled separately by PruneStage (opt-in via `contextPruning.mode: "cache-ttl"`). ```mermaid flowchart TD RAW[Raw Session History] --> S1 S1["Stage 1: limitHistoryTurns
Keep the last N user turns
plus their associated assistant/tool messages"] --> S2 - S2["Stage 2: pruneContextMessages
2-pass tool result trimming
(see Section 6)"] --> S3 - S3["Stage 3: sanitizeHistory
Repair broken tool_use / tool_result pairing
after truncation"] --> OUT[Cleaned History] + S2["Stage 2: sanitizeHistory
Repair broken tool_use / tool_result pairing
after truncation"] --> OUT[Cleaned History] ``` ### Stage 1: limitHistoryTurns Takes the raw session history and a `historyLimit` parameter. Keeps only the last N user turns along with all associated assistant and tool messages that belong to those turns. Earlier messages are discarded. -### Stage 2: pruneContextMessages - -Applies the 2-pass context pruning algorithm described in Section 6. - -### Stage 3: sanitizeHistory +### Stage 2: sanitizeHistory Repairs tool message pairing that may have been broken by truncation or compaction: @@ -429,15 +425,17 @@ Repairs tool message pairing that may have been broken by truncation or compacti ## 6. Context Pruning -Context pruning reduces oversized tool results using a 2-pass algorithm. It only activates when the estimated token-to-context-window ratio crosses a threshold. +Context pruning reduces oversized tool results using a 2-pass algorithm. **It is opt-in** — configure `contextPruning.mode: "cache-ttl"` to enable. When disabled (default), zero overhead. Owned by PruneStage in the agent pipeline. ```mermaid flowchart TD - START[Estimate token ratio vs context window] --> CHECK{Ratio >= softTrimRatio 0.3?} + START[Check mode == cache-ttl?] --> GATE{Mode enabled?} + GATE -->|No| SKIP[No pruning - zero overhead] + GATE -->|Yes| CHECK{Ratio >= softTrimRatio 0.25?} CHECK -->|No| DONE[No pruning needed] CHECK -->|Yes| PASS1 - PASS1["Pass 1: Soft Trim
For each eligible tool result > 4000 chars:
Keep first 1500 chars + last 1500 chars
Replace middle with '...'"] + PASS1["Pass 1: Soft Trim
For each eligible tool result > 6000 chars:
Keep first 3000 chars + last 3000 chars
Replace middle with '...'"] PASS1 --> CHECK2{"Ratio >= hardClearRatio 0.5?"} CHECK2 -->|No| DONE CHECK2 -->|Yes| PASS2 @@ -446,12 +444,25 @@ flowchart TD PASS2 --> DONE ``` +### Configuration + +Enable pruning by setting `contextPruning.mode` in agent defaults: + +```json5 +agents: { + defaults: { + contextPruning: { mode: "cache-ttl" } + } +} +``` + ### Defaults | Parameter | Default | Description | |-----------|---------|-------------| +| `mode` | `""` (disabled) | `""` or `"off"` = disabled; `"cache-ttl"` = enabled | | `keepLastAssistants` | 3 | Number of recent assistant messages protected from pruning | -| `softTrimRatio` | 0.3 | Token ratio threshold to trigger Pass 1 | +| `softTrimRatio` | 0.25 | Token ratio threshold to trigger Pass 1 | | `hardClearRatio` | 0.5 | Token ratio threshold to trigger Pass 2 | | `minPrunableToolChars` | 50,000 | Minimum tool result length eligible for hard clear | @@ -719,8 +730,8 @@ Enabled via the `GOCLAW_TRACE_VERBOSE=1` environment variable. | `internal/agent/loop_run.go` | Run() entry point: dual-mode gate (v2 vs v3), trace creation, span management | | `internal/agent/loop_pipeline_adapter.go` | Bridge v2 Loop to v3 Pipeline: state conversion, dependency injection, callback wiring | | `internal/agent/loop.go` | runLoop() core loop: LLM iteration, tool execution, message buffering (v2 path) | -| `internal/agent/loop_history.go` | History pipeline: limitHistoryTurns, pruneContextMessages, sanitizeHistory, summary injection | -| `internal/agent/pruning.go` | Context pruning: 2-pass soft trim and hard clear algorithm | +| `internal/agent/loop_history.go` | History pipeline: limitHistoryTurns, sanitizeHistory, summary injection | +| `internal/agent/pruning.go` | Context pruning: 2-pass soft trim and hard clear algorithm (opt-in via PruneStage) | | `internal/agent/loop_compact.go` | Mid-loop compaction: in-memory message summarization during iterations | | `internal/agent/systemprompt.go` | System prompt assembly (19+ sections), PromptFull and PromptMinimal modes | | `internal/agent/systemprompt_sections.go` | Individual section builders (tooling, workspace, sandbox, skills, MCP, etc.) | diff --git a/docs/17-changelog.md b/docs/17-changelog.md index c1e29591..b5bbaccd 100644 --- a/docs/17-changelog.md +++ b/docs/17-changelog.md @@ -6,6 +6,40 @@ All notable changes to GoClaw Gateway are documented here. Format follows [Keep ## [Unreleased] — 2026-04-15 +#### ElevenLabs Audio Manager Refactor — Phase 6 (2026-04-15) + +Desktop UI parity: voice picker + STT admin form ported from web to Wails desktop frontend. Completes 6-phase plan; all audio surfaces (TTS/STT/Music/SFX) now manageable from both web and desktop Lite editions. + +### Added +- **Desktop voice picker**: `ui/desktop/frontend/src/components/agents/voice-picker.tsx` — uses desktop's native Combobox primitive; embedded in `AgentDetailPanel` with `other_config.tts_voice_id` persistence via spread-merge pattern +- **Desktop voice preview button**: `ui/desktop/frontend/src/components/agents/voice-preview-button.tsx` — singleton `