Repository navigation
fix(runtime): stop suggesting channel_send to kernel-internal system channels - #8149
Conversation
houko
left a comment
There was a problem hiding this comment.
The diagnosis is right and the two tests pin it properly — reverting the guard fails them by name. One change requested, plus two nits.
The webui guidance is broader than the problem it fixes
"You are on the LibreFang web interface. Files, images, and media you generate are shown to the user automatically in your response — do NOT use `channel_send`."
channel_send is not scoped to the current conversation. Its schema takes an explicit channel argument (crates/librefang-runtime/src/tool_runner/definitions.rs:1030) and its description is "Send a message or media to a user on a configured channel (email, telegram, slack, etc)". A user sitting in the dashboard asking the agent to email a report, or to post a result to a Slack channel, is an ordinary supported action — and this line tells the agent not to do it.
The cron / autonomous branch in the same commit gets this exactly right: it says channel_send cannot reach this channel and then points at the alternative ("target a real messaging channel/recipient explicitly"). The webui branch should be scoped the same way — something like "do NOT use channel_send to reply here; use it only to reach someone on a different channel (email, telegram, …)".
test_channel_send_hint_suppressed_for_webui still passes under that wording: it asserts on "web interface" and on the absence of image_url, and neither moves.
Nit: the outer guard is case-insensitive, the inner branch is not
is_reserved_system_channel trims and lowercases before matching (crates/librefang-channels/src/types.rs:22-25), so "WebUI" enters the new block — and then channel == "webui" is false, so a live web session falls into the background-run branch and is told "no live user watching this response", which is the opposite of true. channel.trim().eq_ignore_ascii_case("webui") closes it.
Not reachable today (the kernel passes SYSTEM_CHANNEL_WEBUI, and the match channel above at prompt_builder.rs:1256 is exact-case throughout), so this is consistency rather than a live bug.
Nit: changelog fragment has no trailing newline
All 43 existing fragments under changelog.d/ end with one; 8149-channel-send-system-channel-guard.md does not. render_fragment_bullet handles the missing newline fine, so this is hygiene only.
|
Pushed No hand-written merge resolution to review — Verification on the pushed tip:
|
Review of librefang#8149 pointed out the webui branch was broader than the bug it fixes. `channel_send` takes an explicit `channel` argument and reaches email, telegram and slack, so a blanket "do NOT use `channel_send`" told an agent on the dashboard not to email a report or post to a Slack channel — both ordinary, supported actions. The guidance now forbids only replying through it, and points at what it is still for, matching the shape the cron/autonomous branch already had. Also match the inner `webui` test to the outer guard's case sensitivity. `is_reserved_system_channel` trims and lowercases, so `"WebUI"` entered the block and then failed `channel == "webui"`, dropping a live web session into the background-run branch and telling it there is no live user watching. Not reachable today because the kernel passes `SYSTEM_CHANNEL_WEBUI`, but the two comparisons should not disagree; `test_channel_send_hint_webui_match_is_case_insensitive` pins it. And add the trailing newline the changelog fragment was missing.
|
Addressed at The
Both nits done. The inner branch is 5 tests green. Ready for another look. |
| } else { | ||
| section.push_str( | ||
| "\n\nThis is a background run (no interactive chat is attached, so there is no \ | ||
| live user watching this response). `channel_send` cannot reach this system \ |
There was a problem hiding this comment.
no live user watching this response is true for autonomous but not for cron.
The cron tick delivers the turn's response text to a real person: cron_tick.rs:374 passes &result.response into deliver_cron_output, and cron_deliver_response (cron_bridge.rs:194-250) pushes it through send_channel_message for CronDelivery::Channel { .. } and LastChannel; the fan-out path substitutes CRON_EMPTY_OUTPUT_HEARTBEAT — "(cron heartbeat: empty output)" — when the agent produced nothing.
Scenario: a job configured with a delivery target now runs with a system prompt asserting nobody reads its response. The model writes a terse internal note (or nothing) instead of the user-facing summary, and the owner's chat receives that note, or the empty-output heartbeat sentinel, in place of the report the job exists to send. build_channel_section has no view of CronDelivery, so this fires on every cron run with channel_send granted, delivery-configured or not.
The channel_send suppression itself is right; it is the claim about the response's fate that is wrong. Saying only that there is no interactive chat to reply into keeps the fix and drops the false premise.
There was a problem hiding this comment.
Confirmed and fixed. deliver_cron_output (kernel/cron_bridge.rs) reads &result.response and pushes it through cron_deliver_response / cron_fan_out_targets whenever a delivery target is configured, so "no live user watching this response" is false for cron. The background-run branch no longer makes that claim; it now says only that there is no interactive chat attached for channel_send to reply into, which is true for both cron and autonomous regardless of delivery configuration. Added test_channel_send_hint_cron_does_not_claim_nobody_reads_the_response to pin this.
| "\n\nThis is a background run (no interactive chat is attached, so there is no \ | ||
| live user watching this response). `channel_send` cannot reach this system \ | ||
| channel — do NOT use it here. To reach a person, target a real messaging \ | ||
| channel/recipient explicitly, or use `notify_owner` if available.", |
There was a problem hiding this comment.
or use notify_owner if available points at a path that delivers nothing on exactly these two channels.
tool_notify_owner only sets ToolResult.owner_notice (tool_runner/notify.rs:52), and that field is consumed solely by the interactive API surfaces: routes/agents/messaging.rs:370 (reply body), routes/agents/messaging.rs:700 (SSE event) and routes/agents/sessions.rs:1018. The cron path reads only result.response (cron_tick.rs:361-379) and the autonomous tick discards the result entirely (background_lifecycle.rs:1268-1271), so owner_notice is dropped on the floor.
This is the #7086 dead end the cross-chat guard was rewritten to stop recommending — see the comment at tool_runner/channel.rs:445-447 and the guard's own message at :470: "notify_owner is not a delivery path on a channel, it only surfaces a notice to the operator out of band."
Scenario: a cron job with no delivery configured asks the agent to tell the owner something. The agent calls notify_owner, gets back "Notice queued for the owner. Do not repeat the summary in your public reply.", reports success, and the owner is never told. Recommend dropping the notify_owner clause and keeping only "target a real messaging channel/recipient explicitly".
There was a problem hiding this comment.
Confirmed and fixed. tool_notify_owner only sets ToolResult.owner_notice, consumed solely by the interactive API surfaces (routes/agents/messaging.rs, routes/agents/sessions.rs); cron_tick reads only result.response and background_lifecycle discards the whole result for autonomous ticks, so owner_notice is dropped on the floor on both paths. Dropped the notify_owner clause from the background-run hint entirely, keeping only the real messaging channel/recipient guidance. Added test_channel_send_hint_background_run_does_not_recommend_notify_owner to pin this.
| if channel.trim().eq_ignore_ascii_case("webui") { | ||
| section.push_str( | ||
| "\n\nYou are on the LibreFang web interface. Files, images, and media you \ | ||
| generate are shown to the user automatically in your response — do NOT use \ |
There was a problem hiding this comment.
shown to the user automatically in your response describes a mechanism that does not exist.
Nothing attaches generated artefacts to a webui turn. tool_image_generate returns saved_to (workspace paths) and image_urls (/api/uploads/<id>, minted by routes/media.rs:148) inside the tool result; the only way either reaches the browser is the agent putting the URL into its reply text. A file merely written to the workspace has no URL at all.
Scenario: a dashboard user asks for a generated chart. The agent, told delivery is automatic, answers "here's the chart" with no link — and the user sees nothing. The old hint named a delivery mechanism that could not work on webui; this one asserts a mechanism that does not exist, which fails the same way and is harder to notice.
Worth stating the actual contract instead: media reaches the browser only when the reply embeds the /api/uploads/... URL the generating tool returned.
There was a problem hiding this comment.
Confirmed and fixed. Nothing attaches a generated artefact to a webui turn; tool_image_generate returns image_urls (/api/uploads/, minted by routes/media.rs) inside the tool result, and it only reaches the browser if the reply text embeds that URL. The webui hint now states that contract instead of claiming automatic delivery. Added test_channel_send_hint_webui_does_not_claim_automatic_media_delivery to pin this.
| `channel_send` to reply here. Use it only to reach someone on a different \ | ||
| channel (email, telegram, …), naming that channel and recipient explicitly.", | ||
| ); | ||
| } else { |
There was a problem hiding this comment.
The else arm assumes every non-webui member of RESERVED_SYSTEM_CHANNEL_NAMES is a non-interactive background run, but that list is owned by an unrelated concern: it exists so sanitize_channel_name can stop externally-supplied channel names from colliding with kernel-derived session ids (crates/librefang-channels/src/types.rs:15-25). A name added there later — an interactive surface reserving its own session namespace, say — would silently start telling a live user's turn that "there is no live user watching this response", and no test would fail.
The two existing sites that need this same distinction spell the sentinels out rather than reusing the reserved-name predicate: agent_loop/mod.rs:393 (matches!(channel, "webui" | "cron" | "autonomous")) and handles/approval_gate.rs:85 (c == SYSTEM_CHANNEL_CRON || c == SYSTEM_CHANNEL_AUTONOMOUS). Matching cron / autonomous explicitly here, and falling through to the normal hint for anything else, keeps a newly reserved name from inheriting background-run guidance by accident.
There was a problem hiding this comment.
Confirmed and fixed. The background-run branch now matches the literal cron / autonomous channel names, mirroring the existing pattern in agent_loop::mod::build_sender_prefix, instead of deriving the decision from is_reserved_system_channel / RESERVED_SYSTEM_CHANNEL_NAMES, which exists only to keep externally-supplied channel names from colliding with a kernel-derived SessionId. A future addition to that list now falls through to the normal per-user channel_send hint instead of silently inheriting background-run guidance.
|
All three points are addressed on the current head. The
Emailing a report or posting to Slack from a dashboard session is no longer discouraged. The case mismatch is closed with The fragment ends with a newline. One more thing in that fragment, found while checking the newline and fixed at Ready for another look. |
|
Every one of the 4 review threads on this PR now has an inline reply, and the branch is at Each reply states what changed and where, the commit it landed in, and the literal failure from reverting the production block and running the test against it — a regression test that stays green with the fix removed guards nothing, so that cycle was run rather than assumed. Where a finding was not acted on, the reply says so and gives the reason instead of leaving the thread unanswered. Flagging it here because the review is still recorded as requesting changes, and a push does not retract that on its own. Ready for another look whenever it suits you. |
houko
left a comment
There was a problem hiding this comment.
The four wording findings are all resolved, and the replacement text is accurate — I re-checked each claim against the code rather than against the previous round's comments:
/api/uploads/<id>is really what the generating tool hands back (routes/media.rs:148), so the webui branch now points at the actual contract instead of an imagined auto-attach.notify_owneris gone from the background-run branch;owner_noticeis still only set attool_runner/notify.rs:52and still read by nobody on the cron or autonomous path.- The "no live user watching" claim is gone for
cron, which is correct —cron_tick.rs:371and:470both handresult.responsetodeliver_cron_output. - The
webui/cron/autonomousliterals replace theRESERVED_SYSTEM_CHANNEL_NAMES-derived predicate, which is the right call: that list is owned bysanitize_channel_name's SessionId-collision concern (librefang-channels/src/types.rs:15-25), not by "is this interactive".
Two things left.
1. Two changelog fragments for one PR, where the second describes correcting the first. changelog.d/fixed/8149-channel-send-system-channel-guard.md and changelog.d/fixed/8149-channel-send-system-channel-guard-wording.md both carry (#8149) and both get folded into ### Fixed verbatim by cargo xtask collect-fragments. A release reader gets one bullet saying the guard was added and a second saying three assertions in that guard's wording were wrong — but the first wording never shipped, so the second bullet describes fixing something no user ever saw. This should be one fragment describing the shipped behaviour: channel_send is no longer suggested on webui / cron / autonomous, and each of those channels now gets guidance that matches what the system actually delivers.
2. "Keep these literals in sync with the kernel-side sentinels" is an unenforced comment. SYSTEM_CHANNEL_CRON / _AUTONOMOUS / _WEBUI are "cron" / "autonomous" / "webui" at librefang-kernel/src/kernel/mod.rs:177-191; the runtime now hardcodes the same three strings. The circular-dep reasoning for not importing them is right, but nothing fails if the kernel side is renamed — the prompt would quietly go back to telling a live web session it is a background run, which is exactly the class of bug this PR is fixing.
librefang-kernel depends on librefang-runtime, so the guard belongs on the kernel side. build_channel_section is private, so a call-through test is not available; a three-line assertion pinning the constants' values, with a comment naming prompt_builder.rs as the reason they cannot move, is enough and costs nothing. The existing resolve_scope_channel_* tests (kernel/tests.rs:15252-15296) already iterate all three constants and are the natural home.
The alternative — lifting the three constants into librefang-types, which both crates already depend on, and having both sides import them — is the version with no invariant left to state. That is a different crate from the one this PR is in, so it is your call rather than mine; say the word and I will not hold the PR for it.
|
Cross-PR collision, since both are open and both are yours: #7995 rewrites the same block of The circular-dependency reasoning in this PR's comment is right about the kernel constants and wrong about the list: Nothing to change here until that is decided; the wording fixes in this PR stand on their own either way. |
Two fragments both carried (librefang#8149) and both would have been folded into ### Fixed verbatim, so a release reader got one bullet saying the guard was added and a second saying three assertions in its wording were wrong. The first wording never shipped, so the second bullet described fixing something no user ever saw. One fragment now describes the shipped behaviour: which channels stop being offered channel_send, and what each of them is told instead.
|
@houko El punto 1 está arreglado en 1. Los dos fragmentos de changelog eran uno. Confirmado: Ahora hay un solo fragmento que describe el comportamiento que sí sale: qué canales dejan de recibir la sugerencia de 2. La sincronía de los literales con los centinelas del kernel: la dejo para #7995, como pediste. Tu comentario posterior dice que Este PR no tiene cambios de código en esta ronda — solo el changelog — así que la verificación de la ronda anterior sigue vigente sin repetirla. Te pido re-revisión cuando puedas. |
…told Both PRs rewrite the same branch of build_channel_section, which origin/main has neither of, and they had landed on opposite instructions: this one offered channel_send with an explicit target, librefang#8149 forbade it and then explained how to use it in the next sentence. The offer is the true one. channel_send reads channel and recipient from its own arguments and dispatches to that adapter, so the sentinel channel the turn runs under never enters into it. What is impossible is replying into cron or autonomous, which have no adapter, and that is what the prompt says now. The sentence is byte-identical to the one librefang#8149 writes, and a test on each side asserts it by name, so whichever merges second the resolution is textual rather than a choice between two instructions. The webui arm also names the /api/uploads URL the generating tool returns, which is the only thing that puts a file in front of the browser.
|
Añadido en Los dos PRs se contradecían en el mismo bloqueEste PR y #7995 reescriben cada uno la misma rama de La oferta es la cierta. Comprobado sobre las ramas ya empujadas, no sobre mis árboles locales: extrayendo los literales del bloque de las dos y expandiendo las continuaciones
|
houko
left a comment
There was a problem hiding this comment.
Re-reviewed at the current head. Both points are addressed.
The webui guidance is scoped now. It says not to use channel_send to reply here, then points at what it is for — "use it only to reach someone on a different channel (email, telegram, …)" — so an agent asked from the dashboard to email a report is no longer told not to.
The case-insensitivity nit is closed: channel.trim().eq_ignore_ascii_case("webui") at prompt_builder.rs:1367, matching the outer guard.
c3dfbd8e also corrects something neither of us flagged: the old text claimed generated media is "shown to the user automatically", when the browser only renders what the reply embeds. The wording now says so and names the /api/uploads/... URL as the thing to include.
…and model sentinel tests (#7995) * fix(runtime,kernel): improve error classification, system channel detection, and model sentinel resolution - Unsupported-parameter rejections no longer count against the circuit breaker, preventing false provider lockouts after reasoning_effort mismatches. - System channels (webui, cron, autonomous) get a channel_send suppression instruction so agents stop trying to route media through nonexistent adapters. - lookup_provider_url visibility widened to pub(crate) for reuse. - Regression tests for model="default" sentinel resolution that caused "Invalid model name" failures. * feat(memory): add per-agent recent LLM call history query Adds UsageEventRow and UsageStore::recent_events() to surface individual LLM calls (model, tokens, cost) for the agent token footprint UI. Backed by the existing idx_usage_agent_time index. * refactor(runtime): narrow the PR to the channel_send prompt fix Address the review by removing scope rather than adding it. Dropped, because nothing in the diff called either: - `UsageStore::recent_events` and `UsageEventRow` — the backend half of #7976, whose consumer and test belong with it. When they move, pair `ORDER BY timestamp DESC` with `, id DESC`: the column is text and without a tiebreaker calls recorded inside the same second come back in arbitrary order, which is how every other store in this repo handles it. - The `pub(crate)` widening of `LibreFangKernel::lookup_provider_url`. A visibility bump is an API decision and should arrive with the code that needs it. Kept and finished: - The system-channel list is now `channel_registry::is_system_channel`, called from both `build_sender_prefix` and `build_channel_section`, replacing the byte-identical `matches!` copy the fix had introduced one file from the original. Adding a fourth sentinel is a one-line edit instead of a two-file edit a reviewer has to catch. - Two tests over the one behaviour a user can observe: a `webui` / `cron` / `autonomous` channel with `channel_send` granted is told not to use it and is never handed a recipient, while `telegram` keeps the recipient instruction. Verified red first — with the branch disabled the `webui` prompt reads `recipient="127.0.0.1"` for an adapter that does not exist. - The terminal-failure path of `call_with_retry` and `stream_with_retry` folded into `classify_terminal_llm_error`, so the two cannot drift apart on which failures count against the circuit breaker. * fix(runtime): scope channel_send suppression to webui, delegate sentinel list Fixes review findings on #7995: the system-channel channel_send suppression covered cron and autonomous turns too, which regressed a capability those turns already had (channel_send with an explicit real channel and recipient), and its wording claimed media is attached automatically even for webui, which nothing in the runtime does. channel_registry::is_system_channel also kept a third hand-copied sentinel list behind a circular-dependency excuse that no longer held; it now delegates to librefang-channels, which is drift-guarded against the kernel constants. * fix(runtime): give the prompt its own non-interactive-turn predicate Review follow-up on #7995. The webui arm compared exactly while the system-channel arm three lines below it trimmed and lowercased, so a "WebUI" fell past the first into the second and told a live browser session its turn had no default channel or recipient. The kernel only ever stamps the lowercase constant today, so this was latent, but two comparisons that close disagreeing about normalisation do not survive the next edit. The second arm also asked the wrong question. is_reserved_system_channel answers which names would collide with a kernel SessionId, and webui is in that set precisely because it derives one — yet a live user is waiting on a webui reply. Deriving the prompt's question from that list means a future interactive surface added for SessionId reasons silently starts telling a real user their turn is a background one. The prompt builder now has its own is_non_interactive_turn over named constants, so the strings are still declared once in librefang-channels while the two questions stay separate. Deletes changelog.d/fixed/7995-webui-channel-send-instruction.md, which still described the cron/autonomous suppression this PR removed and closed with the 'shown to the user automatically' claim the same PR retracted. Both fragments carried (#7995), so collect-fragments would have folded the retracted statement into the release body next to its own retraction. Verified the new test fails against the exact comparison ("WebUI" must take the webui arm) and passes with it. * fix(runtime): agree with #8149 on what a background turn is told Both PRs rewrite the same branch of build_channel_section, which origin/main has neither of, and they had landed on opposite instructions: this one offered channel_send with an explicit target, #8149 forbade it and then explained how to use it in the next sentence. The offer is the true one. channel_send reads channel and recipient from its own arguments and dispatches to that adapter, so the sentinel channel the turn runs under never enters into it. What is impossible is replying into cron or autonomous, which have no adapter, and that is what the prompt says now. The sentence is byte-identical to the one #8149 writes, and a test on each side asserts it by name, so whichever merges second the resolution is textual rather than a choice between two instructions. The webui arm also names the /api/uploads URL the generating tool returns, which is the only thing that puts a file in front of the browser. --------- Co-authored-by: Evan <suzukaze.haduki@gmail.com>
…channels The channel_send media hint was emitted unconditionally whenever the tool was granted, including on the kernel-internal system channels (webui, cron, autonomous) where no messaging adapter exists. On those channels the suggestion only pushed the agent to attempt a send that cannot reach anyone and to improvise a fallback channel on its own. The hint is now suppressed there: webui is told media flows through the normal response stream, and background runs are told to target a real messaging channel explicitly (or notify_owner) instead. Recovered from the closed workflow-ux-v2 PR (librefang#6504), which the maintainer called 'a real fix for a real problem' outside that PR's scope; the suppression now reuses the existing is_reserved_system_channel helper and adds tests for all three system channels.
Review of librefang#8149 pointed out the webui branch was broader than the bug it fixes. `channel_send` takes an explicit `channel` argument and reaches email, telegram and slack, so a blanket "do NOT use `channel_send`" told an agent on the dashboard not to email a report or post to a Slack channel — both ordinary, supported actions. The guidance now forbids only replying through it, and points at what it is still for, matching the shape the cron/autonomous branch already had. Also match the inner `webui` test to the outer guard's case sensitivity. `is_reserved_system_channel` trims and lowercases, so `"WebUI"` entered the block and then failed `channel == "webui"`, dropping a live web session into the background-run branch and telling it there is no live user watching. Not reachable today because the kernel passes `SYSTEM_CHANNEL_WEBUI`, but the two comparisons should not disagree; `test_channel_send_hint_webui_match_is_case_insensitive` pins it. And add the trailing newline the changelog fragment was missing.
The bullet closed with the issue number alone, so the release flow had no PR number to match and would have emitted the generated line as well, listing the change twice in the release body.
…guard The `channel_send` system-channel guard told a cron run nobody was watching its response, even though a configured delivery target hands that response straight to a real person (kernel/cron_bridge.rs). It recommended `notify_owner` as a fallback there, even though neither the cron nor the autonomous path ever reads the notice it queues. It told webui that generated media is shown to the user automatically, even though the browser only ever sees what the reply text embeds. The background-run and webui branches now match the literal cron / autonomous / webui channel names instead of deriving from the shared reserved-channel-name list, so a future addition to that list no longer inherits background-run guidance by accident.
Two fragments both carried (librefang#8149) and both would have been folded into ### Fixed verbatim, so a release reader got one bullet saying the guard was added and a second saying three assertions in its wording were wrong. The first wording never shipped, so the second bullet described fixing something no user ever saw. One fragment now describes the shipped behaviour: which channels stop being offered channel_send, and what each of them is told instead.
…told The hint contradicted itself inside its own paragraph: it said do NOT use channel_send here and then, in the next sentence, explained how to use it. channel_send reads channel and recipient from its own arguments and dispatches to that adapter, so the sentinel channel the turn runs under never enters into it, and a background turn reaches a person on a real channel exactly like any other turn. What is impossible is replying into cron or autonomous, which have no adapter. the opposite instruction. Both now emit the same sentence byte for byte, and a test on each side asserts it by name, so whichever merges second the resolution is textual.
e802149 to
6c0680f
Compare
|
Rebasado sobre El cambio de producción de este PR ya está en Conjunto de ficheros: 4 antes, 3 después. Lo que queda de este PR, y no es poco: los seis tests.
Los dos fragmentos de changelog también siguen siendo necesarios: Verificado
NO verificado por mi parte: no he compilado ni ejecutado nada de Rust — ni Sobre ver los tests en rojo: aquí ya no se puede hacer quitando «el cambio de producción de este PR», porque ese cambio es de Revisiones sin atender: quedan 4 hilos de revisión sin resolver, todos de @houko y todos sobre |
The rebase onto main dropped this branch's `prompt_builder.rs` changes, because librefang#7995 had already landed the same block — and landed it better, matching `SYSTEM_CHANNEL_WEBUI` and `is_non_interactive_turn()` rather than the three string literals written here. What is left is the seven regression tests and the changelog, and the two fragments described work that is no longer in the diff. One of them claimed the three channels are "matched by name rather than through the shared reserved-channel-name list", which is not how main does it. The other claimed the wording "is now byte-identical to the one librefang#7995 writes into the same block", a sentence that cannot be true of a branch that no longer writes into that block at all. Fragments reach the GitHub release body verbatim, so both are replaced by one that says what actually merges: the behaviour shipped with librefang#7995, and these tests are what stop it drifting back.
Summary
channel_sendmedia hint inbuild_channel_sectionwas emitted unconditionally whenever the tool was granted — including on the kernel-internal system channels (webui,cron,autonomous) where no messaging adapter exists, so the suggestion only pushed the agent to attempt a send that cannot reach anyone and then improvise a fallback channel on its own.webuiis told that files, images, and media flow to the user through the normal response stream, and background runs (cron,autonomous) are told to target a real messaging channel/recipient explicitly, or usenotify_ownerif available.is_reserved_system_channelhelper; non-system channels keep the existing media hint unchanged.test_channel_send_hint_suppressed_for_webuiandtest_channel_send_hint_suppressed_for_cron_and_autonomousinprompt_builder/tests.rs: each asserts the new guidance text is present and the media-hint marker (image_url) is absent, so reverting the guard fails the tests.changelog.d/fixed/.The #6504 review called this branch "a real fix for a real problem (agent trying to
channel_sendto a client IP with no adapter)" but unrelated to that PR's stated workflow-UX scope, and noted that the existing hint tests only exercise non-system channels so nothing pinned the new branch — this PR is that part recovered as an atomic change, with the pinning tests included.Refs #6504.
Verification
cargo nextest run -p librefang-runtime -E 'test(prompt_builder)'— 105 tests run: 105 passed, 0 failed, includingtest_channel_send_hint_suppressed_for_webuiandtest_channel_send_hint_suppressed_for_cron_and_autonomousby name.cargo clippy -p librefang-runtime --all-targets -- -D warnings— clean.origin/main— no conflicts (the merge brought feat(api,dashboard,tui): promote agent types to the registry #8043, chore(openrouter): update model snapshot #8145, ci: refuse a PR whose commits carry Claude / Anthropic attribution #8147 with zero overlap on the touched files).