feat(handlers): surface user-quoted portions from reply context - #157
Merged
Conversation
dylanneve1
enabled auto-merge (squash)
May 13, 2026 13:11
dylanneve1
force-pushed
the
feat/reply-with-quote
branch
from
May 13, 2026 13:19
0b4091d to
4a52707
Compare
β¦API 7.0) When a user replies to a message in Telegram, they can highlight a specific substring before sending β that selection arrives on the incoming message as `ctx.message.quote` (TextQuote, Bot API 7.0+). Talon was ignoring it; the model only saw the full replied-to text, never the part the user was actually pointing at. This passes `ctx.message.quote` into `getReplyContext` at both call sites (text + media handlers). When present, the prompt now includes an explicit second line: [Replying to Alice: "<full original message>" msg_id:42] [Quoted portion: "<the substring the user highlighted>"] Server-auto quotes (long-message snippets where `is_manual=false`) are included the same way β still useful signal. Quote text is truncated to 500 chars to match the full-text limit. Empty / whitespace-only quotes are dropped to keep the prompt clean. Inbound only β outbound `send.quote` is a separate change if wanted. - 6 new vitest cases under `getReplyContext β quoted portions (Bot API 7.0)` - typecheck clean, no new lint warnings - 1906 tests / 1 pre-existing flaky `package.functional` failure unrelated
dylanneve1
force-pushed
the
feat/reply-with-quote
branch
from
May 13, 2026 13:26
4a52707 to
1c645ac
Compare
dylanneve1
added a commit
that referenced
this pull request
May 19, 2026
β¦ SDK's native error pipeline (#159) When a turn-terminator tool (`end_turn`, strict `react`) failed to deliver (e.g. Telegram rejected `end_turn` for "Message too long", invalid chat_id, network blip), the PostToolBatch hook terminated the SDK loop anyway β the model saw the error in its tool result but had no turn left to react. End result: silent dropped turn, user sees nothing. Canonical incident (2026-05-13 13:11Z Pandario reply 226264): end-of-turn delivery of a 4326-char message hit Telegram's 4096 cap, bridge returned `{ok: false, error: "Message too long..."}`, hook fired regardless, turn silently ended. Supersedes #158 (content-sniffing approach was fragile β frontend-coupled, schema-drift-vulnerable, false-positive-prone on responses that happened to contain `"ok":false` substrings). This PR uses the SDK's NATIVE error pipeline instead of inspecting bodies. Implementation: 1. `end_turn.execute` and `react.execute` THROW when the bridge returns `{ok: false}` instead of returning the failure object silently. A new `throwIfFailed` helper wraps the bridge result and raises a typed `Error("<tool> delivery failed: <bridge error>")`. The "what counts as a failure" decision now lives in the tool implementation, where the contract is owned. 2. The SDK observes the throw and fires `PostToolUseFailure` with a typed `{tool_name, tool_input, tool_use_id, error, is_interrupt}` payload β no string sniffing, no `unknown` parsing. 3. New `PostToolUseFailure` hook records the failed `tool_use_id` in a per-session `Set<string>`. Ignores interrupts (`is_interrupt: true`) and non-terminator failures (e.g. `send`). 4. `PostToolBatch` hook now consults the Set β if the terminator's `tool_use_id` was flagged, it deletes the flag and returns `{continue: true}` to keep the SDK loop alive. Otherwise terminates as before (perf win from PR #122 preserved on the happy path). 5. The two hooks share state via closure β `buildTurnTerminatorHooks()` creates a fresh Set per `buildSdkOptions()` call, so concurrent chat sessions stay isolated. Frontend-agnostic by design: any frontend whose tools throw on delivery failure gets the same recovery behaviour. No bridge envelope shape is baked into the SDK options layer. Tests: - 9 new `PostToolUseFailure + PostToolBatch coordination` cases (terminator failure preserves loop, success terminates, interrupt ignored, non-terminator failure ignored, soft-react `end_turn:false` ignored, defensive non-failure events, flag-consumed-on-match, per-session isolation). - 8 new messaging-tools cases for `end_turn` / `react` throw behaviour (text path throws on {ok:false}, buttons path throws, generic message when error field missing, success path unchanged, react strict + soft both throw, react strips end_turn param). - All 33 existing PostToolBatch hook tests still pass. - 2001/2014 vitest pass β same pre-existing `package.functional` flake as PR #157 (irrelevant: running tests on a host where Talon daemon is already live). - typecheck clean, prettier clean, no new lint warnings. Co-authored-by: Dylan Neve <dylan.neve@intel.com>
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
Telegram (Bot API 7.0+, Dec 2023) lets users highlight a specific substring of a message before replying β that selection arrives on the incoming message as
ctx.message.quote(TextQuote). Talon was ignoring it: the model only ever saw the full replied-to text and had no way to know which part the user was actually pointing at.This wires
ctx.message.quoteintogetReplyContextat both call sites (text handler + media handler). When the user has highlighted a portion, the prompt now includes an explicit second line:Behaviour details
is_manual: true) β user explicitly selected text. Always shown.is_manual: false) β Telegram snipped a long message. Shown the same way; still useful signal about what the sender considered most relevant.Scope
Inbound only. Outbound
send.quote(so the bot can quote-reply to specific parts when replying) is a separate change if wanted β happy to follow up.Test plan
getReplyContext β quoted portions (Bot API 7.0):is_manual=false) appended same wayquote.textβ omits the second line[photo]and[Quoted portion: β¦]visiblenpm run typecheckcleannpm run lintβ 0 new warnings (11 pre-existing on unrelated lines)npm testβ 1893/1906 pass (1 pre-existingpackage.functionalflake from running tests on a host where Talon is already up; verified failing onmaintoo)π€ Generated with Claude Code