diff --git a/docs/channels/discord.md b/docs/channels/discord.md
index ae1cb5c098e1..ab12a63bdcbe 100644
--- a/docs/channels/discord.md
+++ b/docs/channels/discord.md
@@ -815,25 +815,6 @@ See [Slash commands](/tools/slash-commands) for the command catalog and behavior
-
- Set `channels.discord.subagentProgress: true` to show background child activity on the Discord message that started the parent run.
-
-```json5
-{
- channels: {
- discord: {
- subagentProgress: true,
- },
- },
-}
-```
-
- While child runs are active, OpenClaw keeps Discord typing active for up to one hour and replaces one count reaction (`1️⃣` through `🔟`) as the concurrent count changes; `🔟` also represents 10 or more. The count reaction is removed after the final child ends. A failed, timed-out, or killed child leaves a `🔴` reaction.
-
- This is opt-in and uses fixed internal timing and emoji defaults. The bot needs **Add Reactions** permission for reaction feedback. Account-level `channels.discord.accounts..subagentProgress` overrides the top-level value.
-
-
-
For stable "always-on" ACP workspaces, configure top-level typed ACP bindings targeting Discord conversations.
diff --git a/extensions/discord/src/subagent-hooks.test.ts b/extensions/discord/src/subagent-hooks.test.ts
index 68c2c942d969..1bea26f42d7b 100644
--- a/extensions/discord/src/subagent-hooks.test.ts
+++ b/extensions/discord/src/subagent-hooks.test.ts
@@ -61,7 +61,6 @@ const hookMocks = vi.hoisted(() => {
listThreadBindingsBySessionKey: vi.fn((_params?: unknown): ThreadBindingRecord[] => []),
unbindThreadBindingsBySessionKey: vi.fn(() => []),
progressModuleFactory: vi.fn(),
- handleDiscordSubagentProgress: vi.fn(),
recoverDiscordSubagentProgress: vi.fn(),
};
});
@@ -78,10 +77,7 @@ vi.mock("./monitor/thread-bindings.js", () => ({
}));
vi.mock("./subagent-progress.js", () => {
hookMocks.progressModuleFactory();
- return {
- handleDiscordSubagentProgress: hookMocks.handleDiscordSubagentProgress,
- recoverDiscordSubagentProgress: hookMocks.recoverDiscordSubagentProgress,
- };
+ return { recoverDiscordSubagentProgress: hookMocks.recoverDiscordSubagentProgress };
});
function registerHandlersForTest(
@@ -221,11 +217,10 @@ describe("discord subagent hook handlers", () => {
hookMocks.listThreadBindingsBySessionKey.mockClear();
hookMocks.unbindThreadBindingsBySessionKey.mockClear();
hookMocks.progressModuleFactory.mockClear();
- hookMocks.handleDiscordSubagentProgress.mockClear();
hookMocks.recoverDiscordSubagentProgress.mockClear();
});
- it("keeps progress runtime lazy for unrelated subagent hooks", async () => {
+ it("keeps progress cleanup lazy for unrelated subagent hooks", async () => {
const handlers = registerHandlersForTest();
const handler = getRequiredHookHandler(handlers, "subagent_delivery_target");
@@ -244,7 +239,7 @@ describe("discord subagent hook handlers", () => {
expect(hookMocks.progressModuleFactory).not.toHaveBeenCalled();
});
- it("loads progress runtime for gateway recovery", async () => {
+ it("loads retired progress cleanup on gateway startup", async () => {
const handlers = registerHandlersForTest();
const handler = getRequiredHookHandler(handlers, "gateway_start");
diff --git a/extensions/discord/src/subagent-progress-config.ts b/extensions/discord/src/subagent-progress-config.ts
deleted file mode 100644
index 9ef55d90d118..000000000000
--- a/extensions/discord/src/subagent-progress-config.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { DEFAULT_EMOJIS } from "openclaw/plugin-sdk/channel-feedback";
-import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
-import {
- normalizeOptionalLowercaseString,
- normalizeOptionalStringifiedId,
-} from "openclaw/plugin-sdk/string-coerce-runtime";
-
-export const RUNNING_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"];
-export const FAILURE_EMOJI = "🔴";
-
-export type DiscordProgressRequester = {
- channel?: string;
- accountId?: string;
- to?: string;
- threadId?: string | number;
- channelId?: string | number;
- messageId?: string | number;
-};
-
-function channelIdFromTarget(target?: string): string | undefined {
- const trimmed = target?.trim();
- if (!trimmed) {
- return undefined;
- }
- if (trimmed.startsWith("channel:")) {
- return trimmed.slice("channel:".length).trim() || undefined;
- }
- return /^\d+$/u.test(trimmed) ? trimmed : undefined;
-}
-
-export function resolveDiscordProgressTarget(requester?: DiscordProgressRequester) {
- if (normalizeOptionalLowercaseString(requester?.channel) !== "discord") {
- return undefined;
- }
- const channelId =
- normalizeOptionalStringifiedId(requester?.channelId) ?? channelIdFromTarget(requester?.to);
- const messageId = normalizeOptionalStringifiedId(requester?.messageId);
- if (!channelId || !messageId) {
- return undefined;
- }
- return { channelId, messageId };
-}
-
-export function reservedReactionEmojis(config: OpenClawConfig, ackReaction?: string): Set {
- const reserved = new Set(Object.values(DEFAULT_EMOJIS));
- for (const emoji of [config.messages?.ackReaction, ackReaction]) {
- if (emoji?.trim()) {
- reserved.add(emoji.trim());
- }
- }
- for (const agent of config.agents?.list ?? []) {
- const emoji = agent.identity?.emoji?.trim();
- if (emoji) {
- reserved.add(emoji);
- }
- }
- return reserved;
-}
-
-export function reactionsAreAvailable(config: OpenClawConfig, ackReaction?: string): boolean {
- const reserved = reservedReactionEmojis(config, ackReaction);
- return !RUNNING_EMOJIS.some((emoji) => reserved.has(emoji)) && !reserved.has(FAILURE_EMOJI);
-}
diff --git a/extensions/discord/src/subagent-progress-state.ts b/extensions/discord/src/subagent-progress-state.ts
deleted file mode 100644
index 8a3c32197505..000000000000
--- a/extensions/discord/src/subagent-progress-state.ts
+++ /dev/null
@@ -1,264 +0,0 @@
-import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
-import type { PluginStateKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime";
-
-export const PROGRESS_STORE_TTL_MS = 7 * 24 * 60 * 60_000;
-export const MAX_TRACKED_RUNS = 4_096;
-const TERMINAL_TOMBSTONE_TTL_MS = 60 * 60_000;
-
-export type SubagentProgressOutcome = "ok" | "error" | "timeout" | "killed" | "unknown";
-
-export type ProgressApi = {
- config: OpenClawConfig;
- logger: { debug?: (message: string) => void };
- runtime?: {
- state: {
- openKeyedStore(options: {
- namespace: string;
- maxEntries: number;
- overflowPolicy: "reject-new";
- defaultTtlMs: number;
- }): PluginStateKeyedStore;
- };
- };
-};
-
-type PersistedProgressRunBase = {
- /** Plugin SQLite ownership lets an ended run clean reactions after gateway restart. */
- key: string;
- accountId: string;
- channelId: string;
- messageId: string;
- runningEmoji?: string;
-};
-
-type PersistedActiveProgressRun = PersistedProgressRunBase & { status: "active" };
-type PersistedCleanupProgressRun = PersistedProgressRunBase & {
- status: "cleanup";
- outcome: SubagentProgressOutcome;
-};
-export type PersistedProgressRun = PersistedActiveProgressRun | PersistedCleanupProgressRun;
-
-export function persistedTerminalOutcome(
- persisted?: PersistedProgressRun,
-): SubagentProgressOutcome | undefined {
- return persisted?.status === "cleanup" ? persisted.outcome : undefined;
-}
-
-export type ProgressTracker = {
- accountId: string;
- channelId: string;
- messageId: string;
- activeRunIds: Set;
- persistedRunIds: Set;
- runningEmoji?: string;
- runningEmojiConfirmed: boolean;
- reactionsEnabled: boolean;
- typingTimer?: ReturnType;
- typingExpiresAt: number;
-};
-
-type ProgressRunLookupResult =
- | { status: "found"; value: PersistedProgressRun }
- | { status: "missing" }
- | { status: "error" };
-
-export type PersistProgressResult = "persisted" | "terminal" | "conflict" | "error";
-
-type ProgressStateForKeyResult =
- | {
- ok: true;
- activeRunIds: string[];
- cleanupRuns: Array<{ runId: string; value: PersistedCleanupProgressRun }>;
- ownedEmojis: string[];
- }
- | { ok: false };
-
-let progressStores = new WeakMap