1use std::collections::VecDeque;
4use std::fmt::Write as FmtWrite;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::sync::{Arc, Mutex as StdMutex, OnceLock};
8use std::time::Instant;
9use tokio::sync::Mutex;
10use tracing::{info, instrument, warn};
11
12use crate::errors::{Result, WinxError};
13use crate::runtime::{EmbeddedShellRuntime, ShellRuntime, ShellSessionTransition};
14use crate::state::bash_state::{generate_thread_id, BashState};
15use crate::types::{
16 normalize_thread_id, AllowedCommands, AllowedGlobs, BashCommandMode, BashMode,
17 CodeWriterConfig, FileEditMode, Initialize, InitializeType, Modes, WriteIfEmptyMode,
18};
19use crate::utils::mmap::read_file_to_string;
20use crate::utils::path::{ensure_directory_exists, expand_user, validate_path_in_workspace};
21
22const POLICY_WARNING_CACHE_CAPACITY: usize = 128;
23static WARNED_POLICY_CONFIGURATIONS: OnceLock<StdMutex<VecDeque<Vec<String>>>> = OnceLock::new();
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub(crate) enum InitializeTransition {
27 Created,
28 AttachedExisting,
29 ModeChanged,
30 ShellReset,
31 ResetSkippedHealthy,
32 WorkspaceChanged,
33}
34
35impl InitializeTransition {
36 pub(crate) const fn as_str(self) -> &'static str {
37 match self {
38 Self::Created => "created",
39 Self::AttachedExisting => "attached_existing",
40 Self::ModeChanged => "mode_changed",
41 Self::ShellReset => "shell_reset",
42 Self::ResetSkippedHealthy => "reset_skipped_healthy",
43 Self::WorkspaceChanged => "workspace_changed",
44 }
45 }
46
47 pub(crate) const fn reused(self) -> bool {
48 matches!(self, Self::AttachedExisting | Self::ResetSkippedHealthy)
49 }
50}
51
52#[derive(Debug)]
53pub(crate) struct InitializeOutcome {
54 pub(crate) text: String,
55 pub(crate) transition: InitializeTransition,
56 pub(crate) recovered_missing_session: bool,
57 pub(crate) context_bytes: usize,
58 pub(crate) guidelines_bytes: usize,
59 pub(crate) initial_files_count: usize,
60 pub(crate) compact_response: bool,
61 pub(crate) code_writer_policy_strength: Option<&'static str>,
62 pub(crate) shell_spawners_present: bool,
63 pub(crate) shell_reset_retry_after_seconds: Option<u64>,
64 pub(crate) temporary_artifact_dir: PathBuf,
65 pub(crate) temporary_artifact_ttl_seconds: u64,
66 pub(crate) temporary_artifact_max_bytes: u64,
67 pub(crate) temporary_artifact_max_session_bytes: u64,
68 pub(crate) temporary_artifact_max_file_bytes: u64,
69 pub(crate) temporary_artifact_max_files: usize,
70 pub(crate) temporary_artifact_stale_pruned_files: usize,
71 pub(crate) temporary_artifact_stale_pruned_bytes: u64,
72 pub(crate) temporary_code_map_max_calls: usize,
73 pub(crate) temporary_code_map_max_unique_files: usize,
74}
75
76fn create_playground_dir(thread_id: &str) -> Result<PathBuf> {
79 #[cfg(unix)]
80 let owner = crate::os::unix::effective_uid().to_string();
81 #[cfg(not(unix))]
82 let owner = std::process::id().to_string();
83
84 let dir = std::env::temp_dir().join(format!("winx-playground-{owner}-{thread_id}"));
85 ensure_directory_exists(&dir)?;
86 Ok(dir)
87}
88
89fn command_exists(cmd: &str) -> bool {
91 std::process::Command::new("sh")
92 .args(["-c", &format!("command -v {cmd}")])
93 .output()
94 .is_ok_and(|o| o.status.success())
95}
96
97fn code_writer_state(
98 config: &CodeWriterConfig,
99 workspace_root: &Path,
100) -> (BashCommandMode, FileEditMode, WriteIfEmptyMode) {
101 let mut config = config.clone();
102 config.allowed_globs.normalize();
104 config.allowed_commands.normalize();
105 config.update_relative_globs(&workspace_root.to_string_lossy());
106
107 (
108 BashCommandMode {
109 bash_mode: BashMode::NormalMode,
110 allowed_commands: config.allowed_commands,
111 },
112 FileEditMode { allowed_globs: config.allowed_globs.clone() },
113 WriteIfEmptyMode { allowed_globs: config.allowed_globs },
114 )
115}
116
117fn mode_to_state(
118 mode: Modes,
119 config: Option<&CodeWriterConfig>,
120 workspace_root: &Path,
121) -> Result<(BashCommandMode, FileEditMode, WriteIfEmptyMode)> {
122 match mode {
123 Modes::Wcgw => Ok((
124 BashCommandMode {
125 bash_mode: BashMode::NormalMode,
126 allowed_commands: AllowedCommands::All("all".to_string()),
127 },
128 FileEditMode { allowed_globs: AllowedGlobs::All("all".to_string()) },
129 WriteIfEmptyMode { allowed_globs: AllowedGlobs::All("all".to_string()) },
130 )),
131 Modes::Architect => Ok((
132 BashCommandMode {
133 bash_mode: BashMode::RestrictedMode,
134 allowed_commands: AllowedCommands::List(
135 crate::utils::bash_parser::architect_allowed_commands(),
136 ),
137 },
138 FileEditMode { allowed_globs: AllowedGlobs::List(vec![]) },
139 WriteIfEmptyMode { allowed_globs: AllowedGlobs::List(vec![]) },
140 )),
141 Modes::CodeWriter => {
142 let config = config.ok_or_else(|| {
143 WinxError::ArgumentParseError(
144 "code_writer_config is required when mode_name is code_writer.".to_string(),
145 )
146 })?;
147 Ok(code_writer_state(config, workspace_root))
148 }
149 }
150}
151
152fn active_code_writer_config(state: &BashState) -> Option<CodeWriterConfig> {
153 (state.mode == Modes::CodeWriter).then(|| CodeWriterConfig {
154 allowed_globs: state.file_edit_mode.allowed_globs.clone(),
155 allowed_commands: state.bash_command_mode.allowed_commands.clone(),
156 })
157}
158
159fn active_code_writer_policy(state: &BashState) -> (Option<&'static str>, Vec<String>) {
160 if state.mode != Modes::CodeWriter {
161 return (None, Vec::new());
162 }
163
164 match &state.bash_command_mode.allowed_commands {
165 AllowedCommands::All(value) if value == "all" => (Some("unrestricted"), Vec::new()),
166 AllowedCommands::All(_) => (Some("restricted"), Vec::new()),
167 AllowedCommands::List(commands) => {
168 let bypass = crate::utils::bash_parser::detect_allowlist_bypass(commands);
169 let strength = if bypass.is_empty() { "restricted" } else { "weak" };
170 (Some(strength), bypass)
171 }
172 }
173}
174
175fn warn_code_writer_policy_once(state: &BashState, bypass: &[String]) {
176 if bypass.is_empty() {
177 return;
178 }
179 let AllowedCommands::List(commands) = &state.bash_command_mode.allowed_commands else {
180 return;
181 };
182
183 let cache = WARNED_POLICY_CONFIGURATIONS
184 .get_or_init(|| StdMutex::new(VecDeque::with_capacity(POLICY_WARNING_CACHE_CAPACITY)));
185 let mut cache = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
186 if cache.iter().any(|seen| seen == commands) {
187 return;
188 }
189 if cache.len() == POLICY_WARNING_CACHE_CAPACITY {
190 cache.pop_front();
191 }
192 cache.push_back(commands.clone());
193 drop(cache);
194
195 warn!(
196 commands = ?bypass,
197 "code_writer allowlist includes shell-spawning commands; the command allowlist is \
198 effectively bypassable and does not sandbox the agent"
199 );
200}
201
202fn append_code_writer_policy_warning(response: &mut String, bypass: &[String]) {
203 if bypass.is_empty() {
204 return;
205 }
206 let _ = writeln!(
207 response,
208 "\n⚠️ SECURITY: code_writer allowlist includes shell/eval commands ({}). \
209 They execute arbitrary code from string arguments (e.g. `bash -c …`, \
210 `find -exec …`), so the command allowlist is effectively bypassable and \
211 does NOT sandbox the agent. Drop them if you intended a hard restriction.",
212 bypass.join(", ")
213 );
214}
215
216fn read_initial_files_simple(files: &[String], workspace: &std::path::Path) -> String {
217 let mut output = String::new();
218 for file_path in files {
219 let expanded = expand_user(file_path);
220 let path = if std::path::Path::new(&expanded).is_absolute() {
221 PathBuf::from(&expanded)
222 } else {
223 workspace.join(&expanded)
224 };
225
226 if let Ok(validated) = validate_path_in_workspace(&path, workspace) {
227 if validated.exists() && validated.is_file() {
228 if let Ok(content) = read_file_to_string(&validated, 10_000_000) {
229 let _ = write!(output, "\n{file_path}\n```\n{content}\n```\n");
230 }
231 }
232 }
233 }
234 output
235}
236
237fn prepare_workspace(
238 initialize: &Initialize,
239 thread_id: &str,
240 response: &mut String,
241) -> Result<PathBuf> {
242 let workspace_path_str = expand_user(&initialize.any_workspace_path);
243 if workspace_path_str.is_empty() {
244 let playground = create_playground_dir(thread_id)?;
247 let _ = writeln!(
248 response,
249 "No workspace path provided; created a playground at {}",
250 playground.display()
251 );
252 return Ok(playground);
253 }
254
255 let workspace_path = PathBuf::from(&workspace_path_str);
256 let mut folder_to_start = workspace_path.clone();
257
258 if workspace_path.exists() {
259 if workspace_path.is_file() {
260 folder_to_start = workspace_path.parent().unwrap_or(&workspace_path).to_path_buf();
261 let _ =
262 writeln!(response, "Using parent directory of file: {}", folder_to_start.display());
263 } else if workspace_path.is_dir() {
264 let _ = writeln!(response, "Using workspace directory: {}", folder_to_start.display());
265 }
266 } else if workspace_path.is_absolute() {
267 ensure_directory_exists(&workspace_path).map_err(|e| {
268 WinxError::WorkspacePathError(format!("Failed to create workspace: {e}"))
269 })?;
270 let _ = writeln!(response, "Created workspace directory: {}", workspace_path.display());
271 }
272
273 if folder_to_start.exists() {
277 if let Ok(canonical) = folder_to_start.canonicalize() {
278 folder_to_start = canonical;
279 }
280 }
281
282 Ok(folder_to_start)
283}
284
285fn initialize_thread_id(initialize: &Initialize) -> String {
286 let thread_id = normalize_thread_id(&initialize.thread_id);
287 if thread_id.is_empty() {
288 generate_thread_id()
289 } else {
290 thread_id
291 }
292}
293
294fn validate_thread_id(initialize: &Initialize) -> Result<()> {
295 if initialize.init_type != InitializeType::FirstCall
296 && normalize_thread_id(&initialize.thread_id).is_empty()
297 {
298 return Err(WinxError::ThreadIdMismatch(
299 "Thread id should be provided if type != 'first_call', including when resetting."
300 .to_string(),
301 ));
302 }
303
304 Ok(())
305}
306
307fn load_guidelines(workspace: &Path) -> String {
308 let mut output = String::new();
309 let mut candidates = Vec::new();
310 if let Some(home) = home::home_dir() {
311 candidates.push(home.join(".winx").join("AGENTS.md"));
312 candidates.push(home.join(".winx").join("CLAUDE.md"));
313 candidates.push(home.join(".wcgw").join("AGENTS.md"));
314 candidates.push(home.join(".wcgw").join("CLAUDE.md"));
315 }
316 candidates.push(workspace.join("AGENTS.md"));
317 candidates.push(workspace.join("CLAUDE.md"));
318
319 for path in candidates {
320 if path.is_file() {
321 if let Ok(content) = fs::read_to_string(&path) {
322 let _ = writeln!(output, "\n## {}\n{}", path.display(), content);
323 }
324 }
325 }
326 output
327}
328
329pub async fn handle_tool_call(
330 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
331 initialize: Initialize,
332) -> Result<String> {
333 handle_tool_call_with_runtime(&EmbeddedShellRuntime, bash_state_arc, initialize).await
334}
335
336pub async fn handle_tool_call_with_runtime(
337 runtime: &dyn ShellRuntime,
338 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
339 initialize: Initialize,
340) -> Result<String> {
341 Ok(handle_tool_call_with_runtime_detailed(runtime, bash_state_arc, initialize).await?.text)
342}
343
344#[instrument(level = "info", skip(runtime, bash_state_arc, initialize))]
345#[allow(clippy::too_many_lines)]
346pub(crate) async fn handle_tool_call_with_runtime_detailed(
347 runtime: &dyn ShellRuntime,
348 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
349 mut initialize: Initialize,
350) -> Result<InitializeOutcome> {
351 let mut response = String::new();
352 let mut context_bytes = 0;
353 let mut guidelines_bytes = 0;
354 let initial_files_count = initialize.initial_files_to_read.len();
355
356 info!("Initialize called for workspace: {}", initialize.any_workspace_path);
357
358 validate_thread_id(&initialize)?;
359 let thread_id = initialize_thread_id(&initialize);
360 let mut bash_state_guard = bash_state_arc.lock().await;
361 let recovered_request = if bash_state_guard.is_none() {
362 match initialize.init_type {
363 InitializeType::UserAskedModeChange => Some("user_asked_mode_change"),
364 InitializeType::ResetShell => Some("reset_shell"),
365 InitializeType::FirstCall | InitializeType::UserAskedChangeWorkspace => None,
366 }
367 } else {
368 None
369 };
370 if let Some(requested_transition) = recovered_request {
371 info!(requested_transition, "Initialize recovered a missing live session as first_call");
372 initialize.init_type = InitializeType::FirstCall;
373 }
374 if initialize.init_type != InitializeType::FirstCall && bash_state_guard.is_none() {
375 return Err(WinxError::BashStateNotInitialized);
376 }
377 let local_attach = initialize.init_type == InitializeType::FirstCall
378 && bash_state_guard
379 .as_ref()
380 .is_some_and(|state| state.initialized && state.current_thread_id == thread_id);
381 let folder_to_start = if local_attach {
382 bash_state_guard
383 .as_ref()
384 .map_or_else(|| PathBuf::from("/tmp"), |state| state.workspace_root.clone())
385 } else {
386 prepare_workspace(&initialize, &thread_id, &mut response)?
387 };
388 let requested_mode = Modes::from(&initialize.mode_name);
389 let preserve_active_mode =
390 local_attach || initialize.init_type == InitializeType::UserAskedChangeWorkspace;
391 let (mode, bash_command_mode, file_edit_mode, write_if_empty_mode) = if preserve_active_mode {
392 let Some(state) = bash_state_guard.as_ref() else {
393 return Err(WinxError::BashStateNotInitialized);
394 };
395 (
396 state.mode,
397 state.bash_command_mode.clone(),
398 state.file_edit_mode.clone(),
399 state.write_if_empty_mode.clone(),
400 )
401 } else {
402 let (bash_command_mode, file_edit_mode, write_if_empty_mode) = mode_to_state(
403 requested_mode,
404 initialize.code_writer_config.as_ref(),
405 &folder_to_start,
406 )?;
407 (requested_mode, bash_command_mode, file_edit_mode, write_if_empty_mode)
408 };
409
410 let transition;
411 let mut compact_response = false;
412 let mut shell_reset_retry_after_seconds = None;
413
414 match initialize.init_type {
415 InitializeType::FirstCall => {
416 let configured = if local_attach {
417 let Some(state) = bash_state_guard.as_mut() else {
418 return Err(WinxError::BashStateNotInitialized);
419 };
420 let mut configured = runtime.attach_session(state).await?;
423 configured.attached_existing = true;
424 configured
425 } else {
426 let mut new_bash_state = BashState::new();
427 new_bash_state.current_thread_id.clone_from(&thread_id);
428 new_bash_state.mode = mode;
429 new_bash_state.bash_command_mode = bash_command_mode;
430 new_bash_state.file_edit_mode = file_edit_mode;
431 new_bash_state.write_if_empty_mode = write_if_empty_mode;
432 new_bash_state.initialized = true;
433
434 let resumed_context = if initialize.task_id_to_resume.is_empty() {
435 None
436 } else {
437 crate::tools::context_save::load_saved_context(&initialize.task_id_to_resume)?
438 };
439
440 if let Some((memory_data, snapshot)) = &resumed_context {
441 if let Some(snapshot) = snapshot {
442 new_bash_state.apply_snapshot(snapshot);
443 new_bash_state.current_thread_id.clone_from(&thread_id);
444 }
445 let _ = writeln!(
446 response,
447 "\n# Resumed task {}\nFollowing is the retrieved task context:\n{}",
448 initialize.task_id_to_resume, memory_data
449 );
450 }
451
452 if resumed_context.as_ref().and_then(|(_, snapshot)| snapshot.as_ref()).is_none() {
456 let resumed_root = resumed_context
457 .as_ref()
458 .and_then(|(memory, _)| {
459 crate::tools::context_save::extract_project_root(memory)
460 })
461 .filter(|root| root.exists());
462 let target = resumed_root.as_deref().unwrap_or(folder_to_start.as_path());
463 if target.exists() {
464 new_bash_state.update_cwd(target)?;
465 new_bash_state.update_workspace_root(target)?;
466 }
467 }
468 let configured = runtime
469 .configure_session(&mut new_bash_state, ShellSessionTransition::FirstCall)
470 .await?;
471 *bash_state_guard = Some(new_bash_state);
472 configured
473 };
474
475 transition = if configured.attached_existing {
476 InitializeTransition::AttachedExisting
477 } else {
478 InitializeTransition::Created
479 };
480 compact_response = configured.attached_existing
481 && initialize.initial_files_to_read.is_empty()
482 && initialize.task_id_to_resume.is_empty();
483
484 if compact_response {
485 response.clear();
486 let state = bash_state_guard.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
487 let _ = writeln!(
488 response,
489 "Attached to the existing durable Winx session; PTY, cwd, mode, and prior \
490 context were preserved.\ncwd={}\nmode={}",
491 state.cwd.display(),
492 state.mode
493 );
494 if let Some(attach_hint) = configured.attach_hint.as_deref() {
495 let _ = writeln!(response, "Attach terminal: {attach_hint}");
496 }
497 response.push_str(
498 "Context and instructions are unchanged. Continue with the existing thread; \
499 use an explicit mode/workspace change or reset only when intended.\n",
500 );
501 } else {
502 if configured.attached_existing {
503 response.push_str(
504 "\nAttached to the existing durable session for this principal/workspace; \
505 the guardian-owned PTY and cwd were preserved. Use `reset_shell` or an \
506 explicit workspace/mode change when replacement is intended.\n",
507 );
508 }
509
510 let _ = write!(
511 response,
512 "\n# Environment\nSystem: {}\nMachine: {}\nInitialized in directory: {}\n",
513 std::env::consts::OS,
514 std::env::consts::ARCH,
515 bash_state_guard
516 .as_ref()
517 .map_or(folder_to_start.as_path(), |state| state.cwd.as_path())
518 .display()
519 );
520
521 if command_exists("rg") {
522 let _ = writeln!(
523 response,
524 "\n# Available commands\nUse ripgrep `rg` instead of `grep`/`find -name` — \
525 it's much faster and respects .gitignore."
526 );
527 }
528
529 if let Some(attach_hint) = configured.attach_hint.as_deref() {
530 let _ = writeln!(response, "\nAttach terminal: {attach_hint}");
531 }
532
533 let active_config = bash_state_guard.as_ref().and_then(active_code_writer_config);
536 let active_mode = bash_state_guard.as_ref().map_or(mode, |state| state.mode);
537 let _ = writeln!(
538 response,
539 "\n{}",
540 crate::utils::mode_prompts::mode_prompt(active_mode, active_config.as_ref())
541 );
542
543 let active_workspace = bash_state_guard
544 .as_ref()
545 .map_or(folder_to_start.as_path(), |state| state.workspace_root.as_path());
546
547 let guidelines = load_guidelines(active_workspace);
548 guidelines_bytes = guidelines.len();
549 if !guidelines.is_empty() {
550 let _ = writeln!(response, "\n# Agent guidelines\n{guidelines}");
551 }
552
553 if let Ok((repo_context, _)) =
554 crate::utils::repo::get_repo_context(active_workspace)
555 {
556 context_bytes = repo_context.len();
557 let _ = writeln!(response, "\n# Workspace structure\n{repo_context}");
558 }
559
560 if !initialize.initial_files_to_read.is_empty() {
561 let content = read_initial_files_simple(
562 &initialize.initial_files_to_read,
563 active_workspace,
564 );
565 if !content.is_empty() {
566 let _ = writeln!(response, "\n# Requested files\n{content}");
567 }
568 }
569 }
570 if let Some(requested_transition) = recovered_request {
571 let _ = writeln!(
572 response,
573 "\nRecovered missing live session: `{requested_transition}` arrived after \
574 the adapter no longer had session state. Winx safely performed `first_call` \
575 with the same thread and intended workspace; continue with the returned \
576 thread_id/workspace_root pair."
577 );
578 }
579 }
580 InitializeType::UserAskedModeChange => {
581 transition = InitializeTransition::ModeChanged;
582 if let Some(state) = bash_state_guard.as_mut() {
583 state.mode = mode;
584 state.bash_command_mode = bash_command_mode;
585 state.file_edit_mode = file_edit_mode;
586 state.write_if_empty_mode = write_if_empty_mode;
587 runtime.configure_session(state, ShellSessionTransition::ModeChange).await?;
588 let _ = writeln!(response, "Changed mode to: {mode:?}");
589 let active_config = active_code_writer_config(state);
590 let _ = writeln!(
591 response,
592 "\n{}",
593 crate::utils::mode_prompts::mode_prompt(mode, active_config.as_ref())
594 );
595 } else {
596 return Err(WinxError::BashStateNotInitialized);
597 }
598 }
599 InitializeType::ResetShell => {
600 if let Some(state) = bash_state_guard.as_mut() {
601 if let Some(retry_after) = state.shell_reset_retry_after(Instant::now()) {
602 transition = InitializeTransition::ResetSkippedHealthy;
603 compact_response = true;
604 let retry_after_seconds = retry_after.as_secs().max(1);
605 shell_reset_retry_after_seconds = Some(retry_after_seconds);
606 let _ = writeln!(
607 response,
608 "Skipped redundant reset_shell: the PTY was replaced recently and no \
609 shell-runtime failure has occurred since. The existing PTY, cwd, mode, \
610 and context were preserved. Continue with BashCommand; retry reset_shell \
611 only after a shell-runtime failure or in {retry_after_seconds} seconds."
612 );
613 } else {
614 state.mode = mode;
615 state.bash_command_mode = bash_command_mode;
616 state.file_edit_mode = file_edit_mode;
617 state.write_if_empty_mode = write_if_empty_mode;
618 if let Err(error) =
619 runtime.configure_session(state, ShellSessionTransition::Reset).await
620 {
621 state.record_shell_runtime_failure();
622 return Err(error);
623 }
624 state.record_shell_reset(Instant::now());
625 transition = InitializeTransition::ShellReset;
626 response.push_str("Reset shell (new PTY created)\n");
627 }
628 } else {
629 return Err(WinxError::BashStateNotInitialized);
630 }
631 }
632 InitializeType::UserAskedChangeWorkspace => {
633 transition = InitializeTransition::WorkspaceChanged;
634 if let Some(state) = bash_state_guard.as_mut() {
635 if folder_to_start.exists() {
636 state.update_cwd(&folder_to_start)?;
637 state.update_workspace_root(&folder_to_start)?;
638 runtime
639 .configure_session(state, ShellSessionTransition::WorkspaceChange)
640 .await?;
641 let _ =
642 writeln!(response, "Changed workspace to: {}", folder_to_start.display());
643 } else {
644 let _ = writeln!(
645 response,
646 "Warning: Workspace path {} does not exist",
647 folder_to_start.display()
648 );
649 }
650 } else {
651 return Err(WinxError::BashStateNotInitialized);
652 }
653 }
654 }
655
656 let state = bash_state_guard.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
657 let temporary_artifact =
658 crate::utils::agent_temp::prepare_session(&state.workspace_root, &state.current_thread_id);
659 let temporary_artifact_instruction = if compact_response {
660 format!(
661 "Use temporary_artifact_dir={} for a small stable set of session-local derived \
662 helpers; BashCommand exports the same path as WINX_TEMP_DIR. Reuse helpers instead \
663 of creating CodeMap carriers; helper maps accept one existing file and are limited \
664 to {} unique files / {} calls. Near its quota Winx reclaims only helpers inactive for \
665 the advertised TTL; if it remains over budget, explicitly remove obsolete helpers \
666 before running ordinary commands.",
667 temporary_artifact.directory.display(),
668 crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_UNIQUE_FILES,
669 crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_CALLS,
670 )
671 } else {
672 format!(
673 "Use temporary_artifact_dir={} for session-local derived helpers only; keep names \
674 short, reuse one stable file per purpose, preserve source-path/line provenance, and \
675 treat helpers as non-canonical. Never create a carrier solely for CodeMap. The \
676 directory is created on demand, limited to {} files / {} bytes for this session, and \
677 expired after {} seconds of inactivity. Every BashCommand PTY exports the same path \
678 as WINX_TEMP_DIR; shell-generated helpers must stay beneath it. Winx audits actual \
679 usage after Bash. At a high-water mark it reclaims only helpers inactive for the full \
680 TTL, never fresh artifacts; if the hard limit remains exceeded, explicitly inspect and \
681 remove obsolete helpers before ordinary commands can continue. \
682 Helper CodeMap accepts one existing file and is limited to {} unique files / {} calls \
683 in this live session.",
684 temporary_artifact.directory.display(),
685 temporary_artifact.max_session_files,
686 temporary_artifact.max_session_bytes,
687 temporary_artifact.ttl_seconds,
688 crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_UNIQUE_FILES,
689 crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_CALLS,
690 )
691 };
692 let _ = writeln!(
693 response,
694 "\nUse thread_id={thread_id} for all winx tool calls.\nUse workspace_root={} for all winx tool calls.\n{temporary_artifact_instruction}\nBefore every reuse, confirm workspace_root still matches the user's current project. Keep this exact pair together. workspace_root identifies this project session; it does not restrict target paths allowed by policy.",
695 state.workspace_root.display(),
696 );
697
698 let (code_writer_policy_strength, bypass) =
699 bash_state_guard.as_ref().map_or((None, Vec::new()), active_code_writer_policy);
700 if let Some(state) = bash_state_guard.as_ref() {
701 warn_code_writer_policy_once(state, &bypass);
702 }
703 if compact_response {
704 if let Some(strength) = code_writer_policy_strength {
705 let _ = writeln!(
706 response,
707 "code_writer_policy={strength} shell_spawners_present={}",
708 !bypass.is_empty()
709 );
710 }
711 } else {
712 append_code_writer_policy_warning(&mut response, &bypass);
713 crate::utils::orchestration::append_initialize_instructions(&mut response);
714 }
715
716 Ok(InitializeOutcome {
717 text: response,
718 transition,
719 recovered_missing_session: recovered_request.is_some(),
720 context_bytes,
721 guidelines_bytes,
722 initial_files_count,
723 compact_response,
724 code_writer_policy_strength,
725 shell_spawners_present: !bypass.is_empty(),
726 shell_reset_retry_after_seconds,
727 temporary_artifact_dir: temporary_artifact.directory,
728 temporary_artifact_ttl_seconds: temporary_artifact.ttl_seconds,
729 temporary_artifact_max_bytes: temporary_artifact.max_total_bytes,
730 temporary_artifact_max_session_bytes: temporary_artifact.max_session_bytes,
731 temporary_artifact_max_file_bytes: temporary_artifact.max_file_bytes,
732 temporary_artifact_max_files: temporary_artifact.max_session_files,
733 temporary_artifact_stale_pruned_files: temporary_artifact.stale_pruned_files,
734 temporary_artifact_stale_pruned_bytes: temporary_artifact.stale_pruned_bytes,
735 temporary_code_map_max_calls: crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_CALLS,
736 temporary_code_map_max_unique_files:
737 crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_UNIQUE_FILES,
738 })
739}