feat(pipeline)!: require explicit prefixes for built-in LLM access - #60
Conversation
Admit a new built-in LLM turn only from command match, LLM prefix, continuation, explicit reply_to_bot, or extras explicit_surface. Mentions stay message-chain markers and no longer extra-admit. BREAKING CHANGE: default llm_access.private is prefix; group mention and prefix_or_mention fall back to prefix; extras adapter_preconfigured is replaced by explicit_surface; empty @ no longer request_llm Fixes #58 AI-Generated: true Generated-At: 2026-09-06T11:26:19Z
BegoniaHe
left a comment
There was a problem hiding this comment.
AI-assisted review
Verdict: request changes (posted as Comment because GitHub rejects REQUEST_CHANGES on the authoring account).
Card A of #58 is mostly implemented on route_turn / _llm_gate as specified: default private=prefix, mention is no longer an LLM gate, Discord user/role mentions no longer stamp extras, WebChat uses explicit_surface without injecting /, plugin EventMessageType.ALL still activates, and zh/en docs plus AGENTS.md match. Commit footer, Agent note, and the listed pytest set are in good shape. Most CI jobs are green; coverage was still pending at review time.
This cannot land until the prefix-only wait follow-up is actually admitted. The old waiter re-inserted Mention(self) so the next message would pass mention policy. That injection is gone, and the resubmitted hello has no prefix, no explicit_surface, and no inbound-coalesce window (has_open_window is TurnWindowManager only, default off). Under the new default it is dropped. The new test only asserts that Mention is not synthesized.
Blocking
- Prefix-only wait follow-up is dropped under default
prefix.handle_empty_mentionstill waits on a lone/andsubmit()s the next event through the bounded queue.WakingCheckStagethen runsroute_turnon plainhello. That is not an LLM admit source. Either stampexplicit_surface(or an equivalent one-shot continuation) on the resubmitted event, or delete the wait path. Add a test that the follow-up passesroute_turn/WakingCheckStageunder default config.
Should fix
-
explicit_surfaceskips_llm_gate, includingoff. WebChat, WeCom AI Bot, Discord slash, cron, andPlatformManager.create_event(is_wake=True)still run the built-in LLM when the operator setprivate=off/group=off. Ifoffis the kill switch, honor it after command matching. If synthesized surfaces are exempt, document that and addoff+explicit_surfacetests. -
Dashboard copy and two config keys are now lies.
empty_mention_waitingis unread.ignore_at_allhas no runtime reader afterMentionAllextra-admit was removed.CONFIG_METADATA_*hints anddashboard/src/i18n/locales/{zh-CN,en-US}/features/config-metadata.jsonstill describe “只 @ 机器人是否触发等待” / “Trigger Waiting on Mention-only Messages” and “忽略 @ 全体成员”. FAQ still listsignore_at_allas a reason the bot might not reply. Gate the remaining wait on a live flag, or hide/remove the dead keys and update i18n/hints/FAQ together.
Nits
WakeReason.MENTION_BOT/MENTION_ALL/PRIVATE_DEFAULT/PREFIXare never produced. Drop them or maproute_turnreasons onto this enum so_detect_wakedoes not silently skip unknown strings.- Private
_llm_gatestill checksblocked_by_other_mention, which_first_mention_is_othernever sets for DMs. test_detect_wake_behavior_matrixstill parametrizesgroup=mentionandignore_at_all; those rows are tautological now and hide the missing follow-up coverage.
Process
#58 is a breaking feature. GOVERNANCE.md wants maintainer agreement before the PR. The Issue has no second-maintainer GO. This review is the AI-assisted review; it is not a human maintainer approval and must not be merged on that basis.
Residual risk already named in the Agent note is real: on-disk private=open stays open; saved group=mention silently becomes prefix with no save-time warning.
| yield event.plain_result("想要问什么呢?😄") | ||
|
|
||
| async def empty_mention_waiter( | ||
| async def prefix_only_waiter( |
There was a problem hiding this comment.
This waiter still submit()s the next inbound event through the bounded queue. The follow-up is a plain hello with no LLM prefix, no explicit_surface, and no coalesce window. Old code inserted Mention(self) so mention policy would admit it. After this PR, default prefix drops that follow-up, so the 60s wait cannot collect the actual prompt.
Stamp a one-shot admit on new_event (or stop waiting). Cover it with route_turn / WakingCheckStage, not only “no Mention synthesized”.
| ) | ||
|
|
||
| if inp.adapter_preconfigured: | ||
| if inp.explicit_surface: |
There was a problem hiding this comment.
explicit_surface returns should_run_llm=True without _llm_gate. Command matching still wins, which is correct for WebChat /help, but private=off / group=off cannot stop WebChat, WeCom AI Bot, Discord slash, or cron.
Honor off here unless synthesized surfaces are documented as exempt; add that matrix test either way.
Store the resolved schema defaults as default_config so save-time unknown-key stripping does not drop plugin fields against DEFAULT_CONFIG. AI-Generated: true Generated-At: 2026-09-06T12:08:08Z
Stamp explicit_surface on the prefix-only waiter follow-up, restore empty_mention_waiting as the wait gate, and keep private/group off as the kill switch for synthesized surfaces. AI-Generated: true Generated-At: 2026-09-06T12:08:16Z
BegoniaHe
left a comment
There was a problem hiding this comment.
AI-assisted review
Verdict: request changes (posted as Comment because GitHub rejects REQUEST_CHANGES on the authoring account).
This is an independent review of dc3178d02, not the authoring write-up. The first-commit review's two blocking items are addressed in route_turn / _llm_gate and the prefix-only waiter stamp. Card A of #58 is otherwise in good shape: default private=prefix, mention is not an LLM gate, Discord user/role mentions no longer stamp extras, WebChat uses explicit_surface without injecting /, plugin EventMessageType.ALL still activates, zh/en docs plus AGENTS.md match, and CI on this SHA is green.
This still cannot land while off is documented as the kill switch but the prefix-only plugin path starts a new LLM turn anyway.
Blocking
- Prefix-only wait still calls
request_llmwhenllm_accessisoff.handle_empty_mentionis anEventMessageType.ALLhandler. It does not readllm_access. A lone/underprivate=off/group=offstill yieldsevent.request_llm(...)(courtesy prompt) and still waits 60s.ProcessStageruns that pluginProviderRequesteven whenshould_run_llmis false. The follow-up stamp is then correctly dropped byroute_turn, so the operator sees one LLM reply after they turned the model off. Docs (docs/zh/use/group-wake.md,docs/en/use/group-wake.md) sayoffblocks a new turn and that the wait path honors it. Gate the wait and the courtesyrequest_llmon the same mode check, and add a test that/+offdoes not callrequest_llmorwait_for.
Should fix
-
Lone
/is also anllm_prefixhit.catalog.resolve("")isUNKNOWN_ROOT, soroute_turn("/")falls through to_llm_gateand returnsshould_run_llm=Truewith emptymessage_str. Confirmed locally against this SHA. Defaultempty_mention_waiting_need_reply=truehides it because the pluginrequest_llmwins andProcessStageskips the built-in agent. If waiting is off,/still goes to the model as an empty prompt. Prefix-only should be owned by the waiter, not by_llm_gate. Addroute_turn("/")coverage. -
Unrelated config commit is in this PR.
d819e796c(fix(config): persist plugin schema keys on dashboard save) is a real bugfix and is tested, but it is not Card A, not in the PR body, and not required for the LLM-access change. Split it or say why it has to land here.
Nits
- Waiter follow-up is covered in pieces (plugin
set_extra,route_turnexplicit_surface,process()on a Mention-only chain). AWakingCheckStage.process()case for[Plain("hello")]+explicit_surfaceunder defaultprefixwould match the actual resubmit. _detect_wakestill duplicatesprocess()routing and is test-only.ignore_at_allis nowinvisibleand documented as a persisted no-op. Acceptable residual; delete it in a later cleanup rather than keep a Dashboard lie.
Process
#58 is a breaking feature. GOVERNANCE.md wants maintainer agreement before the PR. The Issue still has no second-maintainer GO. This review is the AI-assisted review; it is not a human maintainer approval and must not be merged on that basis.
Residual risk already named in the Agent note is still real: on-disk private=open stays open; saved group=mention silently becomes prefix with no save-time warning.
Prior inline threads on the waiter stamp and explicit_surface/off in route_turn are addressed on this SHA.
| @@ -78,7 +74,7 @@ async def handle_empty_mention(self, event: AstrMessageEvent): | |||
|
|
|||
| yield event.request_llm( | |||
There was a problem hiding this comment.
request_llm here is a plugin ProviderRequest. ProcessStage runs it whenever this handler is activated, independent of should_run_llm.
cfg is already loaded. If llm_access.private / group is off for this UMO, return before the courtesy prompt and before wait_for. Docs now call off the kill switch for this wait path; the follow-up stamp honors it, this yield does not.
Cover / + off so request_llm and wait_for are not called.
| False, | ||
| ) | ||
|
|
||
| llm_ok, reasons = _llm_gate(inp, blocked_by_other_mention) |
There was a problem hiding this comment.
resolve("") is UNKNOWN_ROOT, so a lone / is not stopped as a command and _llm_gate treats it as llm_prefix with empty message_str.
Default waiting + need_reply masks this because the plugin request_llm wins. If empty_mention_waiting is false, / still starts a built-in turn with an empty prompt. Prefix-only should not admit here; the waiter owns that UX.
Add route_turn("/") under default prefix.
Do not admit a bare LLM prefix as an empty prompt. Skip the prefix-only wait and courtesy request_llm when private or group access is off. AI-Generated: true Generated-At: 2026-09-06T12:27:19Z
Summary
Built-in LLM new turns now require an explicit aim: command match, LLM prefix, continuation,
reply_to_bot, or extrasexplicit_surface. Mentions stay on the message chain and are not an LLM gate. Dashboard WebChat still chats without a prefix because it stampsexplicit_surface; it does not inject/intomessage_str.Related issue
Fixes #58
Behavior
llm_access.privateisprefix.openremains a legal opt-in.llm_access.groupis onlyopen/prefix/off. Savedmention/prefix_or_mentionfall back toprefixat runtime; no config-file migrator.Mention/MentionAlldo not extra-admit the built-in LLM.@otherstill blocks prefix/command.reply_to_botremains an explicit OR, including whengroup=off.explicit_surface(Discord slash / interaction follow-up, WebChat, WeCom AI Bot, cron,PlatformManager.create_event). Discord user/role mentions do not stamp it.@botno longerrequest_llmand no longer reinsertsMention(self). A lone command prefix (/) may still wait for the next message.EventMessageType.ALL/PRIVATE_MESSAGElisteners still activate when the LLM is dropped.Non-goals
platform_settings.group_wake_policyor implicit mention/reply wakeupsWakingCheckStage/into WebChatmessage_strllm_access.groupmention/prefix_or_mentionvaluesImplementation notes
Admit sources stay on
route_turn/_llm_gate. Mentions are not read for LLM admission. Stamp sites share one extras bool; there is no second LLM gate. Runtime code and tests have zeroadapter_preconfiguredhits.Bilingual docs updated under
docs/zh/anddocs/en/(group-wake,astrbot-config,architecture,faq,platform-settings).AGENTS.mdand the archify/audit-product skill bullets now say mention is not a wake policy.Validation
208 pytest items passed.
adapter_preconfiguredhas no runtime or test hits.Compatibility and risk
Breaking for operators on the previous default
private=open: IM DMs now need an LLM prefix unless they opt intoopen. Existing on-diskprivate=openis kept. Savedgroup=mention/prefix_or_mentionsilently becomeprefixat runtime. Dashboard WebChat is covered byexplicit_surface. Empty@no longer talks to the model.Checklist
docs/zh/anddocs/en/.docs/public/openapi.json, and tests change together when routes or schemas change.pyproject.toml,requirements.txt, anduv.locktogether.!and aBREAKING CHANGE:footer.Agent note
Implemented Issue #58 Card A (admit-source redesign) on
route_turnwithout splittingWakingCheckStage. Touched the router, waking stage, defaults/metadata, Discord/WebChat/WeCom/cron/manager stamp sites,handle_empty_mention, bilingual docs, and AGENTS/skill copy. Ran the PLAN pytest set (208 passed), Ruff on touched Python,docs:build, andmake check-md. Residual risk: existing profiles that already storedprivate=openkeep chatting without a prefix; operators who setgroup=mentionfall back to prefix with no save-time warning. Tools: local git/pytest/ruff/prettier/vitepress/gh.