
























@@ -155,6 +155,15 @@ export type HookRunnerOptions = {
155155 * Defaults to fail-open unless explicitly overridden for a hook name.
156156 */
157157failurePolicyByHook?: Partial<Record<PluginHookName, HookFailurePolicy>>;
158+/**
159+ * Optional timeout for void/observation hooks. A timed-out hook is logged and
160+ * the runner continues, but the plugin's underlying work is not cancelled.
161+ */
162+voidHookTimeoutMsByHook?: Partial<Record<PluginHookName, number>>;
163+};
164+165+const DEFAULT_VOID_HOOK_TIMEOUT_MS_BY_HOOK: Partial<Record<PluginHookName, number>> = {
166+agent_end: 30_000,
158167};
159168160169type ModifyingHookPolicy<K extends PluginHookName, TResult> = {
@@ -223,6 +232,10 @@ export function createHookRunner(
223232const logger = options.logger;
224233const catchErrors = options.catchErrors ?? true;
225234const failurePolicyByHook = options.failurePolicyByHook ?? {};
235+const voidHookTimeoutMsByHook = {
236+ ...DEFAULT_VOID_HOOK_TIMEOUT_MS_BY_HOOK,
237+ ...options.voidHookTimeoutMsByHook,
238+};
226239227240const shouldCatchHookErrors = (hookName: PluginHookName): boolean =>
228241catchErrors && (failurePolicyByHook[hookName] ?? "fail-open") === "fail-open";
@@ -364,6 +377,32 @@ export function createHookRunner(
364377return typeof (value as { then?: unknown }).then === "function";
365378};
366379380+const getVoidHookTimeoutMs = (hookName: PluginHookName): number | undefined => {
381+const timeoutMs = voidHookTimeoutMsByHook[hookName];
382+if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
383+return undefined;
384+}
385+return Math.floor(timeoutMs);
386+};
387+388+const withVoidHookTimeout = async <T>(promise: Promise<T>, timeoutMs: number): Promise<T> => {
389+let timer: ReturnType<typeof setTimeout> | undefined;
390+const timeout = new Promise<never>((_, reject) => {
391+timer = setTimeout(() => {
392+reject(new Error(`timed out after ${timeoutMs}ms`));
393+}, timeoutMs);
394+timer.unref?.();
395+});
396+397+try {
398+return await Promise.race([promise, timeout]);
399+} finally {
400+if (timer) {
401+clearTimeout(timer);
402+}
403+}
404+};
405+367406const runSyncHookHandler = <K extends SyncHookName>(
368407hook: PluginHookRegistration<K>,
369408event: SyncHookEvent<K>,
@@ -391,7 +430,15 @@ export function createHookRunner(
391430392431const promises = hooks.map(async (hook) => {
393432try {
394-await (hook.handler as (event: unknown, ctx: unknown) => Promise<void>)(event, ctx);
433+const promise = Promise.resolve(
434+(hook.handler as (event: unknown, ctx: unknown) => Promise<void> | void)(event, ctx),
435+);
436+const timeoutMs = getVoidHookTimeoutMs(hookName);
437+if (timeoutMs) {
438+await withVoidHookTimeout(promise, timeoutMs);
439+} else {
440+await promise;
441+}
395442} catch (err) {
396443handleHookError({ hookName, pluginId: hook.pluginId, error: err });
397444}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。