Skip to content

feat(runtime): RFC - observable long-running shell commands #486

Description

@M4n5ter

Background

Maka's current Bash tool works well for foreground one-shot commands: it executes a command, streams output, retains a bounded tail, handles timeout/cancel, and emits terminal runtime events.

Some tasks do not naturally fit into a single tool call:

  • starting a dev server or test server;
  • starting watch mode;
  • starting a local service and waiting for readiness;
  • running a long benchmark or harness job;
  • observing logs over time before a later step makes a decision.

Without a runtime-owned long-running shell lifecycle, agents are pushed toward shell tricks such as &, nohup, tmux, redirected log files, hand-written pid files, and ad hoc log polling. Those can work for an individual task, but they bypass the runtime contracts Maka already has.

Problem

The core problem is not just "make Bash return asynchronously". The missing concept is an observable, cancellable, waitable, repairable shell process resource owned by the runtime.

Shell tricks introduce structural problems:

  • ToolRuntime only owns the initial launch, not the later lifecycle;
  • permission, approval, and trace only describe the initial command and cannot model later wait/cancel operations;
  • stdout/stderr bounding and redaction-safe tail handling become script conventions;
  • UI, read models, and the AgentRun ledger cannot reliably represent that a command is still running;
  • startup recovery sees incomplete history instead of a runtime fact that can be repaired deterministically;
  • desktop and headless can drift into separate background-process semantics.

Direction

A likely better direction is to avoid modeling this as Bash(background: true).

Instead, a shell command could be promoted by the runtime into an observable ShellRun when it has not finished within the initial yield window.

The flow could look like this:

  1. The agent calls a shell command.
  2. The runtime waits for yield_time_ms.
  3. If the command exits, return the normal terminal result.
  4. If the command is still running, return shellRunId, current output tail, and status.
  5. Follow-up operations such as wait, status, and cancel observe or control that ShellRun.

This direction has a few advantages:

  • long-running behavior is an observed runtime fact, not something the model has to guess before launch;
  • short foreground commands and long-running commands can share the same lower-level process lifecycle;
  • permission, trace, output bounding, and startup repair can stay attached to Maka's runtime spine;
  • the design can leave room for PTY or interactive sessions later without making all terminal semantics part of the first slice.

This is not meant as a final design. It is a proposed shape for discussion.

Questions To Settle

  1. Should ShellRun become a new runtime fact, or should this extend the existing terminal runtime event model?

  2. What should the agent-facing surface look like?

    Possible options:

    • keep Bash, add yield_time_ms, and return shellRunId when the process is still running;
    • add a new exec_command entry point for long-running shell work;
    • keep Bash as a foreground one-shot tool and add separate shell_start, shell_wait, and shell_cancel tools.
  3. Should P0 only cover pipe-mode non-interactive long-running processes?

    PTY and interactive stdin probably need a larger ShellSession-level contract: screen state, resize, prompt handling, secret input, input auditing, and more complex recovery semantics. The lower-level lifecycle should not block future PTY support, but whether tty belongs in P0 needs a deliberate decision.

  4. How should startup recovery be represented?

    If Maka restarts and cannot prove the process is still observable, what terminal state should be recorded? Possible choices include orphaned, failed, or an existing run-repair state. The important property is a stable, testable policy that does not pretend to recover an OS process.

  5. Should desktop and headless share the same lifecycle model from the first slice?

    My inclination is yes, otherwise background shell can become a second fact model. If the implementation cost is too high, the shared contract could be defined first and wired incrementally.

  6. Should P0 retain only bounded tails, or introduce a managed full-output artifact?

    Bounded stdout/stderr tails may be enough for P0. Full output artifacts can be a follow-up unless benchmark or harness workflows need complete logs from the start.

Possible P0 Scope

A first slice could cover:

  • shell command can yield while still running;
  • runtime owns a ShellRun;
  • status and bounded stdout/stderr tails can be read;
  • wait is supported;
  • cancel is supported and cleans up the process tree;
  • runtime events / ledger facts represent the lifecycle explicitly;
  • startup recovery has deterministic behavior for non-terminal ShellRuns;
  • docs state that this is not a sandbox boundary and does not promise process survival across app restart.

Out Of Scope For P0

  • full PTY screen model;
  • interactive secret or prompt handling;
  • remote observation API;
  • recovering the OS process after app restart;
  • treating nohup, tmux, or disown as the product contract;
  • a headless-only job registry independent from the runtime ledger.

Possible Invariants

Whatever surface is chosen, the long-running shell mechanism should preserve these properties:

  • runtime owns the lifecycle;
  • output remains bounded and redaction-safe;
  • cancel cleans up the process tree;
  • missing terminal evidence cannot repair a related run to completed;
  • desktop and headless do not grow drifting shell lifecycle models;
  • recovery semantics are conservative, explicit, and testable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions