fix(keyword-detector): use mainSessionID for session check instead of unreliable API

The keyword-detector was using ctx.client.session.get() to check parentID for
determining subagent sessions, but this API didn't reliably return parentID.

This caused non-ultrawork keywords (search, analyze) to be injected in subagent
sessions when they should only work in main sessions.

Changed to use getMainSessionID() comparison, consistent with other hooks like
session-notification and todo-continuation-enforcer.

- Replace unreliable parentID API check with mainSessionID comparison
- Add comprehensive test coverage for session filtering behavior
- Remove unnecessary session.get API call
This commit is contained in:
YeonGyu-Kim
2026-01-07 02:16:49 +09:00
parent cd97572d0a
commit b5c1cfb57f
2 changed files with 137 additions and 20 deletions

View File

@@ -0,0 +1,125 @@
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
import { createKeywordDetectorHook } from "./index"
import { setMainSession } from "../../features/claude-code-session-state"
import * as sharedModule from "../../shared"
describe("keyword-detector session filtering", () => {
let logCalls: Array<{ msg: string; data?: unknown }>
beforeEach(() => {
setMainSession(undefined)
logCalls = []
spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => {
logCalls.push({ msg, data })
})
})
afterEach(() => {
setMainSession(undefined)
})
function createMockPluginInput(options: { toastCalls?: string[] } = {}) {
const toastCalls = options.toastCalls ?? []
return {
client: {
tui: {
showToast: async (opts: any) => {
toastCalls.push(opts.body.title)
},
},
},
} as any
}
test("should skip non-ultrawork keywords in non-main session (using mainSessionID check)", async () => {
// #given - main session is set, different session submits search keyword
const mainSessionID = "main-123"
const subagentSessionID = "subagent-456"
setMainSession(mainSessionID)
const hook = createKeywordDetectorHook(createMockPluginInput())
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "search mode 찾아줘" }],
}
// #when - non-main session triggers keyword detection
await hook["chat.message"](
{ sessionID: subagentSessionID },
output
)
// #then - search keyword should be filtered out based on mainSessionID comparison
const skipLog = logCalls.find(c => c.msg.includes("Skipping non-ultrawork keywords in non-main session"))
expect(skipLog).toBeDefined()
})
test("should allow ultrawork keywords in non-main session", async () => {
// #given - main session is set, different session submits ultrawork keyword
const mainSessionID = "main-123"
const subagentSessionID = "subagent-456"
setMainSession(mainSessionID)
const toastCalls: string[] = []
const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls }))
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "ultrawork mode" }],
}
// #when - non-main session triggers ultrawork keyword
await hook["chat.message"](
{ sessionID: subagentSessionID },
output
)
// #then - ultrawork should still work (variant set to max)
expect(output.message.variant).toBe("max")
expect(toastCalls).toContain("Ultrawork Mode Activated")
})
test("should allow all keywords in main session", async () => {
// #given - main session submits search keyword
const mainSessionID = "main-123"
setMainSession(mainSessionID)
const hook = createKeywordDetectorHook(createMockPluginInput())
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "search mode 찾아줘" }],
}
// #when - main session triggers keyword detection
await hook["chat.message"](
{ sessionID: mainSessionID },
output
)
// #then - search keyword should be detected (output unchanged but detection happens)
// Note: search keywords don't set variant, they inject messages via context-injector
// This test verifies the detection logic runs without filtering
expect(output.message.variant).toBeUndefined() // search doesn't set variant
})
test("should allow all keywords when mainSessionID is not set", async () => {
// #given - no main session set (early startup or standalone mode)
setMainSession(undefined)
const toastCalls: string[] = []
const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls }))
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "ultrawork search" }],
}
// #when - any session triggers keyword detection
await hook["chat.message"](
{ sessionID: "any-session" },
output
)
// #then - all keywords should work
expect(output.message.variant).toBe("max")
expect(toastCalls).toContain("Ultrawork Mode Activated")
})
})

View File

@@ -1,6 +1,7 @@
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector" import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector"
import { log } from "../../shared" import { log } from "../../shared"
import { getMainSessionID } from "../../features/claude-code-session-state"
export * from "./detector" export * from "./detector"
export * from "./constants" export * from "./constants"
@@ -27,30 +28,21 @@ export function createKeywordDetectorHook(ctx: PluginInput) {
return return
} }
// Check if this is a subagent session (has parent) // Only ultrawork keywords work in non-main sessions
// Only ultrawork keywords work in subagent sessions
// Other keywords (search, analyze, etc.) only work in main sessions // Other keywords (search, analyze, etc.) only work in main sessions
try { const mainSessionID = getMainSessionID()
const sessionInfo = await ctx.client.session.get({ path: { id: input.sessionID } }) const isNonMainSession = mainSessionID && input.sessionID !== mainSessionID
const isSubagentSession = !!sessionInfo.data?.parentID
if (isSubagentSession) { if (isNonMainSession) {
// Filter to only ultrawork keywords in subagent sessions
detectedKeywords = detectedKeywords.filter((k) => k.type === "ultrawork") detectedKeywords = detectedKeywords.filter((k) => k.type === "ultrawork")
if (detectedKeywords.length === 0) { if (detectedKeywords.length === 0) {
log(`[keyword-detector] Skipping non-ultrawork keywords in subagent session`, { log(`[keyword-detector] Skipping non-ultrawork keywords in non-main session`, {
sessionID: input.sessionID, sessionID: input.sessionID,
parentID: sessionInfo.data?.parentID, mainSessionID,
}) })
return return
} }
} }
} catch (err) {
log(`[keyword-detector] Failed to get session info, proceeding with all keywords`, {
error: err,
sessionID: input.sessionID,
})
}
const hasUltrawork = detectedKeywords.some((k) => k.type === "ultrawork") const hasUltrawork = detectedKeywords.some((k) => k.type === "ultrawork")
if (hasUltrawork) { if (hasUltrawork) {