Merge remote-tracking branch 'origin/dev' into fix/background-notification-hook-gate
# Conflicts: # src/features/background-agent/manager.ts
This commit is contained in:
@@ -33,10 +33,10 @@ export interface BackgroundEvent {
|
||||
}
|
||||
|
||||
export interface Todo {
|
||||
content: string
|
||||
status: string
|
||||
priority: string
|
||||
id: string
|
||||
content: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface QueueItem {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { BackgroundTask, ResumeInput } from "./types"
|
||||
import { MIN_IDLE_TIME_MS } from "./constants"
|
||||
import { BackgroundManager } from "./manager"
|
||||
import { ConcurrencyManager } from "./concurrency"
|
||||
import { initTaskToastManager, _resetTaskToastManagerForTesting } from "../task-toast-manager/manager"
|
||||
|
||||
|
||||
const TASK_TTL_MS = 30 * 60 * 1000
|
||||
@@ -190,6 +191,10 @@ function getPendingByParent(manager: BackgroundManager): Map<string, Set<string>
|
||||
return (manager as unknown as { pendingByParent: Map<string, Set<string>> }).pendingByParent
|
||||
}
|
||||
|
||||
function getCompletionTimers(manager: BackgroundManager): Map<string, ReturnType<typeof setTimeout>> {
|
||||
return (manager as unknown as { completionTimers: Map<string, ReturnType<typeof setTimeout>> }).completionTimers
|
||||
}
|
||||
|
||||
function getQueuesByKey(
|
||||
manager: BackgroundManager
|
||||
): Map<string, Array<{ task: BackgroundTask; input: import("./types").LaunchInput }>> {
|
||||
@@ -215,6 +220,23 @@ function stubNotifyParentSession(manager: BackgroundManager): void {
|
||||
;(manager as unknown as { notifyParentSession: () => Promise<void> }).notifyParentSession = async () => {}
|
||||
}
|
||||
|
||||
function createToastRemoveTaskTracker(): { removeTaskCalls: string[]; resetToastManager: () => void } {
|
||||
_resetTaskToastManagerForTesting()
|
||||
const toastManager = initTaskToastManager({
|
||||
tui: { showToast: async () => {} },
|
||||
} as unknown as PluginInput["client"])
|
||||
const removeTaskCalls: string[] = []
|
||||
const originalRemoveTask = toastManager.removeTask.bind(toastManager)
|
||||
toastManager.removeTask = (taskId: string): void => {
|
||||
removeTaskCalls.push(taskId)
|
||||
originalRemoveTask(taskId)
|
||||
}
|
||||
return {
|
||||
removeTaskCalls,
|
||||
resetToastManager: _resetTaskToastManagerForTesting,
|
||||
}
|
||||
}
|
||||
|
||||
function getCleanupSignals(): Array<NodeJS.Signals | "beforeExit" | "exit"> {
|
||||
const signals: Array<NodeJS.Signals | "beforeExit" | "exit"> = ["SIGINT", "SIGTERM", "beforeExit", "exit"]
|
||||
if (process.platform === "win32") {
|
||||
@@ -894,7 +916,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
})
|
||||
|
||||
describe("BackgroundManager.notifyParentSession - aborted parent", () => {
|
||||
test("should skip notification when parent session is aborted", async () => {
|
||||
test("should fall back and still notify when parent session messages are aborted", async () => {
|
||||
//#given
|
||||
let promptCalled = false
|
||||
const promptMock = async () => {
|
||||
@@ -933,7 +955,7 @@ describe("BackgroundManager.notifyParentSession - aborted parent", () => {
|
||||
.notifyParentSession(task)
|
||||
|
||||
//#then
|
||||
expect(promptCalled).toBe(false)
|
||||
expect(promptCalled).toBe(true)
|
||||
|
||||
manager.shutdown()
|
||||
})
|
||||
@@ -1816,6 +1838,32 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
|
||||
const pendingSet = pendingByParent.get(task.parentSessionID)
|
||||
expect(pendingSet?.has(task.id) ?? false).toBe(false)
|
||||
})
|
||||
|
||||
test("should remove task from toast manager when notification is skipped", async () => {
|
||||
//#given
|
||||
const { removeTaskCalls, resetToastManager } = createToastRemoveTaskTracker()
|
||||
const manager = createBackgroundManager()
|
||||
const task = createMockTask({
|
||||
id: "task-cancel-skip-notification",
|
||||
sessionID: "session-cancel-skip-notification",
|
||||
parentSessionID: "parent-cancel-skip-notification",
|
||||
status: "running",
|
||||
})
|
||||
getTaskMap(manager).set(task.id, task)
|
||||
|
||||
//#when
|
||||
const cancelled = await manager.cancelTask(task.id, {
|
||||
source: "test",
|
||||
skipNotification: true,
|
||||
})
|
||||
|
||||
//#then
|
||||
expect(cancelled).toBe(true)
|
||||
expect(removeTaskCalls).toContain(task.id)
|
||||
|
||||
manager.shutdown()
|
||||
resetToastManager()
|
||||
})
|
||||
})
|
||||
|
||||
describe("multiple keys process in parallel", () => {
|
||||
@@ -2776,6 +2824,43 @@ describe("BackgroundManager.handleEvent - session.deleted cascade", () => {
|
||||
|
||||
manager.shutdown()
|
||||
})
|
||||
|
||||
test("should remove tasks from toast manager when session is deleted", () => {
|
||||
//#given
|
||||
const { removeTaskCalls, resetToastManager } = createToastRemoveTaskTracker()
|
||||
const manager = createBackgroundManager()
|
||||
const parentSessionID = "session-parent-toast"
|
||||
const childTask = createMockTask({
|
||||
id: "task-child-toast",
|
||||
sessionID: "session-child-toast",
|
||||
parentSessionID,
|
||||
status: "running",
|
||||
})
|
||||
const grandchildTask = createMockTask({
|
||||
id: "task-grandchild-toast",
|
||||
sessionID: "session-grandchild-toast",
|
||||
parentSessionID: "session-child-toast",
|
||||
status: "pending",
|
||||
startedAt: undefined,
|
||||
queuedAt: new Date(),
|
||||
})
|
||||
const taskMap = getTaskMap(manager)
|
||||
taskMap.set(childTask.id, childTask)
|
||||
taskMap.set(grandchildTask.id, grandchildTask)
|
||||
|
||||
//#when
|
||||
manager.handleEvent({
|
||||
type: "session.deleted",
|
||||
properties: { info: { id: parentSessionID } },
|
||||
})
|
||||
|
||||
//#then
|
||||
expect(removeTaskCalls).toContain(childTask.id)
|
||||
expect(removeTaskCalls).toContain(grandchildTask.id)
|
||||
|
||||
manager.shutdown()
|
||||
resetToastManager()
|
||||
})
|
||||
})
|
||||
|
||||
describe("BackgroundManager.handleEvent - session.error", () => {
|
||||
@@ -2823,6 +2908,35 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
||||
manager.shutdown()
|
||||
})
|
||||
|
||||
test("removes errored task from toast manager", () => {
|
||||
//#given
|
||||
const { removeTaskCalls, resetToastManager } = createToastRemoveTaskTracker()
|
||||
const manager = createBackgroundManager()
|
||||
const sessionID = "ses_error_toast"
|
||||
const task = createMockTask({
|
||||
id: "task-session-error-toast",
|
||||
sessionID,
|
||||
parentSessionID: "parent-session",
|
||||
status: "running",
|
||||
})
|
||||
getTaskMap(manager).set(task.id, task)
|
||||
|
||||
//#when
|
||||
manager.handleEvent({
|
||||
type: "session.error",
|
||||
properties: {
|
||||
sessionID,
|
||||
error: { name: "UnknownError", message: "boom" },
|
||||
},
|
||||
})
|
||||
|
||||
//#then
|
||||
expect(removeTaskCalls).toContain(task.id)
|
||||
|
||||
manager.shutdown()
|
||||
resetToastManager()
|
||||
})
|
||||
|
||||
test("ignores session.error for non-running tasks", () => {
|
||||
//#given
|
||||
const manager = createBackgroundManager()
|
||||
@@ -2968,13 +3082,32 @@ describe("BackgroundManager.pruneStaleTasksAndNotifications - removes pruned tas
|
||||
|
||||
manager.shutdown()
|
||||
})
|
||||
|
||||
test("removes stale task from toast manager", () => {
|
||||
//#given
|
||||
const { removeTaskCalls, resetToastManager } = createToastRemoveTaskTracker()
|
||||
const manager = createBackgroundManager()
|
||||
const staleTask = createMockTask({
|
||||
id: "task-stale-toast",
|
||||
sessionID: "session-stale-toast",
|
||||
parentSessionID: "parent-session",
|
||||
status: "running",
|
||||
startedAt: new Date(Date.now() - 31 * 60 * 1000),
|
||||
})
|
||||
getTaskMap(manager).set(staleTask.id, staleTask)
|
||||
|
||||
//#when
|
||||
pruneStaleTasksAndNotificationsForTest(manager)
|
||||
|
||||
//#then
|
||||
expect(removeTaskCalls).toContain(staleTask.id)
|
||||
|
||||
manager.shutdown()
|
||||
resetToastManager()
|
||||
})
|
||||
})
|
||||
|
||||
describe("BackgroundManager.completionTimers - Memory Leak Fix", () => {
|
||||
function getCompletionTimers(manager: BackgroundManager): Map<string, ReturnType<typeof setTimeout>> {
|
||||
return (manager as unknown as { completionTimers: Map<string, ReturnType<typeof setTimeout>> }).completionTimers
|
||||
}
|
||||
|
||||
function setCompletionTimer(manager: BackgroundManager, taskId: string): void {
|
||||
const completionTimers = getCompletionTimers(manager)
|
||||
const timer = setTimeout(() => {
|
||||
@@ -3500,3 +3633,93 @@ describe("BackgroundManager.handleEvent - non-tool event lastUpdate", () => {
|
||||
expect(task.status).toBe("running")
|
||||
})
|
||||
})
|
||||
|
||||
describe("BackgroundManager regression fixes - resume and aborted notification", () => {
|
||||
test("should keep resumed task in memory after previous completion timer deadline", async () => {
|
||||
//#given
|
||||
const client = {
|
||||
session: {
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
abort: async () => ({}),
|
||||
},
|
||||
}
|
||||
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||
|
||||
const task: BackgroundTask = {
|
||||
id: "task-resume-timer-regression",
|
||||
sessionID: "session-resume-timer-regression",
|
||||
parentSessionID: "parent-session",
|
||||
parentMessageID: "msg-1",
|
||||
description: "resume timer regression",
|
||||
prompt: "test",
|
||||
agent: "explore",
|
||||
status: "completed",
|
||||
startedAt: new Date(),
|
||||
completedAt: new Date(),
|
||||
concurrencyGroup: "explore",
|
||||
}
|
||||
getTaskMap(manager).set(task.id, task)
|
||||
|
||||
const completionTimers = getCompletionTimers(manager)
|
||||
const timer = setTimeout(() => {
|
||||
completionTimers.delete(task.id)
|
||||
getTaskMap(manager).delete(task.id)
|
||||
}, 25)
|
||||
completionTimers.set(task.id, timer)
|
||||
|
||||
//#when
|
||||
await manager.resume({
|
||||
sessionId: "session-resume-timer-regression",
|
||||
prompt: "resume task",
|
||||
parentSessionID: "parent-session-2",
|
||||
parentMessageID: "msg-2",
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, 60))
|
||||
|
||||
//#then
|
||||
expect(getTaskMap(manager).has(task.id)).toBe(true)
|
||||
expect(completionTimers.has(task.id)).toBe(false)
|
||||
|
||||
manager.shutdown()
|
||||
})
|
||||
|
||||
test("should start cleanup timer even when promptAsync aborts", async () => {
|
||||
//#given
|
||||
const client = {
|
||||
session: {
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => {
|
||||
const error = new Error("User aborted")
|
||||
error.name = "MessageAbortedError"
|
||||
throw error
|
||||
},
|
||||
abort: async () => ({}),
|
||||
messages: async () => ({ data: [] }),
|
||||
},
|
||||
}
|
||||
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||
const task: BackgroundTask = {
|
||||
id: "task-aborted-cleanup-regression",
|
||||
sessionID: "session-aborted-cleanup-regression",
|
||||
parentSessionID: "parent-session",
|
||||
parentMessageID: "msg-1",
|
||||
description: "aborted prompt cleanup regression",
|
||||
prompt: "test",
|
||||
agent: "explore",
|
||||
status: "completed",
|
||||
startedAt: new Date(),
|
||||
completedAt: new Date(),
|
||||
}
|
||||
getTaskMap(manager).set(task.id, task)
|
||||
getPendingByParent(manager).set(task.parentSessionID, new Set([task.id]))
|
||||
|
||||
//#when
|
||||
await (manager as unknown as { notifyParentSession: (task: BackgroundTask) => Promise<void> }).notifyParentSession(task)
|
||||
|
||||
//#then
|
||||
expect(getCompletionTimers(manager).has(task.id)).toBe(true)
|
||||
|
||||
manager.shutdown()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
ResumeInput,
|
||||
} from "./types"
|
||||
import { TaskHistory } from "./task-history"
|
||||
import { log, getAgentToolRestrictions, promptWithModelSuggestionRetry } from "../../shared"
|
||||
import { log, getAgentToolRestrictions, normalizeSDKResponse, promptWithModelSuggestionRetry } from "../../shared"
|
||||
import { setSessionTools } from "../../shared/session-tools-store"
|
||||
import { ConcurrencyManager } from "./concurrency"
|
||||
import type { BackgroundTaskConfig, TmuxConfig } from "../../config/schema"
|
||||
@@ -531,6 +531,12 @@ export class BackgroundManager {
|
||||
return existingTask
|
||||
}
|
||||
|
||||
const completionTimer = this.completionTimers.get(existingTask.id)
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
this.completionTimers.delete(existingTask.id)
|
||||
}
|
||||
|
||||
// Re-acquire concurrency using the persisted concurrency group
|
||||
const concurrencyKey = existingTask.concurrencyGroup ?? existingTask.agent
|
||||
await this.concurrencyManager.acquire(concurrencyKey)
|
||||
@@ -648,7 +654,7 @@ export class BackgroundManager {
|
||||
const response = await this.client.session.todo({
|
||||
path: { id: sessionID },
|
||||
})
|
||||
const todos = (response.data ?? response) as Todo[]
|
||||
const todos = normalizeSDKResponse(response, [] as Todo[], { preferResponseOnMissingData: true })
|
||||
if (!todos || todos.length === 0) return false
|
||||
|
||||
const incomplete = todos.filter(
|
||||
@@ -786,6 +792,10 @@ export class BackgroundManager {
|
||||
this.cleanupPendingByParent(task)
|
||||
this.tasks.delete(task.id)
|
||||
this.clearNotificationsForTask(task.id)
|
||||
const toastManager = getTaskToastManager()
|
||||
if (toastManager) {
|
||||
toastManager.removeTask(task.id)
|
||||
}
|
||||
if (task.sessionID) {
|
||||
subagentSessions.delete(task.sessionID)
|
||||
}
|
||||
@@ -833,6 +843,10 @@ export class BackgroundManager {
|
||||
this.cleanupPendingByParent(task)
|
||||
this.tasks.delete(task.id)
|
||||
this.clearNotificationsForTask(task.id)
|
||||
const toastManager = getTaskToastManager()
|
||||
if (toastManager) {
|
||||
toastManager.removeTask(task.id)
|
||||
}
|
||||
if (task.sessionID) {
|
||||
subagentSessions.delete(task.sessionID)
|
||||
}
|
||||
@@ -864,7 +878,7 @@ export class BackgroundManager {
|
||||
path: { id: sessionID },
|
||||
})
|
||||
|
||||
const messages = response.data ?? []
|
||||
const messages = normalizeSDKResponse(response, [] as Array<{ info?: { role?: string } }>, { preferResponseOnMissingData: true })
|
||||
|
||||
// Check for at least one assistant or tool message
|
||||
const hasAssistantOrToolMessage = messages.some(
|
||||
@@ -1003,6 +1017,10 @@ export class BackgroundManager {
|
||||
}
|
||||
|
||||
if (options?.skipNotification) {
|
||||
const toastManager = getTaskToastManager()
|
||||
if (toastManager) {
|
||||
toastManager.removeTask(task.id)
|
||||
}
|
||||
log(`[background-agent] Task cancelled via ${source} (notification skipped):`, task.id)
|
||||
return true
|
||||
}
|
||||
@@ -1232,9 +1250,9 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
||||
|
||||
try {
|
||||
const messagesResp = await this.client.session.messages({ path: { id: task.parentSessionID } })
|
||||
const messages = (messagesResp.data ?? []) as Array<{
|
||||
const messages = normalizeSDKResponse(messagesResp, [] as Array<{
|
||||
info?: { agent?: string; model?: { providerID: string; modelID: string }; modelID?: string; providerID?: string }
|
||||
}>
|
||||
}>)
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const info = messages[i].info
|
||||
if (info?.agent || info?.model || (info?.modelID && info?.providerID)) {
|
||||
@@ -1245,11 +1263,10 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.isAbortedSessionError(error)) {
|
||||
log("[background-agent] Parent session aborted, skipping notification:", {
|
||||
log("[background-agent] Parent session aborted while loading messages; using messageDir fallback:", {
|
||||
taskId: task.id,
|
||||
parentSessionID: task.parentSessionID,
|
||||
})
|
||||
return
|
||||
}
|
||||
const messageDir = getMessageDir(task.parentSessionID)
|
||||
const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
||||
@@ -1283,13 +1300,13 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
||||
})
|
||||
} catch (error) {
|
||||
if (this.isAbortedSessionError(error)) {
|
||||
log("[background-agent] Parent session aborted, skipping notification:", {
|
||||
log("[background-agent] Parent session aborted while sending notification; continuing cleanup:", {
|
||||
taskId: task.id,
|
||||
parentSessionID: task.parentSessionID,
|
||||
})
|
||||
return
|
||||
} else {
|
||||
log("[background-agent] Failed to send notification:", error)
|
||||
}
|
||||
log("[background-agent] Failed to send notification:", error)
|
||||
}
|
||||
} else {
|
||||
log("[background-agent] Parent session notifications disabled, skipping prompt injection:", {
|
||||
@@ -1425,6 +1442,10 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
||||
}
|
||||
}
|
||||
this.clearNotificationsForTask(taskId)
|
||||
const toastManager = getTaskToastManager()
|
||||
if (toastManager) {
|
||||
toastManager.removeTask(taskId)
|
||||
}
|
||||
this.tasks.delete(taskId)
|
||||
if (task.sessionID) {
|
||||
subagentSessions.delete(task.sessionID)
|
||||
@@ -1526,7 +1547,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
||||
this.pruneStaleTasksAndNotifications()
|
||||
|
||||
const statusResult = await this.client.session.status()
|
||||
const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>
|
||||
const allStatuses = normalizeSDKResponse(statusResult, {} as Record<string, { type: string }>)
|
||||
|
||||
await this.checkAndInterruptStaleTasks(allStatuses)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { getMessageDir } from "./message-storage-locator"
|
||||
export { getMessageDir } from "../../shared"
|
||||
|
||||
@@ -1,17 +1 @@
|
||||
import { existsSync, readdirSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { MESSAGE_STORAGE } from "../hook-message-injector"
|
||||
|
||||
export function getMessageDir(sessionID: string): string | null {
|
||||
if (!existsSync(MESSAGE_STORAGE)) return null
|
||||
|
||||
const directPath = join(MESSAGE_STORAGE, sessionID)
|
||||
if (existsSync(directPath)) return directPath
|
||||
|
||||
for (const dir of readdirSync(MESSAGE_STORAGE)) {
|
||||
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
|
||||
if (existsSync(sessionPath)) return sessionPath
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
import { getMessageDir } from "../../shared"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { log } from "../../shared"
|
||||
import { log, normalizeSDKResponse } from "../../shared"
|
||||
|
||||
import { findNearestMessageWithFields } from "../hook-message-injector"
|
||||
import { getTaskToastManager } from "../task-toast-manager"
|
||||
@@ -106,7 +106,7 @@ export async function notifyParentSession(args: {
|
||||
const messagesResp = await client.session.messages({
|
||||
path: { id: task.parentSessionID },
|
||||
})
|
||||
const raw = (messagesResp as { data?: unknown }).data ?? []
|
||||
const raw = normalizeSDKResponse(messagesResp, [] as unknown[])
|
||||
const messages = Array.isArray(raw) ? raw : []
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { OpencodeClient } from "./constants"
|
||||
import type { BackgroundTask } from "./types"
|
||||
import { findNearestMessageWithFields } from "../hook-message-injector"
|
||||
import { getMessageDir } from "./message-storage-locator"
|
||||
import { getMessageDir } from "../../shared"
|
||||
|
||||
type AgentModel = { providerID: string; modelID: string }
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { log } from "../../shared"
|
||||
import { log, normalizeSDKResponse } from "../../shared"
|
||||
|
||||
import {
|
||||
MIN_STABILITY_TIME_MS,
|
||||
@@ -56,7 +56,7 @@ export async function pollRunningTasks(args: {
|
||||
pruneStaleTasksAndNotifications()
|
||||
|
||||
const statusResult = await client.session.status()
|
||||
const allStatuses = ((statusResult as { data?: unknown }).data ?? {}) as SessionStatusMap
|
||||
const allStatuses = normalizeSDKResponse(statusResult, {} as SessionStatusMap)
|
||||
|
||||
await checkAndInterruptStaleTasks(allStatuses)
|
||||
|
||||
@@ -95,10 +95,9 @@ export async function pollRunningTasks(args: {
|
||||
continue
|
||||
}
|
||||
|
||||
const messagesPayload = Array.isArray(messagesResult)
|
||||
? messagesResult
|
||||
: (messagesResult as { data?: unknown }).data
|
||||
const messages = asSessionMessages(messagesPayload)
|
||||
const messages = asSessionMessages(normalizeSDKResponse(messagesResult, [] as SessionMessage[], {
|
||||
preferResponseOnMissingData: true,
|
||||
}))
|
||||
const assistantMsgs = messages.filter((m) => m.info?.role === "assistant")
|
||||
|
||||
let toolCalls = 0
|
||||
@@ -139,7 +138,7 @@ export async function pollRunningTasks(args: {
|
||||
task.stablePolls = (task.stablePolls ?? 0) + 1
|
||||
if (task.stablePolls >= 3) {
|
||||
const recheckStatus = await client.session.status()
|
||||
const recheckData = ((recheckStatus as { data?: unknown }).data ?? {}) as SessionStatusMap
|
||||
const recheckData = normalizeSDKResponse(recheckStatus, {} as SessionStatusMap)
|
||||
const currentStatus = recheckData[sessionID]
|
||||
|
||||
if (currentStatus?.type !== "idle") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type { ResultHandlerContext } from "./result-handler-context"
|
||||
export { formatDuration } from "./duration-formatter"
|
||||
export { getMessageDir } from "./message-storage-locator"
|
||||
export { getMessageDir } from "../../shared"
|
||||
export { checkSessionTodos } from "./session-todo-checker"
|
||||
export { validateSessionHasOutput } from "./session-output-validator"
|
||||
export { tryCompleteTask } from "./background-task-completer"
|
||||
|
||||
@@ -4,7 +4,7 @@ function isTodo(value: unknown): value is Todo {
|
||||
if (typeof value !== "object" || value === null) return false
|
||||
const todo = value as Record<string, unknown>
|
||||
return (
|
||||
typeof todo["id"] === "string" &&
|
||||
(typeof todo["id"] === "string" || todo["id"] === undefined) &&
|
||||
typeof todo["content"] === "string" &&
|
||||
typeof todo["status"] === "string" &&
|
||||
typeof todo["priority"] === "string"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { log } from "../../shared"
|
||||
import { log, normalizeSDKResponse } from "../../shared"
|
||||
|
||||
import type { OpencodeClient } from "./opencode-client"
|
||||
|
||||
@@ -51,7 +51,9 @@ export async function validateSessionHasOutput(
|
||||
path: { id: sessionID },
|
||||
})
|
||||
|
||||
const messages = asSessionMessages((response as { data?: unknown }).data ?? response)
|
||||
const messages = asSessionMessages(normalizeSDKResponse(response, [] as SessionMessage[], {
|
||||
preferResponseOnMissingData: true,
|
||||
}))
|
||||
|
||||
const hasAssistantOrToolMessage = messages.some(
|
||||
(m) => m.info?.role === "assistant" || m.info?.role === "tool"
|
||||
@@ -97,8 +99,9 @@ export async function checkSessionTodos(
|
||||
path: { id: sessionID },
|
||||
})
|
||||
|
||||
const raw = (response as { data?: unknown }).data ?? response
|
||||
const todos = Array.isArray(raw) ? (raw as Todo[]) : []
|
||||
const todos = normalizeSDKResponse(response, [] as Todo[], {
|
||||
preferResponseOnMissingData: true,
|
||||
})
|
||||
if (todos.length === 0) return false
|
||||
|
||||
const incomplete = todos.filter(
|
||||
|
||||
Reference in New Issue
Block a user