From e9f90c15540e928904442cd1465788fc21cb19b9 Mon Sep 17 00:00:00 2001 From: spomichter Date: Sat, 7 Mar 2026 12:32:34 +0000 Subject: [PATCH 1/6] feat(cli): dimos restart command (DIM-683) Re-stop the running instance and re-invoke with the same blueprint args and config overrides. Uses os.execvp to replace the process so the new run inherits the terminal. dimos restart # graceful stop + restart dimos restart --force # SIGKILL + restart dimos restart --daemon # restart in background --- dimos/robot/cli/dimos.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/dimos/robot/cli/dimos.py b/dimos/robot/cli/dimos.py index 0fc35c9801..3a1c1d648a 100644 --- a/dimos/robot/cli/dimos.py +++ b/dimos/robot/cli/dimos.py @@ -15,6 +15,7 @@ from __future__ import annotations import inspect +import os import sys from typing import Any, get_args, get_origin @@ -256,6 +257,45 @@ def stop( typer.echo(f" {msg}") +@main.command() +def restart( + force: bool = typer.Option(False, "--force", "-f", help="Force kill before restarting"), + daemon: bool = typer.Option(False, "--daemon", "-d", help="Restart in background"), +) -> None: + """Restart the running DimOS instance with the same arguments.""" + from dimos.core.run_registry import get_most_recent, stop_entry + + entry = get_most_recent(alive_only=True) + if not entry: + typer.echo("No running DimOS instance to restart", err=True) + raise typer.Exit(1) + + # Save args before stopping (stop removes the entry) + blueprint_args = entry.cli_args + config_overrides = entry.config_overrides + + typer.echo(f"Restarting {entry.run_id} ({entry.blueprint})...") + msg, _ok = stop_entry(entry, force=force) + typer.echo(f" {msg}") + + # Re-invoke run with saved arguments + cmd = [sys.executable, "-m", "dimos.robot.cli.dimos"] + for key, value in config_overrides.items(): + flag = f"--{key.replace('_', '-')}" + if isinstance(value, bool): + if value: + cmd.append(flag) + else: + cmd.extend([flag, str(value)]) + cmd.append("run") + if daemon: + cmd.append("--daemon") + cmd.extend(blueprint_args) + + typer.echo(f" Running: {' '.join(cmd)}") + os.execvp(cmd[0], cmd) + + @main.command() def show_config(ctx: typer.Context) -> None: """Show current config settings and their values.""" From 47623727337465124030f219f88e4086bf7a4d37 Mon Sep 17 00:00:00 2001 From: spomichter Date: Sat, 7 Mar 2026 12:52:30 +0000 Subject: [PATCH 2/6] fix: emit --no-flag for false boolean overrides in restart --- dimos/robot/cli/dimos.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dimos/robot/cli/dimos.py b/dimos/robot/cli/dimos.py index 3a1c1d648a..f88b784809 100644 --- a/dimos/robot/cli/dimos.py +++ b/dimos/robot/cli/dimos.py @@ -285,6 +285,8 @@ def restart( if isinstance(value, bool): if value: cmd.append(flag) + else: + cmd.append(f"--no-{key.replace('_', '-')}") else: cmd.extend([flag, str(value)]) cmd.append("run") From fd0b8a95b9c0bcb713d372b19187b930814a5964 Mon Sep 17 00:00:00 2001 From: spomichter Date: Mon, 9 Mar 2026 15:37:03 +0000 Subject: [PATCH 3/6] refactor: restart replays original_argv instead of reconstructing CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Store sys.argv in RunEntry.original_argv when a run starts. The restart command now replays the exact original command via os.execvp instead of fragile config_overrides → CLI flag reconstruction. Removes --daemon flag from restart (already in saved argv if used). Tested: daemon start → restart → new instance starts with same args. --- dimos/core/run_registry.py | 1 + dimos/robot/cli/dimos.py | 32 ++++++++++---------------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/dimos/core/run_registry.py b/dimos/core/run_registry.py index 9f8e7f3358..617872011c 100644 --- a/dimos/core/run_registry.py +++ b/dimos/core/run_registry.py @@ -52,6 +52,7 @@ class RunEntry: cli_args: list[str] = field(default_factory=list) config_overrides: dict[str, object] = field(default_factory=dict) grpc_port: int = 9877 + original_argv: list[str] = field(default_factory=list) @property def registry_path(self) -> Path: diff --git a/dimos/robot/cli/dimos.py b/dimos/robot/cli/dimos.py index f88b784809..ff850a585e 100644 --- a/dimos/robot/cli/dimos.py +++ b/dimos/robot/cli/dimos.py @@ -187,6 +187,7 @@ def run( log_dir=str(log_dir), cli_args=list(robot_types), config_overrides=cli_config_overrides, + original_argv=sys.argv, ) entry.save() install_signal_handlers(entry, coordinator) @@ -200,6 +201,7 @@ def run( log_dir=str(log_dir), cli_args=list(robot_types), config_overrides=cli_config_overrides, + original_argv=sys.argv, ) entry.save() try: @@ -260,7 +262,6 @@ def stop( @main.command() def restart( force: bool = typer.Option(False, "--force", "-f", help="Force kill before restarting"), - daemon: bool = typer.Option(False, "--daemon", "-d", help="Restart in background"), ) -> None: """Restart the running DimOS instance with the same arguments.""" from dimos.core.run_registry import get_most_recent, stop_entry @@ -270,32 +271,19 @@ def restart( typer.echo("No running DimOS instance to restart", err=True) raise typer.Exit(1) - # Save args before stopping (stop removes the entry) - blueprint_args = entry.cli_args - config_overrides = entry.config_overrides + if not entry.original_argv: + typer.echo("Cannot restart: run entry missing original command", err=True) + raise typer.Exit(1) + + # Save argv before stopping (stop removes the entry) + argv = entry.original_argv typer.echo(f"Restarting {entry.run_id} ({entry.blueprint})...") msg, _ok = stop_entry(entry, force=force) typer.echo(f" {msg}") - # Re-invoke run with saved arguments - cmd = [sys.executable, "-m", "dimos.robot.cli.dimos"] - for key, value in config_overrides.items(): - flag = f"--{key.replace('_', '-')}" - if isinstance(value, bool): - if value: - cmd.append(flag) - else: - cmd.append(f"--no-{key.replace('_', '-')}") - else: - cmd.extend([flag, str(value)]) - cmd.append("run") - if daemon: - cmd.append("--daemon") - cmd.extend(blueprint_args) - - typer.echo(f" Running: {' '.join(cmd)}") - os.execvp(cmd[0], cmd) + typer.echo(f" Running: {' '.join(argv)}") + os.execvp(argv[0], argv) @main.command() From 9b0efa864b01e3a85aa5ce9c7038ec488324abe3 Mon Sep 17 00:00:00 2001 From: spomichter Date: Mon, 9 Mar 2026 16:16:24 +0000 Subject: [PATCH 4/6] fix: handle execvp OSError + wait for old process after SIGKILL - Wrap os.execvp in try/except OSError so a missing binary gives a clean error instead of a traceback (after the old instance is dead) - Poll up to 2s for old process to exit before exec, preventing port conflict races when --force sends SIGKILL --- dimos/robot/cli/dimos.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/dimos/robot/cli/dimos.py b/dimos/robot/cli/dimos.py index ff850a585e..6e77075bab 100644 --- a/dimos/robot/cli/dimos.py +++ b/dimos/robot/cli/dimos.py @@ -17,6 +17,7 @@ import inspect import os import sys +import time from typing import Any, get_args, get_origin from dotenv import load_dotenv @@ -275,15 +276,28 @@ def restart( typer.echo("Cannot restart: run entry missing original command", err=True) raise typer.Exit(1) - # Save argv before stopping (stop removes the entry) + # Save argv and pid before stopping (stop removes the entry) argv = entry.original_argv + old_pid = entry.pid typer.echo(f"Restarting {entry.run_id} ({entry.blueprint})...") msg, _ok = stop_entry(entry, force=force) typer.echo(f" {msg}") + # Wait for the old process to fully exit so ports are released. + from dimos.core.run_registry import is_pid_alive + + for _ in range(20): # up to 2s + if not is_pid_alive(old_pid): + break + time.sleep(0.1) + typer.echo(f" Running: {' '.join(argv)}") - os.execvp(argv[0], argv) + try: + os.execvp(argv[0], argv) + except OSError as exc: + typer.echo(f"Error: failed to restart — {exc}", err=True) + raise typer.Exit(1) @main.command() From 7406c2c0406cb845547889b6c13fb14849726d39 Mon Sep 17 00:00:00 2001 From: spomichter <12108168+spomichter@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:27:08 +0000 Subject: [PATCH 5/6] CI code cleanup --- dimos/robot/cli/dimos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dimos/robot/cli/dimos.py b/dimos/robot/cli/dimos.py index 8ef08c7563..4a64cd880c 100644 --- a/dimos/robot/cli/dimos.py +++ b/dimos/robot/cli/dimos.py @@ -15,8 +15,8 @@ from __future__ import annotations import inspect -import os import json +import os import sys import time from typing import Any, get_args, get_origin From 49fb529e7a27d68629a1f469bc29468d10c69e5d Mon Sep 17 00:00:00 2001 From: spomichter Date: Mon, 9 Mar 2026 19:31:21 +0000 Subject: [PATCH 6/6] chore: trigger CI