feat(openclaw-plugin): add captureMinLength config to reduce unnecessary VLM token consumption#665
Conversation
…context agentId When OpenClaw gateway serves multiple agents, each agent's before_agent_start and agent_end hooks now carry the agent's ID in the second parameter (PluginHookAgentContext). The plugin dynamically switches the client's agentId before each recall/capture operation, ensuring memories are routed to the correct agent_space (md5(user_id + agent_id)[:12]). Changes: - client.ts: Add setAgentId()/getAgentId() to allow dynamic agent switching. Clears cached runtimeIdentity and resolvedSpaceByScope when switching to ensure correct space derivation. - index.ts: Extract agentId from hook ctx (2nd param) in both before_agent_start and agent_end handlers. This is backward compatible: if ctx.agentId is absent (single-agent setup), the plugin falls back to the static config agentId as before.
…ary VLM token consumption
Problem:
The auto-capture feature triggers VLM extraction (deepseek-chat) for almost every
user message, including trivially short ones (as low as 4 CJK chars / 10 EN chars).
In multi-agent setups with high interaction volume, this leads to excessive API calls
and token consumption (observed 100K+ deepseek-chat calls in 2 days).
Solution:
Add a configurable `captureMinLength` option (default: 50 chars) that sets a minimum
sanitized text length threshold for triggering auto-capture. Messages shorter than this
threshold are skipped (reason: `length_out_of_range`), avoiding unnecessary VLM calls.
The new threshold works as a floor: `Math.max(resolveCaptureMinLength(text), captureMinLength)`,
preserving the existing CJK/EN-aware minimum while allowing users to set a higher bar.
Changes:
- config.ts: Add captureMinLength type, DEFAULT_CAPTURE_MIN_LENGTH=50, allowed key,
resolve logic (clamped to 1..1000), and UI hint
- text-utils.ts: Update getCaptureDecision signature to accept captureMinLength,
use Math.max to combine with built-in minimum
- index.ts: Pass cfg.captureMinLength to getCaptureDecision call
Users can override via plugin config:
{ "captureMinLength": 100 } // skip messages under 100 chars
|
Mac seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
| const hookAgentId = ctx?.agentId; | ||
| if (hookAgentId) { | ||
| const client = await getClient(); | ||
| client.setAgentId(hookAgentId); |
There was a problem hiding this comment.
[Bug] setAgentId() mutates shared OpenVikingClient state (agentId, resolvedSpaceByScope, runtimeIdentity). In concurrent multi-agent scenarios, async handlers interleave at await points (e.g., client.find() → normalizeTargetUri → resolveScopeSpace). For example:
- Agent A's handler calls
setAgentId("A"), thenawait client.find()(HTTP in-flight) - Agent B's handler fires, calls
setAgentId("B"), clearing cachedresolvedSpaceByScope - Agent A's
find()resumes —resolveScopeSpacenow usesthis.agentId = "B", deriving the wrongagent_space
This causes memories to be stored in / retrieved from the wrong agent's space. The same issue exists in the agent_end handler below (line 491).
Consider either:
- Creating a per-agent client instance (e.g., clone with agent-specific ID) instead of mutating shared state
- Passing
agentIdas a parameter throughfind()/request()so each call is self-contained
| const hookAgentId = ctx?.agentId; | ||
| if (hookAgentId) { | ||
| const client = await getClient(); | ||
| client.setAgentId(hookAgentId); |
There was a problem hiding this comment.
[Bug] lastProcessedMsgCount (line 484) is a closure variable shared across all agents. In multi-agent setups each agent has its own message history, but this counter tracks a single global position. After Agent A's agent_end processes 10 messages (lastProcessedMsgCount = 10), Agent B's agent_end calls extractNewTurnTexts(messages, 10) — if B's message array has fewer than 10 elements, no new texts are extracted and B's memories are never captured.
Consider using a Map<string, number> keyed by agentId to track per-agent message positions:
const lastProcessedMsgCountByAgent = new Map<string, number>();
// in handler:
const agentKey = hookAgentId ?? cfg.agentId;
const lastCount = lastProcessedMsgCountByAgent.get(agentKey) ?? 0;
// ... after processing:
lastProcessedMsgCountByAgent.set(agentKey, messages.length);
Problem
The auto-capture feature triggers VLM extraction (deepseek-chat) for almost every user message, including trivially short ones (as low as 4 CJK chars / 10 EN chars). In multi-agent setups with high interaction volume, this leads to excessive API calls and token consumption.
Observed impact: 100K+ deepseek-chat API calls in 2 days from a 10-agent setup due to aggressive capture thresholds.
Solution
Add a configurable
captureMinLengthoption (default: 50 chars) that sets a minimum sanitized text length for triggering auto-capture. Messages shorter than this threshold are skipped (reason: length_out_of_range), avoiding unnecessary VLM calls.The new threshold works as a floor:
This preserves the existing CJK/EN-aware minimum while allowing users to set a higher bar.
Changes
config.tscaptureMinLengthtype, default (50), allowed key, resolve (clamped 1..1000), UI hinttext-utils.tsgetCaptureDecisionsignature, useMath.maxto combine with built-in minimumindex.tscfg.captureMinLengthtogetCaptureDecisioncallUsage
Default (50 chars) — no config needed. To customize:
{ "captureMinLength": 100 }Backward Compatible
Math.maxcaptureMinLengthuse the 50-char default