Skip to content

feat: fold quoted/replied-to message into the run prompt (Telegram + VK) - #62

Merged
zaytcevcom merged 4 commits into
mainfrom
duck/263043/quoted-message-context
Jul 24, 2026
Merged

feat: fold quoted/replied-to message into the run prompt (Telegram + VK)#62
zaytcevcom merged 4 commits into
mainfrom
duck/263043/quoted-message-context

Conversation

@zaytcevcom

Copy link
Copy Markdown
Contributor

Summary

When a user replies to or quotes an earlier message, the Telegram and VK adapters previously forwarded only the user's new text to the agent — the referenced original was lost. A reply like "Про это" ("about this") therefore reached the run with no idea what "this" was.

This folds the quoted original into the run prompt on both transports, through a single shared helper, across every input path (text, voice and media). The quoted text is framed as reference context — data, not instructions — and placed before the user's own message, which stays the operative instruction. A non-reply message is byte-for-byte unchanged.

Changes per acceptance criterion

  • AC1 — shared helper (core/chat/media.go): new QuotedPrompt(quotedAuthor, quotedText, userText), mirroring the existing DocumentPrompt/PhotoPrompt pattern. It places the quoted block first (framed as reference data), the user's message last; adds an author clause only when known; returns userText unchanged when the quote is blank (the no-op that keeps non-reply traffic identical); and truncates an over-long quote rune-safely at maxQuotedRunes = 2000 while never touching userText.
  • AC2 — Telegram (cmd/flock-telegram/main.go): quotedContext extracts the quoted original — the exact highlighted portion (msg.Quote.Text) if present, else the replied-to message's text, else its caption, else a generic [media] reference — and folds it via QuotedPrompt into the text, voice and media paths. Only the bot's own message (matched by id) is labeled "the assistant"; any other sender is attributed by name.
  • AC3 — VK (adapters/vk/longpoll.go, adapters/vk/receiver.go): the message object now parses reply_message and fwd_messages; quotedContext folds the replied-to message (preferred) or the first forwarded message — one level only — via the same QuotedPrompt, with a coarse assistant-vs-participant author label derived from from_id's sign (no extra users.get call).
  • AC4 — edges: a media-only quoted message yields a non-empty [media] reference (never a silent drop); an empty user message is still dropped (a bare reply with no new words does not run); a very long quote is truncated. Non-reply traffic and all pre-existing tests are untouched.

How it was verified

Run through the repo's own gate inside the dev-tools image (Go 1.26.5 / golangci-lint 2.11.4) — exactly what CI runs:

  • task testsgo test -race ./..., all packages green, including the pre-existing suites (regression guard) and new unit tests: QuotedPrompt (both texts distinguishable, blank → no-op, rune-safe truncation with a UTF-8 validity check); the Telegram handler (quote-wins-over-reply, own-bot vs third-party-bot label, media-only reply, non-reply unchanged, quote+caption+photo carries the quote and keeps the vision image); the VK receiver (reply, forward first-only, media-only reply/forward, non-reply unchanged).
  • task lint0 issues. task build — green.
  • Not verified: live end-to-end behaviour against real Telegram/VK Long Poll updates (see residual risks).

Pre-PR review

One adversarial review pass on the diff → clean round (no blocker/major), risk: low. It confirmed the load-bearing points: the no-op is byte-for-byte on non-reply, truncation is genuinely rune-safe, there is no double-wrap (media/voice paths return before the text-path wrap; each path calls the helper exactly once), VK pointers/slices are nil-guarded and read one level only, and prompt-injection framing is present.

Minor findings raised and fixed in this branch before opening:

  • The author label originally treated any bot as "the assistant" (from.IsBot), which would attribute a third-party bot's words to ourselves. Narrowed to the bot's own id (the same check as replyToBot); added a test for the third-party-bot case.
  • Added a test for the highest-preference msg.Quote branch (Telegram's native quote), previously unproven.
  • Broadened the media placeholder to any text-less reply (covers polls/locations, not just enumerated media types); dropped the now-redundant replyHasMedia enumeration.
  • Rune-count before allocating on the truncation path.

Residual risks / worth a human eye:

  • Live behaviour not exercised. Verified by unit + -race suite, not against a live bot. Worth a smoke test on each transport: a plain reply, a native quote selection, a forward, a reply to a photo/voice, and a reply to the bot's own message.
  • Quoted media is not described. Replying to a photo/file yields the literal [media] marker — not a description or the image itself (no vision on the quoted target). Intentional for v1.
  • VK author label is coarse ("a participant", no name), and VK JSON shapes are assumed from the API (validated by struct + crafted-JSON tests, not a live Long Poll payload). If VK nests differently, parsing degrades to a safe no-op (the feature simply would not fire).
  • Prompt-injection residue. The quoted block is framed as data, but the model still sees text authored by another (allow-listed) user; the framing mitigates, it does not eliminate.
  • The PR-time multi-arch image builds (Telegram/VK Dockerfiles) run fresh in CI; no Dockerfile was touched here.

Introduces a transport-agnostic helper that folds a replied-to / quoted message into the run prompt so the model sees the original the user is referring to, not just their new text. The quoted text is framed explicitly as reference data (not instructions) and placed before the user's message; a blank quote is a strict no-op and an over-long quote is truncated rune-safely.
Extracts the quoted original (the exact highlighted Quote when present, else the replied-to message's text or caption, else a generic media placeholder) and folds it into every run path — text, voice and media — via chat.QuotedPrompt, labeling the bot's own quoted message as the assistant. A non-reply message is submitted unchanged. Also removes a dead maxUpload parameter from the media test harness that the new test surfaced.
Parses reply_message and fwd_messages on the VK message object and folds the quoted original (the replied-to message when present, else the first forwarded message, else a generic media placeholder) into every run path via chat.QuotedPrompt, with a coarse assistant-vs-participant author label derived from from_id. A non-reply message is submitted unchanged.
Addresses pre-PR review of the quoted-message folding:

- telegram: label only the bot's OWN message (matched by id, the same check as
  replyToBot) as the assistant; a third-party bot is attributed by name rather
  than mislabeled as the assistant.
- both adapters: use the generic [media] reference for ANY reply that has no
  usable text, so a reply to a poll/location (not just an enumerated media type)
  keeps its anchor; drops the now-redundant replyHasMedia enumeration.
- chat: rune-count before allocating on the truncation path (no []rune slice for
  the common short quote).
- tests: cover the msg.Quote highest-preference branch, a media-only reply and a
  media-only forward, and the third-party-bot author label.
@zaytcevcom
zaytcevcom merged commit fbe2e79 into main Jul 24, 2026
4 checks passed
@zaytcevcom
zaytcevcom deleted the duck/263043/quoted-message-context branch July 24, 2026 11:40
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.

1 participant