Skip to content

Processes and PTY

refact-planner edited this page Jun 7, 2026 · 2 revisions

Processes and PTY

Background/service process control, transcript polling, stdin injection, and sleep ticks in the exec runtime.

See also Exec Runtime and Chat System.

The unified exec runtime owns foreground commands, background processes, and services. shell and process_start both accept tty: bool.

PTY vs pipes

  • tty: false uses normal stdout/stderr pipes.
  • tty: true uses the PTY path, exposes an interactive stdin writer, and combines stdout/stderr into the combined stream.
  • PTY output goes through the same bounded runtime buffers as pipe output.
  • PTY can change command behavior; it should be used for REPLs, prompts, interactive CLIs, and programs that need a terminal.
  • The Windows path uses ConPTY where available; if a PTY cannot be allocated, the tool fails clearly rather than silently falling back.

Tools

process_start

Start a runtime-owned background or service process and return its process ID, initial status, output cursor, and metadata.

Schema highlights:

  • command: string
  • description: string
  • mode: "background" | "service" with default background
  • service_name for services
  • workdir
  • startup_wait_ms
  • startup_wait_port
  • startup_wait_keyword
  • tty: boolean = false

Notes:

  • Service mode requires service_name.
  • Duplicate running services in the same owner/workspace are rejected.
  • Workdir is resolved through active worktree privacy rules.

process_list

Schema:

  • optional status: "running" | "completed" | "all" with default running
  • optional scope: "chat" | "workspace" | "all" with default chat

Returns process summaries under extra.exec.processes.

process_read

Schema highlights:

  • process_id: string required
  • optional since_seq
  • optional stream: "stdout" | "stderr" | "combined" | "all"
  • optional output filters

Returns transcript chunks and cursor metadata under extra.exec.transcript:

  • since_seq
  • next_seq
  • latest_seq

Empty-output reads are normal when nothing new has been emitted. Use the returned cursor for the next poll.

process_wait

Wait until terminal status or timeout, then return final/partial transcript metadata.

Schema highlights:

  • process_id: string required
  • optional timeout_ms
  • optional output filters

process_kill

Schema: { "process_id": "exec_..." }.

Kills a runtime-owned process and returns its terminal metadata.

process_write_stdin

This is the PTY stdin path.

Schema:

{
  "type": "object",
  "properties": {
    "process_id": { "type": "string" },
    "chars": { "type": "string", "default": "" },
    "yield_time_ms": { "type": "integer", "default": 250, "maximum": 10000 }
  },
  "required": ["process_id"]
}

Behavior contract:

  • Requires a tty=true process.
  • Writes chars bytes to stdin.
  • Waits up to yield_time_ms for new output or exit.
  • chars: "" means poll only.
  • Returns bytes_written and chunks_returned in addition to standard extra.exec fields.

Background completion notification

ExecRegistry emits a completion event on the first terminal transition for background/service processes with an owning chat_id.

Current delivery is the ordinary MessageAdded envelope carrying a hidden event(process_completed) message. The event payload includes the process ID, status, exit code, duration, and short description. Foreground processes and records without chat_id do not inject notifications.

Sleep

The sleep tool waits for the requested duration without holding a shell process.

Schema:

{
  "type": "object",
  "properties": {
    "duration_ms": { "type": "integer", "minimum": 100, "maximum": 3600000 },
    "tick_interval_ms": { "type": "integer", "minimum": 5000 },
    "description": { "type": "string", "description": "Short description (≤80 chars)." }
  },
  "required": ["duration_ms", "description"]
}

Returns { "slept_ms": number, "interrupted": boolean }. If tick_interval_ms is set, it injects event(tick, "tool.sleep", {elapsed_ms, remaining_ms}, "tick") at each interval.

Clone this wiki locally