Files
openclaw/src/agents/pi-embedded-subscribe.handlers.types.ts
Vincent Koc 0954b6bf5f 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>
2026-03-02 15:11:51 -08:00

177 lines
5.6 KiB
TypeScript

import type { AgentEvent, AgentMessage } from "@mariozechner/pi-agent-core";
import type { ReplyDirectiveParseResult } from "../auto-reply/reply/reply-directives.js";
import type { ReasoningLevel } from "../auto-reply/thinking.js";
import type { InlineCodeState } from "../markdown/code-spans.js";
import type { HookRunner } from "../plugins/hooks.js";
import type { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
import type { MessagingToolSend } from "./pi-embedded-messaging.js";
import type {
BlockReplyChunking,
SubscribeEmbeddedPiSessionParams,
} from "./pi-embedded-subscribe.types.js";
import type { NormalizedUsage } from "./usage.js";
export type EmbeddedSubscribeLogger = {
debug: (message: string) => void;
warn: (message: string) => void;
};
export type ToolErrorSummary = {
toolName: string;
meta?: string;
error?: string;
mutatingAction?: boolean;
actionFingerprint?: string;
};
export type ToolCallSummary = {
meta?: string;
mutatingAction: boolean;
actionFingerprint?: string;
};
export type EmbeddedPiSubscribeState = {
assistantTexts: string[];
toolMetas: Array<{ toolName?: string; meta?: string }>;
toolMetaById: Map<string, ToolCallSummary>;
toolSummaryById: Set<string>;
lastToolError?: ToolErrorSummary;
blockReplyBreak: "text_end" | "message_end";
reasoningMode: ReasoningLevel;
includeReasoning: boolean;
shouldEmitPartialReplies: boolean;
streamReasoning: boolean;
deltaBuffer: string;
blockBuffer: string;
blockState: { thinking: boolean; final: boolean; inlineCode: InlineCodeState };
partialBlockState: { thinking: boolean; final: boolean; inlineCode: InlineCodeState };
lastStreamedAssistant?: string;
lastStreamedAssistantCleaned?: string;
emittedAssistantUpdate: boolean;
lastStreamedReasoning?: string;
lastBlockReplyText?: string;
reasoningStreamOpen: boolean;
assistantMessageIndex: number;
lastAssistantTextMessageIndex: number;
lastAssistantTextNormalized?: string;
lastAssistantTextTrimmed?: string;
assistantTextBaseline: number;
suppressBlockChunks: boolean;
lastReasoningSent?: string;
compactionInFlight: boolean;
pendingCompactionRetry: number;
compactionRetryResolve?: () => void;
compactionRetryReject?: (reason?: unknown) => void;
compactionRetryPromise: Promise<void> | null;
unsubscribed: boolean;
messagingToolSentTexts: string[];
messagingToolSentTextsNormalized: string[];
messagingToolSentTargets: MessagingToolSend[];
messagingToolSentMediaUrls: string[];
pendingMessagingTexts: Map<string, string>;
pendingMessagingTargets: Map<string, MessagingToolSend>;
successfulCronAdds: number;
pendingMessagingMediaUrls: Map<string, string[]>;
lastAssistant?: AgentMessage;
};
export type EmbeddedPiSubscribeContext = {
params: SubscribeEmbeddedPiSessionParams;
state: EmbeddedPiSubscribeState;
log: EmbeddedSubscribeLogger;
blockChunking?: BlockReplyChunking;
blockChunker: EmbeddedBlockChunker | null;
hookRunner?: HookRunner;
noteLastAssistant: (msg: AgentMessage) => void;
shouldEmitToolResult: () => boolean;
shouldEmitToolOutput: () => boolean;
emitToolSummary: (toolName?: string, meta?: string) => void;
emitToolOutput: (toolName?: string, meta?: string, output?: string) => void;
stripBlockTags: (
text: string,
state: { thinking: boolean; final: boolean; inlineCode?: InlineCodeState },
) => string;
emitBlockChunk: (text: string) => void;
flushBlockReplyBuffer: () => void;
emitReasoningStream: (text: string) => void;
consumeReplyDirectives: (
text: string,
options?: { final?: boolean },
) => ReplyDirectiveParseResult | null;
consumePartialReplyDirectives: (
text: string,
options?: { final?: boolean },
) => ReplyDirectiveParseResult | null;
resetAssistantMessageState: (nextAssistantTextBaseline: number) => void;
resetForCompactionRetry: () => void;
finalizeAssistantTexts: (args: {
text: string;
addedDuringMessage: boolean;
chunkerHasBuffered: boolean;
}) => void;
trimMessagingToolSent: () => void;
ensureCompactionPromise: () => void;
noteCompactionRetry: () => void;
resolveCompactionRetry: () => void;
maybeResolveCompactionWait: () => void;
recordAssistantUsage: (usage: unknown) => void;
incrementCompactionCount: () => void;
getUsageTotals: () => NormalizedUsage | undefined;
getCompactionCount: () => number;
};
/**
* Minimal context type for tool execution handlers. Allows
* tests provide only the fields they exercise
* without needing the full `EmbeddedPiSubscribeContext`.
*/
export type ToolHandlerParams = Pick<
SubscribeEmbeddedPiSessionParams,
| "runId"
| "onBlockReplyFlush"
| "onAgentEvent"
| "onToolResult"
| "sessionKey"
| "sessionId"
| "agentId"
>;
export type ToolHandlerState = Pick<
EmbeddedPiSubscribeState,
| "toolMetaById"
| "toolMetas"
| "toolSummaryById"
| "lastToolError"
| "pendingMessagingTargets"
| "pendingMessagingTexts"
| "pendingMessagingMediaUrls"
| "messagingToolSentTexts"
| "messagingToolSentTextsNormalized"
| "messagingToolSentMediaUrls"
| "messagingToolSentTargets"
| "successfulCronAdds"
>;
export type ToolHandlerContext = {
params: ToolHandlerParams;
state: ToolHandlerState;
log: EmbeddedSubscribeLogger;
hookRunner?: HookRunner;
flushBlockReplyBuffer: () => void;
shouldEmitToolResult: () => boolean;
shouldEmitToolOutput: () => boolean;
emitToolSummary: (toolName?: string, meta?: string) => void;
emitToolOutput: (toolName?: string, meta?: string, output?: string) => void;
trimMessagingToolSent: () => void;
};
export type EmbeddedPiSubscribeEvent =
| AgentEvent
| { type: string; [k: string]: unknown }
| { type: "message_start"; message: AgentMessage };