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/CocoonMain.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/CocoonMain.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	// `.env.Land.Node` (Pick, Require), `.env.Land.Extensions` (Lodge,
373	// Extend, Probe, Ship, Wire, Install, Mute, Skip), and the
374	// kernel / Cocoon-spawn / preload gating flags (Spawn, Render).
375	// Each name is a single PascalCase action verb - no LAND_ prefix.
376	// Previously only Product/Tier/Network were forwarded and the
377	// PostHog bridge fell back to the empty-string default; the
378	// AllowList below now enumerates every Land-introduced env var by
379	// 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		"Pick",
392		"Require",
393		"Lodge",
394		"Extend",
395		"Probe",
396		"Ship",
397		"Wire",
398		"Install",
399		"Mute",
400		"Skip",
401		"Spawn",
402		"Render",
403		"Walk",
404		"Trace",
405		"Record",
406		"Profile",
407		"Diagnose",
408		"Resolve",
409		"Open",
410		"Warn",
411		"Catch",
412		"Source",
413		"Track",
414		"Defer",
415		"Boot",
416		"Pack",
417	];
418	for (Key, Value) in std::env::vars() {
419		if Key.starts_with("Product")
420			|| Key.starts_with("Tier")
421			|| Key.starts_with("Network")
422			|| LandEnvAllowList.contains(&Key.as_str())
423		{
424			EnvironmentVariables.insert(Key, Value);
425		}
426	}
427
428	// Atom I11: forward NODE_ENV / TAURI_ENV_DEBUG (Trace is
429	// already covered by the `LAND_` prefix sweep above). Without this,
430	// env_clear() leaves Cocoon seeing NodeEnv="production" /
431	// TauriDebug=false even on the debug-electron profile - silently
432	// disabling dev-only logging and debug-only diagnostics in Cocoon.
433	for Key in ["NODE_ENV", "TAURI_ENV_DEBUG"] {
434		if let Ok(Value) = std::env::var(Key) {
435			EnvironmentVariables.insert(Key.to_string(), Value);
436		}
437	}
438
439	NodeCommand
440		.arg(&ScriptPath)
441		.env_clear()
442		.envs(EnvironmentVariables)
443		.stdin(Stdio::piped())
444		.stdout(Stdio::piped())
445		.stderr(Stdio::piped());
446
447	// Spawn the process with error handling
448	let mut ChildProcess = NodeCommand.spawn().map_err(|Error| {
449		CommonError::IPCError {
450			Description:format!(
451				"Failed to spawn Cocoon with node={} (source={}): {}. Override with Pick=/absolute/path or install \
452				 Node.js.",
453				ResolvedNodeBinary.Path.display(),
454				ResolvedNodeBinary.Source.AsLabel(),
455				Error
456			),
457		}
458	})?;
459
460	let ProcessId = ChildProcess.id().unwrap_or(0);
461	COCOON_PID.store(ProcessId, std::sync::atomic::Ordering::Relaxed);
462	dev_log!("cocoon", "[CocoonManagement] Cocoon process spawned [PID: {}]", ProcessId);
463	crate::dev_log!("cocoon", "spawned PID={}", ProcessId);
464
465	// Capture stdout for trace logging. Two disposition classes:
466	//
467	// 1. Tagged lines produced by `Cocoon/Source/Services/DevLog.ts::
468	//    CocoonDevLog(Tag, Message)` arrive prefixed with `[DEV:<UPPER_TAG>]
469	//    <body>`. Re-emit under the matching Mountain tag (lowercased) so
470	//    `Trace=bootstrap-stage` on Mountain's side surfaces Cocoon's
471	//    `bootstrap-stage` lines without forcing the user to also enable the broad
472	//    `cocoon` tag.
473	//
474	// 2. Plain stdout (console.log, uncaught trace, etc.) stays under the `cocoon`
475	//    tag so it's silent unless explicitly requested.
476	if let Some(stdout) = ChildProcess.stdout.take() {
477		tokio::spawn(async move {
478			let Reader = BufReader::new(stdout);
479			let mut Lines = Reader.lines();
480
481			while let Ok(Some(Line)) = Lines.next_line().await {
482				if let Some(ForwardedTag) = ExtractDevTag(&Line) {
483					// dev_log! macro requires a static string, so match on
484					// the known tag set and fall through to raw 'cocoon'
485					// for anything else. Keep the arms in sync with
486					// `CocoonDevLog` call sites.
487					match ForwardedTag.as_str() {
488						"bootstrap-stage" => dev_log!("bootstrap-stage", "[Cocoon stdout] {}", Line),
489						"ext-activate" => dev_log!("ext-activate", "[Cocoon stdout] {}", Line),
490						"config-prime" => dev_log!("config-prime", "[Cocoon stdout] {}", Line),
491						"breaker" => dev_log!("breaker", "[Cocoon stdout] {}", Line),
492						_ => dev_log!("cocoon", "[Cocoon stdout] {}", Line),
493					}
494				} else {
495					dev_log!("cocoon", "[Cocoon stdout] {}", Line);
496				}
497			}
498		});
499	}
500
501	// Capture stderr for warn-level logging.
502	//
503	// Node and macOS tooling write a stream of informational-only noise
504	// to stderr that is indistinguishable from fatal errors at the line
505	// level. Downgrade these to the verbose `cocoon-stderr-verbose` tag
506	// (silent under `Trace=short`) so the main cocoon channel only
507	// carries actionable Node errors:
508	//
509	// - `: is already signed` / `: replacing existing signature` - macOS codesign
510	//   informational output when Cocoon re-signs a just-rebuilt extension binary.
511	//   Not an error.
512	// - `DeprecationWarning:` / `(node:...) [DEP0...]` - Node deprecation warnings
513	//   from VS Code's upstream dependencies (punycode, url.parse, Buffer()).
514	//   Fixable only in upstream, not in Land.
515	// - `Use \`node --trace-deprecation\` to show where the warning was created` -
516	//   follow-up to the DEP line above.
517	// - `EntryNotFound (FileSystemError):` + follow-up stack frames - extensions
518	//   (svelte, copilot, etc.) probe paths that may not exist and let the
519	//   rejection bubble up. Node's unhandled rejection printer splits the stack
520	//   across stderr lines. The classifier enters a stateful "suppress follow-up
521	//   stack frames" mode after the first EntryNotFound line and exits on a
522	//   non-frame line.
523	if let Some(stderr) = ChildProcess.stderr.take() {
524		tokio::spawn(async move {
525			let Reader = BufReader::new(stderr);
526			let mut Lines = Reader.lines();
527			let mut SuppressStackFrames = false;
528
529			while let Ok(Some(Line)) = Lines.next_line().await {
530				let Trimmed = Line.trim_start();
531				let IsStackFrame = Trimmed.starts_with("at ")
532					|| Trimmed.starts_with("code: '")
533					|| Trimmed == "}"
534					|| Trimmed.is_empty();
535				if SuppressStackFrames && IsStackFrame {
536					dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
537					continue;
538				}
539				// Exited the suppression window. Reset and classify
540				// this line normally.
541				SuppressStackFrames = false;
542
543				let IsBenignSingleLine = Line.contains(": is already signed")
544					|| Line.contains(": replacing existing signature")
545					|| Line.contains("DeprecationWarning:")
546					|| Line.contains("--trace-deprecation")
547					|| Line.contains("--trace-warnings");
548				let IsBenignStackHead = Line.contains("EntryNotFound (FileSystemError):")
549					|| Line.contains("FileNotFound (FileSystemError):")
550					|| Line.contains("[LandFix:UnhandledRejection]")
551					|| Line.starts_with("[Patcher] unhandledRejection:")
552					|| Line.starts_with("[Patcher] uncaughtException:");
553				if IsBenignStackHead {
554					SuppressStackFrames = true;
555				}
556				if IsBenignSingleLine || IsBenignStackHead {
557					dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
558				} else {
559					dev_log!("cocoon", "warn: [Cocoon stderr] {}", Line);
560				}
561			}
562		});
563	}
564
565	// Establish Vine connection to Cocoon with exponential-backoff
566	// retry + child-exit detection.
567	//
568	// Prior policy was 20 × 1000 ms fixed poll. Under healthy timing
569	// (Cocoon binds at 150-600 ms) that wasted ~400 ms of idle time
570	// every boot; under a genuinely dead Cocoon (import error, killed
571	// process, stale bundle) it burned 20 full seconds before giving
572	// up with a generic "is Cocoon running?" hint.
573	//
574	// New policy:
575	//   - Initial 50 ms sleep, doubled per attempt up to a 2 s ceiling.
576	//   - Hard 20 s total-budget (unchanged) so the overall failure ceiling doesn't
577	//     regress for pathological slow-boot hardware.
578	//   - Before each sleep, poll `ChildProcess.try_wait()`: if Node has exited,
579	//     abandon the loop immediately with the exit status embedded in the error -
580	//     no point retrying against a dead process, and the exit code usually
581	//     reveals the import failure (1 = unhandled exception, 13 = invalid
582	//     module).
583	let GRPCAddress = format!("127.0.0.1:{}", COCOON_GRPC_PORT);
584	dev_log!(
585		"cocoon",
586		"[CocoonManagement] Connecting to Cocoon gRPC at {} (exponential backoff, budget={}ms)...",
587		GRPCAddress,
588		GRPC_CONNECT_BUDGET_MS
589	);
590
591	let ConnectStart = tokio::time::Instant::now();
592	let mut CurrentDelayMs:u64 = GRPC_CONNECT_INITIAL_MS;
593	let mut ConnectAttempt = 0u32;
594
595	loop {
596		ConnectAttempt += 1;
597		crate::dev_log!(
598			"grpc",
599			"connecting to Cocoon at {} (attempt {}, elapsed={}ms)",
600			GRPCAddress,
601			ConnectAttempt,
602			ConnectStart.elapsed().as_millis()
603		);
604
605		match Vine::Client::ConnectToSideCar::Fn(SideCarIdentifier.clone(), GRPCAddress.clone()).await {
606			Ok(()) => {
607				crate::dev_log!(
608					"grpc",
609					"connected to Cocoon on attempt {} (elapsed={}ms)",
610					ConnectAttempt,
611					ConnectStart.elapsed().as_millis()
612				);
613				break;
614			},
615			Err(Error) => {
616				// Check if the Node child has already died. If yes,
617				// there is no point waiting any longer - report the
618				// real exit status so the dev log points at the real
619				// failure (import error, crash, oom kill) instead of
620				// the abstract "connect refused" message.
621				match ChildProcess.try_wait() {
622					Ok(Some(ExitStatus)) => {
623						let ExitCode = ExitStatus.code().unwrap_or(-1);
624						crate::dev_log!(
625							"grpc",
626							"attempt {} aborted: Cocoon Node process exited with code={} after {}ms - stderr above \
627							 (if any) explains why",
628							ConnectAttempt,
629							ExitCode,
630							ConnectStart.elapsed().as_millis()
631						);
632						return Err(CommonError::IPCError {
633							Description:format!(
634								"Cocoon spawned but exited with code {} before Mountain could connect. See \
635								 `[DEV:COCOON] warn: [Cocoon stderr] …` lines above for the Node-side error - \
636								 typically a missing bundle (\"Cannot find module …\") or an ESM/CJS import drift \
637								 after a partial build.",
638								ExitCode
639							),
640						});
641					},
642					Ok(None) => { /* still running, keep trying */ },
643					Err(WaitErr) => {
644						// try_wait() itself failed; this is rare
645						// (would imply a kernel-level issue). Surface
646						// it but keep trying - the dial may still
647						// succeed on the next attempt.
648						crate::dev_log!("grpc", "warn: try_wait on Cocoon child failed: {} (continuing)", WaitErr);
649					},
650				}
651
652				let Elapsed = ConnectStart.elapsed().as_millis() as u64;
653				if Elapsed >= GRPC_CONNECT_BUDGET_MS {
654					crate::dev_log!(
655						"grpc",
656						"attempt {} timed out (budget {}ms exhausted): {}",
657						ConnectAttempt,
658						GRPC_CONNECT_BUDGET_MS,
659						Error
660					);
661					return Err(CommonError::IPCError {
662						Description:format!(
663							"Failed to connect to Cocoon gRPC at {} after {} attempts over {}ms: {} (is Cocoon \
664							 running? check `[DEV:COCOON]` log lines for stderr, or re-run with the debug-electron \
665							 build profile if the bundle is stale)",
666							GRPCAddress, ConnectAttempt, GRPC_CONNECT_BUDGET_MS, Error
667						),
668					});
669				}
670
671				crate::dev_log!(
672					"grpc",
673					"attempt {} pending (Cocoon still booting): {}, backing off {}ms",
674					ConnectAttempt,
675					Error,
676					CurrentDelayMs
677				);
678
679				sleep(Duration::from_millis(CurrentDelayMs)).await;
680				// Exponential ramp with a 2 s ceiling. Doubling keeps
681				// the common case fast (4 attempts cover the first
682				// 750 ms) and the cold-boot case bounded.
683				CurrentDelayMs = (CurrentDelayMs * 2).min(GRPC_CONNECT_MAX_DELAY_MS);
684			},
685		}
686	}
687
688	dev_log!(
689		"cocoon",
690		"[CocoonManagement] Connected to Cocoon. Sending initialization data..."
691	);
692
693	// Brief delay to ensure Cocoon's gRPC service handlers are fully registered
694	// after bindAsync resolves (race condition on fast connections like attempt 1)
695	sleep(Duration::from_millis(200)).await;
696
697	// Construct initialization payload
698	let MainInitializationData = InitializationData::ConstructExtensionHostInitializationData(&Environment)
699		.await
700		.map_err(|Error| {
701			CommonError::IPCError { Description:format!("Failed to construct initialization data: {}", Error) }
702		})?;
703
704	// Send initialization request with timeout
705	let Response = Vine::Client::SendRequest::Fn(
706		&SideCarIdentifier,
707		"InitializeExtensionHost".to_string(),
708		MainInitializationData,
709		HANDSHAKE_TIMEOUT_MS,
710	)
711	.await
712	.map_err(|Error| {
713		CommonError::IPCError {
714			Description:format!("Failed to send initialization request to Cocoon: {}", Error),
715		}
716	})?;
717
718	// Validate handshake response
719	match Response.as_str() {
720		Some("initialized") => {
721			dev_log!(
722				"cocoon",
723				"[CocoonManagement] Cocoon handshake complete. Extension host is ready."
724			);
725		},
726		Some(other) => {
727			return Err(CommonError::IPCError {
728				Description:format!("Cocoon initialization failed with unexpected response: {}", other),
729			});
730		},
731		None => {
732			return Err(CommonError::IPCError {
733				Description:"Cocoon initialization failed: no response received".to_string(),
734			});
735		},
736	}
737
738	// Trigger startup extension activation. Cocoon is fully reactive -
739	// it won't activate any extensions until Mountain tells it to.
740	// Fire-and-forget: don't block on activation, and don't fail init if it errors.
741	//
742	// Stock VS Code fires a cascade of activation events at boot:
743	//   1. `*` - unconditional "activate anything that contributes *"
744	//   2. `onStartupFinished` - queued extensions whose start may be deferred
745	//      until after the first frame renders
746	//   3. `workspaceContains:<pattern>` for each pattern any extension
747	//      contributes, fired per matching workspace folder
748	//
749	// Previously only `*` fired, which meant a large class of extensions
750	// that gate on `workspaceContains:package.json`, `onStartupFinished`,
751	// or similar events never activated without user interaction. The
752	// added bursts below bring startup coverage in line with stock.
753	let SideCarId = SideCarIdentifier.clone();
754	let EnvironmentForActivation = Environment.clone();
755	tokio::spawn(async move {
756		// Small delay to let Cocoon finish processing the init response
757		sleep(Duration::from_millis(500)).await;
758
759		crate::dev_log!("exthost", "Sending $activateByEvent(\"*\") to Cocoon");
760
761		if let Err(Error) = Vine::Client::SendRequest::Fn(
762			&SideCarId,
763			"$activateByEvent".to_string(),
764			serde_json::json!({ "activationEvent": "*" }),
765			30_000,
766		)
767		.await
768		{
769			dev_log!("cocoon", "warn: [CocoonManagement] $activateByEvent(\"*\") failed: {}", Error);
770			return;
771		}
772		dev_log!("cocoon", "[CocoonManagement] Startup extensions activation (*) triggered");
773
774		// Phase 2: workspaceContains: events. Iterate the scanned
775		// extension registry, collect every pattern contributed via the
776		// `workspaceContains:<pattern>` activation event, and fire the
777		// event if at least one workspace folder contains a path
778		// matching the pattern. Patterns are treated as filename globs
779		// relative to any workspace folder root; matching is done with
780		// a lightweight walk bounded by depth 3 and 2048 total visited
781		// entries per folder to cap worst-case cost on huge repos.
782		let WorkspacePatterns = {
783			let AppState = &EnvironmentForActivation.ApplicationState;
784			let Folders:Vec<std::path::PathBuf> = AppState
785				.Workspace
786				.WorkspaceFolders
787				.lock()
788				.ok()
789				.map(|Guard| {
790					Guard
791						.iter()
792						.filter_map(|Folder| Folder.URI.to_file_path().ok())
793						.collect::<Vec<_>>()
794				})
795				.unwrap_or_default();
796
797			let Patterns:Vec<String> = AppState
798				.Extension
799				.ScannedExtensions
800				.ScannedExtensions
801				.lock()
802				.ok()
803				.map(|Guard| {
804					let mut Set:std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
805					for Description in Guard.values() {
806						if let Some(Events) = &Description.ActivationEvents {
807							for Event in Events {
808								if let Some(Pattern) = Event.strip_prefix("workspaceContains:") {
809									Set.insert(Pattern.to_string());
810								}
811							}
812						}
813					}
814					Set.into_iter().collect()
815				})
816				.unwrap_or_default();
817
818			(Folders, Patterns)
819		};
820
821		let (WorkspaceFolders, Patterns):(Vec<std::path::PathBuf>, Vec<String>) = WorkspacePatterns;
822		if !WorkspaceFolders.is_empty() && !Patterns.is_empty() {
823			let Matched = FindMatchingWorkspaceContainsPatterns(&WorkspaceFolders, &Patterns);
824			dev_log!(
825				"exthost",
826				"[CocoonManagement] workspaceContains scan: {} pattern(s) matched across {} folder(s)",
827				Matched.len(),
828				WorkspaceFolders.len()
829			);
830			for Pattern in Matched {
831				let Event = format!("workspaceContains:{}", Pattern);
832				if let Err(Error) = Vine::Client::SendRequest::Fn(
833					&SideCarId,
834					"$activateByEvent".to_string(),
835					serde_json::json!({ "activationEvent": Event }),
836					30_000,
837				)
838				.await
839				{
840					dev_log!(
841						"cocoon",
842						"warn: [CocoonManagement] $activateByEvent({}) failed: {}",
843						Event,
844						Error
845					);
846				}
847			}
848		}
849
850		// Phase 3: onStartupFinished. Fire after the `*` burst has had a
851		// moment to complete so late-binding extensions layered on top
852		// of startup contributions resolve in the expected order.
853		sleep(Duration::from_millis(2_000)).await;
854		if let Err(Error) = Vine::Client::SendRequest::Fn(
855			&SideCarId,
856			"$activateByEvent".to_string(),
857			serde_json::json!({ "activationEvent": "onStartupFinished" }),
858			30_000,
859		)
860		.await
861		{
862			dev_log!(
863				"cocoon",
864				"warn: [CocoonManagement] $activateByEvent(onStartupFinished) failed: {}",
865				Error
866			);
867		} else {
868			dev_log!("cocoon", "[CocoonManagement] onStartupFinished activation triggered");
869		}
870	});
871
872	// Store process handle for health monitoring and management
873	{
874		let mut state = COCOON_STATE.lock().await;
875		state.ChildProcess = Some(ChildProcess);
876		state.IsRunning = true;
877		state.StartTime = Some(tokio::time::Instant::now());
878		dev_log!("cocoon", "[CocoonManagement] Process state updated: Running");
879	}
880
881	// Reset health monitor on successful initialization
882	{
883		let mut health = COCOON_HEALTH.lock().await;
884		health.ClearIssues();
885		dev_log!("cocoon", "[CocoonManagement] Health monitor reset to active state");
886	}
887
888	// Start background health monitoring
889	let state_clone = Arc::clone(&COCOON_STATE);
890	tokio::spawn(monitor_cocoon_health_task(state_clone));
891	dev_log!("cocoon", "[CocoonManagement] Background health monitoring started");
892
893	Ok(())
894}
895
896/// Background task that monitors Cocoon process health and logs crashes.
897///
898/// Once the child process has exited (or never existed), the monitor no
899/// longer has anything useful to say - it exits quietly instead of
900/// flooding the log with "No Cocoon process to monitor" every 5s, which
901/// was rendering the dev log unreadable after any Cocoon crash.
902async fn monitor_cocoon_health_task(state:Arc<Mutex<CocoonProcessState>>) {
903	loop {
904		tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_INTERVAL_SECONDS)).await;
905
906		let mut state_guard = state.lock().await;
907
908		// Check if we have a child process to monitor
909		if state_guard.ChildProcess.is_some() {
910			// Get process ID before checking status
911			let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
912
913			// Check if process is still running
914			let exit_status = {
915				let child = state_guard.ChildProcess.as_mut().unwrap();
916				child.try_wait()
917			};
918
919			match exit_status {
920				Ok(Some(exit_code)) => {
921					// Process has exited (crashed or terminated)
922					let uptime = state_guard.StartTime.map(|t| t.elapsed().as_secs()).unwrap_or(0);
923					let exit_code_num = exit_code.code().unwrap_or(-1);
924					dev_log!(
925						"cocoon",
926						"warn: [CocoonHealth] Cocoon process crashed [PID: {}] [Exit Code: {}] [Uptime: {}s]",
927						process_id.unwrap_or(0),
928						exit_code_num,
929						uptime
930					);
931
932					// Update state
933					state_guard.IsRunning = false;
934					state_guard.ChildProcess = None;
935					COCOON_PID.store(0, std::sync::atomic::Ordering::Relaxed);
936
937					// Report health issue
938					{
939						let mut health = COCOON_HEALTH.lock().await;
940						health.AddIssue(HealthIssue::Custom(format!("ProcessCrashed (Exit code: {})", exit_code_num)));
941						dev_log!("cocoon", "warn: [CocoonHealth] Health score: {}", health.HealthScore);
942					}
943
944					// Log that automatic restart would be needed
945					dev_log!(
946						"cocoon",
947						"warn: [CocoonHealth] CRASH DETECTED: Cocoon process has crashed and must be restarted \
948						 manually or via application reinitialization"
949					);
950				},
951				Ok(None) => {
952					// Process is still running
953					dev_log!(
954						"cocoon",
955						"[CocoonHealth] Cocoon process is healthy [PID: {}]",
956						process_id.unwrap_or(0)
957					);
958				},
959				Err(e) => {
960					// Error checking process status
961					dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
962
963					// Report health issue
964					{
965						let mut health = COCOON_HEALTH.lock().await;
966						health.AddIssue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
967					}
968				},
969			}
970		} else {
971			// No child process exists - log exactly once, then exit the
972			// monitor loop. Prior behaviour: flood the log with
973			// "No Cocoon process to monitor" every 5s forever after a
974			// crash, making the dev log unreadable. A future respawn will
975			// spawn a fresh monitor via `StartCocoon`.
976			dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor - exiting monitor loop");
977			drop(state_guard);
978			return;
979		}
980	}
981}
982
983/// Atom I6: post-shutdown hard-kill. Called by RuntimeShutdown after the
984/// `$shutdown` gRPC notification has been sent (and either succeeded or
985/// timed out). Grabs the stored `Child` handle and force-terminates it if
986/// still alive, then resets COCOON_STATE. This plugs the "Mountain exits
987/// cleanly but child stays running" leak that leads to zombie-Cocoon
988/// zombies holding the gRPC port.
989///
990/// Call AFTER the graceful $shutdown attempt - we don't want to race the
991/// child's own cleanup. Safe to call with no stored child (no-op).
992pub async fn HardKillCocoon() {
993	let mut State = COCOON_STATE.lock().await;
994	if let Some(mut Child) = State.ChildProcess.take() {
995		let Pid = Child.id().unwrap_or(0);
996		match Child.try_wait() {
997			Ok(Some(_Status)) => {
998				dev_log!("cocoon", "[CocoonShutdown] Child PID {} already exited; clearing handle.", Pid);
999			},
1000			Ok(None) => {
1001				dev_log!(
1002					"cocoon",
1003					"[CocoonShutdown] Child PID {} still alive after $shutdown; sending SIGKILL.",
1004					Pid
1005				);
1006				if let Err(Error) = Child.start_kill() {
1007					dev_log!("cocoon", "warn: [CocoonShutdown] start_kill failed on PID {}: {}", Pid, Error);
1008				}
1009				// Best-effort wait so the OS reaps and frees the port.
1010				let _ = tokio::time::timeout(std::time::Duration::from_secs(2), Child.wait()).await;
1011			},
1012			Err(Error) => {
1013				dev_log!("cocoon", "warn: [CocoonShutdown] try_wait failed on PID {}: {}", Pid, Error);
1014			},
1015		}
1016	}
1017	State.IsRunning = false;
1018}
1019
1020/// Atom I6: pre-boot sweep. TCP-probe the Cocoon gRPC port and kill any
1021/// stale process still bound to it. Prevents the EADDRINUSE cascade that
1022/// leaves the extension host in degraded mode when a prior Mountain exited
1023/// without cleaning up its child.
1024///
1025/// Behaviour:
1026/// - If the port answers a TCP connect, assume an owner is listening.
1027/// - Use `lsof -nP -iTCP:<port> -sTCP:LISTEN -t` (macOS/Linux) to resolve the
1028///   PID. `lsof` is ubiquitous on macOS/Linux and doesn't require root for
1029///   local user-owned processes.
1030/// - SIGTERM first, 500ms grace window, then SIGKILL if still alive.
1031/// - Logs every step via `dev_log!("cocoon", …)` so the sweep is visible in
1032///   Mountain.dev.log without parsing stderr.
1033/// - Best-effort: failures don't abort Mountain boot. A real EADDRINUSE later
1034///   will surface via Cocoon's own bootstrap error.
1035fn SweepStaleCocoon(Port:u16) {
1036	use std::{net::TcpStream, time::Duration};
1037
1038	let Addr = format!("127.0.0.1:{}", Port);
1039
1040	// Cheap liveness probe. Timeout is aggressive - zombie ports answer
1041	// immediately; a clean port is ECONNREFUSED and returns instantly.
1042	let Probe =
1043		TcpStream::connect_timeout(&Addr.parse().expect("valid socket addr literal"), Duration::from_millis(200));
1044	if Probe.is_err() {
1045		dev_log!("cocoon", "[CocoonSweep] Port {} is clean (no prior listener).", Port);
1046		return;
1047	}
1048
1049	dev_log!(
1050		"cocoon",
1051		"[CocoonSweep] Port {} has a listener - attempting to resolve owner via lsof.",
1052		Port
1053	);
1054
1055	// `lsof -nP -iTCP:<port> -sTCP:LISTEN -t` → one PID per line.
1056	let LsofOutput = std::process::Command::new("lsof")
1057		.args(["-nP", &format!("-iTCP:{}", Port), "-sTCP:LISTEN", "-t"])
1058		.output();
1059
1060	let Output = match LsofOutput {
1061		Ok(O) => O,
1062		Err(Error) => {
1063			dev_log!(
1064				"cocoon",
1065				"warn: [CocoonSweep] lsof unavailable ({}). Skipping sweep; Cocoon spawn may fail with EADDRINUSE.",
1066				Error
1067			);
1068			return;
1069		},
1070	};
1071
1072	if !Output.status.success() {
1073		dev_log!("cocoon", "warn: [CocoonSweep] lsof exited non-zero. Skipping sweep.");
1074		return;
1075	}
1076
1077	let Stdout = String::from_utf8_lossy(&Output.stdout);
1078	let Pids:Vec<i32> = Stdout.lines().filter_map(|L| L.trim().parse::<i32>().ok()).collect();
1079
1080	if Pids.is_empty() {
1081		dev_log!(
1082			"cocoon",
1083			"warn: [CocoonSweep] Port {} answered but lsof found no LISTEN PID - giving up.",
1084			Port
1085		);
1086		return;
1087	}
1088
1089	// Guard against self-kill. Mountain currently binds 50051, not Cocoon's
1090	// 50052, but belt-and-braces for future refactors.
1091	let SelfPid = std::process::id() as i32;
1092	for Pid in Pids {
1093		if Pid == SelfPid {
1094			dev_log!(
1095				"cocoon",
1096				"warn: [CocoonSweep] Port {} owned by Mountain itself (PID {}); refusing to kill.",
1097				Port,
1098				Pid
1099			);
1100			continue;
1101		}
1102		dev_log!("cocoon", "[CocoonSweep] Killing stale PID {} (SIGTERM).", Pid);
1103		let _ = std::process::Command::new("kill").arg(Pid.to_string()).status();
1104		std::thread::sleep(Duration::from_millis(500));
1105		// Recheck - if still alive, escalate.
1106		let StillAlive = std::process::Command::new("kill")
1107			.args(["-0", &Pid.to_string()])
1108			.status()
1109			.map(|S| S.success())
1110			.unwrap_or(false);
1111		if StillAlive {
1112			dev_log!("cocoon", "warn: [CocoonSweep] PID {} survived SIGTERM; sending SIGKILL.", Pid);
1113			let _ = std::process::Command::new("kill").args(["-9", &Pid.to_string()]).status();
1114			std::thread::sleep(Duration::from_millis(200));
1115		}
1116		dev_log!("cocoon", "[CocoonSweep] PID {} reaped.", Pid);
1117	}
1118}
1119
1120/// Return the subset of `Patterns` for which at least one workspace folder
1121/// contains a matching file or directory. Patterns are interpreted the same
1122/// way VS Code does for `workspaceContains:<pattern>` activation events:
1123///
1124/// - A bare filename (no slash, no wildcards) matches an entry with that name
1125///   at the workspace root (e.g. `package.json`).
1126/// - A path with slashes but no wildcards matches a direct descendant relative
1127///   to the root (e.g. `.vscode/launch.json`).
1128/// - A glob with `**/` prefix matches any descendant up to a bounded depth.
1129/// - Any other wildcard form is matched via a simple segment-by-segment walk
1130///   honouring `*` (single segment) and `**` (any number of segments).
1131///
1132/// Matching is bounded to depth 3 and 4096 total directory entries per
1133/// workspace root to keep the cost sub-100 ms on large monorepos. Anything
1134/// deeper is rare for activation-event triggers; the trade-off is
1135/// documented in VS Code's own `ExtensionService.scanExtensions`.
1136fn FindMatchingWorkspaceContainsPatterns(Folders:&[std::path::PathBuf], Patterns:&[String]) -> Vec<String> {
1137	use std::collections::HashSet;
1138
1139	const MAX_DEPTH:usize = 3;
1140	const MAX_ENTRIES_PER_ROOT:usize = 4096;
1141
1142	let mut Matched:HashSet<String> = HashSet::new();
1143	for Folder in Folders {
1144		if !Folder.is_dir() {
1145			continue;
1146		}
1147		// Collect up to MAX_ENTRIES_PER_ROOT paths relative to the folder.
1148		let mut Entries:Vec<String> = Vec::new();
1149		let mut Stack:Vec<(std::path::PathBuf, usize)> = vec![(Folder.clone(), 0)];
1150		while let Some((Current, Depth)) = Stack.pop() {
1151			if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1152				break;
1153			}
1154			let ReadDirResult = std::fs::read_dir(&Current);
1155			let ReadDir = match ReadDirResult {
1156				Ok(R) => R,
1157				Err(_) => continue,
1158			};
1159			for Entry in ReadDir.flatten() {
1160				if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1161					break;
1162				}
1163				let Path = Entry.path();
1164				let Relative = match Path.strip_prefix(Folder) {
1165					Ok(R) => R.to_string_lossy().replace('\\', "/"),
1166					Err(_) => continue,
1167				};
1168				let IsDir = Entry.file_type().map(|T| T.is_dir()).unwrap_or(false);
1169				Entries.push(Relative.clone());
1170				if IsDir && Depth + 1 < MAX_DEPTH {
1171					Stack.push((Path, Depth + 1));
1172				}
1173			}
1174		}
1175
1176		for Pattern in Patterns {
1177			if Matched.contains(Pattern) {
1178				continue;
1179			}
1180			if PatternMatchesAnyEntry(Pattern, &Entries) {
1181				Matched.insert(Pattern.clone());
1182			}
1183		}
1184	}
1185	Matched.into_iter().collect()
1186}
1187
1188/// Very small glob-matcher scoped to VS Code `workspaceContains:` syntax.
1189/// Supports literal paths, `*` (one path segment), and `**` (zero or more
1190/// segments). Case-sensitive per the VS Code spec.
1191fn PatternMatchesAnyEntry(Pattern:&str, Entries:&[String]) -> bool {
1192	let HasWildcard = Pattern.contains('*') || Pattern.contains('?');
1193	if !HasWildcard {
1194		return Entries.iter().any(|E| E == Pattern);
1195	}
1196	let PatternSegments:Vec<&str> = Pattern.split('/').collect();
1197	Entries
1198		.iter()
1199		.any(|E| SegmentMatch(&PatternSegments, &E.split('/').collect::<Vec<_>>()))
1200}
1201
1202fn SegmentMatch(Pattern:&[&str], Entry:&[&str]) -> bool {
1203	if Pattern.is_empty() {
1204		return Entry.is_empty();
1205	}
1206	let Head = Pattern[0];
1207	if Head == "**" {
1208		// `**` matches zero or more segments. Try consuming 0..=entry.len().
1209		for Consumed in 0..=Entry.len() {
1210			if SegmentMatch(&Pattern[1..], &Entry[Consumed..]) {
1211				return true;
1212			}
1213		}
1214		return false;
1215	}
1216	if Entry.is_empty() {
1217		return false;
1218	}
1219	if SingleSegmentMatch(Head, Entry[0]) {
1220		return SegmentMatch(&Pattern[1..], &Entry[1..]);
1221	}
1222	false
1223}
1224
1225fn SingleSegmentMatch(Pattern:&str, Segment:&str) -> bool {
1226	if Pattern == "*" {
1227		return true;
1228	}
1229	if !Pattern.contains('*') && !Pattern.contains('?') {
1230		return Pattern == Segment;
1231	}
1232	// Minimal star-glob on a single segment: split by '*' and check each
1233	// fragment appears in order. Doesn't support `?` (rare in
1234	// workspaceContains patterns); unsupported glob chars fall through to
1235	// literal equality.
1236	let Fragments:Vec<&str> = Pattern.split('*').collect();
1237	let mut Cursor = 0usize;
1238	for (Index, Fragment) in Fragments.iter().enumerate() {
1239		if Fragment.is_empty() {
1240			continue;
1241		}
1242		if Index == 0 {
1243			if !Segment[Cursor..].starts_with(Fragment) {
1244				return false;
1245			}
1246			Cursor += Fragment.len();
1247			continue;
1248		}
1249		match Segment[Cursor..].find(Fragment) {
1250			Some(Offset) => Cursor += Offset + Fragment.len(),
1251			None => return false,
1252		}
1253	}
1254	if let Some(Last) = Fragments.last()
1255		&& !Last.is_empty()
1256	{
1257		return Segment.ends_with(Last);
1258	}
1259	true
1260}