Skill: bobbylindsey/resume-after-gateway-restart (version 1.0.0)

--- name: gateway-resume description: "Reliably resume work after `openclaw gateway restart` (or when you must restart the OpenClaw gateway mid-task). Uses durable one-shot cron `agentTurn` jobs (isolated session + explicit delivery) so the agent comes back after restart and replies to the same chat (Discord/Telegram/etc), usually with the same model/thinking as the requesting session. Use when: user asks for a gateway restart; you need to restart to apply config/updates; or you must ensure a post-restart follow-up message happens automatically." --- # Gateway Resume Gateway restarts kill the current agent process. If you need to restart mid-task, you must create a **durable wakeup** *before* restarting. This skill uses **cron + isolated agentTurn + explicit delivery route** (not `systemEvent`) so it: - survives restarts (cron is persisted to disk) - replies back to the same place (Discord channel, Telegram DM/topic, etc) - can pin the cron run to the **same model/thinking** that was active in the requesting session ## Procedure (recommended) ### 0) Tell the user you’re about to go down (own message) Send a short message in the current chat **before** you schedule the restart: - "Heads up — restarting the gateway for ~10–20s. I’ll be back momentarily and will resume automatically." Keep this as its **own** message (don’t bundle it with other info). **Common failure mode:** writing a plan but forgetting to actually schedule the cron + restart. If you don’t run Steps 3–4, the gateway won’t go down and you won’t come back. **Do not require a user 'go' message.** Instead, schedule the restart to occur ~15s in the future (see Step 4) so this message has time to deliver before the gateway is killed. ### 1) Save resume context Write `memory/post-restart-task.md`: ```md # Post-restart: <brief description> - Channel: <where to reply> - Was doing: <what was in progress> - Next step: <what to do next on resume> - Status: pending ``` ### 2) Resolve the return route + session model (Option B) From the current chat/session, capture: - `sessionKey` (e.g. `agent:main:discord:channel:<id>` or `agent:main:telegram:direct:<id>`) - `deliveryChannel` (e.g. `discord`, `telegram`) - `deliveryTo`: - Discord channel: `channel:<channelId>` - Discord DM: `user:<userId>` - Telegram DM/topic: use the chat id or `-100…:topic:<id>` - `modelProvider` + `model` for the session (e.g. `openai-codex` + `gpt-5.2`) - `thinkingLevel` for the session Tip: `openclaw sessions --active 240 --json` includes these fields. ### 3) Schedule two one-shot cron jobs (back message + resume) Use the bundled script (Option B: it infers route + model from the session store): ```bash skills/gateway-resume/scripts/schedule-resume-cron.sh \ --back-delay 75s \ --delay 90s \ --session-key "<sessionKey>" ``` The script will: - infer the delivery route from the session key (Discord channel/DM, Telegram DM/group) - read `openclaw sessions --json` to infer `modelProvider/model` and thinking level - schedule **two** isolated agentTurn cron jobs with explicit delivery: 1) a short, personable "I’m back" message (own message) 2) a resume job that reads `memory/post-restart-task.md` and completes the next step Notes: - Use `--delay 90s` (or more) so the job fires well after the gateway finishes restarting. - The cron job runs **isolated**, but delivers back to the captured route. - The “back” job is a *separate* message to make the restart feel responsive, even if the resume step takes longer. ### 4) Restart (delayed) Do **not** restart immediately (it can kill the process before the pre-restart message delivers). Instead schedule a restart a few seconds in the future using `systemd-run`: ```bash skills/gateway-resume/scripts/delayed-gateway-restart.sh 15 ``` This schedules a `systemctl --user restart openclaw-gateway.service` in a transient user unit. ### 5) On the cron wake The scheduled jobs should: 1) Send a short, personable "I’m back" message (by itself). 2) Then resume: - read `memory/post-restart-task.md` - do `Next step` - append a handled marker (avoid needing deletes) - reply back to the captured destination with the result Do **not** include raw markers like `BACK_FROM_RESTART` in user-facing text. ## Troubleshooting - **Nothing happens after restart**: confirm the cron jobs exist *before* restarting: - `openclaw cron list` - **Cron fires but no message is delivered**: route inference may be wrong for this `sessionKey` pattern. - Re-run the schedule script with the correct `--session-key`. - **Model inference fails**: the session may have fallen outside the `--active` window. - Edit `schedule-resume-cron.sh` to use a larger `--active` range or no filter. ## Why not `systemEvent`? `systemEvent` targets the **main** session, which may be a different model/provider and may fail (or respond in the wrong place). Isolated `agentTurn` + explicit `delivery` is deterministic.

path: scripts/delayed-gateway-restart.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Schedule a gateway restart after a short delay using systemd-run.
# This avoids killing the current agent before its "about to restart" message is delivered.
#
# Usage:
#   delayed-gateway-restart.sh 8

DELAY_SEC=${1:-15}
UNIT="openclaw-gateway-restart-$(date +%s)"

# Run systemctl restart in a separate transient user unit.
systemd-run --user --quiet --unit "$UNIT" --on-active="${DELAY_SEC}s" /usr/bin/systemctl --user restart openclaw-gateway.service

echo "Scheduled gateway restart in ${DELAY_SEC}s (unit=$UNIT)"

path: scripts/schedule-resume-cron.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Schedule a one-shot resume cron job that will run after a gateway restart.
# Designed to be created BEFORE restarting the gateway.
#
# Option B behavior:
# - infer delivery route (channel/to) from session key
# - infer model + thinking from the session store for that session key
#
# Usage:
#   schedule-resume-cron.sh --delay 90s --session-key agent:main:discord:channel:123
#
# Optional:
#   --back-delay 75s   (send a short "I'm back" message first)

DELAY="90s"
BACK_DELAY="75s"
SESSION_KEY=""
NAME_RESUME="post-restart-resume"
NAME_BACK="post-restart-back"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --delay) DELAY="$2"; shift 2;;
    --back-delay) BACK_DELAY="$2"; shift 2;;
    --session-key) SESSION_KEY="$2"; shift 2;;
    --name) NAME_RESUME="$2"; shift 2;;
    *) echo "Unknown arg: $1" >&2; exit 2;;
  esac
done

if [[ -z "$SESSION_KEY" ]]; then echo "--session-key is required" >&2; exit 2; fi

# Infer delivery route from sessionKey
DELIVERY_CHANNEL=""
DELIVERY_TO=""
case "$SESSION_KEY" in
  agent:*:discord:channel:*)
    DELIVERY_CHANNEL="discord"
    CH_ID="${SESSION_KEY##*:channel:}"
    DELIVERY_TO="channel:${CH_ID}"
    ;;
  agent:*:discord:direct:*)
    DELIVERY_CHANNEL="discord"
    U_ID="${SESSION_KEY##*:direct:}"
    DELIVERY_TO="user:${U_ID}"
    ;;
  agent:*:telegram:direct:*)
    DELIVERY_CHANNEL="telegram"
    CHAT_ID="${SESSION_KEY##*:direct:}"
    DELIVERY_TO="${CHAT_ID}"
    ;;
  agent:*:telegram:group:*)
    DELIVERY_CHANNEL="telegram"
    CHAT_ID="${SESSION_KEY##*:group:}"
    DELIVERY_TO="${CHAT_ID}"
    ;;
  *)
    echo "Unsupported session key for route inference: $SESSION_KEY" >&2
    exit 2
    ;;
esac

# Infer model + thinking from session store.
# Use a reasonably wide window to avoid missing older-but-still-relevant sessions.
# If this still fails, drop the --active filter.
SESSION_JSON=$(openclaw sessions --active 1440 --json)
MODEL_PROVIDER=$(python3 -c "import json,sys; j=json.loads(sys.argv[1]); key=sys.argv[2]; s=[x for x in j['sessions'] if x.get('key')==key]; print((s[0].get('modelProvider') or '').strip() if s else '')" "$SESSION_JSON" "$SESSION_KEY")
MODEL_NAME=$(python3 -c "import json,sys; j=json.loads(sys.argv[1]); key=sys.argv[2]; s=[x for x in j['sessions'] if x.get('key')==key]; print((s[0].get('model') or '').strip() if s else '')" "$SESSION_JSON" "$SESSION_KEY")
THINKING=$(python3 -c "import json,sys; j=json.loads(sys.argv[1]); key=sys.argv[2]; s=[x for x in j['sessions'] if x.get('key')==key]; print((s[0].get('thinkingLevel') or '').strip() if s else '')" "$SESSION_JSON" "$SESSION_KEY")

if [[ -z "$MODEL_PROVIDER" || -z "$MODEL_NAME" ]]; then
  # Fallback: try without an active filter.
  SESSION_JSON=$(openclaw sessions --json)
  MODEL_PROVIDER=$(python3 -c "import json,sys; j=json.loads(sys.argv[1]); key=sys.argv[2]; s=[x for x in j.get('sessions',[]) if x.get('key')==key]; print((s[0].get('modelProvider') or '').strip() if s else '')" "$SESSION_JSON" "$SESSION_KEY")
  MODEL_NAME=$(python3 -c "import json,sys; j=json.loads(sys.argv[1]); key=sys.argv[2]; s=[x for x in j.get('sessions',[]) if x.get('key')==key]; print((s[0].get('model') or '').strip() if s else '')" "$SESSION_JSON" "$SESSION_KEY")
  THINKING=$(python3 -c "import json,sys; j=json.loads(sys.argv[1]); key=sys.argv[2]; s=[x for x in j.get('sessions',[]) if x.get('key')==key]; print((s[0].get('thinkingLevel') or '').strip() if s else '')" "$SESSION_JSON" "$SESSION_KEY")

  if [[ -z "$MODEL_PROVIDER" || -z "$MODEL_NAME" ]]; then
    echo "Could not infer model for session key: $SESSION_KEY" >&2
    exit 2
  fi
fi
MODEL="$MODEL_PROVIDER/$MODEL_NAME"
if [[ -z "$THINKING" ]]; then THINKING="high"; fi

# Remove any previous stale jobs.
openclaw cron rm "$NAME_BACK" >/dev/null 2>&1 || true
openclaw cron rm "$NAME_RESUME" >/dev/null 2>&1 || true

# 1) "I'm back" message in its own Discord/Telegram message.
openclaw cron add \
  --name "$NAME_BACK" \
  --at "$BACK_DELAY" \
  --agent main \
  --session isolated \
  --session-key "$SESSION_KEY" \
  --model "$MODEL" \
  --thinking "$THINKING" \
  --message "You are back from a gateway restart. Send ONE short, personable message confirming you're back (no extra content). Example: 'Back online — grabbing the thread now.'" \
  --announce \
  --channel "$DELIVERY_CHANNEL" \
  --to "$DELIVERY_TO" \
  --delete-after-run

# 2) Resume the pending task.
openclaw cron add \
  --name "$NAME_RESUME" \
  --at "$DELAY" \
  --agent main \
  --session isolated \
  --session-key "$SESSION_KEY" \
  --model "$MODEL" \
  --thinking "$THINKING" \
  --message "Resume work after gateway restart. Read memory/post-restart-task.md. If it exists and Status is not handled, perform its Next step. Then append a line 'Status: handled' plus an ISO timestamp to the file. Reply with the result only (don't mention thinking/model)." \
  --announce \
  --channel "$DELIVERY_CHANNEL" \
  --to "$DELIVERY_TO" \
  --delete-after-run

echo "Scheduled $NAME_BACK in $BACK_DELAY and $NAME_RESUME in $DELAY to $DELIVERY_CHANNEL/$DELIVERY_TO (sessionKey=$SESSION_KEY, model=$MODEL, thinking=$THINKING)"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)