Apus has two independent streaming layers, and there is no true token-delta streaming to channel messages:

Control UI startup status

After chat.send acknowledges an active run, the Gateway can send a typed, coarse startup status before assistant text or tool activity is visible. The Control UI shows this status beside the working indicator, with stages for workspace preparation, environment provisioning, context preparation, and model startup.

The first assistant delta or tool start permanently replaces startup status for that run. Approval status takes precedence while a tool is waiting for operator action. Worktree creation and initial cloud dispatch happen before a chat run exists, so their pre-run RPC progress is not presented as run startup status; environment provisioning appears here only when an active run reprovisions a reclaimed worker.

Block streaming (channel messages)

Block streaming sends assistant output in coarse chunks as it becomes available.

Model output
  └─ text_delta/events
       ├─ (blockStreamingBreak=text_end)
       │    └─ chunker emits blocks as buffer grows
       └─ (blockStreamingBreak=message_end)
            └─ chunker flushes at message_end
                   └─ channel send (block replies)

Controls (all under agents.defaults unless noted):

Key Values / shape Default
blockStreamingDefault "on" / "off" "off"
blockStreamingBreak "text_end" / "message_end" -
blockStreamingChunk { minChars, maxChars, breakPreference? } -
blockStreamingCoalesce { minChars?, maxChars?, idleMs? } (merge streamed blocks before send) -
*.streaming.block.enabled (channel override) true / false, forces block streaming per channel (and per account) -
*.textChunkLimit (e.g. channels.whatsapp.textChunkLimit) number, hard cap 4000
*.streaming.chunkMode "length" / "newline" "length"
channels.discord.maxLinesPerMessage number, soft line cap that splits tall replies to avoid UI clipping 17

streaming.chunkMode: "newline" splits on blank lines (paragraph boundaries), not every newline, before falling back to length chunking once the text exceeds the limit.

Bundled channels spell these overrides as channels.<id>.streaming.{chunkMode,block.enabled,block.coalesce}. The flat *.chunkMode / *.blockStreaming / *.blockStreamingCoalesce spellings are rejected by validation. Run apus doctor --fix to migrate legacy configs into the nested shape before starting the Gateway. See the Doctor migration guidance.

Boundary semantics for blockStreamingBreak:

Pending text phases

Transports that resolve commentary at the tool boundary withhold durable block replies until the phase is known. Chat Completions and native Ollama (api: "ollama", /api/chat) wait until the assistant message finishes, including with blockStreamingBreak: "text_end": earlier text can still become tool narration. A long tool-free answer therefore produces no durable chunks during generation.

Ordinary final answers and length-limited partial answers remain deliverable. The independent live assistant/preview stream can still update while generating when enabled and supported by the channel.

Media delivery with block streaming

When a plugin uses before_agent_finalize to validate the built-in runtime's answer, assistant replies stay deferred until that decision completes. A later answer supersedes deferred text from earlier tool turns, including when the final answer is NO_REPLY. This applies to both reply blocks and preview updates; it does not retract replies that were already sent. Commentary remains live, and media, reasoning, and completed answers to earlier user inputs are preserved. Each steered user input gets its own delivered answer, even when its pending tools were skipped. Media from a superseded answer is delivered without its old caption.

With block streaming off, media-bearing assistant messages can still be sent at message boundaries, with their captions attached. Preview updates do not count as separately delivered captions.

Streaming media must use structured payload fields such as mediaUrl or mediaUrls; streamed text is not parsed as an attachment command. When block streaming sends media early, Apus remembers that delivery for the turn. If the final assistant payload repeats the same media URL, final delivery strips the duplicate media instead of sending the attachment again.

Exact duplicate final payloads are suppressed. If the final payload adds distinct text around media that was already streamed, Apus still sends the new text while keeping the media single-delivery. This prevents duplicate voice notes or files on channels such as Telegram.

Chunking algorithm (low/high bounds)

Block chunking is implemented by EmbeddedBlockChunker:

maxChars is clamped to the channel textChunkLimit, so you cannot exceed per-channel caps.

Coalescing (merge streamed blocks)

When block streaming is enabled, Apus can merge consecutive block chunks before sending them, reducing single-line spam while still providing progressive output.

Human-like pacing between blocks

When block streaming is enabled, add a randomized pause between block replies, after the first block, so multi-bubble responses feel more natural.

agents.defaults.humanDelay.mode Behavior
off (default) No pause
natural 800-2500ms random pause
custom minMs/maxMs

Override per agent via agents.entries.*.humanDelay. Applies only to block replies, not final replies or tool summaries.

"Stream chunks or everything"

Block streaming follows agents.defaults.blockStreamingDefault unless a channel or account sets *.streaming.block.enabled explicitly. QQ Bot has no streaming.block keys and streams block replies unless channels.qqbot.streaming.mode is "off". Channels can stream a live preview (channels.<channel>.streaming.mode) without block replies. The blockStreaming* defaults live under agents.defaults, not the config root.

For Discord and Telegram, an explicitly configured non-off preview mode takes precedence over inherited agents.defaults.blockStreamingDefault: "on". Set that channel's streaming.block.enabled: true when block replies should override its preview. For ordinary single-agent turns, if a reply-modifying hook prevents Telegram previews, completed blocks use normal hooked delivery unless block streaming is explicitly disabled globally or for Telegram. Configured multi-agent group-thread turns do not use this forced fallback; like other turns without a preview, they retain the configured block delivery policy.

Preview streaming modes

Canonical key: channels.<channel>.streaming (nested { mode, ... }; legacy top-level boolean/string spellings are rewritten by apus doctor --fix).

Mode Behavior
off Disable preview streaming
partial Single preview replaced with latest text
block Preview updates in chunked/appended steps
progress Progress/status preview during generation, final answer at completion

streaming.mode: "block" is a preview-streaming mode for edit-capable channels such as Discord and Telegram; it does not by itself enable channel block delivery there. Use streaming.block.enabled for normal block replies. Microsoft Teams is the exception: it has no draft-preview block transport, so streaming.mode: "block" disables native streaming entirely and the reply lands as regular block delivery instead of native partial/progress streaming. Mattermost also differs: in block mode it rotates the preview between completed text and tool-activity blocks, so earlier blocks stay visible as separate posts instead of being overwritten in one editable draft.

Channel mapping

Discord, Telegram, and Slack default to progress when streaming is unset. Mattermost and MS Teams default to partial. Explicitly configured modes remain unchanged; off disables previews.

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 native card in threads; quiet outside threads
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 minChars: 200, maxChars: 800 (clamped to the channel textChunkLimit), and breakPreference: "paragraph".

Slack-only:

Legacy key migration

Channel Legacy keys Status
Telegram streamMode, scalar/boolean streaming Rewritten to streaming.mode by apus doctor --fix; not read at runtime
Discord streamMode, boolean streaming Rewritten to streaming.mode by apus doctor --fix; not read at runtime
Slack streamMode; boolean streaming; legacy nativeStreaming Rewritten to streaming.mode (and streaming.nativeTransport for the boolean/legacy forms) by apus doctor --fix; not read at runtime
Matrix scalar/boolean streaming Rewritten to streaming.mode (including Matrix's "quiet" mode) by apus doctor --fix; not read at runtime
Feishu boolean streaming Rewritten to streaming.mode by apus doctor --fix; not read at runtime
QQ Bot boolean streaming; streaming.c2cStreamApi Rewritten to streaming.mode (and streaming.nativeTransport for the boolean/c2cStreamApi forms) by apus doctor --fix; not read at runtime

Runtime behavior

Telegram

Discord

Slack

Mattermost

Matrix

Tool-progress preview updates

Preview streaming can also include tool-progress updates: short status lines like "searching the web", "reading file", or "calling tool" that appear in the same preview message while tools are running, ahead of the final reply. In Codex app-server mode, Codex preamble/commentary messages use this same preview path, so short "I am checking..." progress notes can stream into the editable draft without becoming part of the final answer. This keeps multi-step tool turns visually alive instead of silent between the first thinking preview and the final answer.

Responses commentary keeps each message item's identity through tool handoffs. Later tool updates do not replay earlier commentary as an extra combined preamble.

Long-running tools may emit typed progress before they return. For example, web_fetch arms a five-second timer when it starts: if the fetch is still pending, the preview shows Fetching page content...; if the fetch finishes or is canceled before then, no progress line is emitted. The later final tool result is still delivered normally to the model.

Supported surfaces:

Progress draft rendering

Progress cards replace the complete plan state in partial, block, and progress previews where plan updates are enabled. Visible steps follow the channel's line limits. Clearing a card removes its checklist and status while preserving other activity; an otherwise empty draft is deleted where the channel supports deletion. Microsoft Teams replaces a cleared interim preview with its progress label. With streaming.progress.label: false, Teams retains the interim preview until the next update or final reply. Failed or blocked writes leave the previous plan in place. Active previews retain a safe failure notice.

Progress-mode drafts (streaming.progress.*) have these per-channel settings:

Key Default Behavior
streaming.progress.maxLines 8 Max compact progress lines kept below the draft label
streaming.progress.maxLineChars 120 Max characters per compact line before truncation (word-aware)
streaming.progress.label "auto" Draft title; a custom string, or false to hide it
streaming.progress.labels built-in pool Candidate labels used when label: "auto"

Slack always renders progress mode as its fixed session-card layout; these limits still bound the activity rows and plan text inside that card.

Commentary progress lane

Beyond tool-progress, the compact progress renderer can surface one more lane in the draft:

{
  "channels": {
    "discord": {
      "streaming": { "mode": "progress", "progress": { "commentary": true } }
    }
  }
}

Keep progress lines visible but hide raw command/exec text:

{
  "channels": {
    "telegram": {
      "streaming": {
        "mode": "partial",
        "preview": {
          "toolProgress": true,
          "commandText": "status"
        }
      }
    }
  }
}

Use the same shape under another compact progress channel key, for example channels.discord, channels.matrix, channels.msteams, channels.mattermost, or Slack draft previews. For progress-draft mode, put the same policy under streaming.progress:

{
  "channels": {
    "telegram": {
      "streaming": {
        "mode": "progress",
        "progress": {
          "toolProgress": true,
          "commandText": "status"
        }
      }
    }
  }
}

Related