feat(host): add /restart slash command and IHostRestarter #37

Merged
jasoncouture merged 2 commits from feat/self-restart into main 2026-05-07 15:19:05 -04:00
jasoncouture commented 2026-05-07 11:29:21 -04:00 (Migrated from github.com)

Summary

Adds a self-restart capability to the host, surfaced as the /restart slash command (channel input only — per the no-UI-buttons-for-control policy, there is no UI button).

Closes the Self-restart control item under TASKS.md → "Web UI" (the spec is "control surface", not literally a button).

What's in here

src/LlamaShears.Hosting/

  • IHostRestarter — host-wide self-restart trigger. Single method, void RequestRestart().
  • HostRestarter — default impl. Behavior:
    1. Idempotent latch (Interlocked.Exchange) — repeated calls past the first are no-ops.
    2. Sniff DOTNET_RUNNING_IN_CONTAINER=true.
    3. Hook IHostApplicationLifetime.ApplicationStopped for the post-shutdown step.
    4. IHostApplicationLifetime.StopApplication().
    5. In container: Environment.Exit(1) — supervisor (Docker restart: unless-stopped, k8s) brings us back. Non-zero exit is the only signal we owe.
    6. Out of container: spawn Environment.ProcessPath with Environment.GetCommandLineArgs()[1..] verbatim; Environment.Exit(0) after spawn. The spawned child is independent of the parent.
    7. Failure modes — no ProcessPath available, or Process.Start throws → log + Environment.Exit(1).

Wiring

HostingServiceCollectionExtensions.AddHostRestarter(). Called from WebApplicationBuilderExtensions.AddApi, so any host that goes through AddApi gets it for free.

Slash command

ChatSession.TryParseCommand recognises /restart; ExecuteCommandAsync calls _restarter.RequestRestart(). ChatSession's constructor now takes an IHostRestarter (DI handles it). ChatCommand enum picks up a Restart value.

Why hook ApplicationStopped instead of running re-exec eagerly

Hosted services need to drain (release ports, flush state) before we either exit or spawn the child. ApplicationStopped fires after the host's StopAsync completes, so by the time Finalize runs the listening sockets are already free for the spawned child to bind.

Tests

2 new TUnit cases (HostRestarterTests):

  • RequestRestartCallsStopApplicationOnce — first call invokes IHostApplicationLifetime.StopApplication.
  • RequestRestartIsIdempotent — three consecutive calls produce one StopApplication invocation.

The actual Environment.Exit / Process.Start paths can't be unit-tested without forking the test host; they will be exercised by hand the first time /restart lands in production.

Test plan

  • dotnet test --solution LlamaShears.slnx — 384 passing (was 382).
  • Husky dotnet-test-on-source-change pre-commit check passed.
  • Build clean, 0 warnings.
  • Manual smoke (post-merge): out-of-container — /restart from chat, observe new pid; in-container — same, observe Docker restarts the container.

Stacking note

Targets feat/cron-tool-stub (PR #36). Will retarget main automatically as the chain merges. The slash command dispatch is left in the ChatSession-direct-call form on purpose; PR 8 introduces the registry and migrates /clear, /archive, /compact, /restart to it.

🤖 Generated with Claude Code

## Summary Adds a self-restart capability to the host, surfaced as the `/restart` slash command (channel input only — per the no-UI-buttons-for-control policy, there is no UI button). Closes the *Self-restart control* item under TASKS.md → "Web UI" (the spec is "control surface", not literally a button). ## What's in here ### `src/LlamaShears.Hosting/` - **`IHostRestarter`** — host-wide self-restart trigger. Single method, `void RequestRestart()`. - **`HostRestarter`** — default impl. Behavior: 1. Idempotent latch (`Interlocked.Exchange`) — repeated calls past the first are no-ops. 2. Sniff `DOTNET_RUNNING_IN_CONTAINER=true`. 3. Hook `IHostApplicationLifetime.ApplicationStopped` for the post-shutdown step. 4. `IHostApplicationLifetime.StopApplication()`. 5. **In container:** `Environment.Exit(1)` — supervisor (Docker `restart: unless-stopped`, k8s) brings us back. Non-zero exit is the only signal we owe. 6. **Out of container:** spawn `Environment.ProcessPath` with `Environment.GetCommandLineArgs()[1..]` verbatim; `Environment.Exit(0)` after spawn. The spawned child is independent of the parent. 7. **Failure modes** — no `ProcessPath` available, or `Process.Start` throws → log + `Environment.Exit(1)`. ### Wiring `HostingServiceCollectionExtensions.AddHostRestarter()`. Called from `WebApplicationBuilderExtensions.AddApi`, so any host that goes through `AddApi` gets it for free. ### Slash command `ChatSession.TryParseCommand` recognises `/restart`; `ExecuteCommandAsync` calls `_restarter.RequestRestart()`. `ChatSession`'s constructor now takes an `IHostRestarter` (DI handles it). `ChatCommand` enum picks up a `Restart` value. ### Why hook `ApplicationStopped` instead of running re-exec eagerly Hosted services need to drain (release ports, flush state) before we either exit or spawn the child. `ApplicationStopped` fires after the host's `StopAsync` completes, so by the time `Finalize` runs the listening sockets are already free for the spawned child to bind. ## Tests 2 new TUnit cases (`HostRestarterTests`): - `RequestRestartCallsStopApplicationOnce` — first call invokes `IHostApplicationLifetime.StopApplication`. - `RequestRestartIsIdempotent` — three consecutive calls produce one `StopApplication` invocation. The actual `Environment.Exit` / `Process.Start` paths can't be unit-tested without forking the test host; they will be exercised by hand the first time `/restart` lands in production. ## Test plan - [x] `dotnet test --solution LlamaShears.slnx` — 384 passing (was 382). - [x] Husky `dotnet-test-on-source-change` pre-commit check passed. - [x] Build clean, 0 warnings. - [ ] Manual smoke (post-merge): out-of-container — `/restart` from chat, observe new pid; in-container — same, observe Docker restarts the container. ## Stacking note Targets `feat/cron-tool-stub` (PR #36). Will retarget `main` automatically as the chain merges. The slash command dispatch is left in the ChatSession-direct-call form on purpose; PR 8 introduces the registry and migrates `/clear`, `/archive`, `/compact`, `/restart` to it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
CLAassistant commented 2026-05-07 11:29:30 -04:00 (Migrated from github.com)

CLA assistant check
All committers have signed the CLA.

[![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/jasoncouture/llama-shears?pullRequest=37) <br/>All committers have signed the CLA.
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-05-07 11:34:20 -04:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull request overview

Adds a host self-restart capability (IHostRestarter + default HostRestarter) and exposes it through a new /restart chat slash command in the Web UI, wired via DI from AddApi.

Changes:

  • Introduces IHostRestarter and HostRestarter to request a graceful shutdown followed by either container-exit or local re-exec.
  • Wires IHostRestarter into the default service setup (AddHostRestarter() called from WebApplicationBuilderExtensions.AddApi).
  • Adds /restart parsing/execution in ChatSession, plus unit tests for idempotent StopApplication() triggering.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/LlamaShears.UnitTests/Hosting/HostRestarterTests.cs Adds unit tests verifying RequestRestart() calls StopApplication() once and is idempotent.
src/LlamaShears.Hosting/IHostRestarter.cs Defines the public restart-trigger interface.
src/LlamaShears.Hosting/HostRestarter.cs Implements restart orchestration (container exit vs. local re-exec) with logging and idempotency latch.
src/LlamaShears.Hosting/HostingServiceCollectionExtensions.cs Adds DI registration helper AddHostRestarter().
src/LlamaShears.Api/WebApplicationBuilderExtensions.cs Wires host restarter registration into AddApi().
src/LlamaShears.Api.Web/Services/ChatSession.cs Adds /restart command parsing and dispatch to IHostRestarter.
src/LlamaShears.Api.Web/LlamaShears.Api.Web.csproj Adds project reference to LlamaShears.Hosting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

## Pull request overview Adds a host self-restart capability (`IHostRestarter` + default `HostRestarter`) and exposes it through a new `/restart` chat slash command in the Web UI, wired via DI from `AddApi`. **Changes:** - Introduces `IHostRestarter` and `HostRestarter` to request a graceful shutdown followed by either container-exit or local re-exec. - Wires `IHostRestarter` into the default service setup (`AddHostRestarter()` called from `WebApplicationBuilderExtensions.AddApi`). - Adds `/restart` parsing/execution in `ChatSession`, plus unit tests for idempotent `StopApplication()` triggering. ### Reviewed changes Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments. <details> <summary>Show a summary per file</summary> | File | Description | | ---- | ----------- | | tests/LlamaShears.UnitTests/Hosting/HostRestarterTests.cs | Adds unit tests verifying `RequestRestart()` calls `StopApplication()` once and is idempotent. | | src/LlamaShears.Hosting/IHostRestarter.cs | Defines the public restart-trigger interface. | | src/LlamaShears.Hosting/HostRestarter.cs | Implements restart orchestration (container exit vs. local re-exec) with logging and idempotency latch. | | src/LlamaShears.Hosting/HostingServiceCollectionExtensions.cs | Adds DI registration helper `AddHostRestarter()`. | | src/LlamaShears.Api/WebApplicationBuilderExtensions.cs | Wires host restarter registration into `AddApi()`. | | src/LlamaShears.Api.Web/Services/ChatSession.cs | Adds `/restart` command parsing and dispatch to `IHostRestarter`. | | src/LlamaShears.Api.Web/LlamaShears.Api.Web.csproj | Adds project reference to `LlamaShears.Hosting`. | </details> --- 💡 <a href="/jasoncouture/llama-shears/new/feat/cron-tool-stub?filename=.github/instructions/*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
@ -0,0 +1,122 @@
using System.Diagnostics;
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-07 11:34:18 -04:00

Finalize is a confusing method name in C# because it collides with object.Finalize/finalizer terminology. Consider renaming it to something like FinalizeRestart/CompleteRestart to avoid ambiguity and potential analyzer confusion.

`Finalize` is a confusing method name in C# because it collides with `object.Finalize`/finalizer terminology. Consider renaming it to something like `FinalizeRestart`/`CompleteRestart` to avoid ambiguity and potential analyzer confusion.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-07 11:34:19 -04:00

Process.Start(psi) returns Process?. If it returns null here, the code still logs pid -1 and then exits with code 0, which can bring the host down without successfully spawning the replacement process. Treat a null return as a failure (log + exit non-zero) before exiting the current process.

`Process.Start(psi)` returns `Process?`. If it returns `null` here, the code still logs pid -1 and then exits with code 0, which can bring the host down without successfully spawning the replacement process. Treat a `null` return as a failure (log + exit non-zero) before exiting the current process.
@ -0,0 +19,4 @@
lifetime.Received(1).StopApplication();
await Task.CompletedTask;
}
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-07 11:34:19 -04:00

These tests are marked async but don't await any real async work (they end with await Task.CompletedTask). Consider making them non-async (or returning Task without async) to keep the intent clear and avoid unnecessary state machine generation.

These tests are marked `async` but don't await any real async work (they end with `await Task.CompletedTask`). Consider making them non-async (or returning `Task` without `async`) to keep the intent clear and avoid unnecessary state machine generation.
github-actions[bot] commented 2026-05-07 15:05:24 -04:00 (Migrated from github.com)
Package Line Rate Branch Rate Complexity Health
LlamaShears.Core.Abstractions.Context 100% 100% 4
LlamaShears.Provider.Ollama 3% 1% 166
LlamaShears.IntegrationTests 87% 73% 71
LlamaShears.Core 46% 32% 861
LlamaShears.Core.Abstractions.Content 0% 100% 1
LlamaShears.Core.Abstractions.Caching 100% 100% 1
LlamaShears.Core.Eventing 96% 76% 51
StrangeSoft.Plugins.Host 20% 21% 87
LlamaShears.Core.Abstractions.Provider 32% 20% 66
LlamaShears.Core.Abstractions.Memory 0% 100% 3
LlamaShears.Api.Web 35% 20% 336
LlamaShears.Hosting 26% 8% 27
LlamaShears.Core.Abstractions.Events 21% 6% 79
LlamaShears.Core.Abstractions.SystemPrompt 100% 100% 2
LlamaShears 65% 25% 11
LlamaShears.Core.Abstractions.PromptContext 89% 100% 2
LlamaShears.Provider.Onnx.Embeddings 4% 0% 68
LlamaShears.Plugins.Host 34% 24% 36
LlamaShears.Core.Abstractions.Agent 73% 100% 11
LlamaShears.Api 10% 3% 342
LlamaShears.Plugins 0% 100% 1
LlamaShears.Core.Eventing.Extensions 100% 100% 1
LlamaShears.Core.Abstractions.Context 100% 100% 4
LlamaShears.Provider.Ollama 3% 1% 166
LlamaShears.Core 45% 31% 861
LlamaShears.Core.Abstractions.Content 0% 100% 1
LlamaShears.Core.Abstractions.Caching 100% 100% 1
LlamaShears.Core.Eventing 96% 76% 51
StrangeSoft.Plugins.Host 20% 21% 87
LlamaShears.Core.Abstractions.Provider 32% 20% 66
LlamaShears.Core.Abstractions.Memory 0% 100% 3
LlamaShears.Api.Web 22% 12% 336
LlamaShears.Hosting 26% 8% 27
LlamaShears.Core.Abstractions.Events 21% 6% 79
LlamaShears.Core.Abstractions.SystemPrompt 100% 100% 2
LlamaShears 65% 25% 11
LlamaShears.Core.Abstractions.PromptContext 89% 100% 2
LlamaShears.Provider.Onnx.Embeddings 4% 0% 68
LlamaShears.Plugins.Host 34% 24% 36
LlamaShears.Core.Abstractions.Agent 73% 100% 11
LlamaShears.Api 9% 1% 342
LlamaShears.Plugins 0% 100% 1
LlamaShears.Core.Eventing.Extensions 100% 100% 1
LlamaShears.Core.Abstractions.Context 100% 100% 4
LlamaShears.Provider.Ollama 44% 24% 166
LlamaShears.Core 47% 43% 861
LlamaShears.Core.Abstractions.Content 0% 100% 1
LlamaShears.Core.Abstractions.Caching 100% 100% 1
LlamaShears.Core.Eventing 93% 86% 51
LlamaShears.Core.Abstractions.Provider 78% 64% 66
LlamaShears.Core.Abstractions.Memory 100% 100% 3
LlamaShears.Hosting 33% 21% 27
LlamaShears.Core.Abstractions.Events 15% 3% 79
LlamaShears.Core.Abstractions.SystemPrompt 100% 100% 2
LlamaShears.Core.Abstractions.PromptContext 89% 100% 2
LlamaShears.Provider.Onnx.Embeddings 33% 36% 68
LlamaShears.Core.Abstractions.Agent 86% 100% 11
LlamaShears.Api 27% 29% 342
LlamaShears.Core.Eventing.Extensions 100% 100% 1
LlamaShears.Analyzers 89% 76% 199
LlamaShears.Analyzers.CodeFixes 85% 69% 60
Summary 50% (8212 / 21326) 38% (1594 / 5785) 6327
Package | Line Rate | Branch Rate | Complexity | Health -------- | --------- | ----------- | ---------- | ------ LlamaShears.Core.Abstractions.Context | 100% | 100% | 4 | ✔ LlamaShears.Provider.Ollama | 3% | 1% | 166 | ❌ LlamaShears.IntegrationTests | 87% | 73% | 71 | ✔ LlamaShears.Core | 46% | 32% | 861 | ❌ LlamaShears.Core.Abstractions.Content | 0% | 100% | 1 | ❌ LlamaShears.Core.Abstractions.Caching | 100% | 100% | 1 | ✔ LlamaShears.Core.Eventing | 96% | 76% | 51 | ✔ StrangeSoft.Plugins.Host | 20% | 21% | 87 | ❌ LlamaShears.Core.Abstractions.Provider | 32% | 20% | 66 | ❌ LlamaShears.Core.Abstractions.Memory | 0% | 100% | 3 | ❌ LlamaShears.Api.Web | 35% | 20% | 336 | ❌ LlamaShears.Hosting | 26% | 8% | 27 | ❌ LlamaShears.Core.Abstractions.Events | 21% | 6% | 79 | ❌ LlamaShears.Core.Abstractions.SystemPrompt | 100% | 100% | 2 | ✔ LlamaShears | 65% | 25% | 11 | ➖ LlamaShears.Core.Abstractions.PromptContext | 89% | 100% | 2 | ✔ LlamaShears.Provider.Onnx.Embeddings | 4% | 0% | 68 | ❌ LlamaShears.Plugins.Host | 34% | 24% | 36 | ❌ LlamaShears.Core.Abstractions.Agent | 73% | 100% | 11 | ➖ LlamaShears.Api | 10% | 3% | 342 | ❌ LlamaShears.Plugins | 0% | 100% | 1 | ❌ LlamaShears.Core.Eventing.Extensions | 100% | 100% | 1 | ✔ LlamaShears.Core.Abstractions.Context | 100% | 100% | 4 | ✔ LlamaShears.Provider.Ollama | 3% | 1% | 166 | ❌ LlamaShears.Core | 45% | 31% | 861 | ❌ LlamaShears.Core.Abstractions.Content | 0% | 100% | 1 | ❌ LlamaShears.Core.Abstractions.Caching | 100% | 100% | 1 | ✔ LlamaShears.Core.Eventing | 96% | 76% | 51 | ✔ StrangeSoft.Plugins.Host | 20% | 21% | 87 | ❌ LlamaShears.Core.Abstractions.Provider | 32% | 20% | 66 | ❌ LlamaShears.Core.Abstractions.Memory | 0% | 100% | 3 | ❌ LlamaShears.Api.Web | 22% | 12% | 336 | ❌ LlamaShears.Hosting | 26% | 8% | 27 | ❌ LlamaShears.Core.Abstractions.Events | 21% | 6% | 79 | ❌ LlamaShears.Core.Abstractions.SystemPrompt | 100% | 100% | 2 | ✔ LlamaShears | 65% | 25% | 11 | ➖ LlamaShears.Core.Abstractions.PromptContext | 89% | 100% | 2 | ✔ LlamaShears.Provider.Onnx.Embeddings | 4% | 0% | 68 | ❌ LlamaShears.Plugins.Host | 34% | 24% | 36 | ❌ LlamaShears.Core.Abstractions.Agent | 73% | 100% | 11 | ➖ LlamaShears.Api | 9% | 1% | 342 | ❌ LlamaShears.Plugins | 0% | 100% | 1 | ❌ LlamaShears.Core.Eventing.Extensions | 100% | 100% | 1 | ✔ LlamaShears.Core.Abstractions.Context | 100% | 100% | 4 | ✔ LlamaShears.Provider.Ollama | 44% | 24% | 166 | ❌ LlamaShears.Core | 47% | 43% | 861 | ❌ LlamaShears.Core.Abstractions.Content | 0% | 100% | 1 | ❌ LlamaShears.Core.Abstractions.Caching | 100% | 100% | 1 | ✔ LlamaShears.Core.Eventing | 93% | 86% | 51 | ✔ LlamaShears.Core.Abstractions.Provider | 78% | 64% | 66 | ✔ LlamaShears.Core.Abstractions.Memory | 100% | 100% | 3 | ✔ LlamaShears.Hosting | 33% | 21% | 27 | ❌ LlamaShears.Core.Abstractions.Events | 15% | 3% | 79 | ❌ LlamaShears.Core.Abstractions.SystemPrompt | 100% | 100% | 2 | ✔ LlamaShears.Core.Abstractions.PromptContext | 89% | 100% | 2 | ✔ LlamaShears.Provider.Onnx.Embeddings | 33% | 36% | 68 | ❌ LlamaShears.Core.Abstractions.Agent | 86% | 100% | 11 | ✔ LlamaShears.Api | 27% | 29% | 342 | ❌ LlamaShears.Core.Eventing.Extensions | 100% | 100% | 1 | ✔ LlamaShears.Analyzers | 89% | 76% | 199 | ✔ LlamaShears.Analyzers.CodeFixes | 85% | 69% | 60 | ✔ **Summary** | **50%** (8212 / 21326) | **38%** (1594 / 5785) | **6327** | ❌ <!-- Sticky Pull Request Commentcoverage -->
Sign in to join this conversation.
No description provided.