1use crate::config::Config;
9use crate::env::{self, AgentEnv};
10use crate::exec::classify::{classify, Classified};
11use crate::exec::spawn::{CommandRunner, RealRunner, SpawnSpec};
12use crate::exec::{path as exec_path, resolve, ExecMode, Family, PassthroughReason};
13use crate::paths::CacheLayout;
14use crate::reduce;
15use crate::store::{write_logs, Db, RunRecord, StoredLogs};
16use crate::{repo, state, util};
17use std::ffi::{OsStr, OsString};
18use std::io::Write;
19use std::panic::AssertUnwindSafe;
20use std::path::{Path, PathBuf};
21use std::time::Instant;
22
23struct EmitPlan {
25 stdout: Vec<u8>,
26 stderr: Vec<u8>,
27}
28
29impl EmitPlan {
30 fn raw(outcome: &crate::exec::ExecOutcome) -> EmitPlan {
31 EmitPlan {
32 stdout: outcome.stdout.clone(),
33 stderr: outcome.stderr.clone(),
34 }
35 }
36 fn write(self) {
37 let _ = std::io::stdout().write_all(&self.stdout);
38 let _ = std::io::stdout().flush();
39 let _ = std::io::stderr().write_all(&self.stderr);
40 let _ = std::io::stderr().flush();
41 }
42}
43
44struct RunCtx {
46 repo_root: PathBuf,
47 cwd: PathBuf,
48 layout: CacheLayout,
49 config: Config,
50 session_id: String,
51}
52
53impl RunCtx {
54 fn resolve(cwd: &Path, agent: Option<&AgentEnv>) -> anyhow::Result<RunCtx> {
55 let (repo_root, layout, session_id) = if let Some(a) = agent {
56 (
57 a.repo_root.clone(),
58 CacheLayout::from_dir(a.cache_dir.clone()),
59 a.session_id.clone(),
60 )
61 } else {
62 let root = repo::detect_repo_root(cwd);
63 let layout = CacheLayout::for_repo(&root)?;
64 let sid = std::env::var(env::SESSION_ID).unwrap_or_else(|_| "no-session".to_string());
65 (root, layout, sid)
66 };
67 let config = Config::load(&repo_root)?;
68 Ok(RunCtx {
69 repo_root,
70 cwd: cwd.to_path_buf(),
71 layout,
72 config,
73 session_id,
74 })
75 }
76}
77
78pub fn run_shim(shim_name: &str, args: &[String]) -> anyhow::Result<i32> {
79 let started = Instant::now();
80 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
81 let agent = AgentEnv::from_current();
82
83 let dejavu_dir = current_exe_dir();
85 let shim_dir = agent
86 .as_ref()
87 .map(|a| a.shim_dir.clone())
88 .or_else(|| std::env::var_os(env::SHIM_DIR).map(PathBuf::from))
89 .unwrap_or_else(|| dejavu_dir.clone());
90
91 let path_os = std::env::var_os("PATH").unwrap_or_default();
92
93 let real = match resolve::resolve_real(
95 shim_name,
96 &resolve::ResolveEnv {
97 path: &path_os,
98 shim_dir: &shim_dir,
99 dejavu_dir: &dejavu_dir,
100 },
101 ) {
102 Some(p) => p,
103 None => {
104 eprintln!("{shim_name}: command not found");
105 return Ok(127);
106 }
107 };
108
109 let sanitized_path = exec_path::without_dir(&shim_dir, &path_os);
110
111 if env::is_disabled() {
113 return passthrough_exec(&real, args, &cwd, &sanitized_path);
114 }
115
116 let ctx = match RunCtx::resolve(&cwd, agent.as_ref()) {
118 Ok(c) => c,
119 Err(_) => return passthrough_exec(&real, args, &cwd, &sanitized_path),
120 };
121
122 let repo_disabled = state::is_repo_disabled(&ctx.layout);
123 let stdin_tty = crate::exec::interactive::stdin_is_tty();
124 let mut classified = classify(
125 shim_name,
126 args,
127 &ctx.config,
128 false,
129 repo_disabled,
130 stdin_tty,
131 );
132
133 if matches!(classified.mode, ExecMode::Optimize { .. }) {
137 use std::io::IsTerminal;
138 if !env::reduction_allowed(std::io::stdout().is_terminal()) {
139 classified.mode = ExecMode::Passthrough(PassthroughReason::NoAgentContext);
140 }
141 }
142
143 match &classified.mode {
144 ExecMode::Passthrough(reason) => {
145 let code = passthrough_exec(&real, args, &cwd, &sanitized_path)?;
146 record_passthrough(&ctx, shim_name, args, &classified, *reason, code, started);
147 Ok(code)
148 }
149 ExecMode::Optimize {
150 family,
151 command_key,
152 } => {
153 let spec = SpawnSpec {
154 program: real.clone(),
155 args: args.iter().map(OsString::from).collect(),
156 cwd: cwd.clone(),
157 env_path: sanitized_path.clone(),
158 capture: true,
159 inherit_stdin: !stdin_tty,
160 capture_limit: Some(ctx.config.max_raw_output_bytes as usize),
161 };
162 let outcome = match RealRunner.run(&spec) {
163 Ok(o) => o,
164 Err(_) => return passthrough_exec(&real, args, &cwd, &sanitized_path),
166 };
167
168 let real_code = outcome.exit_code;
169 let meta = OptimizedMeta {
170 shim_name: shim_name.to_string(),
171 args: args.to_vec(),
172 command_original: classified.command_original.clone(),
173 family: *family,
174 command_key: command_key.clone(),
175 };
176
177 let built = std::panic::catch_unwind(AssertUnwindSafe(|| {
179 finalize_optimized(&ctx, &meta, &outcome, started)
180 }));
181 let plan = match built {
182 Ok(Ok(plan)) => plan,
183 Ok(Err(err)) => {
184 eprintln!("dejavu internal error: {err}");
185 eprintln!("falling back to raw output");
186 EmitPlan::raw(&outcome)
187 }
188 Err(_) => {
189 eprintln!("dejavu internal error: panic during reduction");
190 eprintln!("falling back to raw output");
191 EmitPlan::raw(&outcome)
192 }
193 };
194 plan.write();
195 Ok(real_code)
196 }
197 }
198}
199
200struct OptimizedMeta {
201 shim_name: String,
202 args: Vec<String>,
203 command_original: String,
204 family: Family,
205 command_key: String,
206}
207
208fn finalize_optimized(
211 ctx: &RunCtx,
212 meta: &OptimizedMeta,
213 outcome: &crate::exec::ExecOutcome,
214 started: Instant,
215) -> anyhow::Result<EmitPlan> {
216 let cfg = &ctx.config;
217 let run_id = util::new_id();
218 let created_at = util::now_rfc3339();
219
220 let (red_stdout, red_stderr) = if cfg.redact_secrets {
222 (
223 reduce::redact::redact_bytes(&outcome.stdout).0,
224 reduce::redact::redact_bytes(&outcome.stderr).0,
225 )
226 } else {
227 (outcome.stdout.clone(), outcome.stderr.clone())
228 };
229
230 let git_head = repo::git_head(&ctx.repo_root);
231 let git_worktree = repo::git_worktree_hash(&ctx.repo_root);
232 let repo_root_s = ctx.repo_root.to_string_lossy().into_owned();
233 let cwd_s = ctx.cwd.to_string_lossy().into_owned();
234
235 let db = Db::open(&ctx.layout.db())?;
236 let reduced = reduce::reduce(
237 &db,
238 cfg,
239 &reduce::ReduceInput {
240 run_id: &run_id,
241 created_at: &created_at,
242 repo_root: &repo_root_s,
243 cwd: &cwd_s,
244 shim_name: &meta.shim_name,
245 command_original: &meta.command_original,
246 command_family: meta.family.as_str(),
247 command_key: &meta.command_key,
248 exit_code: outcome.exit_code,
249 git_head: git_head.as_deref(),
250 git_worktree_hash: git_worktree.as_deref(),
251 redacted_stdout: &red_stdout,
252 redacted_stderr: &red_stderr,
253 },
254 )?;
255
256 let stored = if cfg.store_raw_outputs {
259 write_logs(
260 &ctx.layout,
261 &run_id,
262 &red_stdout,
263 &red_stderr,
264 Some(&reduced.normalized),
265 cfg.max_raw_output_bytes as usize,
266 )?
267 } else {
268 let mut s = StoredLogs::default();
269 let _ = std::fs::create_dir_all(ctx.layout.logs_dir());
270 let np = ctx.layout.normalized_log(&run_id);
271 if std::fs::write(&np, &reduced.normalized).is_ok() {
272 s.normalized_path = Some(np);
273 }
274 s
275 };
276
277 let raw_stdout = outcome.stdout.len() as i64;
278 let raw_stderr = outcome.stderr.len() as i64;
279 let emitted_bytes = (reduced.emit_stdout.len() + reduced.emit_stderr.len()) as i64;
280
281 let record = RunRecord {
282 id: run_id,
283 session_id: ctx.session_id.clone(),
284 created_at,
285 repo_root: repo_root_s,
286 cwd: cwd_s,
287 shim_name: meta.shim_name.clone(),
288 argv_json: serde_json::to_string(&meta.args).unwrap_or_else(|_| "[]".to_string()),
289 command_original: meta.command_original.clone(),
290 command_family: meta.family.as_str().to_string(),
291 command_key: meta.command_key.clone(),
292 classification: reduced.classification.as_str().to_string(),
293 exit_code: outcome.exit_code as i64,
294 duration_ms: outcome.duration.as_millis() as i64,
295 overhead_ms: overhead_ms(started, outcome),
296 stdout_path: path_str(&stored.stdout_path),
297 stderr_path: path_str(&stored.stderr_path),
298 normalized_path: path_str(&stored.normalized_path),
299 raw_stdout_bytes: raw_stdout,
300 raw_stderr_bytes: raw_stderr,
301 raw_total_bytes: raw_stdout + raw_stderr,
302 emitted_bytes,
303 estimated_raw_tokens: reduced.estimated_raw_tokens,
304 estimated_emitted_tokens: reduced.estimated_emitted_tokens,
305 estimated_saved_tokens: reduced.estimated_saved_tokens,
306 normalized_hash: Some(reduced.normalized_hash),
307 stdout_hash: Some(util::sha256_hex(&red_stdout)),
308 stderr_hash: Some(util::sha256_hex(&red_stderr)),
309 git_head,
310 git_worktree_hash: git_worktree,
311 comparison_base_run_id: reduced.comparison_base_run_id,
312 comparison_result: reduced.comparison_result,
313 summary: reduced.summary,
314 full_output_requested: 0,
315 internal_error: None,
316 };
317
318 db.insert_run(&record)?;
319 let _ = db.accumulate_session_tokens(
320 &ctx.session_id,
321 reduced.estimated_raw_tokens,
322 reduced.estimated_emitted_tokens,
323 reduced.estimated_saved_tokens,
324 );
325
326 Ok(EmitPlan {
327 stdout: reduced.emit_stdout,
328 stderr: reduced.emit_stderr,
329 })
330}
331
332fn path_str(path: &Option<std::path::PathBuf>) -> Option<String> {
333 path.as_ref().map(|p| p.to_string_lossy().into_owned())
334}
335
336fn record_passthrough(
337 ctx: &RunCtx,
338 shim_name: &str,
339 args: &[String],
340 classified: &Classified,
341 _reason: PassthroughReason,
342 exit_code: i32,
343 started: Instant,
344) {
345 let record = RunRecord {
346 id: util::new_id(),
347 session_id: ctx.session_id.clone(),
348 created_at: util::now_rfc3339(),
349 repo_root: ctx.repo_root.to_string_lossy().into_owned(),
350 cwd: ctx.cwd.to_string_lossy().into_owned(),
351 shim_name: shim_name.to_string(),
352 argv_json: serde_json::to_string(args).unwrap_or_else(|_| "[]".to_string()),
353 command_original: classified.command_original.clone(),
354 command_family: "passthrough".to_string(),
355 command_key: format!("passthrough:{shim_name}"),
356 classification: "passthrough".to_string(),
357 exit_code: exit_code as i64,
358 duration_ms: 0,
359 overhead_ms: started.elapsed().as_millis() as i64,
360 stdout_path: None,
361 stderr_path: None,
362 normalized_path: None,
363 raw_stdout_bytes: 0,
364 raw_stderr_bytes: 0,
365 raw_total_bytes: 0,
366 emitted_bytes: 0,
367 estimated_raw_tokens: 0,
368 estimated_emitted_tokens: 0,
369 estimated_saved_tokens: 0,
370 normalized_hash: None,
371 stdout_hash: None,
372 stderr_hash: None,
373 git_head: None,
374 git_worktree_hash: None,
375 comparison_base_run_id: None,
376 comparison_result: "passthrough".to_string(),
377 summary: None,
378 full_output_requested: 0,
379 internal_error: None,
380 };
381 let _ = persist(ctx, &record, 0, 0, 0);
383}
384
385fn persist(
388 ctx: &RunCtx,
389 record: &RunRecord,
390 raw_tokens: i64,
391 emitted_tokens: i64,
392 saved_tokens: i64,
393) -> anyhow::Result<()> {
394 let db = Db::open(&ctx.layout.db())?;
395 db.insert_run(record)?;
396 let _ = db.accumulate_session_tokens(&ctx.session_id, raw_tokens, emitted_tokens, saved_tokens);
397 Ok(())
398}
399
400fn passthrough_exec(
401 program: &Path,
402 args: &[String],
403 cwd: &Path,
404 env_path: &OsStr,
405) -> anyhow::Result<i32> {
406 let spec = SpawnSpec {
407 program: program.to_path_buf(),
408 args: args.iter().map(OsString::from).collect(),
409 cwd: cwd.to_path_buf(),
410 env_path: env_path.to_os_string(),
411 capture: false,
412 inherit_stdin: true,
413 capture_limit: None,
414 };
415 Ok(RealRunner.run(&spec)?.exit_code)
416}
417
418fn overhead_ms(started: Instant, outcome: &crate::exec::ExecOutcome) -> i64 {
419 let total = started.elapsed().as_millis() as i64;
420 let cmd = outcome.duration.as_millis() as i64;
421 (total - cmd).max(0)
422}
423
424fn current_exe_dir() -> PathBuf {
425 std::env::current_exe()
426 .ok()
427 .and_then(|p| p.parent().map(Path::to_path_buf))
428 .unwrap_or_else(|| PathBuf::from("."))
429}