fix (gateway/agent): route bare /new and /reset through sessions.reset

This commit is contained in:
Vignesh Natarajan
2026-02-14 19:18:15 -08:00
parent cdeedd8093
commit 616658d4b0
3 changed files with 141 additions and 12 deletions

View File

@@ -43,6 +43,7 @@ import { buildGroupIntro } from "./groups.js";
import { buildInboundMetaSystemPrompt, buildInboundUserContextPrefix } from "./inbound-meta.js";
import { resolveQueueSettings } from "./queue.js";
import { routeReply } from "./route-reply.js";
import { BARE_SESSION_RESET_PROMPT } from "./session-reset-prompt.js";
import { ensureSkillSnapshot, prependSystemEvents } from "./session-updates.js";
import { resolveTypingMode } from "./typing-mode.js";
import { appendUntrustedContext } from "./untrusted-context.js";
@@ -50,9 +51,6 @@ import { appendUntrustedContext } from "./untrusted-context.js";
type AgentDefaults = NonNullable<OpenClawConfig["agents"]>["defaults"];
type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
const BARE_SESSION_RESET_PROMPT =
"A new session was started via /new or /reset. Greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.";
type RunPreparedReplyParams = {
ctx: MsgContext;
sessionCtx: TemplateContext;

View File

@@ -0,0 +1,2 @@
export const BARE_SESSION_RESET_PROMPT =
"A new session was started via /new or /reset. Greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.";

View File

@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto";
import type { GatewayRequestHandlers } from "./types.js";
import type { GatewayRequestHandlerOptions, GatewayRequestHandlers } from "./types.js";
import { listAgentIds } from "../../agents/agent-scope.js";
import { BARE_SESSION_RESET_PROMPT } from "../../auto-reply/reply/session-reset-prompt.js";
import { agentCommand } from "../../commands/agent.js";
import { loadConfig } from "../../config/config.js";
import {
@@ -47,9 +48,106 @@ import {
import { formatForLog } from "../ws-log.js";
import { waitForAgentJob } from "./agent-job.js";
import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js";
import { sessionsHandlers } from "./sessions.js";
const RESET_COMMAND_RE = /^\/(new|reset)(?:\s+([\s\S]*))?$/i;
function isGatewayErrorShape(value: unknown): value is { code: string; message: string } {
if (!value || typeof value !== "object") {
return false;
}
const candidate = value as { code?: unknown; message?: unknown };
return typeof candidate.code === "string" && typeof candidate.message === "string";
}
async function runSessionResetFromAgent(params: {
key: string;
reason: "new" | "reset";
idempotencyKey: string;
context: GatewayRequestHandlerOptions["context"];
client: GatewayRequestHandlerOptions["client"];
isWebchatConnect: GatewayRequestHandlerOptions["isWebchatConnect"];
}): Promise<
| { ok: true; key: string; sessionId?: string }
| { ok: false; error: ReturnType<typeof errorShape> }
> {
return await new Promise((resolve) => {
let settled = false;
const settle = (
result:
| { ok: true; key: string; sessionId?: string }
| { ok: false; error: ReturnType<typeof errorShape> },
) => {
if (settled) {
return;
}
settled = true;
resolve(result);
};
const respond: GatewayRequestHandlerOptions["respond"] = (ok, payload, error) => {
if (!ok) {
settle({
ok: false,
error: isGatewayErrorShape(error)
? error
: errorShape(ErrorCodes.UNAVAILABLE, String(error ?? "sessions.reset failed")),
});
return;
}
const payloadObj = payload as
| {
key?: unknown;
entry?: {
sessionId?: unknown;
};
}
| undefined;
const key = typeof payloadObj?.key === "string" ? payloadObj.key : params.key;
const sessionId =
payloadObj?.entry && typeof payloadObj.entry.sessionId === "string"
? payloadObj.entry.sessionId
: undefined;
settle({ ok: true, key, sessionId });
};
void sessionsHandlers["sessions.reset"]({
req: {
type: "req",
id: `${params.idempotencyKey}:reset`,
method: "sessions.reset",
},
params: {
key: params.key,
reason: params.reason,
},
context: params.context,
client: params.client,
isWebchatConnect: params.isWebchatConnect,
respond,
})
.then(() => {
if (!settled) {
settle({
ok: false,
error: errorShape(
ErrorCodes.UNAVAILABLE,
"sessions.reset completed without returning a response",
),
});
}
})
.catch((err) => {
settle({
ok: false,
error: errorShape(ErrorCodes.UNAVAILABLE, String(err)),
});
});
});
}
export const agentHandlers: GatewayRequestHandlers = {
agent: async ({ params, respond, context, client }) => {
agent: async ({ params, respond, context, client, isWebchatConnect }) => {
const p = params;
if (!validateAgentParams(p)) {
respond(
@@ -147,12 +245,6 @@ export const agentHandlers: GatewayRequestHandlers = {
}
}
// Inject timestamp into messages that don't already have one.
// Channel messages (Discord, Telegram, etc.) get timestamps via envelope
// formatting in a separate code path — they never reach this handler.
// See: https://github.com/moltbot/moltbot/issues/3658
message = injectTimestamp(message, timestampOptsFromConfig(cfg));
const isKnownGatewayChannel = (value: string): boolean => isGatewayMessageChannel(value);
const channelHints = [request.channel, request.replyChannel]
.filter((value): value is string => typeof value === "string")
@@ -194,7 +286,7 @@ export const agentHandlers: GatewayRequestHandlers = {
typeof request.sessionKey === "string" && request.sessionKey.trim()
? request.sessionKey.trim()
: undefined;
const requestedSessionKey =
let requestedSessionKey =
requestedSessionKeyRaw ??
resolveExplicitAgentSessionKey({
cfg,
@@ -219,6 +311,43 @@ export const agentHandlers: GatewayRequestHandlers = {
let bestEffortDeliver = false;
let cfgForAgent: ReturnType<typeof loadConfig> | undefined;
let resolvedSessionKey = requestedSessionKey;
let skipTimestampInjection = false;
const resetCommandMatch = message.match(RESET_COMMAND_RE);
if (resetCommandMatch && requestedSessionKey) {
const resetReason = resetCommandMatch[1]?.toLowerCase() === "new" ? "new" : "reset";
const resetResult = await runSessionResetFromAgent({
key: requestedSessionKey,
reason: resetReason,
idempotencyKey: idem,
context,
client,
isWebchatConnect,
});
if (!resetResult.ok) {
respond(false, undefined, resetResult.error);
return;
}
requestedSessionKey = resetResult.key;
resolvedSessionId = resetResult.sessionId ?? resolvedSessionId;
const postResetMessage = resetCommandMatch[2]?.trim() ?? "";
if (postResetMessage) {
message = postResetMessage;
} else {
// Keep bare /new and /reset behavior aligned with chat.send:
// reset first, then run a fresh-session greeting prompt in-place.
message = BARE_SESSION_RESET_PROMPT;
skipTimestampInjection = true;
}
}
// Inject timestamp into user-authored messages that don't already have one.
// Channel messages (Discord, Telegram, etc.) get timestamps via envelope
// formatting in a separate code path — they never reach this handler.
// See: https://github.com/moltbot/moltbot/issues/3658
if (!skipTimestampInjection) {
message = injectTimestamp(message, timestampOptsFromConfig(cfg));
}
if (requestedSessionKey) {
const { cfg, storePath, entry, canonicalKey } = loadSessionEntry(requestedSessionKey);