Merge pull request #4203 from code-yeongyu/fix/continuation-prompt-dispatch-20260520051014

fix(plugin): dispatch synthetic idle hooks
This commit is contained in:
YeonGyu-Kim
2026-05-20 15:23:10 +09:00
committed by GitHub
3 changed files with 69 additions and 9 deletions
+10 -5
View File
@@ -37,10 +37,6 @@ export function saveInjectedRules(
sessionID: string, sessionID: string,
data: { contentHashes: Set<string>; realPaths: Set<string> } data: { contentHashes: Set<string>; realPaths: Set<string> }
): void { ): void {
if (!existsSync(RULES_INJECTOR_STORAGE)) {
mkdirSync(RULES_INJECTOR_STORAGE, { recursive: true });
}
const storageData: InjectedRulesData = { const storageData: InjectedRulesData = {
sessionID, sessionID,
injectedHashes: [...data.contentHashes], injectedHashes: [...data.contentHashes],
@@ -48,7 +44,16 @@ export function saveInjectedRules(
updatedAt: Date.now(), updatedAt: Date.now(),
}; };
writeFileSync(getStoragePath(sessionID), JSON.stringify(storageData, null, 2)); mkdirSync(RULES_INJECTOR_STORAGE, { recursive: true });
try {
writeFileSync(getStoragePath(sessionID), JSON.stringify(storageData, null, 2));
} catch (error) {
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
throw error;
}
mkdirSync(RULES_INJECTOR_STORAGE, { recursive: true });
writeFileSync(getStoragePath(sessionID), JSON.stringify(storageData, null, 2));
}
} }
export function clearInjectedRules(sessionID: string): void { export function clearInjectedRules(sessionID: string): void {
+49
View File
@@ -208,6 +208,55 @@ describe("createEventHandler - idle deduplication", () => {
expect(onEvent.mock.calls[0]?.[0]).toEqual(idleEvent.event) 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 () => { it("#given a readiness retry is pending #when session.idle arrives through the plugin handler #then tmux retry spawns the pane", async () => {
//#given //#given
const sessionStatusData: Record<string, { type: string }> = {} const sessionStatusData: Record<string, { type: string }> = {}
+10 -4
View File
@@ -394,6 +394,12 @@ export function createEventHandler(args: {
return hooks.sessionRecovery.handleInterruptedToolResultsOnIdle(sessionID); return hooks.sessionRecovery.handleInterruptedToolResultsOnIdle(sessionID);
}; };
const dispatchIdleOnlyHooks = async (input: EventInput): Promise<void> => {
managers.tmuxSessionManager?.onEvent?.(input.event);
await runEventHookSafely("teamIdleWakeHint", teamIdleWakeHint, input);
await runEventHookSafely("teamMemberStatusHandler", teamMemberStatusHandler, input);
};
const getFallbackContinuationKeys = (fallbackContext?: FallbackContinuationContext): FallbackContinuationDedupeKeys => { const getFallbackContinuationKeys = (fallbackContext?: FallbackContinuationContext): FallbackContinuationDedupeKeys => {
const agentKey = fallbackContext?.agentName const agentKey = fallbackContext?.agentName
? getAgentConfigKey(fallbackContext.agentName).trim().toLowerCase() ? getAgentConfigKey(fallbackContext.agentName).trim().toLowerCase()
@@ -628,7 +634,8 @@ export function createEventHandler(args: {
if (!shouldDispatchIdleEvent(sessionID, now)) { if (!shouldDispatchIdleEvent(sessionID, now)) {
return; return;
} }
await dispatchToHooks(syntheticIdle as EventInput); const syntheticIdleInput = syntheticIdle as EventInput;
await dispatchToHooks(syntheticIdleInput);
if (pluginConfig.openclaw) { if (pluginConfig.openclaw) {
await dispatchOpenClawEvent({ await dispatchOpenClawEvent({
config: pluginConfig.openclaw, config: pluginConfig.openclaw,
@@ -640,6 +647,7 @@ export function createEventHandler(args: {
}, },
}); });
} }
await dispatchIdleOnlyHooks(syntheticIdleInput);
} }
const { event } = input; const { event } = input;
@@ -761,9 +769,7 @@ export function createEventHandler(args: {
} }
if (event.type === "session.idle") { if (event.type === "session.idle") {
managers.tmuxSessionManager?.onEvent?.(event); await dispatchIdleOnlyHooks(input);
await runEventHookSafely("teamIdleWakeHint", teamIdleWakeHint, input);
await runEventHookSafely("teamMemberStatusHandler", teamMemberStatusHandler, input);
} }
if (event.type === "message.updated") { if (event.type === "message.updated") {