Skip to content

feat(task): replay cached task output - #11347

Merged
jdx merged 3 commits into
mainfrom
agent/task-cache-logs
Jul 26, 2026
Merged

jdx merged 3 commits into
mainfrom
agent/task-cache-logs

Conversation

@jdx

@jdx jdx commented Jul 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • capture successful task stdout and stderr as stream-tagged, redacted cache data
  • replay cached output through the output mode selected for the cache hit
  • preserve prefix, interleave, keep-order, timed, replacing, quiet, silent, and per-stream silence behavior
  • conservatively bypass artifact caching for raw and interactive tasks, which require inherited terminal I/O
  • bump the experimental local artifact format and directory to v2
  • document the behavior and update the temporary parity tracker

This 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-fix
  • cargo fmt --all -- --check
  • cargo check --all-features
  • cargo test --all-features test_cmd_line_runner_execute_async --quiet
  • cargo test --all-features task_cache --quiet
  • mise 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_streams

Note

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 run uses—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

  • New Features
    • Task artifact caching now captures and replays redacted stdout/stderr (cache format v2), including correct behavior across output/ordering/quiet/silent modes.
    • Timed and formatted output replay now supports multiple cached stdout lines.
    • Cache replay is skipped for raw tasks.
  • Documentation
    • Updated cache documentation to reflect v2 format, ordered redacted streams replay, dependency key behavior, and miss/failure handling.
    • Marked stdout/stderr cache parity as complete.
  • Tests
    • Added end-to-end coverage for cache/output-mode replay and resilience to corrupted cache manifests.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Task 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.

Changes

Task artifact output cache

Layer / File(s) Summary
Versioned cache manifest and output contract
src/task/task_cache.rs, src/task/mod.rs
Cache artifacts use format v2 and persist tagged stdout/stderr entries with validated restore and versioned paths.
Command output observation and capture plumbing
src/cmd.rs, src/task/task_executor.rs
Command runners expose stdout/stderr observers, and task execution buffers redacted output for cached artifacts.
Cache execution and output replay
src/task/task_executor.rs, src/task/task_output_handler.rs, src/cli/run.rs
Cached streams are restored, stored, and replayed through the selected output mode; timed output retains multiple lines and raw tasks bypass artifact caching.
Parity documentation and end-to-end validation
docs/tasks/task-configuration.md, TASK_CACHE_PARITY.md, e2e/tasks/test_task_artifact_cache_*
Documentation and tests cover stream capture, redaction, output modes, silence behavior, cache resilience, stdin handling, cache hits, and raw-task execution.

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
Loading

Possibly related PRs

  • jdx/mise#11328: Extends the related local task artifact cache flow with replayable stdout and stderr output.

Poem

I’m a rabbit with streams in my cache,
Redacted and ordered, a neat little stash.
Prefix or quiet, interleave with delight,
Old output returns when the cache feels right.
Raw tasks still hop past the gate!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: replaying cached task output.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread src/task/task_output_handler.rs Resolved
@greptile-apps

greptile-apps Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds redacted, stream-tagged task-output capture and replay for local artifact cache hits.

  • Replays cached stdout and stderr through the output mode selected by the current invocation.
  • Bumps the local task-artifact format and directory to v2.
  • Bypasses artifact caching for raw and interactive tasks.
  • Adds end-to-end coverage and updates cache documentation and parity tracking.

Confidence Score: 3/5

The 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

Filename Overview
src/task/task_executor.rs Integrates cache-hit replay and live output capture into task execution while revising cache-miss fallback behavior.
src/task/task_cache.rs Extends the v2 manifest with ordered captured output and makes unreadable current output fall through as a cache miss.
src/cmd.rs Adds post-redaction stdout and stderr observers alongside existing display callbacks.
src/task/task_output_handler.rs Replays cached stream entries through the currently selected task output mode.
src/cli/run.rs Updates timed-output flushing to print all buffered lines for an elapsed task entry.
e2e/tasks/test_task_artifact_cache_output Covers replay formatting, redaction, stream suppression, silent capture, and raw-task bypass.
e2e/tasks/test_task_artifact_cache_resilience Covers corrupt-manifest fallback and stdin forwarding for an interleave task without configured redactions.

Reviews (3): Last reviewed commit: "fix(task): satisfy cached output clippy ..." | Re-trigger Greptile

Comment thread src/task/task_executor.rs Resolved
Comment thread src/task/task_executor.rs Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
src/task/task_executor.rs (2)

483-502: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Artifact 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 checks self.raw(Some(task)) and throws the result away. For a task with cache.enabled = true that's always run with --raw (or is interactive), 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 win

Repeated 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 across Prefix, KeepOrder, Timed, and Replacing. 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 | 🔵 Trivial

No cap on captured output size persisted in the manifest.

CacheManifest.output holds 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() and current_output() both read+parse the manifest on the fast-skip path.

is_current() now calls self.read_manifest() just to validate it parses, and the caller (task_executor.rs run_task_sched) immediately calls cache.current_output() afterward on a hit, which reads and deserializes the same manifest file again. Since output can 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 single current() method returning Option<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

📥 Commits

Reviewing files that changed from the base of the PR and between 97bd429 and 129fdf5.

📒 Files selected for processing (8)
  • TASK_CACHE_PARITY.md
  • docs/tasks/task-configuration.md
  • e2e/tasks/test_task_artifact_cache_output
  • src/cmd.rs
  • src/task/mod.rs
  • src/task/task_cache.rs
  • src/task/task_executor.rs
  • src/task/task_output_handler.rs

@jdx
jdx enabled auto-merge (squash) July 26, 2026 21:58
@jdx
jdx disabled auto-merge July 26, 2026 21:58
@github-actions

github-actions Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Hyperfine Performance

mise x -- echo

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%

@jdx
jdx enabled auto-merge (squash) July 26, 2026 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant