feat: fold quoted/replied-to message into the run prompt (Telegram + VK) - #62
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
core/chat/media.go): newQuotedPrompt(quotedAuthor, quotedText, userText), mirroring the existingDocumentPrompt/PhotoPromptpattern. It places the quoted block first (framed as reference data), the user's message last; adds an author clause only when known; returnsuserTextunchanged when the quote is blank (the no-op that keeps non-reply traffic identical); and truncates an over-long quote rune-safely atmaxQuotedRunes = 2000while never touchinguserText.cmd/flock-telegram/main.go):quotedContextextracts 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 viaQuotedPromptinto 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.adapters/vk/longpoll.go,adapters/vk/receiver.go): the message object now parsesreply_messageandfwd_messages;quotedContextfolds the replied-to message (preferred) or the first forwarded message — one level only — via the sameQuotedPrompt, with a coarse assistant-vs-participant author label derived fromfrom_id's sign (no extrausers.getcall).[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-toolsimage (Go 1.26.5 / golangci-lint 2.11.4) — exactly what CI runs:task tests—go 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 lint— 0 issues.task build— green.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:
from.IsBot), which would attribute a third-party bot's words to ourselves. Narrowed to the bot's own id (the same check asreplyToBot); added a test for the third-party-bot case.msg.Quotebranch (Telegram's native quote), previously unproven.replyHasMediaenumeration.Residual risks / worth a human eye:
-racesuite, 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.[media]marker — not a description or the image itself (no vision on the quoted target). Intentional for v1.