Files
openclaw/src/plugin-sdk/question-gateway-runtime.ts
T
Peter Steinberger ebb2770000 refactor: eliminate export name collisions (#122084)
* refactor: eliminate export name collisions

* chore(scripts): burn resolved collision baselines

* refactor: narrow legacy session load options

* chore: refresh SDK and session debt baselines

* refactor: adopt upstream secrets collision fix

* test(plugin-sdk): mock renamed session store core

* fix(scripts): track renamed session accessor core
2026-08-11 10:41:50 -07:00

159 lines
5.6 KiB
TypeScript

/** Runtime SDK subpath for Gateway-backed ask_user question controls. */
import { readAskUserQuestionId } from "../auto-reply/reply-payload.js";
import { registerQuestionChannelDelivery } from "../infra/question-channel-runtime.js";
// The pre-release named resolver exports were replaced wholesale by this
// runtime object before any tagged release shipped them; no compat aliases.
import { resolveQuestionOverGateway } from "../infra/question-gateway-resolver.js";
import {
QUESTION_REACTION_EMOJIS,
prepareQuestionReactionPayloadForDelivery,
readQuestionReactionBinding,
resolveQuestionReactionIndex,
resolveQuestionReactionOverGateway,
} from "../infra/question-reaction-runtime.js";
const DEFAULT_QUESTION_REACTION_TARGET_TTL_MS = 24 * 60 * 60 * 1_000;
/** Creates one channel-owned target store for numbered ask_user reactions. */
export function createQuestionReactionTargetStore<TIdentity, TMetadata = undefined>(params: {
channel: string;
channelDisplayName: string;
ttlMs?: number;
buildKey: (identity: TIdentity) => string | null | undefined;
identityMatches?: (stored: TMetadata | undefined, incoming: TMetadata | undefined) => boolean;
registerChannelDelivery?: typeof registerQuestionChannelDelivery;
resolveReaction?: typeof resolveQuestionReactionOverGateway;
}) {
type Target = {
questionId: string;
optionValues: string[];
metadata?: TMetadata;
terminal: boolean;
expiresAtMs: number;
cleanupTimer: ReturnType<typeof setTimeout>;
};
const ttlMs = params.ttlMs ?? DEFAULT_QUESTION_REACTION_TARGET_TTL_MS;
const registerChannelDelivery = params.registerChannelDelivery ?? registerQuestionChannelDelivery;
const resolveReaction = params.resolveReaction ?? resolveQuestionReactionOverGateway;
const targets = new Map<string, Target>();
const findTarget = (identities: readonly TIdentity[]): Target | undefined => {
for (const identity of identities) {
const key = params.buildKey(identity);
const target = key ? targets.get(key) : undefined;
if (target) {
return target;
}
}
return undefined;
};
return {
register(
binding: { questionId: string; optionValues: string[] },
identity: TIdentity,
metadata?: TMetadata,
): boolean {
const key = params.buildKey(identity);
if (!key) {
return false;
}
const existing = targets.get(key);
if (existing) {
clearTimeout(existing.cleanupTimer);
}
const target: Target = {
...binding,
metadata,
terminal: false,
expiresAtMs: Date.now() + ttlMs,
cleanupTimer: setTimeout(() => {
if (targets.get(key) === target) {
targets.delete(key);
}
}, ttlMs),
};
target.cleanupTimer.unref?.();
targets.set(key, target);
// Registration can synchronously finalize an already-terminal question.
registerChannelDelivery({
questionId: binding.questionId,
deliveryId: `${params.channel}-reaction:${key}`,
finalize: () => {
target.terminal = true;
},
});
return true;
},
has(identities: readonly TIdentity[]): boolean {
return Boolean(findTarget(identities));
},
async resolve(resolveParams: {
identities: readonly TIdentity[];
optionIndex: number;
cfg: Parameters<typeof resolveQuestionReactionOverGateway>[0]["cfg"];
senderId: string;
gatewayUrl?: string;
metadata?: TMetadata;
logDebug?: (message: string) => void;
}): Promise<boolean> {
const target = findTarget(resolveParams.identities);
if (
!target ||
(params.identityMatches && !params.identityMatches(target.metadata, resolveParams.metadata))
) {
return false;
}
if (target.expiresAtMs <= Date.now() || target.terminal) {
target.terminal = true;
resolveParams.logDebug?.(
`${params.channel}: stale question reaction ignored id=${target.questionId}`,
);
return true;
}
const optionValue = target.optionValues[resolveParams.optionIndex];
if (!optionValue) {
resolveParams.logDebug?.(
`${params.channel}: out-of-range question reaction ignored id=${target.questionId}`,
);
return true;
}
try {
const result = await resolveReaction({
cfg: resolveParams.cfg,
questionId: target.questionId,
optionValue,
senderId: resolveParams.senderId,
gatewayUrl: resolveParams.gatewayUrl,
clientDisplayName: `${params.channelDisplayName} question (${resolveParams.senderId})`,
});
target.terminal = result?.status === "answered" || result?.status === "already-terminal";
if (result?.status === "already-terminal") {
resolveParams.logDebug?.(
`${params.channel}: stale question reaction ignored id=${target.questionId}`,
);
}
} catch (error) {
resolveParams.logDebug?.(
`${params.channel}: question reaction failed id=${target.questionId}: ${String(error)}`,
);
}
return true;
},
};
}
export const questionGatewayRuntime = {
resolveOption: resolveQuestionOverGateway,
reactionEmojis: QUESTION_REACTION_EMOJIS,
prepareReactionPayloadForDelivery: prepareQuestionReactionPayloadForDelivery,
readAskUserQuestionId,
readReactionBinding: readQuestionReactionBinding,
resolveReactionIndex: resolveQuestionReactionIndex,
resolveReaction: resolveQuestionReactionOverGateway,
registerChannelDelivery: registerQuestionChannelDelivery,
};