diff --git a/CHANGELOG.md b/CHANGELOG.md index d9b17ee6448..aa467af2ccd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai - Exec approvals/allowlist matching: escape regex metacharacters in path-pattern literals (while preserving glob wildcards), preventing crashes on allowlisted executables like `/usr/bin/g++` and correctly matching mixed wildcard/literal token paths. (#32162) Thanks @stakeswky. - Agents/tool-result guard: always clear pending tool-call state on interruptions even when synthetic tool results are disabled, preventing orphaned tool-use transcripts that cause follow-up provider request failures. (#32120) Thanks @jnMetaCode. - Hooks/after_tool_call: include embedded session context (`sessionKey`, `agentId`) and fire the hook exactly once per tool execution by removing duplicate adapter-path dispatch in embedded runs. (#32201) Thanks @jbeno, @scoootscooob, @vincentkoc. +- Hooks/session-scoped memory context: expose ephemeral `sessionId` in embedded plugin tool contexts and `before_tool_call`/`after_tool_call` hook contexts (including compaction and client-tool wiring) so plugins can isolate per-conversation state across `/new` and `/reset`. Related #31253 and #31304. Thanks @Sid-Qin and @Servo-AIpex. - Webchat/stream finalization: persist streamed assistant text when final events omit `message`, while keeping final payload precedence and skipping empty stream buffers to prevent disappearing replies after tool turns. (#31920) Thanks @Sid-Qin. - Cron/store migration: normalize legacy cron jobs with string `schedule` and top-level `command`/`timeout` fields into canonical schedule/payload/session-target shape on load, preventing schedule-error loops on old persisted stores. (#31926) Thanks @bmendonca3. - Gateway/Heartbeat model reload: treat `models.*` and `agents.defaults.model` config updates as heartbeat hot-reload triggers so heartbeat picks up model changes without a full gateway restart. (#32046) Thanks @stakeswky. diff --git a/src/agents/openclaw-tools.plugin-context.test.ts b/src/agents/openclaw-tools.plugin-context.test.ts index ea2898476ad..1cf9116a98e 100644 --- a/src/agents/openclaw-tools.plugin-context.test.ts +++ b/src/agents/openclaw-tools.plugin-context.test.ts @@ -30,4 +30,21 @@ describe("createOpenClawTools plugin context", () => { }), ); }); + + it("forwards ephemeral sessionId to plugin tool context", () => { + createOpenClawTools({ + config: {} as never, + agentSessionKey: "agent:main:telegram:direct:12345", + sessionId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + }); + + expect(resolvePluginToolsMock).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + sessionKey: "agent:main:telegram:direct:12345", + sessionId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + }), + }), + ); + }); }); diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index f0f91a27148..cbd9b7b4140 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -70,6 +70,8 @@ export function createOpenClawTools(options?: { requesterSenderId?: string | null; /** Whether the requesting sender is an owner. */ senderIsOwner?: boolean; + /** Ephemeral session UUID — regenerated on /new and /reset. */ + sessionId?: string; }): AnyAgentTool[] { const workspaceDir = resolveWorkspaceRoot(options?.workspaceDir); const imageTool = options?.agentDir?.trim() @@ -199,6 +201,7 @@ export function createOpenClawTools(options?: { config: options?.config, }), sessionKey: options?.agentSessionKey, + sessionId: options?.sessionId, messageChannel: options?.agentChannel, agentAccountId: options?.agentAccountId, requesterSenderId: options?.requesterSenderId ?? undefined, diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index a6be0ca47d0..361d5f8593a 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -370,6 +370,7 @@ export async function compactEmbeddedPiSessionDirect( messageProvider: params.messageChannel ?? params.messageProvider, agentAccountId: params.agentAccountId, sessionKey: sandboxSessionKey, + sessionId: params.sessionId, groupId: params.groupId, groupChannel: params.groupChannel, groupSpace: params.groupSpace, diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index fa2b508f15c..5acd5cdaaab 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -585,6 +585,7 @@ export async function runEmbeddedAttempt( senderE164: params.senderE164, senderIsOwner: params.senderIsOwner, sessionKey: sandboxSessionKey, + sessionId: params.sessionId, agentDir, workspaceDir: effectiveWorkspace, config: params.config, @@ -858,7 +859,8 @@ export async function runEmbeddedAttempt( }, { agentId: sessionAgentId, - sessionKey: params.sessionKey, + sessionKey: sandboxSessionKey, + sessionId: params.sessionId, loopDetection: clientToolLoopDetection, }, ) @@ -1186,6 +1188,7 @@ export async function runEmbeddedAttempt( enforceFinalTag: params.enforceFinalTag, config: params.config, sessionKey: sandboxSessionKey, + sessionId: params.sessionId, agentId: sessionAgentId, }); diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.ts b/src/agents/pi-embedded-subscribe.handlers.tools.ts index 4a76f62ff62..87e165c9c77 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.ts +++ b/src/agents/pi-embedded-subscribe.handlers.tools.ts @@ -433,6 +433,7 @@ export async function handleToolExecutionEnd( toolName, agentId: ctx.params.agentId, sessionKey: ctx.params.sessionKey, + sessionId: ctx.params.sessionId, }) .catch((err) => { ctx.log.warn(`after_tool_call hook failed: tool=${toolName} error=${String(err)}`); diff --git a/src/agents/pi-embedded-subscribe.handlers.types.ts b/src/agents/pi-embedded-subscribe.handlers.types.ts index d7488d767ad..1a9d48f46f0 100644 --- a/src/agents/pi-embedded-subscribe.handlers.types.ts +++ b/src/agents/pi-embedded-subscribe.handlers.types.ts @@ -132,7 +132,13 @@ export type EmbeddedPiSubscribeContext = { */ export type ToolHandlerParams = Pick< SubscribeEmbeddedPiSessionParams, - "runId" | "onBlockReplyFlush" | "onAgentEvent" | "onToolResult" | "sessionKey" | "agentId" + | "runId" + | "onBlockReplyFlush" + | "onAgentEvent" + | "onToolResult" + | "sessionKey" + | "sessionId" + | "agentId" >; export type ToolHandlerState = Pick< diff --git a/src/agents/pi-embedded-subscribe.types.ts b/src/agents/pi-embedded-subscribe.types.ts index 426daf2fd15..689cd49998e 100644 --- a/src/agents/pi-embedded-subscribe.types.ts +++ b/src/agents/pi-embedded-subscribe.types.ts @@ -31,6 +31,8 @@ export type SubscribeEmbeddedPiSessionParams = { enforceFinalTag?: boolean; config?: OpenClawConfig; sessionKey?: string; + /** Ephemeral session UUID — regenerated on /new and /reset. */ + sessionId?: string; /** Agent identity for hook context — resolved from session config in attempt.ts. */ agentId?: string; }; diff --git a/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts b/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts index 643a14b0338..ec1d8c5d4f1 100644 --- a/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts +++ b/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts @@ -122,6 +122,7 @@ describe("before_tool_call hook integration", () => { const tool = wrapToolWithBeforeToolCallHook({ name: "ReAd", execute } as any, { agentId: "main", sessionKey: "main", + sessionId: "ephemeral-main", }); const extensionContext = {} as Parameters[3]; @@ -136,6 +137,7 @@ describe("before_tool_call hook integration", () => { toolName: "read", agentId: "main", sessionKey: "main", + sessionId: "ephemeral-main", }, ); }); diff --git a/src/agents/pi-tools.before-tool-call.ts b/src/agents/pi-tools.before-tool-call.ts index a0a5ca4cb11..45e48df02eb 100644 --- a/src/agents/pi-tools.before-tool-call.ts +++ b/src/agents/pi-tools.before-tool-call.ts @@ -9,6 +9,8 @@ import type { AnyAgentTool } from "./tools/common.js"; export type HookContext = { agentId?: string; sessionKey?: string; + /** Ephemeral session UUID — regenerated on /new and /reset. */ + sessionId?: string; loopDetection?: ToolLoopDetectionConfig; }; @@ -148,6 +150,7 @@ export async function runBeforeToolCallHook(args: { toolName, agentId: args.ctx?.agentId, sessionKey: args.ctx?.sessionKey, + sessionId: args.ctx?.sessionId, }, ); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index b5e9276b7fc..3f038e9aadf 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -188,6 +188,8 @@ export function createOpenClawCodingTools(options?: { messageThreadId?: string | number; sandbox?: SandboxContext | null; sessionKey?: string; + /** Ephemeral session UUID — regenerated on /new and /reset. */ + sessionId?: string; agentDir?: string; workspaceDir?: string; config?: OpenClawConfig; @@ -493,6 +495,7 @@ export function createOpenClawCodingTools(options?: { requesterAgentIdOverride: agentId, requesterSenderId: options?.senderId, senderIsOwner: options?.senderIsOwner, + sessionId: options?.sessionId, }), ]; const toolsForMessageProvider = applyMessageProviderToolPolicy(tools, options?.messageProvider); @@ -533,6 +536,7 @@ export function createOpenClawCodingTools(options?: { wrapToolWithBeforeToolCallHook(tool, { agentId, sessionKey: options?.sessionKey, + sessionId: options?.sessionId, loopDetection: resolveToolLoopDetectionConfig({ cfg: options?.config, agentId }), }), ); diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 50ad451fd5e..96fd41f555e 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -61,6 +61,8 @@ export type OpenClawPluginToolContext = { agentDir?: string; agentId?: string; sessionKey?: string; + /** Ephemeral session UUID — regenerated on /new and /reset. Use for per-conversation isolation. */ + sessionId?: string; messageChannel?: string; agentAccountId?: string; /** Trusted sender id from inbound context (runtime-provided, not tool args). */ @@ -482,6 +484,8 @@ export type PluginHookMessageSentEvent = { export type PluginHookToolContext = { agentId?: string; sessionKey?: string; + /** Ephemeral session UUID — regenerated on /new and /reset. */ + sessionId?: string; toolName: string; }; diff --git a/src/plugins/wired-hooks-after-tool-call.e2e.test.ts b/src/plugins/wired-hooks-after-tool-call.e2e.test.ts index 11d073e8356..a84c1ad492e 100644 --- a/src/plugins/wired-hooks-after-tool-call.e2e.test.ts +++ b/src/plugins/wired-hooks-after-tool-call.e2e.test.ts @@ -23,6 +23,7 @@ vi.mock("../infra/agent-events.js", () => ({ function createToolHandlerCtx(params: { runId: string; sessionKey?: string; + sessionId?: string; agentId?: string; onBlockReplyFlush?: unknown; }) { @@ -32,6 +33,7 @@ function createToolHandlerCtx(params: { session: { messages: [] }, agentId: params.agentId, sessionKey: params.sessionKey, + sessionId: params.sessionId, onBlockReplyFlush: params.onBlockReplyFlush, }, state: { @@ -83,6 +85,7 @@ describe("after_tool_call hook wiring", () => { runId: "test-run-1", agentId: "main", sessionKey: "test-session", + sessionId: "test-ephemeral-session", }); await handleToolExecutionStart( @@ -90,7 +93,7 @@ describe("after_tool_call hook wiring", () => { { type: "tool_execution_start", toolName: "read", - toolCallId: "call-1", + toolCallId: "wired-hook-call-1", args: { path: "/tmp/file.txt" }, } as never, ); @@ -100,7 +103,7 @@ describe("after_tool_call hook wiring", () => { { type: "tool_execution_end", toolName: "read", - toolCallId: "call-1", + toolCallId: "wired-hook-call-1", isError: false, result: { content: [{ type: "text", text: "file contents" }] }, } as never, @@ -115,7 +118,7 @@ describe("after_tool_call hook wiring", () => { | { toolName?: string; params?: unknown; error?: unknown; durationMs?: unknown } | undefined; const context = firstCall?.[1] as - | { toolName?: string; agentId?: string; sessionKey?: string } + | { toolName?: string; agentId?: string; sessionKey?: string; sessionId?: string } | undefined; expect(event).toBeDefined(); expect(context).toBeDefined(); @@ -129,6 +132,7 @@ describe("after_tool_call hook wiring", () => { expect(context.toolName).toBe("read"); expect(context.agentId).toBe("main"); expect(context.sessionKey).toBe("test-session"); + expect(context.sessionId).toBe("test-ephemeral-session"); }); it("includes error in after_tool_call event on tool failure", async () => {