From 6ca9de1e0a773140d8d1a42d7b87e9e5559b8b7a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 15 May 2026 18:44:04 +0100 Subject: [PATCH] refactor: deprecate legacy reply history helpers (#82236) --- docs/plugins/sdk-channel-turn.md | 12 ++-- docs/plugins/sdk-migration.md | 2 +- docs/plugins/sdk-subpaths.md | 2 +- extensions/mattermost/runtime-api.ts | 2 + .../mattermost/src/mattermost/runtime-api.ts | 2 + extensions/mattermost/src/runtime-api.ts | 2 + src/auto-reply/reply/history.ts | 36 ++++++++++ .../turn/message-turn-guardrails.test.ts | 71 ++++++++++++++++++- src/plugin-sdk/mattermost.ts | 1 + src/plugin-sdk/reply-history.ts | 4 +- .../contracts/plugin-sdk-subpaths.test.ts | 2 + 11 files changed, 125 insertions(+), 11 deletions(-) diff --git a/docs/plugins/sdk-channel-turn.md b/docs/plugins/sdk-channel-turn.md index bfbf4886d64..6eb65b86279 100644 --- a/docs/plugins/sdk-channel-turn.md +++ b/docs/plugins/sdk-channel-turn.md @@ -360,10 +360,12 @@ plugin before they become model-visible media. ## History windows Message-turn code should use `createChannelHistoryWindow(...)` instead of -calling low-level `reply-history` map helpers directly. The window facade keeps -text context, structured `InboundHistory`, history-media normalization, and -clearing behind one core-owned API while still letting the channel choose how a -history line is rendered. +calling low-level `reply-history` map helpers directly. The old map helpers +remain importable as deprecated compatibility exports, but new plugin runtime +code should not call them. The window facade keeps text context, structured +`InboundHistory`, history-media normalization, and clearing behind one +core-owned API while still letting the channel choose how a history line is +rendered. ```typescript const history = createChannelHistoryWindow({ historyMap: groupHistories }); @@ -389,7 +391,7 @@ const combinedBody = history.buildPendingContext({ The older `buildPendingHistoryContextFromMap`, `buildInboundHistoryFromMap`, `recordPendingHistoryEntry*`, and -`clearHistoryEntriesIfEnabled` exports remain for compatibility with plugins +`clearHistoryEntries*` exports remain as deprecated compatibility for plugins that have not migrated yet. New channel work should use the window or the turn kernel record/finalize options. diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 5f0be841d0e..cc08df12f93 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -572,7 +572,7 @@ releases. | `plugin-sdk/webhook-request-guards` | Webhook body guard helpers | Request body read/limit helpers | | `plugin-sdk/reply-runtime` | Shared reply runtime | Inbound dispatch, heartbeat, reply planner, chunking | | `plugin-sdk/reply-dispatch-runtime` | Narrow reply dispatch helpers | Finalize, provider dispatch, and conversation-label helpers | - | `plugin-sdk/reply-history` | Reply-history helpers | `buildHistoryContext`, `buildPendingHistoryContextFromMap`, `recordPendingHistoryEntry`, `clearHistoryEntriesIfEnabled` | + | `plugin-sdk/reply-history` | Reply-history helpers | `createChannelHistoryWindow`; deprecated map-helper compatibility exports such as `buildPendingHistoryContextFromMap`, `recordPendingHistoryEntry`, and `clearHistoryEntriesIfEnabled` | | `plugin-sdk/reply-reference` | Reply reference planning | `createReplyReferencePlanner` | | `plugin-sdk/reply-chunking` | Reply chunk helpers | Text/markdown chunking helpers | | `plugin-sdk/session-store-runtime` | Session store helpers | Store path + updated-at helpers | diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index 17dad14e560..8aaea69bcff 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -244,7 +244,7 @@ focused channel/runtime subpaths, `config-contracts`, `string-coerce-runtime`, | `plugin-sdk/approval-runtime` | Exec/plugin approval helpers, approval-capability builders, auth/profile helpers, native routing/runtime helpers, and structured approval display path formatting | | `plugin-sdk/reply-runtime` | Shared inbound/reply runtime helpers, chunking, dispatch, heartbeat, reply planner | | `plugin-sdk/reply-dispatch-runtime` | Narrow reply dispatch/finalize and conversation-label helpers | - | `plugin-sdk/reply-history` | Shared short-window reply-history compatibility helpers. New message-turn code should use `createChannelHistoryWindow` rather than the lower-level map helpers | + | `plugin-sdk/reply-history` | Shared short-window reply-history helpers. New message-turn code should use `createChannelHistoryWindow`; lower-level map helpers remain deprecated compatibility exports only | | `plugin-sdk/reply-reference` | `createReplyReferencePlanner` | | `plugin-sdk/reply-chunking` | Narrow text/markdown chunking helpers | | `plugin-sdk/session-store-runtime` | Session store path, session-key, updated-at, and store mutation helpers | diff --git a/extensions/mattermost/runtime-api.ts b/extensions/mattermost/runtime-api.ts index 8d19b2d805a..1e45d90e1e0 100644 --- a/extensions/mattermost/runtime-api.ts +++ b/extensions/mattermost/runtime-api.ts @@ -55,6 +55,8 @@ export { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback"; export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media"; export { rawDataToString } from "openclaw/plugin-sdk/webhook-ingress"; export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking"; +// Legacy map-helper exports stay for older plugin consumers. New message-turn +// code should use createChannelHistoryWindow. export { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow, diff --git a/extensions/mattermost/src/mattermost/runtime-api.ts b/extensions/mattermost/src/mattermost/runtime-api.ts index 229d582e24e..a2f6cd8918d 100644 --- a/extensions/mattermost/src/mattermost/runtime-api.ts +++ b/extensions/mattermost/src/mattermost/runtime-api.ts @@ -28,6 +28,8 @@ export { } from "openclaw/plugin-sdk/runtime-group-policy"; export { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/media-runtime"; export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media"; +// Legacy map-helper exports stay for older plugin consumers. New message-turn +// code should use createChannelHistoryWindow. export { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow, diff --git a/extensions/mattermost/src/runtime-api.ts b/extensions/mattermost/src/runtime-api.ts index 6b5b8c3fb19..f9bdc4bf46b 100644 --- a/extensions/mattermost/src/runtime-api.ts +++ b/extensions/mattermost/src/runtime-api.ts @@ -1,3 +1,5 @@ +// Legacy map-helper exports in this facade stay for older plugin consumers. +// New message-turn code should use createChannelHistoryWindow. export { applyAccountNameToChannelSection, applySetupAccountConfigPatch, diff --git a/src/auto-reply/reply/history.ts b/src/auto-reply/reply/history.ts index e48eeca9103..760ccb85853 100644 --- a/src/auto-reply/reply/history.ts +++ b/src/auto-reply/reply/history.ts @@ -71,6 +71,10 @@ export function appendHistoryEntry(params: { return history; } +/** + * @deprecated Plugin message-turn code should use `createChannelHistoryWindow(...).record(...)`. + * This helper remains for core internals and older plugin compatibility. + */ export function recordPendingHistoryEntry(params: { historyMap: Map; historyKey: string; @@ -80,6 +84,10 @@ export function recordPendingHistoryEntry(params: { return appendHistoryEntry(params); } +/** + * @deprecated Plugin message-turn code should use `createChannelHistoryWindow(...).record(...)`. + * This helper remains for core internals and older plugin compatibility. + */ export function recordPendingHistoryEntryIfEnabled(params: { historyMap: Map; historyKey: string; @@ -153,6 +161,11 @@ export function normalizeHistoryMediaEntries(params: { return out; } +/** + * @deprecated Plugin message-turn code should use + * `createChannelHistoryWindow(...).recordWithMedia(...)`. This helper remains + * for core internals and older plugin compatibility. + */ export async function recordPendingHistoryEntryWithMedia(params: { historyMap: Map; historyKey: string; @@ -217,6 +230,11 @@ export async function recordPendingHistoryEntryWithMedia }); } +/** + * @deprecated Plugin message-turn code should use + * `createChannelHistoryWindow(...).buildPendingContext(...)`. This helper remains + * for core internals and older plugin compatibility. + */ export function buildPendingHistoryContextFromMap(params: { historyMap: Map; historyKey: string; @@ -238,6 +256,11 @@ export function buildPendingHistoryContextFromMap(params: { }); } +/** + * @deprecated Plugin message-turn code should use + * `createChannelHistoryWindow(...).buildInboundHistory(...)`. This helper remains + * for core internals and older plugin compatibility. + */ export function buildInboundHistoryFromMap(params: { historyMap: Map; historyKey: string; @@ -275,6 +298,11 @@ export function buildInboundHistoryFromEntries(params: { }); } +/** + * @deprecated Prefer `buildHistoryContextFromEntries(...)` for existing entry + * arrays, or `createChannelHistoryWindow(...)` when working from a history map. + * This helper remains for older plugin compatibility. + */ export function buildHistoryContextFromMap(params: { historyMap: Map; historyKey: string; @@ -305,6 +333,10 @@ export function buildHistoryContextFromMap(params: { }); } +/** + * @deprecated Plugin message-turn code should use `createChannelHistoryWindow(...).clear(...)`. + * This helper remains for core internals and older plugin compatibility. + */ export function clearHistoryEntries(params: { historyMap: Map; historyKey: string; @@ -312,6 +344,10 @@ export function clearHistoryEntries(params: { params.historyMap.set(params.historyKey, []); } +/** + * @deprecated Plugin message-turn code should use `createChannelHistoryWindow(...).clear(...)`. + * This helper remains for core internals and older plugin compatibility. + */ export function clearHistoryEntriesIfEnabled(params: { historyMap: Map; historyKey: string; diff --git a/src/channels/turn/message-turn-guardrails.test.ts b/src/channels/turn/message-turn-guardrails.test.ts index 1878bcdc3dc..aeafa08f66b 100644 --- a/src/channels/turn/message-turn-guardrails.test.ts +++ b/src/channels/turn/message-turn-guardrails.test.ts @@ -1,4 +1,4 @@ -import { readFileSync } from "node:fs"; +import { readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; @@ -43,15 +43,63 @@ const historyWindowFiles = [ const lowLevelHistoryHelpers = [ "buildInboundHistoryFromMap", + "buildHistoryContextFromMap", "buildPendingHistoryContextFromMap", + "clearHistoryEntries", "clearHistoryEntriesIfEnabled", "recordPendingHistoryEntry", "recordPendingHistoryEntryIfEnabled", "recordPendingHistoryEntryWithMedia", ]; +const legacyReplyHistoryCompatibilityFiles = new Set([ + "extensions/mattermost/runtime-api.ts", + "extensions/mattermost/src/mattermost/runtime-api.ts", + "extensions/mattermost/src/runtime-api.ts", +]); + +const skippedExtensionScanDirs = new Set([ + ".cache", + ".turbo", + "build", + "coverage", + "dist", + "node_modules", + "tmp", +]); + function readRepoFile(relativePath: string): string { - return readFileSync(path.join(repoRoot, relativePath), "utf8"); + return readFileSync(path.join(repoRoot, ...relativePath.split("/")), "utf8"); +} + +function listTsFiles(relativeDir: string): string[] { + const dir = path.join(repoRoot, ...relativeDir.split("/")); + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const relativePath = path.posix.join(relativeDir, entry.name); + if (entry.isDirectory()) { + if (skippedExtensionScanDirs.has(entry.name)) { + return []; + } + return listTsFiles(relativePath); + } + if (!entry.isFile() || !entry.name.endsWith(".ts") || entry.name.endsWith(".d.ts")) { + return []; + } + return [relativePath]; + }); +} + +function collectReplyHistoryBindings(source: string): Set { + const bindings = new Set(); + const importOrExportPattern = + /\b(?:import|export)\s*\{([\s\S]*?)\}\s*from\s*["']openclaw\/plugin-sdk\/reply-history["']/g; + for (const match of source.matchAll(importOrExportPattern)) { + const block = match[1] ?? ""; + for (const nameMatch of block.matchAll(/\b[A-Za-z_][A-Za-z0-9_]*\b/g)) { + bindings.add(nameMatch[0]); + } + } + return bindings; } describe("message turn migration guardrails", () => { @@ -73,4 +121,23 @@ describe("message turn migration guardrails", () => { ); } }); + + it("keeps plugin runtime files off deprecated reply-history map helpers", () => { + for (const file of listTsFiles("extensions")) { + if (file.includes(".test.") || file.endsWith(".test.ts")) { + continue; + } + if (legacyReplyHistoryCompatibilityFiles.has(file)) { + continue; + } + const source = readRepoFile(file); + const replyHistoryBindings = collectReplyHistoryBindings(source); + for (const helper of lowLevelHistoryHelpers) { + expect( + replyHistoryBindings.has(helper), + `${file} should use createChannelHistoryWindow instead of ${helper}`, + ).toBe(false); + } + } + }); }); diff --git a/src/plugin-sdk/mattermost.ts b/src/plugin-sdk/mattermost.ts index 31c0b62ff5c..6ce697296c0 100644 --- a/src/plugin-sdk/mattermost.ts +++ b/src/plugin-sdk/mattermost.ts @@ -7,6 +7,7 @@ export { resolveControlCommandGate } from "./command-auth.js"; export { formatPairingApproveHint } from "./channel-plugin-common.js"; export type { HistoryEntry } from "./reply-history.js"; export { + createChannelHistoryWindow, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled, diff --git a/src/plugin-sdk/reply-history.ts b/src/plugin-sdk/reply-history.ts index e93392fdad3..8277ce464dc 100644 --- a/src/plugin-sdk/reply-history.ts +++ b/src/plugin-sdk/reply-history.ts @@ -1,8 +1,8 @@ /** * Shared reply-history helpers for plugins that keep short per-thread context windows. * - * Prefer `createChannelHistoryWindow` for message-turn code. The lower-level map helpers remain - * exported for older plugins and adapter bridges that have not migrated to the channel turn kernel. + * Prefer `createChannelHistoryWindow` for message-turn code. The lower-level map helpers are + * deprecated plugin compatibility exports; core internals still use them behind the facade. */ export type { HistoryEntry, HistoryMediaEntry } from "../auto-reply/reply/history.types.js"; export { diff --git a/src/plugins/contracts/plugin-sdk-subpaths.test.ts b/src/plugins/contracts/plugin-sdk-subpaths.test.ts index 460d3e58bdc..151e4fada61 100644 --- a/src/plugins/contracts/plugin-sdk-subpaths.test.ts +++ b/src/plugins/contracts/plugin-sdk-subpaths.test.ts @@ -551,11 +551,13 @@ describe("plugin-sdk subpath exports", () => { "buildInboundHistoryFromMap", "buildPendingHistoryContextFromMap", "clearHistoryEntriesIfEnabled", + "createChannelHistoryWindow", "recordPendingHistoryEntryIfEnabled", ]); expectSourceMentions("mattermost", [ "buildPendingHistoryContextFromMap", "clearHistoryEntriesIfEnabled", + "createChannelHistoryWindow", "formatPairingApproveHint", "recordPendingHistoryEntryIfEnabled", "resolveControlCommandGate",