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