fix(types): add null checks for optional sessionID and startedAt fields
This commit is contained in:
@@ -311,8 +311,10 @@ export class BackgroundManager {
|
|||||||
|
|
||||||
for (const child of directChildren) {
|
for (const child of directChildren) {
|
||||||
result.push(child)
|
result.push(child)
|
||||||
const descendants = this.getAllDescendantTasks(child.sessionID)
|
if (child.sessionID) {
|
||||||
result.push(...descendants)
|
const descendants = this.getAllDescendantTasks(child.sessionID)
|
||||||
|
result.push(...descendants)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -363,7 +365,9 @@ export class BackgroundManager {
|
|||||||
existingTask.concurrencyGroup = input.concurrencyKey ?? existingTask.agent
|
existingTask.concurrencyGroup = input.concurrencyKey ?? existingTask.agent
|
||||||
}
|
}
|
||||||
|
|
||||||
subagentSessions.add(existingTask.sessionID)
|
if (existingTask.sessionID) {
|
||||||
|
subagentSessions.add(existingTask.sessionID)
|
||||||
|
}
|
||||||
this.startPolling()
|
this.startPolling()
|
||||||
|
|
||||||
// Track for batched notifications if task is pending or running
|
// Track for batched notifications if task is pending or running
|
||||||
@@ -428,6 +432,10 @@ export class BackgroundManager {
|
|||||||
throw new Error(`Task not found for session: ${input.sessionId}`)
|
throw new Error(`Task not found for session: ${input.sessionId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!existingTask.sessionID) {
|
||||||
|
throw new Error(`Task has no sessionID: ${existingTask.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
if (existingTask.status === "running") {
|
if (existingTask.status === "running") {
|
||||||
log("[background-agent] Resume skipped - task already running:", {
|
log("[background-agent] Resume skipped - task already running:", {
|
||||||
taskId: existingTask.id,
|
taskId: existingTask.id,
|
||||||
@@ -460,7 +468,9 @@ export class BackgroundManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.startPolling()
|
this.startPolling()
|
||||||
subagentSessions.add(existingTask.sessionID)
|
if (existingTask.sessionID) {
|
||||||
|
subagentSessions.add(existingTask.sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
if (input.parentSessionID) {
|
if (input.parentSessionID) {
|
||||||
const pending = this.pendingByParent.get(input.parentSessionID) ?? new Set()
|
const pending = this.pendingByParent.get(input.parentSessionID) ?? new Set()
|
||||||
@@ -571,9 +581,12 @@ export class BackgroundManager {
|
|||||||
|
|
||||||
const task = this.findBySession(sessionID)
|
const task = this.findBySession(sessionID)
|
||||||
if (!task || task.status !== "running") return
|
if (!task || task.status !== "running") return
|
||||||
|
|
||||||
|
const startedAt = task.startedAt
|
||||||
|
if (!startedAt) return
|
||||||
|
|
||||||
// Edge guard: Require minimum elapsed time (5 seconds) before accepting idle
|
// Edge guard: Require minimum elapsed time (5 seconds) before accepting idle
|
||||||
const elapsedMs = Date.now() - task.startedAt.getTime()
|
const elapsedMs = Date.now() - startedAt.getTime()
|
||||||
const MIN_IDLE_TIME_MS = 5000
|
const MIN_IDLE_TIME_MS = 5000
|
||||||
if (elapsedMs < MIN_IDLE_TIME_MS) {
|
if (elapsedMs < MIN_IDLE_TIME_MS) {
|
||||||
log("[background-agent] Ignoring early session.idle, elapsed:", { elapsedMs, taskId: task.id })
|
log("[background-agent] Ignoring early session.idle, elapsed:", { elapsedMs, taskId: task.id })
|
||||||
@@ -885,7 +898,7 @@ export class BackgroundManager {
|
|||||||
// Note: Callers must release concurrency before calling this method
|
// Note: Callers must release concurrency before calling this method
|
||||||
// to ensure slots are freed even if notification fails
|
// to ensure slots are freed even if notification fails
|
||||||
|
|
||||||
const duration = this.formatDuration(task.startedAt, task.completedAt)
|
const duration = this.formatDuration(task.startedAt ?? new Date(), task.completedAt)
|
||||||
|
|
||||||
log("[background-agent] notifyParentSession called for task:", task.id)
|
log("[background-agent] notifyParentSession called for task:", task.id)
|
||||||
|
|
||||||
@@ -1057,7 +1070,9 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
this.cleanupPendingByParent(task)
|
this.cleanupPendingByParent(task)
|
||||||
this.clearNotificationsForTask(taskId)
|
this.clearNotificationsForTask(taskId)
|
||||||
this.tasks.delete(taskId)
|
this.tasks.delete(taskId)
|
||||||
subagentSessions.delete(task.sessionID)
|
if (task.sessionID) {
|
||||||
|
subagentSessions.delete(task.sessionID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1067,6 +1082,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const validNotifications = notifications.filter((task) => {
|
const validNotifications = notifications.filter((task) => {
|
||||||
|
if (!task.startedAt) return false
|
||||||
const age = now - task.startedAt.getTime()
|
const age = now - task.startedAt.getTime()
|
||||||
return age <= TASK_TTL_MS
|
return age <= TASK_TTL_MS
|
||||||
})
|
})
|
||||||
@@ -1085,8 +1101,12 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
for (const task of this.tasks.values()) {
|
for (const task of this.tasks.values()) {
|
||||||
if (task.status !== "running") continue
|
if (task.status !== "running") continue
|
||||||
if (!task.progress?.lastUpdate) continue
|
if (!task.progress?.lastUpdate) continue
|
||||||
|
|
||||||
|
const startedAt = task.startedAt
|
||||||
|
const sessionID = task.sessionID
|
||||||
|
if (!startedAt || !sessionID) continue
|
||||||
|
|
||||||
const runtime = now - task.startedAt.getTime()
|
const runtime = now - startedAt.getTime()
|
||||||
if (runtime < MIN_RUNTIME_BEFORE_STALE_MS) continue
|
if (runtime < MIN_RUNTIME_BEFORE_STALE_MS) continue
|
||||||
|
|
||||||
const timeSinceLastUpdate = now - task.progress.lastUpdate.getTime()
|
const timeSinceLastUpdate = now - task.progress.lastUpdate.getTime()
|
||||||
@@ -1105,7 +1125,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.client.session.abort({
|
this.client.session.abort({
|
||||||
path: { id: task.sessionID },
|
path: { id: sessionID },
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
|
|
||||||
log(`[background-agent] Task ${task.id} interrupted: stale timeout`)
|
log(`[background-agent] Task ${task.id} interrupted: stale timeout`)
|
||||||
@@ -1127,14 +1147,17 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
|
|
||||||
for (const task of this.tasks.values()) {
|
for (const task of this.tasks.values()) {
|
||||||
if (task.status !== "running") continue
|
if (task.status !== "running") continue
|
||||||
|
|
||||||
|
const sessionID = task.sessionID
|
||||||
|
if (!sessionID) continue
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessionStatus = allStatuses[task.sessionID]
|
const sessionStatus = allStatuses[sessionID]
|
||||||
|
|
||||||
// Don't skip if session not in status - fall through to message-based detection
|
// Don't skip if session not in status - fall through to message-based detection
|
||||||
if (sessionStatus?.type === "idle") {
|
if (sessionStatus?.type === "idle") {
|
||||||
// Edge guard: Validate session has actual output before completing
|
// Edge guard: Validate session has actual output before completing
|
||||||
const hasValidOutput = await this.validateSessionHasOutput(task.sessionID)
|
const hasValidOutput = await this.validateSessionHasOutput(sessionID)
|
||||||
if (!hasValidOutput) {
|
if (!hasValidOutput) {
|
||||||
log("[background-agent] Polling idle but no valid output yet, waiting:", task.id)
|
log("[background-agent] Polling idle but no valid output yet, waiting:", task.id)
|
||||||
continue
|
continue
|
||||||
@@ -1143,7 +1166,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
// Re-check status after async operation
|
// Re-check status after async operation
|
||||||
if (task.status !== "running") continue
|
if (task.status !== "running") continue
|
||||||
|
|
||||||
const hasIncompleteTodos = await this.checkSessionTodos(task.sessionID)
|
const hasIncompleteTodos = await this.checkSessionTodos(sessionID)
|
||||||
if (hasIncompleteTodos) {
|
if (hasIncompleteTodos) {
|
||||||
log("[background-agent] Task has incomplete todos via polling, waiting:", task.id)
|
log("[background-agent] Task has incomplete todos via polling, waiting:", task.id)
|
||||||
continue
|
continue
|
||||||
@@ -1154,7 +1177,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messagesResult = await this.client.session.messages({
|
const messagesResult = await this.client.session.messages({
|
||||||
path: { id: task.sessionID },
|
path: { id: sessionID },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!messagesResult.error && messagesResult.data) {
|
if (!messagesResult.error && messagesResult.data) {
|
||||||
@@ -1196,14 +1219,17 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
|
|
||||||
// Stability detection: complete when message count unchanged for 3 polls
|
// Stability detection: complete when message count unchanged for 3 polls
|
||||||
const currentMsgCount = messages.length
|
const currentMsgCount = messages.length
|
||||||
const elapsedMs = Date.now() - task.startedAt.getTime()
|
const startedAt = task.startedAt
|
||||||
|
if (!startedAt) continue
|
||||||
|
|
||||||
|
const elapsedMs = Date.now() - startedAt.getTime()
|
||||||
|
|
||||||
if (elapsedMs >= MIN_STABILITY_TIME_MS) {
|
if (elapsedMs >= MIN_STABILITY_TIME_MS) {
|
||||||
if (task.lastMsgCount === currentMsgCount) {
|
if (task.lastMsgCount === currentMsgCount) {
|
||||||
task.stablePolls = (task.stablePolls ?? 0) + 1
|
task.stablePolls = (task.stablePolls ?? 0) + 1
|
||||||
if (task.stablePolls >= 3) {
|
if (task.stablePolls >= 3) {
|
||||||
// Edge guard: Validate session has actual output before completing
|
// Edge guard: Validate session has actual output before completing
|
||||||
const hasValidOutput = await this.validateSessionHasOutput(task.sessionID)
|
const hasValidOutput = await this.validateSessionHasOutput(sessionID)
|
||||||
if (!hasValidOutput) {
|
if (!hasValidOutput) {
|
||||||
log("[background-agent] Stability reached but no valid output, waiting:", task.id)
|
log("[background-agent] Stability reached but no valid output, waiting:", task.id)
|
||||||
continue
|
continue
|
||||||
@@ -1212,7 +1238,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
// Re-check status after async operation
|
// Re-check status after async operation
|
||||||
if (task.status !== "running") continue
|
if (task.status !== "running") continue
|
||||||
|
|
||||||
const hasIncompleteTodos = await this.checkSessionTodos(task.sessionID)
|
const hasIncompleteTodos = await this.checkSessionTodos(sessionID)
|
||||||
if (!hasIncompleteTodos) {
|
if (!hasIncompleteTodos) {
|
||||||
await this.tryCompleteTask(task, "stability detection")
|
await this.tryCompleteTask(task, "stability detection")
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ export function createBackgroundCompactionHook(manager: BackgroundManager) {
|
|||||||
sections.push("## Running Background Tasks")
|
sections.push("## Running Background Tasks")
|
||||||
sections.push("")
|
sections.push("")
|
||||||
for (const t of running) {
|
for (const t of running) {
|
||||||
const elapsed = Math.floor((Date.now() - t.startedAt.getTime()) / 1000)
|
const elapsed = t.startedAt
|
||||||
|
? Math.floor((Date.now() - t.startedAt.getTime()) / 1000)
|
||||||
|
: 0
|
||||||
sections.push(`- **\`${t.id}\`** (${t.agent}): ${t.description} [${elapsed}s elapsed]`)
|
sections.push(`- **\`${t.id}\`** (${t.agent}): ${t.description} [${elapsed}s elapsed]`)
|
||||||
}
|
}
|
||||||
sections.push("")
|
sections.push("")
|
||||||
|
|||||||
@@ -194,6 +194,10 @@ ${promptPreview}
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function formatTaskResult(task: BackgroundTask, client: OpencodeClient): Promise<string> {
|
async function formatTaskResult(task: BackgroundTask, client: OpencodeClient): Promise<string> {
|
||||||
|
if (!task.sessionID) {
|
||||||
|
return `Error: Task has no sessionID`
|
||||||
|
}
|
||||||
|
|
||||||
const messagesResult = await client.session.messages({
|
const messagesResult = await client.session.messages({
|
||||||
path: { id: task.sessionID },
|
path: { id: task.sessionID },
|
||||||
})
|
})
|
||||||
@@ -219,7 +223,7 @@ async function formatTaskResult(task: BackgroundTask, client: OpencodeClient): P
|
|||||||
|
|
||||||
Task ID: ${task.id}
|
Task ID: ${task.id}
|
||||||
Description: ${task.description}
|
Description: ${task.description}
|
||||||
Duration: ${formatDuration(task.startedAt, task.completedAt)}
|
Duration: ${formatDuration(task.startedAt ?? new Date(), task.completedAt)}
|
||||||
Session ID: ${task.sessionID}
|
Session ID: ${task.sessionID}
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -238,7 +242,7 @@ Session ID: ${task.sessionID}
|
|||||||
|
|
||||||
Task ID: ${task.id}
|
Task ID: ${task.id}
|
||||||
Description: ${task.description}
|
Description: ${task.description}
|
||||||
Duration: ${formatDuration(task.startedAt, task.completedAt)}
|
Duration: ${formatDuration(task.startedAt ?? new Date(), task.completedAt)}
|
||||||
Session ID: ${task.sessionID}
|
Session ID: ${task.sessionID}
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -255,7 +259,7 @@ Session ID: ${task.sessionID}
|
|||||||
|
|
||||||
const newMessages = consumeNewMessages(task.sessionID, sortedMessages)
|
const newMessages = consumeNewMessages(task.sessionID, sortedMessages)
|
||||||
if (newMessages.length === 0) {
|
if (newMessages.length === 0) {
|
||||||
const duration = formatDuration(task.startedAt, task.completedAt)
|
const duration = formatDuration(task.startedAt ?? new Date(), task.completedAt)
|
||||||
return `Task Result
|
return `Task Result
|
||||||
|
|
||||||
Task ID: ${task.id}
|
Task ID: ${task.id}
|
||||||
@@ -299,7 +303,7 @@ Session ID: ${task.sessionID}
|
|||||||
.filter((text) => text.length > 0)
|
.filter((text) => text.length > 0)
|
||||||
.join("\n\n")
|
.join("\n\n")
|
||||||
|
|
||||||
const duration = formatDuration(task.startedAt, task.completedAt)
|
const duration = formatDuration(task.startedAt ?? new Date(), task.completedAt)
|
||||||
|
|
||||||
return `Task Result
|
return `Task Result
|
||||||
|
|
||||||
@@ -408,7 +412,7 @@ export function createBackgroundCancel(manager: BackgroundManager, client: Openc
|
|||||||
// Pending task: use manager method (no session to abort)
|
// Pending task: use manager method (no session to abort)
|
||||||
manager.cancelPendingTask(task.id)
|
manager.cancelPendingTask(task.id)
|
||||||
results.push(`- ${task.id}: ${task.description} (pending)`)
|
results.push(`- ${task.id}: ${task.description} (pending)`)
|
||||||
} else {
|
} else if (task.sessionID) {
|
||||||
// Running task: abort session
|
// Running task: abort session
|
||||||
client.session.abort({
|
client.session.abort({
|
||||||
path: { id: task.sessionID },
|
path: { id: task.sessionID },
|
||||||
@@ -452,9 +456,11 @@ Status: ${task.status}`
|
|||||||
// Running task: abort session
|
// Running task: abort session
|
||||||
// Fire-and-forget: abort 요청을 보내고 await 하지 않음
|
// Fire-and-forget: abort 요청을 보내고 await 하지 않음
|
||||||
// await 하면 메인 세션까지 abort 되는 문제 발생
|
// await 하면 메인 세션까지 abort 되는 문제 발생
|
||||||
client.session.abort({
|
if (task.sessionID) {
|
||||||
path: { id: task.sessionID },
|
client.session.abort({
|
||||||
}).catch(() => {})
|
path: { id: task.sessionID },
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
task.status = "cancelled"
|
task.status = "cancelled"
|
||||||
task.completedAt = new Date()
|
task.completedAt = new Date()
|
||||||
|
|||||||
Reference in New Issue
Block a user