fix(background-agent): add polling mechanism for child session tracking

- Replace unreliable event-based tracking with 2-second polling
- Use SDK session.get() to detect completion (status === idle)
- Use SDK session.messages() to count tool_use parts for progress
- Auto-start polling on launch, auto-stop when no running tasks
- Resume polling on restore if running tasks exist

Fixes: Child session events not reaching plugin event handler

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-11 17:12:45 +09:00
parent 245acdabad
commit b5d56246f6
2 changed files with 113 additions and 2 deletions

View File

@@ -7,7 +7,21 @@
"description": "Explore opencode in codebase",
"agent": "explore",
"status": "completed",
"startedAt": 1765434417395,
"completedAt": 1765434456778
"startedAt": "2025-12-11T06:26:57.395Z",
"completedAt": "2025-12-11T06:27:36.778Z"
},
{
"id": "bg_392b9c9b",
"sessionID": "ses_4f38ebf4fffeJZBocIn3UVv7vE",
"parentSessionID": "ses_4f38eefa0ffeKV0pVNnwT37P5L",
"parentMessageID": "msg_b0c7110d2001TMBlPeEYIrByvs",
"description": "Test explore agent",
"agent": "explore",
"status": "running",
"startedAt": "2025-12-11T08:05:07.378Z",
"progress": {
"toolCalls": 0,
"lastUpdate": "2025-12-11T08:05:07.378Z"
}
}
]

View File

@@ -36,6 +36,7 @@ export class BackgroundManager {
private client: OpencodeClient
private storePath: string
private persistTimer?: Timer
private pollingInterval?: Timer
constructor(client: OpencodeClient, storePath: string) {
this.tasks = new Map()
@@ -75,6 +76,7 @@ export class BackgroundManager {
this.tasks.set(task.id, task)
this.persist()
this.startPolling()
this.client.session.promptAsync({
path: { id: sessionID },
@@ -207,6 +209,97 @@ export class BackgroundManager {
}
}
private startPolling(): void {
if (this.pollingInterval) return
this.pollingInterval = setInterval(() => {
this.pollRunningTasks()
}, 2000)
}
private stopPolling(): void {
if (this.pollingInterval) {
clearInterval(this.pollingInterval)
this.pollingInterval = undefined
}
}
private hasRunningTasks(): boolean {
for (const task of this.tasks.values()) {
if (task.status === "running") return true
}
return false
}
private async pollRunningTasks(): Promise<void> {
for (const task of this.tasks.values()) {
if (task.status !== "running") continue
try {
const infoResult = await this.client.session.get({
path: { id: task.sessionID },
})
if (infoResult.error) {
task.status = "error"
task.error = "Session not found"
task.completedAt = new Date()
this.persist()
continue
}
const sessionInfo = infoResult.data as { status?: string }
if (sessionInfo.status === "idle") {
task.status = "completed"
task.completedAt = new Date()
this.markForNotification(task)
this.persist()
continue
}
const messagesResult = await this.client.session.messages({
path: { id: task.sessionID },
})
if (!messagesResult.error && messagesResult.data) {
const messages = messagesResult.data as Array<{
info?: { role?: string }
parts?: Array<{ type?: string; tool?: string; name?: string }>
}>
const assistantMsgs = messages.filter(
(m) => m.info?.role === "assistant"
)
let toolCalls = 0
let lastTool: string | undefined
for (const msg of assistantMsgs) {
const parts = msg.parts ?? []
for (const part of parts) {
if (part.type === "tool_use" || part.tool) {
toolCalls++
lastTool = part.tool || part.name || "unknown"
}
}
}
if (task.progress) {
task.progress.toolCalls = toolCalls
task.progress.lastTool = lastTool
task.progress.lastUpdate = new Date()
}
}
} catch {
void 0
}
}
if (!this.hasRunningTasks()) {
this.stopPolling()
}
}
persist(): void {
if (this.persistTimer) {
clearTimeout(this.persistTimer)
@@ -271,6 +364,10 @@ export class BackgroundManager {
}
this.tasks.set(task.id, task)
}
if (this.hasRunningTasks()) {
this.startPolling()
}
} catch {
void 0
}