TUI/Gateway: emit internal hooks for /new and /reset
This commit is contained in:
@@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory/QMD: treat prefixed `no results found` marker output as an empty result set in qmd JSON parsing. (#11302) Thanks @blazerui.
|
||||
- Memory/QMD: make `memory status` read-only by skipping QMD boot update/embed side effects for status-only manager checks.
|
||||
- Memory/QMD: keep original QMD failures when builtin fallback initialization fails (for example missing embedding API keys), instead of replacing them with fallback init errors.
|
||||
- TUI/Hooks: pass explicit reset reason (`new` vs `reset`) through `sessions.reset` and emit internal command hooks for gateway-triggered resets so `/new` hook workflows fire in TUI/webchat.
|
||||
- Memory/QMD: pass result limits to `search`/`vsearch` commands so QMD can cap results earlier.
|
||||
- Memory/QMD: avoid reading full markdown files when a `from/lines` window is requested in QMD reads.
|
||||
- Memory/QMD: skip rewriting unchanged session export markdown files during sync to reduce disk churn.
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -204,8 +204,11 @@ export class GatewayChatClient {
|
||||
return await this.client.request<SessionsPatchResult>("sessions.patch", opts);
|
||||
}
|
||||
|
||||
async resetSession(key: string) {
|
||||
return await this.client.request("sessions.reset", { key });
|
||||
async resetSession(key: string, reason?: "new" | "reset") {
|
||||
return await this.client.request("sessions.reset", {
|
||||
key,
|
||||
...(reason ? { reason } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
|
||||
@@ -45,4 +45,42 @@ describe("tui command handlers", () => {
|
||||
);
|
||||
expect(requestRender).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes reset reason when handling /new and /reset", async () => {
|
||||
const resetSession = vi.fn().mockResolvedValue({ ok: true });
|
||||
const addSystem = vi.fn();
|
||||
const requestRender = vi.fn();
|
||||
const loadHistory = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const { handleCommand } = createCommandHandlers({
|
||||
client: { resetSession } as never,
|
||||
chatLog: { addSystem } as never,
|
||||
tui: { requestRender } as never,
|
||||
opts: {},
|
||||
state: {
|
||||
currentSessionKey: "agent:main:main",
|
||||
activeChatRunId: null,
|
||||
sessionInfo: {},
|
||||
} as never,
|
||||
deliverDefault: false,
|
||||
openOverlay: vi.fn(),
|
||||
closeOverlay: vi.fn(),
|
||||
refreshSessionInfo: vi.fn(),
|
||||
loadHistory,
|
||||
setSession: vi.fn(),
|
||||
refreshAgents: vi.fn(),
|
||||
abortActive: vi.fn(),
|
||||
setActivityStatus: vi.fn(),
|
||||
formatSessionKey: vi.fn(),
|
||||
applySessionInfoFromPatch: vi.fn(),
|
||||
noteLocalRunId: vi.fn(),
|
||||
});
|
||||
|
||||
await handleCommand("/new");
|
||||
await handleCommand("/reset");
|
||||
|
||||
expect(resetSession).toHaveBeenNthCalledWith(1, "agent:main:main", "new");
|
||||
expect(resetSession).toHaveBeenNthCalledWith(2, "agent:main:main", "reset");
|
||||
expect(loadHistory).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -436,7 +436,7 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
state.sessionInfo.totalTokens = null;
|
||||
tui.requestRender();
|
||||
|
||||
await client.resetSession(state.currentSessionKey);
|
||||
await client.resetSession(state.currentSessionKey, name);
|
||||
chatLog.addSystem(`session ${state.currentSessionKey} reset`);
|
||||
await loadHistory();
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user