Repository navigation
bug: Telegram slash commands appear inert — SDK drift + command-path routing divergence #7140
Description
Activity
- addedarea/channelsMessaging channel adaptersMessaging channel adapters
on Aug 13, 2026 - changed the title
[-]bug: Telegram slash commands (/new, /agents, /reset...) return fake confirmations — no execution path[/-][+]bug: Telegram slash commands appear inert — SDK drift + command-path routing divergence[/+]on Aug 13, 2026 - added a commit that references this issue
on Aug 13, 2026 - addedhas-prA pull request has been linked to this issueA pull request has been linked to this issue
on Aug 13, 2026 Coverage audit before this closes, because
has-pris currently overstating it.What the two linked PRs actually cover
#7159 (
fix/telegram-commands-origin) touchescrates/librefang-api/src/channel_bridge.rs,crates/librefang-channels/src/bridge.rs,crates/librefang-kernel/src/kernel/messaging.rs,crates/librefang-kernel/src/kernel_api.rs— candidate 2 (resolve_for_commandmirroring the chat chain), candidate 3 (set_conversation_bindingnow has production callers, 9 hunks reference it, commit 18ef5fd), and the/thinkstub, which is still live onmainatchannel_bridge.rs:1921with an unused_agent_id.#7701 (
fix/new-reset-canonical-session) touchescrates/librefang-api/src/channel_bridge.rsonly — the session dimension (derived sid vs canonical session).Root cause 1 is untouched by both. Neither diff includes
crates/librefang-channels/src/sidecar.rs,crates/librefang-channels/src/embedded_sdk.rs,crates/librefang-api/src/routes/sidecar_describe.rs, or anything undersdk/. #7159's own body says so: "SDK version drift (deployment concern, not code)".It is not purely a deployment concern — there are four code-level gaps that are exactly why the drift was silent:
- The version field is never populated.
SidecarAdapter.protocol_versiondefaults toNonein both SDKs (sdk/python/librefang/sidecar/runtime.py:110,sdk/rust/librefang-sidecar/src/runtime.rs:139-141) and no first-party adapter overrides it —sdk/python/librefang/sidecar/adapters/telegram.py:830declarescapabilitiesonly.docs/architecture/sidecar-protocol.md:70-71states the current value is1. So thereadyframe from the affected deployment carriedprotocol_version: null. - No expected value exists to compare against.
crates/librefang-channels/src/sidecar.rs:1031-1036logsprotocol_versionat INFO on the "Sidecar adapter ready" line and does nothing with it;SidecarReadyParams.protocol_versionatsidecar.rs:256-259is commented "Reserved for skew diagnostics (logged, never enforced)". There is noSIDECAR_PROTOCOL_VERSIONconstant anywhere in the repo to diff it against. - The SDK package version never reaches the daemon on any path.
librefang.__version__exists (sdk/python/librefang/__init__.py:35, currently2026.8.19persdk/python/pyproject.toml:7) and is not on the wire.Schema.to_dict()(sdk/python/librefang/sidecar/protocol.py:524-530) emitsname,display_name,description,fieldsand nothing else; the daemon-sideSidecarSchema(crates/librefang-api/src/routes/sidecar_describe.rs:30-36) has no field to receive it. So thedescribe_sidecarhalf of the suggested fix has no counterpart on either side. - The mechanism that lets a stale install win, quietly.
pythonpath_with_embeddedskips the binary-embedded SDK wheneverhas_real_sdk_installed(command)is true (crates/librefang-channels/src/embedded_sdk.rs:369-375), and that probe ispython -c "import librefang.sidecar"— importability only, no version read (embedded_sdk.rs:302-315). A March pip install therefore shadows the August tree compiled into the daemon, and the only trace is adebug!. That is the reported 2026.3.2201-vs-2026.7.31 pairing, reproduced from the code path rather than the deployment.
A fix for 1 is a one-liner per adapter; 2 and 3 are the actual work, and 4 is what turns "operator has a stale SDK" from invisible into a WARN at spawn.
The blocker on #7159. It carries a standing CHANGES_REQUESTED (2026-08-22, tip 6e34904) whose substance is unresolved on the current tip: the
/thinkpreference is keyed by agent alone —KernelBridgeAdapter::thinking_pref_key(agent_id) -> agent_id.0.to_string()— while the ack reads "Extended thinking {state} for this chat." One agent serving several Telegram chats (or several channel instances) means/think onin one DM changes reasoning mode and cost for every other conversation routed to that agent, and/think offanywhere disables it for all of them. The pref needs the conversation identity in the key, threaded throughset_thinkingand both send paths, plus a regression with two conversations on one agent proving isolation. CI is otherwise green on that tip, so this is the only thing between #7159 and merge.Not blocking: the
/goalhalf of candidate 4 is unreachable onmain— there is no/goalcommand in the channel bridge, since #6505 was closed. It should not be counted against this issue.So: #7159 (after the scoping fix) + #7701 close candidates 2, 3, the
/thinkstub and the session dimension. Root cause 1 — the headline half of this issue's title — has no PR.- The version field is never populated.
- addedawaiting-responseMaintainer replied — waiting for reporter/contributor feedbackMaintainer replied — waiting for reporter/contributor feedback
on Aug 23, 2026 Closing this as part of a backlog sweep. Re-verified against
origin/main@ 45e9bf0. Two of the four root causes are closed and two are not, so this closes as not planned rather than completed.What landed
Root cause 1 (SDK drift) — closed, and my earlier triage on this thread was out of date the moment #7848 merged. I wrote here that "root cause 1 — the headline half of this issue's title — has no PR". It has one, and it merged: #7848 (
7bb78c9500707e0bf684e79738dc78169a240f7f), which closes all four of the code-level sub-gaps I enumerated:SIDECAR_PROTOCOL_VERSIONnow exists (crates/librefang-channels/src/sidecar.rs:271), withclassify_protocol_versionat:291turning a declared version intoMatch/Older/Unspecified, and thereadyhandler atsidecar.rs:1068emitting aWARNon skew and on an absent value instead of an unexaminedINFOline.- The SDKs now populate it:
sdk/python/librefang/sidecar/runtime.py:113defaultsprotocol_versiontoprotocol.PROTOCOL_VERSION, and the Rust SDK does the same atsdk/rust/librefang-sidecar/src/runtime.rs:142. - The SDK package version reaches the daemon:
sdk/python/librefang/sidecar/protocol.py:551emitssdk_versionunconditionally, andcrates/librefang-api/src/routes/sidecar_describe.rs:40has a field to receive it. - The stale-install path is no longer silent:
installed_sdk_version(crates/librefang-channels/src/embedded_sdk.rs:334) reads the installed version rather than probing importability alone, and:413compares it againstembedded_sdk_version()and warns when they differ. That is exactly the 2026.3.2201-vs-2026.7.31 pairing you reported, now visible at spawn.
Your recommendation in the issue body — that the daemon should warn when a sidecar reports an older protocol/SDK version, and that
describe_sidecarshould surface the adapter version — is what shipped, in both halves.Root cause 4,
/thinkhalf — closed. #7854 (f5d2a5bd40ed7f9eab9136d3fe964bcc798e7c71)./thinkis no longer a stub that stores a preference and promises future support: it is keyed by conversation rather than by agent, and it is read on the send path. Write sidecrates/librefang-channels/src/bridge.rs:7094; key derivation and read sidecrates/librefang-api/src/channel_bridge.rs:646withthinking_override_forat:647. The isolation regression proving one chat's toggle does not rewrite another chat's turns is atchannel_bridge.rs:3425, and the command-path regression is atbridge.rs:8329.model_rejects_thinking(channel_bridge.rs:654) also stops the ack from promising extended thinking on a model whose catalog entry says it has none.Root cause 4,
/goalhalf — not applicable. There is no/goalcommand in the channel bridge onmain; grepping for it inbridge.rsandchannel_bridge.rsreturns nothing. It should not be counted against this issue.What remains
Root cause 2 (command-path routing divergence) — open, exactly as you described it.
resolve_for_command(crates/librefang-channels/src/bridge.rs:6872) still resolves throughrouter.resolve_with_contextand nothing else. The regular message path,resolve_or_fallback(bridge.rs:3531), additionally consults the thread-route agent (:3541), the #5671 conversation binding and the channel-instance binding (:3576), and the #5323 sticky holder and explicit @-addressing (:3584). When those disagree,/newatbridge.rs:6973still resets the router's agent and replies as if it reset the one the user is talking to. Your ranking of this as a real code-level defect holds.Root cause 3 (
/agentdoes not stick) — open.set_conversation_binding(crates/librefang-memory/src/channel_binding_store.rs:123) still has zero production callers; every reference onmainis a test (channel_binding_store.rs:275,:299,:319,:322, andcrates/librefang-api/src/channel_bridge.rs:3572, which sits inside the#[cfg(test)]block starting at:3147). The/agentarm atbridge.rs:6940-6947writes onlyrouter.set_user_default_for_channel/set_user_default. The write-dead store you identified is still write-dead.Where the work lives
PR #7159 — open, but not in a landable state:
CHANGES_REQUESTED,mergeable: CONFLICTING/DIRTY, last pushed 2026-08-22 (6e349043689f057f873a70fc6b74a5f2c961dd12), and currently red on Quality, OpenAPI Drift, Test / Unit (lib+bin), Test / Ubuntu shard 1/4, Build / Linux aarch64 and Kernel Ignored / Restart Override. Its/thinkhalf has since been superseded by #7854, so what is left in it is the root cause 2 and 3 work, on a base several days stale.PR #7701 — open, single file
crates/librefang-api/src/channel_bridge.rs,MERGEABLEbutBLOCKED, red on Quality, Test / Unit (lib+bin), Test / Ubuntu shard 1/4 and Build / Linux aarch64. It covers the session dimension (/new,/rebootand/compactresetting the canonical session), not the routing divergence.Neither PR is closed by this sweep; closing the issue does not touch them.
Closing note
This is a backlog sweep, not a decision that root causes 2 and 3 are unwanted — they are confirmed bugs and I have said so twice on this thread. This was a good report, and the correction you supplied in it, that the dispatcher is fully implemented contrary to the initial framing, is what made the rest of it tractable. Reopening this, or filing a narrower issue for the
resolve_for_commanddivergence alone now that root causes 1 and 4 are out of the way, is welcome.Reopened — closing this as
not plannedwas a mistake on my part.not plannedreads as "we do not intend to do this", which is not true of this issue.
The work is either in flight in an open pull request or tracked as real remaining work, and the preceding comment says which.That comment's content stands: what landed, what remains, and where the work lives are all accurate.
Only the closure was wrong, and the label it carried misrepresented the state to anyone reading the tracker.Issues covered by an open PR will close on their own when that PR merges, with the correct reason.
- removedhas-prA pull request has been linked to this issueA pull request has been linked to this issue
on Aug 26, 2026 Not on
main, so not closing — but the symptom decomposes into two separate mechanisms, and each now has somewhere to live. Posting because this has sat underawaiting-responsesince 2026-08-13 and both halves have been diagnosed since, elsewhere./newreturning a confirmation while the conversation is unchanged — #7701.That is exactly the mechanism, and this issue's own correction ("the command dispatcher is fully implemented … the chain is real end-to-end") is why it was hard to see: the dispatcher runs, the kernel call happens, the ack is honest — it just resets a different session from the one the user is looking at. #7701's changelog puts it as "a reset used to ack success against a session holding no messages while the conversation the user was looking at kept its full history", with a live case where the derived session held 0 messages and the canonical one held 198.
So neither of this issue's ranked root-cause candidates is needed to explain the
/newhalf: it is not SDK drift and not a routing divergence in the dispatcher, it is whichSessionIdthe reset targets.Worth reading my review on #7701 before treating it as done — as written it resets both sessions, which takes the WebUI conversation with it while the ack still says "Other surfaces untouched".
/agentsinline keyboard not switching agent — #8118.Different mechanism, separately reported, with #8117 open against it. My review there found #8117's stated premise does not hold (
dispatch_messagealready routes slash-prefixed button actions atbridge.rs:4538), and suggested the likely real cause:content_to_text'sButtonCallbackarm atbridge.rs:1056renders[Button: /agent X]with no slash passthrough, and it feeds the debounce/coalesce path — so a button pressed inside a debounce window is dispatched as prose and never reaches the command dispatcher.Suggestion: close this one in favour of #7701 and #8118 once either lands, or re-scope it to whichever half turns out not to be covered. The SDK-drift candidate ranked #1 here is worth keeping on record either way — it would produce the same surface symptom for a different reason, and nothing in the two PRs above addresses a stale pip-installed
librefangSDK.Verified against
mainat07cb6fc76.- addedhas-prA pull request has been linked to this issueA pull request has been linked to this issue
on Sep 23, 2026 - added a commit that references this issue
on Sep 23, 2026 - removedhas-prA pull request has been linked to this issueA pull request has been linked to this issueawaiting-responseMaintainer replied — waiting for reporter/contributor feedbackMaintainer replied — waiting for reporter/contributor feedback
on Sep 23, 2026 - added a commit that references this issue
on Sep 23, 2026 - added a commit that references this issue
on Oct 5, 2026
Symptom
On Telegram,
/new,/agents, and related commands return a confirmation phrase but the visible conversation does not change.What was verified (correction of the initial report)
The command dispatcher is fully implemented in
crates/librefang-channels/src/bridge.rs:6828(handle_command), with real kernel calls:"new"→reset_channel_session→kernel.reset_session,"agents"→list_agents(inline keyboard),"agent"→ find/spawn + store user default, plusstart/help/status/model/usage/.... The sidecar parsesbot_command→Content.commandand the daemon deserializes it intoChannelContent::Command(sidecar.rs:1149). The chain is real end-to-end.Root cause candidates (ranked)
1. Sidecar SDK drift (deployment issue, most likely trigger)
The Telegram sidecar runs a pip-installed
librefangSDK. A live deployment showed SDK 2026.3.2201 (March) against a daemon of 2026.7.31 (August). The sidecar protocol has evolved in between (capabilities negotiation, sender-identity fields, content handling). A 4-month-old adapter paired with a current daemon can produce exactly this symptom class: messages that arrive but are misparsed or silently degraded.Recommendation: the daemon should log a WARN when a sidecar reports a protocol/sdk version older than the daemon expects (today it does not), and
describe_sidecarshould surface the adapter version.2. Command-path routing divergence (code-level, real)
/newresolves the target agent viaresolve_for_command(bridge.rs:6845) which consults ONLY the router chain (binding → direct route → per-account user default → channel default → system default). The regular message pathresolve_or_fallback(bridge.rs:3511) additionally consults: addressed agent, #5671 conversation override, #5323 sticky holder, per-peer binding, and thechannel_bindingsSQLite instance default.When those differ (sticky holder points at agent B, router resolves to agent A),
/newresets agent A and replies "Session reset for this telegram chat" — a real reset on the wrong agent, invisible to the user.3.
/agent <name>selection does not stick/agentwrites only the router store (bridge.rs:6913).set_conversation_binding(channel_binding_store.rs:123) has no production caller — the #5671 conversation-binding store is write-dead. A selection made via/agentcan therefore be overridden by older, higher-precedence bindings on the next message.4. Stubs with confirmation phrasing (by design, but misleading)
/think(channel_bridge.rs:1974): explicit stub — stores a preference, returns "This will take effect when supported by the model."/goal(channel_bridge.rs:1339):start_goal_runis fire-and-forget; if the kernel self-handle is unset the run silently never starts (goal_lifecycle.rs:43) while the reply says "Goal created and started".Suggested fixes
describe_sidecarshows the version. Ship/install the matching SDK next to the daemon binary.resolve_for_commandshould use the same chain asresolve_or_fallback(sticky holders + conversation overrides first, router fallback second)./agenttoset_conversation_bindingso the selection binds the active conversation, or document which store is authoritative./thinkand/goalhonest: report "not yet supported" for /think until implemented; await/verifystart_goal_runbefore confirming.