Repository navigation
feat(task): replay cached task output - #11347
Conversation
📝 WalkthroughWalkthroughTask artifact caching now stores ordered, redacted stdout/stderr streams in cache format v2, captures output during execution, and replays it according to configured output modes. Raw tasks bypass caching, with documentation and end-to-end coverage updated. ChangesTask artifact output cache
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant TaskExecutor
participant CmdLineRunner
participant TaskArtifactCache
participant OutputHandler
TaskExecutor->>CmdLineRunner: capture redacted stdout/stderr
CmdLineRunner-->>TaskExecutor: return observed output lines
TaskExecutor->>TaskArtifactCache: store output in cache manifest
TaskExecutor->>TaskArtifactCache: restore cached artifact
TaskArtifactCache-->>TaskExecutor: return cached streams
TaskExecutor->>OutputHandler: replay streams using selected mode
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 129fdf5. Configure here.
Greptile SummaryThis PR adds redacted, stream-tagged task-output capture and replay for local artifact cache hits.
Confidence Score: 3/5The PR is not yet safe to merge because cache-eligible tasks can still receive EOF instead of the caller's stdin when redactions or other capture-capable output modes are used. The manifest-reread issue now falls through to execution, but the stdin fix only inherits stdin in the quiet/interleave branch when raw mode is active or no redactions are configured; normal captured execution otherwise retains an unwritten stdin pipe. Files Needing Attention: src/task/task_executor.rs, src/cmd.rs Important Files Changed
Reviews (3): Last reviewed commit: "fix(task): satisfy cached output clippy ..." | Re-trigger Greptile |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
src/task/task_executor.rs (2)
483-502: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winArtifact cache key material is computed even for raw/interactive tasks that will discard it.
TaskArtifactCache::new()does source hashing, tool-version listing, and key derivation before the code checksself.raw(Some(task))and throws the result away. For a task withcache.enabled = truethat's always run with--raw(or isinteractive), this work is repeated on every execution for no benefit. Move the raw/interactive check before constructing the cache to skip the wasted work.♻️ Suggested reordering
- let artifact_cache = if task.cache.as_ref().is_some_and(|cache| cache.enabled) { + let artifact_cache = if task.cache.as_ref().is_some_and(|cache| cache.enabled) + && !self.dry_run + && !self.raw(Some(task)) + { match TaskArtifactCache::new( task, config, &ts, &env, &task_env, &dependency_state.cache_keys, - !self.dry_run, + true, ) .await? { - Some(_) if self.dry_run => None, - Some(_) if self.raw(Some(task)) => { - warn!( - "task {} artifact caching disabled for raw or interactive execution", - task.name - ); - None - } Some(cache) => { ... } None => None, } } else { + if task.cache.as_ref().is_some_and(|cache| cache.enabled) && self.raw(Some(task)) { + warn!( + "task {} artifact caching disabled for raw or interactive execution", + task.name + ); + } None };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/task/task_executor.rs` around lines 483 - 502, Move the raw/interactive task check in the artifact-cache flow before the TaskArtifactCache::new call, using self.raw(Some(task)) and the task’s interactive state as applicable. Return None with the existing warning behavior for those tasks, and only construct the cache when caching is enabled and execution is not raw or interactive.
1306-1422: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRepeated suppress-or-capture boilerplate across output modes.
The
if output_capture.is_some() { with_on_std{out,err}(|_| {}) } else { std{out,err}(Stdio::null()) }fallback is duplicated identically for stdout and stderr acrossPrefix,KeepOrder,Timed, andReplacing. A small helper would reduce the duplication and the risk of the branches drifting out of sync as new modes are added.♻️ Example helper
fn suppress_or_capture( cmd: CmdLineRunner, output_capture: Option<&TaskOutputCapture>, is_stdout: bool, ) -> CmdLineRunner { match (output_capture.is_some(), is_stdout) { (true, true) => cmd.with_on_stdout(|_| {}), (true, false) => cmd.with_on_stderr(|_| {}), (false, true) => cmd.stdout(Stdio::null()), (false, false) => cmd.stderr(Stdio::null()), } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/task/task_executor.rs` around lines 1306 - 1422, Extract the repeated output-capture suppression fallback into a small helper near the task output setup, accepting the command, capture presence, and stdout/stderr selection and applying either the no-op callback or Stdio::null. Replace the duplicated branches in Prefix, KeepOrder, Replacing, Timed, and Silent while preserving each mode’s existing behavior.src/task/task_cache.rs (2)
54-61: 🚀 Performance & Scalability | 🔵 TrivialNo cap on captured output size persisted in the manifest.
CacheManifest.outputholds every captured stdout/stderr line for the entire task run with no size/line limit. Verbose tasks (e.g. noisy build/test logs) will inflate the JSON manifest significantly, adding to disk usage and parse cost on every cache check (is_current/current_output/restore). Worth considering a size cap or truncation strategy before this leaves the experimental stage.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/task/task_cache.rs` around lines 54 - 61, The TaskCacheOutput data persisted through CacheManifest.output has no size or line limit. Add a bounded capture/truncation strategy for stdout and stderr before they are stored in the manifest, preserving clear indication when output is truncated and ensuring is_current, current_output, and restore continue using the capped representation.
168-185: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
is_current()andcurrent_output()both read+parse the manifest on the fast-skip path.
is_current()now callsself.read_manifest()just to validate it parses, and the caller (task_executor.rsrun_task_sched) immediately callscache.current_output()afterward on a hit, which reads and deserializes the same manifest file again. Sinceoutputcan hold the task's entire captured stdout/stderr, this means every "sources up-to-date, skipping" cache hit parses the (potentially large) output twice.Consider having
is_current()return the validated manifest (or output) so the caller doesn't need a second read, e.g. a singlecurrent()method returningOption<Vec<TaskCacheOutput>>that does the existence/key/parse checks once.♻️ Sketch of a combined accessor
- pub fn is_current(&self) -> bool { - let (archive_path, manifest_path) = self.paths(); - archive_path.is_file() - && manifest_path.is_file() - && file::read_to_string(&self.state_path).is_ok_and(|key| key.trim() == self.key) - && self.read_manifest().is_ok() - } + /// Returns the cached output if this cache entry is current, without a + /// second manifest read on the caller's side. + pub fn current(&self) -> Option<Vec<TaskCacheOutput>> { + let (archive_path, manifest_path) = self.paths(); + if !archive_path.is_file() + || !manifest_path.is_file() + || !file::read_to_string(&self.state_path).is_ok_and(|key| key.trim() == self.key) + { + return None; + } + self.read_manifest().ok().map(|m| m.output) + }Also applies to: 242-256
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/task/task_cache.rs` around lines 168 - 185, Combine the manifest validation and retrieval used by is_current and current_output into a single accessor, such as current, that performs the archive, manifest, state-key, and parsing checks once and returns the validated output or an equivalent optional result. Update run_task_sched to use this returned value on cache hits instead of calling is_current followed by current_output, and remove the redundant manifest read while preserving existing cache-miss behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/task/task_cache.rs`:
- Around line 54-61: The TaskCacheOutput data persisted through
CacheManifest.output has no size or line limit. Add a bounded capture/truncation
strategy for stdout and stderr before they are stored in the manifest,
preserving clear indication when output is truncated and ensuring is_current,
current_output, and restore continue using the capped representation.
- Around line 168-185: Combine the manifest validation and retrieval used by
is_current and current_output into a single accessor, such as current, that
performs the archive, manifest, state-key, and parsing checks once and returns
the validated output or an equivalent optional result. Update run_task_sched to
use this returned value on cache hits instead of calling is_current followed by
current_output, and remove the redundant manifest read while preserving existing
cache-miss behavior.
In `@src/task/task_executor.rs`:
- Around line 483-502: Move the raw/interactive task check in the artifact-cache
flow before the TaskArtifactCache::new call, using self.raw(Some(task)) and the
task’s interactive state as applicable. Return None with the existing warning
behavior for those tasks, and only construct the cache when caching is enabled
and execution is not raw or interactive.
- Around line 1306-1422: Extract the repeated output-capture suppression
fallback into a small helper near the task output setup, accepting the command,
capture presence, and stdout/stderr selection and applying either the no-op
callback or Stdio::null. Replace the duplicated branches in Prefix, KeepOrder,
Replacing, Timed, and Silent while preserving each mode’s existing behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Central YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 05f1271a-ffcf-4a81-9539-0e73dcd01a41
📒 Files selected for processing (8)
TASK_CACHE_PARITY.mddocs/tasks/task-configuration.mde2e/tasks/test_task_artifact_cache_outputsrc/cmd.rssrc/task/mod.rssrc/task/task_cache.rssrc/task/task_executor.rssrc/task/task_output_handler.rs
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.7.14 x -- echo |
18.6 ± 1.0 | 16.5 | 24.8 | 1.00 |
mise x -- echo |
20.2 ± 1.6 | 18.0 | 40.5 | 1.09 ± 0.11 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.7.14 env |
17.5 ± 0.9 | 15.7 | 21.1 | 1.00 |
mise env |
18.7 ± 0.9 | 17.1 | 22.5 | 1.07 ± 0.08 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.7.14 hook-env |
18.1 ± 1.0 | 16.4 | 24.8 | 1.00 |
mise hook-env |
19.6 ± 1.0 | 17.6 | 25.2 | 1.08 ± 0.08 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.7.14 ls |
15.8 ± 0.8 | 14.3 | 20.4 | 1.00 |
mise ls |
17.1 ± 1.0 | 15.3 | 23.3 | 1.09 ± 0.08 |
xtasks/test/perf
| Command | mise-2026.7.14 | mise | Variance |
|---|---|---|---|
| install (cached) | 159ms | 166ms | -4% |
| ls (cached) | 57ms | 59ms | -3% |
| bin-paths (cached) | 60ms | 63ms | -4% |
| task-ls (cached) | 85ms | 87ms | -2% |

Summary
v2This follows #11340 and closes the next local-cache parity item without storing terminal-formatted output in the artifact. A task can therefore execute under one display mode and replay under another.
User impact
Both fresh-output cache hits and restored-output cache hits replay the successful task's logs. Cached logs have already passed through mise's redaction layer, and the current invocation still controls whether streams are prefixed, hidden, buffered, or displayed directly.
Raw and interactive tasks continue to use inherited byte-stream I/O and run uncached rather than changing their terminal semantics to make capture possible.
Validation
mise run lint-fixcargo fmt --all -- --checkcargo check --all-featurescargo test --all-features test_cmd_line_runner_execute_async --quietcargo test --all-features task_cache --quietmise run test:e2e test_task_artifact_cache_output test_task_artifact_cache_dependencies test_task_output_style_decoupling test_task_keep_order test_task_quiet_silent_flags test_task_silent_streamsNote
Medium Risk
Changes core task execution, caching format (v2), and all output modes; behavior is well-covered by e2e tests but touches widely used run paths and invalidates v1 artifact caches.
Overview
Experimental task artifact caching now persists successful runs’ stdout and stderr as ordered, redacted stream chunks in the cache manifest, and replays them on cache hits (fresh skip and restore paths) through whatever output mode the current
mise runuses—prefix, interleave, keep-order, timed, replacing, quiet, silent, and per-stream silence.Replay applies current formatting (e.g. prefixes are not baked into stored lines). Timed mode now buffers multiple stdout lines per task prefix for both live and replayed output.
CmdLineRunner gains stdout/stderr observers so capture can run alongside display handlers without changing live I/O semantics; capture paths avoid nulling stdio when observers are active so stdin stays available for cacheable tasks.
Raw (and interactive) tasks bypass artifact caching so inherited terminal I/O is unchanged. Cache storage moves to
task-artifacts/v2(format version 2); corrupt manifests are treated as misses. Docs and e2e tests cover replay, redaction, resilience, and raw bypass.Reviewed by Cursor Bugbot for commit 4aa75f2. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit