feat(channels): narrated progress drafts + activity receipt on the final answer#103463
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 47fafa7d8e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!text) { | ||
| consecutiveFailures += 1; | ||
| if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { | ||
| disabled = true; | ||
| } |
There was a problem hiding this comment.
Clear stale narration when generation fails
When a narration has already been rendered and later utility-model generations return empty or fail twice, this branch disables the narrator without sending any clearing update. The compositor keeps its narrationText set, so later tool progress remains hidden behind the stale sentence instead of falling back to the accumulated raw tool lines as the new docs promise. Clear the narration before disabling, or let an empty update restore the raw progress draft.
Useful? React with 👍 / 👎.
| `-# 🧠 2 thoughts · 🛠️ 5 tool calls · ⏱️ 12s`), and the status draft is | ||
| deleted once that answer is delivered. Busy channels keep no orphaned tool | ||
| log above the reply; error finals keep the draft as the visible record of | ||
| the failed turn. |
There was a problem hiding this comment.
Keep error progress drafts visible
For Discord progress-mode turns that end with isError: true, deliverDiscordPayload skips markUserFacingFinalDelivered() and createDiscordDraftPreviewController.cleanup() still clears any existing draft while finalReplyDelivered is false, so the newly documented guarantee is not true for failed turns. Either keep the draft after a delivered error final or avoid documenting that behavior.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a19ca1c222
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| toolProgress: z.boolean().optional(), | ||
| commandText: z.enum(["raw", "status"]).optional(), | ||
| commentary: z.boolean().optional(), | ||
| narration: z.boolean().optional(), |
There was a problem hiding this comment.
Regenerate channel metadata for narration opt-out
Adding the Zod key here does not update the channel JSON schemas used by config validation: validation.ts builds bundledChannelSchemaById from GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA, and the generated bundled channel metadata still has progress schemas with additionalProperties:false and no narration key (checked src/config/bundled-channel-config-metadata.generated.ts). As a result, the documented channels.discord.streaming.progress.narration: false opt-out is rejected as an unrecognized channel config key, leaving users with a utilityModel unable to disable the new default through normal validation; regenerate/update the bundled channel metadata and schema expectations with this property.
Useful? React with 👍 / 👎.
| const narrationProgressEnabled = | ||
| Boolean(draftStream) && | ||
| discordStreamMode === "progress" && | ||
| resolveChannelStreamingProgressNarration(params.discordConfig); |
There was a problem hiding this comment.
Honor toolProgress false before enabling narration
When streaming.progress.toolProgress is false, Discord already sets previewToolProgressEnabled false and the compositor shows only the label, matching the docs' “visually quiet until the final answer” behavior. This branch still enables narration from the default narration toggle alone, so any agent with a utilityModel gets onNarrationUpdate and the narrator converts hidden tool starts into a rendered status sentence/model call. Gate narration on previewToolProgressEnabled or require explicit narration: true when tool progress is disabled.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e818095db9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| function buildNarrationUserPrompt(input: ProgressNarrationInput): string { | ||
| const request = truncateAtWordBoundary( | ||
| input.userMessage.replace(/\s+/g, " ").trim(), |
There was a problem hiding this comment.
Redact request text before narration prompts
When a user message contains credentials or tokens and utilityModel points at a different provider than the primary model, this prompt forwards the raw request text to the narration model. Tool summaries go through the existing display/redaction path, but the request string here is only whitespace-normalized and truncated, so the new default narration path can leak secrets that users expected to stay with the primary turn; run the request through the existing sensitive-text redaction before building the utility-model prompt.
Useful? React with 👍 / 👎.
| // tool log above the reply. Error finals skip both and keep the draft | ||
| // as the visible record of the failed turn. | ||
| progressReceiptLine = buildProgressSummaryLine(); | ||
| clearProgressDraftAfterFinalDelivery = true; |
There was a problem hiding this comment.
Keep the draft when the final send fails
If Discord progress mode has already shown a working draft and the fresh final deliverDiscordReply throws, this flag is never consumed because the clear happens only after a successful send; the finally cleanup then sees finalReplyDelivered === false and clears the draft anyway. That contradicts the intended failure path and leaves users with neither the final answer nor the in-progress record when the final send fails, so the cleanup path needs to preserve the draft whenever deletion was pending on final delivery.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ebefa55f3a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const subject = params.hideCommandText | ||
| ? payload.name || "command" | ||
| : payload.title || payload.name || "command"; | ||
| const exit = typeof payload.exitCode === "number" ? ` (exit ${payload.exitCode})` : ""; | ||
| addNote(`${subject} failed${exit}`, { immediate: true }); |
There was a problem hiding this comment.
Redact failed command titles before narration
When Discord progress narration is enabled with a configured utilityModel and a command fails, the default commandText: "raw" path puts payload.title directly into the narration activity note, which is later sent to the utility model prompt. Command-output titles can contain the raw shell command, so a failed command like one with an inline token bypasses the redacted formatToolSummary path used for tool-start notes and can leak credentials to the utility-model provider; redact or reuse the same display formatter before calling addNote.
Useful? React with 👍 / 👎.
…nal answer Progress mode replaces raw tool lines with short utility-model narration of what the agent is doing (streaming.progress.narration, default on, requires an explicit utilityModel). On Discord the final answer now carries the -# activity receipt and the working draft is deleted once the answer lands, so busy channels keep no orphaned tool log above the reply. Formatting verified remotely via Testbox oxfmt --check (hook bypassed: no node_modules in this worktree).
…tream mode Discord resolves its own progress default, so the resolver must not re-derive mode with the generic partial fallback (narration was off for unset config).
streaming.progress.commandText: "status" hides raw exec/bash text from the channel draft; narration input now mirrors that policy so the utility model never receives more command detail than the draft shows (Codex review P2). Formatting verified remotely via Testbox oxfmt --check.
… regen channel metadata Reuse isCommandToolName (exec|shell|bash) so narration's commandText policy matches the draft formatter exactly, and regenerate bundled channel config metadata for the new streaming.progress.narration key (Codex review round 2). Gates verified on Testbox: config:channels:check, oxfmt --check, 4 test shards.
An empty narration update now falls the draft back to raw tool lines, and the narrator emits that clear when it disables after consecutive failures or the per-turn cap, so drafts never pin stale status text (Codex review round 3). Gates verified on Testbox: oxfmt --check + 4 test shards.
ebefa55 to
28bf77c
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 28bf77c91c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| toolProgress: z.boolean().optional(), | ||
| commandText: z.enum(["raw", "status"]).optional(), | ||
| commentary: z.boolean().optional(), | ||
| narration: z.boolean().optional(), |
There was a problem hiding this comment.
Restrict narration config to channels that use it
For Telegram, Mattermost, and Slack configs this shared progress schema now accepts streaming.progress.narration (and generated metadata exposes it), but repo-wide search shows only Discord wires onNarrationUpdate/pushNarrationProgress; the other progress-draft channels create/update tool progress without ever feeding narration. Users setting this accepted key outside Discord get a silent no-op despite the shared config surface, so either keep the schema channel-specific until those channels implement the reply-option path or wire narration for each channel that validates it.
Useful? React with 👍 / 👎.
…ce budget Two deliberate public SDK additions (resolveChannelStreamingProgressNarration, isCommandToolName via the streaming wildcard re-export) bump the pinned public-surface budgets to current counts (exports 10488, callable 5235). Gates verified on Testbox: oxfmt, targeted oxlint, docs:map:check, config:channels:check, check:test-types, 5 test shards.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 16db4f4e67
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (inFlight) { | ||
| pendingImmediate ||= immediate; | ||
| return; |
There was a problem hiding this comment.
Schedule a rerun for queued narration events
When ordinary tool/progress notes arrive while a narration generation is already in flight (the request can run for up to 10s), this branch records only whether any of them were immediate; if they are normal work events, the completion path never calls maybeRun again after the current generation finishes. A multi-step turn can therefore keep showing the first narration until some later event happens, even after enough new activity has accumulated to satisfy MIN_EVENTS_PER_NARRATION; track that any notes arrived while in flight, not just immediate failures.
Useful? React with 👍 / 👎.
…nal answer (openclaw#103463) * feat(channels): narrated progress drafts + activity receipt on the final answer Progress mode replaces raw tool lines with short utility-model narration of what the agent is doing (streaming.progress.narration, default on, requires an explicit utilityModel). On Discord the final answer now carries the -# activity receipt and the working draft is deleted once the answer lands, so busy channels keep no orphaned tool log above the reply. Formatting verified remotely via Testbox oxfmt --check (hook bypassed: no node_modules in this worktree). * fix(channels): keep narration toggle independent of channel-default stream mode Discord resolves its own progress default, so the resolver must not re-derive mode with the generic partial fallback (narration was off for unset config). * fix(auto-reply): honor status-only command text in narration model input streaming.progress.commandText: "status" hides raw exec/bash text from the channel draft; narration input now mirrors that policy so the utility model never receives more command detail than the draft shows (Codex review P2). Formatting verified remotely via Testbox oxfmt --check. * fix(auto-reply): share the draft's command-tool set for narration and regen channel metadata Reuse isCommandToolName (exec|shell|bash) so narration's commandText policy matches the draft formatter exactly, and regenerate bundled channel config metadata for the new streaming.progress.narration key (Codex review round 2). Gates verified on Testbox: config:channels:check, oxfmt --check, 4 test shards. * fix(channels): clear stale narration when the narrator stops mid-turn An empty narration update now falls the draft back to raw tool lines, and the narrator emits that clear when it disables after consecutive failures or the per-turn cap, so drafts never pin stale status text (Codex review round 3). Gates verified on Testbox: oxfmt --check + 4 test shards. * chore(config): regen bundled channel metadata after rebase onto main * chore: CI fixups — lint nits, test harness types, docs map, SDK surface budget Two deliberate public SDK additions (resolveChannelStreamingProgressNarration, isCommandToolName via the streaming wildcard re-export) bump the pinned public-surface budgets to current counts (exports 10488, callable 5235). Gates verified on Testbox: oxfmt, targeted oxlint, docs:map:check, config:channels:check, check:test-types, 5 test shards.
Closes #103461
What Problem This Solves
While an agent works, progress-mode channels accumulate raw tool lines (
🛠️ exec command,🧩 Wait,🔌 Gateway: failed) that read as jargon, get truncated mid-word, and force an edit on every tool event. On completion the draft collapses into a small-# 🛠️ 5 tool calls · ⏱️ 12sline that stays in the channel forever, disconnected from the answer — in busy channels the status block is buried within seconds and the answer lands minutes later with no visible link (analysis of ~1,470 archived agent status messages in the maintainer Discord: none were ever deleted).Why This Change Was Made
utilityModelconfigured, the progress draft replaces tool lines with 1–2 plain present-tense sentences describing the work, generated by the utility model and refreshed on a bounded trigger policy (first work event, then every 4 events or 12s, failures immediately, hard caps + auto-disable after consecutive failures). The label pool ("Clawing…") stays. Tool lines keep accumulating underneath and return if narration stops. The draft is edited only when the narration text changes, which also removes most edit churn. Narration input is bounded and redacted: inbound request text plus the same compact redacted tool summaries the draft renders — never raw command output.GetReplyOptions.onNarrationUpdate; narrator attaches once ingetReplyFromConfig(src/auto-reply/reply/progress-narrator.ts), so every channel can adopt it. Discord is wired in this PR (it defaults to progress mode).channels.<ch>.streaming.progress.narration(defaulttrue); only activates with an explicitutilityModel, never falls back to the primary model.-# 🧠 2 thoughts · 🛠️ 5 tool calls · ⏱️ 12s, task facts only) and the working draft is deleted after the answer delivers. Error finals keep the draft as the visible record of the failed turn. The previous edit-draft-into-summary path and the durable-reasoning special case are removed (one canonical path).extensions/telegram). Telegram/Mattermost narration wiring is follow-up work on the same core seam.User Impact
streaming.progress.narration: false; users without autilityModelkeep exactly the current behavior.docs/concepts/progress-drafts.md(new "Narrated status" section + finalization),docs/concepts/streaming.md(Discord runtime notes),docs/gateway/config-agents.md(utilityModelpowers narration; no primary-model fallback for it).Evidence
coral-hermit):pnpm test src/auto-reply/reply/progress-narrator.test.ts src/channels/progress-draft-compositor.test.ts src/channels/streaming.test.ts extensions/discord/src/monitor/message-handler.process.test.ts— 4 shards green (15 new narrator tests; compositor narration replace/dedupe/turn-reset; Discord receipt-on-final + draft-delete + narration wiring + disabled-toggle).oxfmt --checkgreen on all touched files (Testbox).pnpm check:changedrunning on the same Testbox lease; hosted CI on this PR provides the exact-head gate for landing.-#line), draftclear()after delivery, draft kept on error finals, narration rendering replacing tool lines, and the narration callback omitted when disabled.