Skill: swiftking100/restart-task-recovery (version 1.0.0)

--- name: restart-task-recovery description: Preserve and resume in-progress multi-agent work across OpenClaw config patch/apply restarts. Use when a restart is required during active tasks, when users ask to minimize interruption, or when agent runs/tool calls were interrupted by gateway restart/timeouts. --- # Restart Task Recovery Use this workflow to maximize successful recovery after OpenClaw restart. ## 1) Pre-restart checkpoint (required) Before any `gateway.config.patch`, `gateway.config.apply`, `gateway.update.run`, or `gateway.restart`: 1. List active sessions that may be impacted (`sessions_list`). 2. For each active work session, capture the latest context (`sessions_history`, limit 20-50). 3. Write a compact checkpoint file at: - `memory/restart-checkpoints/<YYYY-MM-DD>/<HHmmss>.md` 4. Include per session: - sessionKey / label / agent - goal - last completed step - next exact step - blocked dependencies (if any) - a ready-to-send resume message (1-2 lines) Keep checkpoint concise and executable. ## 2) Restart with explicit recovery intent When calling gateway restart/config change, set `note` to include recovery intent, e.g.: - “配置已更新并重启；将按 checkpoint 恢复中断任务。” ## 3) Post-restart recovery sweep After restart: 1. Re-list sessions (`sessions_list`) and compare against checkpoint. 2. For each interrupted/idle target session, send resume message via `sessions_send`: - “Continue where you left off. Last completed: <x>. Next: <y>. If previous tool call failed, retry from <y>.” 3. Do **not** poll in tight loops. Check on-demand only. 4. Summarize recovery status to user: - recovered sessions - still blocked sessions - manual follow-up needed ## 4) Idempotent task design rules When resuming tasks, enforce: 1. Re-run-safe steps (idempotency key / upsert / duplicate-safe writes). 2. Small step boundaries with explicit “done markers”. 3. External writes batched, not one-by-one loops. 4. On uncertainty, verify state first then continue. ## 5) V2 automation helper Use script: `scripts/build_checkpoint.py` to generate checkpoint markdown from structured JSON. Example: ```bash cat session-snapshot.json | python3 scripts/build_checkpoint.py memory/restart-checkpoints/$(date +%F)/$(date +%H%M%S).md ``` Expected stdin JSON shape: ```json { "sessions": [ { "sessionKey": "agent:engineer:main", "agentId": "engineer", "goal": "Finish regression verification", "lastDone": "401/幂等/时区/retention case passed", "nextStep": "Publish final acceptance summary", "blockers": "none" } ] } ``` ## 6) V3 resume-plan automation Use script: `scripts/generate_resume_plan.py` to parse the latest checkpoint and produce a structured resume plan. Example: ```bash python3 scripts/generate_resume_plan.py memory/restart-checkpoints/2026-03-09/162200.md /tmp/resume-plan.json ``` Then send each `items[].resumeMessage` to `items[].sessionKey` via `sessions_send`. Rules: - Send once per session (no loop polling). - If a session is already active and progressing, skip resend. - After sends, post one concise recovery summary to user. ## 7) V4 one-click recovery payload generator Use script: `scripts/recover_from_latest_checkpoint.py`. It auto-selects the latest checkpoint file and emits a ready JSON payload list for `sessions_send` calls. Examples: ```bash # Use latest checkpoint automatically python3 scripts/recover_from_latest_checkpoint.py > /tmp/recover-actions.json # Use a specific checkpoint python3 scripts/recover_from_latest_checkpoint.py memory/restart-checkpoints/2026-03-09/162200.md > /tmp/recover-actions.json ``` Execution guidance: - Read `/tmp/recover-actions.json` - Execute each `actions[]` item with `sessions_send` - Post one concise summary to user ## 8) V5 pre-resume verifier + manual confirmation gate Use script: `scripts/pre_resume_verify.py` to score resume actions before sending. Examples: ```bash python3 scripts/pre_resume_verify.py /tmp/recover-actions.json /tmp/recover-verified.json ``` Behavior: - Marks each action as `risk=normal|high` - `high` risk actions are set to `decision=hold` and `requiresManualConfirm=true` - Only send `decision=send` automatically - Ask user confirmation before executing held actions Recommended execution flow: 1. Generate actions with V4 2. Verify with V5 3. Send all `decision=send` 4. Present `decision=hold` list to user for explicit confirmation ## 9) V6 execution-plan generator (auto-send safe items) Use script: `scripts/execute_verified_recovery.py` with V5 output. Example: ```bash python3 scripts/execute_verified_recovery.py /tmp/recover-verified.json > /tmp/recover-exec.json ``` Behavior: - Emits `sendActions[]` for auto-safe resumes (`decision=send`) - Emits `holdForManualConfirm[]` for risky resumes (`decision=hold`) Execution: 1. Execute all `sendActions[]` with `sessions_send` 2. Ask user to confirm `holdForManualConfirm[]` 3. Execute confirmed held items 4. Post concise summary ## 10) Message templates Read and use: `references/templates.md`

path: scripts/generate_resume_plan.py
content: #!/usr/bin/env python3
import json
import re
import sys
from pathlib import Path


def parse_checkpoint(text: str):
    blocks = re.split(r"\n## ", "\n" + text)
    items = []
    for b in blocks:
        b = b.strip()
        if not b:
            continue
        lines = b.splitlines()
        session_key = lines[0].strip()
        if session_key.startswith('#') or ':' not in session_key:
            continue
        fields = {}
        for ln in lines[1:]:
            m = re.match(r"-\s*([^:]+):\s*(.*)", ln.strip())
            if m:
                fields[m.group(1).strip()] = m.group(2).strip()
        msg = fields.get("Resume message") or (
            f"Continue where you left off. Last completed: {fields.get('Last done', '(unknown)')}. Next: {fields.get('Next', '(unknown)')}."
        )
        items.append(
            {
                "sessionKey": session_key,
                "agent": fields.get("Agent", "unknown"),
                "goal": fields.get("Goal", ""),
                "resumeMessage": msg,
            }
        )
    return items


def main():
    if len(sys.argv) < 2:
        print("Usage: generate_resume_plan.py <checkpoint.md> [out.json]", file=sys.stderr)
        sys.exit(1)

    cp = Path(sys.argv[1]).expanduser()
    out = Path(sys.argv[2]).expanduser() if len(sys.argv) > 2 else None

    text = cp.read_text(encoding="utf-8")
    items = parse_checkpoint(text)

    payload = {"count": len(items), "items": items}

    if out:
        out.parent.mkdir(parents=True, exist_ok=True)
        out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
        print(str(out))
    else:
        print(json.dumps(payload, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


path: scripts/pre_resume_verify.py
content: #!/usr/bin/env python3
import json
import sys
from datetime import datetime
from pathlib import Path


def main():
    if len(sys.argv) < 2:
        print("Usage: pre_resume_verify.py <recover-actions.json> [out.json]", file=sys.stderr)
        sys.exit(1)

    src = Path(sys.argv[1]).expanduser()
    out = Path(sys.argv[2]).expanduser() if len(sys.argv) > 2 else None

    data = json.loads(src.read_text(encoding="utf-8"))
    actions = data.get("actions", [])

    verified = []
    for a in actions:
        params = a.get("params", {})
        msg = params.get("message", "")
        # Heuristic risk gate: if message hints external write/retry, require manual confirm first.
        risky = any(k in msg.lower() for k in ["retry", "write", "create", "update", "delete", "payment", "transfer"])
        verified.append(
            {
                "sessionKey": params.get("sessionKey"),
                "message": msg,
                "risk": "high" if risky else "normal",
                "requiresManualConfirm": bool(risky),
                "decision": "hold" if risky else "send",
                "reason": "Potential non-idempotent external write" if risky else "Safe to resume",
            }
        )

    result = {
        "generatedAt": datetime.now().isoformat(timespec="seconds"),
        "source": str(src),
        "count": len(verified),
        "verified": verified,
    }

    txt = json.dumps(result, ensure_ascii=False, indent=2)
    if out:
        out.parent.mkdir(parents=True, exist_ok=True)
        out.write_text(txt, encoding="utf-8")
        print(str(out))
    else:
        print(txt)


if __name__ == "__main__":
    main()


path: scripts/recover_from_latest_checkpoint.py
content: #!/usr/bin/env python3
import json
import re
import sys
from pathlib import Path
from datetime import datetime


def latest_checkpoint(root: Path):
    files = sorted(root.glob("*/*.md"))
    return files[-1] if files else None


def parse_checkpoint(text: str):
    blocks = re.split(r"\n## ", "\n" + text)
    items = []
    for b in blocks:
        b = b.strip()
        if not b:
            continue
        lines = b.splitlines()
        session_key = lines[0].strip()
        if session_key.startswith('#') or ':' not in session_key:
            continue
        fields = {}
        for ln in lines[1:]:
            m = re.match(r"-\s*([^:]+):\s*(.*)", ln.strip())
            if m:
                fields[m.group(1).strip()] = m.group(2).strip()
        resume = fields.get("Resume message") or (
            f"Continue where you left off. Last completed: {fields.get('Last done', '(unknown)')}. Next: {fields.get('Next', '(unknown)')}."
        )
        items.append({"sessionKey": session_key, "message": resume})
    return items


def main():
    root = Path("memory/restart-checkpoints")
    cp = Path(sys.argv[1]).expanduser() if len(sys.argv) > 1 else latest_checkpoint(root)
    if not cp or not cp.exists():
        print("No checkpoint file found", file=sys.stderr)
        sys.exit(1)

    items = parse_checkpoint(cp.read_text(encoding="utf-8"))

    out = {
        "generatedAt": datetime.now().isoformat(timespec="seconds"),
        "checkpoint": str(cp),
        "count": len(items),
        "actions": [
            {
                "tool": "sessions_send",
                "params": {"sessionKey": i["sessionKey"], "message": i["message"]},
            }
            for i in items
        ],
    }
    print(json.dumps(out, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


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

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
WS="$(cd "$ROOT/../.." && pwd)"
TMPDIR="${TMPDIR:-/tmp}"

mkdir -p "$WS/tmp"
cat > "$WS/tmp/session-snapshot.json" <<'JSON'
{
  "sessions": [
    {
      "sessionKey": "agent:engineer:main",
      "agentId": "engineer",
      "goal": "Regression verify",
      "lastDone": "401/idempotency/timezone passed",
      "nextStep": "Publish acceptance summary",
      "blockers": "none"
    },
    {
      "sessionKey": "agent:qa:main",
      "agentId": "qa",
      "goal": "Run retention checks",
      "lastDone": "Case set loaded",
      "nextStep": "Retry write results to report",
      "blockers": "none"
    }
  ]
}
JSON

STAMP_DATE="$(date +%F)"
STAMP_TIME="$(date +%H%M%S)"
CP="$WS/memory/restart-checkpoints/$STAMP_DATE/$STAMP_TIME.md"

cat "$WS/tmp/session-snapshot.json" | python3 "$ROOT/scripts/build_checkpoint.py" "$CP" >/dev/null
python3 "$ROOT/scripts/recover_from_latest_checkpoint.py" "$CP" > "$TMPDIR/recover-actions.json"
python3 "$ROOT/scripts/pre_resume_verify.py" "$TMPDIR/recover-actions.json" "$TMPDIR/recover-verified.json" >/dev/null
python3 "$ROOT/scripts/execute_verified_recovery.py" "$TMPDIR/recover-verified.json" > "$TMPDIR/recover-exec.json"

echo "=== Smoke Test Done ==="
echo "checkpoint: $CP"
echo "actions:    $TMPDIR/recover-actions.json"
echo "verified:   $TMPDIR/recover-verified.json"
echo "exec-plan:  $TMPDIR/recover-exec.json"

echo
echo "Summary:"
python3 - <<'PY'
import json, os
p=os.environ.get('TMPDIR','/tmp')
with open(f"{p}/recover-exec.json","r",encoding='utf-8') as f:
    d=json.load(f)
print(f"- sendActions: {len(d.get('sendActions',[]))}")
print(f"- holdForManualConfirm: {len(d.get('holdForManualConfirm',[]))}")
PY


path: scripts/build_checkpoint.py
content: #!/usr/bin/env python3
import json
import sys
from datetime import datetime
from pathlib import Path


def main():
    if len(sys.argv) < 2:
        print("Usage: build_checkpoint.py <output-file>", file=sys.stderr)
        sys.exit(1)

    out = Path(sys.argv[1]).expanduser()
    raw = sys.stdin.read().strip()
    if not raw:
        print("Expected JSON on stdin", file=sys.stderr)
        sys.exit(2)

    data = json.loads(raw)
    sessions = data.get("sessions", [])
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    lines = []
    lines.append(f"# Restart checkpoint ({now})")
    lines.append("")

    for s in sessions:
        key = s.get("sessionKey", "unknown")
        agent = s.get("agentId", "unknown")
        goal = s.get("goal", "(fill)")
        done = s.get("lastDone", "(fill)")
        nxt = s.get("nextStep", "(fill)")
        blockers = s.get("blockers", "none")

        lines.append(f"## {key}")
        lines.append(f"- Agent: {agent}")
        lines.append(f"- Goal: {goal}")
        lines.append(f"- Last done: {done}")
        lines.append(f"- Next: {nxt}")
        lines.append(f"- Blockers: {blockers}")
        lines.append(
            f"- Resume message: Continue where you left off. Last completed: {done}. Next: {nxt}."
        )
        lines.append("")

    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text("\n".join(lines), encoding="utf-8")
    print(str(out))


if __name__ == "__main__":
    main()


path: scripts/execute_verified_recovery.py
content: #!/usr/bin/env python3
import json
import sys
from pathlib import Path


def main():
    if len(sys.argv) < 2:
        print("Usage: execute_verified_recovery.py <recover-verified.json>", file=sys.stderr)
        sys.exit(1)

    src = Path(sys.argv[1]).expanduser()
    data = json.loads(src.read_text(encoding="utf-8"))
    verified = data.get("verified", [])

    send = [v for v in verified if v.get("decision") == "send"]
    hold = [v for v in verified if v.get("decision") == "hold"]

    # This script outputs execution plan only; caller executes tool calls.
    out = {
        "sendCount": len(send),
        "holdCount": len(hold),
        "sendActions": [
            {
                "tool": "sessions_send",
                "params": {
                    "sessionKey": v.get("sessionKey"),
                    "message": v.get("message"),
                },
            }
            for v in send
        ],
        "holdForManualConfirm": [
            {
                "sessionKey": v.get("sessionKey"),
                "reason": v.get("reason", "Potential non-idempotent write"),
                "message": v.get("message"),
            }
            for v in hold
        ],
    }

    print(json.dumps(out, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


path: references/templates.md
content: # Templates

## Checkpoint entry template

```md
### <sessionKey>
- Agent: <agent>
- Goal: <goal>
- Last done: <done>
- Next: <next>
- Blockers: <none|...>
- Resume message: Continue where you left off. Last completed: <done>. Next: <next>.
```

## User update template

```md
重启已完成，已开始恢复任务：
- 已恢复：<list>
- 待确认：<list>
- 需人工处理：<list>
```

## Manual confirmation template (V5)

```md
以下恢复动作存在潜在非幂等风险，需你确认后再执行：
- <sessionKey>: <reason>
- <sessionKey>: <reason>

回复“确认执行全部”或逐条确认。
```

## Resume message variants

### Generic
Continue where you left off. The previous model attempt may have failed or timed out. Resume from the next unfinished step.

### Tool-call retry
Continue where you left off. Last completed: <done>. Retry the failed tool call and continue with <next>.

### Validation-first
Continue where you left off. First verify current state/results, then proceed with <next> to avoid duplicate writes.


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