Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ terminal the hint uses the crab identity and the same warning/accent palette as
| `clawctl gateway-service start` | Start the OpenClaw gateway in the isolated session and wait for it to listen. Requires setup. |
| `clawctl gateway-service status` | Inspect the gateway without starting it. When the gateway is not running, it may start/probe only the already-recorded isolated session to report file-only config readiness; it never provisions a replacement or starts the gateway. |
| `clawctl gateway-service stop` | Stop the gateway while retaining the session and its data. |
| `clawctl gateway-service restart` | Stop the gateway and start it again as one lifecycle operation. If the stop cannot be verified, it retains the gateway record and does not start a replacement. If no gateway is running, it starts one. |
| `clawctl --version` | Print the packaged launcher version. |

Bare `clawctl`, `clawctl -h`, and `clawctl --help` print help without changing
Expand Down
5 changes: 5 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ OpenClaw resolve its own configuration and prevents a packaging-side default
from overriding upstream behavior or sending a user to a port that was never
observed.

`gateway-service restart` performs the existing stop and start operations
under one lifecycle lock. A missing or already-exited gateway makes the stop a
no-op and the start continues. If the gateway cannot be confirmed stopped, the
operation retains its ownership record and does not launch a replacement.

## Diagnostics and build inputs have different trust roles

[`HostDiagnosticLog`](../src/OpenClaw.Launcher/HostDiagnosticLog.cs) writes
Expand Down
4 changes: 2 additions & 2 deletions docs/mxc-compatibility-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ setup. The collector does not enumerate arbitrary agent-profile files.
naming Windows Update or a newer Windows version as the remedy.
2. Run `openclaw <arguments>` for the upstream OpenClaw CLI, or
`clawctl pwsh` for an interactive agent shell.
3. Use `clawctl gateway-service start`, `status`, and `stop` for the managed
gateway. Use `clawctl status` to inspect session ownership and state.
3. Use `clawctl gateway-service start`, `status`, `stop`, and `restart` for the
managed gateway. Use `clawctl status` to inspect session ownership and state.
4. Run `clawctl collect-logs` when reporting a problem, then review the
resulting ZIP before sharing it.
5. Run `clawctl teardown --force` to confirm removal of the owned isolated
Expand Down
13 changes: 8 additions & 5 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,14 @@ then run:
clawctl gateway-service start
```

For an unhealthy gateway, inspect the emitted log tail or run
`clawctl gateway-service stop` before attempting another start. Use the port
reported by status as the observed endpoint. Do not assume the upstream
default port (18789): an explicit OpenClaw `gateway.port` can differ, and
multiple unclassified listeners intentionally do not identify an endpoint.
For an unhealthy gateway, inspect the emitted log tail, then run
`clawctl gateway-service restart` to stop the verified gateway and start its
replacement. If the stop cannot be verified, restart retains the gateway
record and aborts rather than risk starting a second process. Use
`clawctl gateway-service stop` instead when the gateway should remain stopped.
Use the port reported by status as the observed endpoint. Do not assume the
upstream default port (18789): an explicit OpenClaw `gateway.port` can differ,
and multiple unclassified listeners intentionally do not identify an endpoint.

## Status says the default configuration is missing or not ready

Expand Down
9 changes: 9 additions & 0 deletions src/OpenClaw.Launcher/ClawCtlCommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ internal sealed record ClawCtlHandlers
public required Func<bool, CancellationToken, Task<int>> GatewayStart { get; init; }
public required Func<CancellationToken, Task<int>> GatewayStatus { get; init; }
public required Func<CancellationToken, Task<int>> GatewayStop { get; init; }
public required Func<CancellationToken, Task<int>> GatewayRestart { get; init; }
}

internal sealed record SetupOptions(bool Fresh, bool Force);
Expand Down Expand Up @@ -196,9 +197,17 @@ public static RootCommand Create(
outputOptions.NoColor = parsed.GetValue(noColor);
return handlers.GatewayStop(token);
});
Command gatewayRestart = new("restart", "Stop the gateway and start it again.");
gatewayRestart.SetAction((parsed, token) =>
{
outputOptions.Json = parsed.GetValue(json);
outputOptions.NoColor = parsed.GetValue(noColor);
return handlers.GatewayRestart(token);
});
gateway.Subcommands.Add(gatewayStart);
gateway.Subcommands.Add(gatewayStatus);
gateway.Subcommands.Add(gatewayStop);
gateway.Subcommands.Add(gatewayRestart);

RootCommand root = new(RootDescription)
{
Expand Down
3 changes: 2 additions & 1 deletion src/OpenClaw.Launcher/ClawCtlConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,8 @@ private static void WriteGateway(ResultView view, GatewayCommandResult result)
Gateway.GatewayState.Stopped => result.Action switch
{
"stop" => Status(view, StatusKind.Success, "stopped"),
"start" => Status(view, StatusKind.Failure, "exited during startup"),
"start" or "restart" =>
Status(view, StatusKind.Failure, "exited during startup"),
_ => Status(view, StatusKind.Neutral, "stopped")
},
Gateway.GatewayState.Starting => Status(view, StatusKind.Warning, "starting"),
Expand Down
45 changes: 44 additions & 1 deletion src/OpenClaw.Launcher/Gateway/GatewayController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ internal sealed record GatewayStartResult(
internal sealed record GatewayStopResult(
bool Stopped, string Message, string? Detail = null, bool Succeeded = true);

internal sealed record GatewayRestartResult(
GatewayStopResult Stop,
GatewayStartResult? Start);

/// <summary>
/// Owns this installation's background gateway.
/// </summary>
Expand Down Expand Up @@ -176,6 +180,16 @@ public async Task<GatewayStartResult> StartAsync(
IProgress<GatewayStartProgress>? progress = null)
{
using ISessionLockHandle handle = AcquireLock();
return await StartUnderLockAsync(helperPath, cancellationToken, progress)
.ConfigureAwait(false);
}

private async Task<GatewayStartResult> StartUnderLockAsync(
string helperPath,
CancellationToken cancellationToken,
IProgress<GatewayStartProgress>? progress,
bool? autostartDisabledOverride = null)
{
SessionRecord configured = _requireSetup();
GatewayStateResult existing = _store.Read();
if (existing.Record is null && existing.Fault != GatewayStateFault.Missing)
Expand Down Expand Up @@ -281,7 +295,8 @@ public async Task<GatewayStartResult> StartAsync(

// An explicit earlier choice to disable logon recovery survives a
// restart, so a later manual start does not quietly re-enable it.
AutostartDisabled = existing.Record?.AutostartDisabled ?? false
AutostartDisabled =
autostartDisabledOverride ?? existing.Record?.AutostartDisabled ?? false
};

_store.Write(record);
Expand Down Expand Up @@ -326,6 +341,34 @@ record = record with { ObservedPorts = observed.ListeningPorts };
});
}

/// <summary>
/// Stops the gateway and starts it again without releasing the lifecycle
/// lock between the two operations.
/// </summary>
public async Task<GatewayRestartResult> RestartAsync(
string helperPath,
CancellationToken cancellationToken,
IProgress<GatewayStartProgress>? progress = null)
{
using ISessionLockHandle handle = AcquireLock();
Report(progress, GatewayStartStage.Stopping, "Stopping the gateway.");
bool autostartDisabled = _store.Read().Record?.AutostartDisabled ?? false;
GatewayStopResult stopped = await StopUnderLockAsync(
helperPath,
cancellationToken).ConfigureAwait(false);
if (!stopped.Succeeded)
{
return new GatewayRestartResult(stopped, Start: null);
}

GatewayStartResult started = await StartUnderLockAsync(
helperPath,
cancellationToken,
progress,
autostartDisabled).ConfigureAwait(false);
return new GatewayRestartResult(stopped, started);
}

/// <summary>
/// Polls until a listener the gateway owns appears, or the budget is spent.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/OpenClaw.Launcher/Gateway/GatewayStartProgress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ namespace OpenClaw.Launcher.Gateway;
/// </remarks>
internal enum GatewayStartStage
{
/// <summary>The existing gateway is being stopped before replacement.</summary>
Stopping,

/// <summary>The owned session is being started or confirmed.</summary>
PreparingSession,

Expand All @@ -33,6 +36,10 @@ internal enum GatewayStartStage
internal sealed record GatewayStartProgress(GatewayStartStage Stage, string Message)
: ClawCtlProgress(Message)
{
internal static GatewayStartProgress StoppingFirst { get; } = new(
GatewayStartStage.Stopping,
"Stopping the gateway.");

internal static GatewayStartProgress Initial { get; } = new(
GatewayStartStage.PreparingSession,
"Preparing the isolated session.");
Expand Down
87 changes: 66 additions & 21 deletions src/OpenClaw.Launcher/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ private static string ResolveClawCtlCommand(string[] args)
string? action = args
.Skip(index + 1)
.FirstOrDefault(candidate =>
candidate is "start" or "status" or "stop");
candidate is "start" or "status" or "stop" or "restart");
return action is null ? argument : $"{argument} {action}";
}

Expand Down Expand Up @@ -496,6 +496,34 @@ int WriteResult(IClawCtlResult result)
return result.ExitCode;
}

void AcknowledgeManualGatewayStart(Session.SessionRuntime runtime)
{
new Gateway.AgentGatewayGuidance(
runtime.LifecycleLock,
_ => throw new InvalidOperationException(
"Manual acknowledgement must not check config readiness."),
_ => throw new InvalidOperationException(
"Manual acknowledgement must not inspect the gateway."),
new Gateway.GatewayGuidanceStateStore(
runtime.Paths.GatewayGuidanceStatePath),
getLogonSessionId ?? Gateway.WindowsLogonSession.GetCurrentId,
log,
clock)
.AcknowledgeManualStart();
}

int WriteGatewayStartResult(string action, Gateway.GatewayStartResult result)
{
int? port = Gateway.GatewayAddress.ResolvePort(result.Record);
return WriteResult(new GatewayCommandResult(
action,
result.State,
result.Message,
null,
result.State == Gateway.GatewayState.Running ? 0 : 1,
port));
}

async Task<int> RunSetupCommandAsync(
SetupOptions setupOptions,
CancellationToken cancellationToken)
Expand Down Expand Up @@ -739,18 +767,7 @@ runtime.Paths.PackageFamilyName is string packageFamilyName &&
.Controller;
if (!recovery && !retainedRecoveryInvocation)
{
new Gateway.AgentGatewayGuidance(
runtime.LifecycleLock,
_ => throw new InvalidOperationException(
"Manual acknowledgement must not check config readiness."),
_ => throw new InvalidOperationException(
"Manual acknowledgement must not inspect the gateway."),
new Gateway.GatewayGuidanceStateStore(
runtime.Paths.GatewayGuidanceStatePath),
getLogonSessionId ?? Gateway.WindowsLogonSession.GetCurrentId,
log,
clock)
.AcknowledgeManualStart();
AcknowledgeManualGatewayStart(runtime);
}

// Narration is human guidance, so it is off whenever the
Expand All @@ -770,14 +787,7 @@ runtime.Paths.PackageFamilyName is string packageFamilyName &&
.ConfigureAwait(false);
}

int? port = Gateway.GatewayAddress.ResolvePort(result.Record);
return WriteResult(new GatewayCommandResult(
"start",
result.State,
result.Message,
null,
result.State == Gateway.GatewayState.Running ? 0 : 1,
port));
return WriteGatewayStartResult("start", result);
},
GatewayStatus = async cancellationToken =>
{
Expand Down Expand Up @@ -837,6 +847,41 @@ result.State is Gateway.GatewayState.Running or
result.Detail,
result.Succeeded ? 0 : 1));
},
GatewayRestart = async cancellationToken =>
{
Session.SessionRuntime runtime = GetSessionRuntime();
AcknowledgeManualGatewayStart(runtime);
Gateway.GatewayController controller = Gateway.GatewayRuntime
.Create(options, runtime.Paths, runtime, log, clock)
.Controller;
(bool useColor, IDisposable? restore) = PrepareColor();
Gateway.GatewayRestartResult result;
using (restore)
{
result = await ClawCtlConsole.NarrateAsync(
output,
useColor,
narrate: !outputOptions.Json,
Gateway.GatewayStartProgress.StoppingFirst,
progress => controller.RestartAsync(
runtime.HelperPath,
cancellationToken,
progress))
.ConfigureAwait(false);
}

if (result.Start is null)
{
return WriteResult(new GatewayCommandResult(
"restart",
Gateway.GatewayState.Unknown,
result.Stop.Message,
result.Stop.Detail,
1));
}

return WriteGatewayStartResult("restart", result.Start);
},
},
outputOptions);

Expand Down
14 changes: 14 additions & 0 deletions tests/OpenClaw.Launcher.AotSmoke/SmokeProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ private static async Task<int> Main()
("bare clawctl prints help", BareInvocationPrintsHelpAsync),
("--help prints help", HelpOptionPrintsHelpAsync),
("setup --help prints command help", SetupHelpPrintsCommandHelpAsync),
("gateway-service help includes restart", GatewayServiceHelpIncludesRestartAsync),
("--version reports the launcher", VersionReportsLauncherAssemblyAsync),
("--version wins over trailing arguments", VersionWinsOverTrailingAsync),
("unknown command fails", UnknownCommandFailsAsync),
Expand Down Expand Up @@ -149,6 +150,19 @@ private static async Task SetupHelpPrintsCommandHelpAsync()
fixture.AssertNoInstallationWorkStarted();
}

private static async Task GatewayServiceHelpIncludesRestartAsync()
{
using Fixture fixture = Fixture.CreateWithoutApplication();

int exitCode = await fixture
.RunAsync(["gateway-service", "--help"])
.ConfigureAwait(false);

AssertExitCode(0, exitCode, fixture);
AssertContains(fixture.Output.ToString(), "restart", fixture);
fixture.AssertNoInstallationWorkStarted();
}

// This driver's assembly version is 9.9.9.9. The library's built-in action
// reports the entry assembly, so if the custom action were ever dropped
// this scenario would print 9.9.9.9 instead of the baked build identity.
Expand Down
Loading
Loading