diff --git a/src/auto-reply/reply/commands-core.test.ts b/src/auto-reply/reply/commands-core.test.ts index 226037f957a..c58556ad038 100644 --- a/src/auto-reply/reply/commands-core.test.ts +++ b/src/auto-reply/reply/commands-core.test.ts @@ -40,7 +40,7 @@ describe("emitResetCommandHooks", () => { workspaceDir: "/tmp/openclaw-workspace", }); - await vi.waitFor(() => expect(hookRunnerMocks.runBeforeReset).toHaveBeenCalledTimes(1)); + expect(hookRunnerMocks.runBeforeReset).toHaveBeenCalledTimes(1); const [, ctx] = hookRunnerMocks.runBeforeReset.mock.calls[0] ?? []; return ctx; } diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index d7ddb815ecf..456aa4da52a 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -90,7 +90,7 @@ export async function emitResetCommandHooks(params: { } } - emitBeforeResetPluginHook({ + await emitBeforeResetPluginHook({ sessionKey: params.sessionKey, previousSessionEntry: params.previousSessionEntry, workspaceDir: params.workspaceDir, diff --git a/src/auto-reply/reply/reset-hooks.ts b/src/auto-reply/reply/reset-hooks.ts index ffaaf567b17..3e43851eb3a 100644 --- a/src/auto-reply/reply/reset-hooks.ts +++ b/src/auto-reply/reply/reset-hooks.ts @@ -8,12 +8,12 @@ type BeforeResetSessionEntry = { sessionFile?: string; } | null; -export function emitBeforeResetPluginHook(params: { +export async function emitBeforeResetPluginHook(params: { sessionKey?: string; previousSessionEntry?: BeforeResetSessionEntry; workspaceDir: string; reason: string; -}): void { +}): Promise { const hookRunner = getGlobalHookRunner(); if (!hookRunner?.hasHooks("before_reset")) { return; @@ -22,39 +22,36 @@ export function emitBeforeResetPluginHook(params: { const prevEntry = params.previousSessionEntry; const sessionFile = prevEntry?.sessionFile; - // Fire-and-forget: read old session messages and run hook before reset mutates the store. - void (async () => { - try { - const messages: unknown[] = []; - if (sessionFile) { - const content = await fs.readFile(sessionFile, "utf-8"); - for (const line of content.split("\n")) { - if (!line.trim()) { - continue; - } - try { - const entry = JSON.parse(line); - if (entry.type === "message" && entry.message) { - messages.push(entry.message); - } - } catch { - // Skip malformed transcript lines. - } + try { + const messages: unknown[] = []; + if (sessionFile) { + const content = await fs.readFile(sessionFile, "utf-8"); + for (const line of content.split("\n")) { + if (!line.trim()) { + continue; + } + try { + const entry = JSON.parse(line); + if (entry.type === "message" && entry.message) { + messages.push(entry.message); + } + } catch { + // Skip malformed transcript lines. } - } else { - logVerbose("before_reset: no session file available, firing hook with empty messages"); } - await hookRunner.runBeforeReset( - { sessionFile, messages, reason: params.reason }, - { - agentId: resolveAgentIdFromSessionKey(params.sessionKey), - sessionKey: params.sessionKey, - sessionId: prevEntry?.sessionId, - workspaceDir: params.workspaceDir, - }, - ); - } catch (err: unknown) { - logVerbose(`before_reset hook failed: ${String(err)}`); + } else { + logVerbose("before_reset: no session file available, firing hook with empty messages"); } - })(); + await hookRunner.runBeforeReset( + { sessionFile, messages, reason: params.reason }, + { + agentId: resolveAgentIdFromSessionKey(params.sessionKey), + sessionKey: params.sessionKey, + sessionId: prevEntry?.sessionId, + workspaceDir: params.workspaceDir, + }, + ); + } catch (err: unknown) { + logVerbose(`before_reset hook failed: ${String(err)}`); + } } diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 09e6bffa38d..72a29c429e4 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -490,12 +490,6 @@ export const sessionsHandlers: GatewayRequestHandlers = { }, ); await triggerInternalHook(hookEvent); - emitBeforeResetPluginHook({ - sessionKey: target.canonicalKey ?? key, - previousSessionEntry: entry, - workspaceDir: resolveAgentWorkspaceDir(cfg, target.agentId), - reason: commandReason, - }); const mutationCleanupError = await cleanupSessionBeforeMutation({ cfg, key, @@ -509,6 +503,12 @@ export const sessionsHandlers: GatewayRequestHandlers = { respond(false, undefined, mutationCleanupError); return; } + await emitBeforeResetPluginHook({ + sessionKey: target.canonicalKey ?? key, + previousSessionEntry: entry, + workspaceDir: resolveAgentWorkspaceDir(cfg, target.agentId), + reason: commandReason, + }); let oldSessionId: string | undefined; let oldSessionFile: string | undefined; const next = await updateSessionStore(storePath, (store) => { diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index 4a72194afaf..0e180b7d689 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -1235,6 +1235,7 @@ describe("gateway server sessions", () => { embeddedRunMock.activeIds.add("sess-main"); embeddedRunMock.waitResults.set("sess-main", false); + subagentLifecycleHookState.hasBeforeResetHook = true; const { ws } = await openClient(); @@ -1251,6 +1252,7 @@ describe("gateway server sessions", () => { ); expect(waitCallCountAtSnapshotClear).toEqual([1]); expect(browserSessionTabMocks.closeTrackedBrowserTabsForSessions).not.toHaveBeenCalled(); + expect(beforeResetHookMocks.runBeforeReset).not.toHaveBeenCalled(); const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< string,