Skip to content

Commit b5c1cfb

Browse files
committed
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
1 parent cd97572 commit b5c1cfb

File tree

2 files changed

+137
-20
lines changed

2 files changed

+137
-20
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
2+
import { createKeywordDetectorHook } from "./index"
3+
import { setMainSession } from "../../features/claude-code-session-state"
4+
import * as sharedModule from "../../shared"
5+
6+
describe("keyword-detector session filtering", () => {
7+
let logCalls: Array<{ msg: string; data?: unknown }>
8+
9+
beforeEach(() => {
10+
setMainSession(undefined)
11+
logCalls = []
12+
spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => {
13+
logCalls.push({ msg, data })
14+
})
15+
})
16+
17+
afterEach(() => {
18+
setMainSession(undefined)
19+
})
20+
21+
function createMockPluginInput(options: { toastCalls?: string[] } = {}) {
22+
const toastCalls = options.toastCalls ?? []
23+
return {
24+
client: {
25+
tui: {
26+
showToast: async (opts: any) => {
27+
toastCalls.push(opts.body.title)
28+
},
29+
},
30+
},
31+
} as any
32+
}
33+
34+
test("should skip non-ultrawork keywords in non-main session (using mainSessionID check)", async () => {
35+
// #given - main session is set, different session submits search keyword
36+
const mainSessionID = "main-123"
37+
const subagentSessionID = "subagent-456"
38+
setMainSession(mainSessionID)
39+
40+
const hook = createKeywordDetectorHook(createMockPluginInput())
41+
const output = {
42+
message: {} as Record<string, unknown>,
43+
parts: [{ type: "text", text: "search mode 찾아줘" }],
44+
}
45+
46+
// #when - non-main session triggers keyword detection
47+
await hook["chat.message"](
48+
{ sessionID: subagentSessionID },
49+
output
50+
)
51+
52+
// #then - search keyword should be filtered out based on mainSessionID comparison
53+
const skipLog = logCalls.find(c => c.msg.includes("Skipping non-ultrawork keywords in non-main session"))
54+
expect(skipLog).toBeDefined()
55+
})
56+
57+
test("should allow ultrawork keywords in non-main session", async () => {
58+
// #given - main session is set, different session submits ultrawork keyword
59+
const mainSessionID = "main-123"
60+
const subagentSessionID = "subagent-456"
61+
setMainSession(mainSessionID)
62+
63+
const toastCalls: string[] = []
64+
const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls }))
65+
const output = {
66+
message: {} as Record<string, unknown>,
67+
parts: [{ type: "text", text: "ultrawork mode" }],
68+
}
69+
70+
// #when - non-main session triggers ultrawork keyword
71+
await hook["chat.message"](
72+
{ sessionID: subagentSessionID },
73+
output
74+
)
75+
76+
// #then - ultrawork should still work (variant set to max)
77+
expect(output.message.variant).toBe("max")
78+
expect(toastCalls).toContain("Ultrawork Mode Activated")
79+
})
80+
81+
test("should allow all keywords in main session", async () => {
82+
// #given - main session submits search keyword
83+
const mainSessionID = "main-123"
84+
setMainSession(mainSessionID)
85+
86+
const hook = createKeywordDetectorHook(createMockPluginInput())
87+
const output = {
88+
message: {} as Record<string, unknown>,
89+
parts: [{ type: "text", text: "search mode 찾아줘" }],
90+
}
91+
92+
// #when - main session triggers keyword detection
93+
await hook["chat.message"](
94+
{ sessionID: mainSessionID },
95+
output
96+
)
97+
98+
// #then - search keyword should be detected (output unchanged but detection happens)
99+
// Note: search keywords don't set variant, they inject messages via context-injector
100+
// This test verifies the detection logic runs without filtering
101+
expect(output.message.variant).toBeUndefined() // search doesn't set variant
102+
})
103+
104+
test("should allow all keywords when mainSessionID is not set", async () => {
105+
// #given - no main session set (early startup or standalone mode)
106+
setMainSession(undefined)
107+
108+
const toastCalls: string[] = []
109+
const hook = createKeywordDetectorHook(createMockPluginInput({ toastCalls }))
110+
const output = {
111+
message: {} as Record<string, unknown>,
112+
parts: [{ type: "text", text: "ultrawork search" }],
113+
}
114+
115+
// #when - any session triggers keyword detection
116+
await hook["chat.message"](
117+
{ sessionID: "any-session" },
118+
output
119+
)
120+
121+
// #then - all keywords should work
122+
expect(output.message.variant).toBe("max")
123+
expect(toastCalls).toContain("Ultrawork Mode Activated")
124+
})
125+
})

src/hooks/keyword-detector/index.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { PluginInput } from "@opencode-ai/plugin"
22
import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector"
33
import { log } from "../../shared"
4+
import { getMainSessionID } from "../../features/claude-code-session-state"
45

56
export * from "./detector"
67
export * from "./constants"
@@ -27,29 +28,20 @@ export function createKeywordDetectorHook(ctx: PluginInput) {
2728
return
2829
}
2930

30-
// Check if this is a subagent session (has parent)
31-
// Only ultrawork keywords work in subagent sessions
31+
// Only ultrawork keywords work in non-main sessions
3232
// Other keywords (search, analyze, etc.) only work in main sessions
33-
try {
34-
const sessionInfo = await ctx.client.session.get({ path: { id: input.sessionID } })
35-
const isSubagentSession = !!sessionInfo.data?.parentID
33+
const mainSessionID = getMainSessionID()
34+
const isNonMainSession = mainSessionID && input.sessionID !== mainSessionID
3635

37-
if (isSubagentSession) {
38-
// Filter to only ultrawork keywords in subagent sessions
39-
detectedKeywords = detectedKeywords.filter((k) => k.type === "ultrawork")
40-
if (detectedKeywords.length === 0) {
41-
log(`[keyword-detector] Skipping non-ultrawork keywords in subagent session`, {
42-
sessionID: input.sessionID,
43-
parentID: sessionInfo.data?.parentID,
44-
})
45-
return
46-
}
36+
if (isNonMainSession) {
37+
detectedKeywords = detectedKeywords.filter((k) => k.type === "ultrawork")
38+
if (detectedKeywords.length === 0) {
39+
log(`[keyword-detector] Skipping non-ultrawork keywords in non-main session`, {
40+
sessionID: input.sessionID,
41+
mainSessionID,
42+
})
43+
return
4744
}
48-
} catch (err) {
49-
log(`[keyword-detector] Failed to get session info, proceeding with all keywords`, {
50-
error: err,
51-
sessionID: input.sessionID,
52-
})
5345
}
5446

5547
const hasUltrawork = detectedKeywords.some((k) => k.type === "ultrawork")

0 commit comments

Comments
 (0)