mirror of
https://mirror.skon.top/github.com/code-yeongyu/oh-my-opencode
synced 2026-04-30 18:50:29 +08:00
feat(background-agent): integrate team-mode into background manager
This commit is contained in:
@@ -6,6 +6,7 @@ afterAll(() => { mock.restore() })
|
||||
import { getSessionPromptParams, clearSessionPromptParams } from "../../shared/session-prompt-params-state"
|
||||
import { tmpdir } from "node:os"
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import * as sharedModule from "../../shared"
|
||||
import { _resetForTesting as resetClaudeCodeSessionState, subagentSessions } from "../claude-code-session-state"
|
||||
import type { BackgroundTask, ResumeInput } from "./types"
|
||||
import { MIN_IDLE_TIME_MS } from "./constants"
|
||||
@@ -4229,6 +4230,30 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
||||
{ providers: ["anthropic"], model: "gpt-5.3-codex", variant: "high" },
|
||||
]
|
||||
|
||||
let logCalls: Array<{ message: string; data?: unknown }> = []
|
||||
let logSpy: ReturnType<typeof spyOn> | undefined
|
||||
let verifySessionExistsSpy: ReturnType<typeof spyOn> | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
logCalls = []
|
||||
logSpy = spyOn(sharedModule, "log").mockImplementation((message: string, data?: unknown) => {
|
||||
logCalls.push({ message, data })
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
logSpy?.mockRestore()
|
||||
verifySessionExistsSpy?.mockRestore()
|
||||
})
|
||||
|
||||
const mockVerifySessionExists = (manager: BackgroundManager, sessionExists: boolean): void => {
|
||||
verifySessionExistsSpy?.mockRestore()
|
||||
verifySessionExistsSpy = spyOn(
|
||||
manager as unknown as { verifySessionExists: (sessionID: string) => Promise<boolean> },
|
||||
"verifySessionExists",
|
||||
).mockResolvedValue(sessionExists)
|
||||
}
|
||||
|
||||
const stubProcessKey = (manager: BackgroundManager) => {
|
||||
;(manager as unknown as { processKey: (key: string) => Promise<void> }).processKey = async () => {}
|
||||
}
|
||||
@@ -4260,6 +4285,7 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
||||
test("sets task to error, releases concurrency, and keeps it until delayed cleanup", async () => {
|
||||
//#given
|
||||
const manager = createBackgroundManager()
|
||||
mockVerifySessionExists(manager, false)
|
||||
const concurrencyManager = getConcurrencyManager(manager)
|
||||
const concurrencyKey = "test-provider/test-model"
|
||||
await concurrencyManager.acquire(concurrencyKey)
|
||||
@@ -4308,6 +4334,7 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
||||
//#given
|
||||
const { removeTaskCalls, resetToastManager } = createToastRemoveTaskTracker()
|
||||
const manager = createBackgroundManager()
|
||||
mockVerifySessionExists(manager, false)
|
||||
const sessionID = "ses_error_toast"
|
||||
const task = createMockTask({
|
||||
id: "task-session-error-toast",
|
||||
@@ -4390,6 +4417,141 @@ describe("BackgroundManager.handleEvent - session.error", () => {
|
||||
manager.shutdown()
|
||||
})
|
||||
|
||||
test("does not terminate task on session.error when session is still alive", async () => {
|
||||
//#given
|
||||
const manager = createBackgroundManager()
|
||||
mockVerifySessionExists(manager, true)
|
||||
|
||||
const task = createMockTask({
|
||||
id: "task-session-error-alive",
|
||||
sessionID: "ses-alive",
|
||||
parentSessionID: "parent-session",
|
||||
parentMessageID: "msg-alive",
|
||||
description: "task with transient session.error",
|
||||
agent: "explore",
|
||||
status: "running",
|
||||
})
|
||||
getTaskMap(manager).set(task.id, task)
|
||||
|
||||
//#when
|
||||
manager.handleEvent({
|
||||
type: "session.error",
|
||||
properties: {
|
||||
sessionID: task.sessionID,
|
||||
error: {
|
||||
name: "UnknownError",
|
||||
message: "Out of memory",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await flushBackgroundNotifications()
|
||||
|
||||
//#then
|
||||
expect(task.status).toBe("running")
|
||||
expect(task.error).toBeUndefined()
|
||||
expect(
|
||||
logCalls.some((call) => call.message.includes("session.error received but session still alive")),
|
||||
).toBe(true)
|
||||
|
||||
manager.shutdown()
|
||||
})
|
||||
|
||||
test("terminates task on session.error when session is gone", async () => {
|
||||
//#given
|
||||
const manager = createBackgroundManager()
|
||||
mockVerifySessionExists(manager, false)
|
||||
|
||||
const task = createMockTask({
|
||||
id: "task-session-error-gone",
|
||||
sessionID: "ses-gone",
|
||||
parentSessionID: "parent-session",
|
||||
parentMessageID: "msg-gone",
|
||||
description: "task with fatal session.error",
|
||||
agent: "explore",
|
||||
status: "running",
|
||||
})
|
||||
getTaskMap(manager).set(task.id, task)
|
||||
|
||||
//#when
|
||||
manager.handleEvent({
|
||||
type: "session.error",
|
||||
properties: {
|
||||
sessionID: task.sessionID,
|
||||
error: {
|
||||
name: "UnknownError",
|
||||
message: "Out of memory",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await flushBackgroundNotifications()
|
||||
|
||||
//#then
|
||||
expect(task.status).toBe("error")
|
||||
expect(task.error).toBe("Out of memory")
|
||||
|
||||
manager.shutdown()
|
||||
})
|
||||
|
||||
test("completes task on session.idle after transient session.error", async () => {
|
||||
//#given
|
||||
const sessionID = "ses-alive-idle"
|
||||
const client = {
|
||||
session: {
|
||||
prompt: async () => ({}),
|
||||
promptAsync: async () => ({}),
|
||||
abort: async () => ({}),
|
||||
messages: async () => ({
|
||||
data: [
|
||||
{
|
||||
info: { role: "assistant" },
|
||||
parts: [{ type: "text", text: "ok" }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
todo: async () => ({ data: [] }),
|
||||
},
|
||||
}
|
||||
|
||||
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||
stubNotifyParentSession(manager)
|
||||
mockVerifySessionExists(manager, true)
|
||||
|
||||
const task = createMockTask({
|
||||
id: "task-session-error-recovers",
|
||||
sessionID,
|
||||
parentSessionID: "parent-session",
|
||||
parentMessageID: "msg-recovers",
|
||||
description: "task that recovers after transient error",
|
||||
agent: "explore",
|
||||
status: "running",
|
||||
startedAt: new Date(Date.now() - (MIN_IDLE_TIME_MS + 10)),
|
||||
})
|
||||
getTaskMap(manager).set(task.id, task)
|
||||
|
||||
//#when
|
||||
manager.handleEvent({
|
||||
type: "session.error",
|
||||
properties: {
|
||||
sessionID,
|
||||
error: {
|
||||
name: "UnknownError",
|
||||
message: "Out of memory",
|
||||
},
|
||||
},
|
||||
})
|
||||
await flushBackgroundNotifications()
|
||||
manager.handleEvent({ type: "session.idle", properties: { sessionID } })
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
//#then
|
||||
expect(task.status).toBe("completed")
|
||||
expect(task.error).toBeUndefined()
|
||||
|
||||
manager.shutdown()
|
||||
})
|
||||
|
||||
test("retry path releases current concurrency slot and prefers current provider in fallback entry", async () => {
|
||||
//#given
|
||||
const manager = createBackgroundManager()
|
||||
|
||||
@@ -362,6 +362,7 @@ export class BackgroundManager {
|
||||
spawnDepth: spawnReservation.spawnContext.childDepth,
|
||||
parentSessionID: input.parentSessionID,
|
||||
parentMessageID: input.parentMessageID,
|
||||
teamRunId: input.teamRunId,
|
||||
parentModel: input.parentModel,
|
||||
parentAgent: input.parentAgent,
|
||||
parentTools: input.parentTools,
|
||||
@@ -533,7 +534,7 @@ export class BackgroundManager {
|
||||
parentID: input.parentSessionID,
|
||||
})
|
||||
|
||||
if (this.onSubagentSessionCreated && this.tmuxEnabled && isInsideTmux()) {
|
||||
if (!input.suppressTmuxSpawn && this.onSubagentSessionCreated && this.tmuxEnabled && isInsideTmux()) {
|
||||
log("[background-agent] Invoking tmux callback NOW", { sessionID })
|
||||
await this.onSubagentSessionCreated({
|
||||
sessionID,
|
||||
@@ -545,7 +546,9 @@ export class BackgroundManager {
|
||||
log("[background-agent] tmux callback completed, waiting 200ms")
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
} else {
|
||||
log("[background-agent] SKIP tmux callback - conditions not met")
|
||||
log("[background-agent] SKIP tmux callback - conditions not met", {
|
||||
suppressTmuxSpawn: !!input.suppressTmuxSpawn,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.tasks.get(task.id)?.status === "cancelled") {
|
||||
@@ -1320,6 +1323,19 @@ export class BackgroundManager {
|
||||
canRetry,
|
||||
})
|
||||
|
||||
const sessionID = task.sessionID
|
||||
if (sessionID) {
|
||||
const sessionStillAlive = await this.verifySessionExists(sessionID)
|
||||
if (sessionStillAlive) {
|
||||
log("[background-agent] session.error received but session still alive, treating as transient:", {
|
||||
taskId: task.id,
|
||||
sessionID,
|
||||
errorMessage: errorMsg?.slice(0, 200),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
task.status = "error"
|
||||
task.error = errorMsg
|
||||
task.completedAt = new Date()
|
||||
|
||||
Reference in New Issue
Block a user