Skip to content

Commit 27f6188

Browse files
Mijamind719codex
andauthored
feat(openclaw-plugin): add session-pattern guard for ingest reply assist (#1136)
Co-authored-by: GPT-5.4 <noreply@openai.com>
1 parent 40fa8ba commit 27f6188

File tree

5 files changed

+179
-14
lines changed

5 files changed

+179
-14
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { memoryOpenVikingConfigSchema } from "../config.js";
4+
import {
5+
compileSessionPatterns,
6+
matchesSessionPattern,
7+
shouldSkipIngestReplyAssistSession,
8+
} from "../text-utils.js";
9+
10+
describe("ingest reply assist session patterns", () => {
11+
it("parses ignore session patterns from config", () => {
12+
const cfg = memoryOpenVikingConfigSchema.parse({
13+
ingestReplyAssistIgnoreSessionPatterns: [
14+
"agent:*:cron:**",
15+
"agent:ops:maintenance:**",
16+
],
17+
});
18+
19+
expect(cfg.ingestReplyAssistIgnoreSessionPatterns).toEqual([
20+
"agent:*:cron:**",
21+
"agent:ops:maintenance:**",
22+
]);
23+
});
24+
25+
it("defaults ignore session patterns to an empty list", () => {
26+
const cfg = memoryOpenVikingConfigSchema.parse({});
27+
expect(cfg.ingestReplyAssistIgnoreSessionPatterns).toEqual([]);
28+
});
29+
30+
it("matches lossless-claw style session globs", () => {
31+
const patterns = compileSessionPatterns([
32+
"agent:*:cron:**",
33+
"agent:ops:maintenance:**",
34+
]);
35+
36+
expect(matchesSessionPattern("agent:main:cron:nightly:run:1", patterns)).toBe(true);
37+
expect(matchesSessionPattern("agent:ops:maintenance:weekly", patterns)).toBe(true);
38+
expect(matchesSessionPattern("agent:main:main", patterns)).toBe(false);
39+
});
40+
41+
it("prefers sessionKey over sessionId when deciding whether to skip assist", () => {
42+
const patterns = compileSessionPatterns(["agent:*:cron:**"]);
43+
44+
expect(
45+
shouldSkipIngestReplyAssistSession(
46+
{
47+
sessionId: "agent:main:cron:from-id",
48+
sessionKey: "agent:main:main",
49+
},
50+
patterns,
51+
),
52+
).toBe(false);
53+
});
54+
55+
it("falls back to sessionId when sessionKey is unavailable", () => {
56+
const patterns = compileSessionPatterns(["agent:*:cron:**"]);
57+
58+
expect(
59+
shouldSkipIngestReplyAssistSession(
60+
{
61+
sessionId: "agent:main:cron:nightly:run:1",
62+
},
63+
patterns,
64+
),
65+
).toBe(true);
66+
});
67+
});

examples/openclaw-plugin/config.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type MemoryOpenVikingConfig = {
2727
ingestReplyAssist?: boolean;
2828
ingestReplyAssistMinSpeakerTurns?: number;
2929
ingestReplyAssistMinChars?: number;
30+
ingestReplyAssistIgnoreSessionPatterns?: string[];
3031
/**
3132
* When true (default), emit structured `openviking: diag {...}` lines (and any future
3233
* standard-diagnostics file writes) for assemble/afterTurn. Set false to disable.
@@ -51,6 +52,7 @@ const DEFAULT_COMMIT_TOKEN_THRESHOLD = 20000;
5152
const DEFAULT_INGEST_REPLY_ASSIST = true;
5253
const DEFAULT_INGEST_REPLY_ASSIST_MIN_SPEAKER_TURNS = 2;
5354
const DEFAULT_INGEST_REPLY_ASSIST_MIN_CHARS = 120;
55+
const DEFAULT_INGEST_REPLY_ASSIST_IGNORE_SESSION_PATTERNS: string[] = [];
5456
const DEFAULT_EMIT_STANDARD_DIAGNOSTICS = false;
5557
const DEFAULT_LOCAL_CONFIG_PATH = join(homedir(), ".openviking", "ov.conf");
5658

@@ -86,6 +88,22 @@ function toNumber(value: unknown, fallback: number): number {
8688
return fallback;
8789
}
8890

91+
function toStringArray(value: unknown, fallback: string[]): string[] {
92+
if (Array.isArray(value)) {
93+
return value
94+
.filter((entry): entry is string => typeof entry === "string")
95+
.map((entry) => entry.trim())
96+
.filter(Boolean);
97+
}
98+
if (typeof value === "string") {
99+
return value
100+
.split(/[,\n]/)
101+
.map((entry) => entry.trim())
102+
.filter(Boolean);
103+
}
104+
return fallback;
105+
}
106+
89107
/** True when env is 1 / true / yes (case-insensitive). Used for debug flags without editing plugin JSON. */
90108
function envFlag(name: string): boolean {
91109
const v = process.env[name];
@@ -142,6 +160,7 @@ export const memoryOpenVikingConfigSchema = {
142160
"ingestReplyAssist",
143161
"ingestReplyAssistMinSpeakerTurns",
144162
"ingestReplyAssistMinChars",
163+
"ingestReplyAssistIgnoreSessionPatterns",
145164
"emitStandardDiagnostics",
146165
"logFindRequests",
147166
],
@@ -228,6 +247,10 @@ export const memoryOpenVikingConfigSchema = {
228247
Math.floor(toNumber(cfg.ingestReplyAssistMinChars, DEFAULT_INGEST_REPLY_ASSIST_MIN_CHARS)),
229248
),
230249
),
250+
ingestReplyAssistIgnoreSessionPatterns: toStringArray(
251+
cfg.ingestReplyAssistIgnoreSessionPatterns,
252+
DEFAULT_INGEST_REPLY_ASSIST_IGNORE_SESSION_PATTERNS,
253+
),
231254
emitStandardDiagnostics:
232255
typeof cfg.emitStandardDiagnostics === "boolean"
233256
? cfg.emitStandardDiagnostics
@@ -350,6 +373,12 @@ export const memoryOpenVikingConfigSchema = {
350373
help: "Minimum sanitized text length required before ingest reply assist can trigger.",
351374
advanced: true,
352375
},
376+
ingestReplyAssistIgnoreSessionPatterns: {
377+
label: "Ingest Ignore Session Patterns",
378+
placeholder: "agent:*:cron:**",
379+
help: "Skip ingest reply assist when the session key matches any glob pattern. Use * within one segment and ** across segments.",
380+
advanced: true,
381+
},
353382
emitStandardDiagnostics: {
354383
label: "Standard diagnostics (diag JSON lines)",
355384
advanced: true,

examples/openclaw-plugin/index.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { OpenVikingClient, localClientCache, localClientPendingPromises, isMemor
88
import type { FindResultItem, PendingClientEntry, CommitSessionResult, OVMessage } from "./client.js";
99
import { formatMessageFaithful } from "./context-engine.js";
1010
import {
11+
compileSessionPatterns,
1112
isTranscriptLikeIngest,
1213
extractLatestUserText,
14+
shouldSkipIngestReplyAssistSession,
1315
} from "./text-utils.js";
1416
import {
1517
clampScore,
@@ -273,6 +275,9 @@ const contextEnginePlugin = {
273275
? (api.pluginConfig as Record<string, unknown>)
274276
: {};
275277
const cfg = memoryOpenVikingConfigSchema.parse(api.pluginConfig);
278+
const ingestReplyAssistIgnoreSessionPatterns = compileSessionPatterns(
279+
cfg.ingestReplyAssistIgnoreSessionPatterns,
280+
);
276281
const rawAgentId = rawCfg.agentId;
277282
if (cfg.logFindRequests) {
278283
api.logger.info(
@@ -925,22 +930,28 @@ const contextEnginePlugin = {
925930
}
926931

927932
if (cfg.ingestReplyAssist) {
928-
const decision = isTranscriptLikeIngest(queryText, {
929-
minSpeakerTurns: cfg.ingestReplyAssistMinSpeakerTurns,
930-
minChars: cfg.ingestReplyAssistMinChars,
931-
});
932-
if (decision.shouldAssist) {
933+
if (shouldSkipIngestReplyAssistSession(ctx ?? {}, ingestReplyAssistIgnoreSessionPatterns)) {
933934
verboseRoutingInfo(
934-
`openviking: ingest-reply-assist applied (reason=${decision.reason}, speakerTurns=${decision.speakerTurns}, chars=${decision.chars})`,
935-
);
936-
prependContextParts.push(
937-
"<ingest-reply-assist>\n" +
938-
"The latest user input looks like a multi-speaker transcript used for memory ingestion.\n" +
939-
"Reply with 1-2 concise sentences to acknowledge or summarize key points.\n" +
940-
"Do not output NO_REPLY or an empty reply.\n" +
941-
"Do not fabricate facts beyond the provided transcript and recalled memories.\n" +
942-
"</ingest-reply-assist>",
935+
`openviking: skipping ingest-reply-assist due to session pattern match (sessionKey=${ctx?.sessionKey ?? "none"}, sessionId=${ctx?.sessionId ?? "none"})`,
943936
);
937+
} else {
938+
const decision = isTranscriptLikeIngest(queryText, {
939+
minSpeakerTurns: cfg.ingestReplyAssistMinSpeakerTurns,
940+
minChars: cfg.ingestReplyAssistMinChars,
941+
});
942+
if (decision.shouldAssist) {
943+
verboseRoutingInfo(
944+
`openviking: ingest-reply-assist applied (reason=${decision.reason}, speakerTurns=${decision.speakerTurns}, chars=${decision.chars})`,
945+
);
946+
prependContextParts.push(
947+
"<ingest-reply-assist>\n" +
948+
"The latest user input looks like a multi-speaker transcript used for memory ingestion.\n" +
949+
"Reply with 1-2 concise sentences to acknowledge or summarize key points.\n" +
950+
"Do not output NO_REPLY or an empty reply.\n" +
951+
"Do not fabricate facts beyond the provided transcript and recalled memories.\n" +
952+
"</ingest-reply-assist>",
953+
);
954+
}
944955
}
945956
}
946957

examples/openclaw-plugin/openclaw.plugin.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@
113113
"help": "Minimum sanitized text length required before ingest reply assist can trigger.",
114114
"advanced": true
115115
},
116+
"ingestReplyAssistIgnoreSessionPatterns": {
117+
"label": "Ingest Ignore Session Patterns",
118+
"placeholder": "agent:*:cron:**",
119+
"help": "Skip ingest reply assist when the session key matches any glob pattern. Use * within one segment and ** across segments.",
120+
"advanced": true
121+
},
116122
"emitStandardDiagnostics": {
117123
"label": "Standard diagnostics (diag JSON lines)",
118124
"advanced": true,
@@ -191,6 +197,12 @@
191197
"ingestReplyAssistMinChars": {
192198
"type": "number"
193199
},
200+
"ingestReplyAssistIgnoreSessionPatterns": {
201+
"type": "array",
202+
"items": {
203+
"type": "string"
204+
}
205+
},
194206
"emitStandardDiagnostics": {
195207
"type": "boolean"
196208
},

examples/openclaw-plugin/text-utils.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,52 @@ export type TranscriptLikeIngestDecision = {
8080
chars: number;
8181
};
8282

83+
export function compileSessionPattern(pattern: string): RegExp {
84+
const escaped = pattern
85+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
86+
.replace(/\*\*/g, "\u0000")
87+
.replace(/\*/g, "[^:]*")
88+
.replace(/\u0000/g, ".*");
89+
return new RegExp(`^${escaped}$`);
90+
}
91+
92+
export function compileSessionPatterns(patterns: string[]): RegExp[] {
93+
return patterns.map((pattern) => compileSessionPattern(pattern));
94+
}
95+
96+
export function matchesSessionPattern(sessionRef: string, patterns: RegExp[]): boolean {
97+
return patterns.some((pattern) => pattern.test(sessionRef));
98+
}
99+
100+
export function resolveSessionPatternCandidate(params: {
101+
sessionId?: string;
102+
sessionKey?: string;
103+
}): string | undefined {
104+
const sessionKey = typeof params.sessionKey === "string" ? params.sessionKey.trim() : "";
105+
if (sessionKey) {
106+
return sessionKey;
107+
}
108+
const sessionId = typeof params.sessionId === "string" ? params.sessionId.trim() : "";
109+
return sessionId || undefined;
110+
}
111+
112+
export function shouldSkipIngestReplyAssistSession(
113+
params: {
114+
sessionId?: string;
115+
sessionKey?: string;
116+
},
117+
patterns: RegExp[],
118+
): boolean {
119+
if (patterns.length === 0) {
120+
return false;
121+
}
122+
const candidate = resolveSessionPatternCandidate(params);
123+
if (!candidate) {
124+
return false;
125+
}
126+
return matchesSessionPattern(candidate, patterns);
127+
}
128+
83129
function countSpeakerTurns(text: string): number {
84130
let count = 0;
85131
for (const _match of text.matchAll(SPEAKER_TAG_RE)) {

0 commit comments

Comments
 (0)