fix(kimi): keep a resumed Kimi pane resumable across the next Orca restart (#31311) (#33489)

Kimi sessions keep resuming across repeated Orca restarts: register Kimi's SessionStart hook (matcher ^resume$, so new sessions pay nothing) and map a resume to an idle session-start status that refreshes the resume record; recognise npm-installed Kimi (node_modules/@moonshot-ai/kimi-code/dist/main.mjs) on Windows so the pane isn't mistaken for a shell.

Fixes #31311
This commit is contained in:
Neil
2026-10-25 02:53:58 -07:00
committed by GitHub
parent 3ff1f00d08
commit 0571e45107
6 changed files with 59 additions and 3 deletions
+3 -2
View File
@@ -21,13 +21,14 @@ function deleteEndMarker(text: string): string {
}
describe('kimi managed hooks TOML block', () => {
it('installs every managed event without a matcher', () => {
it('installs every managed event, scoping only SessionStart to resumes', () => {
const block = buildManagedKimiHooksBlock(COMMAND)
for (const event of KIMI_HOOK_EVENTS) {
expect(block).toContain(`event = "${event}"`)
}
// Kimi treats matcher as a regex; omitting it matches all tools.
expect(block).not.toContain('matcher')
expect(block.match(/matcher = .*/g)).toEqual(['matcher = "^resume$"'])
expect(block).toContain('event = "SessionStart"\nmatcher = "^resume$"')
expect(
readManagedKimiHookEvents(applyManagedKimiHooks('', COMMAND, isManaged), isManaged)
).toEqual(new Set(KIMI_HOOK_EVENTS))
+4 -1
View File
@@ -11,6 +11,7 @@ import {
} from '../agent-hooks/managed-toml-ownership'
export const KIMI_HOOK_EVENTS = [
'SessionStart',
'UserPromptSubmit',
'PreToolUse',
'PostToolUse',
@@ -123,11 +124,13 @@ function tomlBasicString(value: string): string {
export function buildManagedKimiHooksBlock(command: string, eol = '\n'): string {
const commandLiteral = tomlBasicString(command)
// Omit matcher: Kimi treats it as regex, so * is invalid. Host timeout backs up the normal curl deadline.
// Kimi treats matcher as regex (so * is invalid); omit it except to scope SessionStart. Host timeout backs up the normal curl deadline.
const entries = KIMI_HOOK_EVENTS.map((event) =>
[
HOOK_TABLE_HEADER,
`event = "${event}"`,
// Why: Kimi awaits SessionStart during a new session's first prompt; only a resume is used.
...(event === 'SessionStart' ? ['matcher = "^resume$"'] : []),
`command = ${commandLiteral}`,
`timeout = ${MANAGED_HOOK_TIMEOUT_SECONDS}`
].join(eol)
@@ -116,6 +116,29 @@ describe('shared agent-hook-listener', () => {
expect(stopped?.providerSession).toMatchObject({ key: 'session_id', id: 'session_abc' })
})
// #31311: a resumed Kimi pane must report its session before any prompt, or quit captures nothing.
it('lands a resumed Kimi SessionStart as an idle boundary carrying its session', () => {
const sessionStart = (source: string) =>
normalizeHookPayload(
state,
'kimi',
{
paneKey: PANE_KEY,
payload: { hook_event_name: 'SessionStart', session_id: 'session_abc', source }
},
'production'
)
const resumed = sessionStart('resume')
expect(resumed?.payload).toMatchObject({
agentType: 'kimi',
state: 'done',
sessionBoundary: true
})
expect(resumed?.providerSession).toMatchObject({ key: 'session_id', id: 'session_abc' })
expect(sessionStart('startup')).toBeNull()
})
// Why: Kimi shares Claude-compatible compact/harness hooks; cover the same sticky-working
// guards so a Kimi-only regression cannot slip past the Claude-only tests (issue #11352).
it('ignores harness-injected UserPromptSubmit for Kimi', () => {
@@ -27,6 +27,18 @@ export function normalizeKimiEvent(
if (shouldIgnoreCompactContinuationUserPromptSubmit(eventName, promptText)) {
return null
}
if (eventName === 'SessionStart') {
// Why: a resumed session's only pre-prompt signal, so its id stays resumable at the next quit
// (#31311); `startup` fires mid-submit of the first message, so it is not an idle boundary.
return hookPayload['source'] === 'resume'
? normalizeAgentStatusPayload({
state: 'done',
prompt: '',
agentType: 'kimi',
sessionBoundary: true
})
: null
}
const toolName = readString(hookPayload, 'tool_name')
const isUserInputTool = isKimiUserInputTool(toolName)
@@ -49,5 +49,12 @@ export const EXACT_NODE_ENTRYPOINT_IDENTITIES: readonly {
/(?:^|\/)(?:node_modules\/@qwen-code\/qwen-code\/(?:cli-entry|cli|scripts\/cli-entry|dist\/cli)|qwen-code\/lib\/cli(?:-entry)?)\.js$/,
agent: 'qwen-code',
processName: 'qwen'
},
// Why: Kimi Code's npm bin is a generic `dist/main.mjs`, and on Windows its process stays
// node.exe instead of retitling to `kimi-code`, so only the package path identifies it.
{
pattern: /(?:^|\/)node_modules\/@moonshot-ai\/kimi-code\/dist\/main\.mjs$/,
agent: 'kimi',
processName: 'kimi'
}
]
@@ -306,6 +306,16 @@ describe('agent process recognition', () => {
expect(isRecognizedAgentType('kimi-code')).toBe(true)
})
// #31311: the npm install on Windows runs as node.exe, observed with this exact .bin shim path.
it('recognizes Kimi Code launched by its npm shim on Windows', () => {
expect(
recognizeAgentProcessFromCommandLine(
'"C:\\Program Files\\nodejs\\node.exe" C:\\Users\\dev\\proj\\node_modules\\.bin/../@moonshot-ai/kimi-code/dist/main.mjs --session session_1'
)
).toEqual({ agent: 'kimi', processName: 'kimi' })
expect(recognizeAgentProcessFromCommandLine('node C:\\tools\\app\\dist\\main.mjs')).toBeNull()
})
it('recognizes Qwen Code by its installed qwen executable', () => {
expect(recognizeAgentProcess('/home/dev/.local/bin/qwen')).toEqual({
agent: 'qwen-code',