diff --git a/backend/internal/adapters/runtime/zellij/process_windows.go b/backend/internal/adapters/runtime/zellij/process_windows.go index f58ea45e..4c5f82e4 100644 --- a/backend/internal/adapters/runtime/zellij/process_windows.go +++ b/backend/internal/adapters/runtime/zellij/process_windows.go @@ -4,15 +4,13 @@ package zellij import ( "os/exec" - "strings" "syscall" "golang.org/x/sys/windows" ) func startBackgroundProcess(env []string, name string, args ...string) error { - script := "Start-Process -FilePath " + psQuote(name) + " -ArgumentList " + psQuote(windowsCommandLine(args)) + " -WindowStyle Hidden" - cmd := exec.Command("powershell.exe", "-NoLogo", "-NoProfile", "-EncodedCommand", powerShellEncodedCommand(script)) + cmd := exec.Command(name, args...) cmd.Env = env cmd.SysProcAttr = &syscall.SysProcAttr{ CreationFlags: windows.CREATE_NEW_CONSOLE, @@ -24,45 +22,3 @@ func startBackgroundProcess(env []string, name string, args ...string) error { go func() { _ = cmd.Wait() }() return nil } - -func windowsCommandLine(args []string) string { - quoted := make([]string, len(args)) - for i, arg := range args { - quoted[i] = windowsQuoteArg(arg) - } - return strings.Join(quoted, " ") -} - -func windowsQuoteArg(arg string) string { - if arg == "" { - return `""` - } - if !strings.ContainsAny(arg, " \t\"") { - return arg - } - - var b strings.Builder - b.WriteByte('"') - backslashes := 0 - for _, r := range arg { - switch r { - case '\\': - backslashes++ - case '"': - b.WriteString(strings.Repeat(`\`, backslashes*2+1)) - b.WriteRune(r) - backslashes = 0 - default: - if backslashes > 0 { - b.WriteString(strings.Repeat(`\`, backslashes)) - backslashes = 0 - } - b.WriteRune(r) - } - } - if backslashes > 0 { - b.WriteString(strings.Repeat(`\`, backslashes*2)) - } - b.WriteByte('"') - return b.String() -} diff --git a/backend/internal/adapters/runtime/zellij/zellij.go b/backend/internal/adapters/runtime/zellij/zellij.go index f71f5ed2..48ce1f3b 100644 --- a/backend/internal/adapters/runtime/zellij/zellij.go +++ b/backend/internal/adapters/runtime/zellij/zellij.go @@ -238,6 +238,7 @@ func (r *Runtime) Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.Ru }() if err := r.createSession(ctx, id, layoutPath, launchEnv); err != nil { + _ = r.Destroy(context.Background(), ports.RuntimeHandle{ID: id}) return ports.RuntimeHandle{}, fmt.Errorf("zellij runtime: create session %s: %w", id, err) } paneID, err := r.findAgentPane(ctx, id) @@ -263,9 +264,10 @@ func (r *Runtime) Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.Ru return handle, nil } -// createSession runs `zellij attach --create-background`. On Windows we spawn -// it via runner.Start (fire-and-forget) because the inherited daemon stdio -// confuses zellij's own readiness probe; on Unix we keep the synchronous run. +// createSession runs `zellij attach --create-background`. Windows cannot use +// CombinedOutput here: zellij may keep stdout/stderr open after creating the +// background session, so we start it detached and let pane polling observe +// readiness. Non-Windows keeps the synchronous command path. func (r *Runtime) createSession(ctx context.Context, id, layoutPath string, env map[string]string) error { args := createSessionArgs(id, layoutPath) if runtime.GOOS != "windows" { @@ -530,9 +532,9 @@ func (r *Runtime) run(ctx context.Context, args ...string) ([]byte, error) { return out, nil } -// startWithEnv fires zellij in the background with extra env vars merged onto +// startWithEnv starts zellij in the background with extra env vars merged onto // the runtime's base env. Used by the Windows createSession path so the daemon -// is not blocked waiting on zellij's `--create-background` to settle. +// is not blocked waiting on zellij's `--create-background` stdio handles. func (r *Runtime) startWithEnv(extra map[string]string, args ...string) error { fullArgs := append(r.baseArgs(), args...) if err := r.runner.Start(r.envWith(extra), r.binary, fullArgs...); err != nil { diff --git a/backend/internal/session_manager/manager.go b/backend/internal/session_manager/manager.go index 0e33d9b5..737a0285 100644 --- a/backend/internal/session_manager/manager.go +++ b/backend/internal/session_manager/manager.go @@ -229,19 +229,19 @@ func (m *Manager) Spawn(ctx context.Context, cfg ports.SpawnConfig) (domain.Sess // post-create commands (e.g. `pnpm install`) before the agent launches. if err := m.provisionWorkspace(ctx, project, ws.Path); err != nil { _ = m.workspace.Destroy(ctx, ws) - m.markSpawnFailedTerminated(ctx, id) + m.rollbackSpawnSeedRow(ctx, id) return domain.SessionRecord{}, fmt.Errorf("spawn %s: provision: %w", id, err) } agent, ok := m.agents.Agent(cfg.Harness) if !ok { _ = m.workspace.Destroy(ctx, ws) - m.markSpawnFailedTerminated(ctx, id) + m.rollbackSpawnSeedRow(ctx, id) return domain.SessionRecord{}, fmt.Errorf("spawn %s: no agent adapter for harness %q", id, cfg.Harness) } if err := m.prepareWorkspace(ctx, agent, id, ws.Path); err != nil { _ = m.workspace.Destroy(ctx, ws) - m.markSpawnFailedTerminated(ctx, id) + m.rollbackSpawnSeedRow(ctx, id) return domain.SessionRecord{}, fmt.Errorf("spawn %s: %w", id, err) } agentConfig := effectiveAgentConfig(cfg.Kind, project.Config) @@ -256,7 +256,7 @@ func (m *Manager) Spawn(ctx context.Context, cfg ports.SpawnConfig) (domain.Sess }) if err != nil { _ = m.workspace.Destroy(ctx, ws) - m.markSpawnFailedTerminated(ctx, id) + m.rollbackSpawnSeedRow(ctx, id) return domain.SessionRecord{}, fmt.Errorf("spawn %s: launch command: %w", id, err) } // Pre-flight: confirm argv[0] actually exists on PATH (or as an absolute @@ -265,7 +265,7 @@ func (m *Manager) Spawn(ctx context.Context, cfg ports.SpawnConfig) (domain.Sess // unresolved binary would leak through as a "live" session that never ran. if err := m.validateAgentBinary(argv); err != nil { _ = m.workspace.Destroy(ctx, ws) - m.markSpawnFailedTerminated(ctx, id) + m.rollbackSpawnSeedRow(ctx, id) return domain.SessionRecord{}, fmt.Errorf("spawn %s: %w", id, err) } handle, err := m.runtime.Create(ctx, ports.RuntimeConfig{ @@ -276,7 +276,7 @@ func (m *Manager) Spawn(ctx context.Context, cfg ports.SpawnConfig) (domain.Sess }) if err != nil { _ = m.workspace.Destroy(ctx, ws) - m.markSpawnFailedTerminated(ctx, id) + m.rollbackSpawnSeedRow(ctx, id) return domain.SessionRecord{}, fmt.Errorf("spawn %s: runtime: %w", id, err) } diff --git a/backend/internal/session_manager/manager_test.go b/backend/internal/session_manager/manager_test.go index d2474a2b..cc85fbfe 100644 --- a/backend/internal/session_manager/manager_test.go +++ b/backend/internal/session_manager/manager_test.go @@ -350,7 +350,7 @@ func TestSpawn_StampsUTCTimestamps(t *testing.T) { } } -func TestSpawn_RollsBackOnRuntimeFailure(t *testing.T) { +func TestSpawn_DeletesSeedRowOnRuntimeFailure(t *testing.T) { m, st, _, ws := newManager() m.runtime = &fakeRuntime{createErr: errors.New("boom")} if _, err := m.Spawn(ctx, ports.SpawnConfig{ProjectID: "mer"}); err == nil { @@ -359,8 +359,23 @@ func TestSpawn_RollsBackOnRuntimeFailure(t *testing.T) { if ws.destroyed != 1 { t.Fatal("workspace should roll back") } + if rec, present := st.sessions["mer-1"]; present { + t.Fatalf("spawn row without runtime handle must be deleted, got %+v", rec) + } +} + +func TestSpawn_ParksRowTerminatedWhenRuntimeFailureDeleteFails(t *testing.T) { + m, st, _, ws := newManager() + m.runtime = &fakeRuntime{createErr: errors.New("boom")} + st.deleteErr = errors.New("db locked") + if _, err := m.Spawn(ctx, ports.SpawnConfig{ProjectID: "mer"}); err == nil { + t.Fatal("expected failure") + } + if ws.destroyed != 1 { + t.Fatal("workspace should roll back") + } if !st.sessions["mer-1"].IsTerminated { - t.Fatal("orphaned spawn should be terminated") + t.Fatal("row must fall back to terminated when delete fails") } } @@ -855,8 +870,8 @@ func TestSpawn_RejectsMissingAgentBinary(t *testing.T) { if ws.destroyed != 1 { t.Fatal("workspace must be torn down when the pre-launch binary check fails") } - if !st.sessions["mer-1"].IsTerminated { - t.Fatal("the orphan row should be marked terminated after the failed spawn") + if rec, present := st.sessions["mer-1"]; present { + t.Fatalf("spawn row without runtime handle must be deleted, got %+v", rec) } } diff --git a/frontend/src/landing/content/docs/faq.mdx b/frontend/src/landing/content/docs/faq.mdx index 8c049229..3a6feba3 100644 --- a/frontend/src/landing/content/docs/faq.mdx +++ b/frontend/src/landing/content/docs/faq.mdx @@ -31,8 +31,8 @@ import { Accordions, Accordion } from "fumadocs-ui/components/accordion"; - Partially — see [Platforms › Windows](/docs/platforms#windows). You need `runtime: process` instead of tmux, and - desktop notifications are a no-op. Everything else (spawning, PR flow, review loop, CI recovery) works the same. + Partially — see [Platforms › Windows](/docs/platforms#windows). The current Go rewrite requires `zellij` 0.44.3+ on + `PATH`; `runtime: process` is not currently wired. Desktop notifications are a no-op. @@ -41,7 +41,7 @@ import { Accordions, Accordion } from "fumadocs-ui/components/accordion"; Yes. The dashboard is a plain Next.js app — port-forward or put it behind your reverse proxy. AO has no built-in auth, - so use your usual SSO / basic-auth layer. Use `runtime: process` in containers. + so use your usual SSO / basic-auth layer. Install `zellij` 0.44.3+ on `PATH` in containers or remote hosts. diff --git a/frontend/src/landing/content/docs/index.mdx b/frontend/src/landing/content/docs/index.mdx index 915175d3..14bda03b 100644 --- a/frontend/src/landing/content/docs/index.mdx +++ b/frontend/src/landing/content/docs/index.mdx @@ -52,7 +52,7 @@ AO is built from plugins. The default setup works out of the box for common GitH | Plugin slot | What it controls | Examples | | ----------- | --------------------------------- | ------------------------------------------- | | Agent | Which coding tool writes changes | Claude Code, Codex, Cursor, Aider, OpenCode | -| Runtime | How the agent process runs | tmux, child process | +| Runtime | How the agent process runs | zellij | | Workspace | Where code is checked out | git worktree, full clone | | Tracker | Where issues come from | GitHub, GitLab, Linear | | SCM | How PRs, reviews, and CI are read | GitHub, GitLab | @@ -68,9 +68,8 @@ Most users start with the defaults and only edit `agent-orchestrator.yaml` when windows="partial" note={ <> - Windows support is actively improving. Use the process runtime instead of tmux by setting{" "} - defaults.runtime: process in agent-orchestrator.yaml. See{" "} - Platforms for details. + Windows support is actively improving. The current Go rewrite requires zellij 0.44.3+ on{" "} + PATH. See Platforms for details. } /> diff --git a/frontend/src/landing/content/docs/installation.mdx b/frontend/src/landing/content/docs/installation.mdx index 1fffc19c..322fd931 100644 --- a/frontend/src/landing/content/docs/installation.mdx +++ b/frontend/src/landing/content/docs/installation.mdx @@ -23,21 +23,21 @@ This page gets your machine ready to run Agent Orchestrator. By the end, the `ao windows="partial" note={ <> - Windows support is actively improving. Use defaults.runtime: process instead of tmux. See{" "} - Platforms. + Windows support is actively improving. The current Go rewrite requires zellij 0.44.3+ on{" "} + PATH. See Platforms. } /> Install these before running AO: -| Tool | Required | Why AO needs it | -| --------------- | ---------------------- | ------------------------------------------------------------------------------------ | -| Node.js 20+ | Yes | Runs the AO CLI, dashboard, and plugins. | -| Git | Yes | Creates worktrees, branches, commits, and cleanup operations. | -| GitHub CLI `gh` | For GitHub projects | Reads issues, PRs, reviews, and CI status as your user. | -| tmux | Default on macOS/Linux | Keeps long-running agent sessions attachable and recoverable. | -| An agent CLI | Yes | The worker that writes code, such as Claude Code, Codex, Cursor, Aider, or OpenCode. | +| Tool | Required | Why AO needs it | +| --------------- | ------------------- | ------------------------------------------------------------------------------------ | +| Node.js 20+ | Yes | Runs the AO CLI, dashboard, and plugins. | +| Git | Yes | Creates worktrees, branches, commits, and cleanup operations. | +| GitHub CLI `gh` | For GitHub projects | Reads issues, PRs, reviews, and CI status as your user. | +| Zellij 0.44.3+ | Yes | Current Go runtime for long-running agent sessions. Must be on `PATH`. | +| An agent CLI | Yes | The worker that writes code, such as Claude Code, Codex, Cursor, Aider, or OpenCode. | If you plan to use GitLab or Linear, install and authenticate their CLIs or credentials as described on the related plugin pages. A GitHub-only setup only needs `gh`. @@ -131,12 +131,7 @@ ao doctor --fix ## Windows Setup -Windows support is in progress. Use the process runtime instead of tmux: - -```yaml title="agent-orchestrator.yaml" -defaults: - runtime: process -``` +Windows support is in progress. The current Go rewrite requires `zellij` 0.44.3+ on `PATH`; `runtime: process` is not currently wired. Use Slack, Discord, webhook, or another network notifier for alerts. Desktop notifications and iTerm2 integration are not available on Windows. @@ -151,8 +146,9 @@ Use Slack, Discord, webhook, or another network notifier for alerts. Desktop not Run `gh auth login`, then `gh auth status`. AO cannot read GitHub issues, PRs, reviews, or CI checks until `gh` is authenticated. - - Install tmux on macOS or Linux, or set `defaults.runtime: process` if you are on Windows or running in a container. + + Install `zellij` 0.44.3 or newer and make sure it is on `PATH`. The current Go rewrite does not wire `runtime: + process`. Install one supported agent CLI and run it once outside AO so it can complete its own sign-in flow. diff --git a/frontend/src/landing/content/docs/platforms.mdx b/frontend/src/landing/content/docs/platforms.mdx index e49376f9..7498c119 100644 --- a/frontend/src/landing/content/docs/platforms.mdx +++ b/frontend/src/landing/content/docs/platforms.mdx @@ -13,33 +13,33 @@ The platform differences come from the tools used to run and attach to long-live macos="full" linux="full" windows="partial" - note={<>Windows support is actively improving. Use the process runtime instead of tmux.} + note={<>Windows support is actively improving. The current Go rewrite requires zellij 0.44.3+ on PATH.} /> ## Recommended Setup -| Platform | Runtime | Notifications | Notes | -| ---------------------- | ------------------- | ---------------------------------- | -------------------------------------------------------------------------------- | -| macOS | `tmux` | `desktop`, Slack, Discord, webhook | Best local experience. iTerm2 attach support is macOS-only. | -| Linux | `tmux` | `desktop`, Slack, Discord, webhook | Best server and workstation setup. Desktop notifications need `notify-send`. | -| Windows | `process` | Slack, Discord, webhook | Native tmux and iTerm2 are unavailable. Windows support is in progress. | -| Container or remote VM | `process` or `tmux` | Slack, Discord, webhook | Use persistent storage for AO data and protect the dashboard with your own auth. | +| Platform | Runtime | Notifications | Notes | +| ---------------------- | -------- | ---------------------------------- | -------------------------------------------------------------------------------- | +| macOS | `zellij` | `desktop`, Slack, Discord, webhook | Current Go runtime. iTerm2 attach support is macOS-only. | +| Linux | `zellij` | `desktop`, Slack, Discord, webhook | Current Go runtime. Desktop notifications need `notify-send`. | +| Windows | `zellij` | Slack, Discord, webhook | `zellij` 0.44.3+ must be on `PATH`. Windows support is in progress. | +| Container or remote VM | `zellij` | Slack, Discord, webhook | Use persistent storage for AO data and protect the dashboard with your own auth. | ## macOS macOS is the default local development target. -Install tmux before using the default runtime: +Install zellij before using AO: ```bash -brew install tmux +brew install zellij ``` Recommended config: ```yaml title="agent-orchestrator.yaml" defaults: - runtime: tmux + runtime: zellij notifiers: - desktop ``` @@ -48,7 +48,7 @@ What works well on macOS: | Capability | Status | | ----------------------------------------- | --------- | -| tmux-backed worker sessions | Supported | +| zellij-backed worker sessions | Supported | | Browser dashboard terminal | Supported | | iTerm2 attach/open helpers | Supported | | Desktop notifications | Supported | @@ -63,45 +63,45 @@ What works well on macOS: Linux is a good fit for local workstations, remote development machines, and always-on hosts. -Install tmux with your distribution package manager: +Install zellij with your distribution package manager: ```bash -sudo apt install tmux +sudo apt install zellij ``` Recommended config: ```yaml title="agent-orchestrator.yaml" defaults: - runtime: tmux + runtime: zellij notifiers: - desktop ``` What to know: -| Capability | Status | -| --------------------------- | ----------------------------------------------------------- | -| tmux-backed worker sessions | Supported | -| Browser dashboard terminal | Supported | -| iTerm2 attach/open helpers | Not available | -| Desktop notifications | Supported when `notify-send` is installed | -| Remote dashboard access | Supported through port forwarding, Tailscale, or your proxy | +| Capability | Status | +| ----------------------------- | ----------------------------------------------------------- | +| zellij-backed worker sessions | Supported | +| Browser dashboard terminal | Supported | +| iTerm2 attach/open helpers | Not available | +| Desktop notifications | Supported when `notify-send` is installed | +| Remote dashboard access | Supported through port forwarding, Tailscale, or your proxy | If you are running AO on a headless Linux host, prefer Slack, Discord, or webhook notifications over desktop notifications. ## Windows - The native Windows path uses `runtime: process`. The core spawn and PR workflow is available, but tmux-specific - workflows and iTerm2 helpers do not apply. + The current Go rewrite requires `zellij` 0.44.3+ on `PATH`. `runtime: process` is not currently wired, so setting it + will not enable a Windows-specific process runtime. Recommended config: ```yaml title="agent-orchestrator.yaml" defaults: - runtime: process + runtime: zellij notifiers: - slack ``` @@ -111,19 +111,19 @@ What works: | Capability | Status | | --------------------------------------------------------- | ------------------------------------------------------- | | `ao start`, `ao stop`, `ao dashboard` | Supported | -| `ao spawn` and `ao spawn --prompt` | Supported through the process runtime | +| `ao spawn` and `ao spawn --prompt` | Supported when zellij is installed | | GitHub, GitLab, and Linear tracker/SCM integrations | Supported when their CLIs or credentials are configured | | Browser dashboard terminal | Supported through the direct PTY server | | Slack, Discord, webhook, Composio, and OpenClaw notifiers | Supported | What is limited: -| Capability | Status | Use instead | -| --------------------------- | ---------------------- | ---------------------------------------------- | -| `runtime: tmux` | Not available natively | `runtime: process` | -| iTerm2 terminal integration | Not available | Browser dashboard terminal | -| Desktop notifier | No-op on Windows | Slack, Discord, webhook, Composio, or OpenClaw | -| tmux attach commands | Not available | Dashboard terminal and AO session commands | +| Capability | Status | Use instead | +| --------------------------- | ------------------- | ---------------------------------------------- | +| `runtime: process` | Not currently wired | Install zellij 0.44.3+ on `PATH` | +| iTerm2 terminal integration | Not available | Browser dashboard terminal | +| Desktop notifier | No-op on Windows | Slack, Discord, webhook, Composio, or OpenClaw | +| tmux attach commands | Not available | Dashboard terminal and AO session commands | Use PowerShell or Git Bash for normal CLI commands. If a command behaves differently because of shell quoting, put longer instructions in a file and send them with `ao send --file`. @@ -135,7 +135,7 @@ Recommended config for containers: ```yaml title="agent-orchestrator.yaml" defaults: - runtime: process + runtime: zellij notifiers: - webhook ``` @@ -145,13 +145,12 @@ Operational notes: - Mount or preserve AO's data directory so session metadata survives restarts. - Forward the dashboard port, usually `3000`, only to trusted networks. - Prefer network notifiers over desktop notifications. -- Use `tmux` only if it is installed and you want attachable terminal sessions inside the host. +- Install `zellij` 0.44.3+ on `PATH`; the process runtime is not currently wired. ## Choosing A Runtime -| Runtime | Best for | Tradeoff | -| --------- | ------------------------------------------------------------------------- | --------------------------------------------------- | -| `tmux` | macOS/Linux machines where you want durable, attachable terminal sessions | Requires tmux and does not work natively on Windows | -| `process` | Windows, containers, and simpler process-managed environments | Less of the workflow is tmux-attachable | +| Runtime | Best for | Tradeoff | +| -------- | ----------------------------------------------- | ----------------------------------------------- | +| `zellij` | Current Go rewrite on macOS, Linux, and Windows | Requires zellij 0.44.3+ to be installed on PATH | Start with the recommended runtime for your OS. Change it only when the default runtime does not match where AO is running. diff --git a/frontend/src/landing/content/docs/plugins/index.mdx b/frontend/src/landing/content/docs/plugins/index.mdx index a3c1285c..4245fed4 100644 --- a/frontend/src/landing/content/docs/plugins/index.mdx +++ b/frontend/src/landing/content/docs/plugins/index.mdx @@ -57,16 +57,10 @@ AO has **eight plugin slots**. Only one plugin per slot is active at a time, and - diff --git a/frontend/src/landing/content/docs/plugins/runtimes/index.mdx b/frontend/src/landing/content/docs/plugins/runtimes/index.mdx index 6ebffcab..e3eb397f 100644 --- a/frontend/src/landing/content/docs/plugins/runtimes/index.mdx +++ b/frontend/src/landing/content/docs/plugins/runtimes/index.mdx @@ -1,33 +1,26 @@ --- title: Runtimes overview -description: Where the agent process runs — tmux on macOS/Linux, plain child process everywhere. +description: Where the agent process runs in the current Go rewrite. --- -The runtime is where your agent's terminal actually lives. Two options ship: +The runtime is where your agent's terminal actually lives. In the current Go rewrite, AO wires the Zellij runtime. -| Plugin | Best for | Binary needed | -| ----------------------------------------- | ---------------------------------------------------- | ------------- | -| [tmux](/docs/plugins/runtimes/tmux) | macOS / Linux default. You can attach interactively. | `tmux` | -| [process](/docs/plugins/runtimes/process) | Windows, Docker, CI-like environments | — | +| Runtime | Best for | Binary needed | +| -------- | ------------------------------------------- | ---------------- | +| `zellij` | Current Go rewrite on macOS, Linux, Windows | `zellij` 0.44.3+ | - ## Choosing -- If you can install `tmux` and you want to occasionally drop into a live session, use `runtime: tmux`. -- If you're on Windows, in a container, or just want fewer moving parts, use `runtime: process`. +- Install `zellij` 0.44.3+ on `PATH` before spawning sessions. +- `runtime: process` is not currently wired in the Go rewrite. -Both runtimes expose the agent's terminal to the dashboard's xterm.js session — attaching via tmux is a _bonus_, not a requirement. +The dashboard terminal connects to the daemon's Zellij-backed terminal mux. diff --git a/frontend/src/landing/content/docs/plugins/runtimes/process.mdx b/frontend/src/landing/content/docs/plugins/runtimes/process.mdx index 4dc0ea81..02ff7f0a 100644 --- a/frontend/src/landing/content/docs/plugins/runtimes/process.mdx +++ b/frontend/src/landing/content/docs/plugins/runtimes/process.mdx @@ -1,6 +1,6 @@ --- title: process -description: Cross-platform child-process runtime. Required on Windows. +description: Legacy child-process runtime; not currently wired in the Go rewrite. ---
@@ -10,32 +10,12 @@ description: Cross-platform child-process runtime. Required on Windows.
-Spawns agents as plain child processes — no tmux involved. Use this on Windows (where tmux isn't available) and in any environment where you'd rather not depend on tmux. +The legacy TypeScript implementation documented a plain child-process runtime for Windows and containers. The current Go rewrite does not wire that runtime yet. - - -## Use +Use the current Zellij runtime instead: ```yaml title="agent-orchestrator.yaml" -runtime: process +runtime: zellij ``` -No plugin-level config. - -## How it works - -- AO spawns the agent via Node's `child_process.spawn` with `shell: true`. -- Stdout + stderr are captured in a rolling 1000-line buffer. -- The dashboard reads from that buffer over the same WebSocket the tmux runtime uses — you won't notice a difference in the UI. -- `isProcessRunning` uses a PID-based signal-0 check. - -## What you lose vs tmux - -- **No tmux attach.** `ao session attach` doesn't work. Use the dashboard terminal instead. -- **No reconnecting to the agent's TTY.** If you `ao stop` the orchestrator, the child process goes with it. The agent's own session-resume features (Claude `--resume`, Codex `resume`, etc.) still work — that's a different layer. - -## When to pick it - -- **Windows** — this is the only runtime that works. -- **Docker / CI-like environments** — fewer moving parts, no tmux install. -- **You never attach interactively** — the dashboard terminal covers all your attach needs anyway. +Install `zellij` 0.44.3 or newer and make sure it is on `PATH` before spawning sessions. diff --git a/frontend/src/landing/content/docs/quickstart.mdx b/frontend/src/landing/content/docs/quickstart.mdx index 121dc927..a63cc123 100644 --- a/frontend/src/landing/content/docs/quickstart.mdx +++ b/frontend/src/landing/content/docs/quickstart.mdx @@ -139,13 +139,13 @@ ao session cleanup ## If Something Looks Wrong -| Symptom | First check | -| --------------------------------------- | ----------------------------------------------------------------------------- | -| Dashboard is not updating | Make sure the `ao start` terminal is still running. | -| `ao spawn` warns that AO is not running | Start AO with `ao start` before spawning. | -| GitHub issue or PR data is missing | Run `gh auth status` and check the `repo` field in `agent-orchestrator.yaml`. | -| Agent started but does nothing | Open the session terminal and send a clear instruction with `ao send`. | -| Windows spawn fails with tmux errors | Set `defaults.runtime: process` in `agent-orchestrator.yaml`. | +| Symptom | First check | +| ----------------------------------------- | ----------------------------------------------------------------------------- | +| Dashboard is not updating | Make sure the `ao start` terminal is still running. | +| `ao spawn` warns that AO is not running | Start AO with `ao start` before spawning. | +| GitHub issue or PR data is missing | Run `gh auth status` and check the `repo` field in `agent-orchestrator.yaml`. | +| Agent started but does nothing | Open the session terminal and send a clear instruction with `ao send`. | +| Runtime startup fails with missing zellij | Install `zellij` 0.44.3+ and make sure it is on `PATH`. | ## Next diff --git a/frontend/src/landing/content/docs/troubleshooting.mdx b/frontend/src/landing/content/docs/troubleshooting.mdx index e51cf5f1..5fb8f126 100644 --- a/frontend/src/landing/content/docs/troubleshooting.mdx +++ b/frontend/src/landing/content/docs/troubleshooting.mdx @@ -21,8 +21,8 @@ Covers: install health, plugin resolution, notifier connectivity, stale temp fil The npm global bin isn't on your PATH. Find it with `npm config get prefix` and add `/bin` to your shell's PATH. - - Expected. Use `runtime: process` in your config. See [Platforms](/docs/platforms#windows). + + Install `zellij` 0.44.3 or newer and make sure it is on `PATH`. The current Go rewrite does not wire `runtime: process`. Run `gh auth login`. AO uses `gh` for every GitHub interaction. diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 112cb75a..6390f887 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -320,7 +320,9 @@ ipcMain.handle("daemon:getStatus", () => daemonStatus); ipcMain.handle("daemon:start", () => startDaemon()); ipcMain.handle("daemon:stop", () => stopDaemon()); ipcMain.handle("app:getVersion", () => app.getVersion()); -ipcMain.handle("telemetry:getBootstrap", () => buildTelemetryBootstrap(process.env, app.getVersion(), process.platform)); +ipcMain.handle("telemetry:getBootstrap", () => + buildTelemetryBootstrap(process.env, app.getVersion(), process.platform), +); ipcMain.handle("app:chooseDirectory", async () => { const options: OpenDialogOptions = { properties: ["openDirectory"], diff --git a/frontend/src/renderer/components/TelemetryBoundary.tsx b/frontend/src/renderer/components/TelemetryBoundary.tsx index 3868a77d..9ed73c07 100644 --- a/frontend/src/renderer/components/TelemetryBoundary.tsx +++ b/frontend/src/renderer/components/TelemetryBoundary.tsx @@ -29,7 +29,9 @@ export class TelemetryBoundary extends React.Component {

The app hit an unexpected error.

-

Restart the app or check the daemon logs if this keeps happening.

+

+ Restart the app or check the daemon logs if this keeps happening. +

); diff --git a/frontend/src/renderer/lib/spawn-orchestrator.test.ts b/frontend/src/renderer/lib/spawn-orchestrator.test.ts new file mode 100644 index 00000000..7bc80038 --- /dev/null +++ b/frontend/src/renderer/lib/spawn-orchestrator.test.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { setApiBaseUrl } from "./api-client"; +import { spawnOrchestrator } from "./spawn-orchestrator"; + +describe("spawnOrchestrator", () => { + afterEach(() => { + vi.restoreAllMocks(); + setApiBaseUrl("http://127.0.0.1:3001"); + }); + + it("throws the daemon error envelope message when spawn fails", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + JSON.stringify({ + error: "internal_error", + code: "orchestrator_spawn_failed", + message: "worktree has uncommitted changes", + requestId: "req-123", + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ), + ); + + await expect(spawnOrchestrator("project-1")).rejects.toThrow("worktree has uncommitted changes"); + }); + + it("falls back to the response status when no error envelope is available", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(JSON.stringify({}), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + await expect(spawnOrchestrator("project-1")).rejects.toThrow("Failed to spawn orchestrator (200)"); + }); +}); diff --git a/frontend/src/renderer/lib/spawn-orchestrator.ts b/frontend/src/renderer/lib/spawn-orchestrator.ts index 15d73b86..70c7d085 100644 --- a/frontend/src/renderer/lib/spawn-orchestrator.ts +++ b/frontend/src/renderer/lib/spawn-orchestrator.ts @@ -1,4 +1,4 @@ -import { apiClient } from "./api-client"; +import { apiClient, apiErrorMessage } from "./api-client"; /** Spawn the project's orchestrator session via the daemon API. */ export async function spawnOrchestrator(projectId: string): Promise { @@ -7,11 +7,8 @@ export async function spawnOrchestrator(projectId: string): Promise { }); if (error || !data?.orchestrator?.id) { - const message = - error && typeof error === "object" && "message" in error && typeof error.message === "string" - ? error.message - : `Failed to spawn orchestrator (${response.status})`; - throw new Error(message); + const fallback = `Failed to spawn orchestrator (${response.status})`; + throw new Error(apiErrorMessage(error, fallback)); } return data.orchestrator.id; diff --git a/frontend/src/renderer/lib/telemetry.test.ts b/frontend/src/renderer/lib/telemetry.test.ts index da7de90e..3b8d9f6f 100644 --- a/frontend/src/renderer/lib/telemetry.test.ts +++ b/frontend/src/renderer/lib/telemetry.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - routeSurface, - sanitizeRendererExceptionProperties, - sanitizeRendererProperties, -} from "./telemetry"; +import { routeSurface, sanitizeRendererExceptionProperties, sanitizeRendererProperties } from "./telemetry"; describe("telemetry sanitizers", () => { it("categorizes routes without exporting raw paths", () => { diff --git a/frontend/src/renderer/lib/telemetry.ts b/frontend/src/renderer/lib/telemetry.ts index e03e853e..cb66a741 100644 --- a/frontend/src/renderer/lib/telemetry.ts +++ b/frontend/src/renderer/lib/telemetry.ts @@ -70,7 +70,7 @@ export async function sanitizeRendererProperties( case "ao.renderer.orchestrator_open_requested": { const projectIDHash = await hashedTelemetryID(properties?.project_id); if (projectIDHash) safe.project_id_hash = projectIDHash; - break + break; } } return safe; @@ -157,10 +157,7 @@ export async function captureRendererEvent(event: string, properties?: Record, -): Promise { +export async function captureRendererException(error: unknown, properties?: Record): Promise { if (!(await initTelemetry())) return; const safeProperties = await sanitizeRendererExceptionProperties(error, properties); posthog.capture("ao.renderer.exception", safeProperties); diff --git a/frontend/src/shared/telemetry.test.ts b/frontend/src/shared/telemetry.test.ts index 8cb4323e..f3684dd5 100644 --- a/frontend/src/shared/telemetry.test.ts +++ b/frontend/src/shared/telemetry.test.ts @@ -7,7 +7,11 @@ import { buildTelemetryBootstrap, defaultDataDir, loadOrCreateTelemetryInstallId const tempDirs: string[] = []; afterEach(async () => { - await Promise.all(tempDirs.splice(0).map((dir) => import("node:fs/promises").then(({ rm }) => rm(dir, { recursive: true, force: true })))); + await Promise.all( + tempDirs + .splice(0) + .map((dir) => import("node:fs/promises").then(({ rm }) => rm(dir, { recursive: true, force: true }))), + ); }); test("defaultDataDir prefers AO_DATA_DIR", () => {