Skip to content

fix(telegram): buffered streaming for Pi token-level chunks#1716

Open
fezzzza wants to merge 4 commits into
coleam00:devfrom
fezzzza:fix/telegram-pi-buffered-streaming
Open

fix(telegram): buffered streaming for Pi token-level chunks#1716
fezzzza wants to merge 4 commits into
coleam00:devfrom
fezzzza:fix/telegram-pi-buffered-streaming

Conversation

@fezzzza
Copy link
Copy Markdown

@fezzzza fezzzza commented May 18, 2026

Summary

  • Problem: Pi provider (via z.ai, GLM-4.5-Air) emits token-level text_delta events that drop newlines and spaces. Each token becomes a separate Telegram message, and the concatenated result has mangled formatting (missing newlines between headings, lists, and body text).
  • Why it matters: Telegram becomes unusable with Pi-based reasoning models — responses arrive as dozens of single-character messages with broken markdown.
  • What changed: (1) Pi event bridge suppresses text_delta streaming and emits the full assembled text from agent_end.messages instead, which preserves correct formatting. (2) Telegram adapter gains a "buffered" streaming mode that coalesces chunks via a 3-second debounce timer.
  • What did not change: No changes to MarkdownV2 conversion, other adapters (Slack, Discord, GitHub), or non-Pi providers (Claude, Codex).

UX Journey

Before

Pi emits text_delta "H" → Telegram message "H"
Pi emits text_delta "e" → Telegram message "e"
Pi emits text_delta "llo" → Telegram message "llo"
... 80+ messages for a single response, newlines dropped, markdown broken

After

Pi emits text_deltas (suppressed) → agent_end fires
  → assembled text emitted as single chunk
  → Telegram adapter buffers, debounces, sends as 1 message
User sees clean response with proper formatting

Architecture Diagram

Before

Pi text_delta → event-bridge → orchestrator → Telegram adapter → Telegram API (per token)

After

Pi text_delta → event-bridge [suppressed]
Pi agent_end → event-bridge [emits assembled text] → orchestrator → Telegram adapter [buffered] → Telegram API (single message)
From To Status Notes
Pi event-bridge Orchestrator modified Suppresses text_delta, emits assembled text
Telegram adapter Orchestrator modified New buffered streaming mode
Other adapters Orchestrator unchanged Not affected

Label Snapshot

  • Risk: risk: medium
  • Size: size: S
  • Scope: adapters, providers
  • Module: adapters:telegram, providers:pi

Change Metadata

  • Change type: bug
  • Primary scope: adapters

Linked Issue

  • Related to Pi provider support and reasoning model compatibility

Validation Evidence (required)

bun run validate

All checks pass: type-check, lint (max-warnings 0), format, tests.

  • Evidence provided: Manual testing with Pi/z.ai GLM-4.5-Air via Telegram. Verified proper markdown rendering (bold, italic, code blocks, headings, lists). Confirmed single-message delivery instead of 80+ token-level messages.

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? No

Compatibility / Migration

  • Backward compatible? Yes — "buffered" is opt-in via TELEGRAM_STREAMING_MODE=buffered env var
  • Config/env changes? Yes — set TELEGRAM_STREAMING_MODE=buffered for Pi providers with token-level streaming
  • Database migration needed? No

Human Verification (required)

  • Verified scenarios: Telegram chat with Pi/z.ai GLM-4.5-Air, markdown-rich responses, long multi-paragraph responses, whitespace-only chunks
  • Edge cases checked: Short buffer re-buffering, shutdown flush, MarkdownV2 fallback on parse failure
  • What was not verified: Other Pi providers (Anthropic, OpenAI via Pi) — they may not need buffered mode

Side Effects / Blast Radius (required)

  • Affected subsystems: Pi event bridge (bridgeSession), Telegram adapter
  • Potential unintended effects: Pi streaming no longer provides real-time token-by-token output (acceptable tradeoff for correct formatting)
  • Guardrails: Buffered mode is opt-in; default stream/batch modes unchanged

Rollback Plan (required)

  • Fast rollback: revert this PR
  • Config toggle: remove TELEGRAM_STREAMING_MODE=buffered to revert to stream mode
  • Observable failure: Telegram messages arrive as single-character tokens (pre-existing behavior)

Risks and Mitigations

  • Risk: Pi assembled text may also drop content in edge cases
    • Mitigation: extractLastAssistantText reads from the full transcript which is the authoritative source
  • Risk: 3-second debounce may feel sluggish for short responses
    • Mitigation: Short-buffer skip (<50 chars) ensures rapid responses still flush quickly
  • Risk: Pi SDK bug — assembled transcript escapes nested markdown (**bold and *italic*** becomes **\\*\\*bold and \\*italic\\*\\*\\***)
    • Mitigation: Cannot be fixed in Archon; filed as upstream Pi SDK issue

Summary by CodeRabbit

  • New Features

    • Theme toggle in the top navigation to switch light/dark.
    • Telegram adapter adds a buffered streaming mode for smoother message delivery.
  • Bug Fixes

    • Prevents theme “flash” on initial load by applying saved preference early.
    • Streamed provider messages now emit a single assembled assistant message for more consistent outputs.
  • Chores

    • Updated telegramify-markdown dependency.
  • Tests

    • Updated tests to cover buffered Telegram mode and adjusted streaming expectations.

Review Change Stack

fezzzza added 3 commits May 17, 2026 21:58
Adds a theme switcher to the Web UI with light and dark mode support.
Defaults to dark (preserving existing appearance). User preference is
persisted in localStorage and applied before React hydrates to prevent
flash of wrong theme.

Changes:
- index.css: Add :root:not(.dark) light theme variables, rename :root
  to :root.dark for dark theme
- index.html: Add inline script to read localStorage and set .dark
  class before first paint (flash prevention)
- useTheme.ts: New hook that toggles .dark class on <html> and
  persists to localStorage
- TopNav.tsx: Add Sun/Moon toggle button next to version number
Addresses CodeRabbit review comments on coleam00#1713.

- index.html: Wrap pre-mount IIFE in try-catch, fallback to dark mode
- useTheme.ts: Wrap both getItem and setItem in try-catch with comments
  matching the established pattern in ProjectContext/Sidebar/WorkflowBuilder
Pi's text_delta events can drop newlines and spaces from the model's
output. The assembled transcript from agent_end.messages preserves the
correct formatting. This change suppresses text_delta streaming and
emits the full assembled text instead.

For Telegram specifically, adds a 'buffered' streaming mode that
coalesces chunks via a 3-second debounce timer. This prevents each
token from becoming a separate Telegram message.

- Pi event bridge: suppress text_delta, emit assembled text at agent_end
- Telegram adapter: buffered mode with debounce, short-buffer skip
- Whitespace-only message guard for Telegram
- Shutdown flush to avoid losing in-flight buffered text
- Tests for buffered mode and updated streaming tail tests

Note: Pi's assembled transcript escapes nested markdown (e.g.
**bold and *italic*** becomes **\*\*bold and \*italic\*\*\*),
which is a Pi SDK bug that cannot be fixed in Archon.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c898918-da40-4a56-9771-068dce970ecc

📥 Commits

Reviewing files that changed from the base of the PR and between 16648c8 and 39b2403.

📒 Files selected for processing (4)
  • packages/adapters/src/chat/telegram/adapter.ts
  • packages/providers/src/community/pi/event-bridge.ts
  • packages/providers/src/community/pi/provider.test.ts
  • packages/server/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/adapters/src/chat/telegram/adapter.ts
  • packages/providers/src/community/pi/event-bridge.ts

📝 Walkthrough

Walkthrough

Adds Telegram buffered streaming mode with per-chat debounced flushing; updates Pi bridge to suppress streaming deltas and emit assembled assistant text at agent_end; implements persistent light/dark theme with pre-render script, hook, toggle UI, and CSS restructuring.

Changes

Telegram Adapter Buffered Mode

Layer / File(s) Summary
Streaming type and exports
packages/adapters/src/chat/telegram/adapter.ts, packages/adapters/src/chat/telegram/index.ts, packages/adapters/src/index.ts, packages/adapters/package.json
Adds `TelegramStreamingMode = 'stream'
Buffered send pipeline and flush behavior
packages/adapters/src/chat/telegram/adapter.ts
Per-chat buffering with debounce, ignore whitespace-only chunks, split/deliver long messages via deliverMessage/sendFormattedChunk, report streaming mode so 'batch' only when configured, flush buffers on stop.
Buffered mode tests
packages/adapters/src/chat/telegram/adapter.test.ts
Unit tests verify buffered mode reports stream, coalesces rapid sends into one debounced send after ~3000ms, and skips whitespace-only input.
Server env validation and usage
packages/server/src/index.ts
Reads and validates TELEGRAM_STREAMING_MODE against `stream

Pi Event Bridge Assembled Text Emission

Layer / File(s) Summary
Event bridge assembled-text handling
packages/providers/src/community/pi/event-bridge.ts
Suppresses per-delta assistant enqueues, captures assembled assistant text at agent_end, enqueues a single assistant chunk with assembled text before terminal result, resets structured-output buffer to assembled text, and removes corrective-suffix emission logic.
Event bridge test suite updates
packages/providers/src/community/pi/event-bridge.test.ts
Streaming scenarios updated to expect a single assembled assistant chunk at agent_end and no per-delta or corrective tail chunks.
Provider test alignment
packages/providers/src/community/pi/provider.test.ts
Scripted agent_end now includes assembled assistant text; tests assert text_delta suppression and single assembled assistant emission; helpers extended to pass assembled text.

Web Theme System Implementation

Layer / File(s) Summary
Pre-render theme script
packages/web/index.html
Inline CSS and script apply stored theme pre-hydration and set body background to avoid theme flash, with safe fallback to dark on error.
useTheme hook and TopNav toggle
packages/web/src/hooks/useTheme.ts, packages/web/src/components/layout/TopNav.tsx
New useTheme hook reads/persists theme, toggles dark class on documentElement, exposes toggleTheme; TopNav renders Sun/Moon toggle tied to hook.
Light-default CSS theme variables
packages/web/src/index.css
Move light palette to :root:not(.dark) and place dark palette under :root.dark; adjust scrollbar comment.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • coleam00/Archon#1581: Modifies Pi event-bridge streaming/tail completion behavior, directly related to assembled-text emission changes.
  • coleam00/Archon#1066: Changes TelegramAdapter message delivery/stop behavior, related to buffered send and flush adjustments.
  • coleam00/Archon#1440: Adjusts structured-output parsing around Pi provider/bridge, relevant to structuredOutput buffering changes.

Poem

🐰 I nudge small chunks into a single song,
Light and dark I toggle all day long,
Bridges now wait till words are whole—
Then sing the answer, tidy and bold.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: introducing buffered streaming mode for the Telegram adapter to handle Pi provider's token-level chunks that drop formatting.
Description check ✅ Passed The description is comprehensive and follows the template structure with all major sections completed: Summary (problem/why/what changed/scope), UX Journey (before/after), Architecture Diagram, validation evidence, security impact, compatibility, human verification, side effects, rollback plan, and risks/mitigations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
packages/adapters/src/chat/telegram/adapter.test.ts (1)

194-205: ⚡ Quick win

Avoid real 3.1s waits in buffered-mode tests.

Using wall-clock delays here makes the suite slower and more timing-sensitive in CI. Prefer deterministic timer control (or a test-specific shorter debounce path) for these assertions.

As per coding guidelines, "Prefer reproducible commands and locked dependency behavior in CI-sensitive paths; keep tests deterministic with no flaky timing or network dependence without guardrails".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adapters/src/chat/telegram/adapter.test.ts` around lines 194 - 205,
The test uses real 3.1s setTimeouts which slows and flakes CI; update the
buffered-mode tests (referencing adapter.sendMessage and mockSendMessage
expectations) to use deterministic timers or a test-only shorter debounce
instead of waiting 3100ms: replace the wall-clock wait with Jest fake timers
(advanceTimersByTime) or inject a configurable/dependency-injected debounce
delay and set it to a small value in the test so you can synchronously advance
timers and immediately assert the mockSendMessage call count and arguments.
packages/adapters/src/chat/telegram/adapter.ts (1)

127-139: ⚡ Quick win

Use standard event-state suffixes for the new buffer log events.

telegram.buffer_skip_short and telegram.buffer_flush do not follow the required {domain}.{action}_{state} pattern with standard states, which will make observability less consistent.

As per coding guidelines, "Use structured logging with Pino; event naming format: {domain}.{action}_{state} with standard states: _started, _completed, _failed, _validated, _rejected".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adapters/src/chat/telegram/adapter.ts` around lines 127 - 139, The
log event names used in the getLog().debug calls don't follow the required
{domain}.{action}_{state} pattern; update the two event strings where
getLog().debug is called around this.buffers handling—replace
'telegram.buffer_skip_short' with 'telegram.buffer_skip_rejected' (skip =>
rejected) and replace 'telegram.buffer_flush' with
'telegram.buffer_flush_started' (flush beginning) so they conform to the
standard states; ensure the calls in the code that reference the same events
(the getLog().debug invocations around the existing variable this.buffers and
the subsequent deliver/send logic) are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/adapters/src/chat/telegram/adapter.ts`:
- Around line 124-135: Re-buffering short Telegram chunks currently stores {
text, timer: null } so without a subsequent chunk the message never flushes;
when updating this.buffers (both the existing branch and the new set branch)
start or reset a flush timer (use BUFFER_FLUSH_TIMEOUT) and store the timer
handle in the buffer object instead of null so the buffer will be auto-flushed;
reference this.buffers, BUFFER_MIN_FLUSH_LENGTH, BUFFER_FLUSH_TIMEOUT, chatId
and the existing variable when implementing the timer logic to match the
module's other buffering/flush behavior.

In `@packages/providers/src/community/pi/event-bridge.ts`:
- Around line 367-371: On handling the agent_end event, always clear/reset
assistantBuffer instead of only doing so when finalAssembledText is truthy:
inside the branch where you check if (event.type === 'agent_end') keep the
queue.push for the final chunk but ensure assistantBuffer is assigned (e.g. to
finalAssembledText or an explicit empty string) unconditionally when
wantsStructured is true so stale text_delta content cannot persist; update the
logic around assistantBuffer and finalAssembledText (and the call to
extractLastAssistantText if used) to guarantee assistantBuffer is reset on every
agent_end.

In `@packages/server/src/index.ts`:
- Around line 618-620: The env value for TELEGRAM_STREAMING_MODE is being
blindly cast and can silently accept typos; before creating TelegramAdapter,
validate process.env.TELEGRAM_STREAMING_MODE (or the default 'stream') against
the allowed TelegramStreamingMode values (imported from '`@archon/adapters`') and
throw a clear startup error if it’s not one of them; then pass the validated
value (not an unchecked cast) into new
TelegramAdapter(process.env.TELEGRAM_BOT_TOKEN, streamingMode).

In `@packages/web/index.html`:
- Around line 25-39: The theme bootstrap IIFE that reads
localStorage.getItem('archon-theme') and toggles
document.documentElement.classList (adding/removing 'dark') must be moved from
the body to the document <head> so it runs before first paint; relocate the
self-invoking function (the anonymous IIFE) into the head element before React
mounts and keep the same try/catch behavior and localStorage key
('archon-theme') to ensure no flash of light occurs on initial load.

---

Nitpick comments:
In `@packages/adapters/src/chat/telegram/adapter.test.ts`:
- Around line 194-205: The test uses real 3.1s setTimeouts which slows and
flakes CI; update the buffered-mode tests (referencing adapter.sendMessage and
mockSendMessage expectations) to use deterministic timers or a test-only shorter
debounce instead of waiting 3100ms: replace the wall-clock wait with Jest fake
timers (advanceTimersByTime) or inject a configurable/dependency-injected
debounce delay and set it to a small value in the test so you can synchronously
advance timers and immediately assert the mockSendMessage call count and
arguments.

In `@packages/adapters/src/chat/telegram/adapter.ts`:
- Around line 127-139: The log event names used in the getLog().debug calls
don't follow the required {domain}.{action}_{state} pattern; update the two
event strings where getLog().debug is called around this.buffers
handling—replace 'telegram.buffer_skip_short' with
'telegram.buffer_skip_rejected' (skip => rejected) and replace
'telegram.buffer_flush' with 'telegram.buffer_flush_started' (flush beginning)
so they conform to the standard states; ensure the calls in the code that
reference the same events (the getLog().debug invocations around the existing
variable this.buffers and the subsequent deliver/send logic) are updated
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e34be7a2-f472-4986-8e51-0c3b605bf7fb

📥 Commits

Reviewing files that changed from the base of the PR and between 7bdf931 and 16648c8.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • packages/adapters/package.json
  • packages/adapters/src/chat/telegram/adapter.test.ts
  • packages/adapters/src/chat/telegram/adapter.ts
  • packages/adapters/src/chat/telegram/index.ts
  • packages/adapters/src/index.ts
  • packages/providers/src/community/pi/event-bridge.test.ts
  • packages/providers/src/community/pi/event-bridge.ts
  • packages/providers/src/community/pi/provider.test.ts
  • packages/server/src/index.ts
  • packages/web/index.html
  • packages/web/src/components/layout/TopNav.tsx
  • packages/web/src/hooks/useTheme.ts
  • packages/web/src/index.css

Comment thread packages/adapters/src/chat/telegram/adapter.ts
Comment thread packages/providers/src/community/pi/event-bridge.ts Outdated
Comment thread packages/server/src/index.ts Outdated
Comment thread packages/web/index.html
Comment on lines +25 to +39
<script>
// Apply saved theme before React renders to prevent flash
(function () {
try {
var theme = localStorage.getItem('archon-theme');
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
} catch (_) {
document.documentElement.classList.add('dark');
}
})();
</script>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move theme bootstrap script into <head> to reliably avoid first-paint flash.

At Line 25, running the class-toggle script in <body> can still allow a light first paint before .dark is applied. Put it in <head> before rendering starts.

Suggested change
   <head>
@@
+    <script>
+      (function () {
+        try {
+          var theme = localStorage.getItem('archon-theme');
+          if (theme === 'light') {
+            document.documentElement.classList.remove('dark');
+          } else {
+            document.documentElement.classList.add('dark');
+          }
+        } catch (_) {
+          document.documentElement.classList.add('dark');
+        }
+      })();
+    </script>
     <style>
@@
   <body>
-    <script>
-      // Apply saved theme before React renders to prevent flash
-      (function () {
-        try {
-          var theme = localStorage.getItem('archon-theme');
-          if (theme === 'light') {
-            document.documentElement.classList.remove('dark');
-          } else {
-            document.documentElement.classList.add('dark');
-          }
-        } catch (_) {
-          document.documentElement.classList.add('dark');
-        }
-      })();
-    </script>
     <div id="root"></div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<script>
// Apply saved theme before React renders to prevent flash
(function () {
try {
var theme = localStorage.getItem('archon-theme');
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
} catch (_) {
document.documentElement.classList.add('dark');
}
})();
</script>
<head>
<script>
(function () {
try {
var theme = localStorage.getItem('archon-theme');
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
} catch (_) {
document.documentElement.classList.add('dark');
}
})();
</script>
<style>
/* ... existing styles ... */
</style>
</head>
<body>
<div id="root"></div>
</body>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/index.html` around lines 25 - 39, The theme bootstrap IIFE that
reads localStorage.getItem('archon-theme') and toggles
document.documentElement.classList (adding/removing 'dark') must be moved from
the body to the document <head> so it runs before first paint; relocate the
self-invoking function (the anonymous IIFE) into the head element before React
mounts and keep the same try/catch behavior and localStorage key
('archon-theme') to ensure no flash of light occurs on initial load.

- adapter.ts: re-buffer short text with fallback timer so short final
  responses aren't held indefinitely in buffered mode
- event-bridge.ts: reset assistantBuffer unconditionally on agent_end
  to prevent stale text_delta content; remove duplicate agent_end block
- server/index.ts: add runtime validation of TELEGRAM_STREAMING_MODE
  env var against valid values ['stream', 'batch', 'buffered']
- provider.test.ts: update scriptedAgentEnd to include text in
  agent_end message content (matches real Pi SDK behavior), fixing
  outputFormat structured output tests that broke when text_delta
  suppression was added
@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented May 19, 2026

Review Summary

Verdict: blocking-issues

This PR adds a buffered streaming mode to the Telegram adapter for coalescing token-level chunks, updates the Pi event-bridge to emit assembled text at agent_end, and adds a light/dark theme toggle to the web UI. The code is well-structured and well-tested, but the new buffered mode and the public TelegramStreamingMode type are entirely undocumented — users won't know these exist or how to use them.

Blocking issues

  • packages/docs-web/src/content/docs/adapters/telegram.md: The new TELEGRAM_STREAMING_MODE=buffered option is not documented anywhere. Add a third option under "Configure Streaming Mode" explaining: debounce behavior, 3s flush timer, 50-char minimum threshold, and recommendation for token-chunk providers (e.g. Pi/z.ai with GLM-4.5-Air).

  • packages/docs-web/src/content/docs/reference/configuration.md: The TELEGRAM_STREAMING_MODE row in the Platform Adapters — Telegram table still shows only stream and batch. Update to list all three valid values (stream, batch, buffered).

Suggested fixes

  • packages/adapters/src/chat/telegram/adapter.ts:flushBuffer (lines ~116–122): The this.buffers.delete(chatId) + early return leaves a microsecond window where the map has no entry. If a new chunk races in before set(), the re-buffered text is lost. Move the buffers.set before the early return:

    // Build re-buffered state first, then set it
    this.buffers.set(chatId, rebuffered);
    if (state.timer) clearTimeout(state.timer);
    state.timer = null;
    // remove the early return's re-buffer — just return
  • packages/adapters/src/chat/telegram/adapter.ts:92-95: The markdown-formatting catch block discards the original error (catch {}). If sendPlainText also fails, the caller has no root-cause trace. Bind the error and propagate context:

    } catch (err) {
      getLog().warn({ err, chatId }, 'telegram.formatting_failed');
      try {
        await this.sendPlainText(id, chunk);
      } catch (plainErr) {
        getLog().error({ err: plainErr, chatId, rootErr: err }, 'telegram.send_both_formats_failed');
        throw plainErr;
      }
    }
  • packages/docs-web/src/content/docs/adapters/telegram.md: The streaming mode example still shows stream # stream (default) | batch without buffered. Update both the example and the inline comment to include buffered.

Minor / nice-to-have

  • packages/adapters/src/chat/telegram/adapter.ts:flushBuffer JSDoc: The multi-line JSDoc describes behavior that happens after this method ("Long responses are split at paragraph boundaries by deliverMessage"). Trim to: /** Accumulate a chunk into the per-chat buffer. Flushes when BUFFER_FLUSH_MS elapses with no new chunks. */

  • packages/adapters/src/chat/telegram/adapter.ts:bufferChunk test: No test for multiple concurrent chat IDs being buffered simultaneously. Add a test that sends chunks to two different chatIds and asserts each gets its own coalesced message.

  • packages/adapters/src/chat/telegram/adapter.ts whitespace skip test: Test covers '\n' but not '' or '\t'. Low priority — behavior is identical.

  • CHANGELOG.md: No entry for the buffered mode addition. If the project maintains a changelog, add an ### Added entry under [Unreleased].

Compliments

  • Excellent WHY-focused comments throughout. The Telegram API constraint (whitespace-only rejection) and the Pi streaming tail suppression rationale are exactly the kind of context that helps future readers.

  • The fire-and-forget delivery (void this.deliverMessage(...).catch(...)) with structured error logging (telegram.buffered_flush_failed) is a clean pattern — appropriate for a non-blocking async path.

  • The Pi event-bridge test updates accurately reflect the new behavior (text_deltas suppressed, assembled text at agent_end). Good test hygiene.


Reviewed via maintainer-review-pr workflow (Pi/Minimax). Aspects run: code-review, error-handling, test-coverage, comment-quality, docs-impact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants