mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 07:20:42 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user