Merge pull request #1963 from MoerAI/fix/multi-issue-1888-1693-1891
fix: resolve issues #1888, #1693, #1891
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
MAX_CONSECUTIVE_FAILURES,
|
||||
} from "./constants"
|
||||
import { isLastAssistantMessageAborted } from "./abort-detection"
|
||||
import { hasUnansweredQuestion } from "./pending-question-detection"
|
||||
import { getIncompleteCount } from "./todo"
|
||||
import type { MessageInfo, ResolvedMessageInfo, Todo } from "./types"
|
||||
import type { SessionStateStore } from "./session-state"
|
||||
@@ -74,6 +75,10 @@ export async function handleSessionIdle(args: {
|
||||
log(`[${HOOK_NAME}] Skipped: last assistant message was aborted (API fallback)`, { sessionID })
|
||||
return
|
||||
}
|
||||
if (hasUnansweredQuestion(messages)) {
|
||||
log(`[${HOOK_NAME}] Skipped: pending question awaiting user response`, { sessionID })
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
log(`[${HOOK_NAME}] Messages fetch failed, continuing`, { sessionID, error: String(error) })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/// <reference types="bun-types" />
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
import { hasUnansweredQuestion } from "./pending-question-detection"
|
||||
|
||||
describe("hasUnansweredQuestion", () => {
|
||||
test("given empty messages, returns false", () => {
|
||||
expect(hasUnansweredQuestion([])).toBe(false)
|
||||
})
|
||||
|
||||
test("given null-ish input, returns false", () => {
|
||||
expect(hasUnansweredQuestion(undefined as never)).toBe(false)
|
||||
})
|
||||
|
||||
test("given last assistant message with question tool_use, returns true", () => {
|
||||
const messages = [
|
||||
{ info: { role: "user" } },
|
||||
{
|
||||
info: { role: "assistant" },
|
||||
parts: [
|
||||
{ type: "tool_use", name: "question" },
|
||||
],
|
||||
},
|
||||
]
|
||||
expect(hasUnansweredQuestion(messages)).toBe(true)
|
||||
})
|
||||
|
||||
test("given last assistant message with question tool-invocation, returns true", () => {
|
||||
const messages = [
|
||||
{ info: { role: "user" } },
|
||||
{
|
||||
info: { role: "assistant" },
|
||||
parts: [
|
||||
{ type: "tool-invocation", toolName: "question" },
|
||||
],
|
||||
},
|
||||
]
|
||||
expect(hasUnansweredQuestion(messages)).toBe(true)
|
||||
})
|
||||
|
||||
test("given user message after question (answered), returns false", () => {
|
||||
const messages = [
|
||||
{
|
||||
info: { role: "assistant" },
|
||||
parts: [
|
||||
{ type: "tool_use", name: "question" },
|
||||
],
|
||||
},
|
||||
{ info: { role: "user" } },
|
||||
]
|
||||
expect(hasUnansweredQuestion(messages)).toBe(false)
|
||||
})
|
||||
|
||||
test("given assistant message with non-question tool, returns false", () => {
|
||||
const messages = [
|
||||
{ info: { role: "user" } },
|
||||
{
|
||||
info: { role: "assistant" },
|
||||
parts: [
|
||||
{ type: "tool_use", name: "bash" },
|
||||
],
|
||||
},
|
||||
]
|
||||
expect(hasUnansweredQuestion(messages)).toBe(false)
|
||||
})
|
||||
|
||||
test("given assistant message with no parts, returns false", () => {
|
||||
const messages = [
|
||||
{ info: { role: "user" } },
|
||||
{ info: { role: "assistant" } },
|
||||
]
|
||||
expect(hasUnansweredQuestion(messages)).toBe(false)
|
||||
})
|
||||
|
||||
test("given role on message directly (not in info), returns true for question", () => {
|
||||
const messages = [
|
||||
{ role: "user" },
|
||||
{
|
||||
role: "assistant",
|
||||
parts: [
|
||||
{ type: "tool_use", name: "question" },
|
||||
],
|
||||
},
|
||||
]
|
||||
expect(hasUnansweredQuestion(messages)).toBe(true)
|
||||
})
|
||||
|
||||
test("given mixed tools including question, returns true", () => {
|
||||
const messages = [
|
||||
{
|
||||
info: { role: "assistant" },
|
||||
parts: [
|
||||
{ type: "tool_use", name: "bash" },
|
||||
{ type: "tool_use", name: "question" },
|
||||
],
|
||||
},
|
||||
]
|
||||
expect(hasUnansweredQuestion(messages)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { log } from "../../shared/logger"
|
||||
import { HOOK_NAME } from "./constants"
|
||||
|
||||
interface MessagePart {
|
||||
type: string
|
||||
name?: string
|
||||
toolName?: string
|
||||
}
|
||||
|
||||
interface Message {
|
||||
info?: { role?: string }
|
||||
role?: string
|
||||
parts?: MessagePart[]
|
||||
}
|
||||
|
||||
export function hasUnansweredQuestion(messages: Message[]): boolean {
|
||||
if (!messages || messages.length === 0) return false
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i]
|
||||
const role = msg.info?.role ?? msg.role
|
||||
|
||||
if (role === "user") return false
|
||||
|
||||
if (role === "assistant" && msg.parts) {
|
||||
const hasQuestion = msg.parts.some(
|
||||
(part) =>
|
||||
(part.type === "tool_use" || part.type === "tool-invocation") &&
|
||||
(part.name === "question" || part.toolName === "question"),
|
||||
)
|
||||
if (hasQuestion) {
|
||||
log(`[${HOOK_NAME}] Detected pending question tool in last assistant message`)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user