TUI/Gateway: emit internal hooks for /new and /reset

This commit is contained in:
Vignesh Natarajan
2026-02-14 16:33:19 -08:00
parent 301b3ff912
commit b08146fad6
7 changed files with 115 additions and 4 deletions

View File

@@ -82,7 +82,10 @@ export const SessionsPatchParamsSchema = Type.Object(
);
export const SessionsResetParamsSchema = Type.Object(
{ key: NonEmptyString },
{
key: NonEmptyString,
reason: Type.Optional(Type.Union([Type.Literal("new"), Type.Literal("reset")])),
},
{ additionalProperties: false },
);

View File

@@ -13,6 +13,7 @@ import {
type SessionEntry,
updateSessionStore,
} from "../../config/sessions.js";
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
import { normalizeAgentId, parseAgentSessionKey } from "../../routing/session-key.js";
import {
ErrorCodes,
@@ -306,6 +307,19 @@ export const sessionsHandlers: GatewayRequestHandlers = {
const cfg = loadConfig();
const target = resolveGatewaySessionStoreTarget({ cfg, key });
const { entry } = loadSessionEntry(key);
const commandReason = p.reason === "new" ? "new" : "reset";
const hookEvent = createInternalHookEvent(
"command",
commandReason,
target.canonicalKey ?? key,
{
sessionEntry: entry,
previousSessionEntry: entry,
commandSource: "gateway:sessions.reset",
cfg,
},
);
await triggerInternalHook(hookEvent);
const sessionId = entry?.sessionId;
const cleanupError = await ensureSessionRuntimeCleanup({ cfg, key, target, sessionId });
if (cleanupError) {

View File

@@ -21,6 +21,10 @@ const sessionCleanupMocks = vi.hoisted(() => ({
stopSubagentsForRequester: vi.fn(() => ({ stopped: 0 })),
}));
const sessionHookMocks = vi.hoisted(() => ({
triggerInternalHook: vi.fn(async () => {}),
}));
vi.mock("../auto-reply/reply/queue.js", async () => {
const actual = await vi.importActual<typeof import("../auto-reply/reply/queue.js")>(
"../auto-reply/reply/queue.js",
@@ -41,6 +45,16 @@ vi.mock("../auto-reply/reply/abort.js", async () => {
};
});
vi.mock("../hooks/internal-hooks.js", async () => {
const actual = await vi.importActual<typeof import("../hooks/internal-hooks.js")>(
"../hooks/internal-hooks.js",
);
return {
...actual,
triggerInternalHook: sessionHookMocks.triggerInternalHook,
};
});
installGatewayTestHooks({ scope: "suite" });
let server: Awaited<ReturnType<typeof startGatewayServer>>;
@@ -74,6 +88,7 @@ describe("gateway server sessions", () => {
beforeEach(() => {
sessionCleanupMocks.clearSessionQueues.mockClear();
sessionCleanupMocks.stopSubagentsForRequester.mockClear();
sessionHookMocks.triggerInternalHook.mockClear();
});
test("lists and patches session store via sessions.* RPC", async () => {
@@ -643,6 +658,43 @@ describe("gateway server sessions", () => {
ws.close();
});
test("sessions.reset emits internal command hook with reason", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-"));
const storePath = path.join(dir, "sessions.json");
testState.sessionStorePath = storePath;
await fs.writeFile(
path.join(dir, "sess-main.jsonl"),
`${JSON.stringify({ role: "user", content: "hello" })}\n`,
"utf-8",
);
await writeSessionStore({
entries: {
main: { sessionId: "sess-main", updatedAt: Date.now() },
},
});
const { ws } = await openClient();
const reset = await rpcReq<{ ok: true; key: string }>(ws, "sessions.reset", {
key: "main",
reason: "new",
});
expect(reset.ok).toBe(true);
expect(sessionHookMocks.triggerInternalHook).toHaveBeenCalledTimes(1);
const [event] = sessionHookMocks.triggerInternalHook.mock.calls[0] ?? [];
expect(event).toMatchObject({
type: "command",
action: "new",
sessionKey: "agent:main:main",
context: {
commandSource: "gateway:sessions.reset",
},
});
expect(event.context.previousSessionEntry).toMatchObject({ sessionId: "sess-main" });
ws.close();
});
test("sessions.reset returns unavailable when active run does not stop", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-"));
const storePath = path.join(dir, "sessions.json");