From 514fa32f3d87f7c5f1fb799a21fa90a683787f04 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 12 Aug 2026 00:50:53 -0700 Subject: [PATCH] fix: repair release delivery and live channel QA (#122529) Backport the exact-delivery claim promotion repair and its prerequisite evidence preservation from main. Restore Buzz canary eligibility and let the Matrix QA scenario request raw progress command text without changing the privacy-safe product default. The Telegram workflow-only repair remains on main and is intentionally not backported. Co-authored-by: Vincent Koc Co-authored-by: Ayaan Zaidi --- docs/.generated/plugin-sdk-api-baseline.jsonl | 34 +++--- .../matrix/substrate/config.test.ts | 13 ++- .../matrix/substrate/config.ts | 31 ++++-- .../shared/scenario-selection.test.ts | 3 +- .../src/scenario-catalog-channels.test.ts | 27 ++++- qa/scenarios/channels/channel-canary.yaml | 1 + ...rix-room-tool-progress-mention-safety.yaml | 5 +- src/infra/delivery-queue-sqlite-claim.test.ts | 87 +++++++++++++++ src/infra/delivery-queue-sqlite-claim.ts | 69 ++++++++++-- ...e.exact-reconciliation.integration.test.ts | 101 ++++++++++++++++++ .../outbound/delivery-queue-platform-lease.ts | 36 +++++++ src/infra/outbound/delivery-queue-storage.ts | 31 ++---- .../outbound/delivery-queue.storage.test.ts | 19 ++++ 13 files changed, 393 insertions(+), 64 deletions(-) create mode 100644 src/infra/delivery-queue-sqlite-claim.test.ts create mode 100644 src/infra/outbound/deliver-queue.exact-reconciliation.integration.test.ts diff --git a/docs/.generated/plugin-sdk-api-baseline.jsonl b/docs/.generated/plugin-sdk-api-baseline.jsonl index 6a6076e0e4a7..1165b7091da6 100644 --- a/docs/.generated/plugin-sdk-api-baseline.jsonl +++ b/docs/.generated/plugin-sdk-api-baseline.jsonl @@ -53,7 +53,7 @@ {"closureHash":"ff3c4616cd6212a6d698831a8b287ad87e3968c8663f4090d095bb30ec40fc1c","declaration":"export function abortAndDrainAgentHarnessRun(params: { sessionId: string; sessionKey?: string; settleMs?: number; forceClear?: boolean; reason?: string; }): Promise;","entrypoint":"agent-harness","exportName":"abortAndDrainAgentHarnessRun","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"58989e3fa91eecf2e05202e36548e6e64a3b9219e83154bd99866c851604e34e","declaration":"export function createAgentToolResultMiddlewareRunner(ctx: AgentToolResultMiddlewareContext, handlers?: AgentToolResultMiddleware[]): { applyToolResultMiddleware(event: AgentToolResultMiddlewareEvent): Promise; };","entrypoint":"agent-harness","exportName":"createAgentToolResultMiddlewareRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"b8f6355d7ad1700aceecddaa6e9ccf43091a792d5b481a1ee3f55294a881a67d","declaration":"export function createCodexAppServerToolResultExtensionRunner(ctx: CodexAppServerExtensionContext, factories?: CodexAppServerExtensionFactory[]): { applyToolResultExtensions(event: CodexAppServerToolResultEvent): Promise>; };","entrypoint":"agent-harness","exportName":"createCodexAppServerToolResultExtensionRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} -{"closureHash":"82a967232dd50d7c7a42d6a576722af500feba6c6170c808a3297ea36f6a9bec","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} +{"closureHash":"2de11782a252f85f6f89fcd833c30c073e57644a1c583c50823a7f12255003cd","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"13649ee853485319e7449fda25c106b3f3f53d3090e53cdbb28e220a8529eb80","declaration":"export function disposeRegisteredAgentHarnesses(): Promise;","entrypoint":"agent-harness","exportName":"disposeRegisteredAgentHarnesses","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"f255a91162ece4c239bb8fa745efa14a1054bc024a291e9954f75f239b2b87c3","declaration":"export function resolveActiveEmbeddedRunSessionId(sessionKey: string): string | undefined;","entrypoint":"agent-harness","exportName":"resolveActiveEmbeddedRunSessionId","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"27d9f61df9ccf68615cb2002a6da891e36ab0c07623bc37c4d07aa980b446881","declaration":"export function resolveWebSearchToolPolicy(params: WebSearchToolPolicyParams): WebSearchToolPolicyResolution;","entrypoint":"agent-harness","exportName":"resolveWebSearchToolPolicy","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} @@ -1097,7 +1097,7 @@ {"closureHash":"9555a73ba2717bba0bf94888017be4cecef2c51cc3887ed34f8d8ef3c79d6abf","declaration":"export const DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS: number;","entrypoint":"channel-message","exportName":"DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"} {"closureHash":"3a2df3ab656149630c004d2d9f71b8ceda99aa81eb6b72b0cc7b3982ed0ce044","declaration":"export const DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS: 8;","entrypoint":"channel-message","exportName":"DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"} {"closureHash":"38c735a14c65067aa7825390d1c5c8876c651d0603e84980dfaf947f5235747a","declaration":"export const INGRESS_CLAIM_PROCESS_ID: string;","entrypoint":"channel-message","exportName":"INGRESS_CLAIM_PROCESS_ID","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"} -{"closureHash":"f5d6c974980d1792af370a89adca4417174efdc8bf3b70e140fdfe21d1692cca","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-message","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"} +{"closureHash":"7d90d0f9c14318d2f1a6a6c4e525eee5913d6fafdee648144d8f656297934d9c","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-message","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"} {"closureHash":"2c52eff200a5b9bd2d93ee72209c2626bbe165f59b538591db661b94773023b0","declaration":"export type AgentPlanStep = AgentPlanStep;","entrypoint":"channel-message","exportName":"AgentPlanStep","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"} {"closureHash":"1e8bcb2c8d8f5327f5b81b8c217f722f090dccba21ba3943324010f4f80a5a50","declaration":"export type AgentPlanStepStatus = AgentPlanStepStatus;","entrypoint":"channel-message","exportName":"AgentPlanStepStatus","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"} {"closureHash":"b036fc26e5330dfd7da1af8839bb3c23e85283b9e16e252a659dd4bb0c35cd5e","declaration":"export type ChannelDeliveryStreamingConfig = ChannelDeliveryStreamingConfig;","entrypoint":"channel-message","exportName":"ChannelDeliveryStreamingConfig","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"} @@ -1226,7 +1226,7 @@ {"closureHash":"9555a73ba2717bba0bf94888017be4cecef2c51cc3887ed34f8d8ef3c79d6abf","declaration":"export const DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS: number;","entrypoint":"channel-outbound","exportName":"DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"} {"closureHash":"3a2df3ab656149630c004d2d9f71b8ceda99aa81eb6b72b0cc7b3982ed0ce044","declaration":"export const DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS: 8;","entrypoint":"channel-outbound","exportName":"DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"} {"closureHash":"38c735a14c65067aa7825390d1c5c8876c651d0603e84980dfaf947f5235747a","declaration":"export const INGRESS_CLAIM_PROCESS_ID: string;","entrypoint":"channel-outbound","exportName":"INGRESS_CLAIM_PROCESS_ID","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"} -{"closureHash":"f5d6c974980d1792af370a89adca4417174efdc8bf3b70e140fdfe21d1692cca","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-outbound","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"} +{"closureHash":"7d90d0f9c14318d2f1a6a6c4e525eee5913d6fafdee648144d8f656297934d9c","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-outbound","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"} {"closureHash":"2c52eff200a5b9bd2d93ee72209c2626bbe165f59b538591db661b94773023b0","declaration":"export type AgentPlanStep = AgentPlanStep;","entrypoint":"channel-outbound","exportName":"AgentPlanStep","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"} {"closureHash":"1e8bcb2c8d8f5327f5b81b8c217f722f090dccba21ba3943324010f4f80a5a50","declaration":"export type AgentPlanStepStatus = AgentPlanStepStatus;","entrypoint":"channel-outbound","exportName":"AgentPlanStepStatus","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"} {"closureHash":"b036fc26e5330dfd7da1af8839bb3c23e85283b9e16e252a659dd4bb0c35cd5e","declaration":"export type ChannelDeliveryStreamingConfig = ChannelDeliveryStreamingConfig;","entrypoint":"channel-outbound","exportName":"ChannelDeliveryStreamingConfig","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"} @@ -2354,7 +2354,7 @@ {"closureHash":"98e0cc16352b7dfcceb4957f8cd2acfe3c2b3acb223c42a5cfb885e088fcb04c","declaration":"export function resolveInboundReplyDispatchCounts(result: ChannelTurnDispatchResultLike): Record;","entrypoint":"inbound-reply-dispatch","exportName":"resolveInboundReplyDispatchCounts","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} {"closureHash":"9d503d05e408ddd1a1c373e3c4485b1f9acff4c240fdc241840a2cfa8b55c3a7","declaration":"export function runChannelInboundEvent(params: RunChannelTurnParams): Promise>;\nexport function runChannelInboundEvent(params: ChannelInboundEventRunnerParams): Promise>;","entrypoint":"inbound-reply-dispatch","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} {"closureHash":"2c68496a174872b31aaf1ac8fad2789deb79d490c5ffbcb157325f1c95d1b51f","declaration":"export function runPreparedInboundReply(params: PreparedChannelTurn): Promise>;","entrypoint":"inbound-reply-dispatch","exportName":"runPreparedInboundReply","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} -{"closureHash":"f5d6c974980d1792af370a89adca4417174efdc8bf3b70e140fdfe21d1692cca","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"inbound-reply-dispatch","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"const","recordType":"export"} +{"closureHash":"7d90d0f9c14318d2f1a6a6c4e525eee5913d6fafdee648144d8f656297934d9c","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"inbound-reply-dispatch","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"const","recordType":"export"} {"closureHash":"278dc8a410230012ddbc3552ec621a224e6f7c5dd4cd3904c943ac98dd4259e4","declaration":"export type AssembledInboundReply = AssembledChannelTurn;","entrypoint":"inbound-reply-dispatch","exportName":"AssembledInboundReply","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} {"closureHash":"3de0d6d1bb8f9a83bd1c6e57aa8618e3fea71054236cd530c555e348bd0b7702","declaration":"export type ChannelBotLoopProtectionFacts = ChannelBotLoopProtectionFacts;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelBotLoopProtectionFacts","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} {"closureHash":"f0da845bbdf49b30f6718a4b7ff012a0536deb98f65418583555dc90825797ca","declaration":"export type ChannelInboundDroppedHistoryOptions = ChannelTurnDroppedHistoryOptions;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelInboundDroppedHistoryOptions","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} @@ -3189,19 +3189,19 @@ {"closureHash":"50394942e0a454c742e80a4f3c8739c80258b387ade498eb5aed66df887108be","declaration":"export function callMeetingBrowserProxyOnNode(params: { runtime: PluginRuntime; adapter: NodeAdapter; nodeId: string; } & MeetingBrowserRequestParams): Promise;","entrypoint":"meeting-runtime","exportName":"callMeetingBrowserProxyOnNode","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"c5fcd1d157edcb4773235adfaf8051e3ec7ca428cb7916b6d55f2511affab56c","declaration":"export function convertMeetingBridgeAudioForStt(audio: Buffer, audioFormat: MeetingRealtimeAudioFormat): Buffer;","entrypoint":"meeting-runtime","exportName":"convertMeetingBridgeAudioForStt","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"aa9063c27c5ff714d3653571c4a2a71e9d2eea5609b161f03e4738e15411358b","declaration":"export function convertMeetingTtsAudioForBridge(audio: Buffer, sampleRate: number, audioFormat: MeetingRealtimeAudioFormat, outputFormat?: string, platformName?: string): Buffer;","entrypoint":"meeting-runtime","exportName":"convertMeetingTtsAudioForBridge","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"c8f1a9163a69ca61d4f88f8129e5f23719f921e5245e060c7a6c77bfaae8c34a","declaration":"export function createLocalMeetingRealtimeAudioTransport(params: { inputCommand: string[]; outputCommand: string[]; bargeInInputCommand?: string[]; bargeInRmsThreshold: number; bargeInPeakThreshold: number; bargeInCooldownMs: number; logger: RuntimeLogger; logScope: string; audioFormat?: MeetingRealtimeAudioFormat; spawn?: MeetingRealtimeAudioSpawn; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createLocalMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"da67f89aa5cab403a21826f782ae63af7cd135aea65d76e03c10e701ff20f0cb","declaration":"export function createLocalMeetingRealtimeAudioTransport(params: { inputCommand: string[]; outputCommand: string[]; bargeInInputCommand?: string[]; bargeInRmsThreshold: number; bargeInPeakThreshold: number; bargeInCooldownMs: number; logger: RuntimeLogger; logScope: string; audioFormat?: MeetingRealtimeAudioFormat; spawn?: MeetingRealtimeAudioSpawn; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createLocalMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"9cb63eefd2e5a70aea960bf17fcf570ef02a2d7b5afd5ad9451802964c653fcf","declaration":"export function createMeetingBrowserNodeCaller(params: { runtime: PluginRuntime; adapter: NodeAdapter; nodeId: string; }): MeetingBrowserRequestCaller;","entrypoint":"meeting-runtime","exportName":"createMeetingBrowserNodeCaller","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"f244ca8c47f556c9f388953cf9a7581c7d4d413af93e0dfa1462779e6a687a7c","declaration":"export function createMeetingBrowserNodeInvokePolicy(options: MeetingBrowserNodePolicyOptions): OpenClawPluginNodeInvokePolicy;","entrypoint":"meeting-runtime","exportName":"createMeetingBrowserNodeInvokePolicy","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"1aa4e3ff39a2cbe162421c62b1fa4a362973381d4d9ff15c671249b1ebd566bc","declaration":"export function createMeetingNodeHost(options: MeetingNodeHostOptions): { handleCommand(paramsJSON?: string | null): Promise; };","entrypoint":"meeting-runtime","exportName":"createMeetingNodeHost","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"2493d5d6a3b2128c167ae4c42129ff96b2dddaac7c12c87fc442ff304b01ca3a","declaration":"export function createMeetingRealtimeEngineBindings(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { agentId?: string; toolPolicy: RealtimeVoiceAgentConsultToolPolicy; }; }; fullConfig: OpenClawConfig; runtime: PluginRuntime; logger: RuntimeLogger; }): { platform: MeetingRuntimePlatform; consultAgent: (consult: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (call: MeetingRealtimeToolCallParams) => Promise; };","entrypoint":"meeting-runtime","exportName":"createMeetingRealtimeEngineBindings","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"14ac7c607f540bb79a096af8a60844201a0544d4f506311d65f298c32c5d42a8","declaration":"export function createMeetingRealtimeEngineBindings(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { agentId?: string; toolPolicy: RealtimeVoiceAgentConsultToolPolicy; }; }; fullConfig: OpenClawConfig; runtime: PluginRuntime; logger: RuntimeLogger; }): { platform: MeetingRuntimePlatform; consultAgent: (consult: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (call: MeetingRealtimeToolCallParams) => Promise; };","entrypoint":"meeting-runtime","exportName":"createMeetingRealtimeEngineBindings","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"9124db05923060c467edb149bd8abdc8be1d00d00f867cf84600972923a58489","declaration":"export function createMeetingSession(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { provider?: string; voiceProvider?: string; transcriptionProvider?: string; model?: string; toolPolicy: TToolPolicy; }; }; resolved: MeetingResolvedJoin; createdAt: string; }): MeetingSessionRecord;","entrypoint":"meeting-runtime","exportName":"createMeetingSession","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"fb276e07e395ab19177b3d7967fd0a8fcda79130883f15092a4a306b826481b9","declaration":"export function createMeetingSetupStatus(checks: MeetingSetupCheck[]): MeetingSetupStatus;","entrypoint":"meeting-runtime","exportName":"createMeetingSetupStatus","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"93c4bb70429a2739151e6f900e67bed81f3cd8f597811220e790341edb0d6008","declaration":"export function createMeetingVoiceCallGateway(params: { config: MeetingVoiceCallConfig; runtime: PluginRuntime; surface: MeetingVoiceCallSurface; connectClient: (params: { config: MeetingVoiceCallConfig; surface: MeetingVoiceCallSurface; }) => Promise; }): MeetingVoiceCallGateway;","entrypoint":"meeting-runtime","exportName":"createMeetingVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"cdf4e39ad1cf11a2826425997ccaea4ffedb057b543b355c375885891476bf23","declaration":"export function createNodeMeetingRealtimeAudioTransport(params: { runtime: PluginRuntime; nodeId: string; bridgeId: string; logger: RuntimeLogger; commandName: string; logScope: string; logPrefix: string; audioFormat?: MeetingRealtimeAudioFormat; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createNodeMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"8f5dbf400a577fb8041e8e4f1035b0a08b1c651dfc391f71f674626414a4463d","declaration":"export function createNodeMeetingRealtimeAudioTransport(params: { runtime: PluginRuntime; nodeId: string; bridgeId: string; logger: RuntimeLogger; commandName: string; logScope: string; logPrefix: string; audioFormat?: MeetingRealtimeAudioFormat; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createNodeMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"c0987b15a391a92d53d3e4e12b61eb122f9547fcf4f45478cdd3c5a8973a3e52","declaration":"export function endMeetingVoiceCallGatewayCall(params: { gateway: MeetingVoiceCallGateway; callId: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"endMeetingVoiceCallGatewayCall","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"b3ebc10fe7ab372e1317eaa8f78621cf277bba1e33fda57acf2254f6759d1e9d","declaration":"export function getMeetingVoiceCallGatewayCall(params: { gateway: MeetingVoiceCallGateway; callId: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"getMeetingVoiceCallGatewayCall","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"aac088df8e80d9ce89c45feccdb04ace3ae4417b3fcdd0f058410ddf25c96b46","declaration":"export function isMeetingVoiceCallMissingError(error: unknown): boolean;","entrypoint":"meeting-runtime","exportName":"isMeetingVoiceCallMissingError","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"337f14177df4f0a622be4d97495e99706782e43917ff560c5fbb439263f89457","declaration":"export function joinMeetingViaVoiceCallGateway(params: { config: MeetingVoiceCallConfig; gateway: MeetingVoiceCallGateway; surface: MeetingVoiceCallSurface; dialInNumber: string; dtmfSequence?: string; logger?: RuntimeLogger; message?: string; requesterSessionKey?: string; agentId?: string; sessionKey?: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"joinMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"32150fd1c5ab0734ab02215d8a0acdb048d598fd69947d4f1d0ed2ea209cf0a6","declaration":"export function joinMeetingViaVoiceCallGateway(params: { config: MeetingVoiceCallConfig; gateway: MeetingVoiceCallGateway; surface: MeetingVoiceCallSurface; dialInNumber: string; dtmfSequence?: string; logger?: RuntimeLogger; message?: string; requesterSessionKey?: string; agentId?: string; sessionKey?: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"joinMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"bac9621cd025754fbcd4c21409b06eb0503405db81f35cac993aaf0b881ab3e1","declaration":"export function leaveMeetingWithBrowser(params: { adapter: BrowserAdapter; callBrowser: MeetingBrowserRequestCaller; launch: boolean; meetingSessionId?: string; meetingUrl: string; tab: MeetingBrowserTab; timeoutMs: number; }): Promise<{ left: boolean; note: string; }>;","entrypoint":"meeting-runtime","exportName":"leaveMeetingWithBrowser","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"c7b1aeac134b2070d053adf33026d876998b852ccd65cd2c9667119b1a1abee2","declaration":"export function openMeetingWithBrowser, Mode extends string, Health extends MeetingBrowserHealth & { browserTitle?: string; browserUrl?: string; notes?: string[]; }, Transcript extends MeetingTranscriptSnapshot>(params: { adapter: BrowserAdapter; callBrowser: MeetingBrowserRequestCaller; config: MeetingBrowserControllerConfig; session: Session; }): Promise<{ launched: boolean; browser?: Health; tab?: MeetingBrowserTab; }>;","entrypoint":"meeting-runtime","exportName":"openMeetingWithBrowser","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"34858979bd248799c2215d9c7bdd5d4a6b27660c9173bfca2e50dd4373ac27db","declaration":"export function readMeetingBrowserTab(result: unknown): MeetingBrowserCandidateTab | undefined;","entrypoint":"meeting-runtime","exportName":"readMeetingBrowserTab","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} @@ -3212,8 +3212,8 @@ {"closureHash":"56874c5b2ced5af647b1dff296c931f0d8b85b1318e98c741aa88b58a9c5c606","declaration":"export function resolveMeetingBrowserNodeInfo(params: { runtime: PluginRuntime; adapter: NodeAdapter; requestedNode?: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"resolveMeetingBrowserNodeInfo","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"f21196c9ea8ab90248ffdcdef3e942f1247ee2b9064ddb9757fffe352362c779","declaration":"export function resolveMeetingRealtimeAudioFormat(audioFormat: MeetingRealtimeAudioFormat): RealtimeVoiceAudioFormat;","entrypoint":"meeting-runtime","exportName":"resolveMeetingRealtimeAudioFormat","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"521dc5c262524a709cdd27cdc88e95004073da8223537bcbc73d5d423172dc2b","declaration":"export function speakMeetingViaVoiceCallGateway(params: { gateway: MeetingVoiceCallGateway; callId: string; message: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"speakMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"318770156465bcaef766d607ed73d3d3ceea9a3d148c80980bed8da4d33ae94e","declaration":"export function startMeetingAgentRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeTranscriptionProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; }): Promise;","entrypoint":"meeting-runtime","exportName":"startMeetingAgentRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"90ca9e0ecea498d28aad6e0d5b1a098779002ff2229035ad43e7c6eae9411f23","declaration":"export function startMeetingRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; talkSessionId?: string; talkContext?: { nodeId: string; bridgeId: string; }; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeVoiceProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (params: MeetingRealtimeToolCallParams) => Promise; }): Promise;","entrypoint":"meeting-runtime","exportName":"startMeetingRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"7174800aae9004a2c5f75a161b27890725b4602f43f335fbf43483c5b86e9aa0","declaration":"export function startMeetingAgentRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeTranscriptionProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; }): Promise;","entrypoint":"meeting-runtime","exportName":"startMeetingAgentRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"9cbdf1184013c4f809bc45c204510dff243d070e7cdad69f30861e6f051957bc","declaration":"export function startMeetingRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; talkSessionId?: string; talkContext?: { nodeId: string; bridgeId: string; }; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeVoiceProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (params: MeetingRealtimeToolCallParams) => Promise; }): Promise;","entrypoint":"meeting-runtime","exportName":"startMeetingRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"663b9c77231b21af697efd5042e22d1c33e9ee972adeb312ad86d81a2b2e36f7","declaration":"export type MeetingAgentConsultParams = MeetingAgentConsultParams;","entrypoint":"meeting-runtime","exportName":"MeetingAgentConsultParams","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"adf0745c611bf737fc28978d766fc2a25b7104e71b6b2fed4e9b95e519c7b115","declaration":"export type MeetingBrowserCandidateTab = MeetingBrowserCandidateTab;","entrypoint":"meeting-runtime","exportName":"MeetingBrowserCandidateTab","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"59b1526172b7491b85f14f13c1ee26d8d6726f33093770748580c767b1689a17","declaration":"export type MeetingBrowserControllerConfig = MeetingBrowserControllerConfig;","entrypoint":"meeting-runtime","exportName":"MeetingBrowserControllerConfig","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} @@ -3245,7 +3245,7 @@ {"closureHash":"8244b408803f57cf2004157e96f2ce11bb036cdf864123b15ba2fac7f03b5d43","declaration":"export type MeetingSessionRuntimeHandles = MeetingSessionRuntimeHandles;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeHandles","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"aa8054fc2c442891197e89bdad61e79999ea15d813c4b127fb6150607b9e67e7","declaration":"export type MeetingSessionRuntimeJoinContext, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab> = MeetingSessionRuntimeJoinContext;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeJoinContext","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"325241724edd38da439c5df03d225e2a7cb8e56d817e98ed7c37850656d3744b","declaration":"export type MeetingSessionRuntimeMessages = MeetingSessionRuntimeMessages;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeMessages","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} -{"closureHash":"bc3ec8d61eeca24b6d2d5d56b91479aed7c4e8bf0b7e86945a1eedc562a9e42e","declaration":"export type MeetingSessionRuntimeOptions, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeOptions","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} +{"closureHash":"fa85bc062083dfd786c365009d368a32b57084e16e6fab1bde67a55099a6fb09","declaration":"export type MeetingSessionRuntimeOptions, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeOptions","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"d3c730cba02d30512c4cdae6e2367c662982e2c25671833ecd8701283924c642","declaration":"export type MeetingSessionState = MeetingSessionState;","entrypoint":"meeting-runtime","exportName":"MeetingSessionState","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"2496931157bf64fdb16013172ab9c023e60c5f85b98f46a3ee9532ce58d636d6","declaration":"export type MeetingSetupCheck = MeetingSetupCheck;","entrypoint":"meeting-runtime","exportName":"MeetingSetupCheck","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"f0aed6f7955a43fc387d8bbc072cb6276b0f2d18e7f829441a2ad45974bb3831","declaration":"export type MeetingSetupStatus = MeetingSetupStatus;","entrypoint":"meeting-runtime","exportName":"MeetingSetupStatus","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} @@ -3259,9 +3259,9 @@ {"closureHash":"20c70a94483aeddda4d1500cfcc5e71fab844b474ba4061c55d6c8693d40821b","declaration":"export type MeetingVoiceCallJoinResult = MeetingVoiceCallJoinResult;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallJoinResult","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"4fb2c5928aa6f809dd4c604d71c5c99cdfe3e98b6e9c1662601c9b4ad9738162","declaration":"export type MeetingVoiceCallStatusResult = MeetingVoiceCallStatusResult;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallStatusResult","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"5ef812397b6409a4ca228943dd1080661365959477b42e705a9aaa219757d14f","declaration":"export type MeetingVoiceCallSurface = MeetingVoiceCallSurface;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallSurface","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} -{"closureHash":"647fd7d11823c5e1228d0cb75a7dfed96c63e16087f74f7dc6c48afaebaf417d","declaration":"export interface MeetingPlatformAdapter extends MeetingPlatformAdapterContract {\n}","entrypoint":"meeting-runtime","exportName":"MeetingPlatformAdapter","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"} +{"closureHash":"af46549b627c17d5f706fe108a92a0d0472055e353059299dfc8204212ee7eb4","declaration":"export interface MeetingPlatformAdapter extends MeetingPlatformAdapterContract {\n}","entrypoint":"meeting-runtime","exportName":"MeetingPlatformAdapter","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"} {"closureHash":"79884bd1df1ea0d07d5ceb4c62d93c1ee744b75d31c94c988845869c0f3cbd2d","declaration":"export interface MeetingRealtimeAudioTransport {\n onFatal(handler: () => void): void;\n startInput(onAudio: (audio: Buffer) => void): void;\n beginOutput?(): void;\n stop(): Promise;\n writeOutput(audio: Buffer): Promise;\n clearOutput(): Promise;\n dispose(): Promise;\n getHealth?(): MeetingRealtimeAudioTransportHealth;\n startBargeInMonitor?(onBargeIn: (audio: Buffer) => boolean): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"} -{"closureHash":"d3d839a5d9a3361c282ff6d2713defc867be38fef32cb3e2731fcd4fbf6645a7","declaration":"export class MeetingSessionRuntime, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map;\n readonly #sessionLeaves: Map>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map Promise>;\n readonly #sessionSpeakers: Map void>;\n readonly #sessionHealth: Map Partial>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts;\n readonly #transcriptStore: MeetingSessionTranscriptStore;\n constructor(private readonly options: MeetingSessionRuntimeOptions);\n list(): TSession[];\n getSession(sessionId: string): TSession | undefined;\n async status(sessionId?: string): Promise<{\n found: boolean;\n session?: TSession;\n sessions?: TSession[];\n }>;\n async transcript(sessionId: string, options: {\n sinceIndex?: number;\n }): Promise<{ found: boolean; sessionId?: string; startIndex?: number; nextIndex?: number; droppedLines?: number; evicted?: boolean; lines?: MeetingTranscriptLine[]; }>;\n async startTranscriptSource(request: TranscriptStartRequest): Promise;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin): boolean;\n async join(request: TRequest): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async leave(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n async speak(sessionId: string, instructions?: string): Promise<{\n found: boolean;\n spoken: boolean;\n session?: TSession;\n }>;\n async speakWhenReady(session: TSession, instructions: string): Promise;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise;\n async refreshCaptionHealth(session: TSession): Promise;\n refreshSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n markSessionEnded(session: TSession, reason: string): void;\n async #joinUnlocked(request: TRequest, resolved: MeetingResolvedJoin): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n #meetingKey(transport: TTransport, url: string): string;\n #inheritBrowserTabOwnership(params: {\n session: TSession;\n transport: TTransport;\n nodeId?: string;\n meetingUrl: string;\n tab?: TTab;\n }): TTab | undefined;\n async #settleRetainedBrowserTabs(retained: Array<{\n session: TSession;\n tab: TTab;\n }>, adopted?: {\n transport: TTransport;\n nodeId?: string;\n tab: TTab;\n }): Promise;\n async #rollbackFailedJoinSession(session: TSession): Promise;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles): void;\n #dropRuntimeHandles(sessionId: string): void;\n #isManagedBrowserSession(session: TSession): boolean;\n #evaluateSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n #noteSession(session: TSession, note: string): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"class","recordType":"export"} +{"closureHash":"c9cedf68602823e1eb0c6dce9206150967a843dd4bb49cfb1cd9f4156e39cd52","declaration":"export class MeetingSessionRuntime, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map;\n readonly #sessionLeaves: Map>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map Promise>;\n readonly #sessionSpeakers: Map void>;\n readonly #sessionHealth: Map Partial>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts;\n readonly #transcriptStore: MeetingSessionTranscriptStore;\n constructor(private readonly options: MeetingSessionRuntimeOptions);\n list(): TSession[];\n getSession(sessionId: string): TSession | undefined;\n async status(sessionId?: string): Promise<{\n found: boolean;\n session?: TSession;\n sessions?: TSession[];\n }>;\n async transcript(sessionId: string, options: {\n sinceIndex?: number;\n }): Promise<{ found: boolean; sessionId?: string; startIndex?: number; nextIndex?: number; droppedLines?: number; evicted?: boolean; lines?: MeetingTranscriptLine[]; }>;\n async startTranscriptSource(request: TranscriptStartRequest): Promise;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin): boolean;\n async join(request: TRequest): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async leave(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n async speak(sessionId: string, instructions?: string): Promise<{\n found: boolean;\n spoken: boolean;\n session?: TSession;\n }>;\n async speakWhenReady(session: TSession, instructions: string): Promise;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise;\n async refreshCaptionHealth(session: TSession): Promise;\n refreshSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n markSessionEnded(session: TSession, reason: string): void;\n async #joinUnlocked(request: TRequest, resolved: MeetingResolvedJoin): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n #meetingKey(transport: TTransport, url: string): string;\n #inheritBrowserTabOwnership(params: {\n session: TSession;\n transport: TTransport;\n nodeId?: string;\n meetingUrl: string;\n tab?: TTab;\n }): TTab | undefined;\n async #settleRetainedBrowserTabs(retained: Array<{\n session: TSession;\n tab: TTab;\n }>, adopted?: {\n transport: TTransport;\n nodeId?: string;\n tab: TTab;\n }): Promise;\n async #rollbackFailedJoinSession(session: TSession): Promise;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles): void;\n #dropRuntimeHandles(sessionId: string): void;\n #isManagedBrowserSession(session: TSession): boolean;\n #evaluateSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n #noteSession(session: TSession, note: string): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"class","recordType":"export"} {"category":null,"entrypoint":"memory-core-host-engine-foundation","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","recordType":"module"} {"closureHash":"e2df8d235401aebf58eb71652fbeaedb42ca00aaa5042a76909ff2c48ba6fd54","declaration":"export function createSubsystemLogger(subsystem: string): SubsystemLogger;","entrypoint":"memory-core-host-engine-foundation","exportName":"createSubsystemLogger","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","kind":"function","recordType":"export"} {"closureHash":null,"declaration":"export function isPathInside(root: string, target: string): boolean;","entrypoint":"memory-core-host-engine-foundation","exportName":"isPathInside","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","kind":"function","recordType":"export"} @@ -3655,10 +3655,10 @@ {"closureHash":"e66adcb55dec79be73a52bc76594b7e9210fd93c69a85b56098fbb58cecbb354","declaration":"export type WriteOAuthCredentialsOptions = WriteOAuthCredentialsOptions;","entrypoint":"provider-auth","exportName":"WriteOAuthCredentialsOptions","importSpecifier":"openclaw/plugin-sdk/provider-auth","kind":"type","recordType":"export"} {"category":null,"entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","recordType":"module"} {"closureHash":"b5665596a3f011454a5614681513516acd50b6c749536c43842d811980cf7ad3","declaration":"export function augmentModelCatalogWithProviderPlugins(params: { config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; metadataSnapshot?: PluginMetadataSnapshot; context: ProviderAugmentModelCatalogContext; }): Promise;","entrypoint":"provider-catalog-runtime","exportName":"augmentModelCatalogWithProviderPlugins","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} -{"closureHash":"94876118e27fd3a011c350765bc51c045da727a430bf560b76b13c36815e2a21","declaration":"export function isPluginProvidersLoadInFlight(params: Parameters[0]): boolean;","entrypoint":"provider-catalog-runtime","exportName":"isPluginProvidersLoadInFlight","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} -{"closureHash":"baabd4ac04b433e27916ad3bfeee5a95952afce9e0685ae5acf3b7dceea69507","declaration":"export function resolveCatalogHookProviderPluginIds(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; metadataSnapshot?: ProviderManifestLoadParams[\"metadataSnapshot\"]; }): string[];","entrypoint":"provider-catalog-runtime","exportName":"resolveCatalogHookProviderPluginIds","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} -{"closureHash":"c21aedf5df214eee9f68930c1aaeef0b9ef5ec66df0a09dc9db095aa1764bd58","declaration":"export function resolveOwningPluginIdsForProvider(params: { provider: string; config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; manifestRegistry?: PluginManifestRegistry; metadataSnapshot?: Pick; }): string[] | undefined;","entrypoint":"provider-catalog-runtime","exportName":"resolveOwningPluginIdsForProvider","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} -{"closureHash":"37b37fd3da65fed3572a4e2d9784e79a338a26b85cacd87039c4741cfb41b07b","declaration":"export function resolvePluginProviders(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; bundledProviderVitestCompat?: boolean; onlyPluginIds?: string[]; providerRefs?: readonly string[]; modelRefs?: readonly string[]; activate?: boolean; cache?: boolean; applyAutoEnable?: boolean; pluginSdkResolution?: PluginLoadOptions[\"pluginSdkResolution\"]; mode?: \"runtime\" | \"setup\"; includeUntrustedWorkspacePlugins?: boolean; pluginMetadataSnapshot?: PluginMetadataRegistryView; skipIfLoadInFlight?: boolean; }): ProviderPlugin[];","entrypoint":"provider-catalog-runtime","exportName":"resolvePluginProviders","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} +{"closureHash":"335c16c1aebef57af0854df3f9c54ae8fe824a5734a14ee0d3701557299560d0","declaration":"export function isPluginProvidersLoadInFlight(params: Parameters[0]): boolean;","entrypoint":"provider-catalog-runtime","exportName":"isPluginProvidersLoadInFlight","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} +{"closureHash":"10890ed322bbcee137a08de3fe0e4321c35126a6f1ee8ad763f1217e6ae32826","declaration":"export function resolveCatalogHookProviderPluginIds(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; metadataSnapshot?: ProviderManifestLoadParams[\"metadataSnapshot\"]; }): string[];","entrypoint":"provider-catalog-runtime","exportName":"resolveCatalogHookProviderPluginIds","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} +{"closureHash":"698f1fcc79c157e9de99df73ecec432cee824af1ec0dca1436a84f31e68b278a","declaration":"export function resolveOwningPluginIdsForProvider(params: { provider: string; config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; manifestRegistry?: PluginManifestRegistry; metadataSnapshot?: Pick; }): string[] | undefined;","entrypoint":"provider-catalog-runtime","exportName":"resolveOwningPluginIdsForProvider","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} +{"closureHash":"fc0e915b8a97d2b5fe2a9e6bc49a8ea09b7be903ff7204157de22eba2465a853","declaration":"export function resolvePluginProviders(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; bundledProviderVitestCompat?: boolean; onlyPluginIds?: string[]; providerRefs?: readonly string[]; modelRefs?: readonly string[]; activate?: boolean; cache?: boolean; applyAutoEnable?: boolean; pluginSdkResolution?: PluginLoadOptions[\"pluginSdkResolution\"]; mode?: \"runtime\" | \"setup\"; includeUntrustedWorkspacePlugins?: boolean; pluginMetadataSnapshot?: PluginMetadataRegistryView; skipIfLoadInFlight?: boolean; }): ProviderPlugin[];","entrypoint":"provider-catalog-runtime","exportName":"resolvePluginProviders","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} {"category":null,"entrypoint":"proxy-capture","importSpecifier":"openclaw/plugin-sdk/proxy-capture","recordType":"module"} {"closureHash":"d0e4e026d736bc00b1758d1089c44c3c5f3a09c7254291584de8cb297365804c","declaration":"export function acquireDebugProxyCaptureStore(dbPath: string, blobDir: string): { store: LegacyDebugProxyCaptureStore; release: () => void; };\nexport function acquireDebugProxyCaptureStore(options?: DebugProxyCaptureStoreOptions): { store: SharedDebugProxyCaptureStore; release: () => void; };","entrypoint":"proxy-capture","exportName":"acquireDebugProxyCaptureStore","importSpecifier":"openclaw/plugin-sdk/proxy-capture","kind":"function","recordType":"export"} {"closureHash":"d41cbb9de51b7aee1c6cf8c713b0bc44f27ac926fbb6422c05726f196d11964e","declaration":"export function captureHttpExchange(params: { url: string; method: string; requestHeaders?: Headers | Record | undefined; requestBody?: BodyInit | Buffer | string | null; response: Response; transport?: \"http\" | \"sse\"; flowId?: string; meta?: Record; }, resolved?: DebugProxySettings, deps?: DebugProxyCaptureRuntimeDeps): void;","entrypoint":"proxy-capture","exportName":"captureHttpExchange","importSpecifier":"openclaw/plugin-sdk/proxy-capture","kind":"function","recordType":"export"} diff --git a/extensions/qa-lab/src/live-transports/matrix/substrate/config.test.ts b/extensions/qa-lab/src/live-transports/matrix/substrate/config.test.ts index 767a66234200..773334f3e949 100644 --- a/extensions/qa-lab/src/live-transports/matrix/substrate/config.test.ts +++ b/extensions/qa-lab/src/live-transports/matrix/substrate/config.test.ts @@ -374,6 +374,7 @@ describe("matrix qa config", () => { chunkMode: "newline", mode: "quiet", preview: { toolProgress: false }, + progress: { commandText: "status", maxLines: 2 }, }, threadBindings: { enabled: false, @@ -463,6 +464,7 @@ describe("matrix qa config", () => { chunkMode: "newline", mode: "quiet", preview: { adjacentPreview: "current", toolProgress: false }, + progress: { commandText: "raw", currentOnly: "current", maxLines: 3 }, }, threadBindings: { adjacentBinding: "current", @@ -541,6 +543,7 @@ describe("matrix qa config", () => { chunkMode: "length", mode: "off", preview: { adjacentPreview: "current", toolProgress: true }, + progress: { commandText: "status", currentOnly: "current", maxLines: 3 }, }, threadBindings: { adjacentBinding: "current", @@ -596,7 +599,10 @@ describe("matrix qa config", () => { }, groupMentionPatterns: ["\\S"], groupPolicy: "open", - streaming: true, + streaming: { + mode: "partial", + progress: { commandText: "raw" }, + }, }, sutAccessToken: "sut-token", sutAccountId: "sut", @@ -613,11 +619,12 @@ describe("matrix qa config", () => { chunkMode: "length", mode: "partial", preview: { toolProgress: true }, + progress: { commandText: "raw" }, }); expect(config.messages?.groupChat?.mentionPatterns).toEqual(["\\S"]); }); - it("resets tool progress when a scalar streaming override follows an opt-out", () => { + it("resets QA streaming detail overrides when a scalar override follows", () => { const optedOut = buildMatrixQaConfig({} as OpenClawConfig, { driverUserId: "@driver:matrix-qa.test", homeserver: "http://127.0.0.1:28008/", @@ -626,6 +633,7 @@ describe("matrix qa config", () => { streaming: { mode: "quiet", preview: { toolProgress: false }, + progress: { commandText: "raw" }, }, }, sutAccessToken: "sut-token", @@ -650,6 +658,7 @@ describe("matrix qa config", () => { chunkMode: "length", mode: "quiet", preview: { toolProgress: false }, + progress: { commandText: "raw" }, }); expect(reset.channels?.matrix?.accounts?.sut?.streaming).toEqual({ block: { enabled: false }, diff --git a/extensions/qa-lab/src/live-transports/matrix/substrate/config.ts b/extensions/qa-lab/src/live-transports/matrix/substrate/config.ts index 79e0d2bc80f3..1444ce0870d1 100644 --- a/extensions/qa-lab/src/live-transports/matrix/substrate/config.ts +++ b/extensions/qa-lab/src/live-transports/matrix/substrate/config.ts @@ -20,9 +20,8 @@ type MatrixQaExecApprovalsEnabled = boolean | "auto"; type MatrixQaAllowBotsMode = boolean | "mentions"; type MatrixQaStreamingConfig = { mode?: MatrixQaStreamingMode; - preview?: { - toolProgress?: boolean; - }; + preview?: { toolProgress?: boolean }; + progress?: { commandText?: "raw" | "status" }; }; type MatrixQaAgentDefaultsOverrides = { blockStreamingChunk?: { @@ -132,6 +131,7 @@ type MatrixQaConfigSnapshot = { startupVerification?: "if-unverified" | "off"; streaming: MatrixQaStreamingMode; streamingPreviewToolProgress: boolean; + streamingProgressCommandText?: "raw" | "status"; textChunkLimit?: number; threadBindings: MatrixQaThreadBindingsConfigOverrides; threadReplies: MatrixQaThreadRepliesMode; @@ -293,10 +293,7 @@ function isMatrixQaStreamingConfig( function resolveMatrixQaStreamingPreviewToolProgress( value: MatrixQaConfigOverrides["streaming"], ): boolean { - if (!isMatrixQaStreamingConfig(value)) { - return true; - } - return value.preview?.toolProgress ?? true; + return isMatrixQaStreamingConfig(value) ? (value.preview?.toolProgress ?? true) : true; } function resolveMatrixQaAutoJoinAllowlist(params: { overrides?: MatrixQaConfigOverrides }) { @@ -450,12 +447,22 @@ function buildMatrixQaChannelAccountConfig(params: { const preview = restoreOwnedFields(current?.streaming?.preview, baseline?.streaming?.preview, [ "toolProgress", ]); + const progress = restoreOwnedFields(current?.streaming?.progress, baseline?.streaming?.progress, [ + "commandText", + ]); + if (params.snapshot.streamingProgressCommandText !== undefined) { + progress.commandText = params.snapshot.streamingProgressCommandText; + } Object.assign(streaming, { block: { ...block, enabled: params.snapshot.blockStreaming }, chunkMode: params.snapshot.chunkMode ?? "length", mode: params.snapshot.streaming, preview: { ...preview, toolProgress: params.snapshot.streamingPreviewToolProgress }, }); + delete streaming.progress; + if (Object.keys(progress).length > 0) { + streaming.progress = progress; + } const threadBindings = restoreOwnedFields( current?.threadBindings, baseline?.threadBindings, @@ -513,6 +520,7 @@ function buildMatrixQaConfigSnapshot(params: { sutUserId: string; topology: MatrixQaProvisionedTopology; }): MatrixQaConfigSnapshot { + const streaming = params.overrides?.streaming; return { allowBots: params.overrides?.allowBots, autoJoin: params.overrides?.autoJoin ?? "off", @@ -532,10 +540,11 @@ function buildMatrixQaConfigSnapshot(params: { }), replyToMode: params.overrides?.replyToMode ?? "off", startupVerification: params.overrides?.startupVerification, - streaming: resolveMatrixQaStreamingMode(params.overrides?.streaming), - streamingPreviewToolProgress: resolveMatrixQaStreamingPreviewToolProgress( - params.overrides?.streaming, - ), + streaming: resolveMatrixQaStreamingMode(streaming), + streamingPreviewToolProgress: resolveMatrixQaStreamingPreviewToolProgress(streaming), + streamingProgressCommandText: isMatrixQaStreamingConfig(streaming) + ? streaming.progress?.commandText + : undefined, threadBindings: { ...params.overrides?.threadBindings }, textChunkLimit: params.overrides?.textChunkLimit, threadReplies: params.overrides?.threadReplies ?? "inbound", diff --git a/extensions/qa-lab/src/live-transports/shared/scenario-selection.test.ts b/extensions/qa-lab/src/live-transports/shared/scenario-selection.test.ts index 85ae5e08c5e5..a0029800deb7 100644 --- a/extensions/qa-lab/src/live-transports/shared/scenario-selection.test.ts +++ b/extensions/qa-lab/src/live-transports/shared/scenario-selection.test.ts @@ -138,6 +138,7 @@ describe("live transport QA scenario selection", () => { it.each([ { channelId: "matrix", scenarioId: "thread-follow-up" }, + { channelId: "buzz", scenarioId: "channel-canary" }, { channelId: "telegram", scenarioId: "channel-canary" }, { channelId: "telegram", scenarioId: "channel-message-flows" }, ] as const)( @@ -165,7 +166,7 @@ describe("live transport QA scenario selection", () => { scenarioIds: ["channel-canary"], }), ).toThrow( - "selected QA scenario(s) do not match the current QA lane: channel-canary (channel=qa-channel|telegram)", + "selected QA scenario(s) do not match the current QA lane: channel-canary (channel=qa-channel|telegram|buzz)", ); }); }); diff --git a/extensions/qa-lab/src/scenario-catalog-channels.test.ts b/extensions/qa-lab/src/scenario-catalog-channels.test.ts index b81fc546418e..12d6d2643427 100644 --- a/extensions/qa-lab/src/scenario-catalog-channels.test.ts +++ b/extensions/qa-lab/src/scenario-catalog-channels.test.ts @@ -213,10 +213,33 @@ describe("qa scenario catalog channel contracts", () => { expect(scenario.gatewayConfigPatch).not.toHaveProperty("channels.telegram.groups"); }); - it("keeps the shared channel canary eligible for QA Channel and Telegram", () => { + it("keeps the shared channel canary eligible for QA Channel, Telegram, and Buzz", () => { const scenario = requireFlowScenario(readQaScenarioById("channel-canary")); - expect(scenario.execution.channels).toEqual(["qa-channel", "telegram"]); + expect(scenario.execution.channels).toEqual(["qa-channel", "telegram", "buzz"]); + }); + + it("keeps raw Matrix command text scoped to the mention-safety scenario", () => { + const scenario = requireFlowScenario( + readQaScenarioById("matrix-room-tool-progress-mention-safety"), + ); + const ordinaryProgressScenario = requireFlowScenario( + readQaScenarioById("matrix-room-tool-progress-preview"), + ); + + expect(scenario.execution.channel).toBe("matrix"); + expect(scenario.execution.config).toMatchObject({ + matrixConfigOverrides: { + streaming: { + mode: "partial", + progress: { commandText: "raw" }, + }, + toolProfile: "coding", + }, + }); + expect(ordinaryProgressScenario.execution.config).not.toHaveProperty( + "matrixConfigOverrides.streaming.progress.commandText", + ); }); it("keeps transcript-role delivery on the Crabline driver", () => { diff --git a/qa/scenarios/channels/channel-canary.yaml b/qa/scenarios/channels/channel-canary.yaml index 56bb62870d65..417e3f3dac23 100644 --- a/qa/scenarios/channels/channel-canary.yaml +++ b/qa/scenarios/channels/channel-canary.yaml @@ -24,6 +24,7 @@ scenario: channels: - qa-channel - telegram + - buzz summary: Run the shared channel canary through QA Channel, Crabline, or a live adapter. transportPolicy: requireGroupMention: true diff --git a/qa/scenarios/channels/matrix-room-tool-progress-mention-safety.yaml b/qa/scenarios/channels/matrix-room-tool-progress-mention-safety.yaml index 219fe5db1d6b..9d6dccfc7baf 100644 --- a/qa/scenarios/channels/matrix-room-tool-progress-mention-safety.yaml +++ b/qa/scenarios/channels/matrix-room-tool-progress-mention-safety.yaml @@ -12,7 +12,10 @@ scenario: retryCount: 0 config: matrixConfigOverrides: - streaming: partial + streaming: + mode: partial + progress: + commandText: raw toolProfile: coding flow: diff --git a/src/infra/delivery-queue-sqlite-claim.test.ts b/src/infra/delivery-queue-sqlite-claim.test.ts new file mode 100644 index 000000000000..a0741c4e6f63 --- /dev/null +++ b/src/infra/delivery-queue-sqlite-claim.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it, vi } from "vitest"; +import { + claimDeliveryQueueEntryPlatformSend, + dispatchDeliveryQueueEntryPlatformSend, +} from "./delivery-queue-sqlite-claim.js"; +import { loadDeliveryQueueEntry, upsertDeliveryQueueEntry } from "./delivery-queue-sqlite.js"; +import { installDeliveryQueueTmpDirHooks } from "./outbound/delivery-queue.test-helpers.js"; + +describe("delivery queue SQLite dispatch ownership", () => { + const { tmpDir } = installDeliveryQueueTmpDirHooks(); + const queueName = "test-dispatch-owner"; + + it("atomically promotes dispatch ownership and rejects expired or replaced claims", () => { + vi.useFakeTimers(); + try { + vi.setSystemTime(new Date("2026-08-10T10:00:00.000Z")); + const stateDir = tmpDir(); + const id = "cron-direct-delivery:v1:dispatch-owner"; + upsertDeliveryQueueEntry({ + queueName, + entry: { + id, + enqueuedAt: Date.now(), + retryCount: 0, + completionRetention: { + idPrefix: "cron-direct-delivery:v1:", + maxAgeMs: 24 * 60 * 60_000, + maxEntries: 2, + }, + requiresProducerClaim: true, + }, + stateDir, + }); + + const expiredClaimId = claimDeliveryQueueEntryPlatformSend({ queueName, id, stateDir }); + if (!expiredClaimId) { + throw new Error("test invariant: the first producer claim must be available"); + } + vi.advanceTimersByTime(30_001); + expect( + dispatchDeliveryQueueEntryPlatformSend({ + queueName, + id, + claimId: expiredClaimId, + stateDir, + }), + ).toBe(false); + + const claimId = claimDeliveryQueueEntryPlatformSend({ queueName, id, stateDir }); + if (!claimId) { + throw new Error("test invariant: the replacement producer claim must be available"); + } + expect( + dispatchDeliveryQueueEntryPlatformSend({ + queueName, + id, + claimId: expiredClaimId, + stateDir, + }), + ).toBe(false); + expect( + dispatchDeliveryQueueEntryPlatformSend({ + queueName, + id, + claimId, + stateDir, + route: { replyToId: "thread-1" }, + }), + ).toBe(true); + expect(loadDeliveryQueueEntry(queueName, id, stateDir)).toMatchObject({ + recoveryState: "send_attempt_started", + platformSendAttemptId: claimId, + platformSendStartedAt: Date.now(), + effectiveReplyToId: "thread-1", + availableAt: Date.now() + 30_000, + }); + expect(loadDeliveryQueueEntry(queueName, id, stateDir)?.producerClaimId).toBeUndefined(); + + vi.advanceTimersByTime(30_001); + expect(dispatchDeliveryQueueEntryPlatformSend({ queueName, id, claimId, stateDir })).toBe( + false, + ); + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/src/infra/delivery-queue-sqlite-claim.ts b/src/infra/delivery-queue-sqlite-claim.ts index 1ebcf0e4a0f7..911b2bf1cdd5 100644 --- a/src/infra/delivery-queue-sqlite-claim.ts +++ b/src/infra/delivery-queue-sqlite-claim.ts @@ -68,9 +68,9 @@ export function transitionOwnedDeliveryQueueEntry( ); } -function transitionUnsentDeliveryQueueEntry( +function transitionDeliveryQueueEntryPlatformSend( params: PlatformClaimParams, - operation: "claim" | "promote", + operation: "claim" | "promote" | "dispatch", transition: (entry: DeliveryQueueEntryState, now: number) => DeliveryQueueEntryState | undefined, ): boolean { // State-database opens reuse the canonical path-owned connection, so both @@ -82,13 +82,16 @@ function transitionUnsentDeliveryQueueEntry( database.db, () => { const current = loadDeliveryQueueEntry(params.queueName, params.id, params.stateDir); + if (!current) { + return false; + } if ( - !current || - (current.platformSendStartedAt !== undefined && - (operation !== "claim" || - current.platformSendStartedAt !== params.reconciledPlatformSendStartedAt || - current.platformSendAttemptId !== params.reconciledPlatformSendAttemptId || - typeof current.platformSendAttemptId !== "string")) + current.platformSendStartedAt !== undefined && + (operation === "promote" || + (operation === "claim" && + (current.platformSendStartedAt !== params.reconciledPlatformSendStartedAt || + current.platformSendAttemptId !== params.reconciledPlatformSendAttemptId || + typeof current.platformSendAttemptId !== "string"))) ) { return false; } @@ -114,7 +117,7 @@ export function claimDeliveryQueueEntryPlatformSend( params: PlatformClaimParams, ): string | undefined { const claimId = generateSecureUuid(); - return transitionUnsentDeliveryQueueEntry(params, "claim", (entry, now) => { + return transitionDeliveryQueueEntryPlatformSend(params, "claim", (entry, now) => { const reconciledNotSent = entry.recoveryState === "send_attempt_started" && typeof params.reconciledPlatformSendStartedAt === "number" && @@ -197,7 +200,7 @@ export function promoteDeliveryQueueEntryPlatformSend( route?: { replyToId?: string | null }; }, ): boolean { - return transitionUnsentDeliveryQueueEntry(params, "promote", (entry, now) => + return transitionDeliveryQueueEntryPlatformSend(params, "promote", (entry, now) => entry.recoveryState === "producer_claimed" && entry.producerClaimId === params.claimId && typeof entry.availableAt === "number" && @@ -219,3 +222,49 @@ export function promoteDeliveryQueueEntryPlatformSend( : undefined, ); } + +/** Atomically authorize dispatch, promoting a producer claim into the active attempt. */ +export function dispatchDeliveryQueueEntryPlatformSend( + params: PlatformClaimParams & { + claimId: string; + route?: { replyToId?: string | null }; + }, +): boolean { + return transitionDeliveryQueueEntryPlatformSend(params, "dispatch", (entry, now) => { + const producerOwned = + entry.recoveryState === "producer_claimed" && + entry.producerClaimId === params.claimId && + typeof entry.availableAt === "number" && + entry.availableAt > now; + const attemptOwned = + (entry.recoveryState === "send_attempt_started" || + entry.recoveryState === "unknown_after_send") && + entry.platformSendAttemptId === params.claimId && + (entry.requiresProducerClaim !== true || + (typeof entry.availableAt === "number" && entry.availableAt > now)); + if (!producerOwned && !attemptOwned) { + return undefined; + } + return { + ...entry, + // Exact reconciliation can skip pre-send promotion, so publish attempt identity + // atomically; later batch dispatches retain stronger unknown-after-send evidence. + availableAt: + entry.requiresProducerClaim === true + ? producerOwned + ? now + PLATFORM_SEND_OWNER_LEASE_MS + : entry.availableAt + : undefined, + producerClaimId: undefined, + platformSendAttemptId: params.claimId, + platformSendStartedAt: now, + ...(params.route && "replyToId" in params.route + ? { effectiveReplyToId: params.route.replyToId ?? null } + : {}), + recoveryState: + entry.recoveryState === "unknown_after_send" + ? "unknown_after_send" + : "send_attempt_started", + }; + }); +} diff --git a/src/infra/outbound/deliver-queue.exact-reconciliation.integration.test.ts b/src/infra/outbound/deliver-queue.exact-reconciliation.integration.test.ts new file mode 100644 index 000000000000..60314adcead8 --- /dev/null +++ b/src/infra/outbound/deliver-queue.exact-reconciliation.integration.test.ts @@ -0,0 +1,101 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { createMessageReceiptFromOutboundResults } from "../../channels/message/receipt.js"; +import type { ChannelMessageSendTextContext } from "../../channels/message/types.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import { createEmptyPluginRegistry } from "../../plugins/registry.js"; +import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js"; +import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; +import { getDeliveryQueueEntryStatus } from "../delivery-queue-sqlite.js"; +import { + boundedCronCompletionRetention, + drainMatrixReconnect, + matrixOutboundForQueueTest, +} from "./deliver.queue-integration.test-support.js"; +import { OUTBOUND_DELIVERY_QUEUE_NAME } from "./delivery-queue-media-staging.js"; +import type { DeliverFn } from "./delivery-queue.js"; +import { installDeliveryQueueTmpDirHooks } from "./delivery-queue.test-helpers.js"; + +let deliverOutboundPayloads: typeof import("./deliver.js").deliverOutboundPayloads; + +describe("exact Matrix delivery queue reconciliation", () => { + const fixtures = installDeliveryQueueTmpDirHooks(); + let tmpDir: string; + + beforeAll(async () => { + ({ deliverOutboundPayloads } = await import("./deliver.js")); + }); + + beforeEach(() => { + tmpDir = fixtures.tmpDir(); + }); + + afterEach(() => { + resetPluginRuntimeStateForTest(); + setActivePluginRegistry(createEmptyPluginRegistry()); + }); + + it.each(["required", "best_effort"] as const)( + "settles one exact Matrix %s send without restart replay", + async (queuePolicy) => { + process.env.OPENCLAW_STATE_DIR = tmpDir; + const deliveryIntentId = `cron-direct-delivery:v1:exact-${queuePolicy}-completion`; + const messageId = `exact-${queuePolicy}-message`; + const reconcileUnknownSend = vi.fn(); + const sendText = vi.fn(async (ctx: ChannelMessageSendTextContext) => { + expect(ctx.deliveryQueueId).toBe(deliveryIntentId); + await ctx.onPlatformSendDispatch?.(); + return { + messageId, + receipt: createMessageReceiptFromOutboundResults({ + results: [{ channel: "matrix", messageId }], + kind: "text", + }), + }; + }); + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "matrix", + source: "test", + plugin: { + ...createOutboundTestPlugin({ id: "matrix", outbound: matrixOutboundForQueueTest }), + message: { + id: "matrix", + durableFinal: { + capabilities: { text: true, reconcileUnknownSend: true }, + reconcileUnknownSendKinds: { text: true }, + reconcileUnknownSend, + }, + send: { text: sendText }, + }, + }, + }, + ]), + ); + const params = { + cfg: {} as OpenClawConfig, + channel: "matrix" as const, + to: "!room:example", + payloads: [{ text: "send exactly once with durable platform identity" }], + queuePolicy, + ...(queuePolicy === "best_effort" ? { bestEffort: true } : {}), + deliveryIntentId, + completionRetention: boundedCronCompletionRetention, + reusePendingDeliveryIntent: true, + requireUnknownSendReconciliation: true, + }; + + await expect(deliverOutboundPayloads(params)).resolves.toMatchObject([{ messageId }]); + expect(sendText).toHaveBeenCalledOnce(); + expect(reconcileUnknownSend).not.toHaveBeenCalled(); + expect( + getDeliveryQueueEntryStatus(OUTBOUND_DELIVERY_QUEUE_NAME, deliveryIntentId, tmpDir), + ).toBe("completed"); + + const recoveryDeliver = vi.fn(async () => []); + await drainMatrixReconnect({ deliver: recoveryDeliver, stateDir: tmpDir }); + expect(recoveryDeliver).not.toHaveBeenCalled(); + expect(sendText).toHaveBeenCalledOnce(); + }, + ); +}); diff --git a/src/infra/outbound/delivery-queue-platform-lease.ts b/src/infra/outbound/delivery-queue-platform-lease.ts index 7d011c061776..73b4b9eb3962 100644 --- a/src/infra/outbound/delivery-queue-platform-lease.ts +++ b/src/infra/outbound/delivery-queue-platform-lease.ts @@ -1,9 +1,26 @@ import { claimDeliveryQueueEntryPlatformSend, + dispatchDeliveryQueueEntryPlatformSend, renewDeliveryQueueEntryPlatformSendLease, } from "../delivery-queue-sqlite-claim.js"; import { OUTBOUND_DELIVERY_QUEUE_NAME } from "./delivery-queue-media-staging.js"; +/** Atomically transfer a stable pending producer intent to one platform sender. */ +export async function claimDeliveryPlatformSendAttempt( + id: string, + stateDir?: string, + reconciledPlatformSendStartedAt?: number, + reconciledPlatformSendAttemptId?: string, +): Promise { + return claimDeliveryQueueEntryPlatformSend({ + queueName: OUTBOUND_DELIVERY_QUEUE_NAME, + id, + stateDir, + ...(reconciledPlatformSendStartedAt !== undefined ? { reconciledPlatformSendStartedAt } : {}), + ...(reconciledPlatformSendAttemptId !== undefined ? { reconciledPlatformSendAttemptId } : {}), + }); +} + /** Claim and atomically upgrade a live reusable producer to renewable ownership. */ export async function claimReusableDeliveryPlatformSendAttempt( id: string, @@ -30,3 +47,22 @@ export async function renewDeliveryPlatformSendLease( claimId, }); } + +/** Promote or refresh the exact live owner at recipient-visible dispatch. */ +export function markOwnedDeliveryPlatformSendDispatched( + id: string, + stateDir: string | undefined, + route: { replyToId?: string | null } | undefined, + claimId: string, +): void { + const dispatched = dispatchDeliveryQueueEntryPlatformSend({ + queueName: OUTBOUND_DELIVERY_QUEUE_NAME, + id, + stateDir, + route, + claimId, + }); + if (!dispatched) { + throw new Error(`Delivery platform claim was lost: ${id}`); + } +} diff --git a/src/infra/outbound/delivery-queue-storage.ts b/src/infra/outbound/delivery-queue-storage.ts index 90cf860fde33..480558a45f84 100644 --- a/src/infra/outbound/delivery-queue-storage.ts +++ b/src/infra/outbound/delivery-queue-storage.ts @@ -9,7 +9,6 @@ import type { import type { ReplyToMode } from "../../config/types.js"; import type { PluginHookReplyPayloadSendingContext } from "../../plugins/hook-types.js"; import { - claimDeliveryQueueEntryPlatformSend, promoteDeliveryQueueEntryPlatformSend, transitionOwnedDeliveryQueueEntry, type InitialDeliveryProducerClaim, @@ -44,6 +43,7 @@ import { OUTBOUND_DELIVERY_QUEUE_NAME, OUTBOUND_LEGACY_PREPARATION_QUEUE_NAME, } from "./delivery-queue-media-staging.js"; +import { markOwnedDeliveryPlatformSendDispatched } from "./delivery-queue-platform-lease.js"; import { StableDeliveryPreparationLostError, type StableDeliveryPreparation, @@ -481,21 +481,7 @@ export async function failDeliveryAfterPlatformSend( ); } -/** Atomically transfer a stable pending producer intent to one platform sender. */ -export async function claimDeliveryPlatformSendAttempt( - id: string, - stateDir?: string, - reconciledPlatformSendStartedAt?: number, - reconciledPlatformSendAttemptId?: string, -): Promise { - return claimDeliveryQueueEntryPlatformSend({ - queueName: OUTBOUND_DELIVERY_QUEUE_NAME, - id, - stateDir, - ...(reconciledPlatformSendStartedAt !== undefined ? { reconciledPlatformSendStartedAt } : {}), - ...(reconciledPlatformSendAttemptId !== undefined ? { reconciledPlatformSendAttemptId } : {}), - }); -} +export { claimDeliveryPlatformSendAttempt } from "./delivery-queue-platform-lease.js"; /** Reserve one durable delivery call before invoking the provider path. */ export async function reserveDeliveryAttempt( @@ -579,18 +565,23 @@ export async function markDeliveryPlatformSendDispatched( route?: { replyToId?: string | null }, expectedPlatformSendAttemptId?: string | null, ): Promise { + if (typeof expectedPlatformSendAttemptId === "string") { + markOwnedDeliveryPlatformSendDispatched(id, stateDir, route, expectedPlatformSendAttemptId); + return; + } updateQueuedDelivery( id, stateDir, (entry) => ({ ...entry, - // Dispatch still belongs to the promoted producer until provider I/O - // settles; clearing its lease lets another process replay an active send. - availableAt: expectedPlatformSendAttemptId ? entry.availableAt : undefined, + availableAt: undefined, producerClaimId: undefined, platformSendStartedAt: Date.now(), ...(route && "replyToId" in route ? { effectiveReplyToId: route.replyToId ?? null } : {}), - recoveryState: "send_attempt_started", + // A later batch send must not erase concrete evidence from an earlier result; + // recovery could otherwise replay the whole batch and duplicate that delivery. + recoveryState: + entry.recoveryState === "unknown_after_send" ? entry.recoveryState : "send_attempt_started", }), expectedPlatformSendAttemptId, ); diff --git a/src/infra/outbound/delivery-queue.storage.test.ts b/src/infra/outbound/delivery-queue.storage.test.ts index 23ec9a60878e..3bf7cce2f06c 100644 --- a/src/infra/outbound/delivery-queue.storage.test.ts +++ b/src/infra/outbound/delivery-queue.storage.test.ts @@ -547,6 +547,25 @@ describe("delivery-queue storage", () => { expect(entry.recoveryState).toBe("send_attempt_started"); }); + it("keeps ambiguous post-send evidence across a later unclaimed batch dispatch", async () => { + const id = await enqueueTextDelivery( + { + channel: "forum", + to: "123", + payloads: [{ text: "test" }], + }, + tmpDir(), + ); + + await markDeliveryPlatformSendAttemptStarted(id, tmpDir()); + await markDeliveryPlatformOutcomeUnknown(id, tmpDir()); + await markDeliveryPlatformSendDispatched(id, tmpDir()); + + // Downgrading to send_attempt_started would let recovery replay the whole + // batch as not_sent and duplicate the payload that already reached the platform. + expect(readQueuedEntry(tmpDir(), id).recoveryState).toBe("unknown_after_send"); + }); + it("increments retryCount, records attempt time, and sets lastError", async () => { const id = await enqueueTextDelivery( {