Merge pull request #2884 from RaviTharuma/fix/runtime-fallback-hook-isolation
Verified: bun test src/plugin/event.test.ts src/hooks/runtime-fallback/index.test.ts -- 68/68 pass. tsc clean.
This commit is contained in:
@@ -744,4 +744,66 @@ describe("createEventHandler - session recovery compaction", () => {
|
|||||||
//#then - continue is still sent even when compaction fails
|
//#then - continue is still sent even when compaction fails
|
||||||
expect(callOrder).toEqual(["summarize", "prompt"])
|
expect(callOrder).toEqual(["summarize", "prompt"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("continues dispatching later event hooks when an earlier hook throws", async () => {
|
||||||
|
//#given
|
||||||
|
const runtimeFallbackCalls: EventInput[] = []
|
||||||
|
|
||||||
|
const eventHandler = createEventHandler({
|
||||||
|
ctx: {
|
||||||
|
directory: "/tmp",
|
||||||
|
client: {
|
||||||
|
session: {
|
||||||
|
abort: async () => ({}),
|
||||||
|
prompt: async () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
pluginConfig: {} as any,
|
||||||
|
firstMessageVariantGate: {
|
||||||
|
markSessionCreated: () => {},
|
||||||
|
clear: () => {},
|
||||||
|
},
|
||||||
|
managers: {
|
||||||
|
tmuxSessionManager: {
|
||||||
|
onSessionCreated: async () => {},
|
||||||
|
onSessionDeleted: async () => {},
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
hooks: {
|
||||||
|
autoUpdateChecker: {
|
||||||
|
event: async () => {
|
||||||
|
throw new Error("upstream hook failed")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtimeFallback: {
|
||||||
|
event: async (input: EventInput) => {
|
||||||
|
runtimeFallbackCalls.push(input)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stopContinuationGuard: { isStopped: () => false },
|
||||||
|
} as any,
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
let thrownError: unknown
|
||||||
|
try {
|
||||||
|
await eventHandler({
|
||||||
|
event: {
|
||||||
|
type: "session.error",
|
||||||
|
properties: {
|
||||||
|
sessionID: "ses_hook_isolation",
|
||||||
|
error: { name: "Error", message: "retry me" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
} catch (error) {
|
||||||
|
thrownError = error
|
||||||
|
}
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(thrownError).toBeUndefined()
|
||||||
|
expect(runtimeFallbackCalls).toHaveLength(1)
|
||||||
|
expect(runtimeFallbackCalls[0]?.event.type).toBe("session.error")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -193,32 +193,68 @@ export function createEventHandler(args: {
|
|||||||
return "opencode";
|
return "opencode";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getEventSessionID = (input: EventInput): string | undefined => {
|
||||||
|
const properties = input.event.properties;
|
||||||
|
if (
|
||||||
|
!properties ||
|
||||||
|
typeof properties !== "object" ||
|
||||||
|
!("sessionID" in properties) ||
|
||||||
|
typeof properties.sessionID !== "string"
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return properties.sessionID;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runEventHookSafely = async (
|
||||||
|
hookName: string,
|
||||||
|
handler: ((input: EventInput) => unknown | Promise<unknown>) | null | undefined,
|
||||||
|
input: EventInput,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!handler) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.resolve(handler(input));
|
||||||
|
} catch (error) {
|
||||||
|
log("[event] hook execution failed", {
|
||||||
|
hook: hookName,
|
||||||
|
eventType: input.event.type,
|
||||||
|
sessionID: getEventSessionID(input),
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const dispatchToHooks = async (input: EventInput): Promise<void> => {
|
const dispatchToHooks = async (input: EventInput): Promise<void> => {
|
||||||
await Promise.resolve(hooks.autoUpdateChecker?.event?.(input));
|
await runEventHookSafely("autoUpdateChecker", hooks.autoUpdateChecker?.event, input);
|
||||||
await Promise.resolve(hooks.legacyPluginToast?.event?.(input));
|
await runEventHookSafely("legacyPluginToast", hooks.legacyPluginToast?.event, input);
|
||||||
await Promise.resolve(hooks.claudeCodeHooks?.event?.(input));
|
await runEventHookSafely("claudeCodeHooks", hooks.claudeCodeHooks?.event, input);
|
||||||
await Promise.resolve(hooks.backgroundNotificationHook?.event?.(input));
|
await runEventHookSafely("backgroundNotificationHook", hooks.backgroundNotificationHook?.event, input);
|
||||||
await Promise.resolve(hooks.sessionNotification?.(input));
|
await runEventHookSafely("sessionNotification", hooks.sessionNotification, input);
|
||||||
await Promise.resolve(hooks.todoContinuationEnforcer?.handler?.(input));
|
await runEventHookSafely("todoContinuationEnforcer", hooks.todoContinuationEnforcer?.handler, input);
|
||||||
await Promise.resolve(hooks.unstableAgentBabysitter?.event?.(input));
|
await runEventHookSafely("unstableAgentBabysitter", hooks.unstableAgentBabysitter?.event, input);
|
||||||
await Promise.resolve(hooks.contextWindowMonitor?.event?.(input));
|
await runEventHookSafely("contextWindowMonitor", hooks.contextWindowMonitor?.event, input);
|
||||||
await Promise.resolve(hooks.preemptiveCompaction?.event?.(input));
|
await runEventHookSafely("preemptiveCompaction", hooks.preemptiveCompaction?.event, input);
|
||||||
await Promise.resolve(hooks.directoryAgentsInjector?.event?.(input));
|
await runEventHookSafely("directoryAgentsInjector", hooks.directoryAgentsInjector?.event, input);
|
||||||
await Promise.resolve(hooks.directoryReadmeInjector?.event?.(input));
|
await runEventHookSafely("directoryReadmeInjector", hooks.directoryReadmeInjector?.event, input);
|
||||||
await Promise.resolve(hooks.rulesInjector?.event?.(input));
|
await runEventHookSafely("rulesInjector", hooks.rulesInjector?.event, input);
|
||||||
await Promise.resolve(hooks.thinkMode?.event?.(input));
|
await runEventHookSafely("thinkMode", hooks.thinkMode?.event, input);
|
||||||
await Promise.resolve(hooks.anthropicContextWindowLimitRecovery?.event?.(input));
|
await runEventHookSafely(
|
||||||
await Promise.resolve(hooks.runtimeFallback?.event?.(input));
|
"anthropicContextWindowLimitRecovery",
|
||||||
await Promise.resolve(hooks.agentUsageReminder?.event?.(input));
|
hooks.anthropicContextWindowLimitRecovery?.event,
|
||||||
await Promise.resolve(hooks.categorySkillReminder?.event?.(input));
|
input,
|
||||||
await Promise.resolve(hooks.interactiveBashSession?.event?.(input as EventInput));
|
);
|
||||||
await Promise.resolve(hooks.ralphLoop?.event?.(input));
|
await runEventHookSafely("runtimeFallback", hooks.runtimeFallback?.event, input);
|
||||||
await Promise.resolve(hooks.stopContinuationGuard?.event?.(input));
|
await runEventHookSafely("agentUsageReminder", hooks.agentUsageReminder?.event, input);
|
||||||
await Promise.resolve(hooks.compactionContextInjector?.event?.(input));
|
await runEventHookSafely("categorySkillReminder", hooks.categorySkillReminder?.event, input);
|
||||||
await Promise.resolve(hooks.compactionTodoPreserver?.event?.(input));
|
await runEventHookSafely("interactiveBashSession", hooks.interactiveBashSession?.event, input as EventInput);
|
||||||
await Promise.resolve(hooks.writeExistingFileGuard?.event?.(input));
|
await runEventHookSafely("ralphLoop", hooks.ralphLoop?.event, input);
|
||||||
await Promise.resolve(hooks.atlasHook?.handler?.(input));
|
await runEventHookSafely("stopContinuationGuard", hooks.stopContinuationGuard?.event, input);
|
||||||
await Promise.resolve(hooks.autoSlashCommand?.event?.(input));
|
await runEventHookSafely("compactionContextInjector", hooks.compactionContextInjector?.event, input);
|
||||||
|
await runEventHookSafely("compactionTodoPreserver", hooks.compactionTodoPreserver?.event, input);
|
||||||
|
await runEventHookSafely("writeExistingFileGuard", hooks.writeExistingFileGuard?.event, input);
|
||||||
|
await runEventHookSafely("atlasHook", hooks.atlasHook?.handler, input);
|
||||||
|
await runEventHookSafely("autoSlashCommand", hooks.autoSlashCommand?.event, input);
|
||||||
};
|
};
|
||||||
|
|
||||||
const recentSyntheticIdles = new Map<string, number>();
|
const recentSyntheticIdles = new Map<string, number>();
|
||||||
|
|||||||
Reference in New Issue
Block a user