textmachine/eval/dovodka/predpolet.sh

203 lines
14 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# predpolet.sh — ПРЕД-ПОЛЁТ: один юнит × пять клеток, и всё, что обязано быть проверено ДО пачки.
#
# Порядок намеренный: сначала $0-ГАРД ОСИ на всех пяти копиях, и только потом деньги. Если ось
# схлопнулась, это видно ДО первой платной клетки, а не после ста.
#
# ⛔ --resnapshot НЕ ДАЁТСЯ НИКОГДА.
set -uo pipefail
B="${B:-$HOME/books/gu-zhenren/probe-4axes}"
REPO="${REPO:-/home/ubuntu/projects/textmachine}"
KEYS="${KEYS:-$REPO/backend/.env}"
PY="$REPO/eval/.venv/bin/python"
SEED=20260902
SANCTION=9.50
UNIT="${UNIT:-1}"
PIN="$REPO/eval/dovodka/prereg-4axes/prereg.json"
declare -A W=( [p7-off]=0.30 [p9-off]=0.30 [p7-low]=0.1666666667 [p9-low]=0.1666666667 [glm-off]=0.0666666667 )
ro() { sqlite3 "file:$1?mode=ro" "$2" 2>/dev/null; }
spend_of() { ro "$1" "select coalesce(sum(committed_usd+reserved_usd),0) from spend;" || echo 0; }
BASE_DB="${BASE_SNAPSHOT_DB:-$(ls -1t "$B"/backups/*.db 2>/dev/null | head -1)}"
BASE=$(spend_of "$BASE_DB")
echo "═══ ПРЕД-ПОЛЁТ · юнит $UNIT · база \$$BASE (из $BASE_DB) ═══"
# ─────────────────────────────────────────────────────────────────────────────────────────────
# ЭТАП 1 — $0-ГАРД ОСИ. Потолок «база + $0.0001» отказывает ПЕРВОЙ ЖЕ редакторской резервации,
# но к этому моменту движок уже: разрешил черновики из чекпойнтов за $0, прогнал майнинг и
# терминолог по чекпойнтам за $0, ВЫЧИСЛИЛ edit-снапшот и завёл edit-джоб. То есть провенанс
# оси ложится в БД БЕЗ ЕДИНОГО ПЛАТНОГО ВЫЗОВА.
# ─────────────────────────────────────────────────────────────────────────────────────────────
echo; echo "── ЭТАП 1: гард оси за \$0 (потолок отказывает первой edit-резервации)"
# ⛔ ПОТОЛОК ГАРДА — «база + 0.04», А НЕ «+0.0001». Куплено ошибкой: при +0.0001 леджер отказывал и
# ТЕРМИНОЛОГУ, а его отказ — не halt, а `break` с логом (terminologist.go:807), то есть банк «не
# двигался» лишь потому, что не на что было купить. Ложная неподвижная точка. 0.04 лежит НИЖЕ
# редакторской резервации (≈16000·3.96/1e6 + 6000·1.32/1e6 ≈ $0.071) и ВЫШЕ батча терминолога
# (≈$0.012): если точка ложная, терминолог купит — и сверка леджера это УВИДИТ.
TINY=$($PY -c "print(f'{$BASE + 0.04:.6f}')")
for arm in p7-off p7-low p9-off p9-low glm-off; do
# ⛔ ПАУЗА КУПЛЕНА ИСПОЛНЕНИЕМ, А НЕ ОСТОРОЖНОСТЬЮ: `preflightBackup` (cmd/tmctl/backup.go) пишет
# точку восстановления в <каталог project_db>/backups/<ts>.db с точностью до СЕКУНДЫ и ОТКАЗЫВАЕТ
# стартовать, если файл уже есть («refusing to overwrite a restore point»). Пять рук лежат в одном
# каталоге, и запуск двух в одну секунду роняет вторую кодом 1 ещё до всякой работы. Поймано на
# $0-гарде оси: три руки из пяти упали именно так. В пачке это остановило бы прогон на сотне клеток.
sleep 2
BEFORE=$(spend_of "$B/arms/$arm.db")
"$B/bin/tmctl" translate --config "$B/arms/$arm.book.yaml" --keys-file "$KEYS" \
--verify-bank --max-units 1 --ceiling-usd "$TINY" > "$B/logs/axis-$arm.log" 2>&1
rc=$?
AFTER=$(spend_of "$B/arms/$arm.db")
printf " %-8s rc=%-2s леджер %s → %s" "$arm" "$rc" "$BEFORE" "$AFTER"
if [ "$(printf '%.6f' "$BEFORE")" != "$(printf '%.6f' "$AFTER")" ]; then
echo " ⛔ ДЕНЬГИ ПОТРАЧЕНЫ НА ГАРДЕ — так быть не должно, СТОП"; exit 1
fi
echo " ✓ \$0"
if [ "$rc" != "4" ]; then
echo " ⛔ ожидался код 4 (денежный отказ), получен $rc:"
tail -2 "$B/logs/axis-$arm.log" | sed 's/^/ /' | cut -c1-200
exit 1
fi
# ⛔ ОТКАЗ ОБЯЗАН НАЗВАТЬ СТАДИЮ edit. Если он назовёт terminology, значит терминолог пере-предложил
# хвост и попросил СВЕЖИЙ батч — тогда банк в копии собрался бы иначе, чем в базе, и это надо
# понять ДО денег, а не после.
if ! grep -q 'denied estimate=.*\/edit)' "$B/logs/axis-$arm.log"; then
echo " ⛔ отказ НЕ на стадии edit:"; grep -o 'denied estimate=[^)]*)' "$B/logs/axis-$arm.log" | tail -1 | sed 's/^/ /'
exit 1
fi
done
# ⛔ УСТОЙЧИВОСТЬ ПРОГОН-К-ПРОГОНУ: весь цикл пачки — это 20 повторных прогонов каждой руки, и если
# второй прогон даёт другой edit-снапшот, цикл сломается на втором юните. Проверяется дважды на одной
# копии: тот же snapshot_id и ноль свежих платных строк.
echo; echo "── УСТОЙЧИВОСТЬ: второй прогон той же руки обязан дать ТО ЖЕ"
S1=$(sqlite3 "file:$B/arms/glm-off.db?mode=ro" "select distinct snapshot_id from jobs where stage='edit';")
N1=$(sqlite3 "file:$B/arms/glm-off.db?mode=ro" "select count(*) from request_log where tm_hit=0 and cost_usd>0;")
sleep 2
"$B/bin/tmctl" translate --config "$B/arms/glm-off.book.yaml" --keys-file "$KEYS" \
--verify-bank --max-units 1 --ceiling-usd "$TINY" > "$B/logs/axis-glm-off-2.log" 2>&1
S2=$(sqlite3 "file:$B/arms/glm-off.db?mode=ro" "select distinct snapshot_id from jobs where stage='edit';")
N2=$(sqlite3 "file:$B/arms/glm-off.db?mode=ro" "select count(*) from request_log where tm_hit=0 and cost_usd>0;")
D2=$(grep -c 'denied by a USD ceiling' "$B/logs/axis-glm-off-2.log")
echo " glm-off прогон 1: снапшот ${S1:0:12}… строк с ценой $N1"
echo " glm-off прогон 2: снапшот ${S2:0:12}… строк с ценой $N2 · denied=$D2"
if [ "$S1" != "$S2" ] || [ "$N1" != "$N2" ]; then
echo " ⛔ ПРОГОН-К-ПРОГОНУ НЕУСТОЙЧИВ — цикл пачки сломается на втором юните. СТОП."; exit 1
fi
[ "$D2" != "0" ] && echo " ⚠ в повторе есть denied by a USD ceiling — терминолог упёрся в потолок гарда"
echo " ✓ устойчиво"
echo; echo "── СВЕРКА ОСИ ПО СНАПШОТАМ (до единой платной клетки)"
$PY - "$B" "$PIN" <<'PYEOF'
import json, sqlite3, sys, pathlib
B, pin = pathlib.Path(sys.argv[1]), json.loads(pathlib.Path(sys.argv[2]).read_text())
SHA = pin["prompt_sha256"]
WANT = {"p7-off": (SHA["P7"], "off", "deepseek-v4-pro"), "p7-low": (SHA["P7"], "low", "deepseek-v4-pro"),
"p9-off": (SHA["P9"], "off", "deepseek-v4-pro"), "p9-low": (SHA["P9"], "low", "deepseek-v4-pro"),
"glm-off": (SHA["P9"], "off", "glm-5")}
edit_ids, draft_ids, bad = {}, {}, []
for arm, want in WANT.items():
con = sqlite3.connect(f"file:{B/'arms'/(arm+'.db')}?mode=ro", uri=True)
snaps = dict(con.execute("select snapshot_id, payload from snapshots"))
for stage, bucket in (("edit", edit_ids), ("draft", draft_ids)):
ids = {r[0] for r in con.execute("select snapshot_id from jobs where stage=?", (stage,))}
bucket[arm] = ids
con.close()
for sid in edit_ids[arm]:
st = next((s for s in json.loads(snaps[sid]).get("stages", []) if s["name"] == "edit"), None)
if st is None:
bad.append(f"{arm}: в снапшоте {sid[:12]} нет стадии edit"); continue
got = (st.get("prompt_sha256"), st.get("reasoning"), st.get("model"))
print(f" {arm:8s} edit {sid[:12]}… sha {got[0][:12]}… reasoning '{got[1]}' model {got[2]}")
if got != want:
bad.append(f"{arm}: на проводе {got}, а запинено {want}")
# ⛔ САМАЯ СИЛЬНАЯ ПРОВЕРКА: у edit-снапшотов пяти рук всё, КРОМЕ плана стадий, обязано совпадать
# побайтно. Иначе «пять разных snapshot_id» пройдёт, а руки будут различаться НЕ осью: например
# разным memory_version, если авто-банк собрался в копиях по-разному. Базовый прогон оставил банк
# ЧАСТИЧНО консолидированным (consolidated=51, unanswered=57, batches_dropped=5) — он константа
# только если в пяти копиях свернулся ОДИНАКОВО, и вот это здесь и проверяется.
rest = {}
for arm in WANT:
con = sqlite3.connect(f"file:{B/'arms'/(arm+'.db')}?mode=ro", uri=True)
snaps = dict(con.execute("select snapshot_id, payload from snapshots")); con.close()
for sid in edit_ids[arm]:
pl = json.loads(snaps[sid]); mv = pl.get("memory_version")
pl.pop("stages", None)
rest.setdefault(arm, []).append((json.dumps(pl, sort_keys=True), mv))
mvs = {arm: v[0][1] for arm, v in rest.items() if v}
print(" memory_version по рукам:", {a: (m or "")[:12] for a, m in mvs.items()})
if len(set(mvs.values())) != 1:
bad.append(f"memory_version РАЗНЫЙ у рук: {mvs} — банк в копиях свернулся по-разному, "
f"редактор получил РАЗНЫЙ вход, и это НЕ ось")
bodies = {arm: v[0][0] for arm, v in rest.items() if v}
if len(set(bodies.values())) != 1:
bad.append("edit-снапшоты различаются НЕ ТОЛЬКО планом стадий — сравни payload вручную")
else:
print(" ✓ всё, кроме плана стадий, у пяти edit-снапшотов побайтно одинаково")
alle = set().union(*edit_ids.values()) if edit_ids else set()
alld = set().union(*draft_ids.values()) if draft_ids else set()
print(f" различных EDIT-снапшотов: {len(alle)} (нужно 5)")
print(f" различных DRAFT-снапшотов: {len(alld)} (нужно РОВНО 1 — иначе черновики перекуплены)")
if len(alle) != 5: bad.append(f"edit-снапшотов {len(alle)} ≠ 5 — ОСЬ СХЛОПНУЛАСЬ")
if len(alld) != 1: bad.append(f"draft-снапшотов {len(alld)} ≠ 1 — черновики НЕ общие")
if bad:
print("\n⛔ ГАРД ОСИ НЕ ПРОШЁЛ:")
for b in bad: print(" ·", b)
sys.exit(1)
print(" ✓ ось на месте, черновики общие — и всё это установлено за $0")
PYEOF
[ $? -ne 0 ] && { echo "СТОП: платить за схлопнувшийся крест нельзя."; exit 1; }
if [ "${ONLY_AXIS:-0}" = "1" ]; then
echo; echo "ONLY_AXIS=1 — остановлено ПЕРЕД платным этапом. Ни одна клетка не куплена."
exit 0
fi
# ─────────────────────────────────────────────────────────────────────────────────────────────
# ЭТАП 2 — ПЯТЬ ПЛАТНЫХ КЛЕТОК ОДНОГО ЮНИТА. С включённым каналом сырых тел запросов:
# это единственный раз, когда он нужен, и он даёт ПОБАЙТНУЮ улику того, что уехало на провод.
# ─────────────────────────────────────────────────────────────────────────────────────────────
echo; echo "── ЭТАП 2: пять платных клеток юнита $UNIT (канал сырых тел ВКЛЮЧЁН)"
ORDER=$($PY "$REPO/eval/dovodka/poryadok.py" --unit "$UNIT" --seed "$SEED")
echo " порядок: $ORDER"
for arm in $ORDER; do
CEIL=$($PY -c "print(f'{$BASE + ${W[$arm]}*($SANCTION-$BASE):.4f}')")
BEFORE=$(spend_of "$B/arms/$arm.db")
sleep 2 # см. пояснение про preflightBackup выше
echo "$arm (--ceiling-usd $CEIL) $(date -u +%H:%M:%SZ)"
LOG_LLM_BODIES=1 LOG_LEVEL=debug "$B/bin/tmctl" translate \
--config "$B/arms/$arm.book.yaml" --keys-file "$KEYS" \
--verify-bank --max-units 1 --ceiling-usd "$CEIL" \
> "$B/logs/pre-$arm.log" 2>&1
rc=$?
AFTER=$(spend_of "$B/arms/$arm.db")
D=$($PY -c "print(f'{$AFTER-$BEFORE:.6f}')")
echo " rc=$rc · клетка стоила \$$D · леджер руки \$$AFTER"
case $rc in
0|2) ;;
*) echo " ⛔ код $rc — СТОП, вопрос владельцу"; exit "$rc" ;;
esac
done
echo; echo "── АССЕРТЫ ПРЕД-ПОЛЁТА (исполняемые стоп-критерии §5.1, не осмотр глазами)"
$PY "$REPO/eval/dovodka/predpolet_proverka.py" "$B"
ASSERT_RC=$?
echo; echo "── АГРЕГАТ ПОСЛЕ ПРЕД-ПОЛЁТА"
TOT=$BASE
for arm in p7-off p7-low p9-off p9-low glm-off; do
S=$(spend_of "$B/arms/$arm.db")
TOT=$($PY -c "print(f'{$TOT + $S - $BASE:.6f}')")
done
echo " израсходовано: \$$TOT из \$$SANCTION"
echo
if [ "${ASSERT_RC:-1}" -ne 0 ]; then
echo "⛔ АССЕРТЫ ПРЕД-ПОЛЁТА НЕ ПРОШЛИ — ПАЧКУ НЕ НАЧИНАТЬ, вопрос владельцу."
exit 1
fi
echo "ДАЛЬШЕ: ФРИЗ №2 (пин сигнатуры) — и только потом пачка."