Skip to content

fix(telegram): smart-split long responses instead of silent (truncated) cut - #54

Closed
neomnezia wants to merge 9 commits into
ClickHouse:mainfrom
neomnezia:telegram-smart-split
Closed

neomnezia wants to merge 9 commits into
ClickHouse:mainfrom
neomnezia:telegram-smart-split

Conversation

@neomnezia

Copy link
Copy Markdown
Contributor

Summary

  • Replaces the silent (truncated) cut in TelegramChannel.format_response with a hierarchical, code-fence-aware splitter.
  • format_response is now identity; chunking happens in send via _smart_split so existing call sites (stream_adapter, router) keep working.
  • Adds (N/M) continuation markers and a 100 ms throttle between chunks to stay inside Telegram's 1 msg/sec/chat limit.

Test plan

  • pytest tests/test_telegram_split.py — 10 unit tests pass (paragraph/line/sentence/char fallback, fence balance, language-tag reopening, regression on truncation).
  • Full suite: pytest tests/ shows no regressions vs main (422 passed, 2 skipped).
  • Manual: ask the live bot for an 8 KB response; verify it arrives as multiple (N/M) chunks, no (truncated).
  • Manual: ask for a long code-fenced response; verify each chunk has balanced ``` fences and the continuation chunk reopens with the original language tag.

Closes 2026-04-25-smart-split-длинных-telegram-сообщений-.

constkolesnyak and others added 9 commits April 21, 2026 17:39
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Global `agent.effort` (e.g. "max") is shared across the main agent and
auxiliary models (`cron_model`, memU recall/memorize). Sonnet/Haiku tiers
only advertise {low, medium, high}; forwarding "max" triggers a 400 from
CLIProxyAPI tier validation (`internal/thinking/validate.go:125`):

    thinking: validation failed | provider=claude model=claude-sonnet-4-6
      error=level "max" not supported, valid levels: low, medium, high

Same shape on Opus 4.6 with "xhigh" (supports max but not xhigh), and on
any auxiliary model the user configures below their main Opus 4.7.

Add `_effective_effort(value, model)` — a small table of known Claude
models with their advertised effort levels, and a step-down lookup that
caps the requested effort to the highest level the target model supports.
Unknown models pass through unchanged (backward compatible).

Preserves "max" for Opus 4.7 main agent while letting Sonnet cron/memU
calls succeed with the tier-appropriate level.

Symmetric with existing `_parse_thinking_config(value, model)` which is
already model-aware.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `_MODEL_EFFORT_LEVELS` keys changed to substring form (`opus-4-7`,
  `opus-4-6`, `sonnet-4-6`) iterated against the full model name so
  dated Anthropic aliases like `claude-opus-4-7-20260416` resolve.
  Mirrors the MODEL_PRICING pattern in nerve/db/usage.py and
  `_model_supports_legacy_enabled_thinking` in the same file.
- `_effective_effort(value, model=None)` — default matches the sibling
  `_parse_thinking_config(value, model=None)`.
- `logger.debug` when capping happens so users can trace why a
  configured `effort: max` was forwarded as `high` to Sonnet.
- Docstring trimmed to one line to match the sibling method style.
- New `tests/test_engine.py` covering the cap table (incl. dated
  aliases, unknown models, None/empty model, invalid effort string).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l dispatch

Phantom \`[mcp__nerve__send_file]\` headers in Telegram fixed: the tool
now actually delivers files via the bound channel, not just persists a
DB record for the web frontend.

- \`SEND_FILES\` channel capability + \`BaseChannel.send_file\` interface.
- Telegram \`send_document\` impl (≤50 MiB cap, path/exists checks).
- Web \`send_file\` no-ops since the existing \`SendFileBlock\` card
  already handles UI delivery.

Cross-channel hardening (addresses 3 P1 findings from Codex review):

- \`AgentEngine\` tracks active channel per session
  (\`_active_channel\` set in \`run()\`, cleared on exit) and exposes
  \`get_active_channel(session_id)\`.
- \`ChannelRouter.send_file(session_id, file_path, channel=...)\`
  requires an explicit channel. Cached \`_message_context\` target is
  reused only when its channel matches the requested one — never to
  pick the destination channel. \`channel=None\` returns False
  unconditionally (cron / planner / notifications / web routes that
  don't pass a channel through \`engine.run()\` no longer leak files
  to a stale Telegram chat from a prior inbound message).
- \`_send_file_impl\` reads \`engine.get_active_channel\` and forwards.
- Path-aware workspace containment check (\`Path.relative_to\` /
  \`ValueError\`) replaces the bypassable \`str.startswith\` guard —
  closes a sibling-prefix exfiltration vector exposed by real
  Telegram delivery.

Test coverage: 29 unit tests across router, Telegram channel, tool
impl, engine accessor — including the cross-channel leakage
scenarios and sibling-prefix bypass attempt.

Smoke-tested end-to-end on Telegram: agent-invoked \`send_file\` on a
workspace file now delivers the document to the chat.
The 50 MiB precheck was redundant — the surrounding try/except already
catches the Telegram API's "request entity too large" response and
returns False with a logged warning. The fallback message in
\`_send_file_impl\` ("File ready: ... open the web panel to download")
still fires either way.

Removing the hard-coded cap also fixes self-hosted Bot API server
deployments, which lift the per-document limit to 2 GiB.

- Drop \`TelegramChannel._DOCUMENT_SIZE_LIMIT\` + size precheck.
- Drop \`test_oversized_file_returns_false\` (covered by
  \`test_send_document_failure_returns_false\` which mocks the API
  rejection directly).
- Behavior unchanged on \`api.telegram.org\` for files \\<50 MiB.
Replaces the (truncated) cut at MAX_MSG_LEN with a hierarchical,
fence-aware splitter. format_response is now identity; send iterates
the split chunks. Adds (N/M) continuation markers and a 100ms throttle
between chunks to stay inside Telegram's 1 msg/sec/chat rate limit.

Closes task 2026-04-25-smart-split-длинных-telegram-сообщений-
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 4 committers have signed the CLA.

✅ constkolesnyak
✅ neomnezia
❌ hun7er
❌ xah7ep@gmail.com


xah7ep@gmail.com seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@neomnezia

Copy link
Copy Markdown
Contributor Author

Wrong target repo. Reopening on neomnezia/nerve.

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.

4 participants