mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(discord): default preview streaming to off (#120376)
Make Discord progress drafts and activity receipts explicit opt-ins while preserving configured streaming modes and legacy explicit progress migrations. Related: #87704.
This commit is contained in:
committed by
GitHub
parent
13e8e1df4a
commit
96a75be170
@@ -325,7 +325,6 @@ src/acp/control-plane/manager.turn-results.test.ts
|
||||
src/acp/event-ledger.ts
|
||||
src/acp/translator.ts
|
||||
src/agents/acp-spawn-parent-stream.test.ts
|
||||
src/agents/acp-spawn-parent-stream.ts
|
||||
src/agents/acp-spawn.test.ts
|
||||
src/agents/agent-bundle-mcp-runtime.test.ts
|
||||
src/agents/agent-bundle-mcp-runtime.ts
|
||||
|
||||
@@ -700,7 +700,7 @@ See [Slash commands](/tools/slash-commands) for the command catalog and behavior
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Live stream preview">
|
||||
OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. `channels.discord.streaming.mode` takes `off` | `partial` | `block` | `progress` (default when no `streaming`/legacy `streamMode` key is set). `streamMode` is a legacy alias; run `openclaw doctor --fix` to rewrite persisted config to the canonical nested `streaming` shape.
|
||||
OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. Discord preview streaming defaults to `off`; set `channels.discord.streaming.mode` to `partial`, `block`, or `progress` to opt in. `streamMode` is a legacy alias; run `openclaw doctor --fix` to rewrite persisted config to the canonical nested `streaming` shape.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -725,7 +725,7 @@ See [Slash commands](/tools/slash-commands) for the command catalog and behavior
|
||||
- `block` emits draft-sized chunks; tune size and breakpoints with `streaming.preview.chunk` (`minChars`, `maxChars`, `breakPreference`), clamped to `textChunkLimit`. An explicit non-`off` preview mode overrides inherited `agents.defaults.blockStreamingDefault: "on"`; explicit `streaming.block.enabled: true` overrides the preview. If a turn cannot use previews, inherited block delivery still applies.
|
||||
- `progress` keeps one editable status draft until final delivery. It shows the agent's latest preamble or narration as a status headline, with the compact tool rows underneath and no generated label.
|
||||
- Media, error, and explicit-reply finals cancel pending preview edits.
|
||||
- `streaming.preview.toolProgress` and `streaming.progress.toolProgress` both default to `true` in every mode. Tool rows such as `🛠️ Bash: run tests` or `🔎 Web Search: for "query"` appear without config; set either key to `false` to keep the status headline only.
|
||||
- `streaming.preview.toolProgress` and `streaming.progress.toolProgress` both default to `true` when preview streaming is active. Tool rows such as `🛠️ Bash: run tests` or `🔎 Web Search: for "query"` need no additional progress config; set either key to `false` to keep the status headline only.
|
||||
- `streaming.progress.commentary` (default `false`) opts into raw assistant commentary in the temporary progress draft. The default preamble/narration status line is independent of this option. Commentary is cleaned before display, stays transient, and does not change final answer delivery.
|
||||
- `streaming.progress.maxLineChars` controls the per-line progress preview budget. Prose is shortened on word boundaries; command and path details keep useful suffixes.
|
||||
- `streaming.preview.commandText` / `streaming.progress.commandText` controls command/exec detail in compact progress lines: `raw` (default) or `status` (tool label only).
|
||||
|
||||
@@ -22,11 +22,11 @@ Working...
|
||||
```
|
||||
|
||||
<Note>
|
||||
Discord and Telegram default to `streaming.mode: "progress"`, so progress
|
||||
drafts show up there without any config. Set `mode: "partial"` on either to
|
||||
stream answer text instead. Every other channel defaults to `partial` or
|
||||
`off`; see [Streaming and chunking](/concepts/streaming#channel-mapping) for
|
||||
the full per-channel default table.
|
||||
Discord defaults preview streaming to `off`; set `streaming.mode: "progress"`
|
||||
to opt in. Telegram defaults to `progress` without additional config. Set
|
||||
`mode: "partial"` on either to stream answer text instead. See
|
||||
[Streaming and chunking](/concepts/streaming#channel-mapping) for the full
|
||||
per-channel default table.
|
||||
</Note>
|
||||
|
||||
## Quick start
|
||||
@@ -413,7 +413,7 @@ the final answer, except for the label if one is configured.
|
||||
|
||||
| Channel | Progress transport | Notes |
|
||||
| --------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Discord | Send one message, then edit it. | Defaults to `progress` mode; the final answer carries a `-#` activity receipt and the status draft is deleted after the answer lands. |
|
||||
| Discord | Send one message, then edit it. | `progress` is explicit opt-in; the final answer carries a `-#` activity receipt and the status draft is deleted after the answer lands. |
|
||||
| Matrix | Send one event, then edit it. | Account-level streaming config controls account-level drafts. |
|
||||
| Microsoft Teams | Native Teams stream in personal chats. | `streaming.mode: "block"` maps to Teams block delivery instead. |
|
||||
| Slack | Native stream or editable draft post. | Needs a reply thread target; top-level DMs without one still get draft preview posts and edits. |
|
||||
|
||||
@@ -183,16 +183,16 @@ instead of being overwritten in one editable draft.
|
||||
|
||||
### Channel mapping
|
||||
|
||||
Discord and Telegram default to `progress` when `streaming` is unset; Slack,
|
||||
Mattermost, and MS Teams default to `partial`.
|
||||
Discord defaults to `off` when `streaming` is unset, Telegram defaults to
|
||||
`progress`, and Slack, Mattermost, and MS Teams default to `partial`.
|
||||
|
||||
| Channel | `off` | `partial` | `block` | `progress` |
|
||||
| ---------- | ----- | --------- | ------- | --------------------------------- |
|
||||
| Telegram | Yes | Yes | Yes | editable progress draft (default) |
|
||||
| Discord | Yes | Yes | Yes | editable progress draft (default) |
|
||||
| Slack | Yes | Yes | Yes | Yes |
|
||||
| Mattermost | Yes | Yes | Yes | Yes |
|
||||
| MS Teams | Yes | Yes | Yes | native progress stream |
|
||||
| Channel | `off` | `partial` | `block` | `progress` |
|
||||
| ---------- | ------------- | --------- | ------- | --------------------------------- |
|
||||
| Telegram | Yes | Yes | Yes | editable progress draft (default) |
|
||||
| Discord | Yes (default) | Yes | Yes | editable progress draft (opt-in) |
|
||||
| Slack | Yes | Yes | Yes | Yes |
|
||||
| Mattermost | Yes | Yes | Yes | Yes |
|
||||
| MS Teams | Yes | Yes | Yes | native progress stream |
|
||||
|
||||
Preview chunk config (`streaming.preview.chunk.*`, e.g. under
|
||||
`channels.discord.streaming` or `channels.telegram.streaming`) defaults to
|
||||
|
||||
@@ -283,7 +283,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
textChunkLimit: 2000,
|
||||
suppressEmbeds: true,
|
||||
streaming: {
|
||||
mode: "progress", // off | partial | block | progress (Discord default: progress)
|
||||
mode: "progress", // explicit opt-in; Discord defaults to off
|
||||
chunkMode: "length", // length | newline
|
||||
progress: {
|
||||
label: "auto",
|
||||
@@ -356,7 +356,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
- `channels.discord.voice.reconnectGraceMs` controls how long a disconnected voice session may take to enter reconnect signalling before OpenClaw destroys it (default `15000`).
|
||||
- Discord voice playback is not interrupted by another user's speaking-start event. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing.
|
||||
- OpenClaw additionally attempts voice receive recovery by leaving/rejoining a voice session after repeated decrypt failures.
|
||||
- `channels.discord.streaming` is the canonical stream mode key. Discord defaults to `streaming.mode: "progress"` so tool/work progress appears in one edited preview message; set `streaming.mode: "off"` to disable it. Legacy flat keys (`streamMode`, `chunkMode`, `blockStreaming`, `draftChunk`, `blockStreamingCoalesce`) are no longer read at runtime; run `openclaw doctor --fix` to migrate persisted config.
|
||||
- `channels.discord.streaming` is the canonical stream mode key. Discord preview streaming defaults to `off`; set `streaming.mode: "progress"` to opt into one edited tool/work progress message, or choose `partial` or `block` for answer previews. Legacy flat keys (`streamMode`, `chunkMode`, `blockStreaming`, `draftChunk`, `blockStreamingCoalesce`) are no longer read at runtime; run `openclaw doctor --fix` to migrate persisted config.
|
||||
- `channels.discord.autoPresence` maps runtime availability to bot presence (healthy => online, degraded => idle, exhausted => dnd) and allows optional status text overrides.
|
||||
- `channels.discord.guilds.<id>.presenceEvents` routes human availability arrivals into one configured Discord channel as agent system events. Eligible members must be able to view `channelId`; public threads inherit parent visibility, while private threads additionally require membership or Manage Threads. `users` can further narrow that audience. It seeds current online members from complete `GUILD_CREATE` snapshots, routes observed offline-to-online transitions, and treats a first later online signal for an unseen member as newly available without asserting whether they came online or joined after the snapshot. Guilds above Discord's 75,000-member snapshot limit require an explicit offline update first. Throttling knobs: `reconnectSuppressSeconds` (quiet window after a new Gateway session while guild presence state is rebuilt, default 300, `0` disables) and `burstLimit`/`burstWindowSeconds` (per-guild successfully queued event rate limit, default 8 events per 60s sliding window). Resumed sessions do not start the reconnect suppression window. The existing per-user re-greet cooldown remains eight hours. It requires `channels.discord.intents.presence=true`, the privileged Presence Intent in Discord's Developer Portal, and an enabled agent heartbeat.
|
||||
- `channels.discord.intents.messageContent` defaults to `true`. Set it to `false` only for mention-only operation when Discord cannot grant the privileged Message Content intent; DMs and explicit bot mentions still carry message content, while other guild messages do not. Keep `requireMention: true` on every configured guild channel in this mode.
|
||||
|
||||
@@ -550,10 +550,10 @@ Two ways to start an ACP session:
|
||||
session as system events. OpenClaw records the full relay history in the
|
||||
child agent's SQLite state and removes it with the child session. Parent
|
||||
progress streams show assistant commentary and ACP status progress by default unless
|
||||
`streaming.progress.commentary=false`. Discord also defaults parent
|
||||
previews to progress mode when no stream mode is configured. Status
|
||||
progress still honors `acp.stream.tagVisibility`, so tags such as `plan`
|
||||
remain hidden unless explicitly enabled.
|
||||
`streaming.progress.commentary=false`. Discord parent progress requires an
|
||||
explicit `streaming.mode: "progress"`; unset Discord streaming stays quiet.
|
||||
Status progress still honors `acp.stream.tagVisibility`, so tags such as
|
||||
`plan` remain hidden unless explicitly enabled.
|
||||
</ParamField>
|
||||
|
||||
ACP `sessions_spawn` runs use `agents.defaults.subagents.runTimeoutSeconds`
|
||||
|
||||
@@ -17,8 +17,8 @@ export const discordChannelConfigUiHints = {
|
||||
},
|
||||
nativeCommands: true,
|
||||
streaming: {
|
||||
"": 'Unified Discord stream preview mode: "off" | "partial" | "block" | "progress". "progress" keeps a single editable progress draft until final delivery. Legacy boolean/streamMode keys are auto-mapped.',
|
||||
mode: 'Canonical Discord preview mode: "off" | "partial" | "block" | "progress".',
|
||||
"": 'Discord preview streaming is off by default. Set mode to "partial", "block", or "progress" to opt in. Run openclaw doctor --fix to migrate legacy keys.',
|
||||
mode: 'Discord preview mode: "off" | "partial" | "block" | "progress". Default: "off".',
|
||||
chunkMode:
|
||||
'Chunking mode for outbound Discord text delivery: "length" (default) or "newline".',
|
||||
"block.enabled":
|
||||
|
||||
@@ -33,11 +33,6 @@ const streamingAliasMigration = defineChannelAliasMigration({
|
||||
// Runtime mode resolution dropped legacy streamMode reads; the doctor
|
||||
// resolver keeps them so migration preserves configured intent.
|
||||
defaultMode: "off",
|
||||
// Discord previews default to progress only while `streaming` is absent;
|
||||
// any present object (even without mode) resolves off, so migration pins
|
||||
// progress when delivery-only aliases create the object with no root
|
||||
// streaming object to inherit from.
|
||||
absentObjectDefault: "progress",
|
||||
includePreviewChunk: true,
|
||||
},
|
||||
// Discord's account merge replaces the root streaming object wholesale
|
||||
|
||||
@@ -159,10 +159,10 @@ describe("discord doctor", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("pins progress mode when migrating delivery-only aliases", () => {
|
||||
it("keeps delivery-only aliases mode-free and preserves explicit legacy progress", () => {
|
||||
const normalize = getDiscordCompatibilityNormalizer();
|
||||
|
||||
const result = normalize({
|
||||
const deliveryOnly = normalize({
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: { blockStreaming: true },
|
||||
@@ -170,17 +170,33 @@ describe("discord doctor", () => {
|
||||
} as never,
|
||||
});
|
||||
|
||||
const migrated = result.config.channels?.discord as Record<string, unknown>;
|
||||
expect(migrated).toEqual({
|
||||
const deliveryOnlyMigrated = deliveryOnly.config.channels?.discord as Record<string, unknown>;
|
||||
expect(deliveryOnlyMigrated).toEqual({
|
||||
streaming: { block: { enabled: true } },
|
||||
});
|
||||
expect(resolveDiscordPreviewStreamMode(deliveryOnlyMigrated)).toBe("off");
|
||||
expect(deliveryOnly.changes).toEqual([
|
||||
"Moved channels.discord.blockStreaming → channels.discord.streaming.block.enabled.",
|
||||
]);
|
||||
|
||||
const explicitProgress = normalize({
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: { streamMode: "progress", blockStreaming: true },
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
const explicitProgressMigrated = explicitProgress.config.channels?.discord as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(explicitProgressMigrated).toEqual({
|
||||
streaming: { mode: "progress", block: { enabled: true } },
|
||||
});
|
||||
// Effective preview-mode parity: `streaming` absent resolved to progress
|
||||
// before migration, so the migrated object must keep progress instead of
|
||||
// falling to the object-without-mode default (off).
|
||||
expect(resolveDiscordPreviewStreamMode(migrated)).toBe(resolveDiscordPreviewStreamMode({}));
|
||||
expect(result.changes).toEqual([
|
||||
expect(resolveDiscordPreviewStreamMode(explicitProgressMigrated)).toBe("progress");
|
||||
expect(explicitProgress.changes).toEqual([
|
||||
"Moved channels.discord.streamMode → channels.discord.streaming.mode (progress).",
|
||||
"Moved channels.discord.blockStreaming → channels.discord.streaming.block.enabled.",
|
||||
"Set channels.discord.streaming.mode (progress) to keep the previous default while migrating flat streaming keys.",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -239,12 +239,13 @@ describe("processDiscordMessage draft streaming final delivery", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults unset Discord preview streaming to progress mode without drafting text-only turns", async () => {
|
||||
it("keeps unset Discord preview streaming off and delivers the final normally", async () => {
|
||||
await runSingleChunkFinalScenario({ maxLinesPerMessage: 5 });
|
||||
expect(getLastDispatchReplyOptions()?.onPartialReply).toBeUndefined();
|
||||
expect(createDiscordDraftStream).toHaveBeenCalledTimes(1);
|
||||
expect(createDiscordDraftStream).not.toHaveBeenCalled();
|
||||
expect(editMessageDiscord).not.toHaveBeenCalled();
|
||||
expect(deliverDiscordReply).toHaveBeenCalledTimes(1);
|
||||
expectFreshFinalText("Hello\nWorld");
|
||||
});
|
||||
|
||||
it("does not stream Discord tool progress before the initial delay", async () => {
|
||||
@@ -258,7 +259,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
|
||||
});
|
||||
|
||||
const ctx = await createAutomaticSourceDeliveryContext({
|
||||
discordConfig: { maxLinesPerMessage: 5 },
|
||||
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 5 },
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
@@ -302,7 +303,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
|
||||
|
||||
const ctx = await createAutomaticSourceDeliveryContext({
|
||||
baseSessionKey: BASE_CHANNEL_ROUTE.sessionKey,
|
||||
discordConfig: { maxLinesPerMessage: 5 },
|
||||
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 5 },
|
||||
route: BASE_CHANNEL_ROUTE,
|
||||
});
|
||||
|
||||
@@ -365,7 +366,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
|
||||
});
|
||||
|
||||
const ctx = await createAutomaticSourceDeliveryContext({
|
||||
discordConfig: { maxLinesPerMessage: 5 },
|
||||
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 5 },
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
@@ -390,7 +391,9 @@ describe("processDiscordMessage draft streaming final delivery", () => {
|
||||
return { queuedFinal: true, counts: { final: 1, tool: 0, block: 0 } };
|
||||
});
|
||||
|
||||
const ctx = await createAutomaticSourceDeliveryContext();
|
||||
const ctx = await createAutomaticSourceDeliveryContext({
|
||||
discordConfig: { streaming: { mode: "progress" } },
|
||||
});
|
||||
await runProcessDiscordMessage(ctx);
|
||||
});
|
||||
|
||||
@@ -445,7 +448,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
|
||||
});
|
||||
|
||||
const ctx = await createAutomaticSourceDeliveryContext({
|
||||
discordConfig: { maxLinesPerMessage: 5 },
|
||||
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 5 },
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
@@ -470,7 +473,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
|
||||
});
|
||||
|
||||
const ctx = await createAutomaticSourceDeliveryContext({
|
||||
discordConfig: { maxLinesPerMessage: 5 },
|
||||
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 5 },
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
@@ -673,6 +676,12 @@ describe("processDiscordMessage draft streaming final delivery", () => {
|
||||
});
|
||||
|
||||
const ctx = await createBaseContext({
|
||||
discordConfig: {
|
||||
streaming: {
|
||||
mode: "progress",
|
||||
progress: { toolProgress: true },
|
||||
},
|
||||
},
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: {
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("processDiscordMessage draft streaming recovery", () => {
|
||||
|
||||
const ctx = await createAutomaticSourceDeliveryContext({
|
||||
baseSessionKey: BASE_CHANNEL_ROUTE.sessionKey,
|
||||
discordConfig: { maxLinesPerMessage: 120 },
|
||||
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 120 },
|
||||
route: BASE_CHANNEL_ROUTE,
|
||||
});
|
||||
|
||||
@@ -450,7 +450,7 @@ describe("processDiscordMessage draft streaming recovery", () => {
|
||||
expect(firstDispatchParams().replyOptions?.disableBlockStreaming).toBe(true);
|
||||
});
|
||||
|
||||
it("shows the agent status above the tool lines in the default Discord progress draft", async () => {
|
||||
it("shows the agent status above the tool lines in an opted-in Discord progress draft", async () => {
|
||||
const elapseProgressDraftStartDelay = useProgressDraftStartDelay();
|
||||
const draftStream = createMockDraftStreamForTest();
|
||||
|
||||
@@ -481,7 +481,7 @@ describe("processDiscordMessage draft streaming recovery", () => {
|
||||
expect(draftStream.update).toHaveBeenCalledWith(
|
||||
"Claiming my square footage. Tastefully, but with claws.\n\n🛠️ Exec\n• exec done",
|
||||
);
|
||||
// No config, so the implicit label stays hidden under the status headline.
|
||||
// With no label override, the implicit label stays hidden under the status headline.
|
||||
expect(String(draftStream.update.mock.calls[0]?.[0])).not.toMatch(/Working/);
|
||||
expect(draftStream.flush).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
|
||||
@@ -9,8 +9,5 @@ export function resolveDiscordPreviewStreamMode(
|
||||
streaming?: unknown;
|
||||
} = {},
|
||||
): StreamingMode {
|
||||
if (params.streaming === undefined) {
|
||||
return "progress";
|
||||
}
|
||||
return resolveChannelPreviewStreamMode(params, "off");
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ export function resolveTelegramPreviewStreamMode(
|
||||
streaming?: unknown;
|
||||
} = {},
|
||||
): StreamingMode {
|
||||
// Telegram defaults to the progress draft like Discord: on tool-heavy turns a
|
||||
// status draft answers "is it working?", which streamed answer text cannot.
|
||||
// Telegram defaults to the progress draft: on tool-heavy turns a status draft
|
||||
// answers "is it working?", which streamed answer text cannot.
|
||||
// Operators who prefer streamed answer text set `streaming.mode: "partial"`.
|
||||
return resolveChannelPreviewStreamMode(params, "progress");
|
||||
}
|
||||
|
||||
@@ -580,7 +580,7 @@ describe("startAcpSpawnParentStreamRelay", () => {
|
||||
relay.dispose();
|
||||
});
|
||||
|
||||
it("relays commentary-phase assistant text in parent progress mode by default", () => {
|
||||
it("relays commentary-phase assistant text in explicit parent progress mode", () => {
|
||||
const relay = startAcpSpawnParentStreamRelay({
|
||||
runId: "run-commentary-default",
|
||||
parentSessionKey: "agent:main:main",
|
||||
@@ -588,7 +588,7 @@ describe("startAcpSpawnParentStreamRelay", () => {
|
||||
agentId: "codex",
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: {},
|
||||
discord: { streaming: { mode: "progress" } },
|
||||
},
|
||||
},
|
||||
deliveryContext: {
|
||||
@@ -819,11 +819,11 @@ describe("startAcpSpawnParentStreamRelay", () => {
|
||||
relay.dispose();
|
||||
});
|
||||
|
||||
it("uses Discord default progress mode for parent commentary", () => {
|
||||
it("suppresses Discord parent progress commentary when streaming is unset", () => {
|
||||
const relay = startAcpSpawnParentStreamRelay({
|
||||
runId: "run-discord-default-progress",
|
||||
runId: "run-discord-unset-streaming",
|
||||
parentSessionKey: "agent:main:main",
|
||||
childSessionKey: "agent:codex:acp:child-discord-default-progress",
|
||||
childSessionKey: "agent:codex:acp:child-discord-unset-streaming",
|
||||
agentId: "codex",
|
||||
cfg: {
|
||||
channels: {
|
||||
@@ -840,7 +840,7 @@ describe("startAcpSpawnParentStreamRelay", () => {
|
||||
});
|
||||
|
||||
emitAgentEvent({
|
||||
runId: "run-discord-default-progress",
|
||||
runId: "run-discord-unset-streaming",
|
||||
stream: "item",
|
||||
data: {
|
||||
itemId: "preamble-1",
|
||||
@@ -850,7 +850,7 @@ describe("startAcpSpawnParentStreamRelay", () => {
|
||||
});
|
||||
vi.advanceTimersByTime(15);
|
||||
|
||||
expect(collectedTexts()).toEqual(["codex: Checking the app-server stream"]);
|
||||
expect(collectedTexts()).toEqual([]);
|
||||
relay.dispose();
|
||||
});
|
||||
|
||||
|
||||
@@ -120,31 +120,6 @@ function mergeStreamingEntry(
|
||||
};
|
||||
}
|
||||
|
||||
function hasConfiguredPreviewStreamMode(entry: StreamingCompatEntry): boolean {
|
||||
return asObjectRecord(entry.streaming)?.mode !== undefined;
|
||||
}
|
||||
|
||||
function applyParentPreviewStreamModeDefault(
|
||||
entry: StreamingCompatEntry,
|
||||
channelId: string,
|
||||
): StreamingCompatEntry {
|
||||
if (channelId !== "discord" || hasConfiguredPreviewStreamMode(entry)) {
|
||||
return entry;
|
||||
}
|
||||
const streaming = asObjectRecord(entry.streaming);
|
||||
return {
|
||||
...entry,
|
||||
streaming: streaming
|
||||
? {
|
||||
...streaming,
|
||||
mode: "progress",
|
||||
}
|
||||
: {
|
||||
mode: "progress",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resolveParentProgressStreamingEntry(params: {
|
||||
cfg: OpenClawConfig | undefined;
|
||||
deliveryContext: DeliveryContext | undefined;
|
||||
@@ -165,10 +140,7 @@ function resolveParentProgressStreamingEntry(params: {
|
||||
normalizeAccountId(params.deliveryContext?.accountId),
|
||||
normalizeAccountId,
|
||||
);
|
||||
return applyParentPreviewStreamModeDefault(
|
||||
mergeStreamingEntry(channelCfg, accountCfg),
|
||||
channelId,
|
||||
);
|
||||
return mergeStreamingEntry(channelCfg, accountCfg);
|
||||
}
|
||||
|
||||
function resolveParentProgressCommentary(params: {
|
||||
@@ -764,4 +736,3 @@ export type AcpSpawnParentRelayHandle = {
|
||||
dispose: () => void;
|
||||
notifyStarted: () => void;
|
||||
};
|
||||
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|
||||
|
||||
@@ -830,10 +830,10 @@ export function resolveChannelStreamingPreviewToolProgress(
|
||||
defaultValue = true,
|
||||
/**
|
||||
* The channel's resolved stream mode. Only the caller knows it: channels pick
|
||||
* their own default when `streaming.mode` is unset (Discord and Telegram use
|
||||
* "progress", Slack and others "partial"), and this helper has no channel
|
||||
* identity to guess with. Omitting it reads the configured mode and treats
|
||||
* unset as "partial".
|
||||
* their own default when `streaming.mode` is unset (Telegram uses "progress",
|
||||
* Discord uses "off", and Slack uses "partial"), and this helper has no
|
||||
* channel identity to guess with. Omitting it reads the configured mode and
|
||||
* treats unset as "partial".
|
||||
*/
|
||||
mode?: StreamingMode,
|
||||
): boolean {
|
||||
@@ -857,7 +857,7 @@ export function resolveChannelStreamingProgressCommentary(
|
||||
* resolveChannelStreamingPreviewToolProgress takes one: only the caller knows
|
||||
* which default applies when `streaming.mode` is unset. Guessing "partial"
|
||||
* here made `progress.commentary: true` a silent no-op on the progress-draft
|
||||
* channels, whose own default is "progress".
|
||||
* channels, such as Telegram, whose own default is "progress".
|
||||
*/
|
||||
mode?: StreamingMode,
|
||||
): boolean {
|
||||
@@ -871,7 +871,7 @@ export function resolveChannelStreamingProgressCommentary(
|
||||
}
|
||||
|
||||
// Pure toggle: progress-mode gating stays with the caller because channels
|
||||
// resolve their own default stream mode (Discord defaults to "progress").
|
||||
// resolve their own default stream mode.
|
||||
export function resolveChannelStreamingProgressNarration(
|
||||
entry: StreamingCompatEntry | null | undefined,
|
||||
defaultValue = true,
|
||||
|
||||
@@ -93,19 +93,19 @@ describe("normalizeCompatibilityConfigValues preview streaming aliases", () => {
|
||||
});
|
||||
|
||||
it("pins the previous default mode when delivery-only aliases create the streaming object", () => {
|
||||
// Discord previews default to progress only while `streaming` is absent;
|
||||
// without aliasOnlyMode the migrated object would resolve to off.
|
||||
// Some channels distinguish an absent streaming object from a mode-free
|
||||
// object, so aliasOnlyMode preserves that channel-owned default.
|
||||
const res = normalizeStreaming({
|
||||
entry: { blockStreaming: true },
|
||||
pathPrefix: "channels.discord",
|
||||
pathPrefix: "channels.layered",
|
||||
resolvedMode: "off",
|
||||
aliasOnlyMode: "progress",
|
||||
});
|
||||
|
||||
expect(res.entry.streaming).toEqual({ mode: "progress", block: { enabled: true } });
|
||||
expect(res.changes).toEqual([
|
||||
"Moved channels.discord.blockStreaming → channels.discord.streaming.block.enabled.",
|
||||
"Set channels.discord.streaming.mode (progress) to keep the previous default while migrating flat streaming keys.",
|
||||
"Moved channels.layered.blockStreaming → channels.layered.streaming.block.enabled.",
|
||||
"Set channels.layered.streaming.mode (progress) to keep the previous default while migrating flat streaming keys.",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -125,15 +125,15 @@ describe("normalizeCompatibilityConfigValues preview streaming aliases", () => {
|
||||
it("does not apply aliasOnlyMode when a legacy mode source exists", () => {
|
||||
const res = normalizeStreaming({
|
||||
entry: { streamMode: "partial", blockStreaming: true },
|
||||
pathPrefix: "channels.discord",
|
||||
pathPrefix: "channels.layered",
|
||||
resolvedMode: "partial",
|
||||
aliasOnlyMode: "progress",
|
||||
});
|
||||
|
||||
expect(res.entry.streaming).toEqual({ mode: "partial", block: { enabled: true } });
|
||||
expect(res.changes).toEqual([
|
||||
"Moved channels.discord.streamMode → channels.discord.streaming.mode (partial).",
|
||||
"Moved channels.discord.blockStreaming → channels.discord.streaming.block.enabled.",
|
||||
"Moved channels.layered.streamMode → channels.layered.streaming.mode (partial).",
|
||||
"Moved channels.layered.blockStreaming → channels.layered.streaming.block.enabled.",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -142,14 +142,14 @@ describe("normalizeCompatibilityConfigValues preview streaming aliases", () => {
|
||||
// semantics, so pinning a mode would change behavior instead of keeping it.
|
||||
const res = normalizeStreaming({
|
||||
entry: { streaming: { chunkMode: "newline" }, blockStreaming: true },
|
||||
pathPrefix: "channels.discord",
|
||||
pathPrefix: "channels.layered",
|
||||
resolvedMode: "off",
|
||||
aliasOnlyMode: "progress",
|
||||
});
|
||||
|
||||
expect(res.entry.streaming).toEqual({ chunkMode: "newline", block: { enabled: true } });
|
||||
expect(res.changes).toEqual([
|
||||
"Moved channels.discord.blockStreaming → channels.discord.streaming.block.enabled.",
|
||||
"Moved channels.layered.blockStreaming → channels.layered.streaming.block.enabled.",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -174,8 +174,8 @@ describe("normalizeCompatibilityConfigValues preview streaming aliases", () => {
|
||||
});
|
||||
|
||||
describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
|
||||
// Discord-shaped options: object-without-mode default "off", absent default
|
||||
// "progress", account merge replaces the root streaming object wholesale.
|
||||
// Synthetic replace-semantics channel: object-without-mode default "off",
|
||||
// absent default "progress", and account streaming replaces the root object.
|
||||
function normalizeChannel(
|
||||
entry: Record<string, unknown>,
|
||||
options?: { seedAccountStreamingFromRoot?: boolean },
|
||||
@@ -183,7 +183,7 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
|
||||
const changes: string[] = [];
|
||||
const result = normalizeLegacyChannelAliases({
|
||||
entry,
|
||||
pathPrefix: "channels.discord",
|
||||
pathPrefix: "channels.layered",
|
||||
changes,
|
||||
seedAccountStreamingFromRoot: options?.seedAccountStreamingFromRoot ?? true,
|
||||
resolveStreamingOptions: (value) => ({
|
||||
@@ -211,8 +211,8 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
|
||||
block: { enabled: true },
|
||||
});
|
||||
expect(res.changes).toEqual([
|
||||
"Moved channels.discord.accounts.work.blockStreaming → channels.discord.accounts.work.streaming.block.enabled.",
|
||||
"Set channels.discord.accounts.work.streaming.mode (progress) to keep the previous default while migrating flat streaming keys.",
|
||||
"Moved channels.layered.accounts.work.blockStreaming → channels.layered.accounts.work.streaming.block.enabled.",
|
||||
"Set channels.layered.accounts.work.streaming.mode (progress) to keep the previous default while migrating flat streaming keys.",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -231,8 +231,8 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
|
||||
});
|
||||
expect(res.entry.streaming).toEqual({ mode: "block", block: { coalesce: { idleMs: 5 } } });
|
||||
expect(res.changes).toEqual([
|
||||
"Moved channels.discord.accounts.work.chunkMode → channels.discord.accounts.work.streaming.chunkMode.",
|
||||
"Copied channels.discord.streaming into channels.discord.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
|
||||
"Moved channels.layered.accounts.work.chunkMode → channels.layered.accounts.work.streaming.chunkMode.",
|
||||
"Copied channels.layered.streaming into channels.layered.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -251,8 +251,8 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
|
||||
});
|
||||
expect(streaming.mode).toBeUndefined();
|
||||
expect(res.changes).toEqual([
|
||||
"Moved channels.discord.accounts.work.blockStreaming → channels.discord.accounts.work.streaming.block.enabled.",
|
||||
"Copied channels.discord.streaming into channels.discord.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
|
||||
"Moved channels.layered.accounts.work.blockStreaming → channels.layered.accounts.work.streaming.block.enabled.",
|
||||
"Copied channels.layered.streaming into channels.layered.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -309,7 +309,7 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
|
||||
mode: "off",
|
||||
});
|
||||
expect(res.changes).toEqual([
|
||||
"Moved channels.discord.accounts.work.streaming (boolean) → channels.discord.accounts.work.streaming.mode (off).",
|
||||
"Moved channels.layered.accounts.work.streaming (boolean) → channels.layered.accounts.work.streaming.mode (off).",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -326,7 +326,7 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
|
||||
|
||||
expect(workStreaming(res.entry)).toEqual({ chunkMode: "newline" });
|
||||
expect(res.changes).toEqual([
|
||||
"Moved channels.discord.accounts.work.chunkMode → channels.discord.accounts.work.streaming.chunkMode.",
|
||||
"Moved channels.layered.accounts.work.chunkMode → channels.layered.accounts.work.streaming.chunkMode.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,19 +8,19 @@ function cfgWith(channelId: string, entry: Record<string, unknown>): OpenClawCon
|
||||
}
|
||||
|
||||
describe("defineChannelAliasMigration message generation", () => {
|
||||
it("generates preview-chunk channel messages (discord shape)", () => {
|
||||
it("generates preview-chunk channel messages with an absent-object default", () => {
|
||||
const migration = defineChannelAliasMigration({
|
||||
channelId: "discord",
|
||||
channelId: "preview",
|
||||
streaming: { defaultMode: "off", absentObjectDefault: "progress", includePreviewChunk: true },
|
||||
});
|
||||
|
||||
expect(migration.legacyConfigRules.map((rule) => rule.message)).toEqual([
|
||||
'channels.discord.streamMode, channels.discord.streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.discord.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}. Run "openclaw doctor --fix".',
|
||||
'channels.discord.accounts.<id>.streamMode, streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.discord.accounts.<id>.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}. Run "openclaw doctor --fix".',
|
||||
'channels.preview.streamMode, channels.preview.streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.preview.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}. Run "openclaw doctor --fix".',
|
||||
'channels.preview.accounts.<id>.streamMode, streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.preview.accounts.<id>.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}. Run "openclaw doctor --fix".',
|
||||
]);
|
||||
expect(migration.legacyConfigRules.map((rule) => rule.path)).toEqual([
|
||||
["channels", "discord"],
|
||||
["channels", "discord", "accounts"],
|
||||
["channels", "preview"],
|
||||
["channels", "preview", "accounts"],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -114,9 +114,9 @@ describe("defineChannelAliasMigration rule matching", () => {
|
||||
});
|
||||
|
||||
describe("defineChannelAliasMigration normalizeChannelConfig", () => {
|
||||
it("migrates root and account aliases with dm normalization", () => {
|
||||
it("migrates preview root and account aliases with dm normalization", () => {
|
||||
const migration = defineChannelAliasMigration({
|
||||
channelId: "discord",
|
||||
channelId: "preview",
|
||||
streaming: { defaultMode: "off", absentObjectDefault: "progress", includePreviewChunk: true },
|
||||
accountStreamingReplacesRoot: true,
|
||||
dm: { root: true, accounts: true },
|
||||
@@ -124,7 +124,7 @@ describe("defineChannelAliasMigration normalizeChannelConfig", () => {
|
||||
|
||||
const changes: string[] = [];
|
||||
const result = migration.normalizeChannelConfig({
|
||||
cfg: cfgWith("discord", {
|
||||
cfg: cfgWith("preview", {
|
||||
streamMode: "block",
|
||||
dm: { policy: "open" },
|
||||
accounts: { work: { draftChunk: { minChars: 9 } } },
|
||||
@@ -133,7 +133,7 @@ describe("defineChannelAliasMigration normalizeChannelConfig", () => {
|
||||
});
|
||||
|
||||
expect(result.changes).toBe(changes);
|
||||
expect((result.config.channels as Record<string, unknown>).discord).toEqual({
|
||||
expect((result.config.channels as Record<string, unknown>).preview).toEqual({
|
||||
dmPolicy: "open",
|
||||
streaming: { mode: "block" },
|
||||
accounts: {
|
||||
@@ -143,11 +143,11 @@ describe("defineChannelAliasMigration normalizeChannelConfig", () => {
|
||||
},
|
||||
});
|
||||
expect(changes).toEqual([
|
||||
"Moved channels.discord.dm.policy → channels.discord.dmPolicy.",
|
||||
"Removed empty channels.discord.dm after migration.",
|
||||
"Moved channels.discord.streamMode → channels.discord.streaming.mode (block).",
|
||||
"Moved channels.discord.accounts.work.draftChunk → channels.discord.accounts.work.streaming.preview.chunk.",
|
||||
"Copied channels.discord.streaming into channels.discord.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
|
||||
"Moved channels.preview.dm.policy → channels.preview.dmPolicy.",
|
||||
"Removed empty channels.preview.dm after migration.",
|
||||
"Moved channels.preview.streamMode → channels.preview.streaming.mode (block).",
|
||||
"Moved channels.preview.accounts.work.draftChunk → channels.preview.accounts.work.streaming.preview.chunk.",
|
||||
"Copied channels.preview.streaming into channels.preview.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user