From 98511453d75a50eae3fbf9749ed0ad90228e9bde Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Sat, 3 Oct 2026 01:08:02 +0000 Subject: [PATCH] =?UTF-8?q?cron-migrate=20r2=20fixes=20(t-261002222610):?= =?UTF-8?q?=20scheduler=20guard=20=D0=BF=D1=80=D0=BE=D1=82=D0=B8=D0=B2=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=B9=D0=B4=D0=B6=D0=B5=D1=81=D1=82-=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20=D1=83=D0=B6?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B4=D0=B0=D1=87;=20runner=20ledger-first=20(best?= =?UTF-8?q?=5Fcompleted=5Fanswer:=20projection=20=D0=BC=D0=BE=D0=B3=D0=BB?= =?UTF-8?q?=D0=B0=20=D0=B1=D1=8B=D1=82=D1=8C=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=82=D1=91=D1=80=D1=82=D0=B0=20digest-=D0=B7=D0=B0=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D1=82=D0=B8=D0=B5=D0=BC);=20generation-only=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=BC=D0=BF=D1=82=20(=D0=BB=D0=B5=D0=B3?= =?UTF-8?q?=D0=B0=D1=81=D0=B8-=D1=80=D0=BE=D1=82=D0=B0=D1=86=D0=B8=D1=8E?= =?UTF-8?q?=20=D0=B8=D0=B7=20=D0=BF=D1=80=D0=BE=D0=BC=D0=BF=D1=82=D0=B0=20?= =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=E2=80=94=20=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D1=8C=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20run?= =?UTF-8?q?ner=20=D0=B2=D0=B5=D0=B4=D1=91=D1=82=20=D1=82=D0=B5=D0=BC=D1=8B?= =?UTF-8?q?);=20=D0=B4=D0=B0=D1=82=D0=B0=20=D0=B2=D1=8B=D0=BF=D1=83=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B2=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- harness/scheduler.py | 15 ++++++ scripts/cron-migrate/daily-submit.py | 49 ++++++++++++++----- .../jobs/daily-video-script-mentor.prompt.md | 6 +-- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/harness/scheduler.py b/harness/scheduler.py index 4ca0bd6..a6d2437 100644 --- a/harness/scheduler.py +++ b/harness/scheduler.py @@ -535,7 +535,22 @@ def run_level_tick(level_cfg, seq: int, model: dict): from .commands import looks_like_budget_stub if (text or "") and looks_like_budget_stub(text): return _guard_budget_stub_requeue(claimed, text) + # guard двойного закрытия (r2 e2e cron-migrate 2026-10-03): воркер мог + # ЗАКРЫТЬ задачу сам (cli complete) ещё до батч-фазы — его TaskCompleted + # содержит целевой ответ; повторный append поверх перезаписал бы answer + # дайджестом тика и испортил бы проверенный результат (перезапись + # «последний-победил» в проекции). Закрываем только ещё-running. + _model = es.run_projections() + _done = {tid for tid in (claimed or []) + if ((_model.get("tasks") or {}).get(tid) or {}).get("status") == "done"} + _first = True for tid in (claimed or []): + if tid in _done: + if _first: + _log("skip батч-догзакрытие готовых задач: " + + ", ".join(sorted(_done))) + _first = False + continue es.append_event("task", tid, "TaskCompleted", { "result_ref": trace_id, "answer": (text or "")[:2000], }) diff --git a/scripts/cron-migrate/daily-submit.py b/scripts/cron-migrate/daily-submit.py index 82d917f..db7683b 100644 --- a/scripts/cron-migrate/daily-submit.py +++ b/scripts/cron-migrate/daily-submit.py @@ -147,7 +147,10 @@ def pick_theme(job): cands = [i for i in range(1, len(themes) + 1) if _days_ago(by_theme.get(i, "")) >= cutoff] pool = cands or list(range(1, len(themes) + 1)) - n = min(pool, key=lambda i: (_days_ago(by_theme.get(i, "")), i)) + # Если ни одна тема не вышла из окна повтора — берём наименее свежую + # (макс. days_ago), НЕ свежую: min по days_ago повторял вчерашнюю тему + # (e2e 2026-10-03: тема 3 вышла 10-02 и снова 10-03). + n = min(pool, key=lambda i: (-_days_ago(by_theme.get(i, "")), i)) return {"num": n, "title": themes[n - 1], "last_used": by_theme.get(n, "никогда"), "note": note_by_theme.get(n, ""), "state_path": sp} @@ -202,16 +205,39 @@ def completed_answer(es, task_id): return json.loads(row[0]).get("answer") or "" +def completed_answers(es, task_id): + """Все ответы TaskCompleted из ledger по порядку (старые→новые). + + Проекция (read model) хранит последний answer: дайджест-закрытие тика + могло перетереть целевой ответ — поэтому quality-решения принимает + ledger-first (r2 e2e 2026-10-03: ответ сценария перезаписан digest'ом). + """ + import sqlite3 as _sq + db = _sq.connect(es.DB_PATH) + rows = db.execute( + "SELECT data FROM events WHERE aggregate_id=? AND event_type='TaskCompleted' " + "ORDER BY id", (task_id,)).fetchall() + db.close() + return [(json.loads(d).get("answer") or "") for _ts, d in rows + if (json.loads(d).get("answer") or "").strip()] + + +def best_completed_answer(es, job, task_id): + """Лучший по quality-gate ответ из ledger; если ни один не качественный — + первый (чтобы честно провалу не изображать успех).""" + ans = completed_answers(es, task_id) + if not ans: + return None + ok = [a for a in ans if quality_ok(job, a)] + return (ok or ans)[0] + + def task_status(es, task_id): model = es.run_projections() t = (model.get("tasks") or {}).get(task_id) if not t: return None - res = t.get("result") or "" - if t.get("status") == "done": - a = completed_answer(es, task_id) - res = a if a is not None else res - return {"status": t.get("status"), "answer": res} + return {"status": t.get("status")} def quality_ok(job, answer): @@ -326,7 +352,8 @@ RETRY_NOTE = ("⚠ ПРЕДЫДУЩАЯ ПОПЫТКА ЭТОЙ ЖЕ ТЕМЫ В def build_details(job, pick, retry=False): head = [prompt_text(job)] - head.append("\nТЕКУЩАЯ ТЕМА ДНЯ: №%d — %s." % (pick["num"], pick["title"])) + head.append("ДАТА ВЫПУСКА: %s." % today_utc()) + head.append("ТЕКУЩАЯ ТЕМА ДНЯ: №%d — %s." % (pick["num"], pick["title"])) head.append("В прошлый раз тема %s выходила %s (%s). Сегодня придумай НОВЫЙ " "угол, прежние формулировки не повторяй." % (pick["num"], pick["last_used"], pick["note"] or "без заметки")) @@ -345,7 +372,7 @@ def attempt(es, job, task_id, pick, retry, deadline_ts): проекцию), просто жду/читаю статус.""" st = task_status(es, task_id) if st and st["status"] == "done": - return "done", st["answer"] + return "done", best_completed_answer(es, job, task_id) or "" if st and st["status"] == "failed": return "failed", "" if not st or st["status"] not in ("ready", "running"): @@ -356,7 +383,7 @@ def attempt(es, job, task_id, pick, retry, deadline_ts): time.sleep(25) st = task_status(es, task_id) if st and st["status"] == "done": - return "done", st["answer"] + return "done", best_completed_answer(es, job, task_id) or "" if st and st["status"] == "failed": return "failed", "" return "wait", "" @@ -407,7 +434,7 @@ def main(): if args.reconcile: st = task_status(es, base) if st and st["status"] == "done": - a = st["answer"] + a = best_completed_answer(es, job, base) or "" if quality_ok(job, a): if was_delivered(job, base, a): record_theme_today(job) @@ -466,7 +493,7 @@ def main(): # ── сегодня: идемпотентные короткие выходы ── st = task_status(es, base) if st and st["status"] == "done": - a = st["answer"] + a = best_completed_answer(es, job, base) or "" if quality_ok(job, a): # тема дня уже записана (запись сделана в handle_day) — не перетираю if was_delivered(job, base, a): diff --git a/scripts/cron-migrate/jobs/daily-video-script-mentor.prompt.md b/scripts/cron-migrate/jobs/daily-video-script-mentor.prompt.md index 6254f55..4e694c1 100644 --- a/scripts/cron-migrate/jobs/daily-video-script-mentor.prompt.md +++ b/scripts/cron-migrate/jobs/daily-video-script-mentor.prompt.md @@ -1,4 +1,4 @@ -Ты — Рита, агент студии Shkor Labs. Задача: каждое утро (сегодня понедельник-пятница, 09:00 Минск = 06:00 UTC) сгенерировать ОДИН сценарий короткого вертикального видео (60-90 сек, Instagram Reels / TikTok / Shorts) для привлечения людей на БЕСПЛАТНЫЙ 20-минутный разбор стартапа от Алекса Шкора. Страница: https://mentor.cil.by — бесплатный разбор, слоты ежедневно 18:00-22:00 по Минску, апсейл — менторство $999/мес. Позиционирование Алекса: строит продукты с 2010, co-founder Paralect и DEIP, Shkor Labs («42 Labs»), 50+ созданных технологий, AI-агенты. +Ты — Рита, агент студии Shkor Labs. Задача: сгенерировать ОДИН сценарий короткого вертикального видео (60-90 сек, Instagram Reels / TikTok / Shorts) для привлечения людей на БЕСПЛАТНЫЙ 20-минутный разбор стартапа от Алекса Шкора. Страница: https://mentor.cil.by — бесплатный разбор, слоты ежедневно 18:00-22:00 по Минску, апсейл — менторство $999/мес. Позиционирование Алекса: строит продукты с 2010, co-founder Paralect и DEIP, Shkor Labs («42 Labs»), 50+ созданных технологий, AI-агенты. Формат сценария (строго): 1. ХУК (первые 3 сек) — провокационный или неожиданный тезис, без клише («вин-вин», «пушечная идея» — запрещено). Без эмодзи. @@ -7,6 +7,6 @@ 4. CTA — «Запишись на бесплатный 20-минутный разбор — слоты каждый день с 18 до 22, mentor.cil.by». Одной фразой. 5. ТЕКСТ ПОД ВИДЕО (2-3 строки) + 5 хэштегов. -Правила: разговорный живой русский, короткие рубленые фразы, никаких «друзья, сегодня поговорим о». Каждый день — НОВАЯ тема (ротация: 1) разбор ошибок фаундеров с ИИ, 2) кейс студии (что-то реальное: Totem-маскоты, видеоконвейер, DEIP-биржа, Simbiot-агент), 3) контр-интуитивный тезис о внедрении ИИ, 4) ответ на частый вопрос, 5) разбор типового сценария «как ИИ даёт деньги в <ниша>»). Темы не должны повторяться минимум 7 дней — хранить последнюю тему в файле ~/.hermes/cron/output/video-script-state.json и читать её. +Правила: разговорный живой русский, короткие рубленые фраза, никаких «друзья, сегодня поговорим о». Тему и дату выпуска берём ИСКЛЮЧИТЕЛЬНО из блока ниже (ТЕКУЩАЯ ТЕМА ДНЯ / ДАТА) — никакие файлы не читаем, состояние ведёт runner. -В конце вывести блок ДЛЯ РУЧНОЙ ЗАПИСИ: дата, тема, 3-5 пунктов что показать на экране (b-roll/текст на экране). Не более 40 строк итога. +В конце вывести блок ДЛЯ РУЧНОЙ ЗАПИСИ: дата, тема, 3-5 пунктов что показать на экране (b-roll/текст на экране). Не более 40 строк итога. Готовый сценарий — целиком в answer задачи: никаких «сделал, записал в файл», никаких отчётов о процессе — только сам сценарий. \ No newline at end of file