fix(gateway): reset webchat /new in place when dmScope is main (#77434) (#71170)

Merged via squash.

Prepared head SHA: 96a9a83eac
Co-authored-by: statxc <181730535+statxc@users.noreply.github.com>
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
This commit is contained in:
Statxc
2026-05-08 09:11:17 -05:00
committed by GitHub
parent dce9261415
commit 9da2f7cf81
5 changed files with 107 additions and 3 deletions

View File

@@ -653,6 +653,7 @@ Docs: https://docs.openclaw.ai
- Agents/current-time: split UTC into a separate `Reference UTC:` prompt line so local `Current time:` stays anchored to the user's timezone. (#42654) Thanks @chencheng-li.
- Agents/reasoning: keep embedded reasoning deltas raw for correct same-line streaming while preserving formatted Telegram, Feishu, Discord, and heartbeat delivery at the channel edge. (#78397) Thanks @medns.
- Agents/failover: rotate auth profiles before deferred cooldown marking on rate-limit failures, so file-lock contention cannot stall profile failover. Fixes #57281. (#57283) Thanks @jeremyknows.
- Gateway/sessions: when `session.dmScope: "main"` is configured, route a bare webchat `/new` against the agent's main session (`sessions.create` with `emitCommandHooks=true`) to an in-place reset instead of creating a parallel `dashboard:` child, matching `/new` behavior on Telegram/Discord. Fixes #77434. (#71170) Thanks @statxc.
## 2026.5.3-1

View File

@@ -125,7 +125,7 @@ Current source-of-truth:
<AccordionGroup>
<Accordion title="Sessions and runs">
- `/new [model]` starts a new session; `/reset` is the reset alias.
- Control UI intercepts typed `/new` to create and switch to a fresh dashboard session; typed `/reset` still runs the Gateway's in-place reset.
- Control UI intercepts typed `/new` to create and switch to a fresh dashboard session, except when `session.dmScope: "main"` is configured and the current parent is the agent's main session; in that case `/new` resets the main session in place. Typed `/reset` still runs the Gateway's in-place reset.
- `/reset soft [message]` keeps the current transcript, drops reused CLI backend session ids, and reruns startup/system-prompt loading in-place.
- `/compact [instructions]` compacts the session context. See [Compaction](/concepts/compaction).
- `/stop` aborts the current run.

View File

@@ -165,7 +165,7 @@ Imported themes are stored only in the current browser profile. They are not wri
- Consecutive duplicate text-only messages render as one bubble with a count badge. Messages that carry images, attachments, tool output, or canvas previews are left uncollapsed.
- The chat header model and thinking pickers patch the active session immediately through `sessions.patch`; they are persistent session overrides, not one-turn-only send options.
- If you send a message while a model picker change for the same session is still saving, the composer waits for that session patch before calling `chat.send` so the send uses the selected model.
- Typing `/new` in the Control UI creates and switches to the same fresh dashboard session as New Chat. Typing `/reset` keeps the Gateway's explicit in-place reset for the current session.
- Typing `/new` in the Control UI creates and switches to the same fresh dashboard session as New Chat, except when `session.dmScope: "main"` is configured and the current parent is the agent's main session; in that case it resets the main session in place. Typing `/reset` keeps the Gateway's explicit in-place reset for the current session.
- The chat model picker requests the Gateway's configured model view. If `agents.defaults.models` is present, that allowlist drives the picker. Otherwise the picker shows explicit `models.providers.*.models` entries plus providers with usable auth. The full catalog stays available through the debug `models.list` RPC with `view: "all"`.
- When fresh Gateway session usage reports include current context tokens, the chat composer area shows a compact context usage indicator. It switches to warning styling at high context pressure and, at recommended compaction levels, shows a compact button that runs the normal session compaction path. Stale token snapshots are hidden until the Gateway reports fresh usage again.

View File

@@ -26,6 +26,7 @@ import {
type SessionEntry,
updateSessionStore,
} from "../../config/sessions.js";
import { resolveAgentMainSessionKey } from "../../config/sessions/main-session.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
createInternalHookEvent,
@@ -1023,6 +1024,46 @@ export const sessionsHandlers: GatewayRequestHandlers = {
}
canonicalParentSessionKey = parent.canonicalKey;
}
if (
canonicalParentSessionKey &&
p.emitCommandHooks === true &&
!requestedKey &&
!resolveOptionalInitialSessionMessage(p) &&
cfg.session?.dmScope === "main"
) {
const parentAgentId = normalizeAgentId(
resolveAgentIdFromSessionKey(canonicalParentSessionKey) ?? resolveDefaultAgentId(cfg),
);
const parentMainKey = resolveAgentMainSessionKey({ cfg, agentId: parentAgentId });
if (canonicalParentSessionKey === parentMainKey) {
const { performGatewaySessionReset } = await loadSessionsRuntimeModule();
const resetResult = await performGatewaySessionReset({
key: canonicalParentSessionKey,
reason: "new",
commandSource: "webchat",
});
if (!resetResult.ok) {
respond(false, undefined, resetResult.error);
return;
}
respond(
true,
{
ok: true,
key: resetResult.key,
sessionId: resetResult.entry.sessionId,
entry: resetResult.entry,
runStarted: false,
},
undefined,
);
emitSessionsChanged(context, {
sessionKey: resetResult.key,
reason: "new",
});
return;
}
}
if (canonicalParentSessionKey && p.emitCommandHooks === true) {
const { entry: parentEntry } = loadSessionEntry(canonicalParentSessionKey);
const parentAgentId = normalizeAgentId(

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { expect, test } from "vitest";
import { embeddedRunMock, writeSessionStore } from "./test-helpers.js";
import { embeddedRunMock, testState, writeSessionStore } from "./test-helpers.js";
import {
setupGatewaySessionsTestHarness,
bootstrapCacheMocks,
@@ -410,6 +410,68 @@ test("sessions.create with emitCommandHooks=true emits reset lifecycle hooks aga
);
});
test("sessions.create with emitCommandHooks=true resets parent in place when session.dmScope is 'main' (#77434)", async () => {
const { dir } = await createSessionStoreDir();
const transcriptPath = path.join(dir, "sess-parent-dms.jsonl");
await fs.writeFile(
transcriptPath,
`${JSON.stringify({
type: "message",
id: "m1",
message: { role: "user", content: "hello before /new" },
})}\n`,
"utf-8",
);
testState.sessionConfig = { dmScope: "main" };
try {
await writeSessionStore({
entries: {
main: {
sessionId: "sess-parent-dms",
sessionFile: transcriptPath,
updatedAt: Date.now(),
},
},
});
const result = await directSessionReq<{
ok: boolean;
key: string;
sessionId: string;
runStarted: boolean;
}>("sessions.create", {
parentSessionKey: "main",
emitCommandHooks: true,
});
expect(result.ok).toBe(true);
// Reset-in-place: response key matches the parent main key, NOT a dashboard child.
expect(result.payload?.key).toBe("agent:main:main");
expect(result.payload?.runStarted).toBe(false);
expect(result.payload?.sessionId).not.toBe("sess-parent-dms");
expect(sessionLifecycleHookMocks.runSessionEnd).toHaveBeenCalledTimes(1);
expect(sessionLifecycleHookMocks.runSessionStart).toHaveBeenCalledTimes(1);
const [endEvent] = (
sessionLifecycleHookMocks.runSessionEnd.mock.calls as unknown as Array<[unknown, unknown]>
)[0] ?? [undefined, undefined];
const [startEvent] = (
sessionLifecycleHookMocks.runSessionStart.mock.calls as unknown as Array<[unknown, unknown]>
)[0] ?? [undefined, undefined];
expect(endEvent).toMatchObject({
sessionId: "sess-parent-dms",
sessionKey: "agent:main:main",
reason: "new",
});
expect(startEvent).toMatchObject({
sessionKey: "agent:main:main",
resumedFrom: "sess-parent-dms",
});
} finally {
testState.sessionConfig = undefined;
}
});
test("sessions.create without emitCommandHooks does not fire command:new hook (#76957)", async () => {
const { dir } = await createSessionStoreDir();
await writeSingleLineSession(dir, "sess-parent2", "hello from parent 2");