fix(keyword-detector): skip synthetic turns
This commit is contained in:
@@ -1,20 +1,20 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import type { KeywordDetectorConfig } from "../../config/schema/keyword-detector"
|
import type { KeywordDetectorConfig } from "../../config/schema/keyword-detector"
|
||||||
import type { DetectedKeyword } from "./detector"
|
|
||||||
import { detectKeywordsWithType, extractPromptText, looksLikeSlashCommand } from "./detector"
|
|
||||||
import { isPlannerAgent, isNonOmoAgent } from "./constants"
|
|
||||||
import { log } from "../../shared"
|
|
||||||
import {
|
|
||||||
isSystemDirective,
|
|
||||||
removeSystemReminders,
|
|
||||||
} from "../../shared/system-directive"
|
|
||||||
import {
|
import {
|
||||||
getMainSessionID,
|
getMainSessionID,
|
||||||
getSessionAgent,
|
getSessionAgent,
|
||||||
subagentSessions,
|
subagentSessions,
|
||||||
} from "../../features/claude-code-session-state"
|
} from "../../features/claude-code-session-state"
|
||||||
import type { ContextCollector } from "../../features/context-injector"
|
import type { ContextCollector } from "../../features/context-injector"
|
||||||
|
import { log } from "../../shared"
|
||||||
|
import {
|
||||||
|
isSystemDirective,
|
||||||
|
removeSystemReminders,
|
||||||
|
} from "../../shared/system-directive"
|
||||||
import type { RalphLoopHook } from "../ralph-loop"
|
import type { RalphLoopHook } from "../ralph-loop"
|
||||||
|
import { isNonOmoAgent, isPlannerAgent } from "./constants"
|
||||||
|
import type { DetectedKeyword } from "./detector"
|
||||||
|
import { detectKeywordsWithType, extractPromptText, looksLikeSlashCommand } from "./detector"
|
||||||
|
|
||||||
function suppressComboStandalones(detected: DetectedKeyword[]): DetectedKeyword[] {
|
function suppressComboStandalones(detected: DetectedKeyword[]): DetectedKeyword[] {
|
||||||
const hasCombo = detected.some((k) => k.type === "hyperplan-ultrawork")
|
const hasCombo = detected.some((k) => k.type === "hyperplan-ultrawork")
|
||||||
@@ -22,6 +22,11 @@ function suppressComboStandalones(detected: DetectedKeyword[]): DetectedKeyword[
|
|||||||
return detected.filter((k) => k.type !== "ultrawork" && k.type !== "hyperplan")
|
return detected.filter((k) => k.type !== "ultrawork" && k.type !== "hyperplan")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSyntheticTextMessage(parts: Array<{ type: string; text?: string; [key: string]: unknown }>): boolean {
|
||||||
|
const textParts = parts.filter((part) => part.type === "text" && part.text !== undefined)
|
||||||
|
return textParts.length > 0 && textParts.every((part) => part.synthetic === true)
|
||||||
|
}
|
||||||
|
|
||||||
export function createKeywordDetectorHook(
|
export function createKeywordDetectorHook(
|
||||||
ctx: PluginInput,
|
ctx: PluginInput,
|
||||||
_collector?: ContextCollector,
|
_collector?: ContextCollector,
|
||||||
@@ -30,8 +35,8 @@ export function createKeywordDetectorHook(
|
|||||||
) {
|
) {
|
||||||
const disabledKeywords = config?.disabled_keywords
|
const disabledKeywords = config?.disabled_keywords
|
||||||
function getRuntimeVariant(input: { variant?: string }, message: Record<string, unknown>): string | undefined {
|
function getRuntimeVariant(input: { variant?: string }, message: Record<string, unknown>): string | undefined {
|
||||||
if (typeof message["variant"] === "string") {
|
if (typeof message.variant === "string") {
|
||||||
return message["variant"]
|
return message.variant
|
||||||
}
|
}
|
||||||
|
|
||||||
return typeof input.variant === "string" ? input.variant : undefined
|
return typeof input.variant === "string" ? input.variant : undefined
|
||||||
@@ -51,6 +56,11 @@ export function createKeywordDetectorHook(
|
|||||||
parts: Array<{ type: string; text?: string; [key: string]: unknown }>
|
parts: Array<{ type: string; text?: string; [key: string]: unknown }>
|
||||||
}
|
}
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
|
if (isSyntheticTextMessage(output.parts)) {
|
||||||
|
log(`[keyword-detector] Skipping synthetic text message`, { sessionID: input.sessionID })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const promptText = extractPromptText(output.parts)
|
const promptText = extractPromptText(output.parts)
|
||||||
|
|
||||||
if (isSystemDirective(promptText)) {
|
if (isSystemDirective(promptText)) {
|
||||||
|
|||||||
@@ -134,6 +134,31 @@ describe("keyword-detector message transform", () => {
|
|||||||
expect(textPart).toBeDefined()
|
expect(textPart).toBeDefined()
|
||||||
expect(textPart!.text).toBe("just a normal message")
|
expect(textPart!.text).toBe("just a normal message")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should not prepend mode instructions to synthetic team peer messages", async () => {
|
||||||
|
// given - team mailbox injection created a synthetic peer message containing search keywords
|
||||||
|
const collector = new ContextCollector()
|
||||||
|
const sessionID = "synthetic-peer-message-session"
|
||||||
|
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
|
||||||
|
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
|
||||||
|
const output = {
|
||||||
|
message: {} as Record<string, unknown>,
|
||||||
|
parts: [{
|
||||||
|
type: "text",
|
||||||
|
synthetic: true,
|
||||||
|
text: '<peer_message from="researcher">search the issue thread and report findings</peer_message>',
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when - keyword detection sees the synthetic peer message
|
||||||
|
await hook["chat.message"]({ sessionID }, output)
|
||||||
|
|
||||||
|
// then - peer message content is preserved without search-mode becoming part of the user turn
|
||||||
|
const textPart = output.parts.find((part) => part.type === "text")
|
||||||
|
expect(textPart).toBeDefined()
|
||||||
|
expect(textPart?.text).toBe('<peer_message from="researcher">search the issue thread and report findings</peer_message>')
|
||||||
|
expect(textPart?.text).not.toContain("[search-mode]")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("keyword-detector session filtering", () => {
|
describe("keyword-detector session filtering", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user