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