fix(session): ignore internal synthetic turns
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import type { KeywordType } from "../../config/schema/keyword-detector"
|
||||
import { isRealUserTextPart } from "../../shared/internal-initiator-marker"
|
||||
import {
|
||||
KEYWORD_DETECTORS,
|
||||
CODE_BLOCK_PATTERN,
|
||||
INLINE_CODE_PATTERN,
|
||||
KEYWORD_DETECTORS,
|
||||
} from "./constants"
|
||||
|
||||
export interface DetectedKeyword {
|
||||
@@ -61,10 +62,10 @@ export function detectKeywordsWithType(
|
||||
}
|
||||
|
||||
export function extractPromptText(
|
||||
parts: Array<{ type: string; text?: string }>
|
||||
parts: Array<{ type: string; text?: string; synthetic?: boolean }>
|
||||
): string {
|
||||
return parts
|
||||
.filter((p) => p.type === "text")
|
||||
.filter(isRealUserTextPart)
|
||||
.map((p) => p.text || "")
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
subagentSessions,
|
||||
} from "../../features/claude-code-session-state"
|
||||
import type { ContextCollector } from "../../features/context-injector"
|
||||
import { log } from "../../shared"
|
||||
import {
|
||||
isRealUserTextPart,
|
||||
isSyntheticOrInternalOnlyTextParts,
|
||||
log,
|
||||
} from "../../shared"
|
||||
import {
|
||||
isSystemDirective,
|
||||
removeSystemReminders,
|
||||
@@ -22,11 +26,6 @@ function suppressComboStandalones(detected: DetectedKeyword[]): DetectedKeyword[
|
||||
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(
|
||||
ctx: PluginInput,
|
||||
_collector?: ContextCollector,
|
||||
@@ -56,8 +55,8 @@ export function createKeywordDetectorHook(
|
||||
parts: Array<{ type: string; text?: string; [key: string]: unknown }>
|
||||
}
|
||||
): Promise<void> => {
|
||||
if (isSyntheticTextMessage(output.parts)) {
|
||||
log(`[keyword-detector] Skipping synthetic text message`, { sessionID: input.sessionID })
|
||||
if (isSyntheticOrInternalOnlyTextParts(output.parts)) {
|
||||
log(`[keyword-detector] Skipping synthetic/internal text message`, { sessionID: input.sessionID })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -191,7 +190,7 @@ export function createKeywordDetectorHook(
|
||||
.catch((err) => log(`[keyword-detector] Failed to show toast`, { error: err, sessionID: input.sessionID }))
|
||||
}
|
||||
|
||||
const textPartIndex = output.parts.findIndex((p) => p.type === "text" && p.text !== undefined)
|
||||
const textPartIndex = output.parts.findIndex(isRealUserTextPart)
|
||||
if (textPartIndex === -1) {
|
||||
log(`[keyword-detector] No text part found, skipping injection`, { sessionID: input.sessionID })
|
||||
return
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
|
||||
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { createKeywordDetectorHook } from "./index"
|
||||
import { setMainSession, updateSessionAgent, clearSessionAgent, _resetForTesting } from "../../features/claude-code-session-state"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
import * as sessionState from "../../features/claude-code-session-state"
|
||||
import { _resetForTesting, clearSessionAgent, setMainSession, updateSessionAgent } from "../../features/claude-code-session-state"
|
||||
import { ContextCollector } from "../../features/context-injector"
|
||||
import * as sharedModule from "../../shared"
|
||||
import * as sessionState from "../../features/claude-code-session-state"
|
||||
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
import { OMO_INTERNAL_INITIATOR_MARKER } from "../../shared/internal-initiator-marker"
|
||||
import { createKeywordDetectorHook } from "./index"
|
||||
|
||||
type ToastOptions = { body: { title: string } }
|
||||
|
||||
@@ -159,6 +160,27 @@ describe("keyword-detector message transform", () => {
|
||||
expect(textPart?.text).toBe('<peer_message from="researcher">search the issue thread and report findings</peer_message>')
|
||||
expect(textPart?.text).not.toContain("[search-mode]")
|
||||
})
|
||||
|
||||
test("should not prepend mode instructions to internally marked peer messages", async () => {
|
||||
// given - an internal peer message contains a search keyword but is not user intent
|
||||
const collector = new ContextCollector()
|
||||
const sessionID = "internal-peer-message-session"
|
||||
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
|
||||
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
|
||||
const peerText = `<peer_message from="researcher">search the issue thread</peer_message>\n${OMO_INTERNAL_INITIATOR_MARKER}`
|
||||
const output = {
|
||||
message: {} as Record<string, unknown>,
|
||||
parts: [{ type: "text", text: peerText }],
|
||||
}
|
||||
|
||||
// when
|
||||
await hook["chat.message"]({ sessionID }, output)
|
||||
|
||||
// then
|
||||
const textPart = output.parts.find((part) => part.type === "text")
|
||||
expect(textPart?.text).toBe(peerText)
|
||||
expect(textPart?.text).not.toContain("[search-mode]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("keyword-detector session filtering", () => {
|
||||
|
||||
Reference in New Issue
Block a user