Skip to main content

Mountain/ProcessManagement/
CocoonManagement.rs

1//! # Cocoon Management
2//!
3//! This module provides comprehensive lifecycle management for the Cocoon
4//! sidecar process, which serves as the VS Code extension host within the
5//! Mountain editor.
6//!
7//! ## Overview
8//!
9//! Cocoon is a Node.js-based process that provides compatibility with VS Code
10//! extensions. This module handles:
11//!
12//! - **Process Spawning**: Launching Node.js with the Cocoon bootstrap script
13//! - **Environment Configuration**: Setting up environment variables for IPC
14//!   and logging
15//! - **Communication Setup**: Establishing gRPC/Vine connections on port 50052
16//! - **Health Monitoring**: Tracking process state and handling failures
17//! - **Lifecycle Management**: Graceful shutdown and restart capabilities
18//! - **IO Redirection**: Capturing stdout/stderr for logging and debugging
19//!
20//! ## Process Communication
21//!
22//! The Cocoon process communicates via:
23//! - gRPC on port 50052 (configured via MOUNTAIN_GRPC_PORT/COCOON_GRPC_PORT)
24//! - Vine protocol for cross-process messaging
25//! - Standard streams for logging (VSCODE_PIPE_LOGGING)
26//!
27//! ## Dependencies
28//!
29//! - `scripts/cocoon/bootstrap-fork.js`: Bootstrap script for launching Cocoon
30//! - Node.js runtime: Required for executing Cocoon
31//! - Vine gRPC server: Must be running on port 50051 for handshake
32//!
33//! ## Error Handling
34//!
35//! The module provides graceful degradation:
36//! - If the bootstrap script is missing, returns `FileSystemNotFound` error
37//! - If Node.js cannot be spawned, returns `IPCError`
38//! - If gRPC connection fails, returns `IPCError` with context
39//!
40//! # Module Contents
41//!
42//! - [`InitializeCocoon`]: Main entry point for Cocoon initialization
43//! - `LaunchAndManageCocoonSideCar`: Process spawning and lifecycle
44//! management
45//!
46//! ## Example
47//!
48//! ```rust,no_run
49//! use crate::Source::ProcessManagement::CocoonManagement::InitializeCocoon;
50//!
51//! // Initialize Cocoon with application handle and environment
52//! match InitializeCocoon(&app_handle, &environment).await {
53//! 	Ok(()) => println!("Cocoon initialized successfully"),
54//! 	Err(e) => eprintln!("Cocoon initialization failed: {:?}", e),
55//! }
56//! ```
57
58use std::{collections::HashMap, process::Stdio, sync::Arc, time::Duration};
59
60use CommonLibrary::Error::CommonError::CommonError;
61use tauri::{
62	AppHandle,
63	Manager,
64	Wry,
65	path::{BaseDirectory, PathResolver},
66};
67use tokio::{
68	io::{AsyncBufReadExt, BufReader},
69	process::{Child, Command},
70	sync::Mutex,
71	time::sleep,
72};
73
74use super::{InitializationData, NodeResolver};
75use crate::{
76	Environment::MountainEnvironment::MountainEnvironment,
77	IPC::Common::HealthStatus::{HealthIssue::Enum as HealthIssue, HealthMonitor::Struct as HealthMonitor},
78	ProcessManagement::ExtractDevTag::Fn as ExtractDevTag,
79	Vine,
80	dev_log,
81};
82
83/// Configuration constants for Cocoon process management
84const COCOON_SIDE_CAR_IDENTIFIER:&str = "cocoon-main";
85const COCOON_GRPC_PORT:u16 = 50052;
86const MOUNTAIN_GRPC_PORT:u16 = 50051;
87const BOOTSTRAP_SCRIPT_PATH:&str = "scripts/cocoon/bootstrap-fork.js";
88
89/// Exponential-backoff retry parameters for the Mountain → Cocoon gRPC
90/// handshake. Replaces the previous "20 × 1000 ms fixed poll" which
91/// under-probed the common race (Cocoon's stage2 binds the port at
92/// ~200 ms so attempts 1-2 fail and we sat idle through 18 more whole-
93/// second sleeps) and over-waited the real failure (when Cocoon is
94/// genuinely dead, we wasted 20 s before reporting).
95///
96/// Policy: start at 50 ms, double each attempt up to a 2 s ceiling,
97/// with a hard 20 s total-budget. Under healthy spawn timing (Cocoon
98/// up at 150-600 ms) this converges on attempts 3-5 in <~400 ms total;
99/// under a genuinely dead Cocoon the loop abandons at the budget.
100const GRPC_CONNECT_INITIAL_MS:u64 = 50;
101const GRPC_CONNECT_MAX_DELAY_MS:u64 = 2_000;
102const GRPC_CONNECT_BUDGET_MS:u64 = 20_000;
103
104/// Relative path from the resolved Cocoon package root to the bundled
105/// entry module. Used by the pre-flight guard below to fail fast with
106/// an actionable error when the bundle is missing (esbuild failure,
107/// partial rm -rf, freshly cloned checkout without `pnpm run
108/// prepublishOnly`, etc.) instead of spawning Node into a dying
109/// require() chain.
110const COCOON_BUNDLE_PROBE:&str = "../Cocoon/Target/Bootstrap/Implementation/Cocoon/Main.js";
111const HANDSHAKE_TIMEOUT_MS:u64 = 60000;
112const HEALTH_CHECK_INTERVAL_SECONDS:u64 = 5;
113#[allow(dead_code)]
114const MAX_RESTART_ATTEMPTS:u32 = 3;
115#[allow(dead_code)]
116const RESTART_WINDOW_SECONDS:u64 = 300;
117
118/// Global state for tracking Cocoon process lifecycle
119#[allow(dead_code)]
120struct CocoonProcessState {
121	ChildProcess:Option<Child>,
122	IsRunning:bool,
123	StartTime:Option<tokio::time::Instant>,
124	RestartCount:u32,
125	LastRestartTime:Option<tokio::time::Instant>,
126}
127
128impl Default for CocoonProcessState {
129	fn default() -> Self {
130		Self {
131			ChildProcess:None,
132			IsRunning:false,
133			StartTime:None,
134			RestartCount:0,
135			LastRestartTime:None,
136		}
137	}
138}
139
140// Global state for Cocoon process management
141lazy_static::lazy_static! {
142	static ref COCOON_STATE: Arc<Mutex<CocoonProcessState>> =
143		Arc::new(Mutex::new(CocoonProcessState::default()));
144
145	static ref COCOON_HEALTH: Arc<Mutex<HealthMonitor>> =
146		Arc::new(Mutex::new(HealthMonitor::new()));
147}
148
149/// Last-known PID of the Cocoon child process. Mirrored here so callers can
150/// read it without taking the async `COCOON_STATE` mutex (e.g. from IPC
151/// handlers such as `extensionHostStarter:start`). Set after spawn and
152/// cleared on shutdown. `0` means "not running".
153static COCOON_PID:std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
154
155/// Return the Cocoon child process's OS PID, or `None` if Cocoon has not
156/// been spawned (or has exited).
157pub fn GetCocoonPid() -> Option<u32> {
158	match COCOON_PID.load(std::sync::atomic::Ordering::Relaxed) {
159		0 => None,
160		Pid => Some(Pid),
161	}
162}
163
164/// The main entry point for initializing the Cocoon sidecar process manager.
165///
166/// This orchestrates the complete initialization sequence including:
167/// - Validating feature flags and dependencies
168/// - Launching the Cocoon process with proper configuration
169/// - Establishing gRPC communication
170/// - Performing the initialization handshake
171/// - Setting up process health monitoring
172///
173/// # Arguments
174///
175/// * `ApplicationHandle` - Tauri application handle for path resolution
176/// * `Environment` - Mountain environment containing application state and
177///   services
178///
179/// # Returns
180///
181/// * `Ok(())` - Cocoon initialized successfully and ready to accept extension
182///   requests
183/// * `Err(CommonError)` - Initialization failed with detailed error context
184///
185/// # Errors
186///
187/// - `FileSystemNotFound`: Bootstrap script not found
188/// - `IPCError`: Failed to spawn process or establish gRPC connection
189///
190/// # Example
191///
192/// ```rust,no_run
193/// use crate::Source::ProcessManagement::CocoonManagement::InitializeCocoon;
194///
195/// InitializeCocoon(&app_handle, &environment).await?;
196/// ```
197pub async fn InitializeCocoon(
198	ApplicationHandle:&AppHandle,
199	Environment:&Arc<MountainEnvironment>,
200) -> Result<(), CommonError> {
201	dev_log!("cocoon", "[CocoonManagement] Initializing Cocoon sidecar manager...");
202
203	// Atom N1: `debug-mountain-only` / `release-mountain-only` profiles set
204	// Spawn=false so Mountain boots without the extension host.
205	// Extension-related IPC returns the empty-state envelope; the workbench
206	// loads but no extension activates. Useful for integration tests that
207	// exercise Mountain in isolation and for the smallest shippable surface.
208	if matches!(std::env::var("Spawn").as_deref(), Ok("0") | Ok("false")) {
209		dev_log!("cocoon", "[CocoonManagement] Skipping spawn (Spawn=false)");
210		return Ok(());
211	}
212
213	#[cfg(feature = "ExtensionHostCocoon")]
214	{
215		LaunchAndManageCocoonSideCar(ApplicationHandle.clone(), Environment.clone()).await
216	}
217
218	#[cfg(not(feature = "ExtensionHostCocoon"))]
219	{
220		dev_log!(
221			"cocoon",
222			"[CocoonManagement] 'ExtensionHostCocoon' feature is disabled. Cocoon will not be launched."
223		);
224		Ok(())
225	}
226}
227
228/// Spawns the Cocoon process, manages its communication channels, and performs
229/// the complete initialization handshake sequence.
230///
231/// This function implements the complete Cocoon lifecycle:
232/// 1. Validates bootstrap script availability
233/// 2. Constructs environment variables for IPC and logging
234/// 3. Spawns Node.js process with proper IO redirection
235/// 4. Captures stdout/stderr for logging
236/// 5. Waits for gRPC server to be ready
237/// 6. Establishes Vine connection
238/// 7. Sends initialization payload and validates response
239///
240/// # Arguments
241///
242/// * `ApplicationHandle` - Tauri application handle for resolving resource
243///   paths
244/// * `Environment` - Mountain environment containing application state
245///
246/// # Returns
247///
248/// * `Ok(())` - Cocoon process spawned, connected, and initialized successfully
249/// * `Err(CommonError)` - Any failure during the initialization sequence
250///
251/// # Errors
252///
253/// - `FileSystemNotFound`: Bootstrap script not found in resources
254/// - `IPCError`: Failed to spawn process, connect gRPC, or complete handshake
255///
256/// # Lifecycle
257///
258/// The process runs as a background task with IO redirection for logging.
259/// Process failures are logged but not automatically restarted (callers should
260/// implement restart strategies based on their requirements).
261async fn LaunchAndManageCocoonSideCar(
262	ApplicationHandle:AppHandle,
263	Environment:Arc<MountainEnvironment>,
264) -> Result<(), CommonError> {
265	let SideCarIdentifier = COCOON_SIDE_CAR_IDENTIFIER.to_string();
266	let path_resolver:PathResolver<Wry> = ApplicationHandle.path().clone();
267
268	// Resolve bootstrap script path.
269	// 1) Try Tauri bundled resources (production builds).
270	// 2) Fallback: resolve relative to the executable (dev builds). Dev layout:
271	//    Target/debug/binary → ../../scripts/cocoon/bootstrap-fork.js
272	let ScriptPath = path_resolver
273		.resolve(BOOTSTRAP_SCRIPT_PATH, BaseDirectory::Resource)
274		.ok()
275		.filter(|P| P.exists())
276		.or_else(|| {
277			std::env::current_exe().ok().and_then(|Exe| {
278				let MountainRoot = Exe.parent()?.parent()?.parent()?;
279				let Candidate = MountainRoot.join(BOOTSTRAP_SCRIPT_PATH);
280				if Candidate.exists() { Some(Candidate) } else { None }
281			})
282		})
283		.ok_or_else(|| {
284			CommonError::FileSystemNotFound(
285				format!(
286					"Cocoon bootstrap script '{}' not found in resources or relative to executable",
287					BOOTSTRAP_SCRIPT_PATH
288				)
289				.into(),
290			)
291		})?;
292
293	dev_log!(
294		"cocoon",
295		"[CocoonManagement] Found bootstrap script at: {}",
296		ScriptPath.display()
297	);
298	crate::dev_log!("cocoon", "bootstrap script: {}", ScriptPath.display());
299
300	// Pre-flight: Cocoon's bundle must exist or the spawned Node will
301	// die silently on the first `import()` and we'll sit through 20+
302	// seconds of `attempt N/M` retries with no diagnostic.
303	//
304	// bootstrap-fork.js is in `Mountain/scripts/cocoon/`. The Cocoon
305	// bundle is at `Cocoon/Target/Bootstrap/Implementation/Cocoon/Main.js`
306	// relative to the repo root. Compose the probe path by walking up
307	// from the bootstrap script to the `Element/` root, then descending.
308	if let Some(BootstrapDirectory) = ScriptPath.parent() {
309		let ProbePath = BootstrapDirectory.join("../..").join(COCOON_BUNDLE_PROBE);
310		if !ProbePath.exists() {
311			return Err(CommonError::IPCError {
312				Description:format!(
313					"Cocoon bundle is missing at {}. Run `pnpm run prepublishOnly --filter=@codeeditorland/cocoon` \
314					 (or the full `./Maintain/Debug/Build.sh --profile debug-electron`) before launching - node will \
315					 fail to import without it and Mountain will fall into degraded mode with zero extensions \
316					 available. Root cause is typically an esbuild failure in an upstream Cocoon source file or a \
317					 stale `rm -rf Element/Cocoon/Target` without a rebuild.",
318					ProbePath.display()
319				),
320			});
321		}
322		dev_log!("cocoon", "[CocoonManagement] pre-flight OK: bundle at {}", ProbePath.display());
323	}
324
325	// Atom I6: zombie-Cocoon sweep. If a prior Mountain exited without
326	// killing its child (segfault, SIGKILL, debugger detach, …), the stale
327	// node process keeps port COCOON_GRPC_PORT bound. The new Mountain's
328	// VineClient then "successfully connects" to the zombie while the
329	// freshly-spawned Cocoon fails to bind with EADDRINUSE, and the whole
330	// extension host enters degraded mode with zero extensions visible.
331	//
332	// Probe the port. If it answers, find the owning PID via `lsof -t -i
333	// :<port>` and SIGTERM → 500ms wait → SIGKILL. Then proceed as normal.
334	SweepStaleCocoon(COCOON_GRPC_PORT);
335
336	// Atom N1: resolve Node binary via NodeResolver (shipped → version
337	// managers → homebrew → PATH). Logs the pick + source for forensics.
338	// Overridable via `Pick=/absolute/path/to/node`.
339	let ResolvedNodeBinary = NodeResolver::ResolveNodeBinary::Fn(&ApplicationHandle);
340
341	// Build Node.js command with comprehensive environment configuration
342	let mut NodeCommand = Command::new(&ResolvedNodeBinary.Path);
343
344	let mut EnvironmentVariables = HashMap::new();
345
346	// VS Code protocol environment variables for extension host compatibility
347	EnvironmentVariables.insert("VSCODE_PIPE_LOGGING".to_string(), "true".to_string());
348	EnvironmentVariables.insert("VSCODE_VERBOSE_LOGGING".to_string(), "true".to_string());
349	EnvironmentVariables.insert("VSCODE_PARENT_PID".to_string(), std::process::id().to_string());
350
351	// gRPC port configuration for Vine communication
352	EnvironmentVariables.insert("MOUNTAIN_GRPC_PORT".to_string(), MOUNTAIN_GRPC_PORT.to_string());
353	EnvironmentVariables.insert("COCOON_GRPC_PORT".to_string(), COCOON_GRPC_PORT.to_string());
354
355	// Preserve PATH so `node` resolves. env_clear() was stripping it.
356	if let Ok(Path) = std::env::var("PATH") {
357		EnvironmentVariables.insert("PATH".to_string(), Path);
358	}
359	if let Ok(Home) = std::env::var("HOME") {
360		EnvironmentVariables.insert("HOME".to_string(), Home);
361	}
362
363	// Atom I5: forward every Product*, Tier*, Network* env var from
364	// .env.Land into the Cocoon subprocess. Cocoon's InitData.ts +
365	// ExtensionHostHandler.ts read these at startup for version,
366	// identity, and port configuration. Without this forwarding, the
367	// whitelist above drops them and Cocoon falls back to defaults,
368	// defeating the single-source-of-truth design.
369	//
370	// PascalCase single-word vars: covers `.env.Land.PostHog` (Authorize,
371	// Beam, Report, Brand, Replay, Ask, Throttle, Buffer, Batch, Cap,
372	// Capture, OTLPEndpoint, OTLPEnabled), `.env.Land.Node` (Pick,
373	// Require), `.env.Land.Extensions` (Lodge, Extend, Probe, Ship, Wire,
374	// Install, Mute, Skip), and the kernel / Cocoon-spawn / preload
375	// gating flags (Spawn, Render). Each name is a single PascalCase
376	// action verb - no LAND_ prefix. Previously only Product/Tier/Network
377	// were forwarded and the PostHog bridge fell back to the empty-string
378	// default; the AllowList below now enumerates every Land-introduced
379	// env var by name so Cocoon sees the same values Mountain reads.
380	const LandEnvAllowList:&[&str] = &[
381		"Authorize",
382		"Beam",
383		"Report",
384		"Brand",
385		"Replay",
386		"Ask",
387		"Throttle",
388		"Buffer",
389		"Batch",
390		"Cap",
391		"Capture",
392		"OTLPEndpoint",
393		"OTLPEnabled",
394		"Pick",
395		"Require",
396		"Lodge",
397		"Extend",
398		"Probe",
399		"Ship",
400		"Wire",
401		"Install",
402		"Mute",
403		"Skip",
404		"Spawn",
405		"Render",
406		"Walk",
407		"Trace",
408		"Record",
409		"Profile",
410		"Diagnose",
411		"Resolve",
412		"Open",
413		"Warn",
414		"Catch",
415		"Source",
416		"Track",
417		"Defer",
418		"Boot",
419		"Pack",
420	];
421	for (Key, Value) in std::env::vars() {
422		if Key.starts_with("Product")
423			|| Key.starts_with("Tier")
424			|| Key.starts_with("Network")
425			|| LandEnvAllowList.contains(&Key.as_str())
426		{
427			EnvironmentVariables.insert(Key, Value);
428		}
429	}
430
431	// Atom I11: forward NODE_ENV / TAURI_ENV_DEBUG (Trace is
432	// already covered by the `LAND_` prefix sweep above). Without this,
433	// env_clear() leaves Cocoon seeing NodeEnv="production" /
434	// TauriDebug=false even on the debug-electron profile - silently
435	// disabling dev-only logging and debug-only diagnostics in Cocoon.
436	for Key in ["NODE_ENV", "TAURI_ENV_DEBUG"] {
437		if let Ok(Value) = std::env::var(Key) {
438			EnvironmentVariables.insert(Key.to_string(), Value);
439		}
440	}
441
442	NodeCommand
443		.arg(&ScriptPath)
444		.env_clear()
445		.envs(EnvironmentVariables)
446		.stdin(Stdio::piped())
447		.stdout(Stdio::piped())
448		.stderr(Stdio::piped());
449
450	// Spawn the process with error handling
451	let mut ChildProcess = NodeCommand.spawn().map_err(|Error| {
452		CommonError::IPCError {
453			Description:format!(
454				"Failed to spawn Cocoon with node={} (source={}): {}. Override with Pick=/absolute/path or install \
455				 Node.js.",
456				ResolvedNodeBinary.Path.display(),
457				ResolvedNodeBinary.Source.AsLabel(),
458				Error
459			),
460		}
461	})?;
462
463	let ProcessId = ChildProcess.id().unwrap_or(0);
464	COCOON_PID.store(ProcessId, std::sync::atomic::Ordering::Relaxed);
465	dev_log!("cocoon", "[CocoonManagement] Cocoon process spawned [PID: {}]", ProcessId);
466	crate::dev_log!("cocoon", "spawned PID={}", ProcessId);
467
468	// Capture stdout for trace logging. Two disposition classes:
469	//
470	// 1. Tagged lines produced by `Cocoon/Source/Services/DevLog.ts::
471	//    CocoonDevLog(Tag, Message)` arrive prefixed with `[DEV:<UPPER_TAG>]
472	//    <body>`. Re-emit under the matching Mountain tag (lowercased) so
473	//    `Trace=bootstrap-stage` on Mountain's side surfaces Cocoon's
474	//    `bootstrap-stage` lines without forcing the user to also enable the broad
475	//    `cocoon` tag.
476	//
477	// 2. Plain stdout (console.log, uncaught trace, etc.) stays under the `cocoon`
478	//    tag so it's silent unless explicitly requested.
479	if let Some(stdout) = ChildProcess.stdout.take() {
480		tokio::spawn(async move {
481			let Reader = BufReader::new(stdout);
482			let mut Lines = Reader.lines();
483
484			while let Ok(Some(Line)) = Lines.next_line().await {
485				if let Some(ForwardedTag) = ExtractDevTag(&Line) {
486					// dev_log! macro requires a static string, so match on
487					// the known tag set and fall through to raw 'cocoon'
488					// for anything else. Keep the arms in sync with
489					// `CocoonDevLog` call sites.
490					match ForwardedTag.as_str() {
491						"bootstrap-stage" => dev_log!("bootstrap-stage", "[Cocoon stdout] {}", Line),
492						"ext-activate" => dev_log!("ext-activate", "[Cocoon stdout] {}", Line),
493						"config-prime" => dev_log!("config-prime", "[Cocoon stdout] {}", Line),
494						"breaker" => dev_log!("breaker", "[Cocoon stdout] {}", Line),
495						_ => dev_log!("cocoon", "[Cocoon stdout] {}", Line),
496					}
497				} else {
498					dev_log!("cocoon", "[Cocoon stdout] {}", Line);
499				}
500			}
501		});
502	}
503
504	// Capture stderr for warn-level logging.
505	//
506	// Node and macOS tooling write a stream of informational-only noise
507	// to stderr that is indistinguishable from fatal errors at the line
508	// level. Downgrade these to the verbose `cocoon-stderr-verbose` tag
509	// (silent under `Trace=short`) so the main cocoon channel only
510	// carries actionable Node errors:
511	//
512	// - `: is already signed` / `: replacing existing signature` - macOS codesign
513	//   informational output when Cocoon re-signs a just-rebuilt extension binary.
514	//   Not an error.
515	// - `DeprecationWarning:` / `(node:...) [DEP0...]` - Node deprecation warnings
516	//   from VS Code's upstream dependencies (punycode, url.parse, Buffer()).
517	//   Fixable only in upstream, not in Land.
518	// - `Use \`node --trace-deprecation\` to show where the warning was created` -
519	//   follow-up to the DEP line above.
520	// - `EntryNotFound (FileSystemError):` + follow-up stack frames - extensions
521	//   (svelte, copilot, etc.) probe paths that may not exist and let the
522	//   rejection bubble up. Node's unhandled rejection printer splits the stack
523	//   across stderr lines. The classifier enters a stateful "suppress follow-up
524	//   stack frames" mode after the first EntryNotFound line and exits on a
525	//   non-frame line.
526	if let Some(stderr) = ChildProcess.stderr.take() {
527		tokio::spawn(async move {
528			let Reader = BufReader::new(stderr);
529			let mut Lines = Reader.lines();
530			let mut SuppressStackFrames = false;
531
532			while let Ok(Some(Line)) = Lines.next_line().await {
533				let Trimmed = Line.trim_start();
534				let IsStackFrame = Trimmed.starts_with("at ")
535					|| Trimmed.starts_with("code: '")
536					|| Trimmed == "}"
537					|| Trimmed.is_empty();
538				if SuppressStackFrames && IsStackFrame {
539					dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
540					continue;
541				}
542				// Exited the suppression window. Reset and classify
543				// this line normally.
544				SuppressStackFrames = false;
545
546				let IsBenignSingleLine = Line.contains(": is already signed")
547					|| Line.contains(": replacing existing signature")
548					|| Line.contains("DeprecationWarning:")
549					|| Line.contains("--trace-deprecation")
550					|| Line.contains("--trace-warnings");
551				let IsBenignStackHead = Line.contains("EntryNotFound (FileSystemError):")
552					|| Line.contains("FileNotFound (FileSystemError):")
553					|| Line.contains("[LandFix:UnhandledRejection]")
554					|| Line.starts_with("[Patcher] unhandledRejection:")
555					|| Line.starts_with("[Patcher] uncaughtException:");
556				if IsBenignStackHead {
557					SuppressStackFrames = true;
558				}
559				if IsBenignSingleLine || IsBenignStackHead {
560					dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
561				} else {
562					dev_log!("cocoon", "warn: [Cocoon stderr] {}", Line);
563				}
564			}
565		});
566	}
567
568	// Establish Vine connection to Cocoon with exponential-backoff
569	// retry + child-exit detection.
570	//
571	// Prior policy was 20 × 1000 ms fixed poll. Under healthy timing
572	// (Cocoon binds at 150-600 ms) that wasted ~400 ms of idle time
573	// every boot; under a genuinely dead Cocoon (import error, killed
574	// process, stale bundle) it burned 20 full seconds before giving
575	// up with a generic "is Cocoon running?" hint.
576	//
577	// New policy:
578	//   - Initial 50 ms sleep, doubled per attempt up to a 2 s ceiling.
579	//   - Hard 20 s total-budget (unchanged) so the overall failure ceiling doesn't
580	//     regress for pathological slow-boot hardware.
581	//   - Before each sleep, poll `ChildProcess.try_wait()`: if Node has exited,
582	//     abandon the loop immediately with the exit status embedded in the error -
583	//     no point retrying against a dead process, and the exit code usually
584	//     reveals the import failure (1 = unhandled exception, 13 = invalid
585	//     module).
586	let GRPCAddress = format!("127.0.0.1:{}", COCOON_GRPC_PORT);
587	dev_log!(
588		"cocoon",
589		"[CocoonManagement] Connecting to Cocoon gRPC at {} (exponential backoff, budget={}ms)...",
590		GRPCAddress,
591		GRPC_CONNECT_BUDGET_MS
592	);
593
594	let ConnectStart = tokio::time::Instant::now();
595	let mut CurrentDelayMs:u64 = GRPC_CONNECT_INITIAL_MS;
596	let mut ConnectAttempt = 0u32;
597
598	loop {
599		ConnectAttempt += 1;
600		crate::dev_log!(
601			"grpc",
602			"connecting to Cocoon at {} (attempt {}, elapsed={}ms)",
603			GRPCAddress,
604			ConnectAttempt,
605			ConnectStart.elapsed().as_millis()
606		);
607
608		match Vine::Client::ConnectToSideCar::Fn(SideCarIdentifier.clone(), GRPCAddress.clone()).await {
609			Ok(()) => {
610				crate::dev_log!(
611					"grpc",
612					"connected to Cocoon on attempt {} (elapsed={}ms)",
613					ConnectAttempt,
614					ConnectStart.elapsed().as_millis()
615				);
616				break;
617			},
618			Err(Error) => {
619				// Check if the Node child has already died. If yes,
620				// there is no point waiting any longer - report the
621				// real exit status so the dev log points at the real
622				// failure (import error, crash, oom kill) instead of
623				// the abstract "connect refused" message.
624				match ChildProcess.try_wait() {
625					Ok(Some(ExitStatus)) => {
626						let ExitCode = ExitStatus.code().unwrap_or(-1);
627						crate::dev_log!(
628							"grpc",
629							"attempt {} aborted: Cocoon Node process exited with code={} after {}ms - stderr above \
630							 (if any) explains why",
631							ConnectAttempt,
632							ExitCode,
633							ConnectStart.elapsed().as_millis()
634						);
635						return Err(CommonError::IPCError {
636							Description:format!(
637								"Cocoon spawned but exited with code {} before Mountain could connect. See \
638								 `[DEV:COCOON] warn: [Cocoon stderr] …` lines above for the Node-side error - \
639								 typically a missing bundle (\"Cannot find module …\") or an ESM/CJS import drift \
640								 after a partial build.",
641								ExitCode
642							),
643						});
644					},
645					Ok(None) => { /* still running, keep trying */ },
646					Err(WaitErr) => {
647						// try_wait() itself failed; this is rare
648						// (would imply a kernel-level issue). Surface
649						// it but keep trying - the dial may still
650						// succeed on the next attempt.
651						crate::dev_log!("grpc", "warn: try_wait on Cocoon child failed: {} (continuing)", WaitErr);
652					},
653				}
654
655				let Elapsed = ConnectStart.elapsed().as_millis() as u64;
656				if Elapsed >= GRPC_CONNECT_BUDGET_MS {
657					crate::dev_log!(
658						"grpc",
659						"attempt {} timed out (budget {}ms exhausted): {}",
660						ConnectAttempt,
661						GRPC_CONNECT_BUDGET_MS,
662						Error
663					);
664					return Err(CommonError::IPCError {
665						Description:format!(
666							"Failed to connect to Cocoon gRPC at {} after {} attempts over {}ms: {} (is Cocoon \
667							 running? check `[DEV:COCOON]` log lines for stderr, or re-run with the debug-electron \
668							 build profile if the bundle is stale)",
669							GRPCAddress, ConnectAttempt, GRPC_CONNECT_BUDGET_MS, Error
670						),
671					});
672				}
673
674				crate::dev_log!(
675					"grpc",
676					"attempt {} pending (Cocoon still booting): {}, backing off {}ms",
677					ConnectAttempt,
678					Error,
679					CurrentDelayMs
680				);
681
682				sleep(Duration::from_millis(CurrentDelayMs)).await;
683				// Exponential ramp with a 2 s ceiling. Doubling keeps
684				// the common case fast (4 attempts cover the first
685				// 750 ms) and the cold-boot case bounded.
686				CurrentDelayMs = (CurrentDelayMs * 2).min(GRPC_CONNECT_MAX_DELAY_MS);
687			},
688		}
689	}
690
691	dev_log!(
692		"cocoon",
693		"[CocoonManagement] Connected to Cocoon. Sending initialization data..."
694	);
695
696	// Brief delay to ensure Cocoon's gRPC service handlers are fully registered
697	// after bindAsync resolves (race condition on fast connections like attempt 1)
698	sleep(Duration::from_millis(200)).await;
699
700	// Construct initialization payload
701	let MainInitializationData = InitializationData::ConstructExtensionHostInitializationData(&Environment)
702		.await
703		.map_err(|Error| {
704			CommonError::IPCError { Description:format!("Failed to construct initialization data: {}", Error) }
705		})?;
706
707	// Send initialization request with timeout
708	let Response = Vine::Client::SendRequest::Fn(
709		&SideCarIdentifier,
710		"InitializeExtensionHost".to_string(),
711		MainInitializationData,
712		HANDSHAKE_TIMEOUT_MS,
713	)
714	.await
715	.map_err(|Error| {
716		CommonError::IPCError {
717			Description:format!("Failed to send initialization request to Cocoon: {}", Error),
718		}
719	})?;
720
721	// Validate handshake response
722	match Response.as_str() {
723		Some("initialized") => {
724			dev_log!(
725				"cocoon",
726				"[CocoonManagement] Cocoon handshake complete. Extension host is ready."
727			);
728		},
729		Some(other) => {
730			return Err(CommonError::IPCError {
731				Description:format!("Cocoon initialization failed with unexpected response: {}", other),
732			});
733		},
734		None => {
735			return Err(CommonError::IPCError {
736				Description:"Cocoon initialization failed: no response received".to_string(),
737			});
738		},
739	}
740
741	// Trigger startup extension activation. Cocoon is fully reactive -
742	// it won't activate any extensions until Mountain tells it to.
743	// Fire-and-forget: don't block on activation, and don't fail init if it errors.
744	//
745	// Stock VS Code fires a cascade of activation events at boot:
746	//   1. `*` - unconditional "activate anything that contributes *"
747	//   2. `onStartupFinished` - queued extensions whose start may be deferred
748	//      until after the first frame renders
749	//   3. `workspaceContains:<pattern>` for each pattern any extension
750	//      contributes, fired per matching workspace folder
751	//
752	// Previously only `*` fired, which meant a large class of extensions
753	// that gate on `workspaceContains:package.json`, `onStartupFinished`,
754	// or similar events never activated without user interaction. The
755	// added bursts below bring startup coverage in line with stock.
756	let SideCarId = SideCarIdentifier.clone();
757	let EnvironmentForActivation = Environment.clone();
758	tokio::spawn(async move {
759		// Small delay to let Cocoon finish processing the init response
760		sleep(Duration::from_millis(500)).await;
761
762		crate::dev_log!("exthost", "Sending $activateByEvent(\"*\") to Cocoon");
763
764		if let Err(Error) = Vine::Client::SendRequest::Fn(
765			&SideCarId,
766			"$activateByEvent".to_string(),
767			serde_json::json!({ "activationEvent": "*" }),
768			30_000,
769		)
770		.await
771		{
772			dev_log!("cocoon", "warn: [CocoonManagement] $activateByEvent(\"*\") failed: {}", Error);
773			return;
774		}
775		dev_log!("cocoon", "[CocoonManagement] Startup extensions activation (*) triggered");
776
777		// Phase 2: workspaceContains: events. Iterate the scanned
778		// extension registry, collect every pattern contributed via the
779		// `workspaceContains:<pattern>` activation event, and fire the
780		// event if at least one workspace folder contains a path
781		// matching the pattern. Patterns are treated as filename globs
782		// relative to any workspace folder root; matching is done with
783		// a lightweight walk bounded by depth 3 and 2048 total visited
784		// entries per folder to cap worst-case cost on huge repos.
785		let WorkspacePatterns = {
786			let AppState = &EnvironmentForActivation.ApplicationState;
787			let Folders:Vec<std::path::PathBuf> = AppState
788				.Workspace
789				.WorkspaceFolders
790				.lock()
791				.ok()
792				.map(|Guard| {
793					Guard
794						.iter()
795						.filter_map(|Folder| Folder.URI.to_file_path().ok())
796						.collect::<Vec<_>>()
797				})
798				.unwrap_or_default();
799
800			let Patterns:Vec<String> = AppState
801				.Extension
802				.ScannedExtensions
803				.ScannedExtensions
804				.lock()
805				.ok()
806				.map(|Guard| {
807					let mut Set:std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
808					for Description in Guard.values() {
809						if let Some(Events) = &Description.ActivationEvents {
810							for Event in Events {
811								if let Some(Pattern) = Event.strip_prefix("workspaceContains:") {
812									Set.insert(Pattern.to_string());
813								}
814							}
815						}
816					}
817					Set.into_iter().collect()
818				})
819				.unwrap_or_default();
820
821			(Folders, Patterns)
822		};
823
824		let (WorkspaceFolders, Patterns):(Vec<std::path::PathBuf>, Vec<String>) = WorkspacePatterns;
825		if !WorkspaceFolders.is_empty() && !Patterns.is_empty() {
826			let Matched = FindMatchingWorkspaceContainsPatterns(&WorkspaceFolders, &Patterns);
827			dev_log!(
828				"exthost",
829				"[CocoonManagement] workspaceContains scan: {} pattern(s) matched across {} folder(s)",
830				Matched.len(),
831				WorkspaceFolders.len()
832			);
833			for Pattern in Matched {
834				let Event = format!("workspaceContains:{}", Pattern);
835				if let Err(Error) = Vine::Client::SendRequest::Fn(
836					&SideCarId,
837					"$activateByEvent".to_string(),
838					serde_json::json!({ "activationEvent": Event }),
839					30_000,
840				)
841				.await
842				{
843					dev_log!(
844						"cocoon",
845						"warn: [CocoonManagement] $activateByEvent({}) failed: {}",
846						Event,
847						Error
848					);
849				}
850			}
851		}
852
853		// Phase 3: onStartupFinished. Fire after the `*` burst has had a
854		// moment to complete so late-binding extensions layered on top
855		// of startup contributions resolve in the expected order.
856		sleep(Duration::from_millis(2_000)).await;
857		if let Err(Error) = Vine::Client::SendRequest::Fn(
858			&SideCarId,
859			"$activateByEvent".to_string(),
860			serde_json::json!({ "activationEvent": "onStartupFinished" }),
861			30_000,
862		)
863		.await
864		{
865			dev_log!(
866				"cocoon",
867				"warn: [CocoonManagement] $activateByEvent(onStartupFinished) failed: {}",
868				Error
869			);
870		} else {
871			dev_log!("cocoon", "[CocoonManagement] onStartupFinished activation triggered");
872		}
873	});
874
875	// Store process handle for health monitoring and management
876	{
877		let mut state = COCOON_STATE.lock().await;
878		state.ChildProcess = Some(ChildProcess);
879		state.IsRunning = true;
880		state.StartTime = Some(tokio::time::Instant::now());
881		dev_log!("cocoon", "[CocoonManagement] Process state updated: Running");
882	}
883
884	// Reset health monitor on successful initialization
885	{
886		let mut health = COCOON_HEALTH.lock().await;
887		health.ClearIssues();
888		dev_log!("cocoon", "[CocoonManagement] Health monitor reset to active state");
889	}
890
891	// Start background health monitoring
892	let state_clone = Arc::clone(&COCOON_STATE);
893	tokio::spawn(monitor_cocoon_health_task(state_clone));
894	dev_log!("cocoon", "[CocoonManagement] Background health monitoring started");
895
896	Ok(())
897}
898
899/// Background task that monitors Cocoon process health and logs crashes.
900///
901/// Once the child process has exited (or never existed), the monitor no
902/// longer has anything useful to say - it exits quietly instead of
903/// flooding the log with "No Cocoon process to monitor" every 5s, which
904/// was rendering the dev log unreadable after any Cocoon crash.
905async fn monitor_cocoon_health_task(state:Arc<Mutex<CocoonProcessState>>) {
906	loop {
907		tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_INTERVAL_SECONDS)).await;
908
909		let mut state_guard = state.lock().await;
910
911		// Check if we have a child process to monitor
912		if state_guard.ChildProcess.is_some() {
913			// Get process ID before checking status
914			let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
915
916			// Check if process is still running
917			let exit_status = {
918				let child = state_guard.ChildProcess.as_mut().unwrap();
919				child.try_wait()
920			};
921
922			match exit_status {
923				Ok(Some(exit_code)) => {
924					// Process has exited (crashed or terminated)
925					let uptime = state_guard.StartTime.map(|t| t.elapsed().as_secs()).unwrap_or(0);
926					let exit_code_num = exit_code.code().unwrap_or(-1);
927					dev_log!(
928						"cocoon",
929						"warn: [CocoonHealth] Cocoon process crashed [PID: {}] [Exit Code: {}] [Uptime: {}s]",
930						process_id.unwrap_or(0),
931						exit_code_num,
932						uptime
933					);
934
935					// Update state
936					state_guard.IsRunning = false;
937					state_guard.ChildProcess = None;
938					COCOON_PID.store(0, std::sync::atomic::Ordering::Relaxed);
939
940					// Report health issue
941					{
942						let mut health = COCOON_HEALTH.lock().await;
943						health.AddIssue(HealthIssue::Custom(format!("ProcessCrashed (Exit code: {})", exit_code_num)));
944						dev_log!("cocoon", "warn: [CocoonHealth] Health score: {}", health.HealthScore);
945					}
946
947					// Log that automatic restart would be needed
948					dev_log!(
949						"cocoon",
950						"warn: [CocoonHealth] CRASH DETECTED: Cocoon process has crashed and must be restarted \
951						 manually or via application reinitialization"
952					);
953				},
954				Ok(None) => {
955					// Process is still running
956					dev_log!(
957						"cocoon",
958						"[CocoonHealth] Cocoon process is healthy [PID: {}]",
959						process_id.unwrap_or(0)
960					);
961				},
962				Err(e) => {
963					// Error checking process status
964					dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
965
966					// Report health issue
967					{
968						let mut health = COCOON_HEALTH.lock().await;
969						health.AddIssue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
970					}
971				},
972			}
973		} else {
974			// No child process exists - log exactly once, then exit the
975			// monitor loop. Prior behaviour: flood the log with
976			// "No Cocoon process to monitor" every 5s forever after a
977			// crash, making the dev log unreadable. A future respawn will
978			// spawn a fresh monitor via `StartCocoon`.
979			dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor - exiting monitor loop");
980			drop(state_guard);
981			return;
982		}
983	}
984}
985
986/// Atom I6: post-shutdown hard-kill. Called by RuntimeShutdown after the
987/// `$shutdown` gRPC notification has been sent (and either succeeded or
988/// timed out). Grabs the stored `Child` handle and force-terminates it if
989/// still alive, then resets COCOON_STATE. This plugs the "Mountain exits
990/// cleanly but child stays running" leak that leads to zombie-Cocoon
991/// zombies holding the gRPC port.
992///
993/// Call AFTER the graceful $shutdown attempt - we don't want to race the
994/// child's own cleanup. Safe to call with no stored child (no-op).
995pub async fn HardKillCocoon() {
996	let mut State = COCOON_STATE.lock().await;
997	if let Some(mut Child) = State.ChildProcess.take() {
998		let Pid = Child.id().unwrap_or(0);
999		match Child.try_wait() {
1000			Ok(Some(_Status)) => {
1001				dev_log!("cocoon", "[CocoonShutdown] Child PID {} already exited; clearing handle.", Pid);
1002			},
1003			Ok(None) => {
1004				dev_log!(
1005					"cocoon",
1006					"[CocoonShutdown] Child PID {} still alive after $shutdown; sending SIGKILL.",
1007					Pid
1008				);
1009				if let Err(Error) = Child.start_kill() {
1010					dev_log!("cocoon", "warn: [CocoonShutdown] start_kill failed on PID {}: {}", Pid, Error);
1011				}
1012				// Best-effort wait so the OS reaps and frees the port.
1013				let _ = tokio::time::timeout(std::time::Duration::from_secs(2), Child.wait()).await;
1014			},
1015			Err(Error) => {
1016				dev_log!("cocoon", "warn: [CocoonShutdown] try_wait failed on PID {}: {}", Pid, Error);
1017			},
1018		}
1019	}
1020	State.IsRunning = false;
1021}
1022
1023/// Atom I6: pre-boot sweep. TCP-probe the Cocoon gRPC port and kill any
1024/// stale process still bound to it. Prevents the EADDRINUSE cascade that
1025/// leaves the extension host in degraded mode when a prior Mountain exited
1026/// without cleaning up its child.
1027///
1028/// Behaviour:
1029/// - If the port answers a TCP connect, assume an owner is listening.
1030/// - Use `lsof -nP -iTCP:<port> -sTCP:LISTEN -t` (macOS/Linux) to resolve the
1031///   PID. `lsof` is ubiquitous on macOS/Linux and doesn't require root for
1032///   local user-owned processes.
1033/// - SIGTERM first, 500ms grace window, then SIGKILL if still alive.
1034/// - Logs every step via `dev_log!("cocoon", …)` so the sweep is visible in
1035///   Mountain.dev.log without parsing stderr.
1036/// - Best-effort: failures don't abort Mountain boot. A real EADDRINUSE later
1037///   will surface via Cocoon's own bootstrap error.
1038fn SweepStaleCocoon(Port:u16) {
1039	use std::{net::TcpStream, time::Duration};
1040
1041	let Addr = format!("127.0.0.1:{}", Port);
1042
1043	// Cheap liveness probe. Timeout is aggressive - zombie ports answer
1044	// immediately; a clean port is ECONNREFUSED and returns instantly.
1045	let Probe =
1046		TcpStream::connect_timeout(&Addr.parse().expect("valid socket addr literal"), Duration::from_millis(200));
1047	if Probe.is_err() {
1048		dev_log!("cocoon", "[CocoonSweep] Port {} is clean (no prior listener).", Port);
1049		return;
1050	}
1051
1052	dev_log!(
1053		"cocoon",
1054		"[CocoonSweep] Port {} has a listener - attempting to resolve owner via lsof.",
1055		Port
1056	);
1057
1058	// `lsof -nP -iTCP:<port> -sTCP:LISTEN -t` → one PID per line.
1059	let LsofOutput = std::process::Command::new("lsof")
1060		.args(["-nP", &format!("-iTCP:{}", Port), "-sTCP:LISTEN", "-t"])
1061		.output();
1062
1063	let Output = match LsofOutput {
1064		Ok(O) => O,
1065		Err(Error) => {
1066			dev_log!(
1067				"cocoon",
1068				"warn: [CocoonSweep] lsof unavailable ({}). Skipping sweep; Cocoon spawn may fail with EADDRINUSE.",
1069				Error
1070			);
1071			return;
1072		},
1073	};
1074
1075	if !Output.status.success() {
1076		dev_log!("cocoon", "warn: [CocoonSweep] lsof exited non-zero. Skipping sweep.");
1077		return;
1078	}
1079
1080	let Stdout = String::from_utf8_lossy(&Output.stdout);
1081	let Pids:Vec<i32> = Stdout.lines().filter_map(|L| L.trim().parse::<i32>().ok()).collect();
1082
1083	if Pids.is_empty() {
1084		dev_log!(
1085			"cocoon",
1086			"warn: [CocoonSweep] Port {} answered but lsof found no LISTEN PID - giving up.",
1087			Port
1088		);
1089		return;
1090	}
1091
1092	// Guard against self-kill. Mountain currently binds 50051, not Cocoon's
1093	// 50052, but belt-and-braces for future refactors.
1094	let SelfPid = std::process::id() as i32;
1095	for Pid in Pids {
1096		if Pid == SelfPid {
1097			dev_log!(
1098				"cocoon",
1099				"warn: [CocoonSweep] Port {} owned by Mountain itself (PID {}); refusing to kill.",
1100				Port,
1101				Pid
1102			);
1103			continue;
1104		}
1105		dev_log!("cocoon", "[CocoonSweep] Killing stale PID {} (SIGTERM).", Pid);
1106		let _ = std::process::Command::new("kill").arg(Pid.to_string()).status();
1107		std::thread::sleep(Duration::from_millis(500));
1108		// Recheck - if still alive, escalate.
1109		let StillAlive = std::process::Command::new("kill")
1110			.args(["-0", &Pid.to_string()])
1111			.status()
1112			.map(|S| S.success())
1113			.unwrap_or(false);
1114		if StillAlive {
1115			dev_log!("cocoon", "warn: [CocoonSweep] PID {} survived SIGTERM; sending SIGKILL.", Pid);
1116			let _ = std::process::Command::new("kill").args(["-9", &Pid.to_string()]).status();
1117			std::thread::sleep(Duration::from_millis(200));
1118		}
1119		dev_log!("cocoon", "[CocoonSweep] PID {} reaped.", Pid);
1120	}
1121}
1122
1123/// Return the subset of `Patterns` for which at least one workspace folder
1124/// contains a matching file or directory. Patterns are interpreted the same
1125/// way VS Code does for `workspaceContains:<pattern>` activation events:
1126///
1127/// - A bare filename (no slash, no wildcards) matches an entry with that name
1128///   at the workspace root (e.g. `package.json`).
1129/// - A path with slashes but no wildcards matches a direct descendant relative
1130///   to the root (e.g. `.vscode/launch.json`).
1131/// - A glob with `**/` prefix matches any descendant up to a bounded depth.
1132/// - Any other wildcard form is matched via a simple segment-by-segment walk
1133///   honouring `*` (single segment) and `**` (any number of segments).
1134///
1135/// Matching is bounded to depth 3 and 4096 total directory entries per
1136/// workspace root to keep the cost sub-100 ms on large monorepos. Anything
1137/// deeper is rare for activation-event triggers; the trade-off is
1138/// documented in VS Code's own `ExtensionService.scanExtensions`.
1139fn FindMatchingWorkspaceContainsPatterns(Folders:&[std::path::PathBuf], Patterns:&[String]) -> Vec<String> {
1140	use std::collections::HashSet;
1141
1142	const MAX_DEPTH:usize = 3;
1143	const MAX_ENTRIES_PER_ROOT:usize = 4096;
1144
1145	let mut Matched:HashSet<String> = HashSet::new();
1146	for Folder in Folders {
1147		if !Folder.is_dir() {
1148			continue;
1149		}
1150		// Collect up to MAX_ENTRIES_PER_ROOT paths relative to the folder.
1151		let mut Entries:Vec<String> = Vec::new();
1152		let mut Stack:Vec<(std::path::PathBuf, usize)> = vec![(Folder.clone(), 0)];
1153		while let Some((Current, Depth)) = Stack.pop() {
1154			if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1155				break;
1156			}
1157			let ReadDirResult = std::fs::read_dir(&Current);
1158			let ReadDir = match ReadDirResult {
1159				Ok(R) => R,
1160				Err(_) => continue,
1161			};
1162			for Entry in ReadDir.flatten() {
1163				if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1164					break;
1165				}
1166				let Path = Entry.path();
1167				let Relative = match Path.strip_prefix(Folder) {
1168					Ok(R) => R.to_string_lossy().replace('\\', "/"),
1169					Err(_) => continue,
1170				};
1171				let IsDir = Entry.file_type().map(|T| T.is_dir()).unwrap_or(false);
1172				Entries.push(Relative.clone());
1173				if IsDir && Depth + 1 < MAX_DEPTH {
1174					Stack.push((Path, Depth + 1));
1175				}
1176			}
1177		}
1178
1179		for Pattern in Patterns {
1180			if Matched.contains(Pattern) {
1181				continue;
1182			}
1183			if PatternMatchesAnyEntry(Pattern, &Entries) {
1184				Matched.insert(Pattern.clone());
1185			}
1186		}
1187	}
1188	Matched.into_iter().collect()
1189}
1190
1191/// Very small glob-matcher scoped to VS Code `workspaceContains:` syntax.
1192/// Supports literal paths, `*` (one path segment), and `**` (zero or more
1193/// segments). Case-sensitive per the VS Code spec.
1194fn PatternMatchesAnyEntry(Pattern:&str, Entries:&[String]) -> bool {
1195	let HasWildcard = Pattern.contains('*') || Pattern.contains('?');
1196	if !HasWildcard {
1197		return Entries.iter().any(|E| E == Pattern);
1198	}
1199	let PatternSegments:Vec<&str> = Pattern.split('/').collect();
1200	Entries
1201		.iter()
1202		.any(|E| SegmentMatch(&PatternSegments, &E.split('/').collect::<Vec<_>>()))
1203}
1204
1205fn SegmentMatch(Pattern:&[&str], Entry:&[&str]) -> bool {
1206	if Pattern.is_empty() {
1207		return Entry.is_empty();
1208	}
1209	let Head = Pattern[0];
1210	if Head == "**" {
1211		// `**` matches zero or more segments. Try consuming 0..=entry.len().
1212		for Consumed in 0..=Entry.len() {
1213			if SegmentMatch(&Pattern[1..], &Entry[Consumed..]) {
1214				return true;
1215			}
1216		}
1217		return false;
1218	}
1219	if Entry.is_empty() {
1220		return false;
1221	}
1222	if SingleSegmentMatch(Head, Entry[0]) {
1223		return SegmentMatch(&Pattern[1..], &Entry[1..]);
1224	}
1225	false
1226}
1227
1228fn SingleSegmentMatch(Pattern:&str, Segment:&str) -> bool {
1229	if Pattern == "*" {
1230		return true;
1231	}
1232	if !Pattern.contains('*') && !Pattern.contains('?') {
1233		return Pattern == Segment;
1234	}
1235	// Minimal star-glob on a single segment: split by '*' and check each
1236	// fragment appears in order. Doesn't support `?` (rare in
1237	// workspaceContains patterns); unsupported glob chars fall through to
1238	// literal equality.
1239	let Fragments:Vec<&str> = Pattern.split('*').collect();
1240	let mut Cursor = 0usize;
1241	for (Index, Fragment) in Fragments.iter().enumerate() {
1242		if Fragment.is_empty() {
1243			continue;
1244		}
1245		if Index == 0 {
1246			if !Segment[Cursor..].starts_with(Fragment) {
1247				return false;
1248			}
1249			Cursor += Fragment.len();
1250			continue;
1251		}
1252		match Segment[Cursor..].find(Fragment) {
1253			Some(Offset) => Cursor += Offset + Fragment.len(),
1254			None => return false,
1255		}
1256	}
1257	if let Some(Last) = Fragments.last()
1258		&& !Last.is_empty()
1259	{
1260		return Segment.ends_with(Last);
1261	}
1262	true
1263}