Fix: prevent system-reminder tags from triggering mode keywords (#1155)
Automated system messages with <system-reminder> tags were incorrectly triggering [search-mode], [analyze-mode], and other keyword modes when they contained words like "search", "find", "explore", etc. Changes: - Add removeSystemReminders() to strip <system-reminder> content before keyword detection - Add hasSystemReminder() utility function - Update keyword-detector to clean text before pattern matching - Add comprehensive test coverage for system-reminder filtering Fixes issue where automated system notifications caused agents to incorrectly enter MAXIMUM SEARCH EFFORT mode. Co-authored-by: TheEpTic <git@eptic.me>
This commit is contained in:
@@ -338,6 +338,197 @@ describe("keyword-detector word boundary", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("keyword-detector system-reminder filtering", () => {
|
||||
let logCalls: Array<{ msg: string; data?: unknown }>
|
||||
let logSpy: ReturnType<typeof spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
setMainSession(undefined)
|
||||
logCalls = []
|
||||
logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => {
|
||||
logCalls.push({ msg, data })
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
logSpy?.mockRestore()
|
||||
setMainSession(undefined)
|
||||
})
|
||||
|
||||
function createMockPluginInput() {
|
||||
return {
|
||||
client: {
|
||||
tui: {
|
||||
showToast: async () => {},
|
||||
},
|
||||
},
|
||||
} as any
|
||||
}
|
||||
|
||||
test("should NOT trigger search mode from keywords inside <system-reminder> tags", async () => {
|
||||
// #given - message contains search keywords only inside system-reminder tags
|
||||
const collector = new ContextCollector()
|
||||
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
|
||||
const sessionID = "test-session"
|
||||
const output = {
|
||||
message: {} as Record<string, unknown>,
|
||||
parts: [{
|
||||
type: "text",
|
||||
text: `<system-reminder>
|
||||
The system will search for the file and find all occurrences.
|
||||
Please locate and scan the directory.
|
||||
</system-reminder>`
|
||||
}],
|
||||
}
|
||||
|
||||
// #when - keyword detection runs on system-reminder content
|
||||
await hook["chat.message"]({ sessionID }, output)
|
||||
|
||||
// #then - should NOT trigger search mode (text should remain unchanged)
|
||||
const textPart = output.parts.find(p => p.type === "text")
|
||||
expect(textPart).toBeDefined()
|
||||
expect(textPart!.text).not.toContain("[search-mode]")
|
||||
expect(textPart!.text).toContain("<system-reminder>")
|
||||
})
|
||||
|
||||
test("should NOT trigger analyze mode from keywords inside <system-reminder> tags", async () => {
|
||||
// #given - message contains analyze keywords only inside system-reminder tags
|
||||
const collector = new ContextCollector()
|
||||
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
|
||||
const sessionID = "test-session"
|
||||
const output = {
|
||||
message: {} as Record<string, unknown>,
|
||||
parts: [{
|
||||
type: "text",
|
||||
text: `<system-reminder>
|
||||
You should investigate and examine the code carefully.
|
||||
Research the implementation details.
|
||||
</system-reminder>`
|
||||
}],
|
||||
}
|
||||
|
||||
// #when - keyword detection runs on system-reminder content
|
||||
await hook["chat.message"]({ sessionID }, output)
|
||||
|
||||
// #then - should NOT trigger analyze mode
|
||||
const textPart = output.parts.find(p => p.type === "text")
|
||||
expect(textPart).toBeDefined()
|
||||
expect(textPart!.text).not.toContain("[analyze-mode]")
|
||||
expect(textPart!.text).toContain("<system-reminder>")
|
||||
})
|
||||
|
||||
test("should detect keywords in user text even when system-reminder is present", async () => {
|
||||
// #given - message contains both system-reminder and user search keyword
|
||||
const collector = new ContextCollector()
|
||||
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
|
||||
const sessionID = "test-session"
|
||||
const output = {
|
||||
message: {} as Record<string, unknown>,
|
||||
parts: [{
|
||||
type: "text",
|
||||
text: `<system-reminder>
|
||||
System will find and locate files.
|
||||
</system-reminder>
|
||||
|
||||
Please search for the bug in the code.`
|
||||
}],
|
||||
}
|
||||
|
||||
// #when - keyword detection runs on mixed content
|
||||
await hook["chat.message"]({ sessionID }, output)
|
||||
|
||||
// #then - should trigger search mode from user text only
|
||||
const textPart = output.parts.find(p => p.type === "text")
|
||||
expect(textPart).toBeDefined()
|
||||
expect(textPart!.text).toContain("[search-mode]")
|
||||
expect(textPart!.text).toContain("Please search for the bug in the code.")
|
||||
})
|
||||
|
||||
test("should handle multiple system-reminder tags in message", async () => {
|
||||
// #given - message contains multiple system-reminder blocks with keywords
|
||||
const collector = new ContextCollector()
|
||||
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
|
||||
const sessionID = "test-session"
|
||||
const output = {
|
||||
message: {} as Record<string, unknown>,
|
||||
parts: [{
|
||||
type: "text",
|
||||
text: `<system-reminder>
|
||||
First reminder with search and find keywords.
|
||||
</system-reminder>
|
||||
|
||||
User message without keywords.
|
||||
|
||||
<system-reminder>
|
||||
Second reminder with investigate and examine keywords.
|
||||
</system-reminder>`
|
||||
}],
|
||||
}
|
||||
|
||||
// #when - keyword detection runs on message with multiple system-reminders
|
||||
await hook["chat.message"]({ sessionID }, output)
|
||||
|
||||
// #then - should NOT trigger any mode (only user text exists, no keywords)
|
||||
const textPart = output.parts.find(p => p.type === "text")
|
||||
expect(textPart).toBeDefined()
|
||||
expect(textPart!.text).not.toContain("[search-mode]")
|
||||
expect(textPart!.text).not.toContain("[analyze-mode]")
|
||||
})
|
||||
|
||||
test("should handle case-insensitive system-reminder tags", async () => {
|
||||
// #given - message contains system-reminder with different casing
|
||||
const collector = new ContextCollector()
|
||||
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
|
||||
const sessionID = "test-session"
|
||||
const output = {
|
||||
message: {} as Record<string, unknown>,
|
||||
parts: [{
|
||||
type: "text",
|
||||
text: `<SYSTEM-REMINDER>
|
||||
System will search and find files.
|
||||
</SYSTEM-REMINDER>`
|
||||
}],
|
||||
}
|
||||
|
||||
// #when - keyword detection runs on uppercase system-reminder
|
||||
await hook["chat.message"]({ sessionID }, output)
|
||||
|
||||
// #then - should NOT trigger search mode
|
||||
const textPart = output.parts.find(p => p.type === "text")
|
||||
expect(textPart).toBeDefined()
|
||||
expect(textPart!.text).not.toContain("[search-mode]")
|
||||
})
|
||||
|
||||
test("should handle multiline system-reminder content with search keywords", async () => {
|
||||
// #given - system-reminder with multiline content containing various search keywords
|
||||
const collector = new ContextCollector()
|
||||
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
|
||||
const sessionID = "test-session"
|
||||
const output = {
|
||||
message: {} as Record<string, unknown>,
|
||||
parts: [{
|
||||
type: "text",
|
||||
text: `<system-reminder>
|
||||
Commands executed:
|
||||
- find: searched for pattern
|
||||
- grep: located file
|
||||
- scan: completed
|
||||
|
||||
Please explore the codebase and discover patterns.
|
||||
</system-reminder>`
|
||||
}],
|
||||
}
|
||||
|
||||
// #when - keyword detection runs on multiline system-reminder
|
||||
await hook["chat.message"]({ sessionID }, output)
|
||||
|
||||
// #then - should NOT trigger search mode
|
||||
const textPart = output.parts.find(p => p.type === "text")
|
||||
expect(textPart).toBeDefined()
|
||||
expect(textPart!.text).not.toContain("[search-mode]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("keyword-detector agent-specific ultrawork messages", () => {
|
||||
let logCalls: Array<{ msg: string; data?: unknown }>
|
||||
let logSpy: ReturnType<typeof spyOn>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector"
|
||||
import { isPlannerAgent } from "./constants"
|
||||
import { log } from "../../shared"
|
||||
import { isSystemDirective } from "../../shared/system-directive"
|
||||
import { hasSystemReminder, isSystemDirective, removeSystemReminders } from "../../shared/system-directive"
|
||||
import { getMainSessionID, getSessionAgent, subagentSessions } from "../../features/claude-code-session-state"
|
||||
import type { ContextCollector } from "../../features/context-injector"
|
||||
|
||||
@@ -32,7 +32,10 @@ export function createKeywordDetectorHook(ctx: PluginInput, collector?: ContextC
|
||||
}
|
||||
|
||||
const currentAgent = getSessionAgent(input.sessionID) ?? input.agent
|
||||
let detectedKeywords = detectKeywordsWithType(removeCodeBlocks(promptText), currentAgent)
|
||||
|
||||
// Remove system-reminder content to prevent automated system messages from triggering mode keywords
|
||||
const cleanText = removeSystemReminders(promptText)
|
||||
let detectedKeywords = detectKeywordsWithType(removeCodeBlocks(cleanText), currentAgent)
|
||||
|
||||
if (isPlannerAgent(currentAgent)) {
|
||||
detectedKeywords = detectedKeywords.filter((k) => k.type !== "ultrawork")
|
||||
|
||||
191
src/shared/system-directive.test.ts
Normal file
191
src/shared/system-directive.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import {
|
||||
hasSystemReminder,
|
||||
removeSystemReminders,
|
||||
isSystemDirective,
|
||||
createSystemDirective,
|
||||
} from "./system-directive"
|
||||
|
||||
describe("system-directive utilities", () => {
|
||||
describe("hasSystemReminder", () => {
|
||||
test("should return true for messages containing <system-reminder> tags", () => {
|
||||
const text = `<system-reminder>
|
||||
Some system content
|
||||
</system-reminder>`
|
||||
expect(hasSystemReminder(text)).toBe(true)
|
||||
})
|
||||
|
||||
test("should return false for messages without system-reminder tags", () => {
|
||||
const text = "Just a normal user message"
|
||||
expect(hasSystemReminder(text)).toBe(false)
|
||||
})
|
||||
|
||||
test("should be case-insensitive for tag names", () => {
|
||||
const text = `<SYSTEM-REMINDER>content</SYSTEM-REMINDER>`
|
||||
expect(hasSystemReminder(text)).toBe(true)
|
||||
})
|
||||
|
||||
test("should detect system-reminder in mixed content", () => {
|
||||
const text = `User text here
|
||||
<system-reminder>
|
||||
System content
|
||||
</system-reminder>
|
||||
More user text`
|
||||
expect(hasSystemReminder(text)).toBe(true)
|
||||
})
|
||||
|
||||
test("should handle empty system-reminder tags", () => {
|
||||
const text = `<system-reminder></system-reminder>`
|
||||
expect(hasSystemReminder(text)).toBe(true)
|
||||
})
|
||||
|
||||
test("should handle multiline system-reminder content", () => {
|
||||
const text = `<system-reminder>
|
||||
Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
</system-reminder>`
|
||||
expect(hasSystemReminder(text)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("removeSystemReminders", () => {
|
||||
test("should remove system-reminder tags and content", () => {
|
||||
const text = `<system-reminder>
|
||||
System content that should be removed
|
||||
</system-reminder>`
|
||||
expect(removeSystemReminders(text)).toBe("")
|
||||
})
|
||||
|
||||
test("should preserve user text outside system-reminder tags", () => {
|
||||
const text = `User message here
|
||||
<system-reminder>
|
||||
System content to remove
|
||||
</system-reminder>
|
||||
More user text`
|
||||
const result = removeSystemReminders(text)
|
||||
expect(result).toContain("User message here")
|
||||
expect(result).toContain("More user text")
|
||||
expect(result).not.toContain("System content to remove")
|
||||
})
|
||||
|
||||
test("should remove multiple system-reminder blocks", () => {
|
||||
const text = `<system-reminder>First block</system-reminder>
|
||||
User text
|
||||
<system-reminder>Second block</system-reminder>`
|
||||
const result = removeSystemReminders(text)
|
||||
expect(result).toContain("User text")
|
||||
expect(result).not.toContain("First block")
|
||||
expect(result).not.toContain("Second block")
|
||||
})
|
||||
|
||||
test("should be case-insensitive for tag names", () => {
|
||||
const text = `<SYSTEM-REMINDER>Content</SYSTEM-REMINDER>`
|
||||
expect(removeSystemReminders(text)).toBe("")
|
||||
})
|
||||
|
||||
test("should handle nested tags correctly", () => {
|
||||
const text = `<system-reminder>
|
||||
Outer content
|
||||
<inner>Some inner tag</inner>
|
||||
</system-reminder>`
|
||||
expect(removeSystemReminders(text)).toBe("")
|
||||
})
|
||||
|
||||
test("should trim whitespace from result", () => {
|
||||
const text = `
|
||||
<system-reminder>Remove this</system-reminder>
|
||||
|
||||
User text
|
||||
|
||||
`
|
||||
const result = removeSystemReminders(text)
|
||||
expect(result).toBe("User text")
|
||||
})
|
||||
|
||||
test("should handle empty string input", () => {
|
||||
expect(removeSystemReminders("")).toBe("")
|
||||
})
|
||||
|
||||
test("should handle text with no system-reminder tags", () => {
|
||||
const text = "Just normal user text without any system reminders"
|
||||
expect(removeSystemReminders(text)).toBe(text)
|
||||
})
|
||||
|
||||
test("should preserve code blocks in user text", () => {
|
||||
const text = `Here's some code:
|
||||
\`\`\`javascript
|
||||
const x = 1;
|
||||
\`\`\`
|
||||
<system-reminder>System info</system-reminder>`
|
||||
const result = removeSystemReminders(text)
|
||||
expect(result).toContain("Here's some code:")
|
||||
expect(result).toContain("```javascript")
|
||||
expect(result).not.toContain("System info")
|
||||
})
|
||||
})
|
||||
|
||||
describe("isSystemDirective", () => {
|
||||
test("should return true for OH-MY-OPENCODE system directives", () => {
|
||||
const directive = createSystemDirective("TEST")
|
||||
expect(isSystemDirective(directive)).toBe(true)
|
||||
})
|
||||
|
||||
test("should return false for system-reminder tags", () => {
|
||||
const text = `<system-reminder>content</system-reminder>`
|
||||
expect(isSystemDirective(text)).toBe(false)
|
||||
})
|
||||
|
||||
test("should return false for normal user messages", () => {
|
||||
expect(isSystemDirective("Just a normal message")).toBe(false)
|
||||
})
|
||||
|
||||
test("should handle leading whitespace", () => {
|
||||
const directive = ` ${createSystemDirective("TEST")}`
|
||||
expect(isSystemDirective(directive)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("integration with keyword detection", () => {
|
||||
test("should prevent search keywords in system-reminders from triggering mode", () => {
|
||||
const text = `<system-reminder>
|
||||
The system will search for the file and find all occurrences.
|
||||
Please locate and scan the directory.
|
||||
</system-reminder>`
|
||||
|
||||
// After removing system reminders, no search keywords should remain
|
||||
const cleanText = removeSystemReminders(text)
|
||||
expect(cleanText).not.toMatch(/\b(search|find|locate|scan)\b/i)
|
||||
})
|
||||
|
||||
test("should preserve search keywords in user text while removing system-reminder keywords", () => {
|
||||
const text = `<system-reminder>
|
||||
System will find and locate files.
|
||||
</system-reminder>
|
||||
|
||||
Please search for the bug in the code.`
|
||||
|
||||
const cleanText = removeSystemReminders(text)
|
||||
expect(cleanText).toContain("search")
|
||||
expect(cleanText).not.toContain("find and locate")
|
||||
})
|
||||
|
||||
test("should handle complex mixed content with multiple modes", () => {
|
||||
const text = `<system-reminder>
|
||||
System will search and investigate.
|
||||
</system-reminder>
|
||||
|
||||
User wants to explore the codebase and analyze the implementation.
|
||||
|
||||
<system-reminder>
|
||||
Another system reminder with research keyword.
|
||||
</system-reminder>`
|
||||
|
||||
const cleanText = removeSystemReminders(text)
|
||||
expect(cleanText).toContain("explore")
|
||||
expect(cleanText).toContain("analyze")
|
||||
expect(cleanText).not.toContain("search and investigate")
|
||||
expect(cleanText).not.toContain("research")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -26,6 +26,26 @@ export function isSystemDirective(text: string): boolean {
|
||||
return text.trimStart().startsWith(SYSTEM_DIRECTIVE_PREFIX)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a message contains system-generated content that should be excluded
|
||||
* from keyword detection and mode triggering.
|
||||
* @param text - The message text to check
|
||||
* @returns true if the message contains system-reminder tags
|
||||
*/
|
||||
export function hasSystemReminder(text: string): boolean {
|
||||
return /<system-reminder>[\s\S]*?<\/system-reminder>/i.test(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes system-reminder tag content from text.
|
||||
* This prevents automated system messages from triggering mode keywords.
|
||||
* @param text - The message text to clean
|
||||
* @returns text with system-reminder content removed
|
||||
*/
|
||||
export function removeSystemReminders(text: string): string {
|
||||
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, "").trim()
|
||||
}
|
||||
|
||||
export const SystemDirectiveTypes = {
|
||||
TODO_CONTINUATION: "TODO CONTINUATION",
|
||||
RALPH_LOOP: "RALPH LOOP",
|
||||
|
||||
Reference in New Issue
Block a user