mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 09:48:39 +00:00
fix(hooks): propagate ephemeral sessionId through embedded tool contexts (#32273)
* fix(plugins): expose ephemeral sessionId in tool contexts for per-conversation isolation The plugin tool context (`OpenClawPluginToolContext`) and tool hook context (`PluginHookToolContext`) only provided `sessionKey`, which is a durable channel identifier that survives /new and /reset. Plugins like mem0 that need per-conversation isolation (e.g. mapping Mem0 `run_id`) had no way to distinguish between conversations, causing session-scoped memories to persist unbounded across resets. Add `sessionId` (ephemeral UUID regenerated on /new and /reset) to: - `OpenClawPluginToolContext` (factory context for plugin tools) - `PluginHookToolContext` (before_tool_call / after_tool_call hooks) - Internal `HookContext` for tool call wrappers Thread the value from the run attempt through createOpenClawCodingTools → createOpenClawTools → resolvePluginTools and through the tool hook wrapper. Closes #31253 Made-with: Cursor * fix(agents): propagate embedded sessionId through tool hook context * test(hooks): cover sessionId in embedded tool hook contexts * docs(changelog): add sessionId hook context follow-up note * test(hooks): avoid toolCallId collision in after_tool_call e2e --------- Co-authored-by: SidQin-cyber <sidqin0410@gmail.com>
This commit is contained in:
@@ -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.
|
- 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.
|
- 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/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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ export function createOpenClawTools(options?: {
|
|||||||
requesterSenderId?: string | null;
|
requesterSenderId?: string | null;
|
||||||
/** Whether the requesting sender is an owner. */
|
/** Whether the requesting sender is an owner. */
|
||||||
senderIsOwner?: boolean;
|
senderIsOwner?: boolean;
|
||||||
|
/** Ephemeral session UUID — regenerated on /new and /reset. */
|
||||||
|
sessionId?: string;
|
||||||
}): AnyAgentTool[] {
|
}): AnyAgentTool[] {
|
||||||
const workspaceDir = resolveWorkspaceRoot(options?.workspaceDir);
|
const workspaceDir = resolveWorkspaceRoot(options?.workspaceDir);
|
||||||
const imageTool = options?.agentDir?.trim()
|
const imageTool = options?.agentDir?.trim()
|
||||||
@@ -199,6 +201,7 @@ export function createOpenClawTools(options?: {
|
|||||||
config: options?.config,
|
config: options?.config,
|
||||||
}),
|
}),
|
||||||
sessionKey: options?.agentSessionKey,
|
sessionKey: options?.agentSessionKey,
|
||||||
|
sessionId: options?.sessionId,
|
||||||
messageChannel: options?.agentChannel,
|
messageChannel: options?.agentChannel,
|
||||||
agentAccountId: options?.agentAccountId,
|
agentAccountId: options?.agentAccountId,
|
||||||
requesterSenderId: options?.requesterSenderId ?? undefined,
|
requesterSenderId: options?.requesterSenderId ?? undefined,
|
||||||
|
|||||||
@@ -370,6 +370,7 @@ export async function compactEmbeddedPiSessionDirect(
|
|||||||
messageProvider: params.messageChannel ?? params.messageProvider,
|
messageProvider: params.messageChannel ?? params.messageProvider,
|
||||||
agentAccountId: params.agentAccountId,
|
agentAccountId: params.agentAccountId,
|
||||||
sessionKey: sandboxSessionKey,
|
sessionKey: sandboxSessionKey,
|
||||||
|
sessionId: params.sessionId,
|
||||||
groupId: params.groupId,
|
groupId: params.groupId,
|
||||||
groupChannel: params.groupChannel,
|
groupChannel: params.groupChannel,
|
||||||
groupSpace: params.groupSpace,
|
groupSpace: params.groupSpace,
|
||||||
|
|||||||
@@ -585,6 +585,7 @@ export async function runEmbeddedAttempt(
|
|||||||
senderE164: params.senderE164,
|
senderE164: params.senderE164,
|
||||||
senderIsOwner: params.senderIsOwner,
|
senderIsOwner: params.senderIsOwner,
|
||||||
sessionKey: sandboxSessionKey,
|
sessionKey: sandboxSessionKey,
|
||||||
|
sessionId: params.sessionId,
|
||||||
agentDir,
|
agentDir,
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
@@ -858,7 +859,8 @@ export async function runEmbeddedAttempt(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
agentId: sessionAgentId,
|
agentId: sessionAgentId,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: sandboxSessionKey,
|
||||||
|
sessionId: params.sessionId,
|
||||||
loopDetection: clientToolLoopDetection,
|
loopDetection: clientToolLoopDetection,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1186,6 +1188,7 @@ export async function runEmbeddedAttempt(
|
|||||||
enforceFinalTag: params.enforceFinalTag,
|
enforceFinalTag: params.enforceFinalTag,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
sessionKey: sandboxSessionKey,
|
sessionKey: sandboxSessionKey,
|
||||||
|
sessionId: params.sessionId,
|
||||||
agentId: sessionAgentId,
|
agentId: sessionAgentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -433,6 +433,7 @@ export async function handleToolExecutionEnd(
|
|||||||
toolName,
|
toolName,
|
||||||
agentId: ctx.params.agentId,
|
agentId: ctx.params.agentId,
|
||||||
sessionKey: ctx.params.sessionKey,
|
sessionKey: ctx.params.sessionKey,
|
||||||
|
sessionId: ctx.params.sessionId,
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
ctx.log.warn(`after_tool_call hook failed: tool=${toolName} error=${String(err)}`);
|
ctx.log.warn(`after_tool_call hook failed: tool=${toolName} error=${String(err)}`);
|
||||||
|
|||||||
@@ -132,7 +132,13 @@ export type EmbeddedPiSubscribeContext = {
|
|||||||
*/
|
*/
|
||||||
export type ToolHandlerParams = Pick<
|
export type ToolHandlerParams = Pick<
|
||||||
SubscribeEmbeddedPiSessionParams,
|
SubscribeEmbeddedPiSessionParams,
|
||||||
"runId" | "onBlockReplyFlush" | "onAgentEvent" | "onToolResult" | "sessionKey" | "agentId"
|
| "runId"
|
||||||
|
| "onBlockReplyFlush"
|
||||||
|
| "onAgentEvent"
|
||||||
|
| "onToolResult"
|
||||||
|
| "sessionKey"
|
||||||
|
| "sessionId"
|
||||||
|
| "agentId"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type ToolHandlerState = Pick<
|
export type ToolHandlerState = Pick<
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export type SubscribeEmbeddedPiSessionParams = {
|
|||||||
enforceFinalTag?: boolean;
|
enforceFinalTag?: boolean;
|
||||||
config?: OpenClawConfig;
|
config?: OpenClawConfig;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
|
/** Ephemeral session UUID — regenerated on /new and /reset. */
|
||||||
|
sessionId?: string;
|
||||||
/** Agent identity for hook context — resolved from session config in attempt.ts. */
|
/** Agent identity for hook context — resolved from session config in attempt.ts. */
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ describe("before_tool_call hook integration", () => {
|
|||||||
const tool = wrapToolWithBeforeToolCallHook({ name: "ReAd", execute } as any, {
|
const tool = wrapToolWithBeforeToolCallHook({ name: "ReAd", execute } as any, {
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
sessionKey: "main",
|
sessionKey: "main",
|
||||||
|
sessionId: "ephemeral-main",
|
||||||
});
|
});
|
||||||
const extensionContext = {} as Parameters<typeof tool.execute>[3];
|
const extensionContext = {} as Parameters<typeof tool.execute>[3];
|
||||||
|
|
||||||
@@ -136,6 +137,7 @@ describe("before_tool_call hook integration", () => {
|
|||||||
toolName: "read",
|
toolName: "read",
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
sessionKey: "main",
|
sessionKey: "main",
|
||||||
|
sessionId: "ephemeral-main",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type { AnyAgentTool } from "./tools/common.js";
|
|||||||
export type HookContext = {
|
export type HookContext = {
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
|
/** Ephemeral session UUID — regenerated on /new and /reset. */
|
||||||
|
sessionId?: string;
|
||||||
loopDetection?: ToolLoopDetectionConfig;
|
loopDetection?: ToolLoopDetectionConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,6 +150,7 @@ export async function runBeforeToolCallHook(args: {
|
|||||||
toolName,
|
toolName,
|
||||||
agentId: args.ctx?.agentId,
|
agentId: args.ctx?.agentId,
|
||||||
sessionKey: args.ctx?.sessionKey,
|
sessionKey: args.ctx?.sessionKey,
|
||||||
|
sessionId: args.ctx?.sessionId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ export function createOpenClawCodingTools(options?: {
|
|||||||
messageThreadId?: string | number;
|
messageThreadId?: string | number;
|
||||||
sandbox?: SandboxContext | null;
|
sandbox?: SandboxContext | null;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
|
/** Ephemeral session UUID — regenerated on /new and /reset. */
|
||||||
|
sessionId?: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
config?: OpenClawConfig;
|
config?: OpenClawConfig;
|
||||||
@@ -493,6 +495,7 @@ export function createOpenClawCodingTools(options?: {
|
|||||||
requesterAgentIdOverride: agentId,
|
requesterAgentIdOverride: agentId,
|
||||||
requesterSenderId: options?.senderId,
|
requesterSenderId: options?.senderId,
|
||||||
senderIsOwner: options?.senderIsOwner,
|
senderIsOwner: options?.senderIsOwner,
|
||||||
|
sessionId: options?.sessionId,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
const toolsForMessageProvider = applyMessageProviderToolPolicy(tools, options?.messageProvider);
|
const toolsForMessageProvider = applyMessageProviderToolPolicy(tools, options?.messageProvider);
|
||||||
@@ -533,6 +536,7 @@ export function createOpenClawCodingTools(options?: {
|
|||||||
wrapToolWithBeforeToolCallHook(tool, {
|
wrapToolWithBeforeToolCallHook(tool, {
|
||||||
agentId,
|
agentId,
|
||||||
sessionKey: options?.sessionKey,
|
sessionKey: options?.sessionKey,
|
||||||
|
sessionId: options?.sessionId,
|
||||||
loopDetection: resolveToolLoopDetectionConfig({ cfg: options?.config, agentId }),
|
loopDetection: resolveToolLoopDetectionConfig({ cfg: options?.config, agentId }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ export type OpenClawPluginToolContext = {
|
|||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
|
/** Ephemeral session UUID — regenerated on /new and /reset. Use for per-conversation isolation. */
|
||||||
|
sessionId?: string;
|
||||||
messageChannel?: string;
|
messageChannel?: string;
|
||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
/** Trusted sender id from inbound context (runtime-provided, not tool args). */
|
/** Trusted sender id from inbound context (runtime-provided, not tool args). */
|
||||||
@@ -482,6 +484,8 @@ export type PluginHookMessageSentEvent = {
|
|||||||
export type PluginHookToolContext = {
|
export type PluginHookToolContext = {
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
|
/** Ephemeral session UUID — regenerated on /new and /reset. */
|
||||||
|
sessionId?: string;
|
||||||
toolName: string;
|
toolName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ vi.mock("../infra/agent-events.js", () => ({
|
|||||||
function createToolHandlerCtx(params: {
|
function createToolHandlerCtx(params: {
|
||||||
runId: string;
|
runId: string;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
|
sessionId?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
onBlockReplyFlush?: unknown;
|
onBlockReplyFlush?: unknown;
|
||||||
}) {
|
}) {
|
||||||
@@ -32,6 +33,7 @@ function createToolHandlerCtx(params: {
|
|||||||
session: { messages: [] },
|
session: { messages: [] },
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
|
sessionId: params.sessionId,
|
||||||
onBlockReplyFlush: params.onBlockReplyFlush,
|
onBlockReplyFlush: params.onBlockReplyFlush,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
@@ -83,6 +85,7 @@ describe("after_tool_call hook wiring", () => {
|
|||||||
runId: "test-run-1",
|
runId: "test-run-1",
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
sessionKey: "test-session",
|
sessionKey: "test-session",
|
||||||
|
sessionId: "test-ephemeral-session",
|
||||||
});
|
});
|
||||||
|
|
||||||
await handleToolExecutionStart(
|
await handleToolExecutionStart(
|
||||||
@@ -90,7 +93,7 @@ describe("after_tool_call hook wiring", () => {
|
|||||||
{
|
{
|
||||||
type: "tool_execution_start",
|
type: "tool_execution_start",
|
||||||
toolName: "read",
|
toolName: "read",
|
||||||
toolCallId: "call-1",
|
toolCallId: "wired-hook-call-1",
|
||||||
args: { path: "/tmp/file.txt" },
|
args: { path: "/tmp/file.txt" },
|
||||||
} as never,
|
} as never,
|
||||||
);
|
);
|
||||||
@@ -100,7 +103,7 @@ describe("after_tool_call hook wiring", () => {
|
|||||||
{
|
{
|
||||||
type: "tool_execution_end",
|
type: "tool_execution_end",
|
||||||
toolName: "read",
|
toolName: "read",
|
||||||
toolCallId: "call-1",
|
toolCallId: "wired-hook-call-1",
|
||||||
isError: false,
|
isError: false,
|
||||||
result: { content: [{ type: "text", text: "file contents" }] },
|
result: { content: [{ type: "text", text: "file contents" }] },
|
||||||
} as never,
|
} as never,
|
||||||
@@ -115,7 +118,7 @@ describe("after_tool_call hook wiring", () => {
|
|||||||
| { toolName?: string; params?: unknown; error?: unknown; durationMs?: unknown }
|
| { toolName?: string; params?: unknown; error?: unknown; durationMs?: unknown }
|
||||||
| undefined;
|
| undefined;
|
||||||
const context = firstCall?.[1] as
|
const context = firstCall?.[1] as
|
||||||
| { toolName?: string; agentId?: string; sessionKey?: string }
|
| { toolName?: string; agentId?: string; sessionKey?: string; sessionId?: string }
|
||||||
| undefined;
|
| undefined;
|
||||||
expect(event).toBeDefined();
|
expect(event).toBeDefined();
|
||||||
expect(context).toBeDefined();
|
expect(context).toBeDefined();
|
||||||
@@ -129,6 +132,7 @@ describe("after_tool_call hook wiring", () => {
|
|||||||
expect(context.toolName).toBe("read");
|
expect(context.toolName).toBe("read");
|
||||||
expect(context.agentId).toBe("main");
|
expect(context.agentId).toBe("main");
|
||||||
expect(context.sessionKey).toBe("test-session");
|
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 () => {
|
it("includes error in after_tool_call event on tool failure", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user