Skip to content

Commit a871dc3

Browse files
committed
refactor(openclaw): make skipRetainSessionPatterns configurable
Replace hardcoded includes() checks with configurable glob patterns via new skipRetainSessionPatterns config field. Bare tokens like 'heartbeat' auto-wrap as '**:heartbeat**'. Adds PluginConfig type, 4 unit tests, and documentation in openclaw integration docs. Per PR #1410 reviewer feedback.
1 parent 63e1a0e commit a871dc3

5 files changed

Lines changed: 68 additions & 17 deletions

File tree

hindsight-docs/docs-integrations/openclaw.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ Optional settings in `~/.openclaw/openclaw.json`:
141141
- `recallRoles` - Which message roles to include when composing the contextual recall query (default: `["user", "assistant"]`).
142142
- `retainEveryNTurns` - Retain every Nth turn (default: `1` = every turn). Values > 1 enable chunked retention.
143143
- `retainOverlapTurns` - Extra prior turns included when chunked retention fires (default: `0`).
144+
- `skipRetainSessionPatterns` - Glob patterns to skip retain from machine-driven sessions. Bare tokens like `"heartbeat"` are auto-wrapped as `**:heartbeat**`. Defaults: `["heartbeat", "cron", "subagent", "autonomic"]`.
144145
- `debug` - Enable debug logging (default: `false`).
145146

146147
### Memory Isolation

hindsight-integrations/openclaw/src/index.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,14 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
14071407
statelessSessionPatterns: Array.isArray(config.statelessSessionPatterns)
14081408
? config.statelessSessionPatterns
14091409
: [],
1410+
skipRetainSessionPatterns: (() => {
1411+
const raw = Array.isArray(config.skipRetainSessionPatterns)
1412+
? config.skipRetainSessionPatterns
1413+
: ["heartbeat", "cron", "subagent", "autonomic"];
1414+
// Convert bare tokens to glob patterns: "heartbeat" -> "**:heartbeat**"
1415+
// Pass through patterns that already contain glob wildcards
1416+
return raw.map((p: string) => (p.includes("*") ? p : `**:${p}**`));
1417+
})(),
14101418
skipStatelessSessions: config.skipStatelessSessions !== false,
14111419
debug: config.debug ?? false,
14121420
};
@@ -2218,6 +2226,23 @@ ${memoriesFormatted}
22182226
return;
22192227
}
22202228

2229+
// Defense-in-depth: skip retain for operational session patterns
2230+
// These sessions are machine-driven and produce low-value or duplicative retains
2231+
// Patterns are configurable via skipRetainSessionPatterns; defaults match common operational keys
2232+
const skipRetainPatterns = compileSessionPatterns(
2233+
pluginConfig.skipRetainSessionPatterns ?? []
2234+
);
2235+
if (
2236+
agentEndSessionKey &&
2237+
skipRetainPatterns.length > 0 &&
2238+
matchesSessionPattern(agentEndSessionKey, skipRetainPatterns)
2239+
) {
2240+
debug(
2241+
`[Hindsight] Skipping retain - operational session pattern matched: ${agentEndSessionKey}`
2242+
);
2243+
return;
2244+
}
2245+
22212246
if (
22222247
!Array.isArray(event.context?.sessionEntry?.messages ?? event.messages) ||
22232248
(event.context?.sessionEntry?.messages ?? event.messages ?? []).length === 0
@@ -2231,22 +2256,6 @@ ${memoriesFormatted}
22312256
return;
22322257
}
22332258

2234-
// Defense-in-depth: skip retain for operational session patterns
2235-
// These sessions are machine-driven and produce low-value or duplicative retains
2236-
if (
2237-
agentEndSessionKey && (
2238-
agentEndSessionKey.includes(":heartbeat") ||
2239-
agentEndSessionKey.includes(":cron:") ||
2240-
agentEndSessionKey.includes(":subagent") ||
2241-
agentEndSessionKey.includes(":autonomic:")
2242-
)
2243-
) {
2244-
debug(
2245-
`[Hindsight] Skipping retain - operational session pattern matched: ${agentEndSessionKey}`
2246-
);
2247-
return;
2248-
}
2249-
22502259
// Chunked retention: skip non-Nth turns and use a sliding window when firing
22512260
const retainEveryN = pluginConfig.retainEveryNTurns ?? 1;
22522261
const allMessages = event.context?.sessionEntry?.messages ?? event.messages ?? [];

hindsight-integrations/openclaw/src/session-patterns.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,44 @@ describe("matchesSessionPattern", () => {
8585
expect(matchesSessionPattern("agent:main:sess-abc", patterns)).toBe(false);
8686
});
8787
});
88+
89+
// ---------------------------------------------------------------------------
90+
// skipRetainSessionPatterns bare-token wrapping
91+
// ---------------------------------------------------------------------------
92+
93+
describe("skipRetainSessionPatterns bare-token wrapping", () => {
94+
// Simulates the wrapping logic from getPluginConfig:
95+
// bare tokens like "heartbeat" become "**:heartbeat**",
96+
// patterns already containing "*" pass through unchanged.
97+
function wrapTokens(raw: string[]): string[] {
98+
return raw.map((p) => (p.includes("*") ? p : `**:${p}**`));
99+
}
100+
101+
it("wraps bare tokens as **:token** glob patterns", () => {
102+
const wrapped = wrapTokens(["heartbeat", "cron", "subagent", "autonomic"]);
103+
expect(wrapped).toEqual(["**:heartbeat**", "**:cron**", "**:subagent**", "**:autonomic**"]);
104+
});
105+
106+
it("passes through patterns that already contain wildcards", () => {
107+
const wrapped = wrapTokens(["agent:*:cron:**", "**:heartbeat**"]);
108+
expect(wrapped).toEqual(["agent:*:cron:**", "**:heartbeat**"]);
109+
});
110+
111+
it("default patterns match operational session keys", () => {
112+
const patterns = compileSessionPatterns(
113+
wrapTokens(["heartbeat", "cron", "subagent", "autonomic"])
114+
);
115+
expect(matchesSessionPattern("agent:main:heartbeat:abc123", patterns)).toBe(true);
116+
expect(matchesSessionPattern("agent:worker:cron:nightly:cleanup", patterns)).toBe(true);
117+
expect(matchesSessionPattern("agent:main:subagent:worker:456", patterns)).toBe(true);
118+
expect(matchesSessionPattern("agent:main:autonomic:def", patterns)).toBe(true);
119+
});
120+
121+
it("default patterns do not match user sessions", () => {
122+
const patterns = compileSessionPatterns(
123+
wrapTokens(["heartbeat", "cron", "subagent", "autonomic"])
124+
);
125+
expect(matchesSessionPattern("agent:main:sess-abc123", patterns)).toBe(false);
126+
expect(matchesSessionPattern("telegram:123456:agent:main", patterns)).toBe(false);
127+
});
128+
});

hindsight-integrations/openclaw/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export interface PluginConfig {
103103
recallInjectionPosition?: "prepend" | "append" | "user"; // Where to inject recalled memories. 'prepend' = start of system prompt (default), 'append' = end of system prompt (preserves prompt cache), 'user' = before user message.
104104
ignoreSessionPatterns?: string[]; // Session key glob patterns to skip entirely (no recall, no retain). E.g. ["agent:main:**", "agent:*:cron:**"]
105105
statelessSessionPatterns?: string[]; // Session key glob patterns for read-only sessions (recall allowed, retain skipped). E.g. ["agent:*:subagent:**"]
106+
skipRetainSessionPatterns?: string[]; // Glob patterns to skip retain from machine-driven sessions. Defaults: ["heartbeat", "cron", "subagent", "autonomic"].
106107
skipStatelessSessions?: boolean; // When true (default), stateless sessions also skip recall. When false, they recall but never retain.
107108
debug?: boolean; // Enable debug logging (default: false)
108109
logLevel?: "off" | "error" | "warning" | "info" | "debug"; // Console log verbosity (default: 'info').

skills/hindsight-docs/references/developer/configuration.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ To switch between backends:
174174
| `HINDSIGHT_API_LLM_GROQ_SERVICE_TIER` | Groq service tier: `on_demand`, `flex`, `auto` | `auto` |
175175
| `HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER` | OpenAI service tier: `flex` for 50% cost savings (OpenAI Flex Processing) | None (default) |
176176
| `HINDSIGHT_API_LLM_EXTRA_BODY` | JSON dict merged into `extra_body` for all OpenAI-compatible API calls. Useful for custom model servers (e.g., vLLM `chat_template_kwargs`). | `null` |
177-
| `HINDSIGHT_API_LLM_DEFAULT_HEADERS` | JSON dict passed as `default_headers` to provider SDK clients. Used by operators routing through proxies / request-tracing middleware (e.g. Cloudflare AI Gateway, Helicone, corporate proxies). Currently wired into the Anthropic provider; other providers can opt in. | `null` |
178177
| `HINDSIGHT_API_LLM_GEMINI_SAFETY_SETTINGS` | JSON-encoded list of `{category, threshold}` dicts for Gemini/VertexAI content safety filtering | `null` |
179178

180179
**Provider Examples**

0 commit comments

Comments
 (0)