diff --git a/src/plugin/event.test.ts b/src/plugin/event.test.ts index 55a88ffa5..df3eb27d9 100644 --- a/src/plugin/event.test.ts +++ b/src/plugin/event.test.ts @@ -208,6 +208,55 @@ describe("createEventHandler - idle deduplication", () => { expect(onEvent.mock.calls[0]?.[0]).toEqual(idleEvent.event) }) + it("#given tmux integration enabled #when session.status reports idle #then synthetic idle forwards to tmuxSessionManager.onEvent", async () => { + //#given + const onEvent = mock<(event: EventInput["event"]) => void>(() => {}) + const eventHandler = createEventHandler({ + ctx: asEventHandlerContext({ + directory: "/tmp", + client: { + session: {}, + }, + }), + pluginConfig: asPluginConfig({ + tmux: { enabled: true }, + }), + firstMessageVariantGate: { + markSessionCreated: () => {}, + clear: () => {}, + }, + managers: createEventHandlerManagers({ + tmuxSessionManager: { + onEvent, + onSessionCreated: async () => {}, + onSessionDeleted: async () => {}, + }, + }), + hooks: createEventHandlerHooks({}), + }) + + //#when + await eventHandler(asEventHandlerInput({ + event: { + type: "session.status", + properties: { + sessionID: "ses_tmux_synthetic_idle", + status: { type: "idle" }, + }, + }, + })) + + //#then + expect(onEvent).toHaveBeenCalledTimes(1) + expect(onEvent.mock.calls[0]?.[0]).toEqual({ + type: "session.idle", + properties: { + sessionID: "ses_tmux_synthetic_idle", + synthetic: true, + }, + }) + }) + it("#given a readiness retry is pending #when session.idle arrives through the plugin handler #then tmux retry spawns the pane", async () => { //#given const sessionStatusData: Record = {} diff --git a/src/plugin/event.ts b/src/plugin/event.ts index d60222ab7..cb653f8d7 100644 --- a/src/plugin/event.ts +++ b/src/plugin/event.ts @@ -394,6 +394,12 @@ export function createEventHandler(args: { return hooks.sessionRecovery.handleInterruptedToolResultsOnIdle(sessionID); }; + const dispatchIdleOnlyHooks = async (input: EventInput): Promise => { + managers.tmuxSessionManager?.onEvent?.(input.event); + await runEventHookSafely("teamIdleWakeHint", teamIdleWakeHint, input); + await runEventHookSafely("teamMemberStatusHandler", teamMemberStatusHandler, input); + }; + const getFallbackContinuationKeys = (fallbackContext?: FallbackContinuationContext): FallbackContinuationDedupeKeys => { const agentKey = fallbackContext?.agentName ? getAgentConfigKey(fallbackContext.agentName).trim().toLowerCase() @@ -628,7 +634,8 @@ export function createEventHandler(args: { if (!shouldDispatchIdleEvent(sessionID, now)) { return; } - await dispatchToHooks(syntheticIdle as EventInput); + const syntheticIdleInput = syntheticIdle as EventInput; + await dispatchToHooks(syntheticIdleInput); if (pluginConfig.openclaw) { await dispatchOpenClawEvent({ config: pluginConfig.openclaw, @@ -640,6 +647,7 @@ export function createEventHandler(args: { }, }); } + await dispatchIdleOnlyHooks(syntheticIdleInput); } const { event } = input; @@ -761,9 +769,7 @@ export function createEventHandler(args: { } if (event.type === "session.idle") { - managers.tmuxSessionManager?.onEvent?.(event); - await runEventHookSafely("teamIdleWakeHint", teamIdleWakeHint, input); - await runEventHookSafely("teamMemberStatusHandler", teamMemberStatusHandler, input); + await dispatchIdleOnlyHooks(input); } if (event.type === "message.updated") {