refactor(features/background-agent): remove dead code submodules
Delete 15 unused files in background-agent module: - background-task-completer.ts - format-duration.ts - message-dir.ts - parent-session-context-resolver.ts - parent-session-notifier.ts (and its test file) - result-handler-context.ts - result-handler.ts - session-output-validator.ts - session-task-cleanup.ts - session-todo-checker.ts - spawner/background-session-creator.ts - spawner/concurrency-key-from-launch-input.ts - spawner/spawner-context.ts - spawner/tmux-callback-invoker.ts Update index.ts barrel and manager.ts/spawner.ts imports Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
b709fa8e83
commit
2428a46e6d
@ -1,40 +0,0 @@
|
|||||||
import type { BackgroundTask } from "./types"
|
|
||||||
import type { ResultHandlerContext } from "./result-handler-context"
|
|
||||||
import { log } from "../../shared"
|
|
||||||
import { notifyParentSession } from "./parent-session-notifier"
|
|
||||||
|
|
||||||
export async function tryCompleteTask(
|
|
||||||
task: BackgroundTask,
|
|
||||||
source: string,
|
|
||||||
ctx: ResultHandlerContext
|
|
||||||
): Promise<boolean> {
|
|
||||||
const { concurrencyManager, state } = ctx
|
|
||||||
|
|
||||||
if (task.status !== "running") {
|
|
||||||
log("[background-agent] Task already completed, skipping:", {
|
|
||||||
taskId: task.id,
|
|
||||||
status: task.status,
|
|
||||||
source,
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
task.status = "completed"
|
|
||||||
task.completedAt = new Date()
|
|
||||||
|
|
||||||
if (task.concurrencyKey) {
|
|
||||||
concurrencyManager.release(task.concurrencyKey)
|
|
||||||
task.concurrencyKey = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
state.markForNotification(task)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await notifyParentSession(task, ctx)
|
|
||||||
log(`[background-agent] Task completed via ${source}:`, task.id)
|
|
||||||
} catch (error) {
|
|
||||||
log("[background-agent] Error in notifyParentSession:", { taskId: task.id, error })
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
export function formatDuration(start: Date, end?: Date): string {
|
|
||||||
const duration = (end ?? new Date()).getTime() - start.getTime()
|
|
||||||
const seconds = Math.floor(duration / 1000)
|
|
||||||
const minutes = Math.floor(seconds / 60)
|
|
||||||
const hours = Math.floor(minutes / 60)
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
|
|
||||||
}
|
|
||||||
if (minutes > 0) {
|
|
||||||
return `${minutes}m ${seconds % 60}s`
|
|
||||||
}
|
|
||||||
return `${seconds}s`
|
|
||||||
}
|
|
||||||
@ -1,5 +1,2 @@
|
|||||||
export * from "./types"
|
export * from "./types"
|
||||||
export { BackgroundManager, type SubagentSessionCreatedEvent, type OnSubagentSessionCreated } from "./manager"
|
export { BackgroundManager, type SubagentSessionCreatedEvent, type OnSubagentSessionCreated } from "./manager"
|
||||||
export { TaskHistory, type TaskHistoryEntry } from "./task-history"
|
|
||||||
export { ConcurrencyManager } from "./concurrency"
|
|
||||||
export { TaskStateManager } from "./state"
|
|
||||||
|
|||||||
@ -268,7 +268,7 @@ export class BackgroundManager {
|
|||||||
body: {
|
body: {
|
||||||
parentID: input.parentSessionID,
|
parentID: input.parentSessionID,
|
||||||
title: `${input.description} (@${input.agent} subagent)`,
|
title: `${input.description} (@${input.agent} subagent)`,
|
||||||
} as any,
|
} as Record<string, unknown>,
|
||||||
query: {
|
query: {
|
||||||
directory: parentDirectory,
|
directory: parentDirectory,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export { getMessageDir } from "../../shared"
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
import type { OpencodeClient } from "./constants"
|
|
||||||
import type { BackgroundTask } from "./types"
|
|
||||||
import { findNearestMessageWithFields } from "../hook-message-injector"
|
|
||||||
import { getMessageDir } from "../../shared"
|
|
||||||
import { normalizePromptTools, resolveInheritedPromptTools } from "../../shared"
|
|
||||||
|
|
||||||
type AgentModel = { providerID: string; modelID: string }
|
|
||||||
|
|
||||||
function isObject(value: unknown): value is Record<string, unknown> {
|
|
||||||
return typeof value === "object" && value !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractAgentAndModelFromMessage(message: unknown): {
|
|
||||||
agent?: string
|
|
||||||
model?: AgentModel
|
|
||||||
tools?: Record<string, boolean>
|
|
||||||
} {
|
|
||||||
if (!isObject(message)) return {}
|
|
||||||
const info = message["info"]
|
|
||||||
if (!isObject(info)) return {}
|
|
||||||
|
|
||||||
const agent = typeof info["agent"] === "string" ? info["agent"] : undefined
|
|
||||||
const modelObj = info["model"]
|
|
||||||
const tools = normalizePromptTools(isObject(info["tools"]) ? info["tools"] as Record<string, unknown> as Record<string, boolean | "allow" | "deny" | "ask"> : undefined)
|
|
||||||
if (isObject(modelObj)) {
|
|
||||||
const providerID = modelObj["providerID"]
|
|
||||||
const modelID = modelObj["modelID"]
|
|
||||||
if (typeof providerID === "string" && typeof modelID === "string") {
|
|
||||||
return { agent, model: { providerID, modelID }, tools }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerID = info["providerID"]
|
|
||||||
const modelID = info["modelID"]
|
|
||||||
if (typeof providerID === "string" && typeof modelID === "string") {
|
|
||||||
return { agent, model: { providerID, modelID }, tools }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { agent, tools }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolveParentSessionAgentAndModel(input: {
|
|
||||||
client: OpencodeClient
|
|
||||||
task: BackgroundTask
|
|
||||||
}): Promise<{ agent?: string; model?: AgentModel; tools?: Record<string, boolean> }> {
|
|
||||||
const { client, task } = input
|
|
||||||
|
|
||||||
let agent: string | undefined = task.parentAgent
|
|
||||||
let model: AgentModel | undefined
|
|
||||||
let tools: Record<string, boolean> | undefined = task.parentTools
|
|
||||||
|
|
||||||
try {
|
|
||||||
const messagesResp = await client.session.messages({
|
|
||||||
path: { id: task.parentSessionID },
|
|
||||||
})
|
|
||||||
|
|
||||||
const messagesRaw = "data" in messagesResp ? messagesResp.data : []
|
|
||||||
const messages = Array.isArray(messagesRaw) ? messagesRaw : []
|
|
||||||
|
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
|
||||||
const extracted = extractAgentAndModelFromMessage(messages[i])
|
|
||||||
if (extracted.agent || extracted.model || extracted.tools) {
|
|
||||||
agent = extracted.agent ?? task.parentAgent
|
|
||||||
model = extracted.model
|
|
||||||
tools = extracted.tools ?? tools
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
const messageDir = getMessageDir(task.parentSessionID)
|
|
||||||
const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
|
||||||
agent = currentMessage?.agent ?? task.parentAgent
|
|
||||||
model =
|
|
||||||
currentMessage?.model?.providerID && currentMessage?.model?.modelID
|
|
||||||
? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID }
|
|
||||||
: undefined
|
|
||||||
tools = normalizePromptTools(currentMessage?.tools) ?? tools
|
|
||||||
}
|
|
||||||
|
|
||||||
return { agent, model, tools: resolveInheritedPromptTools(task.parentSessionID, tools) }
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
declare const require: (name: string) => any
|
|
||||||
const { describe, test, expect } = require("bun:test")
|
|
||||||
import type { BackgroundTask } from "./types"
|
|
||||||
import { buildBackgroundTaskNotificationText } from "./background-task-notification-template"
|
|
||||||
|
|
||||||
describe("notifyParentSession", () => {
|
|
||||||
test("displays INTERRUPTED for interrupted tasks", () => {
|
|
||||||
// given
|
|
||||||
const task: BackgroundTask = {
|
|
||||||
id: "test-task",
|
|
||||||
parentSessionID: "parent-session",
|
|
||||||
parentMessageID: "parent-message",
|
|
||||||
description: "Test task",
|
|
||||||
prompt: "Test prompt",
|
|
||||||
agent: "test-agent",
|
|
||||||
status: "interrupt",
|
|
||||||
startedAt: new Date(),
|
|
||||||
completedAt: new Date(),
|
|
||||||
}
|
|
||||||
const duration = "1s"
|
|
||||||
const statusText = task.status === "completed" ? "COMPLETED" : task.status === "interrupt" ? "INTERRUPTED" : "CANCELLED"
|
|
||||||
const allComplete = false
|
|
||||||
const remainingCount = 1
|
|
||||||
const completedTasks: BackgroundTask[] = []
|
|
||||||
|
|
||||||
// when
|
|
||||||
const notification = buildBackgroundTaskNotificationText({
|
|
||||||
task,
|
|
||||||
duration,
|
|
||||||
statusText,
|
|
||||||
allComplete,
|
|
||||||
remainingCount,
|
|
||||||
completedTasks,
|
|
||||||
})
|
|
||||||
|
|
||||||
// then
|
|
||||||
expect(notification).toContain("INTERRUPTED")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
import type { BackgroundTask } from "./types"
|
|
||||||
import type { ResultHandlerContext } from "./result-handler-context"
|
|
||||||
import { TASK_CLEANUP_DELAY_MS } from "./constants"
|
|
||||||
import { createInternalAgentTextPart, log } from "../../shared"
|
|
||||||
import { getTaskToastManager } from "../task-toast-manager"
|
|
||||||
import { formatDuration } from "./duration-formatter"
|
|
||||||
import { buildBackgroundTaskNotificationText } from "./background-task-notification-template"
|
|
||||||
import { resolveParentSessionAgentAndModel } from "./parent-session-context-resolver"
|
|
||||||
|
|
||||||
export async function notifyParentSession(
|
|
||||||
task: BackgroundTask,
|
|
||||||
ctx: ResultHandlerContext
|
|
||||||
): Promise<void> {
|
|
||||||
const { client, state } = ctx
|
|
||||||
|
|
||||||
const duration = formatDuration(task.startedAt ?? task.completedAt ?? new Date(), task.completedAt)
|
|
||||||
log("[background-agent] notifyParentSession called for task:", task.id)
|
|
||||||
|
|
||||||
const toastManager = getTaskToastManager()
|
|
||||||
if (toastManager) {
|
|
||||||
toastManager.showCompletionToast({
|
|
||||||
id: task.id,
|
|
||||||
description: task.description,
|
|
||||||
duration,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingSet = state.pendingByParent.get(task.parentSessionID)
|
|
||||||
if (pendingSet) {
|
|
||||||
pendingSet.delete(task.id)
|
|
||||||
if (pendingSet.size === 0) {
|
|
||||||
state.pendingByParent.delete(task.parentSessionID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allComplete = !pendingSet || pendingSet.size === 0
|
|
||||||
const remainingCount = pendingSet?.size ?? 0
|
|
||||||
|
|
||||||
const statusText = task.status === "completed" ? "COMPLETED" : task.status === "interrupt" ? "INTERRUPTED" : "CANCELLED"
|
|
||||||
|
|
||||||
const completedTasks = allComplete
|
|
||||||
? Array.from(state.tasks.values()).filter(
|
|
||||||
(t) =>
|
|
||||||
t.parentSessionID === task.parentSessionID &&
|
|
||||||
t.status !== "running" &&
|
|
||||||
t.status !== "pending"
|
|
||||||
)
|
|
||||||
: []
|
|
||||||
|
|
||||||
const notification = buildBackgroundTaskNotificationText({
|
|
||||||
task,
|
|
||||||
duration,
|
|
||||||
statusText,
|
|
||||||
allComplete,
|
|
||||||
remainingCount,
|
|
||||||
completedTasks,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { agent, model, tools } = await resolveParentSessionAgentAndModel({ client, task })
|
|
||||||
|
|
||||||
log("[background-agent] notifyParentSession context:", {
|
|
||||||
taskId: task.id,
|
|
||||||
resolvedAgent: agent,
|
|
||||||
resolvedModel: model,
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.session.promptAsync({
|
|
||||||
path: { id: task.parentSessionID },
|
|
||||||
body: {
|
|
||||||
noReply: !allComplete,
|
|
||||||
...(agent !== undefined ? { agent } : {}),
|
|
||||||
...(model !== undefined ? { model } : {}),
|
|
||||||
...(tools ? { tools } : {}),
|
|
||||||
parts: [createInternalAgentTextPart(notification)],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
log("[background-agent] Sent notification to parent session:", {
|
|
||||||
taskId: task.id,
|
|
||||||
allComplete,
|
|
||||||
noReply: !allComplete,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
log("[background-agent] Failed to send notification:", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!allComplete) return
|
|
||||||
|
|
||||||
for (const completedTask of completedTasks) {
|
|
||||||
const taskId = completedTask.id
|
|
||||||
state.clearCompletionTimer(taskId)
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
state.completionTimers.delete(taskId)
|
|
||||||
if (state.tasks.has(taskId)) {
|
|
||||||
state.clearNotificationsForTask(taskId)
|
|
||||||
state.tasks.delete(taskId)
|
|
||||||
log("[background-agent] Removed completed task from memory:", taskId)
|
|
||||||
}
|
|
||||||
}, TASK_CLEANUP_DELAY_MS)
|
|
||||||
state.setCompletionTimer(taskId, timer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import type { OpencodeClient } from "./constants"
|
|
||||||
import type { ConcurrencyManager } from "./concurrency"
|
|
||||||
import type { TaskStateManager } from "./state"
|
|
||||||
|
|
||||||
export interface ResultHandlerContext {
|
|
||||||
client: OpencodeClient
|
|
||||||
concurrencyManager: ConcurrencyManager
|
|
||||||
state: TaskStateManager
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export type { ResultHandlerContext } from "./result-handler-context"
|
|
||||||
export { formatDuration } from "./duration-formatter"
|
|
||||||
export { getMessageDir } from "../../shared"
|
|
||||||
export { checkSessionTodos } from "./session-todo-checker"
|
|
||||||
export { validateSessionHasOutput } from "./session-output-validator"
|
|
||||||
export { tryCompleteTask } from "./background-task-completer"
|
|
||||||
export { notifyParentSession } from "./parent-session-notifier"
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
import type { OpencodeClient } from "./constants"
|
|
||||||
import { log } from "../../shared"
|
|
||||||
|
|
||||||
type SessionMessagePart = {
|
|
||||||
type?: string
|
|
||||||
text?: string
|
|
||||||
content?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
function isObject(value: unknown): value is Record<string, unknown> {
|
|
||||||
return typeof value === "object" && value !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMessageRole(message: unknown): string | undefined {
|
|
||||||
if (!isObject(message)) return undefined
|
|
||||||
const info = message["info"]
|
|
||||||
if (!isObject(info)) return undefined
|
|
||||||
const role = info["role"]
|
|
||||||
return typeof role === "string" ? role : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMessageParts(message: unknown): SessionMessagePart[] {
|
|
||||||
if (!isObject(message)) return []
|
|
||||||
const parts = message["parts"]
|
|
||||||
if (!Array.isArray(parts)) return []
|
|
||||||
|
|
||||||
return parts
|
|
||||||
.filter((part): part is SessionMessagePart => isObject(part))
|
|
||||||
.map((part) => ({
|
|
||||||
type: typeof part["type"] === "string" ? part["type"] : undefined,
|
|
||||||
text: typeof part["text"] === "string" ? part["text"] : undefined,
|
|
||||||
content: part["content"],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function partHasContent(part: SessionMessagePart): boolean {
|
|
||||||
if (part.type === "text" || part.type === "reasoning") {
|
|
||||||
return Boolean(part.text && part.text.trim().length > 0)
|
|
||||||
}
|
|
||||||
if (part.type === "tool") return true
|
|
||||||
if (part.type === "tool_result") {
|
|
||||||
if (typeof part.content === "string") return part.content.trim().length > 0
|
|
||||||
if (Array.isArray(part.content)) return part.content.length > 0
|
|
||||||
return Boolean(part.content)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function validateSessionHasOutput(
|
|
||||||
client: OpencodeClient,
|
|
||||||
sessionID: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const response = await client.session.messages({
|
|
||||||
path: { id: sessionID },
|
|
||||||
})
|
|
||||||
|
|
||||||
const messagesRaw =
|
|
||||||
isObject(response) && "data" in response ? (response as { data?: unknown }).data : response
|
|
||||||
const messages = Array.isArray(messagesRaw) ? messagesRaw : []
|
|
||||||
|
|
||||||
const hasAssistantOrToolMessage = messages.some((message) => {
|
|
||||||
const role = getMessageRole(message)
|
|
||||||
return role === "assistant" || role === "tool"
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!hasAssistantOrToolMessage) {
|
|
||||||
log("[background-agent] No assistant/tool messages found in session:", sessionID)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasContent = messages.some((message) => {
|
|
||||||
const role = getMessageRole(message)
|
|
||||||
if (role !== "assistant" && role !== "tool") return false
|
|
||||||
const parts = getMessageParts(message)
|
|
||||||
return parts.some(partHasContent)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!hasContent) {
|
|
||||||
log("[background-agent] Messages exist but no content found in session:", sessionID)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
log("[background-agent] Error validating session output:", error)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import { subagentSessions } from "../claude-code-session-state"
|
|
||||||
import type { BackgroundTask } from "./types"
|
|
||||||
|
|
||||||
export function cleanupTaskAfterSessionEnds(args: {
|
|
||||||
task: BackgroundTask
|
|
||||||
tasks: Map<string, BackgroundTask>
|
|
||||||
idleDeferralTimers: Map<string, ReturnType<typeof setTimeout>>
|
|
||||||
completionTimers: Map<string, ReturnType<typeof setTimeout>>
|
|
||||||
cleanupPendingByParent: (task: BackgroundTask) => void
|
|
||||||
clearNotificationsForTask: (taskId: string) => void
|
|
||||||
releaseConcurrencyKey?: (key: string) => void
|
|
||||||
}): void {
|
|
||||||
const {
|
|
||||||
task,
|
|
||||||
tasks,
|
|
||||||
idleDeferralTimers,
|
|
||||||
completionTimers,
|
|
||||||
cleanupPendingByParent,
|
|
||||||
clearNotificationsForTask,
|
|
||||||
releaseConcurrencyKey,
|
|
||||||
} = args
|
|
||||||
|
|
||||||
const completionTimer = completionTimers.get(task.id)
|
|
||||||
if (completionTimer) {
|
|
||||||
clearTimeout(completionTimer)
|
|
||||||
completionTimers.delete(task.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const idleTimer = idleDeferralTimers.get(task.id)
|
|
||||||
if (idleTimer) {
|
|
||||||
clearTimeout(idleTimer)
|
|
||||||
idleDeferralTimers.delete(task.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task.concurrencyKey && releaseConcurrencyKey) {
|
|
||||||
releaseConcurrencyKey(task.concurrencyKey)
|
|
||||||
task.concurrencyKey = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupPendingByParent(task)
|
|
||||||
clearNotificationsForTask(task.id)
|
|
||||||
tasks.delete(task.id)
|
|
||||||
if (task.sessionID) {
|
|
||||||
subagentSessions.delete(task.sessionID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import type { OpencodeClient, Todo } from "./constants"
|
|
||||||
|
|
||||||
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" || todo["id"] === undefined) &&
|
|
||||||
typeof todo["content"] === "string" &&
|
|
||||||
typeof todo["status"] === "string" &&
|
|
||||||
typeof todo["priority"] === "string"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkSessionTodos(
|
|
||||||
client: OpencodeClient,
|
|
||||||
sessionID: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const response = await client.session.todo({
|
|
||||||
path: { id: sessionID },
|
|
||||||
})
|
|
||||||
|
|
||||||
const todosRaw = "data" in response ? response.data : response
|
|
||||||
if (!Array.isArray(todosRaw) || todosRaw.length === 0) return false
|
|
||||||
|
|
||||||
const incomplete = todosRaw
|
|
||||||
.filter(isTodo)
|
|
||||||
.filter((todo) => todo.status !== "completed" && todo.status !== "cancelled")
|
|
||||||
return incomplete.length > 0
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -61,9 +61,7 @@ export async function startTask(
|
|||||||
const createResult = await client.session.create({
|
const createResult = await client.session.create({
|
||||||
body: {
|
body: {
|
||||||
parentID: input.parentSessionID,
|
parentID: input.parentSessionID,
|
||||||
title: `Background: ${input.description}`,
|
} as Record<string, unknown>,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} as any,
|
|
||||||
query: {
|
query: {
|
||||||
directory: parentDirectory,
|
directory: parentDirectory,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
import type { OpencodeClient } from "../constants"
|
|
||||||
import type { ConcurrencyManager } from "../concurrency"
|
|
||||||
import type { LaunchInput } from "../types"
|
|
||||||
import { log } from "../../../shared"
|
|
||||||
|
|
||||||
export async function createBackgroundSession(options: {
|
|
||||||
client: OpencodeClient
|
|
||||||
input: LaunchInput
|
|
||||||
parentDirectory: string
|
|
||||||
concurrencyManager: ConcurrencyManager
|
|
||||||
concurrencyKey: string
|
|
||||||
}): Promise<string> {
|
|
||||||
const { client, input, parentDirectory, concurrencyManager, concurrencyKey } = options
|
|
||||||
|
|
||||||
const body = {
|
|
||||||
parentID: input.parentSessionID,
|
|
||||||
title: `Background: ${input.description}`,
|
|
||||||
}
|
|
||||||
|
|
||||||
const createResult = await client.session
|
|
||||||
.create({
|
|
||||||
body,
|
|
||||||
query: {
|
|
||||||
directory: parentDirectory,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch((error: unknown) => {
|
|
||||||
concurrencyManager.release(concurrencyKey)
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
|
|
||||||
if (createResult.error) {
|
|
||||||
concurrencyManager.release(concurrencyKey)
|
|
||||||
throw new Error(`Failed to create background session: ${createResult.error}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!createResult.data?.id) {
|
|
||||||
concurrencyManager.release(concurrencyKey)
|
|
||||||
throw new Error("Failed to create background session: API returned no session ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionID = createResult.data.id
|
|
||||||
log("[background-agent] Background session created", { sessionID })
|
|
||||||
return sessionID
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import type { LaunchInput } from "../types"
|
|
||||||
|
|
||||||
export function getConcurrencyKeyFromLaunchInput(input: LaunchInput): string {
|
|
||||||
return input.model
|
|
||||||
? `${input.model.providerID}/${input.model.modelID}`
|
|
||||||
: input.agent
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import type { BackgroundTask } from "../types"
|
|
||||||
import type { ConcurrencyManager } from "../concurrency"
|
|
||||||
import type { OpencodeClient, OnSubagentSessionCreated } from "../constants"
|
|
||||||
|
|
||||||
export interface SpawnerContext {
|
|
||||||
client: OpencodeClient
|
|
||||||
directory: string
|
|
||||||
concurrencyManager: ConcurrencyManager
|
|
||||||
tmuxEnabled: boolean
|
|
||||||
onSubagentSessionCreated?: OnSubagentSessionCreated
|
|
||||||
onTaskError: (task: BackgroundTask, error: Error) => void
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import { setTimeout } from "timers/promises"
|
|
||||||
import type { OnSubagentSessionCreated } from "../constants"
|
|
||||||
import { TMUX_CALLBACK_DELAY_MS } from "../constants"
|
|
||||||
import { log } from "../../../shared"
|
|
||||||
import { isInsideTmux } from "../../../shared/tmux"
|
|
||||||
|
|
||||||
export async function maybeInvokeTmuxCallback(options: {
|
|
||||||
onSubagentSessionCreated?: OnSubagentSessionCreated
|
|
||||||
tmuxEnabled: boolean
|
|
||||||
sessionID: string
|
|
||||||
parentID: string
|
|
||||||
title: string
|
|
||||||
}): Promise<void> {
|
|
||||||
const { onSubagentSessionCreated, tmuxEnabled, sessionID, parentID, title } = options
|
|
||||||
|
|
||||||
log("[background-agent] tmux callback check", {
|
|
||||||
hasCallback: !!onSubagentSessionCreated,
|
|
||||||
tmuxEnabled,
|
|
||||||
isInsideTmux: isInsideTmux(),
|
|
||||||
sessionID,
|
|
||||||
parentID,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!onSubagentSessionCreated || !tmuxEnabled || !isInsideTmux()) {
|
|
||||||
log("[background-agent] SKIP tmux callback - conditions not met")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log("[background-agent] Invoking tmux callback NOW", { sessionID })
|
|
||||||
await onSubagentSessionCreated({
|
|
||||||
sessionID,
|
|
||||||
parentID,
|
|
||||||
title,
|
|
||||||
}).catch((error: unknown) => {
|
|
||||||
log("[background-agent] Failed to spawn tmux pane:", error)
|
|
||||||
})
|
|
||||||
|
|
||||||
log("[background-agent] tmux callback completed, waiting")
|
|
||||||
await setTimeout(TMUX_CALLBACK_DELAY_MS)
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user