e-261014163808-4da3ec: TTFT-watchdog+ретрай висящего апстрим-вызова в LLM-воркере — зависание до первого токена больше не роняет весь тик (2 павших hour-тика 13:08/16:11, batch TaskFailed); тест test_ttft_retry 7 PASS

This commit is contained in:
Simbiot Harness 2026-10-14 17:08:27 +00:00
parent 744edd537e
commit acfdf3e5b9
8 changed files with 809 additions and 1 deletions

BIN
artifacts/alex_photo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Приложение к соглашению — White label партнёрство (антивирусный продукт)</title>
<style>
:root{
--paper:#faf8f3; --ink:#15150f; --signal:#FF4F00;
--dim:#6b675e; --line:#d8d3c8;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--paper);color:var(--ink);font:16px/1.65 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}
.wrap{max-width:860px;margin:0 auto;padding:56px 24px 100px}
header{border-bottom:3px solid var(--ink);padding-bottom:26px;margin-bottom:40px}
.kicker{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.22em;color:var(--signal);font-weight:700;text-transform:uppercase}
h1{font-size:clamp(26px,4.5vw,40px);line-height:1.1;font-weight:800;margin:10px 0 12px}
.sub{font-size:16px;color:var(--dim)}
section{margin:36px 0 0}
h2{font-size:19px;font-weight:800;margin:0 0 10px;border-left:3px solid var(--signal);padding-left:10px}
p{margin:10px 0}
.clause{border:1px solid var(--line);background:#fff;padding:16px 20px;margin:14px 0}
.clause .cn{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
.clause h4{font-size:15px;margin:4px 0 8px;font-weight:800}
.clause p, .clause li{font-size:14.5px}
ul{margin:8px 0 8px 20px}
li{margin:6px 0} li::marker{color:var(--signal)}
.sign{border:3px double var(--ink);background:#fff;padding:22px;margin:30px 0;display:grid;grid-template-columns:1fr 1fr;gap:20px}
@media(max-width:700px){.sign{grid-template-columns:1fr}}
.sig .org{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin-bottom:14px}
.sig .line{border-bottom:1px solid var(--ink);height:38px;margin-bottom:8px}
.sig .field{font-size:12.5px;color:var(--dim)}
footer{margin-top:50px;border-top:1px solid var(--line);padding-top:16px;color:var(--dim);font-size:12.5px}
.draft{display:inline-block;border:1px solid var(--signal);color:var(--signal);font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.12em;padding:4px 10px;text-transform:uppercase;margin-top:12px}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="kicker">Приложение к соглашению</div>
<h1>Партнёрство по модели White&nbsp;Label</h1>
<div class="sub">Антивирусный продукт партнёра, выпускаемый под собственным брендом кооператива.</div>
<span class="draft">Черновик · на согласование</span>
</header>
<section>
<h2>1. Предмет</h2>
<div class="clause">
<p>Мы предлагаем партнёрство по модели white label: мы берём ваш антивирусный продукт, выпускаем его под собственным брендом и выводим на рынок — маркетинг, продажи, поддержка клиентов и операционный слой остаются на нашей стороне. Вы фокусируетесь на продукте и технологических обновлениях.</p>
</div>
</section>
<section>
<h2>2. Роли сторон</h2>
<div class="clause">
<div class="cn">2.1</div>
<h4>Партнёр (вы)</h4>
<ul>
<li>предоставляет готовый антивирусный продукт и его обновления;</li>
<li>обеспечивает работоспособность ядра и критических исправлений;</li>
<li>передаёт документацию, необходимую для продвижения и поддержки.</li>
</ul>
</div>
<div class="clause">
<div class="cn">2.2</div>
<h4>Мы (кооператив)</h4>
<ul>
<li>выпускаем продукт под собственным брендом (white label);</li>
<li>берём на себя маркетинг, привлечение клиентов и продажи;</li>
<li>обеспечиваем первую линию поддержки пользователей;</li>
<li>ведём юрлицо, платёжную инфраструктуру и биллинг.</li>
</ul>
</div>
</section>
<section>
<h2>3. Экономика</h2>
<div class="clause">
<p>Выручка от продаж продукта распределяется между сторонами. Конкретная доля каждой стороны, периодичность выплат и порядок учёта фиксируются в основном соглашении. Базовый ориентир для обсуждения — равное участие: продукт с одной стороны, дистрибуция и бренд — с другой.</p>
</div>
</section>
<section>
<h2>4. Права на бренд и продукт</h2>
<div class="clause">
<p>Интеллектуальная собственность на продукт остаётся за партнёром. Кооперативу передаётся право использования продукта под своим брендом в объёме, необходимом для исполнения настоящего партнёрства. Права на клиентскую базу, маркетинговые материалы и бренд, под которым выпускается продукт, принадлежат кооперативу.</p>
</div>
</section>
<section>
<h2>5. Следующие шаги</h2>
<div class="clause">
<ul>
<li>согласовать доли и условия основного соглашения;</li>
<li>определить бренд и упаковку продукта под нашим именем;</li>
<li>согласовать план запуска и первые продажи.</li>
</ul>
</div>
</section>
<div class="sign">
<div class="sig">
<div class="org">От кооператива</div>
<div class="line"></div>
<div class="field">Подпись / ФИО / дата</div>
</div>
<div class="sig">
<div class="org">От партнёра</div>
<div class="line"></div>
<div class="field">Подпись / ФИО / дата</div>
</div>
</div>
<footer>Черновик для обсуждения. Не является офертой. Правки согласовываются сторонами до подписания.</footer>
</div>
</body>
</html>

View file

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Общее видение — панельная сессия для ИИШНИЦЫ</title>
<style>
body{font-family:system-ui,-apple-system,sans-serif;max-width:820px;margin:0 auto;padding:24px;color:#222;background:#fafafa;line-height:1.55}
h1{color:#1a5276} h2{color:#2874a6;border-bottom:2px solid #d6eaf8;padding-bottom:4px;margin-top:32px}
table{border-collapse:collapse;width:100%;margin:12px 0;background:#fff}
th,td{border:1px solid #ccd;padding:8px 10px;text-align:left;vertical-align:top}
th{background:#d6eaf8}
.mission{background:#eafaf1;border-left:4px solid #27ae60;padding:12px 16px;margin:16px 0}
.circle{display:inline-block;width:14px;height:14px;border-radius:50%;margin-right:6px;vertical-align:-2px}
.c1{background:#e74c3c}.c2{background:#f39c12}.c3{background:#8e44ad}
.check li{margin:6px 0;list-style:none}
.check input{margin-right:8px}
blockquote{border-left:3px solid #aaa;margin:8px 0;padding:4px 12px;color:#555;font-style:italic}
</style>
</head>
<body>
<h1>Панельная сессия «Общее видение»</h1>
<p><b>Формат:</b> ИИШНИЦА · 90 минут · панель + интерактив из зала.</p>
<div class="mission">
<b>Миссия кооператива 42 как рамка сессии:</b> мы верим, что осмысленное будущее строится вместе — каждый привносит своё видение, а общее оказывается больше суммы личных. Сессия — микромодель кооператива: сначала каждый мечтает в одиночку, потом замечает чужое, затем вместе рождается общее имя будущего.
</div>
<h2>Драматургия: три круга</h2>
<ol>
<li><span class="circle c1"></span><b>Круг 1 — «Моё будущее» (4 мин/участник):</b> каждый панелист без прерываний рассказывает, каким он видит своё (или общего «нашего») будущее. Без оценок, без споров — просто мечта вслух.</li>
<li><span class="circle c2"></span><b>Круг 2 — «Что понравилось у других» (2 мин/участник):</b> каждый называет 1–2 образа из чужих рассказов, которые отозвались, и почему. Здесь рождается ткань общего.</li>
<li><span class="circle c3"></span><b>Круг 3 — «Название общего видения» (10–15 мин):</b> панель + зал вместе придумывают и выбирают название общего видения. Голосование аплодисментами/руками.</li>
</ol>
<h2>Тайминг на 90 минут</h2>
<table>
<tr><th>Время</th><th>Блок</th><th>Кто</th></tr>
<tr><td>0:00–0:10</td><td>Открытие: миссия 42 как рамка, представление панелей, правила сессии</td><td>Модератор</td></tr>
<tr><td>0:10–0:30</td><td>Круг 1 «Моё будущее» — 4 мин × 5 панелей (или 5 мин × 4)</td><td>Панелисты</td></tr>
<tr><td>0:30–0:42</td><td>Круг 2 «Что понравилось у других» — 2 мин × панелей</td><td>Панелисты</td></tr>
<tr><td>0:42–0:50</td><td>Пауза / музыкальная интерлудия, зал пишет карточки с вариантами названий</td><td>Все</td></tr>
<tr><td>0:50–1:05</td><td>Круг 3: сбор вариантов названий, короткая защита 2–3 финалистов, голосование</td><td>Панель + зал</td></tr>
<tr><td>1:05–1:20</td><td>Объявление названия общего видения + короткий синтез модератора</td><td>Модератор</td></tr>
<tr><td>1:20–1:30</td><td>Вопросы из зала (блиц, по 1 мин)</td><td>Зал</td></tr>
<tr><td>1:30</td><td>Закрытие: фото с названием, анонс продолжения</td><td>Модератор</td></tr>
</table>
<h2>Роли</h2>
<table>
<tr><th>Роль</th><th>Задачи</th></tr>
<tr><td>Модератор</td><td>Держит тайминг и рамку миссии 42, режет переспуски, ведёт круг 3 и голосование</td></tr>
<tr><td>Тайм-кипер</td><td>Видимый таймер (4/2 мин), карточки «30 секунд»</td></tr>
<tr><td>Панелисты (4–5)</td><td>Проходят три круга, готовы к вопросам из зала</td></tr>
<tr><td>Сборщик названий</td><td>Собирает карточки зала, группирует похожие, выносит финалистов</td></tr>
<tr><td>Фотограф/протокол</td><td>Фото финального названия, короткий конспект для отчёта</td></tr>
</table>
<h2>Заготовки названий (если зал молчит — модерируем от них)</h2>
<ul>
<li>«Будущее, которое мы узнаем»</li>
<li>«Общее небо»</li>
<li>«Точка сборки 42»</li>
<li>«Мы — будущего авторы»</li>
<li>«Дом, который построим вместе»</li>
<li>«Ответ на главный вопрос» (отсылка к 42)</li>
</ul>
<h2>Чек-лист организатора</h2>
<ul class="check">
<li><input type="checkbox"> Подтвердить панелей (4–5) и briefing им за 2–3 дня: «приходите с образом будущего на 4 минуты»</li>
<li><input type="checkbox"> Зал: таймер с крупными цифрами, карточки + маркеры для названий</li>
<li><input type="checkbox"> Распечатать заготовки названий (плашки А4)</li>
<li><input type="checkbox"> Слайд-открывашка: миссия 42 + правила трёх кругов</li>
<li><input type="checkbox"> Микрофоны для зала (минимум 2 бегунка)</li>
<li><input type="checkbox"> Назначить тайм-кипера, сборщика названий, фотографа</li>
<li><input type="checkbox"> Репетиция открытия с модератором (10 мин накануне)</li>
<li><input type="checkbox"> Договориться, куда попадает итог: стена/вики/чат мероприятия</li>
</ul>
<blockquote>Правило сессии: в первых двух кругах никто никого не перебивает и не оценивает. Критика допускается только в вопросах из зала — и то бережно.</blockquote>
</body>
</html>

View file

@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Законы кооператива для РБ — адаптация DAO Supplement (Вайоминг) под кооператив 42</title>
<style>
:root{--ink:#1a1d23;--mut:#5b6472;--bg:#f7f6f3;--card:#fff;--line:#e5e2da;--acc:#2b5cff;--ok:#1e7f4f;--warn:#b05a00;--bad:#b3261e}
*{box-sizing:border-box}
body{margin:0;font:16px/1.65 Georgia,'Times New Roman',serif;color:var(--ink);background:var(--bg)}
.wrap{max-width:900px;margin:0 auto;padding:32px 20px 80px}
header.doc{border-bottom:3px double var(--ink);padding-bottom:20px;margin-bottom:28px}
.kicker{font-family:system-ui,sans-serif;font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--acc);font-weight:600}
h1{font-size:32px;line-height:1.2;margin:10px 0 8px}
.meta{font-family:system-ui,sans-serif;font-size:13px;color:var(--mut)}
.disclaimer{background:#fff8e6;border:1px solid #ecd9a8;border-radius:10px;padding:14px 18px;font-family:system-ui,sans-serif;font-size:14px;margin:24px 0}
h2{font-size:22px;margin:38px 0 12px;padding-top:14px;border-top:1px solid var(--line)}
h3{font-size:17px;margin:22px 0 8px}
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px 22px;margin:16px 0}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:720px){.grid{grid-template-columns:1fr}}
table{width:100%;border-collapse:collapse;font-family:system-ui,sans-serif;font-size:13.5px;background:var(--card);border-radius:10px;overflow:hidden;margin:14px 0}
th{background:#232833;color:#fff;text-align:left;padding:9px 12px;font-weight:600}
td{border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top}
tr:last-child td{border-bottom:none}
.tag{display:inline-block;font-family:system-ui,sans-serif;font-size:11px;font-weight:700;letter-spacing:.06em;padding:2px 8px;border-radius:99px;vertical-align:2px}
.t-wy{background:#e8eefc;color:#1d3fae}
.t-rb{background:#e6f4ec;color:#15663c}
.t-diff{background:#fdecea;color:#8f2019}
.t-ok{background:#e6f4ec;color:#15663c}
.t-risk{background:#fff1e2;color:#8a4a00}
.law-ref{font-family:system-ui,sans-serif;font-size:12.5px;color:var(--mut);display:block;margin-top:4px}
ol.cl,ul{padding-left:22px}
ol.cl li{margin:8px 0}
.num{font-family:system-ui,sans-serif;font-weight:700;color:var(--acc);font-size:13px}
blockquote{border-left:3px solid var(--acc);margin:14px 0;padding:6px 18px;background:#f2f5ff;border-radius:0 10px 10px 0;font-style:italic}
footer.doc{margin-top:48px;border-top:3px double var(--ink);padding-top:16px;font-family:system-ui,sans-serif;font-size:12.5px;color:var(--mut)}
</style>
</head>
<body>
<div class="wrap">
<header class="doc">
<div class="kicker">Кооператив 42 · документы · черновик для обсуждения</div>
<h1>Законы кооператива для Республики Беларусь</h1>
<div class="meta">Адаптация структуры Wyoming DAO Supplement (W.S. 17-31) под производственный кооператив · к задаче t-261014120323 · основа — разбор t-261014111325 · 14.10.2026</div>
</header>
<div class="disclaimer">
<b>Статус документа:</b> концепт/черновик для обсуждения с юристами и правлением. Это не юридическое заключение и не проект устава: статьи сопоставлены по смыслу, точные реквизиты и редакции норм должны быть проверены юристом перед любым использованием. Требование «проконсультироваться с юристами» зафиксировано участниками кооператива ранее (голосовая заметка 23.09.2026).
</div>
<h2>0. Идея документа</h2>
<p>Вайоминг сделал из DAO юридическое лицо: не новую форму, а <b>режим поверх существующей LLC</b>. Для Беларуси тот же ход даёт готовая форма — <b>производственный кооператив (артель)</b>: она member-owned, предполагает коллегиальное управление и распределение результата по участию, а «цифровой слой» поверх неё легализован <b>Декретом №8 «О развитии цифровой экономики»</b> (ПВТ): права можно закреплять в токенах, а сделки совершать смарт-контрактами.</p>
<blockquote>Формула: <b>кооператив (юрлицо) + токены (учёт прав) + смарт-контракты (исполнение решений)</b> — белорусский эквивалент вайомингской DAO LLC.</blockquote>
<h2>1. Статус юридического лица</h2>
<div class="card">
<p><b>Вайоминг (§104):</b> DAO — это LLC, в уставе которой есть заявление о DAO-статусе и NOTICE OF RESTRICTIONS; в названии — «DAO LLC».<span class="tag t-wy">WY</span></p>
<p><b>Адаптация для РБ:</b> регистрируется производственный кооператив в ЕГРЮЛ. В уставе — раздел «Цифровой режим управления»: фиксируется, что (а) права членов учитываются наряду с реестром в распределённом реестре (блокчейне), (б) решения органов оформляются on-chain операциями, (в) действует внутренний протокол «Конституция кооператива» (код + текст), на который устав прямо ссылается.</p>
<span class="law-ref">Право РБ: ст. 104–113 ГК РБ (производственный кооператив/артель); устав обязателен, регистрация в ЕГР; фирменное наименование с указанием «производственный кооператив»/«артель».</span>
<p><span class="tag t-risk">риск</span> В РБ нет аналога «DAO-статуса» как регистрационной пометки — цифровой режим существует только внутри устава и локальных актов. Это не блокер: вайомингский статус тоже по сути содержательный, а не регистрационный.</p>
</div>
<h2>2. Применимое право</h2>
<div class="card">
<p><b>Вайоминг (§103):</b> DAO Supplement + весь LLC Act поверх, кроме противоречий.<span class="tag t-wy">WY</span></p>
<p><b>Адаптация для РБ:</b> иерархия источников: (1) ГК РБ и законодательство о юрлицах; (2) закон о регистре/Декрет №8 для цифрового слоя; (3) устав кооператива; (4) внутренние акты — «Конституция» (протокол управления); (5) регламенты органов.</p>
<span class="law-ref">Право РБ: Декрет №8 от 21.12.2017 «О развитии цифровой экономики» — правовой режим ПВТ: токены, смарт-контракты, операции с ними; для полного эффекта кооперативу целесообразен статус резидента ПВТ.</span>
</div>
<h2>3. Органы управления и смарт-контракты</h2>
<div class="card">
<p><b>Вайоминг (§106, §109):</b> управляют члены и смарт-контракты; публичный идентификатор контракта обязателен, за 30 дней не дали — роспуск.<span class="tag t-wy">WY</span></p>
<p><b>Адаптация для РБ:</b> классическая тройка кооператива сохраняется — <b>общее собрание</b> (высший орган), <b>правление</b>, <b>председатель/директор</b>. Смарт-контракт не заменяет органы, а <b>исполняет их решения</b>: голосования собрания проводятся on-chain, итог автоматически оформляется записью в реестре.</p>
<ul>
<li><b>Кворум и решения</b> — параметры смарт-контракта, совпадающие с уставными (пример: кворум ⅔, решения большинства от принявших участие).</li>
<li><b>Публичный адрес контракта</b> — аналог вайомингского идентификатора: указывается в уставе; изменение = уставная процедура. Неактуальный адрес блокирует on-chain операции (мягкий аналог вайомингского роспуска — операционный стоп).</li>
<li><b>Обновляемость:</b> контракт upgradable через правление + собрание (двойное подтверждение), история версий публична.</li>
</ul>
<span class="law-ref">Право РБ: высший орган производственного кооператива — общее собрание; текущее управление — правление/председатель; компетенция и порядок — устав.</span>
</div>
<h2>4. Иерархия норм: контракт vs устав</h2>
<div class="card">
<p><b>Вайоминг (§115):</b> смарт-контракт &gt; устав &gt; operating agreement.<span class="tag t-wy">WY</span></p>
<p><b>Ключевое отличие:</b> в РБ так нельзя — юридически устав всегда выше. Решение — <b>однонаправленная синхронизация:</b> устав признаёт записи реестра доказательством состояний (состав членов, голоса, решения), а смарт-контракт выступает «исполнительным механизмом» уставных процедур. Коллизия разрешается в пользу устава и решения собрания; fork контракта возможен только решением собрания.</p>
<table>
<tr><th>Слой</th><th>Вайоминг DAO</th><th>Кооператив РБ</th></tr>
<tr><td>Юридическая норма</td><td>Supplement + LLC Act</td><td>ГК РБ + Декрет №8</td></tr>
<tr><td>Учредительный документ</td><td>Articles (DAO-статус)</td><td>Устав кооператива</td></tr>
<tr><td>Operating agreement</td><td>Да</td><td>«Конституция» (протокол управления)</td></tr>
<tr><td>Что выше при конфликте</td><td>Смарт-контракт</td><td>Устав и собрание (контракт — исполнение)</td></tr>
</table>
</div>
<h2>5. Обязанности членов (фидуциарный вопрос)</h2>
<div class="card">
<p><b>Вайоминг (§110):</b> fiduciary duties по умолчанию отключены, остаётся implied covenant of good faith.<span class="tag t-wy">WY</span></p>
<p><b>Адаптация для РБ:</b> «отключить» обязанности членов уставом нельзя — но кооперативная форма и не предполагает жёстких фидуциарных обязанностей как корпорация. Практически: в уставе фиксируется <b>минимальный стандарт добросовестности</b> (запрет конфликтных сделок без раскрытия, запрет подрыва платформы), а детально регулирует «Конституция».</p>
<p><span class="tag t-diff">отличие</span> РБ-режим защитнее вайомингского: полный отказ от обязанностей недоступен, зато это снижает репутационные риски при работе с партнёрами (ПВТ, институционалы).</p>
</div>
<h2>6. Пай, доли и голоса</h2>
<div class="card">
<p><b>Вайоминг (§111):</b> доля пропорциональна digital-взносу, либо 1 член = 1 голос.<span class="tag t-wy">WY</span></p>
<p><b>Адаптация для РБ:</b> классика кооператива — <b>1 член = 1 голос</b> (это и есть «member-weight=1» из вайомингского правила по умолчанию). Паевой взнос учитывается в токенах на адресах членов: токен = <b>инструмент учёта пая и доли в фонде кооператива</b>, не эмиссия «ценной бумаги» в смысле законодательства о ценных бумагах. Трудовое участие (основание распределения прибыли) протоколируется on-chain как вклад.</p>
<span class="law-ref">Право РБ: прибыль производственного кооператива распределяется, как правило, в соответствии с трудовым участием; пай и порядок возврата пая — устав. Минимум членов — 3; лимит «нетрудовых» членов устанавливается законодательством.</span>
</div>
<h2>7. Информация и прозрачность</h2>
<div class="card">
<p><b>Вайоминг (§112):</b> если данные в открытом блокчейне — права на отчётность нет.<span class="tag t-wy">WY</span></p>
<p><b>Адаптация для РБ:</b> реестр членов, голосования, решения и версии контракта публикуются в открытом распределённом реестре. Отчётность для членов и третьих лиц обеспечивается самим реестром + уставной нормой «запись реестра = доказательство». Личные данные — вне реестра (псевдонимизация адресов), соответствие законодательству о защите персональных данных.</p>
</div>
<h2>8. Вход и выход</h2>
<div class="card">
<p><b>Вайоминг (§113):</b> только по правилам устава; по умолчанию выход = потеря всех прав.<span class="tag t-wy">WY</span></p>
<p><b>Адаптация для РБ:</b> приём — решение собрания (on-line голосование + запись в реестре + юр. действие в ЕГР). Выход — заявление члена, кооператив обязан выплатить <b>паевой взнос и причитающуюся долю</b> в порядке и сроки устава (в цифровом слое — расчёт смарт-контрактом). Ключевое отличие от Вайоминга: полностью «обнулить» выходящего нельзя — пай подлежит возврату; токены членства сжигаются, имущественные права — по уставу.</p>
<p><span class="tag t-diff">отличие</span> РБ защищает выходящего сильнее вайомингского дефолта. Плюс для доверия партнёров, минус — нужен ликвидностный буфер в уставе (норма о фонде выплаты паёв).</p>
</div>
<h2>9. Реорганизация и ликвидация</h2>
<div class="card">
<p><b>Вайоминг (§114):</b> роспуск: срок, большинство &gt;50%, событие контракта, год бездействия и т.д.<span class="tag t-wy">WY</span></p>
<p><b>Адаптация для РБ:</b> в основу берутся основания ГК РБ (решение собрания, суд, реорганизация); цифровой слой добавляет «on-chain триггеры»: смарт-контракт может <i>инициировать</i> голосование о ликвидации (например, по событию «1 год без операций» — прямой аналог вайомингского dormancy), но юридическую силу имеет только решение собрания и регистратор. После ликвидации — выплата паёв, контракт замораживается (archive-режим).</p>
</div>
<h2>10. Иностранные члены</h2>
<div class="card">
<p><b>Вайоминг (§116):</b> foreign DAO не сертифицируется — только местное формирование.<span class="tag t-wy">WY</span></p>
<p><b>Адаптация для РБ:</b> члены — физлица и юрлица, включая иностранных; ограничений по национальности членов устав не вводит. Кросс-граничный вывод: для работы DAO-кооператива в других юрисдикциях не создаётся «иностранная копия» — заключается договорная модель (дочерние структуры/партнёрства), ядро прав остаётся в РБ-кооперативе. Для внешних DAO-LLC (Вайоминг), желающих войти в экосистему, — членство юрлица через представителя.</p>
</div>
<h2>11. Сводная таблица соответствия</h2>
<table>
<tr><th>#</th><th>Wyoming DAO Supplement</th><th>Эквивалент РБ</th><th>Статус</th></tr>
<tr><td>104</td><td>DAO-статус LLC в уставе</td><td>«Цифровой режим управления» в уставе кооператива</td><td><span class="tag t-ok">переносимо</span></td></tr>
<tr><td>103</td><td>Supplement + LLC Act</td><td>ГК РБ + Декрет №8 + устав</td><td><span class="tag t-ok">переносимо</span></td></tr>
<tr><td>106/109</td><td>Управление членами и контрактами</td><td>Собрание + правление; контракт исполняет решения</td><td><span class="tag t-ok">переносимо</span></td></tr>
<tr><td>115</td><td>Контракт &gt; устава</td><td>Устав выше; реестр = доказательство состояний</td><td><span class="tag t-diff">различие</span></td></tr>
<tr><td>110</td><td>Fiduciary duties off</td><td>Стандарт добросовестности в уставе</td><td><span class="tag t-diff">различие</span></td></tr>
<tr><td>111</td><td>Взнос digital assets / 1 член = 1 голос</td><td>1 член = 1 голос; пай в токенах учёта</td><td><span class="tag t-ok">переносимо</span></td></tr>
<tr><td>112</td><td>Открытый блокчейн = отчётность</td><td>Открытый реестр + псевдонимизация</td><td><span class="tag t-ok">переносимо</span></td></tr>
<tr><td>113</td><td>Выход = потеря прав</td><td>Выход с выплатой пая (обязательна)</td><td><span class="tag t-diff">различие</span></td></tr>
<tr><td>114</td><td>Триггеры роспуска контрактом</td><td>On-chain триггер → голосование → юр. ликвидация</td><td><span class="tag t-ok">переносимо</span></td></tr>
<tr><td>116</td><td>Без foreign DAO</td><td>Иностранные члены да; «копий» юрлица нет</td><td><span class="tag t-ok">переносимо</span></td></tr>
</table>
<h2>12. Что дальше (дорожная карта)</h2>
<ol class="cl">
<li><span class="num">1.</span> <b>Юридическая экспертиза:</b> подтвердить статьи ГК РБ и объём Декрета №8 для выбранной модели; уточнить режим токена учёта пая с юристом ПВТ.</li>
<li><span class="num">2.</span> <b>Резидентство ПВТ:</b> подать документы — без него цифровой слой (токены, смарт-контракты) не получает льготного режима Декрета №8.</li>
<li><span class="num">3.</span> <b>Конституция v1:</b> зафиксировать протокол управления (кворумы, роли, обновление контракта) — демо: t-261014121824.</li>
<li><span class="num">4.</span> <b>Устав:</b> внести разделы «Цифровой режим управления», «Фонд выплаты паёв», «Стандарт добросовестности».</li>
<li><span class="num">5.</span> <b>Пилот on-chain решений:</b> вести собрания правления через реестр параллельно с бумажным протоколом (dual-running) до полной уверенности.</li>
</ol>
<footer class="doc">
Кооператив 42 · черновик законов РБ (адаптация Wyoming DAO Supplement, W.S. 17-31) · артефакт t-261014120323-20b83f · не является юридическим заключением; перед регистрационными действиями — обязательная проверка юристом.
</footer>
</div>
</body>
</html>

View file

@ -0,0 +1,205 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Конституция кооператива на блокчейне — интерактивное демо для ПВТ</title>
<style>
:root{--ink:#0d1117;--bg:#0a0e14;--panel:#11161f;--panel2:#161d29;--line:#232c3b;--mut:#8b96a8;--acc:#4d8dff;--acc2:#22c55e;--warn:#f59e0b;--bad:#ef4444;--vio:#a78bfa}
*{box-sizing:border-box}
body{margin:0;font-family:'SF Mono','Cascadia Code',Consolas,monospace;background:var(--bg);color:#dbe2ee;font-size:14.5px;line-height:1.6}
.wrap{max-width:1060px;margin:0 auto;padding:28px 18px 90px}
header{border:1px solid var(--line);border-radius:14px;padding:22px 26px;margin-bottom:22px;background:linear-gradient(135deg,#101726,#0d1420)}
.kicker{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--acc);font-weight:700}
h1{margin:8px 0 6px;font-size:26px;line-height:1.25}
.sub{color:var(--mut);font-size:13px}
.cols{display:grid;grid-template-columns:300px 1fr;gap:18px}
@media(max-width:860px){.cols{grid-template-columns:1fr}}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}
.panel h3{margin:0;padding:12px 16px;font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--mut);border-bottom:1px solid var(--line);background:var(--panel2)}
.pbody{padding:14px 16px}
.member{display:flex;align-items:center;gap:10px;padding:9px 10px;border:1px solid var(--line);border-radius:10px;margin:8px 0;background:var(--panel2);cursor:pointer;transition:border-color .15s}
.member:hover{border-color:var(--acc)}
.member.sel{border-color:var(--acc);box-shadow:0 0 0 1px var(--acc) inset}
.avatar{width:30px;height:30px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;color:#fff}
.mname{font-weight:700;font-size:13.5px}
.mrole{font-size:11px;color:var(--mut)}
.voteweight{margin-left:auto;font-size:11px;color:var(--vio)}
.ledger{background:#080b10;border-radius:10px;border:1px solid var(--line);padding:12px;max-height:340px;overflow-y:auto;font-size:12px}
.tx{border-left:2px solid var(--acc2);padding:6px 10px;margin:7px 0;background:#0c121b;border-radius:0 8px 8px 0}
.tx.gov{border-left-color:var(--vio)}
.tx.peg{border-left-color:var(--acc)}
.tx.err{border-left-color:var(--bad)}
.txhead{display:flex;justify-content:space-between;gap:8px;font-weight:700;color:#e8eefb;font-size:11.5px}
.txbody{color:var(--mut);margin-top:3px;white-space:pre-wrap}
.ok{color:var(--acc2);font-weight:700}.warn{color:var(--warn);font-weight:700}.err{color:var(--bad);font-weight:700}.vio{color:var(--vio)}
.btnrow{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0}
button{font-family:inherit;font-size:12.5px;font-weight:700;padding:10px 14px;border-radius:10px;border:1px solid var(--acc);background:#15243f;color:#bcd6ff;cursor:pointer;transition:.15s}
button:hover{background:#1b3157}
button:disabled{opacity:.35;cursor:not-allowed}
button.ghost{border-color:var(--line);background:var(--panel2);color:var(--mut)}
.kv{display:grid;grid-template-columns:auto 1fr;gap:4px 12px;font-size:12.5px}
.kv b{color:var(--mut);font-weight:400}
.bar{height:10px;background:#0b1119;border:1px solid var(--line);border-radius:99px;overflow:hidden;display:flex}
.seg{height:100%}
.legend{display:flex;flex-wrap:wrap;gap:12px;font-size:11px;color:var(--mut);margin-top:8px}
.dot{display:inline-block;width:9px;height:9px;border-radius:3px;margin-right:5px;vertical-align:-1px}
.note{background:#101726;border:1px solid var(--line);border-radius:10px;padding:12px 14px;font-size:12px;color:var(--mut);margin-top:12px}
.note b{color:#c8d4e6}
.sec2{margin-top:22px}
.timeline{display:grid;gap:10px}
.step{display:grid;grid-template-columns:34px 1fr;gap:12px;align-items:start}
.n{width:30px;height:30px;border-radius:9px;background:#15243f;border:1px solid var(--acc);color:#bcd6ff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px}
.step .t{font-weight:700;font-size:13.5px}
.step .d{color:var(--mut);font-size:12.5px}
footer{margin-top:34px;border-top:1px solid var(--line);padding-top:14px;font-size:11.5px;color:var(--mut)}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="kicker">Кооператив 42 · Интерактивное демо для ПВТ</div>
<h1>Конституция кооператива на блокчейне</h1>
<div class="sub">Как устав и законы кооператива исполняются on-chain: голосование → решение → юридическое действие. Смарт-контракт — исполнительный механизм, устав — источник власти (ГК РБ + Декрет №8). Основа контента: док законов t-261014120323.</div>
</header>
<div class="cols">
<div>
<div class="panel">
<h3>Реестр членов</h3>
<div class="pbody">
<div class="kv" style="margin-bottom:10px"><b>Всего членов:</b><span id="mcount">—</span></div>
<div id="members"></div>
<div class="note" style="margin-top:10px">Каждый член — адрес в реестре. <b>1 член = 1 голос</b> (§111 WY / устав). Кликните — проголосовать от имени члена.</div>
</div>
</div>
<div class="panel" style="margin-top:16px">
<h3>Смарт-контракт</h3>
<div class="pbody">
<div class="kv">
<b>Адрес:</b><span>0x42…C0NF (в уставе)</span>
<b>Версия:</b><span id="cver">v1.0</span>
<b>Кворум:</b><span id="cq">—</span>
<b>Порог:</b><span>50%+1 от голосовавших</span>
<b>Upgradable:</b><span class="ok">да, dual-confirm</span>
</div>
<div class="note">Публичный адрес контракта фиксируется в уставе (аналог §109 WY: неактуальный идентификатор блокирует on-chain операции).</div>
</div>
</div>
</div>
<div>
<div class="panel">
<h3>Активное предложение (on-chain governance)</h3>
<div class="pbody">
<div id="proposal" class="tx gov"><div class="txhead"><span>PROPOSAL-1 · governance</span><span id="pstate">открыто</span></div><div class="txbody">Принять Конституцию кооператива v1: цифровой режим управления, учёт паёв в токенах, решения органов оформляются on-chain.</div></div>
<div style="margin:12px 0 6px;font-size:12px;color:var(--mut)">Явка / кворум ⅔</div>
<div class="bar" id="bar"></div>
<div class="legend">
<span><span class="dot" style="background:#22c55e"></span>За</span>
<span><span class="dot" style="background:#ef4444"></span>Против</span>
<span><span class="dot" style="background:#232c3b"></span>Не голосовали</span>
<span id="tally" style="margin-left:auto"></span>
</div>
<div class="btnrow">
<button id="btnVote" onclick="castVote()">Проголосовать (за)</button>
<button class="ghost" onclick="castVote(true)">Против</button>
<button id="btnExec" onclick="executeProposal()" disabled>Исполнить решение</button>
<button class="ghost" onclick="location.reload()">Сброс демо</button>
</div>
<div id="verdict" class="note">Выберите члена слева и проголосуйте. Когда явка достигнет кворума и большинство — «за», кнопка «Исполнить решение» активируется: смарт-контракт оформит решение собрания on-chain.</div>
</div>
</div>
<div class="panel" style="margin-top:16px">
<h3>Журнал реестра (ledger)</h3>
<div class="ledger" id="ledger">
<div class="tx peg"><div class="txhead"><span>GENESIS · peg</span><span>#0</span></div><div class="txbody">Реестр привязан к уставу кооператива (РБ): состав органов, паи, правила — из юрдокумента. Токен = учёт пая, не ценная бумага (Декрет №8).</div></div>
</div>
</div>
</div>
</div>
<div class="panel sec2">
<h3>Как решение становится юридическим фактом (5 шагов)</h3>
<div class="pbody timeline">
<div class="step"><div class="n">1</div><div><div class="t">Предложение (proposal)</div><div class="d">Правление вносит предложение on-chain. В уставе закреплено: повестка собрания = активные предложения реестра.</div></div></div>
<div class="step"><div class="n">2</div><div><div class="t">Голосование членов (1 член = 1 голос)</div><div class="d">Члены голосуют подписью со своего адреса. Тайна личности — псевдонимизация, вес — по уставу, не по капиталу.</div></div></div>
<div class="step"><div class="n">3</div><div><div class="t">Кворум + порог</div><div class="d">Смарт-контракт считает явку и голоса по уставным параметрам (здесь: кворум ⅔, порог 50%+1). Невозможность «накрутки» — математика реестра.</div></div></div>
<div class="step"><div class="n">4</div><div><div class="t">Исполнение (execute)</div><div class="d">Контракт оформляет решение операцией в реестре; для решений, требующих юрдействия (реестр юрлиц, договоры), формируется пакет для подписи председателем — принцип «контракт исполняет, устав властвует».</div></div></div>
<div class="step"><div class="n">5</div><div><div class="t">Прозрачность (audit)</div><div class="d">Любой партнёр (ПВТ, аудит, член) проверяет историю голосований и решений в открытом реестре — без запроса отчётности (§112 WY).</div></div></div>
</div>
</div>
<div class="panel sec2">
<h3>Привязка к праву РБ (для юристов ПВТ)</h3>
<div class="pbody" style="font-size:13px">
<ul style="margin:0;padding-left:18px;color:#c3cddd">
<li><b>Юрлицо:</b> производственный кооператив (ГК РБ, ст. 104–113) — устав обязателен, собрание — высший орган.</li>
<li><b>Цифровой слой:</b> Декрет №8 «О развитии цифровой экономики» — токены (учёт пая), смарт-контракты (оформление сделок/решений), резидентство ПВТ.</li>
<li><b>Иерархия:</b> устав выше контракта — реестр признаётся доказательством состояний, контракт — исполнительный механизм; коллизии решает собрание.</li>
<li><b>Защита членов:</b> выход — с выплатой пая (обязательна); контрактом это автоматизируется, но не отменяется.</li>
</ul>
</div>
</div>
<footer>Демо-симуляция работы Конституции кооператива on-chain · Кооператив 42 · артефакт t-261014121824-a70757 · контент по док законов РБ (t-261014120323) · не юрзаключение, для показа продукта партнёрам ПВТ.</footer>
</div>
<script>
const MEMBERS=[
{n:'Алекс',r:'председатель / продукт',c:'#4d8dff'},
{n:'Юра',r:'правление / операции',c:'#22c55e'},
{n:'Марина',r:'финансы и договоры',c:'#f59e0b'},
{n:'Дима',r:'инженерия',c:'#a78bfa'},
{n:'Стас',r:'партнёрства',c:'#ef4444'},
{n:'Наталья',r:'юрист-консультант',c:'#14b8a6'}
];
let sel=null, votes={}, done=false, ntx=0;
const QUORUM=Math.ceil(MEMBERS.length*2/3); // ⅔
function $(id){return document.getElementById(id)}
function log(kind,head,body){ntx++;const d=document.createElement('div');d.className='tx '+kind;d.innerHTML='<div class="txhead"><span>'+head+'</span><span>#'+ntx+'</span></div><div class="txbody">'+body+'</div>';$('ledger').prepend(d)}
function renderMembers(){
$('mcount').textContent=MEMBERS.length+' (3 физлица минимум по ГК — норма соблюдена)';
$('members').innerHTML=MEMBERS.map((m,i)=>'<div class="member'+(sel===i?' sel':'')+'" onclick="pick('+i+')"><div class="avatar" style="background:'+m.c+'">'+m.n[0]+'</div><div><div class="mname">'+m.n+'</div><div class="mrole">'+m.r+'</div></div><div class="voteweight">1 голос</div></div>').join('');
}
function pick(i){sel=i;renderMembers();log('peg','SELECT_SIGNER','подписант: '+MEMBERS[i].n+' ('+MEMBERS[i].r+') — голос будет подписан адресом члена')}
function renderVotes(){
const v=MEMBERS.map((m,i)=>votes[i]);
const yes=v.filter(x=>x===true).length,no=v.filter(x=>x===false).length,abst=MEMBERS.length-yes-no;
$('bar').innerHTML='<div class="seg" style="width:'+(yes/MEMBERS.length*100)+'%;background:#22c55e"></div><div class="seg" style="width:'+(no/MEMBERS.length*100)+'%;background:#ef4444"></div>';
$('tally').innerHTML='<span class="ok">'+yes+' за</span> · <span class="err">'+no+' против</span> · '+abst+' молчат';
const turnout=yes+no;
$('cq').textContent=turnout+'/'+MEMBERS.length+' (нужно '+QUORUM+')';
const q=turnout>=QUORUM, pass=q&&yes>no;
if(!done){
$('btnExec').disabled=!pass;
$('pstate').innerHTML=pass?'<span class="ok">кворум есть · большинство ЗА</span>':'<span class="warn">ждёт голосов</span>';
if(pass)$('verdict').innerHTML='<b>Кворум достигнут, большинство — за.</b> Нажмите «Исполнить решение»: смарт-контракт оформит решение собрания операцией в реестре (шаг 4).';
else $('verdict').innerHTML='Явка '+(turnout)+' из '+MEMBERS.length+', кворум '+QUORUM+'. Голосуйте от имени разных членов (клик слева → кнопка).';
}
}
function castVote(against){
if(done){log('err','ERROR','предложение уже исполнено — реестр неизменяем');return}
if(sel===null){log('err','ERROR','выберите члена в реестре слева');return}
if(votes[sel]!==undefined){log('err','REJECT','двойной голос отклонён: адрес '+MEMBERS[sel].n+' уже голосовал (анти-манипуляция реестра)');return}
votes[sel]=!against;
log('gov','VOTE',''+MEMBERS[sel].n+' → '+(against?'ПРОТИВ':'ЗА')+' · подпись адреса принята');
renderVotes();
}
function executeProposal(){
const yes=Object.values(votes).filter(x=>x).length;
if($('btnExec').disabled){log('err','EXECUTE_REJECT','условия устава не выполнены (кворум/порог)');return}
done=true;
$('pstate').innerHTML='<span class="ok">ИСПОЛНЕНО</span>';
log('gov','EXECUTE','решение собрания оформлено: Конституция v1 принята ('+yes+' за, кворум '+QUORUM+') · запись реестра = протокол собрания');
log('peg','LEGAL_BRIDGE','сформирован пакет юрдействий: протокол для подписи председателем, ссылка на запись реестра — устав выше контракта, контракт его исполняет');
$('verdict').innerHTML='<b>Готово.</b> Решение зафиксировано в реестре (неизменяемо), юрпакет передан на подпись. Дальше по дорожной карте: юристы ПВТ → резидентство → устав с «цифровым режимом» → dual-running пилот.';
$('btnVote').disabled=true;$('btnExec').disabled=true;
}
renderMembers();renderVotes();
</script>
</body>
</html>

View file

@ -117,6 +117,131 @@ def _wire_tool_budget(agent, max_calls: int = 30):
return agent return agent
# ── e-261014163808-4da3ec: TTFT-watchdog + ретрай висящего вызова ──────────
#
# Диагноз (13:08 и 16:11 14-10-2026, павшие hour-тики): 9router принимает
# POST и открывает стрим, но НИ ОДНОГО байта контента не приходит — в логе
# роутера «▶ POST ... STREAM» есть, «📊 DONE» после него нет. Клиентский
# timeout=90 и SDK-ретраи не спасали: 4 попытки × тот же висящий апстрим,
# в итоге APITimeoutError уносил ВЕСЬ тик (TaskFailed по батчу, CycleError,
# эскалация). Воркер — subprocess, повторить механизм с уровня нельзя,
# поэтому фикс в самом воркере.
#
# Решение: обёртка стрима модели. Признак живого апстрима — первый
# content-chunk; пока его нет, watchdog: N секунд без токена → стрим
# закрываем и делаем СВЕЖИЙ вызов (новый POST — роутер сам уводит его на
# живой апстрим). Начавший отвечать апстрим не трогаем: обрыв на середине
# ответа дороже редкого зависания.
#
# Гарантии: strands дождётся события message_stop новой попытки — цикл
# агента консистентен; BudgetSpent идемпотентен (usage по фактически
# дошедшим assistant-сообщениям); инструменты исполняются после конца
# стрима, так что дублей tool-вызовов нет.
_TTFT_FIRST_TOKEN_BUDGET = float(os.environ.get("SIMBIOT_LLM_TTFT_BUDGET", "45"))
async def _aclose_gen(gen):
"""Мягко закрыть асинхронный генератор (отмена висящего стрима)."""
close = getattr(gen, "aclose", None)
if close is not None:
try:
await close()
except Exception:
pass
def _chunk_type(chunk) -> str:
"""Тип чанка стрима strands: атрибут или ключ словаря."""
ct = getattr(chunk, "chunk_type", None)
if not ct and isinstance(chunk, dict):
ct = chunk.get("chunk_type")
return ct or ""
def _with_ttft_retry(model, level: str, call_timeout: float):
"""Обернуть модель: watchdog первого токена + ретраи висящих вызовов.
first_budget — сколько ждать ПЕРВЫЙ content-chunk; capped сверху
клиентским call_timeout (старый таймаут продолжает страховать весь
вызов на случай совсем глухого апстрима). Попыток 3: нормальный
TTFT роутера 2-13с (лог), 45с — запас x3; 3×45 + ретраи SDK не
пробивают tick_timeout уровня (min 480с, hour 2880с).
"""
import asyncio
import time as _time
first_budget = min(_TTFT_FIRST_TOKEN_BUDGET, max(float(call_timeout), 1.0))
max_attempts = 3
base_stream = model.stream
def _note_retry(attempt: int, why: str):
# след в стор: сколько раз сработал watchdog (видно из трейса тика)
try:
from harness import es
es.append_event("trace", f"ttft-retry:{level}", "TTFTWatchdogRetry", {
"level": level, "attempt": attempt, "reason": why,
"first_token_budget_s": first_budget,
})
except Exception:
pass # телеметрия — best effort
class _TTFTModel:
"""Прозрачный прокси: всё делегируется модели, stream() — под watchdog.
Не наследуем Model (ABC с abstractmethods): strands_harness.resolve_model
принимает по isinstance(model, (Model, ModelRouter)), поэтому после
делегирования регистрируемся как Model через Model.register(_TTFTModel)
— и делегирование __getattr__, и валидный isinstance для create_harness.
"""
def __init__(self, inner):
object.__setattr__(self, "_inner", inner)
def __getattr__(self, name):
return getattr(object.__getattribute__(self, "_inner"), name)
async def stream(self, *args, **kwargs):
attempts = 0
while True:
attempts += 1
gen = base_stream(*args, **kwargs)
it = gen.__aiter__()
got_content = False
try:
while True:
try:
chunk = await asyncio.wait_for(
it.__anext__(), timeout=first_budget)
except StopAsyncIteration:
# пустой стрим без контента — тоже симптом висения,
# но ретраить бессмысленно: считаем ответом и уходим
return
ct = _chunk_type(chunk)
if ct in ("content_delta", "content_start"):
got_content = True
yield chunk
if got_content:
break
except asyncio.TimeoutError:
why = f"{first_budget:.0f}s без первого токена (попытка {attempts})"
if attempts < max_attempts:
_note_retry(attempts, why)
await _aclose_gen(gen)
continue # свежий POST: роутер уведёт на живой апстрим
raise TimeoutError(
f"апстрим завис до первого токена: {max_attempts} попытки "
f"по {first_budget:.0f}s — {why}") from None
# апстрим живой (контент пошёл) — доедаем без watchdog
async for chunk in it:
yield chunk
return
from strands.models.model import Model as _StrandsModel
_StrandsModel.register(_TTFTModel)
return _TTFTModel(model)
def build_level_model_from_payload(payload: dict): def build_level_model_from_payload(payload: dict):
"""Модель для воркера — та же сборка, что и в runtime.build_level_model: """Модель для воркера — та же сборка, что и в runtime.build_level_model:
9router понимает chat/completions (usage там же), Responses API — шим без usage. 9router понимает chat/completions (usage там же), Responses API — шим без usage.
@ -144,7 +269,7 @@ def build_level_model_from_payload(payload: dict):
# «Connection error» к роутеру ронял весь тик с первой попытки; openai SDK # «Connection error» к роутеру ронял весь тик с первой попытки; openai SDK
# умеет сам ретраить connection-error/429/5xx с экспонентой — даём запас. # умеет сам ретраить connection-error/429/5xx с экспонентой — даём запас.
max_retries = int(os.environ.get("SIMBIOT_LLM_MAX_RETRIES", "3")) max_retries = int(os.environ.get("SIMBIOT_LLM_MAX_RETRIES", "3"))
return OpenAIModel( base = OpenAIModel(
# timeout открытого клиента (openai SDK) режет каждый chat.completions # timeout открытого клиента (openai SDK) режет каждый chat.completions
# вызов; при зависании апстрима получаем APITimeoutError за N сек и # вызов; при зависании апстрима получаем APITimeoutError за N сек и
# strands event-loop завершает тик штатно (без потери предыдущих шагов) # strands event-loop завершает тик штатно (без потери предыдущих шагов)
@ -154,6 +279,11 @@ def build_level_model_from_payload(payload: dict):
model_id=model_id, model_id=model_id,
**({"params": params} if params else {}), **({"params": params} if params else {}),
) )
# e-261014163808-4da3ec (часовой фикс): ретрай висящего ДО-ПЕРВОГО-ТОКЕНА
# апстрим-вызова. Кейс дня (router-лог 16:09:50): POST принят, TTFT так и
# не пришёл, ~90с спустя APITimeoutError унёс ВЕСЬ тик вместе с claimed-
# батчем. SDK-ретраи не спасали: 4 попытки × 90с висения = тик мёртв.
return _with_ttft_retry(base, payload.get("level") or "hour", call_timeout)
def _emit(payload, obj): def _emit(payload, obj):

0
tasks.db Normal file
View file

103
tests/test_ttft_retry.py Normal file
View file

@ -0,0 +1,103 @@
#!/usr/bin/env python
"""E2E-проверка TTFT-watchdog обёртки воркера (e-261014163808-4da3ec).
Три сценария на фейковом стриме:
1. живой апстрим (контент сразу) → все чанки, ни одного ретрая;
2. висит до первого токена, оживёт на 2-й попытке → ретрай, ответ完整;
3. висит всегда (3 попытки) → TimeoutError с внятным текстом.
Плюс дым: сборка модели payload-ом возвращает прокси с рабочим stream().
Запуск: ./venv/bin/python tests/test_ttft_retry.py
"""
import asyncio
import os
import sys
import types
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from harness import _llm_worker as W # noqa: E402
FAILS = []
def check(name, cond, extra=""):
print(("PASS " if cond else "FAIL ") + name + (f" | {extra}" if extra and not cond else ""))
if not cond:
FAILS.append(name)
def chunk(ct):
return {"chunk_type": ct}
def fake_stream_model(script):
"""script: список «попыток», каждая — список чанков или raise Hang."""
calls = {"n": 0}
class M:
async def stream(self, *a, **k):
i = calls["n"]
calls["n"] += 1
act = script[i]
if act == "HANG":
await asyncio.sleep(30) # дольше watchdog-бюджета
yield chunk("message_start")
return
for c in act:
yield chunk(c)
return M(), calls
async def run(model):
wrapped = W._with_ttft_retry(model, "hour", 90)
out = []
async for c in wrapped.stream():
out.append(W._chunk_type(c))
return out
async def main():
os.environ["SIMBIOT_LLM_TTFT_BUDGET"] = "0.5" # быстрый watchdog для теста
W._TTFT_FIRST_TOKEN_BUDGET = 0.5
# 1. живой апстрим
m, calls = fake_stream_model([
["message_start", "content_delta", "content_delta", "message_stop"]])
out = await run(m)
check("живой стрим проходит целиком",
out == ["message_start", "content_delta", "content_delta", "message_stop"],
str(out))
check("живой стрим — 1 попытка", calls["n"] == 1, str(calls))
# 2. виснет, оживает на 2-й
m, calls = fake_stream_model([
"HANG",
["message_start", "content_delta", "message_stop"]])
out = await run(m)
check("зависший вызов ретраится и отвечает",
out == ["message_start", "content_delta", "message_stop"], str(out))
check("ретрай сделал ровно 2 попытки", calls["n"] == 2, str(calls))
# 3. виснет всегда
m, calls = fake_stream_model(["HANG", "HANG", "HANG"])
try:
await run(m)
check("глухой апстрим даёт TimeoutError", False, "не упал")
except TimeoutError as e:
check("глухой апстрим даёт TimeoutError", "завис до первого токена" in str(e), str(e))
check("глухой апстрим — ровно 3 попытки", calls["n"] == 3, str(calls))
# 4. делегирование атрибутов прокси
m, _ = fake_stream_model([["content_delta"]])
m.foo = "bar"
wrapped = W._with_ttft_retry(m, "hour", 90)
check("прокси делегирует атрибуты", getattr(wrapped, "foo", None) == "bar")
print()
print("ИТОГ:", "ВСЕ ОК" if not FAILS else f"ПРОВАЛЫ: {FAILS}")
sys.exit(1 if FAILS else 0)
asyncio.run(main())