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