Files
openclaw/extensions/msteams/src/feedback-reflection.ts
Peter Steinberger 0e792b6de3 refactor(channels): centralize inbound orchestration and remove internal compat (#109716)
* refactor(channels): centralize inbound turn orchestration

* refactor(runtime): remove stale compatibility paths

* chore(guards): reject internal deprecated API use

* refactor(channels): simplify core turn planning

* chore(guards): keep deprecated checks boundary-focused

* refactor(memory): keep modern config off compat barrel

* fix(msteams): preserve feedback learning

* test(channels): align modern inbound fixtures

* refactor(channels): finish modern inbound migration

* refactor(channels): tighten core inbound kernel

* fix(channels): preserve turn assembly narrowing

* test(sdk): keep runtime mock binding immutable

* test(matrix): isolate read policy runtime

* test(msteams): mock canonical reply factory

* test(slack): mock core inbound turn dispatch

* test(telegram): inject core session recorder

* test(signal): inject core session recorder

* test(googlechat): assert canonical inbound routing

* test(synology-chat): align core turn fixture

* fix(sdk): preserve direct DM runtime compat

* refactor(channels): own inbound envelope compat in core

* refactor(channels): trim inbound dispatch seams

* refactor(channels): remove redundant async wrappers

* test(synology-chat): type canonical dispatcher mock

* refactor(channels): remove remaining dead compat seams

* chore(sdk): refresh API baseline after rebase

* fix(channels): preserve direct DM identity metadata
2026-07-17 00:56:46 -07:00

145 lines
4.5 KiB
TypeScript

// Msteams plugin module implements feedback reflection behavior.
import {
DEFAULT_CHANNEL_FEEDBACK_REFLECTION_COOLDOWN_MS,
runChannelFeedbackReflection,
} from "openclaw/plugin-sdk/channel-inbound";
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { OpenClawConfig } from "../runtime-api.js";
import { resolveMSTeamsSdkCloudOptions } from "./cloud.js";
import type { StoredConversationReference } from "./conversation-store.js";
import { formatUnknownError } from "./errors.js";
import { storeSessionLearning } from "./feedback-reflection-store.js";
import { buildConversationReference } from "./messenger.js";
import type { MSTeamsMonitorLogger } from "./monitor-types.js";
import { sendMSTeamsActivityWithReference } from "./sdk-proactive.js";
import type { MSTeamsApp } from "./sdk.js";
type FeedbackEvent = {
type: "custom";
event: "feedback";
ts: number;
messageId: string;
value: "positive" | "negative";
comment?: string;
sessionKey: string;
agentId: string;
conversationId: string;
};
export function buildFeedbackEvent(params: {
messageId: string;
value: "positive" | "negative";
comment?: string;
sessionKey: string;
agentId: string;
conversationId: string;
}): FeedbackEvent {
return {
type: "custom",
event: "feedback",
ts: Date.now(),
messageId: params.messageId,
value: params.value,
comment: params.comment,
sessionKey: params.sessionKey,
agentId: params.agentId,
conversationId: params.conversationId,
};
}
type RunFeedbackReflectionParams = {
cfg: OpenClawConfig;
app: MSTeamsApp;
conversationRef: StoredConversationReference;
sessionKey: string;
agentId: string;
conversationId: string;
conversationKind: "direct" | "group" | "channel";
thumbedDownResponse?: string;
userComment?: string;
log: MSTeamsMonitorLogger;
};
/**
* Run a background reflection after negative feedback.
* This is designed to be called fire-and-forget (don't await in the invoke handler).
*/
export async function runFeedbackReflection(params: RunFeedbackReflectionParams): Promise<void> {
const { cfg, log, sessionKey } = params;
const cooldownMs =
cfg.channels?.msteams?.feedbackReflectionCooldownMs ??
DEFAULT_CHANNEL_FEEDBACK_REFLECTION_COOLDOWN_MS;
let reflection;
try {
reflection = await runChannelFeedbackReflection({
cfg,
channel: "msteams",
channelLabel: "Teams",
agentId: params.agentId,
sessionKey,
conversationId: params.conversationId,
conversationKind: params.conversationKind,
thumbedDownResponse: params.thumbedDownResponse,
userComment: params.userComment,
cooldownMs,
onRecordError: (err) =>
log.debug?.("reflection session record failed", { error: formatUnknownError(err) }),
onDispatchError: (err) =>
log.debug?.("reflection reply error", { error: formatUnknownError(err) }),
});
} catch (err) {
log.error("reflection dispatch failed", { error: formatUnknownError(err) });
return;
}
if (reflection.status === "cooldown") {
log.debug?.("skipping reflection (cooldown active)", { sessionKey });
return;
}
if (reflection.status === "empty") {
log.debug?.("reflection produced no output");
return;
}
log.info("reflection complete", {
sessionKey,
responseLength: reflection.responseLength,
followUp: reflection.followUp,
});
try {
await storeSessionLearning({
storePath: reflection.storePath,
sessionKey,
learning: reflection.learning,
});
} catch (err) {
log.debug?.("failed to store reflection learning", { error: formatUnknownError(err) });
}
const conversationType = normalizeOptionalLowercaseString(
params.conversationRef.conversation?.conversationType,
);
const shouldNotify =
conversationType === "personal" && reflection.followUp && Boolean(reflection.userMessage);
if (!shouldNotify) {
if (reflection.followUp && conversationType !== "personal") {
log.debug?.("skipping reflection follow-up outside direct message", {
sessionKey,
conversationType,
});
}
return;
}
try {
await sendMSTeamsActivityWithReference(
params.app,
buildConversationReference(params.conversationRef),
{ type: "message", text: reflection.userMessage! },
{ serviceUrlBoundary: resolveMSTeamsSdkCloudOptions(cfg.channels?.msteams) },
);
log.info("sent reflection follow-up", { sessionKey });
} catch (err) {
log.debug?.("failed to send reflection follow-up", { error: formatUnknownError(err) });
}
}