feat(openclaw): add session pattern filtering for ignore and stateless sessions (#909)
* feat(openclaw): add session pattern filtering for ignore and stateless sessions
Adds three new config options to the OpenClaw plugin that allow filtering
sessions by key pattern before recall and retain operations fire:
- `ignoreSessionPatterns`: glob patterns for sessions to skip entirely
(no recall, no retain). Useful for cron/scheduled agent sessions.
- `statelessSessionPatterns`: glob patterns for read-only sessions —
retain is always skipped; recall is also skipped when
`skipStatelessSessions` is true (default).
- `skipStatelessSessions`: boolean (default: true). When false, sessions
matching statelessSessionPatterns can still recall but never retain.
Pattern syntax mirrors lossless-claw: `*` matches non-colon characters,
`**` matches anything including colons. Session keys follow the OpenClaw
format `agent:<agentId>:<type>:<uuid>`.
Example config:
ignoreSessionPatterns: ["agent:*:cron:**"]
statelessSessionPatterns: ["agent:*:subagent:**", "agent:*💓**"]
skipStatelessSessions: true
Implementation:
- New `session-patterns.ts` module with compile/match utilities
- Session filter applied in `before_prompt_build` and `agent_end` hooks
immediately after the existing `excludeProviders` check
- New fields wired through `getPluginConfig`
- Schema added to `openclaw.plugin.json` (additionalProperties: false
was already set, causing config validation errors without this)
- 11 unit tests in `session-patterns.test.ts`
- 5 integration tests added to `hooks.integration.test.ts`
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(openclaw): support HINDSIGHT_API_TOKEN in integration tests
Pass HINDSIGHT_API_TOKEN env var through to HindsightClient and plugin
config in integration tests so tests work against authenticated APIs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(openclaw): document session pattern filtering options
Add ignoreSessionPatterns, statelessSessionPatterns, and skipStatelessSessions
to the README config table with glob syntax reference and usage examples.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Marco Rutsch <marco@rutimka.de>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1f1716bdb0
commit
5a61ac50e9
8 changed files with 297 additions and 0 deletions
|
|
@ -67,6 +67,40 @@ Optional settings in `~/.openclaw/openclaw.json` under `plugins.entries.hindsigh
|
||||||
| `recallPromptPreamble` | built-in string | Prompt text placed above recalled memories in the injected `<hindsight_memories>` system-context block. |
|
| `recallPromptPreamble` | built-in string | Prompt text placed above recalled memories in the injected `<hindsight_memories>` system-context block. |
|
||||||
| `hindsightApiUrl` | — | External Hindsight API URL (skips local daemon) |
|
| `hindsightApiUrl` | — | External Hindsight API URL (skips local daemon) |
|
||||||
| `hindsightApiToken` | — | Auth token for external API |
|
| `hindsightApiToken` | — | Auth token for external API |
|
||||||
|
| `ignoreSessionPatterns` | `[]` | Session key glob patterns to skip entirely — no recall, no retain (e.g. `["agent:*:cron:**"]`) |
|
||||||
|
| `statelessSessionPatterns` | `[]` | Session key glob patterns for read-only sessions — retain is always skipped; recall is skipped when `skipStatelessSessions` is `true` (e.g. `["agent:*:subagent:**", "agent:*:heartbeat:**"]`) |
|
||||||
|
| `skipStatelessSessions` | `true` | When `true`, sessions matching `statelessSessionPatterns` also skip recall. Set to `false` to allow recall but still skip retain. |
|
||||||
|
|
||||||
|
### Session pattern filtering
|
||||||
|
|
||||||
|
`ignoreSessionPatterns` and `statelessSessionPatterns` accept glob patterns matched against the session key (format: `agent:<agentId>:<type>:<uuid>`).
|
||||||
|
|
||||||
|
Glob syntax:
|
||||||
|
- `*` — matches any characters except `:` (single segment)
|
||||||
|
- `**` — matches anything including `:` (multiple segments)
|
||||||
|
|
||||||
|
| Pattern | Matches |
|
||||||
|
|---|---|
|
||||||
|
| `agent:*:cron:**` | All cron sessions for any agent |
|
||||||
|
| `agent:*:subagent:**` | All subagent sessions for any agent |
|
||||||
|
| `agent:main:**` | All sessions under the `main` agent |
|
||||||
|
|
||||||
|
**Difference between the two options:**
|
||||||
|
|
||||||
|
| | `ignoreSessionPatterns` | `statelessSessionPatterns` |
|
||||||
|
|---|---|---|
|
||||||
|
| Retain | Skipped | Always skipped |
|
||||||
|
| Recall | Skipped | Skipped only when `skipStatelessSessions: true` |
|
||||||
|
|
||||||
|
**Example config** — exclude cron jobs from memory entirely, allow subagents to read but not write memories:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ignoreSessionPatterns": ["agent:*:cron:**"],
|
||||||
|
"statelessSessionPatterns": ["agent:*:subagent:**"],
|
||||||
|
"skipStatelessSessions": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Retention details
|
## Retention details
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,21 @@
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "How often to attempt flushing queued retains in ms. Default: 60000 (1 min).",
|
"description": "How often to attempt flushing queued retains in ms. Default: 60000 (1 min).",
|
||||||
"default": 60000
|
"default": 60000
|
||||||
|
},
|
||||||
|
"ignoreSessionPatterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Session key glob patterns to skip entirely (no recall, no retain). E.g. [\"agent:main:**\", \"agent:*:cron:**\"]. * matches non-colon chars, ** matches anything."
|
||||||
|
},
|
||||||
|
"statelessSessionPatterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Session key glob patterns for read-only sessions: retain is always skipped, recall is skipped when skipStatelessSessions is true. E.g. [\"agent:*:subagent:**\", \"agent:*:heartbeat:**\"]."
|
||||||
|
},
|
||||||
|
"skipStatelessSessions": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "When true (default), sessions matching statelessSessionPatterns also skip recall. When false, they can recall but never retain.",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|
@ -404,6 +419,18 @@
|
||||||
"retainQueueFlushIntervalMs": {
|
"retainQueueFlushIntervalMs": {
|
||||||
"label": "Retain Queue Flush Interval (ms)",
|
"label": "Retain Queue Flush Interval (ms)",
|
||||||
"placeholder": "60000"
|
"placeholder": "60000"
|
||||||
|
},
|
||||||
|
"ignoreSessionPatterns": {
|
||||||
|
"label": "Ignore Session Patterns",
|
||||||
|
"placeholder": "e.g. [\"agent:main:**\", \"agent:*:cron:**\"]"
|
||||||
|
},
|
||||||
|
"statelessSessionPatterns": {
|
||||||
|
"label": "Stateless Session Patterns",
|
||||||
|
"placeholder": "e.g. [\"agent:*:subagent:**\", \"agent:*:heartbeat:**\"]"
|
||||||
|
},
|
||||||
|
"skipStatelessSessions": {
|
||||||
|
"label": "Skip Stateless Sessions",
|
||||||
|
"placeholder": "true (default)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { MoltbotPluginAPI, PluginConfig, PluginHookAgentContext, MemoryResu
|
||||||
import { HindsightEmbedManager } from './embed-manager.js';
|
import { HindsightEmbedManager } from './embed-manager.js';
|
||||||
import { HindsightClient, type HindsightClientOptions } from './client.js';
|
import { HindsightClient, type HindsightClientOptions } from './client.js';
|
||||||
import { RetainQueue } from './retain-queue.js';
|
import { RetainQueue } from './retain-queue.js';
|
||||||
|
import { compileSessionPatterns, matchesSessionPattern } from './session-patterns.js';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
@ -841,6 +842,9 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
|
||||||
: DEFAULT_RECALL_PROMPT_PREAMBLE,
|
: DEFAULT_RECALL_PROMPT_PREAMBLE,
|
||||||
recallInjectionPosition: typeof config.recallInjectionPosition === 'string' && ['prepend', 'append', 'user'].includes(config.recallInjectionPosition) ? config.recallInjectionPosition as PluginConfig['recallInjectionPosition'] : undefined,
|
recallInjectionPosition: typeof config.recallInjectionPosition === 'string' && ['prepend', 'append', 'user'].includes(config.recallInjectionPosition) ? config.recallInjectionPosition as PluginConfig['recallInjectionPosition'] : undefined,
|
||||||
recallTimeoutMs: typeof config.recallTimeoutMs === 'number' && config.recallTimeoutMs >= 1000 ? config.recallTimeoutMs : undefined,
|
recallTimeoutMs: typeof config.recallTimeoutMs === 'number' && config.recallTimeoutMs >= 1000 ? config.recallTimeoutMs : undefined,
|
||||||
|
ignoreSessionPatterns: Array.isArray(config.ignoreSessionPatterns) ? config.ignoreSessionPatterns : [],
|
||||||
|
statelessSessionPatterns: Array.isArray(config.statelessSessionPatterns) ? config.statelessSessionPatterns : [],
|
||||||
|
skipStatelessSessions: config.skipStatelessSessions !== false,
|
||||||
debug: config.debug ?? false,
|
debug: config.debug ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1209,6 +1213,24 @@ export default function (api: MoltbotPluginAPI) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session pattern filtering
|
||||||
|
const sessionKey = ctx?.sessionKey;
|
||||||
|
if (sessionKey) {
|
||||||
|
const ignorePatterns = compileSessionPatterns(pluginConfig.ignoreSessionPatterns ?? []);
|
||||||
|
if (ignorePatterns.length > 0 && matchesSessionPattern(sessionKey, ignorePatterns)) {
|
||||||
|
debug(`[Hindsight] Skipping recall: session '${sessionKey}' matches ignoreSessionPatterns`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const skipStateless = pluginConfig.skipStatelessSessions !== false;
|
||||||
|
if (skipStateless) {
|
||||||
|
const statelessPatterns = compileSessionPatterns(pluginConfig.statelessSessionPatterns ?? []);
|
||||||
|
if (statelessPatterns.length > 0 && matchesSessionPattern(sessionKey, statelessPatterns)) {
|
||||||
|
debug(`[Hindsight] Skipping recall: session '${sessionKey}' matches statelessSessionPatterns (skipStatelessSessions=true)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Skip auto-recall when disabled (agent has its own recall tool)
|
// Skip auto-recall when disabled (agent has its own recall tool)
|
||||||
if (!pluginConfig.autoRecall) {
|
if (!pluginConfig.autoRecall) {
|
||||||
debug('[Hindsight] Auto-recall disabled via config, skipping');
|
debug('[Hindsight] Auto-recall disabled via config, skipping');
|
||||||
|
|
@ -1361,6 +1383,21 @@ ${memoriesFormatted}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session pattern filtering
|
||||||
|
const agentEndSessionKey = effectiveCtx?.sessionKey;
|
||||||
|
if (agentEndSessionKey) {
|
||||||
|
const ignorePatterns = compileSessionPatterns(pluginConfig.ignoreSessionPatterns ?? []);
|
||||||
|
if (ignorePatterns.length > 0 && matchesSessionPattern(agentEndSessionKey, ignorePatterns)) {
|
||||||
|
debug(`[Hindsight] Skipping retain: session '${agentEndSessionKey}' matches ignoreSessionPatterns`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const statelessPatterns = compileSessionPatterns(pluginConfig.statelessSessionPatterns ?? []);
|
||||||
|
if (statelessPatterns.length > 0 && matchesSessionPattern(agentEndSessionKey, statelessPatterns)) {
|
||||||
|
debug(`[Hindsight] Skipping retain: session '${agentEndSessionKey}' matches statelessSessionPatterns`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Derive bank ID from context — enrich ctx.senderId from the session cache.
|
// Derive bank ID from context — enrich ctx.senderId from the session cache.
|
||||||
// event.messages in agent_end is clean history without OpenClaw's metadata blocks;
|
// event.messages in agent_end is clean history without OpenClaw's metadata blocks;
|
||||||
// the sender ID was captured during before_prompt_build where event.prompt has them.
|
// the sender ID was captured during before_prompt_build where event.prompt has them.
|
||||||
|
|
|
||||||
87
hindsight-integrations/openclaw/src/session-patterns.test.ts
Normal file
87
hindsight-integrations/openclaw/src/session-patterns.test.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
compileSessionPattern,
|
||||||
|
compileSessionPatterns,
|
||||||
|
matchesSessionPattern,
|
||||||
|
} from './session-patterns.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// compileSessionPattern
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('compileSessionPattern', () => {
|
||||||
|
it('matches an exact key', () => {
|
||||||
|
const p = compileSessionPattern('agent:main:sess-123');
|
||||||
|
expect(p.test('agent:main:sess-123')).toBe(true);
|
||||||
|
expect(p.test('agent:main:sess-456')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('single * does not cross colon', () => {
|
||||||
|
const p = compileSessionPattern('agent:*:sess');
|
||||||
|
expect(p.test('agent:main:sess')).toBe(true);
|
||||||
|
expect(p.test('agent:subagent:sess')).toBe(true);
|
||||||
|
expect(p.test('agent:a:b:sess')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('double ** crosses colons', () => {
|
||||||
|
const p = compileSessionPattern('agent:main:**');
|
||||||
|
expect(p.test('agent:main:sess-abc123')).toBe(true);
|
||||||
|
expect(p.test('agent:main:a:b:c')).toBe(true);
|
||||||
|
expect(p.test('agent:other:sess-abc123')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('double ** at start matches any prefix', () => {
|
||||||
|
const p = compileSessionPattern('**:subagent:**');
|
||||||
|
expect(p.test('claude-code:subagent:sess-abc')).toBe(true);
|
||||||
|
expect(p.test('mybot:subagent:sess-xyz')).toBe(true);
|
||||||
|
expect(p.test('mybot:main:sess-xyz')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches lossless-claw cron pattern', () => {
|
||||||
|
const p = compileSessionPattern('agent:*:cron:**');
|
||||||
|
expect(p.test('agent:mybot:cron:sess-123')).toBe(true);
|
||||||
|
expect(p.test('agent:mybot:subagent:sess-123')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches lossless-claw subagent pattern', () => {
|
||||||
|
const p = compileSessionPattern('agent:*:subagent:**');
|
||||||
|
expect(p.test('agent:main:subagent:sess-abc')).toBe(true);
|
||||||
|
expect(p.test('agent:x:subagent:sess-123')).toBe(true);
|
||||||
|
expect(p.test('agent:a:b:subagent:sess')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// matchesSessionPattern
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('matchesSessionPattern', () => {
|
||||||
|
it('returns true when any pattern matches', () => {
|
||||||
|
const patterns = compileSessionPatterns(['agent:main:**', 'agent:*:cron:**']);
|
||||||
|
expect(matchesSessionPattern('agent:main:sess-abc', patterns)).toBe(true);
|
||||||
|
expect(matchesSessionPattern('agent:mybot:cron:sess-xyz', patterns)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when no pattern matches', () => {
|
||||||
|
const patterns = compileSessionPatterns(['agent:main:**']);
|
||||||
|
expect(matchesSessionPattern('agent:subagent:sess-abc', patterns)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for empty pattern list', () => {
|
||||||
|
expect(matchesSessionPattern('agent:main:sess', [])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lossless-claw ignoreSessionPatterns example', () => {
|
||||||
|
const patterns = compileSessionPatterns(['agent:main:**', 'agent:*:cron:**']);
|
||||||
|
expect(matchesSessionPattern('agent:main:sess-abc123', patterns)).toBe(true);
|
||||||
|
expect(matchesSessionPattern('agent:mybot:cron:sess-123', patterns)).toBe(true);
|
||||||
|
expect(matchesSessionPattern('agent:mybot:subagent:sess-123', patterns)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lossless-claw statelessSessionPatterns example', () => {
|
||||||
|
const patterns = compileSessionPatterns(['agent:*:subagent:**', 'agent:*:heartbeat:**']);
|
||||||
|
expect(matchesSessionPattern('agent:main:subagent:sess-abc', patterns)).toBe(true);
|
||||||
|
expect(matchesSessionPattern('agent:main:heartbeat:sess-abc', patterns)).toBe(true);
|
||||||
|
expect(matchesSessionPattern('agent:main:sess-abc', patterns)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
23
hindsight-integrations/openclaw/src/session-patterns.ts
Normal file
23
hindsight-integrations/openclaw/src/session-patterns.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Compile a session glob into a regex.
|
||||||
|
*
|
||||||
|
* `*` matches any non-colon characters, while `**` can span colons.
|
||||||
|
*/
|
||||||
|
export function compileSessionPattern(pattern: string): RegExp {
|
||||||
|
const escaped = pattern
|
||||||
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
||||||
|
.replace(/\*\*/g, "\u0000")
|
||||||
|
.replace(/\*/g, "[^:]*")
|
||||||
|
.replace(/\u0000/g, ".*");
|
||||||
|
return new RegExp(`^${escaped}$`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compile all configured ignore patterns once at startup. */
|
||||||
|
export function compileSessionPatterns(patterns: string[]): RegExp[] {
|
||||||
|
return patterns.map((pattern) => compileSessionPattern(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check whether a session key matches any compiled ignore pattern. */
|
||||||
|
export function matchesSessionPattern(sessionKey: string, patterns: RegExp[]): boolean {
|
||||||
|
return patterns.some((pattern) => pattern.test(sessionKey));
|
||||||
|
}
|
||||||
|
|
@ -83,6 +83,9 @@ export interface PluginConfig {
|
||||||
recallMaxQueryChars?: number; // Max chars for composed recall query. Default: 800
|
recallMaxQueryChars?: number; // Max chars for composed recall query. Default: 800
|
||||||
recallPromptPreamble?: string; // Prompt preamble placed above recalled memories. Default: built-in guidance text.
|
recallPromptPreamble?: string; // Prompt preamble placed above recalled memories. Default: built-in guidance text.
|
||||||
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.
|
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.
|
||||||
|
ignoreSessionPatterns?: string[]; // Session key glob patterns to skip entirely (no recall, no retain). E.g. ["agent:main:**", "agent:*:cron:**"]
|
||||||
|
statelessSessionPatterns?: string[]; // Session key glob patterns for read-only sessions (recall allowed, retain skipped). E.g. ["agent:*:subagent:**"]
|
||||||
|
skipStatelessSessions?: boolean; // When true (default), stateless sessions also skip recall. When false, they recall but never retain.
|
||||||
debug?: boolean; // Enable debug logging (default: false)
|
debug?: boolean; // Enable debug logging (default: false)
|
||||||
logLevel?: 'off' | 'error' | 'warning' | 'info' | 'debug'; // Console log verbosity (default: 'info').
|
logLevel?: 'off' | 'error' | 'warning' | 'info' | 'debug'; // Console log verbosity (default: 'info').
|
||||||
logSummaryIntervalMs?: number; // Batch retain/recall log summaries over this interval in ms. 0 = log every event. Default: 300000 (5 min).
|
logSummaryIntervalMs?: number; // Batch retain/recall log summaries over this interval in ms. 0 = log every event. Default: 300000 (5 min).
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import type { MoltbotPluginAPI, PluginConfig } from '../src/types.js';
|
||||||
import type { RecallResponse, RetainResponse } from '../src/types.js';
|
import type { RecallResponse, RetainResponse } from '../src/types.js';
|
||||||
|
|
||||||
const HINDSIGHT_API_URL = process.env.HINDSIGHT_API_URL || 'http://localhost:8888';
|
const HINDSIGHT_API_URL = process.env.HINDSIGHT_API_URL || 'http://localhost:8888';
|
||||||
|
const HINDSIGHT_API_TOKEN = process.env.HINDSIGHT_API_TOKEN || '';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
@ -145,7 +146,12 @@ beforeAll(async () => {
|
||||||
recallContextTurns: 3,
|
recallContextTurns: 3,
|
||||||
recallMaxQueryChars: 180,
|
recallMaxQueryChars: 180,
|
||||||
recallRoles: ['user'],
|
recallRoles: ['user'],
|
||||||
|
// Session pattern filtering — only affects keys matching these patterns
|
||||||
|
ignoreSessionPatterns: ['agent:main:**', 'agent:*:cron:**'],
|
||||||
|
statelessSessionPatterns: ['agent:*:subagent:**', 'agent:*:heartbeat:**'],
|
||||||
|
skipStatelessSessions: true,
|
||||||
// No bankMission — keeps init lean
|
// No bankMission — keeps init lean
|
||||||
|
...(HINDSIGHT_API_TOKEN ? { hindsightApiUrl: HINDSIGHT_API_URL, hindsightApiToken: HINDSIGHT_API_TOKEN } : {}),
|
||||||
});
|
});
|
||||||
triggerHook = handle.trigger;
|
triggerHook = handle.trigger;
|
||||||
stopServicesFn = handle.stopServices;
|
stopServicesFn = handle.stopServices;
|
||||||
|
|
@ -569,3 +575,81 @@ describe('agent_end hook', () => {
|
||||||
expect(req.metadata?.message_count).toBe('2');
|
expect(req.metadata?.message_count).toBe('2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// session pattern filtering
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('session pattern filtering', () => {
|
||||||
|
it('skips retain when session key matches ignoreSessionPatterns', async () => {
|
||||||
|
if (!apiReachable) return;
|
||||||
|
|
||||||
|
await triggerHook(
|
||||||
|
'agent_end',
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
messages: [{ role: 'user', content: 'I love TypeScript.' }],
|
||||||
|
},
|
||||||
|
{ messageProvider: 'telegram', senderId: 'U100', sessionKey: 'agent:mybot:cron:sess-cron-001' },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(retainSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips retain when session key matches statelessSessionPatterns', async () => {
|
||||||
|
if (!apiReachable) return;
|
||||||
|
|
||||||
|
await triggerHook(
|
||||||
|
'agent_end',
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
messages: [{ role: 'user', content: 'I prefer Python.' }],
|
||||||
|
},
|
||||||
|
{ messageProvider: 'telegram', senderId: 'U101', sessionKey: 'agent:mybot:subagent:sess-sub-001' },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(retainSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips recall when session key matches ignoreSessionPatterns', async () => {
|
||||||
|
if (!apiReachable) return;
|
||||||
|
|
||||||
|
const result = await triggerHook(
|
||||||
|
'before_prompt_build',
|
||||||
|
{ rawMessage: 'What programming language do I like?', prompt: '', messages: [] },
|
||||||
|
{ messageProvider: 'telegram', senderId: 'U102', sessionKey: 'agent:mybot:cron:sess-cron-002' },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(recallSpy).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips recall for stateless session when skipStatelessSessions is true (default)', async () => {
|
||||||
|
if (!apiReachable) return;
|
||||||
|
|
||||||
|
const result = await triggerHook(
|
||||||
|
'before_prompt_build',
|
||||||
|
{ rawMessage: 'What programming language do I like?', prompt: '', messages: [] },
|
||||||
|
{ messageProvider: 'telegram', senderId: 'U103', sessionKey: 'agent:mybot:heartbeat:sess-hb-001' },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(recallSpy).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not skip a main session that matches no pattern', async () => {
|
||||||
|
if (!apiReachable) return;
|
||||||
|
retainSpy.mockResolvedValue(OK_RETAIN);
|
||||||
|
|
||||||
|
await triggerHook(
|
||||||
|
'agent_end',
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
messages: [{ role: 'user', content: 'I enjoy hiking.' }],
|
||||||
|
},
|
||||||
|
{ messageProvider: 'telegram', senderId: 'U104', sessionKey: 'agent:mybot:main:sess-main-001' },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(retainSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ const __dirname = dirname(__filename);
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const HINDSIGHT_API_URL = process.env.HINDSIGHT_API_URL || 'http://localhost:8888';
|
const HINDSIGHT_API_URL = process.env.HINDSIGHT_API_URL || 'http://localhost:8888';
|
||||||
|
const HINDSIGHT_API_TOKEN = process.env.HINDSIGHT_API_TOKEN || '';
|
||||||
const LLM_PROVIDER = process.env.HINDSIGHT_API_LLM_PROVIDER || '';
|
const LLM_PROVIDER = process.env.HINDSIGHT_API_LLM_PROVIDER || '';
|
||||||
const LLM_API_KEY = process.env.HINDSIGHT_API_LLM_API_KEY || '';
|
const LLM_API_KEY = process.env.HINDSIGHT_API_LLM_API_KEY || '';
|
||||||
const LLM_MODEL = process.env.HINDSIGHT_API_LLM_MODEL || '';
|
const LLM_MODEL = process.env.HINDSIGHT_API_LLM_MODEL || '';
|
||||||
|
|
@ -81,6 +82,7 @@ describe('HindsightClient – HTTP Mode', () => {
|
||||||
llmApiKey: LLM_API_KEY || 'test-key',
|
llmApiKey: LLM_API_KEY || 'test-key',
|
||||||
llmModel: LLM_MODEL || undefined,
|
llmModel: LLM_MODEL || undefined,
|
||||||
apiUrl: HINDSIGHT_API_URL,
|
apiUrl: HINDSIGHT_API_URL,
|
||||||
|
apiToken: HINDSIGHT_API_TOKEN || undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue