mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 13:14:58 +00:00
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: 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: 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.
|
- 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: 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: 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.
|
- 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(
|
export const SessionsResetParamsSchema = Type.Object(
|
||||||
{ key: NonEmptyString },
|
{
|
||||||
|
key: NonEmptyString,
|
||||||
|
reason: Type.Optional(Type.Union([Type.Literal("new"), Type.Literal("reset")])),
|
||||||
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
updateSessionStore,
|
updateSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
|
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
||||||
import { normalizeAgentId, parseAgentSessionKey } from "../../routing/session-key.js";
|
import { normalizeAgentId, parseAgentSessionKey } from "../../routing/session-key.js";
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
@@ -306,6 +307,19 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
||||||
const { entry } = loadSessionEntry(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 sessionId = entry?.sessionId;
|
||||||
const cleanupError = await ensureSessionRuntimeCleanup({ cfg, key, target, sessionId });
|
const cleanupError = await ensureSessionRuntimeCleanup({ cfg, key, target, sessionId });
|
||||||
if (cleanupError) {
|
if (cleanupError) {
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ const sessionCleanupMocks = vi.hoisted(() => ({
|
|||||||
stopSubagentsForRequester: vi.fn(() => ({ stopped: 0 })),
|
stopSubagentsForRequester: vi.fn(() => ({ stopped: 0 })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const sessionHookMocks = vi.hoisted(() => ({
|
||||||
|
triggerInternalHook: vi.fn(async () => {}),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../auto-reply/reply/queue.js", async () => {
|
vi.mock("../auto-reply/reply/queue.js", async () => {
|
||||||
const actual = await vi.importActual<typeof import("../auto-reply/reply/queue.js")>(
|
const actual = await vi.importActual<typeof import("../auto-reply/reply/queue.js")>(
|
||||||
"../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" });
|
installGatewayTestHooks({ scope: "suite" });
|
||||||
|
|
||||||
let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
||||||
@@ -74,6 +88,7 @@ describe("gateway server sessions", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sessionCleanupMocks.clearSessionQueues.mockClear();
|
sessionCleanupMocks.clearSessionQueues.mockClear();
|
||||||
sessionCleanupMocks.stopSubagentsForRequester.mockClear();
|
sessionCleanupMocks.stopSubagentsForRequester.mockClear();
|
||||||
|
sessionHookMocks.triggerInternalHook.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("lists and patches session store via sessions.* RPC", async () => {
|
test("lists and patches session store via sessions.* RPC", async () => {
|
||||||
@@ -643,6 +658,43 @@ describe("gateway server sessions", () => {
|
|||||||
ws.close();
|
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 () => {
|
test("sessions.reset returns unavailable when active run does not stop", async () => {
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-"));
|
||||||
const storePath = path.join(dir, "sessions.json");
|
const storePath = path.join(dir, "sessions.json");
|
||||||
|
|||||||
@@ -204,8 +204,11 @@ export class GatewayChatClient {
|
|||||||
return await this.client.request<SessionsPatchResult>("sessions.patch", opts);
|
return await this.client.request<SessionsPatchResult>("sessions.patch", opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetSession(key: string) {
|
async resetSession(key: string, reason?: "new" | "reset") {
|
||||||
return await this.client.request("sessions.reset", { key });
|
return await this.client.request("sessions.reset", {
|
||||||
|
key,
|
||||||
|
...(reason ? { reason } : {}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatus() {
|
async getStatus() {
|
||||||
|
|||||||
@@ -45,4 +45,42 @@ describe("tui command handlers", () => {
|
|||||||
);
|
);
|
||||||
expect(requestRender).toHaveBeenCalled();
|
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;
|
state.sessionInfo.totalTokens = null;
|
||||||
tui.requestRender();
|
tui.requestRender();
|
||||||
|
|
||||||
await client.resetSession(state.currentSessionKey);
|
await client.resetSession(state.currentSessionKey, name);
|
||||||
chatLog.addSystem(`session ${state.currentSessionKey} reset`);
|
chatLog.addSystem(`session ${state.currentSessionKey} reset`);
|
||||||
await loadHistory();
|
await loadHistory();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user