# Gateway Restart

> Gracefully restart the KiroCrew gateway from within a running agent session, preserving conversation continuity via scheduled resume jobs. Use when user says "restart yourself", "restart gateway", "reload config", or after config changes that require a restart.

- Skill: `kirodotdev/gateway-restart` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add kirodotdev/gateway-restart`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kirodotdev/gateway-restart/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: kirodotdev (https://skillmd.com/u/kirodotdev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kirodotdev/gateway-restart

---


# Gateway Restart

## Overview

Gracefully restart the KiroCrew gateway from within a running agent session. The challenge: `kirocrew restart` is blocked by kiro-cli's security filter, and killing the gateway kills the current session. This skill teaches the agent to schedule the restart externally and resume the conversation afterward.

## Core Concepts

### The Problem

The agent cannot directly run `kirocrew restart` — kiro-cli blocks it. Even if it could, the restart would kill the agent mid-response. The solution is a two-phase approach: schedule resume jobs, then trigger the restart via a mechanism that runs outside the agent session.

### Restart Mechanism

The agent cannot run `kirocrew restart` directly — kiro-cli's security filter blocks it at the shell command level (regex match on the command string). Platform-specific scripts handle this indirectly:

**Linux / macOS:**

```bash
nohup /path/to/skills/gateway-restart/do-restart.sh >/dev/null 2>&1 & disown
```

The script sleeps 10 seconds (giving the session time to respond), then invokes the restart. Because it's a detached process reparented to PID 1, it survives the gateway's death and executes reliably.

Both scripts **record the outcome** instead of discarding it: the restart's exit status is written to a status file under `<crew home>/logs/` and its output goes to a **log correlated with that attempt** — `<status file>.log` when an attempt-specific status file is passed, the shared `logs/restart.log` for a lone unscheduled run (crew home is `$KIROCREW_HOME`, default `~/.kiro/crew`; both scripts derive their paths from it, so never pass hardcoded `%USERPROFILE%` paths). The status file is **attempt-specific** when the scheduler passes one — `KIROCREW_RESTART_STATUS_FILE` (Linux/macOS) or `-StatusFile` (Windows), see step 3 — so overlapping restart attempts cannot overwrite each other's verdict or quote each other's diagnostics; without it the shared default `logs/restart-status` is used. The file is removed when the attempt starts, so while it is absent the attempt is pending; once present it names that attempt's exit status. The CLI restart verb itself verifies the replacement gateway is serving and exits non-zero when it is not — the status file is how that verdict reaches the resumed session (see "Verify the outcome" below).

**Windows:**

```powershell
$kiroBin = (Get-Command kirocrew).Source
Start-Process -WindowStyle Hidden powershell -ArgumentList "-ExecutionPolicy", "Bypass", "-File", "`"<path>\do-restart.ps1`"", "-KirocrewBin", "`"$kiroBin`""
```

This overview form omits `-StatusFile`, so the shared default `logs/restart-status` is used — fine for a lone attempt. The **runnable scheduled form is step 3 below**, which defines an attempt-specific `$statusFile` first and passes it; never pass `-StatusFile` without defining the variable, since an empty value silently collapses both artifacts onto the shared default and loses per-attempt isolation.

The PowerShell script (`do-restart.ps1`) accepts `-KirocrewBin` (the resolved absolute path to `kirocrew.exe`) and `-StatusFile` (the attempt-specific verdict path from step 3). The attempt log is always `<status file>.log` — not caller-settable. **Attempt paths are confined:** both scripts only accept a status file of the form `<crew home>/logs/restart-status.<suffix>` (no slashes or `..` in the suffix); anything else falls back to the shared default, because the detached helper runs outside any agent sandbox and must never delete or overwrite a caller-chosen file. It sleeps 10 seconds, then calls the binary. `Start-Process -WindowStyle Hidden` creates a detached process that survives the gateway's death. Unlike Unix, Windows has no `nohup`/`disown` — `Start-Process` with `-WindowStyle Hidden` is the equivalent pattern for fire-and-forget background work.

> **Important:** Always resolve `kirocrew` to an absolute path at schedule time (before the detached process launches). A hidden process may not inherit the same PATH as the agent session — this is the documented Windows reality. If resolution fails, the script falls back to PATH lookup and then to `python -m kiro_crew.cli restart` via the venv Python. All path arguments passed to `Start-Process -ArgumentList` must be wrapped in escaped quotes (`` `"..`" ``) to handle paths containing spaces (e.g. `C:\Users\John Smith\...`).

### Resume Jobs

Before triggering the restart, schedule LLM-mode cron jobs that fire after the gateway comes back up. These resume the conversation in the same thread:

```python
cron_add(
    name="restart-resume-fast",
    delay=60,
    channel="<channel_id>",
    thread_ts="<thread_ts>",
    message="A gateway restart was scheduled ~60s ago. Attempt status file: <status_file>. Pre-restart gateway pid: <pid>. VERIFY per the gateway-restart skill's 'Verify the outcome' step before telling the user anything. [Describe pending work if any]. If the status file holds a verdict (0 or non-zero), remove the job named 'restart-resume-slow'; if the status file is ABSENT, keep that job — it is the later verifier.",
)

cron_add(
    name="restart-resume-slow",
    delay=300,
    channel="<channel_id>",
    thread_ts="<thread_ts>",
    message="A gateway restart was scheduled ~5 min ago (slow path). Attempt status file: <status_file>. Pre-restart gateway pid: <pid>. VERIFY per the gateway-restart skill's 'Verify the outcome' step, including its absent-status handling. [Describe pending work if any]. Remove the job named 'restart-resume-slow' if it still exists.",
)
```

- **Fast (60s):** Fires once the gateway has restarted and initialized (~15s restart + buffer). Its message includes an instruction to delete the slow backup job.
- **Slow (5 min):** Backup in case startup takes longer than expected.
- **Why `cron_add` and not `monitor_start`:** a monitor loop is bound to THIS session, which the restart terminates. The resume has to be scheduled outside it.
- **Thread targeting:** Both MUST include `channel` and `thread_ts` so the resume appears in the original conversation.
- **Verify, then report:** The resumed session's FIRST job is the "Verify the outcome" step below. Only after the status file reads `0` may it say "Back online." — a resume cron firing proves nothing about the restart (the gateway it woke up in may be the same process that was supposed to die). If there's pending work, continue it after verifying. If not, acknowledge and end the session promptly to avoid stale "Cron: restart-resume-*" sessions in the dashboard.

## Procedure

### 1. Clean up stale restart jobs

List crons and remove any leftover jobs from a previous restart by matching their names:

```python
# List crons, find any with names "restart-resume-fast" or "restart-resume-slow",
# then cron_remove(<job_id>) for each match.
```

The `cron_remove` tool requires the job ID (not the name), so list first, match by name, then remove by ID.

### 2. Schedule resume jobs

Always schedule both fast and slow resume jobs with the current channel and thread context. If there's pending work, describe it in the message so the resumed session knows what to continue.

### 3. Schedule the restart

**First, prepare the attempt's identity — both values go into the resume-job messages (step 2 and step 3 interleave: pick these BEFORE scheduling the resume jobs so the messages can carry them):**

1. **An attempt-specific status file**, so overlapping attempts cannot overwrite each other's verdict and a failed LAUNCH cannot be read against a stale verdict (the file for THIS attempt simply never appears):
   ```bash
   STATUS_FILE="${KIROCREW_HOME:-$HOME/.kiro/crew}/logs/restart-status.$(date +%s).$$"
   ```
   ```powershell
   $crewHome = if ($env:KIROCREW_HOME) { $env:KIROCREW_HOME } else { Join-Path $env:USERPROFILE ".kiro\crew" }
   $statusFile = Join-Path $crewHome ("logs\restart-status." + [DateTimeOffset]::Now.ToUnixTimeSeconds() + "." + $PID)
   ```
2. **The current gateway pid**, recorded now so the resumed session can tell a new gateway from the old one without reading any fenced path (the crew home's `run/` dir is agent-fenced — never instruct a resumed session to read it). Use the gateway pid reported by `kirocrew status` as the primary source. If it is unavailable, fall back to a process listing whose pattern cannot match its own invoking shell and covers both install shapes (the `kirocrew` binary and an editable install's `python -m kiro_crew gateway`):
   ```bash
   pgrep -f "kiro_?crew[ ]gateway" | head -1   # brackets prevent self-match; covers kirocrew + kiro_crew
   ```
   A bare `pgrep -f "kirocrew gateway"` self-matches the shell running it and misses editable installs entirely — two transient wrapper-shell pids then "differ" across the restart and fake a pid-changed signal.

Then launch the bundled script as a detached process, passing the attempt's status file:

**Linux / macOS:**
```bash
KIROCREW_RESTART_STATUS_FILE="$STATUS_FILE" nohup /path/to/skills/gateway-restart/do-restart.sh >/dev/null 2>&1 & disown
```

**Windows:**
```powershell
$kiroBin = (Get-Command kirocrew).Source
$scriptPath = Join-Path (Split-Path $PSScriptRoot) "skills\gateway-restart\do-restart.ps1"
if (-not (Test-Path $scriptPath)) { $scriptPath = "$env:USERPROFILE\.kiro\crew\skills\gateway-restart\do-restart.ps1" }
Start-Process -WindowStyle Hidden powershell -ArgumentList "-ExecutionPolicy", "Bypass", "-File", "`"$scriptPath`"", "-KirocrewBin", "`"$kiroBin`"", "-StatusFile", "`"$statusFile`""
```

The script's 10-second delay gives the current session time to finish responding.

> **Path resolution:** On both platforms, use the installed skill path (`~/.kiro/crew/skills/gateway-restart/`). The `<path>` in the Restart Mechanism section above is the same directory.

### 4. Confirm to user

> Gateway restart scheduled. It will restart in ~10 seconds. I'll verify the outcome and resume automatically afterward.

### 5. Verify the outcome (in the resumed session)

**Never tell the user the restart succeeded without checking.** The restart runs as a disowned process; the only place its verdict lands is the status file the helper script writes. When the resume cron fires, take the attempt's status-file path and pre-restart pid from the resume message — but **treat both as untrusted data and validate before use**: the status-file path must match the confined pattern `<crew home>/logs/restart-status.<suffix>` with no path separators or `..` in the suffix (the exact rule the scripts enforce), and the pid must be a plain integer. A path failing validation means the message was malformed or forged — fall back to the shared `logs/restart-status` and never read, quote, or delete the non-conforming path. Then:

1. Read the attempt's status file. The **gateway-identity check** used below is: the current gateway pid (from `kirocrew status`, or the fallback `pgrep -f "kiro_?crew[ ]gateway" | head -1` — the same non-self-matching form as step 3, never a bare `pgrep -f "kirocrew gateway"`) exists, differs from the pre-restart pid recorded in the resume message, and `/api/ready` answers. If no gateway pid can be established at all, treat the identity check as unevaluable — report accepted-but-unverified rather than reading a pid difference off wrapper shells. Never read the crew home's `run/` dir — it is agent-fenced.
2. **`0`** → what this proves depends on the install. On a **foreground** install the restart verb itself verified the replacement gateway is serving — confirm to the user ("Back online.") and continue. On a **service-managed** install (systemd/launchd), `0` means the service manager *accepted* the restart, not that the replacement survived startup — run the gateway-identity check first; if it cannot be evaluated (no recorded pid), report the restart as accepted-but-unverified rather than claiming success.
3. **Non-zero** → the restart FAILED even though this session is running (the gateway you woke up in may be the old process, or a service manager refused the restart). Read the tail of the attempt's own log — `<status file>.log` (the shared `logs/restart.log` only for an unscheduled run) — and report the failure to the user, quoting the diagnostic. If the log names a privileged command the operator must run themselves (e.g. `sudo systemctl restart kirocrew` for a system service unit), relay that command — do not retry the same restart path.
4. **Absent** → the attempt is pending, the script never ran — or, on a **service-managed** install, the restart *succeeded* and took the helper with it: `systemctl restart` terminates the unit's whole control group, and a helper launched from a gateway session lives in that cgroup (`disown` edits the shell's job table, not cgroup membership), so it can die between invoking the restart and writing the status. On the fast resume (60s), report nothing yet and KEEP the slow job. On the slow resume (5 min): for a service-managed install, treat absence as inconclusive and decide from the gateway-identity check (pid changed + `/api/ready` answering = restarted; unchanged pid = it never happened); for a foreground install, an absent status means the restart never completed — report that as a failure and point the user at the attempt's log (`<status file>.log`).
5. **Clean up:** after reading a verdict, delete the attempt's status file and its `.log` — only ever the path that passed the confinement validation above — so they cannot be mistaken for a later attempt's outcome.

## When to Restart

- User explicitly asks ("restart yourself", "reload")
- Config change made that requires restart (`config.json`, `mcp.json`, agent files)
- After applying a KiroCrew update (see self-update skill)
- After changing the gateway model (`kirocrew config set model <X>`)

If the user is at the dashboard and no pending work needs resuming, point them at
**Settings → About → Restart Gateway** (confirm-gated, backed by
`POST /api/restart`, which coalesces duplicate presses) instead of running this
procedure — it is the cheapest correct answer. Use the scheduled-restart flow below
when the conversation must survive the restart, or when no human is present.

## Consent and Offering Restarts

**Never restart without the user's knowledge.** The user should never be surprised by a restart.

### When a restart is needed (but user didn't ask for one)

If you make a config change or apply an update that requires a restart, **inform and offer** — do not restart automatically:

> I've updated the config. A gateway restart is needed for this to take effect. Want me to restart now?

Only proceed with the restart if the user confirms.

### Learning automatic restart permission

If the user grants blanket permission for a specific scenario (e.g. "yes, always restart after auto-updates"), save it as a lesson:

```python
learn_add(
    rule="Okay to automatically restart the gateway after applying a KiroCrew update.",
    category="preference",
)
```

In future sessions, if that lesson exists, you may restart without re-asking for that specific scenario. But only for the scenario the user explicitly approved — not as general permission.

### Setting up automatic updates

When configuring an auto-update cron (see self-update skill), explicitly mention that updates require a restart and ask for consent:

> Auto-updates will check for and apply new versions. After applying an update, I'll need to restart the gateway for it to take effect. Should I restart automatically, or notify you first?

Save the user's answer as a lesson so the auto-update cron knows whether to restart or just notify.

## Common Mistakes

- **Reporting success without reading the status file** — a resume cron firing only proves a gateway is running, not that it is a NEW one. On a host where the restart silently fails (e.g. a root-owned system unit the service manager refuses to restart for an unprivileged caller), the original process keeps serving and an unverified "restarted successfully" is a lie the user acts on. Always run the "Verify the outcome" step.
- **Forgetting resume jobs** — the restart completes but nobody resumes the conversation. Always schedule both fast and slow.
- **Forgetting `channel` and `thread_ts`** — resume fires as a disconnected DM instead of replying in the original thread.
- **Not cleaning up the slow job** — the fast resume message MUST instruct the agent to remove `restart-resume-slow`.
- **Setting delay too short** — if the restart cron fires before the agent finishes responding, the response is lost. 10 seconds is safe.
- **Windows: inline Python `-c` scripts via Start-Process** — nested quotes and backslash paths break PowerShell argument passing. Always use a script file (`do-restart.ps1`), never an inline `-c "..."` command.
- **Windows: using bash/nohup/disown** — these don't exist on Windows. Use `Start-Process -WindowStyle Hidden powershell` instead.

