-
Notifications
You must be signed in to change notification settings - Fork 3.6k
fix(tui): gate recommended_plugins to once per engine, suppressed by loaded skills #6279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -873,6 +873,12 @@ pub struct Engine { | |
| mcp_event_generation: u64, | ||
| /// Workspace-scoped immutable plugin catalogue and authority receipts. | ||
| plugin_registry: Arc<crate::plugins::PluginRegistry>, | ||
| /// Keeps the append-only `<recommended_plugins>` fragment once-per- | ||
| /// Engine-lifetime per plugin id, and suppresses plugins whose name a | ||
| /// catalogue skill already covers (#6274). The skill-name snapshot is | ||
| /// taken at construction from the same catalogue the system prompt | ||
| /// indexes (see the gate's known-limitations note). | ||
| recommended_plugin_gate: StdMutex<crate::plugins::recommend::RecommendedPluginGate>, | ||
| api_provider: ApiProvider, | ||
| /// Exact configured route key. Named custom providers share the `Custom` | ||
| /// enum, so the enum alone cannot prove that the active client is current. | ||
|
|
@@ -1674,6 +1680,28 @@ impl Engine { | |
| // `run_turn` restarts it per turn; this initial value only matters | ||
| // for hosts that inspect the engine before the first turn. | ||
| let turn_wall_clock_budget = config.turn_wall_clock; | ||
| // Skill-name snapshot for the plugin-suggestion gate (#6274): the | ||
| // SAME catalogue the system prompt indexes (prompts.rs skills block — | ||
| // workspace roots + configured skills_dir + plugin-sourced skills), | ||
| // so suppression sees everything the session actually has. | ||
| let gate_skill_names: std::collections::BTreeSet<String> = | ||
| crate::skills::discover_for_workspace_and_dir_with_mode_and_plugins( | ||
| &config.workspace, | ||
| &config.skills_dir, | ||
| crate::skills::SkillDiscoveryMode::from_codewhale_only( | ||
| config.skills_scan_codewhale_only, | ||
| ), | ||
| Some(plugin_registry.as_ref()), | ||
| ) | ||
| .list() | ||
| .iter() | ||
| .flat_map(|skill| { | ||
| std::iter::once(skill.name.clone()).chain(skill.aliases.iter().cloned()) | ||
| }) | ||
| .map(|name| name.trim().to_ascii_lowercase()) | ||
| .filter(|name| !name.is_empty()) | ||
| .collect(); | ||
|
|
||
| let engine = Engine { | ||
| config, | ||
| api_config: api_config.clone(), | ||
|
|
@@ -1698,6 +1726,11 @@ impl Engine { | |
| mcp_boot_generation: None, | ||
| mcp_event_generation: 0, | ||
| plugin_registry, | ||
| recommended_plugin_gate: StdMutex::new( | ||
| crate::plugins::recommend::RecommendedPluginGate::with_skill_names( | ||
| gate_skill_names, | ||
| ), | ||
| ), | ||
| api_provider, | ||
| api_provider_identity, | ||
| api_provider_id, | ||
|
|
@@ -3708,13 +3741,20 @@ impl Engine { | |
| cache_control: None, | ||
| }]; | ||
| } | ||
| let recommended_plugins = crate::plugins::recommend::recommended_plugins_user_fragment( | ||
| &text, | ||
| self.plugin_registry.as_ref(), | ||
| &crate::plugins::recommend::load_marketplace_candidates( | ||
| self.plugin_registry.state_path(), | ||
| ), | ||
| ); | ||
| let recommended_plugins = { | ||
| let mut recommended_plugin_gate = self | ||
| .recommended_plugin_gate | ||
| .lock() | ||
| .unwrap_or_else(std::sync::PoisonError::into_inner); | ||
| crate::plugins::recommend::recommended_plugins_user_fragment( | ||
| &text, | ||
| self.plugin_registry.as_ref(), | ||
| &crate::plugins::recommend::load_marketplace_candidates( | ||
| self.plugin_registry.state_path(), | ||
| ), | ||
| &mut recommended_plugin_gate, | ||
| ) | ||
| }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Narrow the guarded region: load the marketplace candidates before taking the lock (and ideally let the fragment take the already-loaded slice plus a pre-loaded dismissal set), so the gate mutex only covers the |
||
| let expanded = crate::image_attach::expand_attachment_blocks(&text); | ||
| let mut content = Vec::with_capacity(3 + expanded.blocks.len()); | ||
| content.push(ContentBlock::Text { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[INFO] Engine gate mutex is held across filesystem reads and the whole match
The new block takes
self.recommended_plugin_gate.lock()and then evaluatescrate::plugins::recommend::load_marketplace_candidates(self.plugin_registry.state_path())(a marketplace-state file read) and the entirerecommended_plugins_user_fragmentcall (which itself doesSettings::load_read_only()plus plugin matching) while the guard is live; the guard is only needed forgate.admits. On a host that composes user turns on more than one thread for the same Engine (the engine is long-lived and the lock is the only synchronisation onshown), the second composer blocks on the first composer's disk I/O under this std mutex. No correctness change — the once-per-engine set stays consistent — but the lock scope is wider than the state it protects.