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