diff --git a/packages/web/server/lib/opencode/session-runtime.js b/packages/web/server/lib/opencode/session-runtime.js index 6bcbbec4..4aea8cf6 100644 --- a/packages/web/server/lib/opencode/session-runtime.js +++ b/packages/web/server/lib/opencode/session-runtime.js @@ -96,7 +96,8 @@ export const createSessionRuntime = ({ writeSseEvent, getNotificationClients, br const timer = setTimeout(() => { const now = sessionActivityPhases.get(sessionId); if (now?.phase === 'cooldown') { - sessionActivityPhases.set(sessionId, { phase: 'idle', updatedAt: Date.now() }); + setSessionActivityPhase(sessionId, 'idle'); + return; } sessionActivityCooldowns.delete(sessionId); }, SESSION_COOLDOWN_DURATION_MS); @@ -178,7 +179,9 @@ export const createSessionRuntime = ({ writeSseEvent, getNotificationClients, br } const phase = status === 'busy' || status === 'retry' ? 'busy' : 'idle'; - setSessionActivityPhase(sessionId, phase); + if (phase !== 'idle' || sessionActivityPhases.get(sessionId)?.phase !== 'cooldown') { + setSessionActivityPhase(sessionId, phase); + } }; const getSessionStateSnapshot = () => { diff --git a/packages/web/server/lib/opencode/session-runtime.test.js b/packages/web/server/lib/opencode/session-runtime.test.js index e0988864..1d4807d2 100644 --- a/packages/web/server/lib/opencode/session-runtime.test.js +++ b/packages/web/server/lib/opencode/session-runtime.test.js @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { createSessionRuntime } from './session-runtime.js'; @@ -97,4 +97,55 @@ describe('session runtime', () => { }), }); }); + + it('broadcasts idle activity when cooldown expires', () => { + vi.useFakeTimers(); + const events = []; + const runtime = createSessionRuntime({ + writeSseEvent() { + throw new Error('SSE fallback should not be used when broadcastEvent is provided'); + }, + getNotificationClients: () => new Set(), + broadcastEvent: (payload) => { + events.push(payload); + }, + }); + + try { + runtime.processOpenCodeSsePayload({ + type: 'session.status', + properties: { + sessionID: 'session-activity-1', + status: { + type: 'busy', + }, + }, + }); + runtime.processOpenCodeSsePayload({ + type: 'session.status', + properties: { + sessionID: 'session-activity-1', + status: { + type: 'idle', + }, + }, + }); + + const activityPhases = () => events + .filter((event) => event.type === 'openchamber:session-activity') + .map((event) => event.properties.phase); + + expect(activityPhases()).toEqual(['busy', 'cooldown']); + + vi.advanceTimersByTime(1999); + expect(activityPhases()).toEqual(['busy', 'cooldown']); + + vi.advanceTimersByTime(1); + + expect(activityPhases()).toEqual(['busy', 'cooldown', 'idle']); + } finally { + runtime.dispose(); + vi.useRealTimers(); + } + }); });