From a1dfd47edd45bfd6c555ed71c6754b11942210fb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 16 Jul 2026 22:35:29 -0700 Subject: [PATCH] refactor(sdk): collapse plan updates to typed steps (#109660) * refactor(sdk): collapse plan updates to typed steps Per owner decision, remove the shipped steps: string[] SDK field and its unreleased planSteps replacement early. Collapse onPlanUpdate to one typed steps field. Retain wire-level string normalization for external Codex plugin version skew. * chore(sdk): tighten rebased surface budget --- .../monitor/message-handler.process.test.ts | 6 ++--- .../src/monitor/message-handler.process.ts | 2 +- .../matrix/src/matrix/monitor/handler.test.ts | 7 +++--- .../matrix/src/matrix/monitor/handler.ts | 2 +- extensions/msteams/src/reply-dispatcher.ts | 2 +- .../dispatch.preview-fallback.test.ts | 15 ++++-------- .../src/monitor/message-handler/dispatch.ts | 2 +- .../src/bot-message-dispatch-progress.ts | 2 +- ...essage-dispatch.progress-rendering.test.ts | 4 ++-- scripts/plugin-sdk-surface-report.mjs | 14 ++++++----- src/auto-reply/get-reply-options.types.ts | 9 +------ .../reply/agent-runner-cli-dispatch.test.ts | 6 ++--- .../reply/agent-runner-cli-dispatch.ts | 4 ++-- .../reply/agent-runner-event-handler.ts | 4 ++-- ...nt-runner-execution-command-events.test.ts | 3 +-- ...from-config.abort-and-dedupe.test-utils.ts | 2 +- ...ispatch-from-config.progress.test-utils.ts | 12 +++++----- ...dispatch-from-config.routing.test-utils.ts | 6 ++--- src/auto-reply/reply/dispatch-from-config.ts | 17 ++++++------- src/auto-reply/reply/followup-runner.ts | 4 ++-- src/channels/streaming.test.ts | 11 +-------- src/channels/streaming.ts | 24 ++----------------- 22 files changed, 54 insertions(+), 104 deletions(-) diff --git a/extensions/discord/src/monitor/message-handler.process.test.ts b/extensions/discord/src/monitor/message-handler.process.test.ts index f2af3f7f2393..ce9f45531243 100644 --- a/extensions/discord/src/monitor/message-handler.process.test.ts +++ b/extensions/discord/src/monitor/message-handler.process.test.ts @@ -167,8 +167,7 @@ type DispatchInboundParams = { onPlanUpdate?: (payload: { phase?: string; explanation?: string; - steps?: string[]; - planSteps?: Array<{ step: string; status: "pending" | "in_progress" | "completed" }>; + steps?: Array<{ step: string; status: "pending" | "in_progress" | "completed" }>; }) => Promise | void; onApprovalEvent?: (payload: { phase?: string; command?: string }) => Promise | void; onCommandOutput?: (payload: { @@ -3243,8 +3242,7 @@ describe("processDiscordMessage draft streaming", () => { await params?.replyOptions?.onPlanUpdate?.({ phase: "update", explanation: "Implementing the change.", - steps: ["Inspect", "Patch", "Test"], - planSteps: [ + steps: [ { step: "Inspect", status: "completed" }, { step: "Patch", status: "in_progress" }, { step: "Test", status: "pending" }, diff --git a/extensions/discord/src/monitor/message-handler.process.ts b/extensions/discord/src/monitor/message-handler.process.ts index 9411d4ceaaf7..9629e33ab6cf 100644 --- a/extensions/discord/src/monitor/message-handler.process.ts +++ b/extensions/discord/src/monitor/message-handler.process.ts @@ -1188,7 +1188,7 @@ async function processDiscordMessageInner( if (payload.phase !== "update") { return; } - await draftPreview.pushPlanProgress(payload.planSteps, { + await draftPreview.pushPlanProgress(payload.steps, { explanation: payload.explanation, }); }, diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index 00dc7bde177f..29713aae400d 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -2952,8 +2952,7 @@ describe("matrix monitor handler draft streaming", () => { onPlanUpdate?: (payload: { phase: string; explanation?: string; - steps?: string[]; - planSteps?: Array<{ step: string; status: "pending" | "in_progress" | "completed" }>; + steps?: Array<{ step: string; status: "pending" | "in_progress" | "completed" }>; }) => Promise; onApprovalEvent?: (payload: { phase: string; command?: string }) => Promise; onCommandOutput?: (payload: { @@ -3126,7 +3125,7 @@ describe("matrix monitor handler draft streaming", () => { await opts.onPlanUpdate?.({ phase: "update", explanation: "Initial plan", - planSteps: [{ step: "Inspect", status: "in_progress" }], + steps: [{ step: "Inspect", status: "in_progress" }], }); await vi.waitFor(() => { expect(singleTextMessageBody()).toBe("`Initial plan`\n\n`▸ Inspect`"); @@ -3135,7 +3134,7 @@ describe("matrix monitor handler draft streaming", () => { await opts.onPlanUpdate?.({ phase: "update", explanation: "Revised plan", - planSteps: [ + steps: [ { step: "Inspect", status: "completed" }, { step: "Patch", status: "in_progress" }, ], diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 1ae06b184491..6604b5cfa73b 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -1969,7 +1969,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam if (payload.phase !== "update") { return; } - await pushPlanProgress(payload.planSteps, payload.explanation); + await pushPlanProgress(payload.steps, payload.explanation); }, onApprovalEvent: async (payload) => { if (payload.phase !== "requested") { diff --git a/extensions/msteams/src/reply-dispatcher.ts b/extensions/msteams/src/reply-dispatcher.ts index cd0168c717dd..98f4d6353049 100644 --- a/extensions/msteams/src/reply-dispatcher.ts +++ b/extensions/msteams/src/reply-dispatcher.ts @@ -460,7 +460,7 @@ export function createMSTeamsReplyDispatcher(params: { if (payload?.phase !== "update") { return; } - await streamController.pushPlanProgress(normalizeAgentPlanSteps(payload.planSteps), { + await streamController.pushPlanProgress(normalizeAgentPlanSteps(payload.steps), { explanation: typeof payload.explanation === "string" ? payload.explanation : undefined, }); }, diff --git a/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts b/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts index 0ff62e705245..e41dced5fca4 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts @@ -105,8 +105,7 @@ let capturedReplyOptions: onPlanUpdate?: (payload: { phase?: string; explanation?: string; - steps?: string[]; - planSteps?: Array<{ + steps?: Array<{ step: string; status: "pending" | "in_progress" | "completed"; }>; @@ -1106,8 +1105,7 @@ vi.mock("../reply.runtime.js", () => ({ onPlanUpdate?: (payload: { phase?: string; explanation?: string; - steps?: string[]; - planSteps?: Array<{ + steps?: Array<{ step: string; status: "pending" | "in_progress" | "completed"; }>; @@ -1173,8 +1171,7 @@ vi.mock("../reply.runtime.js", () => ({ await params.replyOptions?.onPlanUpdate?.({ phase: entry.phase, explanation: entry.explanation, - steps: entry.steps.map((step) => step.step), - planSteps: entry.steps, + steps: entry.steps, }); } else if (entry.kind === "concurrent_items") { await Promise.all( @@ -1281,8 +1278,7 @@ vi.mock("../reply.runtime.js", () => ({ onPlanUpdate?: (payload: { phase?: string; explanation?: string; - steps?: string[]; - planSteps?: Array<{ + steps?: Array<{ step: string; status: "pending" | "in_progress" | "completed"; }>; @@ -1334,8 +1330,7 @@ vi.mock("../reply.runtime.js", () => ({ await params.replyOptions?.onPlanUpdate?.({ phase: entry.phase, explanation: entry.explanation, - steps: entry.steps.map((step) => step.step), - planSteps: entry.steps, + steps: entry.steps, }); } else if (entry.kind === "concurrent_items") { await Promise.all( diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index da2fd9fcaa99..0731498e9180 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -2203,7 +2203,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag if (payload.phase !== "update") { return; } - await pushPlanProgress(payload.planSteps, payload.explanation); + await pushPlanProgress(payload.steps, payload.explanation); }, onApprovalEvent: async (payload) => { if (payload.phase !== "requested") { diff --git a/extensions/telegram/src/bot-message-dispatch-progress.ts b/extensions/telegram/src/bot-message-dispatch-progress.ts index 9b76bbb06397..053208a5b9d1 100644 --- a/extensions/telegram/src/bot-message-dispatch-progress.ts +++ b/extensions/telegram/src/bot-message-dispatch-progress.ts @@ -270,7 +270,7 @@ export function createTelegramProgressController(params: { }; const handlePlanUpdate = async (payload: CallbackPayload<"onPlanUpdate">) => { if (payload.phase === "update" && canPushToolProgress()) { - await compositor.pushPlanProgress(payload.planSteps, { + await compositor.pushPlanProgress(payload.steps, { explanation: payload.explanation, }); } diff --git a/extensions/telegram/src/bot-message-dispatch.progress-rendering.test.ts b/extensions/telegram/src/bot-message-dispatch.progress-rendering.test.ts index bd9704c6b65a..af5c96804cb0 100644 --- a/extensions/telegram/src/bot-message-dispatch.progress-rendering.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.progress-rendering.test.ts @@ -37,7 +37,7 @@ describeTelegramDispatch("dispatchTelegramMessage progress-rendering", () => { await replyOptions?.onPlanUpdate?.({ phase: "update", explanation: "Implementing the change.", - planSteps: [ + steps: [ { step: "Inspect", status: "completed" }, { step: "Patch", status: "in_progress" }, { step: "Test", status: "pending" }, @@ -66,7 +66,7 @@ describeTelegramDispatch("dispatchTelegramMessage progress-rendering", () => { dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => { await replyOptions?.onPlanUpdate?.({ phase: "update", - planSteps: [ + steps: [ { step: "Patch", status: "in_progress" }, { step: "Test", status: "pending" }, ], diff --git a/scripts/plugin-sdk-surface-report.mjs b/scripts/plugin-sdk-surface-report.mjs index 4cffa41da335..32edadded6f8 100644 --- a/scripts/plugin-sdk-surface-report.mjs +++ b/scripts/plugin-sdk-surface-report.mjs @@ -235,7 +235,7 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // Harvest: retired qa-live-transport-scenarios subpath -6. // +12: typed plan step/status and checklist formatter across channel barrels. // +8: plan-step ingress union and normalizer across channel barrels. - // +4: dual-field plan payload builder for the steps deprecation window. + // Harvest: retired dual-field plan payload builder -1. // +12: active plan-step consumers pinned through channel-outbound and mirrors. // +6: app-guided provider setup types retained by plugin-entry and mirrors. // +3: widget HTML validation helpers and tool input error. @@ -258,7 +258,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // tuning constants, and telegram-consumed claim helpers with compat mirrors, // after harvesting exports orphaned by the split-out WhatsApp adapter (#108656). // +10: supplemental sender helpers plus host-owned SQLite lease contracts. - 8046, + // Harvest: retired dual-field plan payload builder -1. + 8045, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( @@ -271,7 +272,7 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // Harvest: retired qa-live-transport-scenarios subpath -3. // +4: shared plan checklist formatter across channel barrels. // +4: plan-step normalizer across channel barrels. - // +4: dual-field plan payload builder for the steps deprecation window. + // Harvest: retired dual-field plan payload builder -1. // +6: active plan-step helpers pinned through channel-outbound and mirrors. // +2: widget HTML document detection and size assertion. // Used-union narrowing of the 31 wildcard barrels. @@ -289,7 +290,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // +9: narrowed drain seam functions and compat mirrors after the // WhatsApp-split harvest (#108656). // +3: supplemental sender helpers plus the PluginStateLeaseRunner callback. - 4489, + // Harvest: retired dual-field plan payload builder -1. + 4488, env, ), publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv( @@ -299,7 +301,6 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // +77: five zero-consumer subpaths enter their removal window. // +9: typed plan exports and formatter through deprecated channel barrels. // +6: plan-step ingress union and normalizer through deprecated channel barrels. - // +3: dual-field plan payload builder through deprecated channel barrels. // +8: channel-outbound plan pins mirrored through deprecated barrels. // Used-union narrowing drops inherited deprecated exports. // +1: Telegram runner alias retained for plugin SDK compatibility. @@ -308,7 +309,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // +1: unified implicit-mention config type through deprecated config-types. // +24: narrowed drain seam compat mirrors in the channel-message // deprecation-window barrels (#108656). - 3014, + // Harvest: retired dual-field plan payload builder -1; lower-only drift -8. + 3005, env, ), publicWildcardReexports: readPluginSdkSurfaceBudgetEnv( diff --git a/src/auto-reply/get-reply-options.types.ts b/src/auto-reply/get-reply-options.types.ts index fce2b817c9d9..86fb72d7fee2 100644 --- a/src/auto-reply/get-reply-options.types.ts +++ b/src/auto-reply/get-reply-options.types.ts @@ -242,14 +242,7 @@ export type GetReplyOptions = { phase?: string; title?: string; explanation?: string; - /** - * @deprecated Shipped pre-2026.8 shape: plain step text without statuses. - * Still populated for existing consumers; migrate to `planSteps` and - * remove with the deprecation window. - */ - steps?: string[]; - /** Canonical typed checklist snapshot; replaces `steps`. */ - planSteps?: AgentPlanStep[]; + steps?: AgentPlanStep[]; source?: string; }) => Promise | void; /** Called when an approval becomes pending or resolves. */ diff --git a/src/auto-reply/reply/agent-runner-cli-dispatch.test.ts b/src/auto-reply/reply/agent-runner-cli-dispatch.test.ts index f0d2759c0109..8a013527d275 100644 --- a/src/auto-reply/reply/agent-runner-cli-dispatch.test.ts +++ b/src/auto-reply/reply/agent-runner-cli-dispatch.test.ts @@ -79,8 +79,7 @@ describe("runCliAgentWithLifecycle", () => { title: "Plan updated", explanation: undefined, source: "codex-exec", - steps: ["Inspect", "Patch"], - planSteps: [ + steps: [ { step: "Inspect", status: "completed" }, { step: "Patch", status: "pending" }, ], @@ -122,8 +121,7 @@ describe("runCliAgentWithLifecycle", () => { expect(onPlanUpdate).toHaveBeenCalledWith( expect.objectContaining({ - steps: ["Inspect", "Patch"], - planSteps: [ + steps: [ { step: "Inspect", status: "pending" }, { step: "Patch", status: "pending" }, ], diff --git a/src/auto-reply/reply/agent-runner-cli-dispatch.ts b/src/auto-reply/reply/agent-runner-cli-dispatch.ts index 203ad80b365a..6d81c338d949 100644 --- a/src/auto-reply/reply/agent-runner-cli-dispatch.ts +++ b/src/auto-reply/reply/agent-runner-cli-dispatch.ts @@ -18,7 +18,7 @@ import { resolveAgentRunAbortLifecycleFields, resolveAgentRunErrorLifecycleFields, } from "../../agents/run-termination.js"; -import { buildPlanUpdateStepFields } from "../../channels/streaming.js"; +import { normalizeAgentPlanSteps } from "../../channels/streaming.js"; import type { SessionEntry } from "../../config/sessions.js"; import { updateSessionEntry } from "../../config/sessions/session-accessor.js"; import type { AgentEventPayload } from "../../infra/agent-events.js"; @@ -378,7 +378,7 @@ function createPlanUpdateBridge(params: { phase: normalizeOptionalString(evt.data.phase), title: normalizeOptionalString(evt.data.title), explanation: normalizeOptionalString(evt.data.explanation), - ...buildPlanUpdateStepFields(evt.data.steps), + steps: normalizeAgentPlanSteps(evt.data.steps), source: normalizeOptionalString(evt.data.source), }; }, diff --git a/src/auto-reply/reply/agent-runner-event-handler.ts b/src/auto-reply/reply/agent-runner-event-handler.ts index 9c6f0ea63e91..c982158afdb7 100644 --- a/src/auto-reply/reply/agent-runner-event-handler.ts +++ b/src/auto-reply/reply/agent-runner-event-handler.ts @@ -1,7 +1,7 @@ import { readStringValue } from "@openclaw/normalization-core/string-coerce"; import { isMessagingToolSendAction } from "../../agents/embedded-agent-messaging.js"; import type { RunEmbeddedAgentParams } from "../../agents/embedded-agent-runner/run/params.js"; -import { buildPlanUpdateStepFields } from "../../channels/streaming.js"; +import { normalizeAgentPlanSteps } from "../../channels/streaming.js"; import { logVerbose } from "../../globals.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import type { ReplyPayload } from "../types.js"; @@ -203,7 +203,7 @@ export function createAgentRunEventHandler(params: { phase: readStringValue(evt.data.phase), title: readStringValue(evt.data.title), explanation: readStringValue(evt.data.explanation), - ...buildPlanUpdateStepFields(evt.data.steps), + steps: normalizeAgentPlanSteps(evt.data.steps), source: readStringValue(evt.data.source), }); } diff --git a/src/auto-reply/reply/agent-runner-execution-command-events.test.ts b/src/auto-reply/reply/agent-runner-execution-command-events.test.ts index 692ac8857b0b..45efed26dd32 100644 --- a/src/auto-reply/reply/agent-runner-execution-command-events.test.ts +++ b/src/auto-reply/reply/agent-runner-execution-command-events.test.ts @@ -103,8 +103,7 @@ describe("runAgentTurnWithFallback: command events", () => { phase: "update", title: "Assistant proposed a plan", explanation: "Inspect code, patch it, run tests.", - steps: ["Inspect code", "Patch code", "Run tests", "legacy string"], - planSteps: [ + steps: [ { step: "Inspect code", status: "completed" }, { step: "Patch code", status: "in_progress" }, { step: "Run tests", status: "pending" }, diff --git a/src/auto-reply/reply/dispatch-from-config.abort-and-dedupe.test-utils.ts b/src/auto-reply/reply/dispatch-from-config.abort-and-dedupe.test-utils.ts index bb2dbe4efca3..72e497b68f46 100644 --- a/src/auto-reply/reply/dispatch-from-config.abort-and-dedupe.test-utils.ts +++ b/src/auto-reply/reply/dispatch-from-config.abort-and-dedupe.test-utils.ts @@ -63,7 +63,7 @@ describe("dispatchReplyFromConfig", () => { await opts?.onPlanUpdate?.({ phase: "update", explanation: "Inspect code.", - planSteps: [{ step: "Patch code", status: "in_progress" }], + steps: [{ step: "Patch code", status: "in_progress" }], }); await opts?.onApprovalEvent?.({ phase: "requested", diff --git a/src/auto-reply/reply/dispatch-from-config.progress.test-utils.ts b/src/auto-reply/reply/dispatch-from-config.progress.test-utils.ts index fec1c500c565..c9e302909dc8 100644 --- a/src/auto-reply/reply/dispatch-from-config.progress.test-utils.ts +++ b/src/auto-reply/reply/dispatch-from-config.progress.test-utils.ts @@ -668,7 +668,7 @@ describe("dispatchReplyFromConfig", () => { await opts?.onPlanUpdate?.({ phase: "update", explanation: "Inspect code, patch it, run tests.", - planSteps: [ + steps: [ { step: "Inspect code", status: "completed" }, { step: "Patch code", status: "in_progress" }, { step: "Run tests", status: "pending" }, @@ -711,11 +711,11 @@ describe("dispatchReplyFromConfig", () => { ) => { await opts?.onPlanUpdate?.({ phase: "update", - planSteps: [{ step: "Inspect code", status: "in_progress" }], + steps: [{ step: "Inspect code", status: "in_progress" }], }); await opts?.onPlanUpdate?.({ phase: "update", - planSteps: [ + steps: [ { step: "Inspect code", status: "completed" }, { step: "Patch code", status: "in_progress" }, ], @@ -794,7 +794,7 @@ describe("dispatchReplyFromConfig", () => { await opts?.onPlanUpdate?.({ phase: "update", explanation: "Inspect code, patch it, run tests.", - planSteps: [ + steps: [ { step: "Inspect code", status: "completed" }, { step: "Patch code", status: "in_progress" }, { step: "Run tests", status: "pending" }, @@ -848,7 +848,7 @@ describe("dispatchReplyFromConfig", () => { await opts?.onPlanUpdate?.({ phase: "update", explanation: "Inspect code, patch it, run tests.", - planSteps: [ + steps: [ { step: "Inspect code", status: "completed" }, { step: "Patch code", status: "in_progress" }, { step: "Run tests", status: "pending" }, @@ -905,7 +905,7 @@ describe("dispatchReplyFromConfig", () => { await opts?.onPlanUpdate?.({ phase: "update", explanation: "Inspect code, patch it, run tests.", - planSteps: [ + steps: [ { step: "Inspect code", status: "completed" }, { step: "Patch code", status: "in_progress" }, { step: "Run tests", status: "pending" }, diff --git a/src/auto-reply/reply/dispatch-from-config.routing.test-utils.ts b/src/auto-reply/reply/dispatch-from-config.routing.test-utils.ts index 16f2e777b19f..1f114ef9acbe 100644 --- a/src/auto-reply/reply/dispatch-from-config.routing.test-utils.ts +++ b/src/auto-reply/reply/dispatch-from-config.routing.test-utils.ts @@ -752,10 +752,9 @@ describe("dispatchReplyFromConfig", () => { ) => { await opts?.onToolStart?.({ name: "exec", phase: "start" }); await opts?.onItemEvent?.({ itemId: "1", kind: "tool", progressText: "running exec" }); - // Shipped pre-2026.8 producer shape: plain string steps. await opts?.onPlanUpdate?.({ phase: "update", - steps: ["Run command"], + steps: ["Run command"] as never, }); await opts?.onApprovalEvent?.({ phase: "requested", command: "pnpm test" }); await opts?.onCommandOutput?.({ phase: "end", name: "exec", status: "ok", exitCode: 0 }); @@ -793,8 +792,7 @@ describe("dispatchReplyFromConfig", () => { }); expect(onPlanUpdate).toHaveBeenCalledWith({ phase: "update", - steps: ["Run command"], - planSteps: [{ step: "Run command", status: "pending" }], + steps: [{ step: "Run command", status: "pending" }], }); expect(onApprovalEvent).toHaveBeenCalledWith({ phase: "requested", diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index 90135ccaa833..973e2a1b2706 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -2314,16 +2314,13 @@ async function dispatchReplyFromConfigInner( if (isDispatchOperationAborted()) { return; } - // External resolvers call this SDK callback directly and may - // send only the shipped string form; normalize once so - // channel forwards and fallback notices see both fields. - const planSteps = - normalizeAgentPlanSteps(payload.planSteps) ?? - normalizeAgentPlanSteps(payload.steps); + const steps = normalizeAgentPlanSteps(payload.steps); const normalized = { - ...payload, - steps: planSteps?.map((entry) => entry.step) ?? payload.steps, - planSteps, + phase: payload.phase, + title: payload.title, + explanation: payload.explanation, + steps, + source: payload.source, }; markProgress(); await waitForPendingDirectBlockReplyDelivery( @@ -2349,7 +2346,7 @@ async function dispatchReplyFromConfigInner( } await sendPlanUpdate({ explanation: normalized.explanation, - steps: planSteps, + steps, }); }, onApprovalEvent: async (payload) => { diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index 242012644c1f..e7ba67c53283 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -43,7 +43,7 @@ import { import { withLocalSessionPlacementTurnAdmission } from "../../agents/session-placement-admission.js"; import { resolveSessionRuntimeOverrideForProvider } from "../../agents/session-runtime-compat.js"; import { resolveCandidateThinkingLevel } from "../../agents/thinking-runtime.js"; -import { buildPlanUpdateStepFields } from "../../channels/streaming.js"; +import { normalizeAgentPlanSteps } from "../../channels/streaming.js"; import type { SessionEntry } from "../../config/sessions.js"; import { loadSessionEntry, updateSessionEntry } from "../../config/sessions/session-accessor.js"; import type { TypingMode } from "../../config/types.js"; @@ -296,7 +296,7 @@ async function forwardFollowupProgressEvent(params: { phase: readStringValue(evt.data.phase), title: readStringValue(evt.data.title), explanation: readStringValue(evt.data.explanation), - ...buildPlanUpdateStepFields(evt.data.steps), + steps: normalizeAgentPlanSteps(evt.data.steps), source: readStringValue(evt.data.source), }); } diff --git a/src/channels/streaming.test.ts b/src/channels/streaming.test.ts index 73c7926efb27..e48b190e9c84 100644 --- a/src/channels/streaming.test.ts +++ b/src/channels/streaming.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "vitest"; import { buildChannelProgressDraftLine, - buildPlanUpdateStepFields, formatChannelProgressDraftText, formatPlanChecklistLines, normalizeAgentPlanSteps, @@ -115,7 +114,7 @@ describe("mergeChannelProgressDraftLine", () => { }); describe("normalizeAgentPlanSteps", () => { - it("normalizes legacy strings and typed entries, dropping blanks", () => { + it("normalizes external-plugin string steps and typed entries, dropping blanks", () => { expect( normalizeAgentPlanSteps([ "Inspect", @@ -130,14 +129,6 @@ describe("normalizeAgentPlanSteps", () => { ]); expect(normalizeAgentPlanSteps(undefined)).toBeUndefined(); }); - - it("builds both deprecation-window payload fields", () => { - expect(buildPlanUpdateStepFields([{ step: "Inspect", status: "completed" }])).toEqual({ - steps: ["Inspect"], - planSteps: [{ step: "Inspect", status: "completed" }], - }); - expect(buildPlanUpdateStepFields(undefined)).toEqual({}); - }); }); describe("streaming config resolution", () => { diff --git a/src/channels/streaming.ts b/src/channels/streaming.ts index ab455a442b3c..f57b0245fece 100644 --- a/src/channels/streaming.ts +++ b/src/channels/streaming.ts @@ -186,10 +186,6 @@ export type AgentPlanStep = { status: AgentPlanStepStatus; }; -/** - * Plan-event ingress shape for the SDK deprecation window: shipped producers - * through 2026.7.x sent `steps: string[]`. Remove with the string form. - */ export type AgentPlanStepInput = AgentPlanStep | string; function isAgentPlanStepStatus(value: unknown): value is AgentPlanStepStatus { @@ -197,25 +193,9 @@ function isAgentPlanStepStatus(value: unknown): value is AgentPlanStepStatus { } /** - * Normalizes plan-event steps at public ingress boundaries. Legacy string - * steps become pending typed steps; malformed entries are dropped. + * Older installed @openclaw/codex releases emit plan steps as strings. + * Normalize that external-plugin version skew to pending typed steps. */ -/** - * Builds both plan-step payload fields for `onPlanUpdate` during the SDK - * deprecation window: canonical `planSteps` plus the shipped pre-2026.8 - * `steps: string[]` form. Collapse to `planSteps` when the window closes. - */ -export function buildPlanUpdateStepFields(value: unknown): { - steps?: string[]; - planSteps?: AgentPlanStep[]; -} { - const planSteps = normalizeAgentPlanSteps(value); - if (!planSteps) { - return {}; - } - return { steps: planSteps.map((entry) => entry.step), planSteps }; -} - export function normalizeAgentPlanSteps(value: unknown): AgentPlanStep[] | undefined { if (!Array.isArray(value)) { return undefined;