From 8a5f61724db25db1ac0a1ca58e51a2d2d735e291 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 15 Feb 2026 14:26:25 +0900 Subject: [PATCH] fix(background-agent): handle message.part.delta for heartbeat (OpenCode >=1.2.0) OpenCode 1.2.0+ changed reasoning-delta and text-delta to emit 'message.part.delta' instead of 'message.part.updated'. Without handling this event, lastUpdate was only refreshed at reasoning-start and reasoning-end, leaving a gap where extended thinking (>3min) could trigger stale timeout. Accept both event types as heartbeat sources for forward compatibility. --- .../background-event-handler.ts | 2 +- src/features/background-agent/manager.test.ts | 40 +++++++++++++++++++ src/features/background-agent/manager.ts | 2 +- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/features/background-agent/background-event-handler.ts b/src/features/background-agent/background-event-handler.ts index 1f909b67..0d6290f3 100644 --- a/src/features/background-agent/background-event-handler.ts +++ b/src/features/background-agent/background-event-handler.ts @@ -52,7 +52,7 @@ export function handleBackgroundEvent(args: { const props = event.properties - if (event.type === "message.part.updated") { + if (event.type === "message.part.updated" || event.type === "message.part.delta") { if (!props || !isRecord(props)) return const sessionID = getString(props, "sessionID") if (!sessionID) return diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 789fac21..35f1afe4 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -3413,4 +3413,44 @@ describe("BackgroundManager.handleEvent - non-tool event lastUpdate", () => { //#then - task should still be running (text event refreshed lastUpdate) expect(task.status).toBe("running") }) + + test("should refresh lastUpdate on message.part.delta events (OpenCode >=1.2.0)", async () => { + //#given - a running task with stale lastUpdate + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), + }, + } + const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, { staleTimeoutMs: 180_000 }) + stubNotifyParentSession(manager) + + const task: BackgroundTask = { + id: "task-delta-1", + sessionID: "session-delta-1", + parentSessionID: "parent-1", + parentMessageID: "msg-1", + description: "Reasoning task with delta events", + prompt: "Extended thinking", + agent: "oracle", + status: "running", + startedAt: new Date(Date.now() - 600_000), + progress: { + toolCalls: 0, + lastUpdate: new Date(Date.now() - 300_000), + }, + } + getTaskMap(manager).set(task.id, task) + + //#when - a message.part.delta event arrives (reasoning-delta or text-delta in OpenCode >=1.2.0) + manager.handleEvent({ + type: "message.part.delta", + properties: { sessionID: "session-delta-1", field: "text", delta: "thinking..." }, + }) + await manager["checkAndInterruptStaleTasks"]() + + //#then - task should still be running (delta event refreshed lastUpdate) + expect(task.status).toBe("running") + }) }) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 5066039b..86ab03d3 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -660,7 +660,7 @@ export class BackgroundManager { handleEvent(event: Event): void { const props = event.properties - if (event.type === "message.part.updated") { + if (event.type === "message.part.updated" || event.type === "message.part.delta") { if (!props || typeof props !== "object" || !("sessionID" in props)) return const partInfo = props as unknown as MessagePartInfo const sessionID = partInfo?.sessionID