The finished-turn digest ('🧠 N thoughts · 💬 N notes · 🛠️ N tool calls ·
⏱️ Ns') was synthesized onto three surfaces: Slack's Block Kit card footer
(removed in #123851), Discord's final-answer subtext, and a duplicate Telegram
implementation added to match Discord.
Nothing gated it — no config key for the receipt has ever existed on any
channel — so PR #122976, which deleted it from Slack's native progress surface,
could only ever be a partial removal. Finish it: live progress surfaces keep
their in-flight indicators, and a finished turn leaves the answer with nothing
synthesized appended.
Telegram's window collapsed *into* the digest, so it now retires like every
other surface; Discord already discarded its draft after the final landed. The
core tracker keeps only the live counters Slack's working footer reads.
The four Telegram dispatch controllers were partitions of one closure: ~75 factory parameter slots, 7 post-construction back-edge setters, a shared mutable state bag, and load-bearing construction order. One turn record now carries the once-resolved config and all state; the four files remain as implementation with a hand-written leaf type contract (four state-slice types). Rides along: dead generation fence deleted (constant-0 from birth), queuedFinal ||= fix with regression (suppressed exec-approval turns no longer trigger a spurious fallback), collapse resolver/mutator split. Dispatch tests and harness byte-identical to main; live E2E lifecycle proof on the PR.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(telegram): suppress message-tool-only reply fallbacks
Honor message-tool-only delivery when a non-silent Telegram reply is skipped in both ordinary group dispatch and native slash-command turns. Keep the existing visible fallback for genuinely empty native replies and cover both dispatch surfaces with regression tests.
Fixes#90091
* fix(telegram): preserve genuine delivery failure fallbacks
Suppress only non-silent skips owned by message-tool-only delivery. Preserve visible fallbacks when the same turn also encounters a real dispatch or final-delivery failure, with an explicit regression for the mixed failure path.
* fix(telegram): preserve native message-tool delivery errors
Track genuine native slash delivery failures so message-tool-only skip suppression never hides a failed response. Cover the exact mixed empty-skip and real-error path with a regression while preserving true-empty and intentionally cancelled fallbacks.
* test(telegram): preserve synchronous native delivery callbacks
* refactor(media): delete internal MsgContext.Media* parallel fields for fact-only runtime
Internal runtime now carries a fact-only RuntimeMsgContext: the legacy
MediaPath/MediaUrl/MediaType parallel fields (and plural/transcribed
variants), their alignment and default helpers, and bundled legacy payload
builders are removed from internal paths. Staging, hooks, Gateway, media
understanding, and the Telegram, QQ, Signal, Slack, iMessage, Discord, QA,
and Zalo plugins consume ordered MediaFact[] directly.
Retained boundaries per the program audit: public Plugin SDK MsgContext,
persisted transcript Media* rows, and documented template variables keep
working via projectMediaFacts at the five declared seams (fact owner,
channel payload, inbound-event, transcript persistence, SDK adapter).
Review-round hardening, each with regressions:
- hasStagedMediaProjection requires every path-bearing fact staged (was
any-satisfied; mixed contexts skipped staging unstaged facts).
- finalizeInboundContext returns Omit<T, LegacyMediaContextKey> so deleted
legacy fields leave the type when they leave the object.
- QQ image facts carry explicit kind: "image" (remote URLs have no MIME).
- resolveStagedMediaFacts adopts staged legacy paths positionally while
retaining canonical fact metadata and cardinality (staged projections
previously replaced canonical facts wholesale).
1,615 tests across 47 files; goldens untouched; delegated check:changed
green (run 30009882062).
* test(media): migrate remaining legacy Media* consumer suites to structured facts
Exact-head CI caught suites outside the curated affected set that still
asserted internal legacy fields. All were stale internal-field assertions
migrated to facts, except QA Channel, which had a real migration gap: it
still constructed the bundled legacy payload internally and now normalizes
saved attachments with toInboundMediaFacts and passes the declared media
parameter directly. Exhaustive rg sweep over every test referencing the
deleted fields (133 files, each run individually) is green.
Final producer batch of the media-placeholder program: Telegram primary
bodies are caption-only with one aligned structured fact per native
media; the message-cache kind-parsing regex is deleted (native kind
stored directly); reply-chain, debounce/forward, group-history, and
ambient transcript lines render structured facts via the shared
formatter (removing the plugin-local duplicate of the caption-less
literal); audio-transcript and sticker-description replacements gate on
structured facts instead of exact placeholder strings. Also restores
unconditional failed-retryable recording when media resolution is
aborted for live updates, so shutdown cannot silently settle an
undispatched update.
Both transports now enqueue durably and pump the shared drain: polling keeps
offset-after-committed-enqueue, webhook keeps 200-after-spool-write. Deletes
the reply fence, private claim-owner, retry policy, and per-transport
claim/replay/watchdog loops. Dispatch outcomes propagate: failed-retryable
releases for replay instead of tombstoning. Supersede policy stays
Telegram-owned, authorization-gated with pairing-store and owner identities
via the same resolver as normal ingress; room_event ambient pending remains
supersedable by a later user turn (#108656).
* fix(channels): time-gate progress drafts and defer narration until visible
Progress drafts started immediately on the second work event, so the 5s
initial delay never filtered small fast tasks and Discord showed narrated
status for turns that finish in seconds. The gate is now purely time-based
(explicit startNow paths like approvals/patches are unchanged), and the
progress narrator no longer spends utility-model completions before the
channel draft is visible - notes buffer so the first visible narration has
full context.
Surface: src/channels/streaming.ts gate, src/auto-reply/reply/progress-narrator.ts,
Discord draft-preview wiring; test updates across discord/matrix/msteams/slack.
Refs #105872
* feat(channels): model-preamble status headline for progress drafts
In progress mode the draft's status line is now the model's own typed pre-tool
preamble whenever it is fresh (always on, no new config); utility-model
narration fills the slot only when the model has been quiet for ~20s or emits
no preamble. Preambles never start the draft early - the time gate still owns
the small-vs-big decision - and narration generation is suppressed while a
fresh preamble exists, saving utility calls. Discord and Telegram render the
headline; the opt-in streaming.progress.commentary lane and its receipt
counters are unchanged.
Refs #105872
* refactor(channels): split status-text and telegram preview helpers to honor the LOC ratchet
The LOC ratchet forbids raising baseline entries, so the headline feature's
growth is paid for with clean splits: reasoning/commentary text normalization
moves to src/channels/progress-draft-status-text.ts, Telegram progress preview
rendering to extensions/telegram/src/progress-draft-preview.ts, and small
ownership moves (formatCompactionModelRef, resolveFollowupAbortSignal, shared
CLI preamble gate, Discord preamble item-event handling) shrink the runners
back under their pins. Baseline entries lowered to exact new counts; the
compositor entry is removed (424 lines, under the 500 ceiling).
Refs #105872
* test(auto-reply): add resolveFollowupAbortSignal to the queue mock factory
The explicit ./queue.js vi.mock factory must export every binding prod touches;
the relocated resolveFollowupAbortSignal was missing and failed the whole
followup-runner suite.
Refs #105872
* fix(channels): sanitize preamble headlines and retry skipped narration
Review findings: preamble headlines now strip inline delivery directives and
reject silent-control tokens (NO_REPLY) before rendering, sharing the
commentary sanitizer; rejected input keeps the previous valid headline. The
narrator schedules cancellation-aware retries when a run is skipped for draft
invisibility or preamble freshness, so single long tool calls still narrate
and the ~20s utility filler fires without needing another event. Silent
preambles no longer stamp freshness or add narration notes.
Refs #105872
* fix(channels): bound narrator visibility polling and gate Telegram headline mode on accepted preambles
Visibility retries stop after 30 one-second attempts (turn completion has no
narrator disposal signal) and re-arm on new activity; Telegram derives its
headline-rendering state from the compositor's sanitized headline instead of
raw payload text, so directive-only or NO_REPLY preambles keep structured
tool-line rendering. Docs scope the preamble headline to Discord and Telegram.
Refs #105872
* fix(telegram): read headline state from the compositor at render time
The progressHeadlineActive mirror flag was set after pushPreambleHeadline had
already rendered, so the first valid preamble arriving after the draft opened
rendered through the structured-line path and the headline never appeared
until the next event. The renderer and formatLine now read the compositor's
sanitized hasStatusHeadline directly and the mirror flag is deleted; adds an
ordering regression test. LOC baseline lowered to the exact new count.
Refs #105872
* fix(channels): let the opt-in commentary lane own preambles and scope narration docs to Discord
With streaming.progress.commentary enabled, the status headline would replace
the documented interleaved 💬 lines for any turn with a preamble (the exact
event the lane consumes). The compositor headline now declines when the
commentary lane is enabled, preserving that lane's shipped shape, and docs
state the utility-model filler is Discord-only today (Telegram's headline is
preamble-only; it has no narrator wiring).
Refs #105872
* fix(msteams): cancel the pending progress gate at finalize
With the purely time-based gate, every fast Teams progress turn leaves a
pending 5s start timer; the SDK resets its stream id on close, so a late
timer fired renderInformativeUpdate against the closed stream and posted a
stale working card below the final answer. finalize() now cancels the gate
and the informative renderer refuses to run once final text is queued.
Refs #105872
* docs(channels): record the headline-vs-commentary ownership decision inline
Refs #105872
* style: oxfmt the late review-fix rounds
Four files from the final review-fix commits were never oxfmt-formatted and
failed the shared CI formatting step; baseline lowered for the one-line
telegram shrink.
Refs #105872
* fix(channels): harden progress draft lifecycle
* fix(channels): refresh repeated preamble items
* fix(plugin-sdk): expose progress lifecycle options
* style(auto-reply): simplify narrator reset
* docs(channels): refresh progress draft docs map
* fix(auto-reply): clear queued narrator request context
* fix(discord): cancel delayed progress gate at final
* docs: refresh plugin sdk api baseline
Telegram no longer sends a generic "Something went wrong" fallback after a
final answer was already delivered and a later dispatch/cleanup step failed.
Failures with only partial or no visible output still send the error fallback
and stay retryable.
Related: #87299Closes#90152
Persist room-event observations as durable bare user transcript rows and carry an ambient transcript watermark through session state so Telegram chat windows only include the unpersisted gap.
Fixes#99257
ClawSweeper review finding: F3 only wrapped the cleanup-fallback bar send.
The normal path applyProgressCollapseSummary awaited an unguarded durable
send when finalizeToPreview could not edit in place, and sendPayload throws
durable.error on delivery failure — so a cosmetic summary-bar flood-wait
could propagate and fail an otherwise-complete turn (merge-risk:
message-delivery).
Route BOTH cosmetic-bar sends through one shared guarded helper
postCosmeticSummaryBar (swallow + logVerbose), so neither the cleanup path
nor the finalizeToPreview-miss path can fail turn delivery. Add the missing
regression: no-live-message fallback bar send throwing keeps the turn alive
and the final answer delivered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses five codex red-team findings on the streamed progress-window
collapse bar. Channel-side only; no core src/** changes.
F1 (transcript pollution): the collapse bar went through
sendPayload({durable:true}), which unconditionally passed the transcript
mirror to the deliverReplies fallback, writing the cosmetic digest
("💬 1 note · 🛠️ 1 tool call · ⏱️ 4s") into the session transcript so the
model read it back as its own prior turn. Added a mirrorTranscript option to
sendPayload; the bar now sends durably with mirrorTranscript:false. Discord
parity: its summary bar (monitor/reply-delivery.ts deliverDiscordReply →
sendDurableMessageBatch) has no transcript-mirror seam at all. Real finals
keep mirroring.
F4 (tool overcount): progressSummary.noteToolCall() fired for ANY start-phase
tool, but the compositor renders a line only for work tools
(isChannelProgressDraftWorkToolName) and only when toolProgress is on, so
codex/message_tool_only turns showed "🛠️ 1 tool call" with no tool line. The
count is now gated by the same public work-tool-name check plus
streamToolProgressEnabled; non-counting tool starts still close the
reasoning/commentary bursts as a boundary.
F2 (silent collapse drop): finalizeToPreview ignored a false return from the
in-place edit (flood-wait 429 / terminal error), so applyProgressCollapseSummary
assumed "edited", cleared state, posted no bar, and left the tall window.
finalizeToPreview now returns undefined when the edit did not apply, so the
dispatch falls back to the existing durable-post path.
F3 (cosmetic send fails the turn): the cleanup-time deliverProgressCollapseSummary
could throw (429/network) and propagate out of dispatch after the real final
already delivered. The bar send is now wrapped: failures log via logVerbose and
never fail the turn; the once-guard is preserved.
F5 (ghost-preview race): rotateToNewMessageDeferringDelete rewound while a FIRST
send was still in flight; the late send landed as superseded {retain:true}, which
the dispatch handler kept as an orphaned stale bubble. A reposition now records
the in-flight generation and deletes its late-landing message (deferred), while
forceNewMessage's retain-as-durable-chunk contract is unchanged.
Tests: added unit tests for each invariant (bar delivered but absent from the
transcript mirror; message-tool start → no 🛠️ count and toolProgress-off → no
count; failed edit → undefined; bar send throw → turn still succeeds; reposition
race → superseded send deleted not retained). Telegram suites green
(bot-message-dispatch 165, draft-stream 58, progress-summary 16); extensions
tsgo clean for telegram.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Progress mode's streaming window is a pure activity log (Discord parity): it
carries only 🧠/💬/🛠️ progress lines, and the answer appears once, as the final
message below the collapsed bar. Answer PARTIALS were already suppressed
(updateDraftFromPartial early-returns for the answer lane), but intermediate
answer BLOCKS (info.kind === "block", before the final) still streamed into the
window via deliverLaneText, so interim assistant prose flashed inside the
working bubble mid-turn.
Fix: in the deliver() answer-block branch, suppress plain interim answer blocks
in progress mode — buffer them (like the existing skipTextOnlyBlock path) so they
still feed the final/collapse, and skip the draft-stream delivery. Media,
approval, and button blocks are not plain interim prose and fall through to
normal delivery. With plain blocks now suppressed upstream, the block-branch's
progress rotation guard only guards those remaining media/button blocks; comment
updated to say so.
This is the product decision Peter approved (interim answer-block visibility in
the window); it completes the single-message model so the window truly only ever
shows progress lines.
Tests: added "never streams an interim answer block into the progress window
(Discord parity)" — an interim block never reaches update/updatePreview and never
appears in delivered replies, while the final answer is delivered below. 233
green across bot-message-dispatch, draft-stream, progress-summary;
extensions/telegram typechecks clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Progress mode now keeps ONE window message per turn, edited through every lane
handover and edited into the bar only at collapse — matching Discord's
single-message model. Previously the tool-progress window was rotated/repositioned
to a fresh bubble on every interim answer chunk and on the tools->text handover,
even though progress mode never renders interim answer text into the window
(updateDraftFromPartial already returns early for the answer lane). Each rotation
spawned a new message; that churn was the on-off jump's underlying source.
Fix — progress-mode-gated so the rotation branches fire only where they must
(block/partial); block/partial and finalized-rotation paths are untouched:
- prepareAnswerLaneForText returns early in progress mode — no rotate/reposition
for interim answer text that never displays. (bot-message-dispatch.ts:1288)
- prepareAnswerLaneForToolProgress no longer rotates in progress mode; the tool
lane always edits the same message (rotate stays for block/partial where answer
text streamed first). (bot-message-dispatch.ts:1064)
- the interim answer-block path no longer rotates the window to a new bubble in
progress mode. (bot-message-dispatch.ts:2356)
Rotation/delete paths REMAIN (all post-first / deferred-delete per cc9c2b13a9)
only for: block mode, forceNewMessage after a finalized answer (new turn), error
teardown with nothing to summarize, and superseded-generation cleanup. Collapse
uses task-9 order (final first, then edit the one window into the bar in place);
zero deleteMessage in the happy path. The end-of-turn forceNewMessage-after-
collapse is unchanged (end-of-turn, not mid-turn churn).
LOC: net +~24 prod lines, all justified — three small streamMode==="progress"
guards plus the invariant comments AGENTS.md requires for intentional caller
differences. The rotation branches are shared with block/partial and cannot be
deleted, only gated.
Tests: removed the now-obsolete task-8 "repositions the tool-progress window"
test (that path no longer runs in progress mode); added "keeps a single
stationary window when text follows durable reasoning" and a multi-boundary turn
"uses one stationary window message across a multi-boundary turn
(commentary->tool->commentary->tool->final)" asserting one window message id used
throughout, edited N times, zero deletes, one bar edit last, final posted before
the bar edit. 231 green across bot-message-dispatch, draft-stream, progress-summary;
extensions/telegram typechecks clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live off-off finding (no reposition involved): the final answer posted below the
viewport. Cause: collapseProgressWindowIntoSummary edited the tall window bubble
DOWN to a one-line bar BEFORE the final was sent; shrinking content under the
viewport breaks the Telegram client's at-bottom follow, so the freshly sent final
landed off-screen.
Fix — swap the order in deliverProgressModeFinalAnswer:
- Snapshot the bar line (resolveProgressCollapseSummaryLine, which also consumes
the once-guard) BEFORE the final send, so the final's own delivery cannot
perturb the counts/timer.
- Send the final answer FIRST (it lands at the bottom of the anchored viewport),
THEN collapse the window above it. The bar is an edit of an already-posted
message that sits above the final, so shrinking it is harmless.
- Failure path: if the final send skips/fails, still collapse to the bar (or tear
the window down when there is nothing to summarize) so no stale window lingers.
- Mark answerLane.finalized / markProgressFinalDelivered LAST, after the collapse
resets lane state, so end-of-turn cleanup sees a finalized lane (stop(), not a
spurious clear()).
Renamed collapseProgressWindowIntoSummary -> applyProgressCollapseSummary (takes
the pre-resolved line; "edited" | "posted").
Tests: updated the collapse tests to assert the new order (final delivered before
finalizeToPreview); added "sends the final answer before collapsing the window
into the bar" (order + counter snapshot) and "still collapses the window when the
final answer send is skipped". 230 green across bot-message-dispatch, draft-stream,
progress-summary; extensions/telegram typechecks clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Peter isolated the on-off focus-jump precisely: when a durable 🧠 posts BELOW
the streaming window, the window (now above the reasoning) is repositioned to
stay newest by DELETING it and reposting below. Telegram cannot move messages,
so the delete-then-repost order scroll-jumps the client.
Root cause: rotateAnswerLaneAfterToolProgress rewound the tool-progress window
with stream.clear(), which deletes the old message IMMEDIATELY when it has been
on screen past the dwell — before the replacement message is sent. Delete-first,
post-second.
Fix — invert the order, preserving arrival order:
- draft-stream: new rotateToNewMessageDeferringDelete() rewinds the stream so the
NEXT update creates a fresh message, and schedules the superseded message's
delete for AFTER it (detached, floored at 1.5s so the new message lands first).
Extracted the shared deferred-delete scheduler (scheduleDetachedDelete) used by
both clear() and the reposition. (draft-stream.ts)
- dispatch: rotateAnswerLaneAfterToolProgress now repositions via that method
instead of clear()+forceNewMessage, so no window reposition deletes before the
replacement lands. (bot-message-dispatch.ts:1259)
Tests: draft-stream unit tests prove the sequencing (new message sent before the
old is deleted; delete deferred; no-op with no live message). Added a dispatch
repro (durable 🧠 then answer text mid-turn -> reposition, no clear). Updated the
predating tool-progress-rotation tests to assert the deferred-delete reposition
and that any deliverer-cleanup clear() runs only AFTER the rewind. 228 green
across bot-message-dispatch, draft-stream, progress-summary; extensions/telegram
typechecks clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live on-off finding: the collapse path was inconsistent. Edit-in-place worked
in off-off/stream-off, but in on-off (tool-progress-only window, reasoning
durable) finalizeToPreview returned undefined and the caller fell into a bare
clear() -> deleteMessage -> Telegram focus-jump; a sibling sub-branch dropped
the bar entirely. Two divergent outcomes from one fallback.
Root cause: a throttled tool-progress preview could still be pending (coalesced,
never sent) when the turn ended, so the window had no message id even though it
had "rendered". finalizeToPreview gave up and the dispatch fallback deleted the
(late-landing) window.
Fixes:
- draft-stream finalizeToPreview: settle the stream, then MATERIALIZE a still-
pending preview (send it, as a final flush would) so the window message exists
and can be edited in place. Only when no message could be established does it
return undefined. (draft-stream.ts:597)
- dispatch collapse: one deterministic path returning "edited" | "posted" |
"none". A bar is ALWAYS surfaced when one exists — edited in place, or posted
durably with ZERO deleteMessage. clear()/delete now runs ONLY for the "none"
case (error final or nothing to summarize), never when a bar exists, so no
collapse path can focus-jump. Split into resolveProgressCollapseSummaryLine /
collapseProgressWindowIntoSummary / resetAnswerLaneAfterCollapse /
teardownProgressWindow for a readable branch. (bot-message-dispatch.ts:1949)
Tests: added on-off dispatch repro (tool-progress-only + durable reasoning +
mid-turn rotation + final -> edits into bar, no clear, exactly one bar), the
no-live-message durable-bar-without-delete case, and three draft-stream
finalizeToPreview cases (edit-in-place, pending-materialize, no-window).
225 green across bot-message-dispatch, draft-stream, progress-summary;
extensions/telegram typechecks clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Five progress-window fixes for the Telegram streaming lane, aligning it with
the Discord reference surface:
- FIX 2: /reasoning on + /verbose off no longer kills the progress window.
forceBlockStreamingForReasoning is now scoped to non-progress modes, so
durable reasoning removes only the 🧠 lane; commentary/tools still stream and
the collapse bar still posts.
- FIX 3: durable thoughts render behind the 🧠 marker instead of the literal
"Thinking" header. The core formatReasoningMessage output is rewritten
channel-side in reasoning-lane-coordinator (no core change), keeping the
italic body.
- FIX 4: /verbose on no longer duplicates tool calls. canPushStreamToolProgress
now yields under verbose so the durable verbose lane owns every progress
surface (invariant: persistent message XOR window).
- FIX 1: the progress window collapses by EDITING the existing message in place
into the summary bar (draft-stream finalizeToPreview), then posts the final
below — no delete + repost, which scroll-jumped the client. Falls back to
clearing the window when there is no bar to collapse into.
- FIX 5: message_tool_only/codex finals that bypass the in-band answer path now
post the collapse bar from a cleanup-time fallback (sawProgressFinal from the
dispatch counts).
Tests: adapted predating dispatch/reasoning tests to the new 🧠 marker and
collapse-by-edit behavior; added coverage for FIX 2 (window alive under
/reasoning on), FIX 4 (no window tool dup under verbose), and FIX 5
(message_tool_only collapse bar). 220 green across bot-message-dispatch,
draft-stream, progress-summary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the dev-beta2 progress-summary work onto the compositor-era dispatch:
- Post a one-line activity digest (🧠 N thoughts · 💬 N notes · 🛠️ N tool
calls · ⏱️ Ns, Discord parity) as a durable message when the progress
window collapses at end-of-turn, before the final answer. Counted
per-burst channel-side; only window-streamed activity feeds the bar
(durable rv/verbose items never do — persistent message XOR bar count),
no bar for error finals or turns where the window never rendered.
- Render multi-line commentary with its line structure intact (per-line
markdown conversion, joined as line breaks) instead of collapsing to
one run-on line; per-line conversion also keeps block markdown (setext
headings) from forming across lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Streamed progress on Telegram rendered reasoning and commentary
indistinguishably and leaked the model's raw markdown: the compositor
refactor dropped the per-channel lane markers and the markdown rendering
that the pre-compositor path had.
- Pass the 🧠/💬 lane prefixes and commentaryItalics:false to the shared
progress-draft compositor (mirroring Discord), so reasoning renders
italic and commentary plain, each behind its own marker.
- Render lane bodies through renderTelegramHtmlText — the parse_mode=HTML
safe converter — so **bold**, inline code, and _italic_ render as
intended rather than leaking raw markers. Lane lines collapse to one
line first: multi-line commentary otherwise forms block markdown (a
`\n\n---\n\n` separator turns the paragraph above it into a setext
<h2>), which Telegram rejects, dropping the whole preview (all lanes)
to unformatted plain text.
- Clip long lane lines INSIDE the whole-line _…_ wrapper: clipping the
assembled line chopped the closing underscore, silently degrading every
long reasoning line (the common case) from italic to plain text.
Generic item payloads keep their monospace styling. Matters most for
deepseek, which narrates progress almost entirely through its
markdown-heavy reasoning lane.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move Telegram group history out of the message body fold and into the structured chat-window context used by the inbound metadata renderer. Preserve outbound self entries, post-self watermarks, recovered forum-topic routing, and richer cached reply/media fields while dropping stale recovered-topic windows.\n\nFixes #99218