fix: skip session:patch hook clone without listeners

This commit is contained in:
Ayaan Zaidi 2026-03-25 16:11:33 +05:30
parent ee0dcaa7b0
commit 765182dcc6
No known key found for this signature in database
3 changed files with 56 additions and 20 deletions

View File

@ -19,6 +19,7 @@ import {
updateSessionStore,
} from "../../config/sessions.js";
import {
hasInternalHookListeners,
triggerInternalHook,
type SessionPatchHookContext,
type SessionPatchHookEvent,
@ -899,20 +900,22 @@ export const sessionsHandlers: GatewayRequestHandlers = {
return;
}
const hookContext: SessionPatchHookContext = structuredClone({
sessionEntry: applied.entry,
patch: p,
cfg,
});
const hookEvent: SessionPatchHookEvent = {
type: "session",
action: "patch",
sessionKey: target.canonicalKey ?? key,
context: hookContext,
timestamp: new Date(),
messages: [],
};
void triggerInternalHook(hookEvent);
if (hasInternalHookListeners("session", "patch")) {
const hookContext: SessionPatchHookContext = structuredClone({
sessionEntry: applied.entry,
patch: p,
cfg,
});
const hookEvent: SessionPatchHookEvent = {
type: "session",
action: "patch",
sessionKey: target.canonicalKey ?? key,
context: hookContext,
timestamp: new Date(),
messages: [],
};
void triggerInternalHook(hookEvent);
}
const parsed = parseAgentSessionKey(target.canonicalKey ?? key);
const agentId = normalizeAgentId(parsed?.agentId ?? resolveDefaultAgentId(cfg));

View File

@ -34,6 +34,7 @@ const bootstrapCacheMocks = vi.hoisted(() => ({
}));
const sessionHookMocks = vi.hoisted(() => ({
hasInternalHookListeners: vi.fn(() => true),
triggerInternalHook: vi.fn(async (_event: unknown) => {}),
}));
@ -96,6 +97,7 @@ vi.mock("../hooks/internal-hooks.js", async () => {
);
return {
...actual,
hasInternalHookListeners: sessionHookMocks.hasInternalHookListeners,
triggerInternalHook: sessionHookMocks.triggerInternalHook,
};
});
@ -260,6 +262,8 @@ describe("gateway server sessions", () => {
sessionCleanupMocks.clearSessionQueues.mockClear();
sessionCleanupMocks.stopSubagentsForRequester.mockClear();
bootstrapCacheMocks.clearBootstrapSnapshot.mockReset();
sessionHookMocks.hasInternalHookListeners.mockReset();
sessionHookMocks.hasInternalHookListeners.mockReturnValue(true);
sessionHookMocks.triggerInternalHook.mockClear();
subagentLifecycleHookMocks.runSubagentEnded.mockClear();
subagentLifecycleHookState.hasSubagentEndedHook = true;
@ -1938,6 +1942,30 @@ describe("gateway server sessions", () => {
ws.close();
});
test("session:patch skips clone and dispatch when no hooks listen", async () => {
const structuredCloneSpy = vi.spyOn(globalThis, "structuredClone");
sessionHookMocks.hasInternalHookListeners.mockReturnValue(false);
const { ws } = await openClient();
const patched = await rpcReq(ws, "sessions.patch", {
key: "agent:main:main",
label: "no-hook-listener",
});
expect(patched.ok).toBe(true);
expect(structuredCloneSpy).not.toHaveBeenCalledWith(
expect.objectContaining({
cfg: expect.any(Object),
patch: expect.any(Object),
sessionEntry: expect.any(Object),
}),
);
expect(sessionHookMocks.triggerInternalHook).not.toHaveBeenCalled();
structuredCloneSpy.mockRestore();
ws.close();
});
test("session:patch hook mutations cannot change the response path", async () => {
await createSessionStoreDir();
await writeSessionStore({

View File

@ -268,6 +268,12 @@ export function getRegisteredEventKeys(): string[] {
return Array.from(handlers.keys());
}
export function hasInternalHookListeners(type: InternalHookEventType, action: string): boolean {
return (
(handlers.get(type)?.length ?? 0) > 0 || (handlers.get(`${type}:${action}`)?.length ?? 0) > 0
);
}
/**
* Trigger a hook event
*
@ -281,15 +287,14 @@ export function getRegisteredEventKeys(): string[] {
* @param event - The event to trigger
*/
export async function triggerInternalHook(event: InternalHookEvent): Promise<void> {
const typeHandlers = handlers.get(event.type) ?? [];
const specificHandlers = handlers.get(`${event.type}:${event.action}`) ?? [];
const allHandlers = [...typeHandlers, ...specificHandlers];
if (allHandlers.length === 0) {
if (!hasInternalHookListeners(event.type, event.action)) {
return;
}
const typeHandlers = handlers.get(event.type) ?? [];
const specificHandlers = handlers.get(`${event.type}:${event.action}`) ?? [];
const allHandlers = [...typeHandlers, ...specificHandlers];
for (const handler of allHandlers) {
try {
await handler(event);