Skip to content

feat(tasks): minimal interactive runtime rework with segment rwlock#8473

Closed
mackwic wants to merge 1 commit intojdx:mainfrom
mackwic:feat/minimal-interactive-rwlock-segment
Closed

feat(tasks): minimal interactive runtime rework with segment rwlock#8473
mackwic wants to merge 1 commit intojdx:mainfrom
mackwic:feat/minimal-interactive-rwlock-segment

Conversation

@mackwic
Copy link
Copy Markdown
Contributor

@mackwic mackwic commented Mar 5, 2026

Summary

This PR implements a minimal interactive runtime rework for tasks, intentionally scoped to avoid the complexity explored previously.

What’s included

  • Add interactive task metadata (true|false, default false):

    • task model/header parsing
    • config parsing from mise.toml
    • JSON schemas
    • mise tasks info --json
    • mise tasks ls --json
    • MCP mise://tasks payload
  • Keep scheduler/--jobs behavior aligned with current main:

    • no semaphore draining/barrier logic
    • no ticket ordering logic
  • Add one global async runtime lock in task execution:

    • non-interactive runtime segment acquires read lock
    • interactive runtime segment acquires write lock
    • stop check (is_stopping()) happens immediately after lock acquisition, before executing the runtime segment
  • Interactive runtime behavior:

    • requires TTY on stdin
    • uses inherited stdio (stdin/stdout/stderr = inherit)
    • preserves terminal restore behavior for raw/interactive command execution
    • keeps PID tracking in raw path for signal handling consistency

Non-goals intentionally not implemented

  • no ticket ordering logic
  • no env-marker propagation for interactive injected chains
  • no semaphore draining with acquire_many_owned(max_permits)
  • no transitive graph walk for non-TTY fail-fast
  • no strict ordering guarantees between injected subgraphs and unrelated tasks
  • no attempt to fix preexisting --jobs=1 mixed runtime+injection behavior in this PR

Tests

Added unit tests

  • config::config_file::mise_toml::tests::test_tasks_interactive_deserialize_true_and_default_false
  • config::config_file::mise_toml::tests::test_tasks_interactive_rejects_non_bool
  • task::task_executor::tests::test_stop_check_happens_after_runtime_lock_acquisition
  • task::task_executor::tests::test_interactive_segment_uses_write_lock_and_blocks_readers

Updated existing e2e expectations

  • e2e/tasks/test_task_info (includes interactive: false in JSON expectation)
  • e2e/tasks/test_task_ls (includes interactive: false in JSON expectation)

Validation run

  • cargo fmt --all
  • cargo check -q
  • cargo test -q
  • Relevant interactive/mcp e2e scenarios pass locally (including non-tty and terminal restore regression cases)

Notes

This PR supersedes #8464 with a narrower, lower-risk implementation aligned to the minimal spec.

Copilot AI review requested due to automatic review settings March 5, 2026 10:36
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a focused rework of the task execution runtime to support interactive tasks. It adds an interactive flag to task definitions, ensuring that such tasks run with direct TTY passthrough and exclusive access to the runtime via a read-write lock. This change allows for proper handling of interactive processes while maintaining the existing concurrency model for non-interactive tasks, providing a robust foundation for future interactive features.

Highlights

  • Interactive Task Metadata: Introduced an interactive boolean metadata field for tasks, defaulting to false. This includes parsing from task models/headers and mise.toml, and integration into JSON schemas, mise tasks info --json, mise tasks ls --json, and the MCP mise://tasks payload.
  • Global Async Runtime Lock: Implemented a global asynchronous runtime RwLock for task execution. Non-interactive tasks acquire a read lock, allowing concurrent execution, while interactive tasks acquire a write lock, ensuring exclusive execution.
  • Interactive Runtime Behavior: Interactive tasks now require a TTY on stdin, use inherited stdio (stdin/stdout/stderr), preserve terminal restore behavior for raw/interactive command execution, and maintain PID tracking for consistent signal handling.
  • Scheduler Alignment: The scheduler and --jobs behavior remain aligned with the current main branch, intentionally avoiding complex semaphore draining, barrier logic, or ticket ordering in this minimal rework.
Changelog
  • e2e/tasks/test_task_info
    • Updated the JSON expectation to include the interactive: false field for mise tasks info --json.
  • e2e/tasks/test_task_ls
    • Updated JSON expectations across multiple assertions to include the interactive: false field for mise tasks ls --json.
  • schema/mise-task.json
    • Added the interactive boolean property with a default of false and a description to the task definition.
  • schema/mise.json
    • Added the interactive boolean property with a default of false and a description to the task definition within the main mise.json schema in several locations.
  • src/cli/mcp.rs
    • Included the interactive task property in the JSON payload for the mise://tasks MCP endpoint.
  • src/cli/run.rs
    • Imported the ensure macro from eyre.
    • Added validation within validate_task to enforce that interactive tasks require a TTY on stdin.
  • src/cli/tasks/info.rs
    • Added the interactive field to the JSON output when running mise tasks info --json.
  • src/cli/tasks/ls.rs
    • Added the interactive field to the JSON output when running mise tasks ls --json.
  • src/cmd.rs
    • Added std::os::unix::process::CommandExt import for Unix-specific process control.
    • Implemented a Unix-specific pre_exec hook to manage process groups for raw commands in non-TTY contexts.
    • Added logic to save and restore TTY settings for interactive tasks on Unix systems.
    • Integrated PID tracking for interactive/raw commands to facilitate global Ctrl-C handling.
  • src/config/config_file/mise_toml.rs
    • Added test_tasks_interactive_deserialize_true_and_default_false to verify correct parsing of the interactive task property.
    • Added test_tasks_interactive_rejects_non_bool to ensure non-boolean values for interactive are rejected during parsing.
  • src/task/mod.rs
    • Introduced the pub interactive: bool field to the Task struct, defaulting to false.
    • Updated Task::from_file to correctly parse the interactive property from task headers.
    • Updated the Task::default implementation to set interactive to false.
    • Added interactive=true to the example task header comment for documentation.
    • Added an assertion for task.interactive in the example task test.
  • src/task/task_executor.rs
    • Introduced TASK_RUNTIME_LOCK as a global RwLock to manage task execution concurrency.
    • Defined the RuntimeSegmentLockGuard enum to differentiate between read and write locks.
    • Implemented acquire_runtime_segment_lock to obtain a write lock for interactive tasks and a read lock for non-interactive tasks.
    • Implemented acquire_runtime_segment_lock_and_check_stop to acquire the appropriate lock and then verify stop conditions.
    • Added TTY validation for interactive tasks within the execute_task function.
    • Modified the raw determination logic to consider interactive tasks as raw.
    • Configured stdin, stdout, and stderr to inherit for interactive tasks.
    • Ensured the runtime segment lock is acquired before command execution.
    • Added test_stop_check_happens_after_runtime_lock_acquisition unit test.
    • Added test_interactive_segment_uses_write_lock_and_blocks_readers unit test.
Activity
  • Added unit tests for config::config_file::mise_toml::tests::test_tasks_interactive_deserialize_true_and_default_false and test_tasks_interactive_rejects_non_bool.
  • Added unit tests for task::task_executor::tests::test_stop_check_happens_after_runtime_lock_acquisition and test_interactive_segment_uses_write_lock_and_blocks_readers.
  • Updated existing e2e expectations for e2e/tasks/test_task_info and e2e/tasks/test_task_ls to include the new interactive: false field.
  • Validated changes with cargo fmt --all, cargo check -q, and cargo test -q.
  • Confirmed relevant interactive/mcp e2e scenarios pass locally, including non-tty and terminal restore regression cases.
  • This PR supersedes feat(task): minimal interactive tty scheduler barrier #8464, offering a narrower and lower-risk implementation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for interactive tasks by adding an interactive flag and managing concurrent execution with a global RwLock. Non-interactive tasks acquire a read lock, while interactive tasks acquire a write lock, ensuring exclusive access to the terminal when needed. The changes also include TTY validation for interactive tasks, proper terminal state restoration for raw-mode commands, and PID tracking for improved signal handling. The implementation is well-structured and includes thorough unit tests for the new concurrency logic. I have reviewed the changes and found no issues.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 5, 2026

Greptile Summary

This PR adds an interactive boolean task attribute that gives a task exclusive TTY access during execution, implemented via a new process-wide async TASK_RUNTIME_LOCK (tokio::sync::RwLock) in task_executor.rs: interactive tasks acquire a write lock (blocking all concurrent task commands), while non-interactive tasks acquire a read lock (allowing concurrency with each other). The executor-level lock composes with the pre-existing RAW_LOCK in cmd.rs, which serialises raw subprocess execution. Supporting changes include schema additions, JSON output fields (tasks info, tasks ls, MCP payload), task-file header parsing (#MISE interactive=true), and TTY state save/restore in execute_raw.

Key issues:

  • The raw_redactions hint fires with an incorrect message "--raw will prevent mise from being able to use redactions" when interactive = true is set, because raw is eagerly set to true || self.raw(...) — but the user never passed --raw. The hint should either be gated on explicit --raw usage or the message should differentiate the interactive case.
  • The pre_exec block calling setpgid(0, 0) in non-TTY contexts was moved from the non-raw execute() path to the raw execute_raw() path. This is a behavior change for existing --raw tasks: they now run in a new process group in non-TTY environments, potentially altering signal delivery semantics for CI workflows. The inline comment's rationale ("aligned with non-raw execution") is now unclear since non-raw execution no longer performs this setpgid call.

The core interactive lock semantics are sound and unit tests are well-structured with deterministic channel-based synchronization. However, both the user-facing hint message and the --raw task behavior change require clarification or fixes before merging.

Confidence Score: 2/5

  • The core interactive lock semantics are sound, but two UX/documentation issues should be addressed: a misleading hint message and an unclear behavior change for existing --raw tasks.
  • The PR implements the interactive lock feature correctly with well-designed tests and proper schema updates. However, two issues prevent confident merge: (1) The raw_redactions hint fires with an incorrect message --raw will prevent... when only interactive=true is set, misleading users about the cause. (2) The setpgid behavior in execute_raw() is a behavioral change for all raw tasks in non-TTY environments (CI), but the inline comment's rationale is unclear. Both issues require clarification or fixes. The core functionality is not broken, but these issues affect user experience and CI compatibility.
  • src/task/task_executor.rs (misleading hint message) and src/cmd.rs (setpgid behavior change for --raw tasks)

Sequence Diagram

sequenceDiagram
    participant CLI as CLI (run.rs)
    participant Exec as TaskExecutor
    participant Lock as TASK_RUNTIME_LOCK<br/>(tokio RwLock)
    participant Cmd as CmdLineRunner
    participant RAW as RAW_LOCK<br/>(std RwLock)
    participant Child as Child Process

    Note over CLI,Child: Non-interactive task
    CLI->>Exec: exec_program(task.interactive=false)
    Exec->>Lock: acquire read lock
    Lock-->>Exec: read guard held
    Exec->>Cmd: execute() [raw=false]
    Cmd->>RAW: acquire read lock
    RAW-->>Cmd: read guard held
    Cmd->>Child: spawn + wait (piped I/O)
    Child-->>Cmd: exit
    Cmd-->>Exec: Ok(())
    Exec-->>CLI: Ok(())

    Note over CLI,Child: Interactive task
    CLI->>CLI: validate_task → is_terminal() check
    CLI->>Exec: exec_program(task.interactive=true)
    Exec->>Lock: acquire WRITE lock (blocks new readers)
    Lock-->>Exec: write guard held
    Exec->>Cmd: execute() [raw=true → execute_raw()]
    Cmd->>RAW: acquire WRITE lock
    RAW-->>Cmd: write guard held
    Cmd->>Child: pre_exec (setpgid if non-TTY) + spawn
    Note over Cmd: save TTY state (tcgetattr)
    Cmd->>Child: wait()
    Child-->>Cmd: exit
    Note over Cmd: restore TTY state (tcsetattr)
    Cmd-->>Exec: Ok(())
    Note over Exec: drop write guard → unblocks readers
    Exec-->>CLI: Ok(())
Loading

Comments Outside Diff (1)

  1. src/task/task_executor.rs, line 692-698 (link)

    Misleading --raw hint message for interactive = true tasks

    The hint fires when raw && !redactions.is_empty(). Since raw is set to true whenever interactive = true (line 686: let raw = interactive || self.raw(Some(task))), the message "--raw will prevent mise from being able to use redactions" appears even when the user only set interactive = true and never passed --raw. This misleads users about the cause of the behavior.

    The hint condition should exclude the interactive-triggered case, or the message should differentiate between the two scenarios:

    Alternatively, if preserving the hint for interactive tasks is intentional, update the message to clarify: e.g., "interactive tasks run in raw mode; redactions will not be applied".

Last reviewed commit: b4acc27

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a minimal “interactive task” execution model by adding an interactive task flag and enforcing exclusive access to the runtime segment (write lock) for interactive tasks while allowing concurrent non-interactive tasks (read lock).

Changes:

  • Add interactive: bool to the task model, config parsing, schemas, and JSON outputs (tasks info/ls, MCP payload).
  • Enforce TTY-on-stdin for interactive tasks and run them with inherited stdio.
  • Add a global async runtime segment RwLock to serialize interactive runtime execution vs concurrent non-interactive execution, with targeted unit tests.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/task/task_executor.rs Adds global runtime segment lock (rwlock), interactive stdio/TTY behavior, and unit tests for locking/stop-check ordering.
src/task/mod.rs Adds Task.interactive field, header parsing, default value, and parsing test expectation updates.
src/config/config_file/mise_toml.rs Adds unit tests validating interactive TOML deserialization and type enforcement.
src/cmd.rs Adjusts raw execution to restore tty state and track raw PIDs; adds unix pre_exec process-group tweak.
src/cli/tasks/ls.rs Includes interactive in mise tasks ls --json output.
src/cli/tasks/info.rs Includes interactive in mise tasks info --json output.
src/cli/run.rs Validates interactive tasks require a TTY on stdin before running.
src/cli/mcp.rs Adds interactive to MCP mise://tasks payload.
schema/mise.json Adds interactive to task schemas.
schema/mise-task.json Adds interactive to task schemas.
e2e/tasks/test_task_ls Updates JSON expectations to include interactive: false.
e2e/tasks/test_task_info Updates JSON expectations to include interactive: false.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mackwic mackwic force-pushed the feat/minimal-interactive-rwlock-segment branch from 66e699c to 2811348 Compare March 5, 2026 11:01
@jdx
Copy link
Copy Markdown
Owner

jdx commented Mar 5, 2026

@greptileai review

@mackwic mackwic force-pushed the feat/minimal-interactive-rwlock-segment branch from 2811348 to b4acc27 Compare March 6, 2026 08:51
@jdx
Copy link
Copy Markdown
Owner

jdx commented Mar 6, 2026

@greptileai

@jdx
Copy link
Copy Markdown
Owner

jdx commented Mar 6, 2026

The setpgid in execute_raw() is concerning given the history here.

We reverted setpgid entirely in #8347 because it caused hangs with nested mise tasks (Playwright killing a process group left grandchildren in separate groups alive, holding pipes open). The revert commit explicitly removed setpgid from all paths — both execute() and execute_raw().

This PR re-introduces setpgid(0, 0) into execute_raw() for non-TTY contexts. The problem is that execute_raw() is called for all raw tasks (--raw, settings.raw = true, and now interactive = true). So any raw = true task running in CI (non-TTY) will get the exact same setpgid behavior that was reverted in #8347, potentially re-introducing the Playwright-style hang.

The comment says "Keep raw-mode process group behavior aligned with non-raw execution in non-tty contexts" but the non-raw execute() path on main no longer does setpgid at all — it was removed in the revert.

My recommendation: drop the setpgid block entirely from this PR. The interactive feature doesn't need it — interactive tasks require a TTY by definition (there's an ensure!(is_terminal()) check), so the non-TTY setpgid branch would never even execute for interactive tasks. And for regular --raw tasks, re-introducing setpgid risks the same breakage we already reverted.

If there's a specific signal-handling problem that setpgid was meant to solve for interactive tasks, let's address it separately with a narrower fix that doesn't affect all raw tasks.

This comment was generated by Claude Code.

@mackwic mackwic force-pushed the feat/minimal-interactive-rwlock-segment branch from b4acc27 to 4e7a455 Compare March 6, 2026 13:24
@jdx
Copy link
Copy Markdown
Owner

jdx commented Mar 6, 2026

Thanks for scoping setpgid to self.interactive — that addresses the regression risk for existing --raw tasks.

However, there's still a logical issue: the setpgid is now dead code. Interactive tasks require a TTY (ensure!(is_terminal()) in both validate_task and exec_program), but the setpgid only fires when !is_terminal(). These conditions are mutually exclusive — if the task is interactive, we guaranteed stdin is a TTY, so the setpgid branch never executes.

I'd suggest just removing the setpgid block entirely. It can't fire for interactive tasks, and we've already established it shouldn't fire for regular raw tasks. Keeping dead code around process group manipulation is a footgun for future readers.

The TTY save/restore and PID tracking additions in execute_raw() look good and are useful regardless.

This comment was generated by Claude Code.

@jdx
Copy link
Copy Markdown
Owner

jdx commented Mar 6, 2026

look just stop, it's clear to me you don't understand the code you're trying to write. We need to be very careful around this logic.

@jdx jdx closed this Mar 6, 2026
@mackwic
Copy link
Copy Markdown
Contributor Author

mackwic commented Mar 6, 2026

@jdx You're right about the setpgid — I had an automated fixup loop pushing without manual review on each iteration. That specific mistake is on me.

That said, I want to share some honest context on this whole process.

I've submitted 5 PRs over several weeks to fix a real regression — TTY tasks are broken since the scheduler rework, and this affects real workflows (ie: saml2aws → interactive tools in the same DAG). Each time, I got short feedback asking for something simpler, but without enough detail on what the target implementation should look like. I went from a static DAG planner, to a dynamic policy, to a semaphore barrier, to a segment RwLock — each one a genuine attempt to match what I understood you wanted.

I know TTYs, DAGs, and schedulers well. What I don't know well is the internal history of every code path in cmd.rs, and I didn't get enough direction to avoid these specific pitfalls. The setpgid/#8347 context wasn't something I could have inferred without guidance.

Since the implementation is this sensitive to internal context, I'd welcome you taking it from here. What matters most to me is fixing this TTY regression that affects your users (my team included).

@jdx
Copy link
Copy Markdown
Owner

jdx commented Mar 6, 2026

I'm not going to argue about it but your PRs are the most slop I've ever received on this project.

jdx added a commit that referenced this pull request Mar 7, 2026
)

## Summary

- Adds `interactive = true` task field that acquires an exclusive write
lock on a global `RwLock`, giving the task sole access to
stdin/stdout/stderr
- Non-interactive tasks use a shared read lock and continue running in
parallel with each other
- More targeted alternative to `raw = true` which forces `jobs=1`
globally — `interactive` only blocks concurrent tasks while the
interactive task is running
- Supersedes #8473 with a much simpler approach: no `cmd.rs` changes, no
TTY save/restore, no setpgid

## Changes

- `src/task/mod.rs` — add `interactive: bool` field, file-task parsing,
default
- `src/task/task_executor.rs` — global `TASK_RUNTIME_LOCK: RwLock<()>`,
acquire write (interactive) or read (non-interactive) before
`cmd.execute()`
- `src/task/task_output_handler.rs` — treat `interactive` as `raw` for
I/O
- `src/cli/tasks/info.rs`, `src/cli/tasks/ls.rs`, `src/cli/mcp.rs` — add
`interactive` to JSON output
- `schema/mise-task.json`, `schema/mise.json` — add `interactive`
boolean property
- `e2e/tasks/test_task_info`, `e2e/tasks/test_task_ls` — update
expectations

## Test plan

- [x] `mise run lint-fix` passes
- [x] `cargo test` — all 520 unit tests pass
- [x] `mise run test:e2e test_task_info test_task_ls` passes
- [x] Manual test: `interactive-task ::: bg-task-1 ::: bg-task-2` —
interactive task waits for exclusive access while bg tasks run in
parallel

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches core task execution/concurrency and terminal I/O behavior via
a new global lock, which could introduce deadlocks or unexpected
scheduling/throughput changes if edge cases aren’t covered.
> 
> **Overview**
> Adds a new boolean task field `interactive` (default `false`) that
marks a task as requiring exclusive terminal access, and wires it
through task parsing and defaults.
> 
> Task execution now uses a global runtime `RwLock` to coordinate
concurrency: interactive tasks take an exclusive lock (including during
confirmation prompts), while non-interactive tasks take a shared lock;
the lock is held across consecutive run-script entries but dropped while
waiting on injected sub-tasks to avoid deadlocks, and command execution
is wrapped in `block_in_place` to avoid starving the tokio runtime.
> 
> CLI/MCP JSON outputs and `tasks info` property display now include
`interactive`, JSON schemas (`mise.json`, `mise-task.json`) are updated
accordingly, interactive tasks are treated as raw I/O in the output
handler (with a new hint warning about redactions), and e2e expectations
are updated.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4126daa. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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.

3 participants