#!/usr/bin/env bash
# Run one dispatch, classify how it ended, and record the verdict durably.
#
# A dispatch that dies in a second writes a few hundred bytes and exits. Nothing
# reads it, so the launcher's "LAUNCHED <slug> pid=N" is the only trace and the
# run is reported as live for hours. Every dispatch goes through here so the
# record of record is the OUTCOME, never the launch.
#
# usage: dispatch-guard <slug> <outfile> -- <command> [args...]
set -uo pipefail

STATE_DIR="${DISPATCH_GUARD_STATE:-${XDG_STATE_HOME:-$HOME/.local/state}/overdeck/dispatch}"
LEDGER="$STATE_DIR/dispatches.jsonl"
# Below this an output file carries a launcher error and no model output. Every
# real run's transcript clears it in its first exchange.
MIN_OUTPUT_BYTES="${DISPATCH_GUARD_MIN_BYTES:-200}"
# A run that ends this fast never reached a model, whatever it exited with.
MIN_RUNTIME_S="${DISPATCH_GUARD_MIN_RUNTIME:-20}"

die() { printf 'dispatch-guard: %s\n' "$1" >&2; exit 2; }

if [ "${1:-}" = "status" ]; then
  [ -s "$LEDGER" ] || { echo "no dispatches recorded ($LEDGER)"; exit 0; }
  python3 - "$LEDGER" <<'PY'
import json, sys
latest = {}
for line in open(sys.argv[1]):
    line = line.strip()
    if not line:
        continue
    try:
        rec = json.loads(line)
    except ValueError:
        continue
    latest[rec.get("slug", "?")] = rec
for slug in sorted(latest):
    r = latest[slug]
    print(f'{r.get("verdict","?"):<11} {slug:<16} {r.get("elapsed_s","?")}s {r.get("bytes","?")}B {r.get("t","?")}')
    if r.get("verdict") != "OK" and r.get("detail"):
        print(f'            {r["detail"][:160]}')
bad = sum(1 for r in latest.values() if r.get("verdict") != "OK")
print(f'\n{len(latest)} dispatches, {bad} not OK')
PY
  exit 0
fi

[ $# -ge 4 ] || die "usage: dispatch-guard <slug> <outfile> -- <command> [args...]
       dispatch-guard status"
SLUG="$1"; OUT="$2"; shift 2
[ "$1" = "--" ] || die "expected -- before the command"
shift
[ $# -ge 1 ] || die "no command given"

mkdir -p "$STATE_DIR" "$(dirname "$OUT")" || die "cannot create state dir $STATE_DIR"

json_escape() {
  local s=${1//\\/\\\\}
  s=${s//\"/\\\"}
  s=${s//$'\n'/\\n}
  s=${s//$'\r'/\\r}
  s=${s//$'\t'/\\t}
  printf '%s' "$s"
}

JOURNAL="${DISPATCH_GUARD_JOURNAL:-${XDG_STATE_HOME:-$HOME/.local/state}/overdeck/items.jsonl}"

# A run nobody can see is a run nobody can trust. Same id per slug, so a retry
# supersedes its own earlier row instead of stacking a second one.
publish() {
  local verdict="$1" elapsed="$2" bytes="$3" detail="$4" severity kind
  case "$verdict" in
    OK)      severity=info; kind=progress ;;
    RUNNING) severity=info; kind=progress ;;
    *)       severity=act;  kind=alert ;;
  esac
  [ -d "$(dirname "$JOURNAL")" ] || return 0
  printf '{"id":"dispatch:%s","source":"dispatch","severity":"%s","kind":"%s","title":"%s %s","detail":"%s"}\n' \
    "$(json_escape "$SLUG")" "$severity" "$kind" "$(json_escape "$SLUG")" "$verdict" \
    "$(json_escape "${elapsed}s ${bytes}B ${detail}")" >> "$JOURNAL" 2>/dev/null || true
}

record() {
  local verdict="$1" exit_code="$2" elapsed="$3" bytes="$4" detail="$5"
  printf '{"t":"%s","slug":"%s","verdict":"%s","exit":%s,"elapsed_s":%s,"bytes":%s,"out":"%s","detail":"%s"}\n' \
    "$(date -u +%FT%TZ)" "$(json_escape "$SLUG")" "$verdict" "$exit_code" "$elapsed" "$bytes" \
    "$(json_escape "$OUT")" "$(json_escape "$detail")" >> "$LEDGER"
}

started=$(date +%s)
publish RUNNING 0 0 "started"
"$@" > "$OUT" 2>&1
code=$?
elapsed=$(( $(date +%s) - started ))
bytes=$(wc -c < "$OUT" 2>/dev/null || echo 0)
tailtext=$(tail -c 400 "$OUT" 2>/dev/null | tr '\n' ' ')

# Ordered ladder: stop at the first rung that holds. A zero exit does NOT clear
# the earlier rungs — the 16 runs this exists for all exited through the wrapper
# without a model ever seeing the prompt.
if [ "$elapsed" -lt "$MIN_RUNTIME_S" ]; then
  verdict=DIED-EARLY
  detail="ended in ${elapsed}s, under the ${MIN_RUNTIME_S}s floor: $tailtext"
elif [ "$bytes" -lt "$MIN_OUTPUT_BYTES" ]; then
  verdict=NO-OUTPUT
  detail="produced ${bytes}B, under the ${MIN_OUTPUT_BYTES}B floor: $tailtext"
elif [ "$code" -ne 0 ]; then
  verdict=FAILED
  detail="exit=$code: $tailtext"
else
  verdict=OK
  detail=""
fi

record "$verdict" "$code" "$elapsed" "$bytes" "$detail"
publish "$verdict" "$elapsed" "$bytes" "$detail"

if [ "$verdict" = OK ]; then
  printf 'dispatch-guard: OK %s (%ss, %sB) %s\n' "$SLUG" "$elapsed" "$bytes" "$OUT"
  exit 0
fi

# Loud on stderr, and loud in the ledger. A caller that ignores stderr still
# cannot read this run as alive: the ledger carries the verdict, not the launch.
{
  printf '\n'
  printf 'dispatch-guard: ######## DISPATCH %s: %s ########\n' "$SLUG" "$verdict"
  printf 'dispatch-guard: %s\n' "$detail"
  printf 'dispatch-guard: full output %s\n' "$OUT"
  printf 'dispatch-guard: ledger %s\n' "$LEDGER"
  printf '\n'
} >&2
exit 1
