mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-29 23:08:46 +00:00
Address the ClawSweeper R2 finding that the pre-bind stopped guard introduced in this PR drops a delivered entry without any cleanup. The prior PR comment block was correct only for adapters whose deliverPending has no in-process side effects; Matrix registers a reaction target in both an in-memory Map and a persistent store inside deliverPending, so the entry would leak until the 24h TTL (or process restart) every time stop() landed between deliverPending and bindPending. Add an optional cancelDelivered interaction hook on the runtime types, forward it through both the spec-to-adapter wrapper (createChannelApprovalNativeRuntimeAdapter) and the lazy adapter wrapper (createLazyChannelApprovalNativeRuntimeAdapter), and invoke it from the two stopped guards in deliverTarget: the pre-bind guard always calls it, and the post-bind guard calls it on the branch where bindPending returned no handle (so unbindPending cannot run). Matrix implements the hook by calling unregisterMatrixApprovalReactionTarget on the entry's roomId + reactionEventId, which is the exact key registerMatrixApprovalReactionTarget uses inside deliverPending. The other native runtime adapters (Slack, Discord, Telegram, qqbot) leave the hook unimplemented because their deliverPending paths only emit remote messages and keep no in-process state to drop. Regression coverage: - invokes cancelDelivered when stop() fires between deliverPending and bindPending (Deferred-gated deliverPending, asserts bindPending / unbindPending never run and cancelDelivered receives the entry) - invokes cancelDelivered when stop() fires after bindPending returned null (asserts unbindPending stays uncalled while cancelDelivered fires) AI-assisted: drafted with claude code (claude-opus-4-7).
299 lines
9.6 KiB
TypeScript
299 lines
9.6 KiB
TypeScript
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import type { ChannelApprovalNativePlannedTarget } from "./approval-native-delivery.js";
|
|
import type { PreparedChannelNativeApprovalTarget } from "./approval-native-runtime.js";
|
|
import type { ChannelApprovalKind } from "./approval-types.js";
|
|
import type {
|
|
ExpiredApprovalView,
|
|
PendingApprovalView,
|
|
ResolvedApprovalView,
|
|
} from "./approval-view-model.types.js";
|
|
import type { ExecApprovalChannelRuntimeEventKind } from "./exec-approval-channel-runtime.types.js";
|
|
import type { ExecApprovalRequest, ExecApprovalResolved } from "./exec-approvals.js";
|
|
import type { PluginApprovalRequest, PluginApprovalResolved } from "./plugin-approvals.js";
|
|
|
|
export type { ChannelApprovalKind } from "./approval-types.js";
|
|
|
|
export type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
|
export type ApprovalResolved = ExecApprovalResolved | PluginApprovalResolved;
|
|
|
|
export type ChannelApprovalCapabilityHandlerContext = {
|
|
cfg: OpenClawConfig;
|
|
accountId?: string | null;
|
|
gatewayUrl?: string;
|
|
context?: unknown;
|
|
};
|
|
|
|
export type ChannelApprovalNativeFinalAction<TPayload> =
|
|
| { kind: "update"; payload: TPayload }
|
|
| { kind: "delete" }
|
|
| { kind: "clear-actions" }
|
|
| { kind: "leave" };
|
|
|
|
export type ChannelApprovalNativeAvailabilityAdapter = {
|
|
isConfigured: (params: ChannelApprovalCapabilityHandlerContext) => boolean;
|
|
shouldHandle: (
|
|
params: ChannelApprovalCapabilityHandlerContext & { request: ApprovalRequest },
|
|
) => boolean;
|
|
};
|
|
|
|
export type ChannelApprovalNativePresentationAdapter<
|
|
TPendingPayload = unknown,
|
|
TFinalPayload = unknown,
|
|
> = {
|
|
buildPendingPayload: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
request: ApprovalRequest;
|
|
approvalKind: ChannelApprovalKind;
|
|
nowMs: number;
|
|
view: PendingApprovalView;
|
|
},
|
|
) => TPendingPayload | Promise<TPendingPayload>;
|
|
buildResolvedResult: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
request: ApprovalRequest;
|
|
resolved: ApprovalResolved;
|
|
view: ResolvedApprovalView;
|
|
entry: unknown;
|
|
},
|
|
) =>
|
|
| ChannelApprovalNativeFinalAction<TFinalPayload>
|
|
| Promise<ChannelApprovalNativeFinalAction<TFinalPayload>>;
|
|
buildExpiredResult: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
request: ApprovalRequest;
|
|
view: ExpiredApprovalView;
|
|
entry: unknown;
|
|
},
|
|
) =>
|
|
| ChannelApprovalNativeFinalAction<TFinalPayload>
|
|
| Promise<ChannelApprovalNativeFinalAction<TFinalPayload>>;
|
|
};
|
|
|
|
type ChannelApprovalNativeTransportAdapterForView<
|
|
TPreparedTarget = unknown,
|
|
TPendingEntry = unknown,
|
|
TPendingPayload = unknown,
|
|
TFinalPayload = unknown,
|
|
TPendingView extends PendingApprovalView = PendingApprovalView,
|
|
> = {
|
|
prepareTarget: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
plannedTarget: ChannelApprovalNativePlannedTarget;
|
|
request: ApprovalRequest;
|
|
approvalKind: ChannelApprovalKind;
|
|
view: TPendingView;
|
|
pendingPayload: TPendingPayload;
|
|
},
|
|
) =>
|
|
| PreparedChannelNativeApprovalTarget<TPreparedTarget>
|
|
| null
|
|
| Promise<PreparedChannelNativeApprovalTarget<TPreparedTarget> | null>;
|
|
deliverPending: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
plannedTarget: ChannelApprovalNativePlannedTarget;
|
|
preparedTarget: TPreparedTarget;
|
|
request: ApprovalRequest;
|
|
approvalKind: ChannelApprovalKind;
|
|
view: TPendingView;
|
|
pendingPayload: TPendingPayload;
|
|
},
|
|
) => TPendingEntry | null | Promise<TPendingEntry | null>;
|
|
updateEntry?: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
entry: TPendingEntry;
|
|
payload: TFinalPayload;
|
|
phase: "resolved" | "expired";
|
|
},
|
|
) => Promise<void>;
|
|
deleteEntry?: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
entry: TPendingEntry;
|
|
phase: "resolved" | "expired";
|
|
},
|
|
) => Promise<void>;
|
|
};
|
|
|
|
export type ChannelApprovalNativeTransportAdapter<
|
|
TPreparedTarget = unknown,
|
|
TPendingEntry = unknown,
|
|
TPendingPayload = unknown,
|
|
TFinalPayload = unknown,
|
|
> = ChannelApprovalNativeTransportAdapterForView<
|
|
TPreparedTarget,
|
|
TPendingEntry,
|
|
TPendingPayload,
|
|
TFinalPayload
|
|
>;
|
|
|
|
type ChannelApprovalNativeInteractionAdapterForView<
|
|
TPendingEntry = unknown,
|
|
TBinding = unknown,
|
|
TPendingPayload = unknown,
|
|
TPendingView extends PendingApprovalView = PendingApprovalView,
|
|
> = {
|
|
bindPending?: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
entry: TPendingEntry;
|
|
request: ApprovalRequest;
|
|
approvalKind: ChannelApprovalKind;
|
|
view: TPendingView;
|
|
pendingPayload: TPendingPayload;
|
|
},
|
|
) => TBinding | null | Promise<TBinding | null>;
|
|
unbindPending?: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
entry: TPendingEntry;
|
|
binding: TBinding;
|
|
request: ApprovalRequest;
|
|
approvalKind: ChannelApprovalKind;
|
|
},
|
|
) => Promise<void> | void;
|
|
clearPendingActions?: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
entry: TPendingEntry;
|
|
phase: "resolved" | "expired";
|
|
},
|
|
) => Promise<void>;
|
|
cancelDelivered?: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
entry: TPendingEntry;
|
|
request: ApprovalRequest;
|
|
approvalKind: ChannelApprovalKind;
|
|
},
|
|
) => Promise<void> | void;
|
|
};
|
|
|
|
export type ChannelApprovalNativeInteractionAdapter<
|
|
TPendingEntry = unknown,
|
|
TBinding = unknown,
|
|
> = ChannelApprovalNativeInteractionAdapterForView<TPendingEntry, TBinding>;
|
|
|
|
type ChannelApprovalNativeObserveAdapterForView<
|
|
TPreparedTarget = unknown,
|
|
TPendingPayload = unknown,
|
|
TPendingEntry = unknown,
|
|
TPendingView extends PendingApprovalView = PendingApprovalView,
|
|
> = {
|
|
onDeliveryError?: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
error: unknown;
|
|
plannedTarget: ChannelApprovalNativePlannedTarget;
|
|
request: ApprovalRequest;
|
|
approvalKind: ChannelApprovalKind;
|
|
view: TPendingView;
|
|
pendingPayload: TPendingPayload;
|
|
},
|
|
) => void;
|
|
onDuplicateSkipped?: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
plannedTarget: ChannelApprovalNativePlannedTarget;
|
|
preparedTarget: PreparedChannelNativeApprovalTarget<TPreparedTarget>;
|
|
request: ApprovalRequest;
|
|
approvalKind: ChannelApprovalKind;
|
|
view: TPendingView;
|
|
pendingPayload: TPendingPayload;
|
|
},
|
|
) => void;
|
|
onDelivered?: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
plannedTarget: ChannelApprovalNativePlannedTarget;
|
|
preparedTarget: PreparedChannelNativeApprovalTarget<TPreparedTarget>;
|
|
request: ApprovalRequest;
|
|
approvalKind: ChannelApprovalKind;
|
|
view: TPendingView;
|
|
pendingPayload: TPendingPayload;
|
|
entry: TPendingEntry;
|
|
},
|
|
) => void;
|
|
};
|
|
|
|
export type ChannelApprovalNativeObserveAdapter<
|
|
TPreparedTarget = unknown,
|
|
TPendingPayload = unknown,
|
|
TPendingEntry = unknown,
|
|
> = ChannelApprovalNativeObserveAdapterForView<TPreparedTarget, TPendingPayload, TPendingEntry>;
|
|
|
|
export type ChannelApprovalNativeRuntimeAdapter<
|
|
TPendingPayload = unknown,
|
|
TPreparedTarget = unknown,
|
|
TPendingEntry = unknown,
|
|
TBinding = unknown,
|
|
TFinalPayload = unknown,
|
|
> = {
|
|
eventKinds?: readonly ExecApprovalChannelRuntimeEventKind[];
|
|
resolveApprovalKind?: (request: ApprovalRequest) => ChannelApprovalKind;
|
|
availability: ChannelApprovalNativeAvailabilityAdapter;
|
|
presentation: ChannelApprovalNativePresentationAdapter<TPendingPayload, TFinalPayload>;
|
|
transport: ChannelApprovalNativeTransportAdapter<
|
|
TPreparedTarget,
|
|
TPendingEntry,
|
|
TPendingPayload,
|
|
TFinalPayload
|
|
>;
|
|
interactions?: ChannelApprovalNativeInteractionAdapter<TPendingEntry, TBinding>;
|
|
observe?: ChannelApprovalNativeObserveAdapter;
|
|
};
|
|
|
|
export type ChannelApprovalNativeRuntimeSpec<
|
|
TPendingPayload,
|
|
TPreparedTarget,
|
|
TPendingEntry,
|
|
TBinding = unknown,
|
|
TFinalPayload = unknown,
|
|
TPendingView extends PendingApprovalView = PendingApprovalView,
|
|
TResolvedView extends ResolvedApprovalView = ResolvedApprovalView,
|
|
TExpiredView extends ExpiredApprovalView = ExpiredApprovalView,
|
|
> = {
|
|
eventKinds?: readonly ExecApprovalChannelRuntimeEventKind[];
|
|
resolveApprovalKind?: (request: ApprovalRequest) => ChannelApprovalKind;
|
|
availability: ChannelApprovalNativeAvailabilityAdapter;
|
|
presentation: {
|
|
buildPendingPayload: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
request: ApprovalRequest;
|
|
approvalKind: ChannelApprovalKind;
|
|
nowMs: number;
|
|
view: TPendingView;
|
|
},
|
|
) => TPendingPayload | Promise<TPendingPayload>;
|
|
buildResolvedResult: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
request: ApprovalRequest;
|
|
resolved: ApprovalResolved;
|
|
view: TResolvedView;
|
|
entry: TPendingEntry;
|
|
},
|
|
) =>
|
|
| ChannelApprovalNativeFinalAction<TFinalPayload>
|
|
| Promise<ChannelApprovalNativeFinalAction<TFinalPayload>>;
|
|
buildExpiredResult: (
|
|
params: ChannelApprovalCapabilityHandlerContext & {
|
|
request: ApprovalRequest;
|
|
view: TExpiredView;
|
|
entry: TPendingEntry;
|
|
},
|
|
) =>
|
|
| ChannelApprovalNativeFinalAction<TFinalPayload>
|
|
| Promise<ChannelApprovalNativeFinalAction<TFinalPayload>>;
|
|
};
|
|
transport: ChannelApprovalNativeTransportAdapterForView<
|
|
TPreparedTarget,
|
|
TPendingEntry,
|
|
TPendingPayload,
|
|
TFinalPayload,
|
|
TPendingView
|
|
>;
|
|
interactions?: ChannelApprovalNativeInteractionAdapterForView<
|
|
TPendingEntry,
|
|
TBinding,
|
|
TPendingPayload,
|
|
TPendingView
|
|
>;
|
|
observe?: ChannelApprovalNativeObserveAdapterForView<
|
|
TPreparedTarget,
|
|
TPendingPayload,
|
|
TPendingEntry,
|
|
TPendingView
|
|
>;
|
|
};
|