Compare commits
No commits in common. "b2391c2b5b6cf33711088b79eaa6bb4f7de66901" and "fa815cd55bd237d14462279a2e3883b7750ea0fc" have entirely different histories.
b2391c2b5b
...
fa815cd55b
41 changed files with 58 additions and 2509 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
|
|
@ -1,119 +0,0 @@
|
||||||
<!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 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>
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
<!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> смарт-контракт > устав > 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> роспуск: срок, большинство >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>Контракт > устава</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>
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
<!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>
|
|
||||||
35
cli.py
35
cli.py
|
|
@ -187,9 +187,6 @@ def main():
|
||||||
p_mcp.add_argument("--list", action="store_true", help="показать тулы шлюза и выйти")
|
p_mcp.add_argument("--list", action="store_true", help="показать тулы шлюза и выйти")
|
||||||
p_mcp.add_argument("--timeout", type=int, default=None, help="таймаут запроса, сек")
|
p_mcp.add_argument("--timeout", type=int, default=None, help="таймаут запроса, сек")
|
||||||
|
|
||||||
p_mcs = sub.add_parser("mcp-stats", help="статистика MCP-вызовов по журналу mcp_calls.log (t-261015091108)")
|
|
||||||
p_mcs.add_argument("--tail", type=int, default=10, help="последние N вызовов (0 — только сводка)")
|
|
||||||
|
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
if args.cmd == "init":
|
if args.cmd == "init":
|
||||||
|
|
@ -450,38 +447,6 @@ def main():
|
||||||
ap.error("mcp-call: укажи tool (или --list)")
|
ap.error("mcp-call: укажи tool (или --list)")
|
||||||
kw = {"timeout": args.timeout} if args.timeout else {}
|
kw = {"timeout": args.timeout} if args.timeout else {}
|
||||||
print(portal_mcp.call_tool(args.tool, args.arguments, **kw))
|
print(portal_mcp.call_tool(args.tool, args.arguments, **kw))
|
||||||
elif args.cmd == "mcp-stats":
|
|
||||||
# t-261015091108: статистика MCP-вызовов из журнала mcp_calls.log
|
|
||||||
import collections
|
|
||||||
log_path = os.path.join(_paths.home(), "mcp_calls.log")
|
|
||||||
if not os.path.exists(log_path):
|
|
||||||
print("(журнала ещё нет — mcp_calls.log пуст, вызовов не было)")
|
|
||||||
return
|
|
||||||
rows = []
|
|
||||||
with open(log_path, encoding="utf-8") as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
rows.append(json.loads(line))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
by_tool = collections.Counter(r.get("tool", "?") for r in rows)
|
|
||||||
by_level = collections.Counter(r.get("level", "?") for r in rows)
|
|
||||||
fails = sum(1 for r in rows if not r.get("ok"))
|
|
||||||
ms = [r.get("ms", 0) for r in rows if isinstance(r.get("ms"), int)]
|
|
||||||
print(f"всего вызовов: {len(rows)} (ошибок: {fails})")
|
|
||||||
if ms:
|
|
||||||
print(f"время ответа: медиана {sorted(ms)[len(ms)//2]}мс, макс {max(ms)}мс")
|
|
||||||
print("по тулам: " + ", ".join(f"{k}×{v}" for k, v in by_tool.most_common()))
|
|
||||||
print("по уровням: " + ", ".join(f"{k}×{v}" for k, v in by_level.most_common()))
|
|
||||||
if args.tail:
|
|
||||||
print(f"\nпоследние {min(args.tail, len(rows))}:")
|
|
||||||
for r in rows[-args.tail:]:
|
|
||||||
args_s = json.dumps(r.get("arguments", {}), ensure_ascii=False)[:120]
|
|
||||||
print(f"{r.get('ts','?')} [{r.get('level','?')}] {r.get('tool','?')} "
|
|
||||||
f"{'ok' if r.get('ok') else 'FAIL'} {r.get('ms','?')}мс {args_s}")
|
|
||||||
elif args.cmd == "escalate":
|
elif args.cmd == "escalate":
|
||||||
es.init_db()
|
es.init_db()
|
||||||
eid, to_level = commands.raise_escalation(
|
eid, to_level = commands.raise_escalation(
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
# Аудит burn 9router (7д) — t-261014152017-42a51d, 2026-10-14
|
|
||||||
|
|
||||||
**Итог 7д: 198.07 у.е., 15 592 запроса.** AUM=731.44; burn = −27%/нед к AUM — прямой минус.
|
|
||||||
|
|
||||||
## Топ-потребители (по apiKey, 7д)
|
|
||||||
|
|
||||||
| # | Потребитель | Запросов | у.е. | Доля | Ср. промпт |
|
|
||||||
|---|---|---:|---:|---:|---:|
|
|
||||||
| 1 | **simbiot-harness** (мы сами) | 5 230 | **68.52** | 35% | 21.8k ток |
|
|
||||||
| 2 | agent-Cody | 3 934 | 39.61 | 20% | 18.4k |
|
|
||||||
| 3 | agent-Mon | 4 538 | 26.52 | 13% | 19.4k |
|
|
||||||
| 4 | fleet-agents | 391 | 23.05 | 12% | 72.8k |
|
|
||||||
| 5 | user-alex-sddsaa | 954 | 17.23 | 9% | 28.9k |
|
|
||||||
| 6-8 | unDREAMer/AI-corp/Rita | ~450 | 20.3 | 10% | 65-73k |
|
|
||||||
|
|
||||||
Модель-монополист: **glm-5.3 = 186.50 у.е. (94%)**, эффективные ставки ~$0.62/M_вход, ~$48.8/M_выход. Цены из факт-использования (usageHistory), не прайс.
|
|
||||||
|
|
||||||
## Собственный burn харнеса — уже оптимизирован гейтом
|
|
||||||
|
|
||||||
- Недели (по 7д): **553 → 70 → 20 у.е.** (нед.39→41). Гейт важности (e-260930062242+) срезал ~96% от пика.
|
|
||||||
- Тек. неделя: 68.57 у.е. — рост от 20 из-за тяжёлых M/L-тиков с агентными циклами.
|
|
||||||
- 99.9% бюджета — промпт-токены (113.4M in vs 1.36M out за 7д): дорого сама пересылка накопленного контекста на каждый шажок агентного цикла (до 75k ток/шаг, серии по ~20 шагов, напр. 02:33 UTC 14.10).
|
|
||||||
- 46% запросов (2 431) завершаются ≤100 токенов выхода — «пустые» итерации, каждая пересылает полный контекст.
|
|
||||||
|
|
||||||
## Конкретные рекомендации (оценка экономии за 7д)
|
|
||||||
|
|
||||||
1. **Кэп контекста агентных циклов** (S, сама фиксится): ограничить пересылку накопленного контекста в изолированных ранерах. Серии 50-99k ток на шаг при ответах ≤100 ток. Потенциал: −30-40% собственного burn харнеса (≈ −20 у.е./7д).
|
|
||||||
2. **Глубокий ретрай контекста неявный**: TTFT-watchdog ретраит вызов с полным контекстом (3×45с). Дешевле — короткий ping-запрос как healthcheck перед ретраем. Потенциал: −5-8 у.е./7д.
|
|
||||||
3. **M-задачи с агентными циклами на glm-5.3-flash** (flash_share → 0.57, объём х2 ниже текущего): пилот-симуляция дала +351% к E[ΔAUM год] против статус-кво (−758→+1908 у.е. на горизонте). Требует решения Alex — в манифест дня.
|
|
||||||
4. **Квоты чужим агентам** (fleet-agents, ср. промпт 72.8k) — вне моего контроля, рекомендовано дню/Alex'у через отчёт-адвокат.
|
|
||||||
|
|
||||||
## Цель «−10% без потери качества»
|
|
||||||
|
|
||||||
- Собственный burn 68.5 → ~48 у.е./7д (−30%) реализуемо пунктами 1-2 без смены моделей и потери качества тиков: чистая экономия ≈ −20.4 у.е./7д ≈ **−10.3% от общего burn 198** — цель задачи закрывается только своей частью, без квот чужим агентам.
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
# Дайджест hour 2026-10-15 ~23:00 UTC (тик hour-497807)
|
|
||||||
|
|
||||||
## Выжимка часа (22:00→23:00 UTC)
|
|
||||||
|
|
||||||
- **Тихий час на входящих: 0 неотвеченных, мин-уровень 6 тиков подряд ниже порога важности
|
|
||||||
(LLM не запускался).** Весь час — договорная серия Алекса m-516174602–626 (SPA без
|
|
||||||
ЗП-обязательств v2: Placement Fee 25% от годового, разово, salary growth убран),
|
|
||||||
вся нить закрыта батч-ответами мин-уровня до начала часа.
|
|
||||||
- **Алекс поймал баг памяти контекста (m-516174619–621, эскалация e-261015222515):** минутка
|
|
||||||
не помнила только что обсуждённый SPA-договор — после ReplySent входящее выпадало из
|
|
||||||
контекста тика. Фикс этого часа (см. ниже), эскалация закрыта.
|
|
||||||
|
|
||||||
## Эскалации — разбор и закрытие
|
|
||||||
|
|
||||||
- **e-261015222515-9121d0 (warn) «Потеря контекста разговора на минутном уровне» — ЗАКРЫТА,
|
|
||||||
фикс вкоммичен (0daf101).** build_context теперь добавляет секцию «НЕДАВНИЙ РАЗГОВОР»:
|
|
||||||
последние 12 отвеченных реплик тех же чатов (текст + наш ответ `reply_text`) рядом
|
|
||||||
с батчем неотвеченных. Источник — существующая проекция инбокса (кап 100), отдельный
|
|
||||||
стор не нужен; секция помечена «НЕ отвечай снова». Тест test_inbox_batch.py +6 PASS,
|
|
||||||
регрессии чисто (reply_routing, seen_indication, msg_artifact_race, gate, proactive,
|
|
||||||
task_batch, agent_channel). Рестарт демона отложен через restart_after_tick.sh —
|
|
||||||
подхватит фикс сразу после конца LLM-воркера (не убивая живой тик).
|
|
||||||
|
|
||||||
## Состояние лестницы
|
|
||||||
|
|
||||||
- Уровни: sec/min/hour/day — все «ожидает», очередь задач пуста (0 ready / 0 running).
|
|
||||||
- Открытых эскалаций: 0 (единственная этого часа закрыта).
|
|
||||||
- Ошибки уровней — исторические (min 42, hour 9 — таймауты воркеров 480с/2880с, разобраны
|
|
||||||
прошлыми часами), новых за час нет.
|
|
||||||
|
|
||||||
## AUM
|
|
||||||
|
|
||||||
- AUM = 731.44 у.е. (без изменений); burn 9router 7д = 238.62 у.е. (было 193.68 — рост
|
|
||||||
~23% за сутки, источник: тики этого дня, включая длинные портальные задачи). Burn —
|
|
||||||
прямой минус к AUM; при сохранении темпа день-тик стоит взять под контроль расхода
|
|
||||||
(кандидат в задачи дня: ревизия spend по уровням).
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
# Дайджест hour 2026-10-16 ~13:05 UTC (тик hour-497821)
|
|
||||||
|
|
||||||
## Выжимка часа (12:00→13:05 UTC)
|
|
||||||
|
|
||||||
- **Главная работа — t-261016120111 (M, ~60м) закрыта: Claude Code на серверах —
|
|
||||||
bypass permissions по умолчанию + права на редактирование любых файлов.**
|
|
||||||
- Правки scripts/deploy-claude-code.sh (коммит b975518): флаг
|
|
||||||
`--dangerously-skip-permissions` прокидывается в claude ВСЕГДА по умолчанию
|
|
||||||
(opt-out `--ask-permissions`); сервис ttyd-claude.service теперь ставится от
|
|
||||||
**root** (User=root, HOME=/root) — агенту доступен любой файл на сервере;
|
|
||||||
ограниченный режим сохранён как явный opt-in: `--run-as USER` (+ юнит
|
|
||||||
перезапишется на непривилегированного юзера).
|
|
||||||
- Боевой прогон на **hermes-gennady (46.224.196.212)**: claude 2.1.301,
|
|
||||||
ttyd 1.7.7 установлены, юнит active, проверено юнит-содержимое
|
|
||||||
(User=root + флаг в ExecStart), HTTP 200 с basic-auth, claude принимает
|
|
||||||
флаг. Терминал: http://46.224.196.212:7681 (claude / GennadyCoop261016).
|
|
||||||
- Смысл для AUM: единый стандарт развёртки — каждый новый агент коопа получает
|
|
||||||
полноценный автономный Claude Code без ручных согласований прав.
|
|
||||||
|
|
||||||
- **Эскалация e-261016101039 (Gennady-миграция, ждала бюджетного грин-лайта дня) —
|
|
||||||
закрыта как выполненная фактически:** caddy уже переключён на
|
|
||||||
46.224.196.212:3101, панель https://gennady.ui.shkor.com → 200,
|
|
||||||
cloudcli-ui.service active на новом сервере, heartbeat агента 13:00:50 UTC.
|
|
||||||
Бюджетный вопрос снят исполнением; логика «1 агент = 1 сервер» закрыта.
|
|
||||||
|
|
||||||
- **Эскалация e-261016124005 (min-тик упал по 480с-таймауту LLM-воркера) —
|
|
||||||
закрыта с выводом:** разовый таймаут на фоне хронической перегрузки общего
|
|
||||||
хоста (load ~5-7, swap 7.8/8G, disk 91%). Часовой уровень продолжает следить:
|
|
||||||
min-тик идёт (последний 13:03), ошибки исторические.
|
|
||||||
|
|
||||||
## Состояние лестницы
|
|
||||||
|
|
||||||
- Уровни sec/min/hour/day — работают (min last_tick 13:03, hour 13:00). Ошибки:
|
|
||||||
min 43 / hour 10 (все — таймауты воркеров 480с/2880с, хроника перегрузки).
|
|
||||||
- Открытых эскалаций: 1 — e-261016114807 (hour→day, таймаут часового воркера
|
|
||||||
2880с, 11:48 UTC) — зона дневного уровня, ждёт разбора дня.
|
|
||||||
- Задачи: t-261016120111 закрыта (доставлено в TG); t-261016120312
|
|
||||||
(S, плашка Deploy Claude Panel) — running, идёт минутным уровнем.
|
|
||||||
|
|
||||||
## AUM
|
|
||||||
|
|
||||||
- AUM = 731.44 у.е. (без изменений); burn 9router 24ч = 38.27, 7д = 239.26 у.е.
|
|
||||||
(всплеск к прошлому часу: 218.97 → 239.26) — ревизия spend по уровням
|
|
||||||
остаётся кандидатом в задачи дня.
|
|
||||||
- Вклад часа в долгосрочную AUM: стандарт развёртки агентов (bypass+root) —
|
|
||||||
ускорение онбординга каждого следующего агента стада; закрытая Gennady-миграция
|
|
||||||
— снятый риск утечки кода коопа и разгрузка общего хоста.
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
# Аудит мета-информации в событиях (t-261015083454, принцип m-516174369)
|
|
||||||
|
|
||||||
Дата: 2026-10-15. Стор: ~2.8M событий. Принцип Alex: **события должны
|
|
||||||
сохраняться с максимумом мета-информации** — событие без контекста теряет
|
|
||||||
ценность при разбирательстве инцидентов и аналитике burn/AUM.
|
|
||||||
|
|
||||||
## 1. Системные поля (у всех событий, таблица `events`)
|
|
||||||
|
|
||||||
| Поле | Есть | Оценка |
|
|
||||||
|---|---|---|
|
|
||||||
| `id`, `timestamp` (UTC ISO), `version` | ✅ | ок |
|
|
||||||
| `aggregate` / `aggregate_id` | ✅ | ок, но двойная кодировка меты (см. §3) |
|
|
||||||
| `sig` / `key_id` / `prev_sig` (ed25519-цепочка) | ✅ | криптоаудит t-260930083716 |
|
|
||||||
| `trace_id`, `level`, `источник`, `context` | ❌ в payload | точечно, неравномерно |
|
|
||||||
|
|
||||||
## 2. Аудит payload по типам (выборка последних 150 на тип)
|
|
||||||
|
|
||||||
| Тип (кол-во) | trace_id | level | ts | источник | прочая мета |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| `CycleTicked` (1.35M) | — | ✅ agg_id | — | — | seq |
|
|
||||||
| `ReflexObserved` (1.33M) | — | ✅ agg_id | ✅ | — | load1 |
|
|
||||||
| `ToolCalled` (34.7k) | **❌** | **❌** | **❌** | — | tool, input |
|
|
||||||
| `ToolResult` (34.7k) | **❌** | **❌** | **❌** | — | tool=**«?»-заглушка**, result |
|
|
||||||
| `BudgetSpent` (28.6k) | ✅ | ✅ | — | ✅ | model, tokens |
|
|
||||||
| `DigestWritten` (21.3k) | — (summary_ref) | ✅ | — | stub-флаг | period, text |
|
|
||||||
| `InvocationStarted` (1.4k) | ✅ | ❌ | ✅ | — | model |
|
|
||||||
| `TaskSubmitted` (230) | — | — | — | ✅ source | id, size, est_minutes |
|
|
||||||
| `TaskClaimed` (434) | — | ✅ | — | — | session_id |
|
|
||||||
| `TaskCompleted` (317) | — (result_ref) | **❌** | — | — | answer |
|
|
||||||
| `TaskFailed` (12) | — | ×2/12 | — | ×1/12 | error |
|
|
||||||
| `EscalationRaised` (96) | — | ✅ from/to | — | ✅ source | severity, task_id, context |
|
|
||||||
| `MessageReceived` (1238) | — | — | ✅ date | ✅ username | 19 полей, эталон |
|
|
||||||
| `MessageTranscribed` (65) | — | — | — | ✅ engine | transcript |
|
|
||||||
| `RagQueried` (412) / `PortalMcpCalled` (64) | ✅ | ✅ | ✅ ms | — | query/result_preview |
|
|
||||||
| `CycleError` (51) | — | ✅ | — | — | error, trace |
|
|
||||||
| `AumMeasured` (16) | — | — | ✅ | — | aum, burn_24h/7d, policy |
|
|
||||||
|
|
||||||
## 3. Найденные пробелы (по убыванию веса)
|
|
||||||
|
|
||||||
1. **`ToolCalled`/`ToolResult` — 68k событий без `trace_id`/`level`/`ts`**
|
|
||||||
(⅔ всех нетаймовых событий стора; связь с тиком только через
|
|
||||||
декодирование `aggregate_id = "<level>-<seq>:<n>"`). У `ToolResult`
|
|
||||||
имя инструмента — заглушка `"?"` (после параллельных вызовов вообще
|
|
||||||
непонятно, чей результат).
|
|
||||||
2. **`TaskCompleted`/`TaskFailed` без `level`** — не видно, какой уровень
|
|
||||||
лестницы закрыл/уронил задачу; `trace_id` только как `result_ref`.
|
|
||||||
3. `DigestWritten.summary_ref` ≠ trace_id напрямую — это trace_id тика,
|
|
||||||
но имя не говорит об этом (путаница при разборе инцидентов).
|
|
||||||
4. `BudgetSpent` без `ts` (после миграции v2 на инкрементальные записи —
|
|
||||||
только `timestamp` колонки; хронология внутри тика не видна).
|
|
||||||
5. Двойная кодировка: `level` дублируется в `aggregate_id` и payload —
|
|
||||||
данные об оси «уровень» живут в двух местах с разной полнотой.
|
|
||||||
|
|
||||||
## 4. Внедрено в этом тике (коммит f3ab66e)
|
|
||||||
|
|
||||||
- **`harness/runtime.py` `_wire_trace_hooks`**: `trace_id`, `ts`, `level`
|
|
||||||
теперь в каждом trace-событии (`InvocationStarted`/`ToolCalled`/
|
|
||||||
`ToolResult`); `ToolResult` дополнительно `duration_ms` (из
|
|
||||||
`AfterToolCallEvent.duration`), `ok: bool`, `exception` (до 500с),
|
|
||||||
имя инструмента из `tool_use` (заглушка `"?"` убрана).
|
|
||||||
- **`harness/_llm_worker.py`**: проброс `level` в `_wire_trace_hooks`.
|
|
||||||
- **`harness/scheduler.py`**: `TaskCompleted`/`TaskFailed` получают
|
|
||||||
`level` + `trace_id` тика-воркера.
|
|
||||||
- Проверено: смоук-скрипт (мета во всех 4 типах), 5 смежных тестов PASS
|
|
||||||
(task_batch 11, timeout_kill 2, ttft_retry, tool_budget, task_claim_race).
|
|
||||||
|
|
||||||
Прирост мета-покрытия: trace-события (доля LLM-активности без мета-
|
|
||||||
контекста) — с **0% → 100%** новых событий; TaskCompleted/TaskFailed —
|
|
||||||
с 0% → 100% `level`.
|
|
||||||
|
|
||||||
## 5. План на следующие итерации (не внедрено — вне бюджета тика)
|
|
||||||
|
|
||||||
| # | Что | Зачем | Где |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 1 | `ts` в `BudgetSpent` (воркер уже пишет после каждого вызова) | хронология spend внутри тика, детект всплесков burn | `_llm_worker._emit_budget_incremental` |
|
|
||||||
| 2 | `trace_id` в `DigestWritten` (рядом с `summary_ref`) и переименование семантики `summary_ref` в доках | единая ось «тик → трасса → spend» | `scheduler.py` |
|
|
||||||
| 3 | `source` (`cli`/`poller`/`scheduler`/`daemon`) в `TaskClaimed`, `MessageSeen`, `ReplySent` | отличать ручные команды от автоматических | `scheduler.py`, `commands.py` |
|
|
||||||
| 4 | `host`/`pid`/`commit` в `InvocationStarted` (или в `CycleTicked` раз в тик) | мульти-инстансность: кто именно писал (инцидент rogue-key t-261008084600) | `runtime.py` |
|
|
||||||
| 5 | `duration_ms` в `CycleTicked`/`DigestWritten` | находить медленные тики без парсинга логов | `scheduler.py` |
|
|
||||||
| 6 | Унификация: helper `es.meta(level=…)` для единообразной меты во всех `append_event` | убрать ручную возню, единый контракт | `es.py` |
|
|
||||||
|
|
||||||
Ретро-заполнение старых событий **не делаем**: стор append-only + подписи
|
|
||||||
ed25519 — перезапись сломает тампер-цепочку. Граница: события до
|
|
||||||
f3ab66e — «до мета-стандарта», после — полные.
|
|
||||||
|
|
||||||
## 6. AUM-фильтр
|
|
||||||
|
|
||||||
Аудит меты — инвестиция в долгосрочную базу: разбор инцидентов (часы
|
|
||||||
диагностики → минуты по SQL-запросу), честный burn-контроль 9router
|
|
||||||
(193.68 у.е./7д — прямой минус AUM), доверие ко стору у внешних
|
|
||||||
проверяющих (подписи + мета =аудито-готовность). Слабого короткого дохода
|
|
||||||
нет — чистый плюс в базу.
|
|
||||||
|
|
@ -117,131 +117,6 @@ 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.
|
||||||
|
|
@ -269,7 +144,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"))
|
||||||
base = OpenAIModel(
|
return OpenAIModel(
|
||||||
# timeout открытого клиента (openai SDK) режет каждый chat.completions
|
# timeout открытого клиента (openai SDK) режет каждый chat.completions
|
||||||
# вызов; при зависании апстрима получаем APITimeoutError за N сек и
|
# вызов; при зависании апстрима получаем APITimeoutError за N сек и
|
||||||
# strands event-loop завершает тик штатно (без потери предыдущих шагов)
|
# strands event-loop завершает тик штатно (без потери предыдущих шагов)
|
||||||
|
|
@ -279,11 +154,6 @@ 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):
|
||||||
|
|
@ -447,8 +317,7 @@ def main():
|
||||||
memory=False,
|
memory=False,
|
||||||
skills=False,
|
skills=False,
|
||||||
)
|
)
|
||||||
agent = _wire_trace_hooks(agent, es, trace_id, model=payload.get("model"),
|
agent = _wire_trace_hooks(agent, es, trace_id, model=payload.get("model"))
|
||||||
level=level)
|
|
||||||
agent = _wire_budget_hooks(agent, level, trace_id, model=payload.get("model"))
|
agent = _wire_budget_hooks(agent, level, trace_id, model=payload.get("model"))
|
||||||
# хард-кап шагов (e-260930031800): cancel_tool + end_turn.
|
# хард-кап шагов (e-260930031800): cancel_tool + end_turn.
|
||||||
# ВАЖНО: НЕ используем limits={"turns": N} — нативный стоп SDK
|
# ВАЖНО: НЕ используем limits={"turns": N} — нативный стоп SDK
|
||||||
|
|
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Канал агент↔агент через MCP-шлюз портала (t-261015090129).
|
|
||||||
|
|
||||||
Петля обмена между агентами кооператива:
|
|
||||||
- ВХОД: MCP-тул poll_messages (портал, t-261015090129) — забрать новые
|
|
||||||
agent_message из своей очереди. Рефлекс демона вызывает его раз в
|
|
||||||
минуту (MIN_REFLEX_GAP), новые сообщения → MessageReceived в стор
|
|
||||||
(kind=agent_message, chat_id=portal:agent:<имя>) — минутный триаж
|
|
||||||
видит их как обычные входящие и отвечает.
|
|
||||||
- ВЫХОД: MCP-тул ask_agent — просьба/вопрос/передача другому агенту;
|
|
||||||
send_reply для чата portal:agent:* идёт через ask_agent, НЕ через TG.
|
|
||||||
|
|
||||||
Транспорт — тот же MCP-шлюз cil.by, что и portal_mcp (Bearer agt_...);
|
|
||||||
токен один (secrets/portal-mcp-token / SIMBIOT_PORTAL_MCP_TOKEN).
|
|
||||||
Курсор чтения — на портале (agent_message_cursors): прочитанное не
|
|
||||||
возвращается повторно, локальный дедуп по update_id в stor'е — вторая
|
|
||||||
линия защиты (POST-подтверждение может потеряться).
|
|
||||||
|
|
||||||
Наблюдаемость: трейс AgentChannelPull в event store (как PortalMcpCalled)
|
|
||||||
+ штамп канала harness/channel_agent.json (как у portal-канала).
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
from . import paths as _paths
|
|
||||||
|
|
||||||
# не чаще раза в минуту: очередь агент↔агент не требует суб-секундной реакции
|
|
||||||
MIN_REFLEX_GAP = 60.0
|
|
||||||
# не поднимать очередь сообщений чаще, чем раз в N минут для одного чата?
|
|
||||||
# нет: курсор на портале, повторный poll ничего не стоит.
|
|
||||||
_last_reflex = {"ts": 0.0}
|
|
||||||
_lock = threading.Lock()
|
|
||||||
|
|
||||||
|
|
||||||
def _log(msg: str):
|
|
||||||
print(f"[agent-channel] {msg}", flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _cfg_token() -> str:
|
|
||||||
"""Токен MCP: env сильнее файла (та же схема, что portal_mcp)."""
|
|
||||||
env = os.environ.get("SIMBIOT_PORTAL_MCP_TOKEN")
|
|
||||||
if env:
|
|
||||||
return env.strip()
|
|
||||||
try:
|
|
||||||
p = _paths.portal_mcp_token_path()
|
|
||||||
if p and os.path.exists(p):
|
|
||||||
return open(p).read().strip()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _rpc_tool(tool: str, arguments: dict) -> str:
|
|
||||||
"""Вызов MCP-тула через клиент portal_mcp (трейс/лимиты/guardrail там)."""
|
|
||||||
from . import portal_mcp as _mcp
|
|
||||||
return _mcp.call_tool(tool, json.dumps(arguments or {}), level="min")
|
|
||||||
|
|
||||||
|
|
||||||
def _es():
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
from harness import es
|
|
||||||
return es
|
|
||||||
|
|
||||||
|
|
||||||
def _agent_chat_id(from_agent: str) -> str:
|
|
||||||
return f"portal:agent:{(from_agent or '?').strip()[:60]}"
|
|
||||||
|
|
||||||
|
|
||||||
def store_id(portal_ev_id) -> str:
|
|
||||||
"""Ключ входящего в сторе: 'm-' + id события портала (агентные длиной 6+)."""
|
|
||||||
return "m-" + str(portal_ev_id or "").strip()[:80]
|
|
||||||
|
|
||||||
|
|
||||||
def _ingest_messages(msgs: list) -> int:
|
|
||||||
"""[{id, from, kind, text, ts}] → MessageReceived (идемпотентно по id).
|
|
||||||
|
|
||||||
Дедуп по read model: портальный курсор не сдвигается при сбое ingest —
|
|
||||||
повторный poll вернёт те же id, стор их уже не примет.
|
|
||||||
"""
|
|
||||||
if not msgs:
|
|
||||||
return 0
|
|
||||||
try:
|
|
||||||
es = _es()
|
|
||||||
model = es.run_projections()
|
|
||||||
except Exception:
|
|
||||||
return 0 # стор занят — poll повторится (курсор не сдвинулся, ок)
|
|
||||||
existing = {str(m.get("update_id")) for m in (model.get("inbox") or {}).values()
|
|
||||||
if isinstance(m, dict) and m.get("update_id") is not None}
|
|
||||||
ingested = 0
|
|
||||||
for d in msgs:
|
|
||||||
mid = str((d or {}).get("id") or "").strip()[:80]
|
|
||||||
from_agent = str((d or {}).get("from") or "?").strip()[:60] or "?"
|
|
||||||
text = str((d or {}).get("text") or "").strip()
|
|
||||||
if not mid or not text or mid in existing:
|
|
||||||
continue
|
|
||||||
data = {
|
|
||||||
"update_id": mid,
|
|
||||||
"chat_id": _agent_chat_id(from_agent),
|
|
||||||
"user_id": None,
|
|
||||||
"username": from_agent,
|
|
||||||
"first_name": f"агент {from_agent}",
|
|
||||||
"text": text[:4000],
|
|
||||||
"kind": "agent_message",
|
|
||||||
"file_id": None, "file_unique_id": None, "duration": None,
|
|
||||||
"date": None,
|
|
||||||
"source": "agent_mcp",
|
|
||||||
"agent_kind": str((d or {}).get("kind") or "message")[:20],
|
|
||||||
"agent_ts": str((d or {}).get("ts") or "")[:40],
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
es.append_event("inbox", store_id(mid), "MessageReceived", data)
|
|
||||||
except Exception:
|
|
||||||
continue # стор занят на этом msg — следующий проход
|
|
||||||
ingested += 1
|
|
||||||
return ingested
|
|
||||||
|
|
||||||
|
|
||||||
def pull() -> int:
|
|
||||||
"""ВХОД: poll_messages → ingest MessageReceived. 0 = тихо/ошибка (тихо)."""
|
|
||||||
token = _cfg_token()
|
|
||||||
if not token:
|
|
||||||
return 0 # канал выключен: харнес без MCP-токена
|
|
||||||
raw = _rpc_tool("poll_messages", {})
|
|
||||||
if raw.startswith("error:"):
|
|
||||||
_log(f"poll_messages: {raw[:200]}")
|
|
||||||
return 0
|
|
||||||
try:
|
|
||||||
data = json.loads(raw)
|
|
||||||
except Exception:
|
|
||||||
_log(f"poll_messages: не-JSON ответ {raw[:200]}")
|
|
||||||
return 0
|
|
||||||
msgs = data.get("messages") or []
|
|
||||||
return _ingest_messages(msgs)
|
|
||||||
|
|
||||||
|
|
||||||
def push_agent(to_agent: str, text: str, kind: str = "help") -> bool:
|
|
||||||
"""ВЫХОД: ask_agent(to, text, kind) → очередь другого агента.
|
|
||||||
|
|
||||||
True — портал принял (caller пишет ReplySent); False — не доставлено,
|
|
||||||
ответ останется в инбоксе и повторится на следующем тике.
|
|
||||||
"""
|
|
||||||
to = (to_agent or "").strip()
|
|
||||||
body = (text or "").strip()
|
|
||||||
if not to or not body:
|
|
||||||
return False
|
|
||||||
raw = _rpc_tool("ask_agent", {"to": to, "text": body[:4000], "kind": kind})
|
|
||||||
if raw.startswith("error:"):
|
|
||||||
_log(f"ask_agent({to}): {raw[:200]}")
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
return bool(json.loads(raw).get("ok"))
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def sec_reflex() -> dict:
|
|
||||||
"""Сек-рефлекс: poll очереди агент↔агент. Не чаще MIN_REFLEX_GAP."""
|
|
||||||
now = time.time()
|
|
||||||
if now - _last_reflex["ts"] < MIN_REFLEX_GAP:
|
|
||||||
return {"skipped": "rate-limit"}
|
|
||||||
_last_reflex["ts"] = now
|
|
||||||
out = {"ingested": 0}
|
|
||||||
try:
|
|
||||||
out["ingested"] = pull()
|
|
||||||
except Exception as e:
|
|
||||||
out["error"] = str(e)[:200]
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def write_channel_stamp(name: str = "agent", ok: bool = True, **extra):
|
|
||||||
"""Атомарный штамп канала: harness/channel_agent.json (как portal)."""
|
|
||||||
import datetime
|
|
||||||
try:
|
|
||||||
path = os.path.join(_paths.home(), f"channel_{name}.json")
|
|
||||||
tmp = path + ".tmp"
|
|
||||||
rec = {"ok": ok, "ts": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"), **extra}
|
|
||||||
with open(tmp, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(rec, f, ensure_ascii=False)
|
|
||||||
os.replace(tmp, path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
@ -336,95 +336,6 @@ def append_event(aggregate: str, aggregate_id: str, event_type: str, data: dict)
|
||||||
return (eid, version)
|
return (eid, version)
|
||||||
|
|
||||||
|
|
||||||
def claim_task_if_ready(task_id: str, claim: dict):
|
|
||||||
"""Атомарный условный клейм задачи: TaskClaimed ТОЛЬКО если она сейчас ready.
|
|
||||||
|
|
||||||
t-261015000631-13aef0 (гонка hour/day 2026-10-15 00:00): оба уровня читали
|
|
||||||
read model ДО клейма друг друга и каждый по своему снапшоту писал TaskClaimed
|
|
||||||
→ L-задача исполнялась дважды (двойной LLM-burn 9router). Read-check-write
|
|
||||||
в scheduler таймингом не закрыть: снапшот модели к моменту записи — вчерашний.
|
|
||||||
|
|
||||||
Атомарность гарантирует СТОР, не тайминг: проверка статуса и INSERT — в одной
|
|
||||||
BEGIN IMMEDIATE транзакции. Статус считаем не из read model (она может
|
|
||||||
отставать), а прямой свёрткой хвоста событий агрегата из events — истина
|
|
||||||
на момент транзакции. Побеждает ровно один писатель; проигравший получает
|
|
||||||
False и молча пропускает задачу (её уже ведёт другой уровень).
|
|
||||||
|
|
||||||
Возвращает (event_id, version) при успехе, False — задача не ready
|
|
||||||
(клеймнута/закрыта/не существует), None — дубль записи (интegrity).
|
|
||||||
"""
|
|
||||||
with _LOCK:
|
|
||||||
_ensure_signature_schema()
|
|
||||||
db = _conn()
|
|
||||||
try:
|
|
||||||
db.execute("BEGIN IMMEDIATE")
|
|
||||||
except sqlite3.OperationalError:
|
|
||||||
pass
|
|
||||||
row = db.execute(
|
|
||||||
"SELECT event_type, version FROM events "
|
|
||||||
"WHERE aggregate='task' AND aggregate_id=? ORDER BY version",
|
|
||||||
(task_id,),
|
|
||||||
).fetchall()
|
|
||||||
# свёртка статуса по потоку событий задачи (та же логика, что
|
|
||||||
# _apply_event, но без полного read model — только этот агрегат)
|
|
||||||
status = None
|
|
||||||
for r in row:
|
|
||||||
et = r["event_type"]
|
|
||||||
if et == "TaskSubmitted":
|
|
||||||
status = "ready"
|
|
||||||
elif et == "TaskClaimed":
|
|
||||||
status = "running"
|
|
||||||
elif et in ("TaskCompleted",):
|
|
||||||
status = "done"
|
|
||||||
elif et == "TaskFailed":
|
|
||||||
status = "failed"
|
|
||||||
elif et == "TaskClosed":
|
|
||||||
if status == "failed":
|
|
||||||
status = "closed"
|
|
||||||
elif et in ("TaskPromoted", "TaskRequeued"):
|
|
||||||
status = "ready"
|
|
||||||
elif et == "TaskBlocked":
|
|
||||||
status = "blocked"
|
|
||||||
if status != "ready":
|
|
||||||
db.rollback()
|
|
||||||
db.close()
|
|
||||||
return False
|
|
||||||
version = (row[-1]["version"] if row else 0) + 1
|
|
||||||
ts = datetime.now(timezone.utc).isoformat()
|
|
||||||
data_str = json.dumps(claim, ensure_ascii=False)
|
|
||||||
sig_f = {}
|
|
||||||
try:
|
|
||||||
from . import audit as _audit
|
|
||||||
prev = db.execute(
|
|
||||||
"SELECT sig FROM events ORDER BY id DESC LIMIT 1").fetchone()
|
|
||||||
sig_f = _audit.sign_event_row("task", task_id, "TaskClaimed",
|
|
||||||
data_str, ts, version,
|
|
||||||
prev["sig"] if prev else None)
|
|
||||||
if sig_f.get("key_id"):
|
|
||||||
dom = db.execute(
|
|
||||||
"SELECT key_id FROM events WHERE key_id IS NOT NULL "
|
|
||||||
"ORDER BY id DESC LIMIT 1").fetchone()
|
|
||||||
if dom and dom["key_id"] and dom["key_id"] != sig_f["key_id"]:
|
|
||||||
sig_f = {}
|
|
||||||
except Exception:
|
|
||||||
sig_f = {}
|
|
||||||
try:
|
|
||||||
cur = db.execute(
|
|
||||||
"INSERT INTO events (aggregate,aggregate_id,event_type,data,timestamp,version,"
|
|
||||||
"sig,key_id,prev_sig) VALUES (?,?,?,?,?,?,?,?,?)",
|
|
||||||
("task", task_id, "TaskClaimed", data_str, ts, version,
|
|
||||||
sig_f.get("sig"), sig_f.get("key_id"), sig_f.get("prev_sig")),
|
|
||||||
)
|
|
||||||
db.commit()
|
|
||||||
eid = cur.lastrowid
|
|
||||||
except sqlite3.IntegrityError:
|
|
||||||
db.rollback()
|
|
||||||
db.close()
|
|
||||||
return None
|
|
||||||
db.close()
|
|
||||||
return (eid, version)
|
|
||||||
|
|
||||||
|
|
||||||
def append_tick(level: str, seq: int):
|
def append_tick(level: str, seq: int):
|
||||||
"""CycleTicked идемпотентно: aggregate_id = 'level:seq' → UNIQUE даёт защиту."""
|
"""CycleTicked идемпотентно: aggregate_id = 'level:seq' → UNIQUE даёт защиту."""
|
||||||
return append_event("cycle", f"{level}:{seq}", "CycleTicked", {"level": level, "seq": seq})
|
return append_event("cycle", f"{level}:{seq}", "CycleTicked", {"level": level, "seq": seq})
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,7 @@ LEVELS = {
|
||||||
"(MCP): booking_slots/booking_create — свободные слоты и создание "
|
"(MCP): booking_slots/booking_create — свободные слоты и создание "
|
||||||
"звонка с Алексом (jitsi-ссылка, гостю письмо, запись и транскрипция "
|
"звонка с Алексом (jitsi-ссылка, гостю письмо, запись и транскрипция "
|
||||||
"автоматом), list_calls/get_call — прошедшие звонки и саммари, "
|
"автоматом), list_calls/get_call — прошедшие звонки и саммари, "
|
||||||
"list_members/member_create(email[,name]) — реестр членов и "
|
"list_members/projects_list/list_agents/treasury_status/list_events — "
|
||||||
"создание аккаунта с magic-link (только агентам председателя), "
|
|
||||||
"projects_list/list_agents/treasury_status/list_events — "
|
|
||||||
"реестры, казна, лента. Когда Alex просит назначить/перенести звонок "
|
"реестры, казна, лента. Когда Alex просит назначить/перенести звонок "
|
||||||
"или «посмотреть расписание» — это portal_mcp (до 4 вызовов за тик): "
|
"или «посмотреть расписание» — это portal_mcp (до 4 вызовов за тик): "
|
||||||
"сначала booking_slots (arguments '{\"type\": \"call\"}'), потом "
|
"сначала booking_slots (arguments '{\"type\": \"call\"}'), потом "
|
||||||
|
|
@ -137,9 +135,7 @@ LEVELS = {
|
||||||
"Инструмент portal_mcp — живой портал кооператива (MCP cil.by): "
|
"Инструмент portal_mcp — живой портал кооператива (MCP cil.by): "
|
||||||
"booking_slots/booking_create (звонки с Алексом: слот → создание "
|
"booking_slots/booking_create (звонки с Алексом: слот → создание "
|
||||||
"jitsi-звонка со ссылкой, гостю письмо), list_calls/get_call, "
|
"jitsi-звонка со ссылкой, гостю письмо), list_calls/get_call, "
|
||||||
"list_members/member_create (создание аккаунта члена с magic-link, "
|
"list_members/projects_list/list_agents, treasury_status, list_events. "
|
||||||
"только агентам председателя), projects_list/list_agents, "
|
|
||||||
"treasury_status, list_events. "
|
|
||||||
"Просьбы про назначение/перенос звонков и «что по расписанию» — "
|
"Просьбы про назначение/перенос звонков и «что по расписанию» — "
|
||||||
"через него (до 4 вызовов за тик). НЕ создавай звонки без явной "
|
"через него (до 4 вызовов за тик). НЕ создавай звонки без явной "
|
||||||
"просьбы в чате; деньги-тулы (product_create/product_set_mode) "
|
"просьбы в чате; деньги-тулы (product_create/product_set_mode) "
|
||||||
|
|
|
||||||
|
|
@ -298,27 +298,17 @@ def _apply_tg_command(text: str) -> None:
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
# 1) самотест токена ДО записи: getMe от Telegram
|
# 1) самотест токена ДО записи: getMe от Telegram
|
||||||
# (t-261016141XXX: 3 попытки с паузой — таймаут сети не должен ронять привязку)
|
try:
|
||||||
me = None
|
req = urllib.request.Request(f"https://api.telegram.org/bot{token}/getMe")
|
||||||
_last_err = None
|
with urllib.request.urlopen(req, timeout=10) as r:
|
||||||
for _attempt in range(3):
|
me = json.loads(r.read().decode())
|
||||||
try:
|
if not (me.get("ok") and (me.get("result") or {}).get("username")):
|
||||||
req = urllib.request.Request(f"https://api.telegram.org/bot{token}/getMe")
|
raise RuntimeError(str(me)[:150])
|
||||||
with urllib.request.urlopen(req, timeout=15) as r:
|
uname = me["result"].get("username") or "bot"
|
||||||
me = json.loads(r.read().decode())
|
except Exception as e:
|
||||||
if not (me.get("ok") and (me.get("result") or {}).get("username")):
|
_report(f"❌ TG-токен не принят Telegram (getMe): {e}")
|
||||||
raise RuntimeError(str(me)[:150])
|
print(f"[set-tg] токен не валиден: {e}", flush=True)
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
_last_err = e
|
|
||||||
me = None
|
|
||||||
if _attempt < 2:
|
|
||||||
time.sleep(3 * (_attempt + 1))
|
|
||||||
if me is None:
|
|
||||||
_report(f"❌ TG-токен не принят Telegram (getMe, 3 попытки): {_last_err}")
|
|
||||||
print(f"[set-tg] токен не валиден: {_last_err}", flush=True)
|
|
||||||
return
|
return
|
||||||
uname = me["result"].get("username") or "bot"
|
|
||||||
# 2) аллоулист: из команды (allowed=…), иначе сохранённый в .env
|
# 2) аллоулист: из команды (allowed=…), иначе сохранённый в .env
|
||||||
allowed = payload.get("allowed") or payload.get("owner_tg") or ""
|
allowed = payload.get("allowed") or payload.get("owner_tg") or ""
|
||||||
if not allowed:
|
if not allowed:
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,6 @@ def _known_tools() -> list:
|
||||||
|
|
||||||
def _trace(level, trace_id, payload: dict):
|
def _trace(level, trace_id, payload: dict):
|
||||||
"""Трейс MCP-вызова в стор — spend/observability едины с LLM-тиками."""
|
"""Трейс MCP-вызова в стор — spend/observability едины с LLM-тиками."""
|
||||||
_log_jsonl(level, trace_id, payload)
|
|
||||||
if not (level and trace_id):
|
if not (level and trace_id):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|
@ -134,20 +133,6 @@ def _trace(level, trace_id, payload: dict):
|
||||||
pass # трейс — best effort, не валить тик
|
pass # трейс — best effort, не валить тик
|
||||||
|
|
||||||
|
|
||||||
# t-261015091108: журнал всех MCP-вызовов (кто/что/аргументы/время) — JSONL
|
|
||||||
def _log_jsonl(level, trace_id, payload: dict):
|
|
||||||
import datetime
|
|
||||||
try:
|
|
||||||
path = os.path.join(_paths.home(), "mcp_calls.log")
|
|
||||||
rec = {"ts": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"),
|
|
||||||
"level": level or "unknown", "trace_id": trace_id or "",
|
|
||||||
**payload}
|
|
||||||
with open(path, "a", encoding="utf-8") as f:
|
|
||||||
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
|
||||||
except Exception:
|
|
||||||
pass # журнал — best effort, не валить тик
|
|
||||||
|
|
||||||
|
|
||||||
_trace_log: list = []
|
_trace_log: list = []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -174,18 +159,11 @@ def call_tool(name: str, arguments=None, level: str = None,
|
||||||
"""
|
"""
|
||||||
started = time.time()
|
started = time.time()
|
||||||
ok, result = False, ""
|
ok, result = False, ""
|
||||||
_log_args = {"raw": str(arguments)[:500]} # t-261015091108: аргументы в лог
|
|
||||||
try:
|
|
||||||
args = _coerce_arguments(arguments)
|
|
||||||
_log_args = args
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _finish(res: str, _ok: bool) -> str:
|
def _finish(res: str, _ok: bool) -> str:
|
||||||
_trace(level, trace_id, {
|
_trace(level, trace_id, {
|
||||||
"tool": name, "ok": _ok,
|
"tool": name, "ok": _ok,
|
||||||
"ms": int((time.time() - started) * 1000),
|
"ms": int((time.time() - started) * 1000),
|
||||||
"arguments": _log_args,
|
|
||||||
"result_preview": res[:300],
|
"result_preview": res[:300],
|
||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
|
|
@ -244,8 +222,6 @@ def describe_tools(max_names: int = 12) -> str:
|
||||||
"kind='call'|'session') — создать jitsi-звонок со ссылкой (гостю "
|
"kind='call'|'session') — создать jitsi-звонок со ссылкой (гостю "
|
||||||
"письмо, Алексу — в TG, запись+транскрипция автоматом); booking_cancel"
|
"письмо, Алексу — в TG, запись+транскрипция автоматом); booking_cancel"
|
||||||
"(meeting_id); list_calls/get_call — прошедшие звонки и их саммари; "
|
"(meeting_id); list_calls/get_call — прошедшие звонки и их саммари; "
|
||||||
"list_members/member_create(email[,name]) — реестр членов и создание "
|
"list_members/projects_list/list_agents/treasury_status/list_events — "
|
||||||
"аккаунта с magic-link письмом (member_create — только агентам "
|
|
||||||
"председателя); projects_list/list_agents/treasury_status/list_events — "
|
|
||||||
"реестры и казна; whoami/report_task/report_event/read_context — "
|
"реестры и казна; whoami/report_task/report_event/read_context — "
|
||||||
"идентичность и лента портала.")
|
"идентичность и лента портала.")
|
||||||
|
|
|
||||||
|
|
@ -77,20 +77,12 @@ def setup_provider():
|
||||||
os.environ["OPENAI_BASE_URL"] = _router_base_url()
|
os.environ["OPENAI_BASE_URL"] = _router_base_url()
|
||||||
|
|
||||||
|
|
||||||
def _wire_trace_hooks(agent, es, trace_id: str, model: str = None, level: str = None):
|
def _wire_trace_hooks(agent, es, trace_id: str, model: str = None):
|
||||||
"""Каждая стадия агентного цикла → событие агрегата trace.
|
"""Каждая стадия агентного цикла → событие агрегата trace.
|
||||||
|
|
||||||
model (e-260930062120): model_id уровня в InvocationStarted —
|
model (e-260930062120): model_id уровня в InvocationStarted —
|
||||||
видно, КАКАЯ модель запускалась на тике (запрошенная; "auto"
|
видно, КАКАЯ модель запускалась на тике (запрошенная; "auto"
|
||||||
резолвится роутером на его стороне).
|
резолвится роутером на его стороне).
|
||||||
|
|
||||||
t-261015083454 (аудит мета-информации в событиях): В КАЖДОМ
|
|
||||||
trace-событии теперь trace_id + ts + level (ToolCalled/ToolResult
|
|
||||||
раньше несли только tool/input — 68k событий без связи с тиком),
|
|
||||||
ToolResult дополнительно duration + ok/exception (из хука strands),
|
|
||||||
имя инструмента в ToolResult — из tool_use, а не заглушка "?".
|
|
||||||
Уровень недоступен (legacy-вызов) — поле не пишем, aggregate_id
|
|
||||||
по-прежнему "<level>-<seq>:<n>".
|
|
||||||
"""
|
"""
|
||||||
from strands import Agent
|
from strands import Agent
|
||||||
from strands.hooks import BeforeInvocationEvent, BeforeToolCallEvent, AfterToolCallEvent
|
from strands.hooks import BeforeInvocationEvent, BeforeToolCallEvent, AfterToolCallEvent
|
||||||
|
|
@ -101,42 +93,25 @@ def _wire_trace_hooks(agent, es, trace_id: str, model: str = None, level: str =
|
||||||
seq["n"] += 1
|
seq["n"] += 1
|
||||||
es.append_event("trace", f"{trace_id}:{seq['n']}", kind, payload)
|
es.append_event("trace", f"{trace_id}:{seq['n']}", kind, payload)
|
||||||
|
|
||||||
def _meta(extra: dict = None) -> dict:
|
|
||||||
base = {"trace_id": trace_id, "ts": _now()}
|
|
||||||
if level:
|
|
||||||
base["level"] = level
|
|
||||||
if extra:
|
|
||||||
base.update(extra)
|
|
||||||
return base
|
|
||||||
|
|
||||||
def on_before_invocation(event):
|
def on_before_invocation(event):
|
||||||
data = _meta()
|
data = {"trace_id": trace_id, "ts": _now()}
|
||||||
if model:
|
if model:
|
||||||
data["model"] = model
|
data["model"] = model
|
||||||
_trace("InvocationStarted", data)
|
_trace("InvocationStarted", data)
|
||||||
|
|
||||||
def on_before_tool(event):
|
def on_before_tool(event):
|
||||||
tool = getattr(event, "tool_use", None) or {}
|
tool = getattr(event, "tool_use", None) or {}
|
||||||
_trace("ToolCalled", _meta({
|
_trace("ToolCalled", {
|
||||||
"tool": tool.get("name", "?"),
|
"tool": tool.get("name", "?"),
|
||||||
"input": json.dumps(tool.get("input", {}), ensure_ascii=False, default=str)[:4000],
|
"input": json.dumps(tool.get("input", {}), ensure_ascii=False, default=str)[:4000],
|
||||||
}))
|
})
|
||||||
|
|
||||||
def on_after_tool(event):
|
def on_after_tool(event):
|
||||||
tool = getattr(event, "tool_use", None) or {}
|
|
||||||
try:
|
try:
|
||||||
res = str(getattr(event, "result", ""))[:4000]
|
res = str(getattr(event, "result", ""))[:4000]
|
||||||
except Exception:
|
except Exception:
|
||||||
res = "<unreadable>"
|
res = "<unreadable>"
|
||||||
extra = {"tool": tool.get("name", "?"), "result": res}
|
_trace("ToolResult", {"tool": "?", "result": res})
|
||||||
duration = getattr(event, "duration", None)
|
|
||||||
if duration is not None:
|
|
||||||
extra["duration_ms"] = round(float(duration) * 1000)
|
|
||||||
exc = getattr(event, "exception", None)
|
|
||||||
extra["ok"] = exc is None
|
|
||||||
if exc is not None:
|
|
||||||
extra["exception"] = str(exc)[:500]
|
|
||||||
_trace("ToolResult", _meta(extra))
|
|
||||||
|
|
||||||
agent.add_hook(on_before_invocation, BeforeInvocationEvent)
|
agent.add_hook(on_before_invocation, BeforeInvocationEvent)
|
||||||
agent.add_hook(on_before_tool, BeforeToolCallEvent)
|
agent.add_hook(on_before_tool, BeforeToolCallEvent)
|
||||||
|
|
|
||||||
|
|
@ -380,7 +380,6 @@ def build_context(level_cfg, model: dict, batch=None):
|
||||||
if m.get("reply_to_message_id") is not None else ""))
|
if m.get("reply_to_message_id") is not None else ""))
|
||||||
+ ": " + m.get('text','')[:1200])
|
+ ": " + m.get('text','')[:1200])
|
||||||
batch_ids = ",".join(ids)
|
batch_ids = ",".join(ids)
|
||||||
chat_ids = {m.get("chat_id") for m in inbox if m.get("chat_id") is not None}
|
|
||||||
# t-261013131228: если в батче есть reply на сообщение без текста —
|
# t-261013131228: если в батче есть reply на сообщение без текста —
|
||||||
# добавляем последние документы этого чата с caption, чтобы триаж
|
# добавляем последние документы этого чата с caption, чтобы триаж
|
||||||
# видел, о каком документе речь, даже если «голова» старше капа инбокса
|
# видел, о каком документе речь, даже если «голова» старше капа инбокса
|
||||||
|
|
@ -396,29 +395,6 @@ def build_context(level_cfg, model: dict, batch=None):
|
||||||
+ "; ".join(docs))
|
+ "; ".join(docs))
|
||||||
if docs_ctx:
|
if docs_ctx:
|
||||||
parts.append("КОНТЕКСТ ДОКУМЕНТОВ ЧАТА:\n" + "\n".join(docs_ctx.values()))
|
parts.append("КОНТЕКСТ ДОКУМЕНТОВ ЧАТА:\n" + "\n".join(docs_ctx.values()))
|
||||||
# t-261015222515: память разговора — рядом с батчем неотвеченных лежит
|
|
||||||
# нить недавнего диалога (отвеченные реплики + наши ответы) тех же
|
|
||||||
# чатов. Раньше после ReplySent входящее выпадало из контекста тика —
|
|
||||||
# минутный уровень отвечал без памяти о разговоре (Alex про SPA-договор:
|
|
||||||
# «только что же обсуждали»). Источник — та же проекция инбокса
|
|
||||||
# (reply_text из ReplySent), отдельный стор не нужен.
|
|
||||||
replied_hist = [m for m in (model.get("inbox") or {}).values()
|
|
||||||
if m.get("replied") and not m.get("ignored")
|
|
||||||
and m.get("chat_id") in chat_ids
|
|
||||||
and (m.get("text") or "").strip()]
|
|
||||||
if replied_hist:
|
|
||||||
replied_hist = sorted(replied_hist, key=lambda x: x.get("ts") or "")[-12:]
|
|
||||||
hist_lines = []
|
|
||||||
for m in replied_hist:
|
|
||||||
m_id = "m-" + str(m.get("update_id"))
|
|
||||||
who = m.get("username") or m.get("first_name") or m.get("user_id")
|
|
||||||
line = f"- [{m_id}] {who}: {(m.get('text') or '')[:600]}"
|
|
||||||
rt = (m.get("reply_text") or "").strip()
|
|
||||||
if rt:
|
|
||||||
line += f"\n └ наш ответ: {rt[:600]}"
|
|
||||||
hist_lines.append(line)
|
|
||||||
parts.append("НЕДАВНИЙ РАЗГОВОР (уже отвечено; это память тика — НЕ отвечай "
|
|
||||||
"на них снова, используй как контекст):\n" + "\n".join(hist_lines))
|
|
||||||
# t-260930064924: батч-триаж — ВСЕ неотвеченные обрабатываются ОДНИМ тиком.
|
# t-260930064924: батч-триаж — ВСЕ неотвеченные обрабатываются ОДНИМ тиком.
|
||||||
# Раньше мин-уровень отвечал по одному за тик (m-516173222..228 — 7 тиков
|
# Раньше мин-уровень отвечал по одному за тик (m-516173222..228 — 7 тиков
|
||||||
# подряд на один разговор). Теперь в контексте лежит ГОТОВАЯ команда
|
# подряд на один разговор). Теперь в контексте лежит ГОТОВАЯ команда
|
||||||
|
|
@ -546,33 +522,16 @@ def run_level_tick(level_cfg, seq: int, model: dict):
|
||||||
batch = [model["tasks"].get(tid) for tid in batch_ids]
|
batch = [model["tasks"].get(tid) for tid in batch_ids]
|
||||||
batch = [t for t in batch if t]
|
batch = [t for t in batch if t]
|
||||||
claimed = []
|
claimed = []
|
||||||
stale = [] # заняты другим уровнем после снапшота модели (гонка клейма)
|
|
||||||
for t in batch:
|
for t in batch:
|
||||||
# t-261015000631-13aef0: атомарный условный клейм НА УРОВНЕ СТОРА
|
es.append_event("task", t["id"], "TaskClaimed", {
|
||||||
# (проверка статуса + INSERT в одной BEGIN IMMEDIATE транзакции).
|
|
||||||
# Гонка 2026-10-15 00:00: hour и day читали модель ДО клейма друг
|
|
||||||
# друга → L-задача t-261014221908 клеймнута дважды (TaskClaimed
|
|
||||||
# id=2767823/2767828) и исполнена дважды = двойной burn 9router.
|
|
||||||
# Снапшот модели здесь — вчерашний; истина только в сторе.
|
|
||||||
res = es.claim_task_if_ready(t["id"], {
|
|
||||||
"level": level_cfg.name, "session_id": f"s-{level_cfg.name}-{seq}",
|
"level": level_cfg.name, "session_id": f"s-{level_cfg.name}-{seq}",
|
||||||
})
|
})
|
||||||
if not res:
|
|
||||||
# False — уже не ready (взята/закрыта), None — конфликт записи:
|
|
||||||
# события нет, клейма нет → пропускаем, задача не наша.
|
|
||||||
stale.append(t["id"])
|
|
||||||
continue
|
|
||||||
claimed.append(t["id"])
|
claimed.append(t["id"])
|
||||||
# TG-visibility: взяли задачу — сообщаем в чат (анти-спам: не чаще 15м на уровень)
|
# TG-visibility: взяли задачу — сообщаем в чат (анти-спам: не чаще 15м на уровень)
|
||||||
try:
|
try:
|
||||||
_notify_busy(level_cfg, t)
|
_notify_busy(level_cfg, t)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # уведомление — best effort, не валить тик
|
pass # уведомление — best effort, не валить тик
|
||||||
if stale:
|
|
||||||
_log("claim-race: пропущены задачи, занятые другим уровнем/не ready: "
|
|
||||||
+ ", ".join(stale))
|
|
||||||
# в промпт — только задачи, чей клейм реально наш
|
|
||||||
batch = [t for t in batch if t["id"] in set(claimed)]
|
|
||||||
if not claimed:
|
if not claimed:
|
||||||
claimed = None
|
claimed = None
|
||||||
prompt = build_context(level_cfg, model, batch)
|
prompt = build_context(level_cfg, model, batch)
|
||||||
|
|
@ -648,8 +607,6 @@ def run_level_tick(level_cfg, seq: int, model: dict):
|
||||||
if not _task_still_active(tid):
|
if not _task_still_active(tid):
|
||||||
continue
|
continue
|
||||||
es.append_event("task", tid, "TaskFailed", {
|
es.append_event("task", tid, "TaskFailed", {
|
||||||
# t-261015083454: мета — уровень/тик-воркер и его трасса
|
|
||||||
"level": level_cfg.name, "trace_id": trace_id,
|
|
||||||
"error": str(usage.get("error", "llm worker failed"))[:500],
|
"error": str(usage.get("error", "llm worker failed"))[:500],
|
||||||
})
|
})
|
||||||
raise RuntimeError(str(usage.get("error", "llm worker failed")))
|
raise RuntimeError(str(usage.get("error", "llm worker failed")))
|
||||||
|
|
@ -702,8 +659,6 @@ def run_level_tick(level_cfg, seq: int, model: dict):
|
||||||
_log("skip батч-догзакрытие задач, решённых воркером: " + ", ".join(_skipped))
|
_log("skip батч-догзакрытие задач, решённых воркером: " + ", ".join(_skipped))
|
||||||
for tid in _resolve:
|
for tid in _resolve:
|
||||||
es.append_event("task", tid, "TaskCompleted", {
|
es.append_event("task", tid, "TaskCompleted", {
|
||||||
# t-261015083454: мета-поля — какой тик/уровень закрыл задачу
|
|
||||||
"level": level_cfg.name, "trace_id": trace_id,
|
|
||||||
"result_ref": trace_id, "answer": (text or "")[:2000],
|
"result_ref": trace_id, "answer": (text or "")[:2000],
|
||||||
})
|
})
|
||||||
try:
|
try:
|
||||||
|
|
@ -992,19 +947,6 @@ def _reflex_tick(level_cfg, seq: int, model: dict):
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[portal-channel] рефлекс не смог: {e}", flush=True)
|
print(f"[portal-channel] рефлекс не смог: {e}", flush=True)
|
||||||
# канал агент↔агент (t-261015090129): очередь ask_agent/poll_messages
|
|
||||||
# MCP-шлюза. Лучший эффект — на мин-уровне: сообщения других агентов
|
|
||||||
# попадают в инбокс триажа как обычные входящие.
|
|
||||||
try:
|
|
||||||
from . import agent_channel as _ach
|
|
||||||
res = _ach.sec_reflex()
|
|
||||||
if not res.get("skipped") and res.get("ingested"):
|
|
||||||
payload["agent_channel"] = res
|
|
||||||
_log(f"agent-channel: {res}")
|
|
||||||
except SystemExit:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[agent-channel] рефлекс не смог: {e}", flush=True)
|
|
||||||
es.append_event("cycle", f"reflex:{seq}", "ReflexObserved", payload)
|
es.append_event("cycle", f"reflex:{seq}", "ReflexObserved", payload)
|
||||||
return payload, None
|
return payload, None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -346,18 +346,6 @@ def receive_message(update: dict):
|
||||||
target = _resolve_reply_target(chat.get("id"), rt)
|
target = _resolve_reply_target(chat.get("id"), rt)
|
||||||
if target:
|
if target:
|
||||||
data["reply_to_desc"] = target
|
data["reply_to_desc"] = target
|
||||||
# t-261015080211: явный author_* блок + received_at — агентам проще
|
|
||||||
# читать метаданные автора одним полем, не собирая по отдельным ключам
|
|
||||||
data["author"] = {
|
|
||||||
"user_id": data.get("user_id"),
|
|
||||||
"username": data.get("username"),
|
|
||||||
"first_name": data.get("first_name"),
|
|
||||||
"chat_id": data.get("chat_id"),
|
|
||||||
"chat_type": data.get("chat_type"),
|
|
||||||
"chat_title": data.get("chat_title"),
|
|
||||||
"date": data.get("date"),
|
|
||||||
}
|
|
||||||
data["received_at"] = data.get("date")
|
|
||||||
es.append_event("inbox", f"m-{update['update_id']}", "MessageReceived", data)
|
es.append_event("inbox", f"m-{update['update_id']}", "MessageReceived", data)
|
||||||
return f"m-{update['update_id']}"
|
return f"m-{update['update_id']}"
|
||||||
|
|
||||||
|
|
@ -380,20 +368,6 @@ def send_reply(msg_id: str, text: str, timeout: int = 35):
|
||||||
# HTTP-push'ем на портал — БЕЗ Telegram. У агента может не быть TG-бота
|
# HTTP-push'ем на портал — БЕЗ Telegram. У агента может не быть TG-бота
|
||||||
# (simbiot-деплой делает TG опциональным), а портал — не телеграм-юзер:
|
# (simbiot-деплой делает TG опциональным), а портал — не телеграм-юзер:
|
||||||
# разговоры не смешиваются (по chat_id), доставка — отдельный транспорт.
|
# разговоры не смешиваются (по chat_id), доставка — отдельный транспорт.
|
||||||
if str(m.get("chat_id") or "").startswith("portal:agent:"):
|
|
||||||
# канал агент↔агент (t-261015090129): ответ уходит ask_agent'ом
|
|
||||||
# в очередь того агента, чьё сообщение триажируем. Транспорт — MCP.
|
|
||||||
from . import agent_channel
|
|
||||||
to_agent = str(m.get("chat_id")).split(":", 2)[-1]
|
|
||||||
kind = {"help": "help", "review": "review", "handoff": "handoff"}.get(
|
|
||||||
m.get("agent_kind"), "help")
|
|
||||||
ok = agent_channel.push_agent(to_agent, text, kind)
|
|
||||||
if not ok:
|
|
||||||
raise RuntimeError(f"ask_agent({to_agent}) не доставил — ответят на следующем тике")
|
|
||||||
es.append_event("inbox", msg_id, "ReplySent", {
|
|
||||||
"to_chat": m["chat_id"], "text": text[:2000], "channel": "agent_mcp",
|
|
||||||
})
|
|
||||||
return msg_id
|
|
||||||
if str(m.get("chat_id") or "").startswith("portal"):
|
if str(m.get("chat_id") or "").startswith("portal"):
|
||||||
from . import portal_channel
|
from . import portal_channel
|
||||||
ok = portal_channel.push(m["chat_id"], text, [m.get("update_id") or msg_id])
|
ok = portal_channel.push(m["chat_id"], text, [m.get("update_id") or msg_id])
|
||||||
|
|
@ -465,32 +439,6 @@ def send_batch_reply(msg_ids: list, text: str, timeout: int = 35):
|
||||||
by_chat = {}
|
by_chat = {}
|
||||||
for mid, m in pending:
|
for mid, m in pending:
|
||||||
by_chat.setdefault(m["chat_id"], []).append((mid, m))
|
by_chat.setdefault(m["chat_id"], []).append((mid, m))
|
||||||
# ── канал агент↔агент (t-261015090129): батч сообщений других агентов —
|
|
||||||
# сводный ответ одним ask_agent каждому адресату. Chat_id вида
|
|
||||||
# portal:agent:<имя>; портальный UI-чат ('portal:<имя>' без agent:) —
|
|
||||||
# отдельная ветка ниже.
|
|
||||||
agent_chats = [c for c in by_chat if str(c).startswith("portal:agent:")]
|
|
||||||
if agent_chats:
|
|
||||||
if len(by_chat) != len(agent_chats):
|
|
||||||
raise ValueError("батч смешивает агент-канал и другие каналы — раздели ответы")
|
|
||||||
from . import agent_channel
|
|
||||||
for chat_id in agent_chats:
|
|
||||||
to_agent = str(chat_id).split(":", 2)[-1]
|
|
||||||
m0 = by_chat[chat_id][0][1]
|
|
||||||
kind = {"help": "help", "review": "review", "handoff": "handoff"}.get(
|
|
||||||
m0.get("agent_kind"), "help")
|
|
||||||
ok = agent_channel.push_agent(to_agent, text, kind)
|
|
||||||
if not ok:
|
|
||||||
raise RuntimeError(f"ask_agent({to_agent}) не доставил — попробуем на следующем тике")
|
|
||||||
for mid, m in by_chat[chat_id]:
|
|
||||||
es.append_event("inbox", mid, "ReplySent", {
|
|
||||||
"to_chat": chat_id,
|
|
||||||
"text": (text if mid == head_mid
|
|
||||||
else f"(сводный ответ батча {batch_id} — см. {head_mid})")[:2000],
|
|
||||||
"channel": "agent_mcp",
|
|
||||||
"batch": {"id": batch_id, "size": len(pending), "head": head_mid},
|
|
||||||
})
|
|
||||||
return head_mid
|
|
||||||
# ── файловый портал-канал (t-261001123121): батч целиком из чата 'portal:*'
|
# ── файловый портал-канал (t-261001123121): батч целиком из чата 'portal:*'
|
||||||
# доставляется HTTP-push'ем на портал, без Telegram. Смешивать чаты
|
# доставляется HTTP-push'ем на портал, без Telegram. Смешивать чаты
|
||||||
# нельзя — один сводный текст на один транспорт.
|
# нельзя — один сводный текст на один транспорт.
|
||||||
|
|
@ -577,18 +525,8 @@ def poll_loop():
|
||||||
user = (msg.get("from") or {}).get("id")
|
user = (msg.get("from") or {}).get("id")
|
||||||
if ALLOWED and str(user) not in ALLOWED:
|
if ALLOWED and str(user) not in ALLOWED:
|
||||||
# чужой: пишем событие, но не отвечаем (потенциально спам)
|
# чужой: пишем событие, но не отвечаем (потенциально спам)
|
||||||
# t-261015080211: метаданные автора и в игнор-событии
|
|
||||||
# (кто писал, чат, время) — не только в MessageReceived
|
|
||||||
_ig_chat = msg.get("chat") or {}
|
|
||||||
es.append_event("inbox", f"m-{u['update_id']}", "MessageIgnored", {
|
es.append_event("inbox", f"m-{u['update_id']}", "MessageIgnored", {
|
||||||
"user_id": user,
|
"user_id": user, "username": (msg.get("from") or {}).get("username"),
|
||||||
"username": (msg.get("from") or {}).get("username"),
|
|
||||||
"first_name": (msg.get("from") or {}).get("first_name"),
|
|
||||||
"chat_id": _ig_chat.get("id"),
|
|
||||||
"chat_type": _ig_chat.get("type"),
|
|
||||||
"chat_title": _ig_chat.get("title"),
|
|
||||||
"date": msg.get("date"),
|
|
||||||
"message_id": msg.get("message_id"),
|
|
||||||
"reason": "not_allowed", "text": (msg.get("text") or "")[:500],
|
"reason": "not_allowed", "text": (msg.get("text") or "")[:500],
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
# ИИШНИЦА · анонс — 3 текстовых варианта
|
|
||||||
|
|
||||||
Панельная сессия «Общее видение» · 45 мин · 3 спикера: Алекс Шкор, Влад Шемардинов, Юрий Бокач · Минск · cil.by
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Вариант 1 — «Круги» (структурный, как было, ужато)
|
|
||||||
|
|
||||||
🎤 ПАНЕЛЬНАЯ СЕССИЯ «ОБЩЕЕ ВИДЕНИЕ» — на ИИШНИЦЕ
|
|
||||||
|
|
||||||
45 минут · 3 участника · один результат — название общего видения будущего.
|
|
||||||
|
|
||||||
Не панель с вопросами модератора. Три круга:
|
|
||||||
1️⃣ «Моё будущее» — 4 минуты каждому без перебиваний
|
|
||||||
2️⃣ «Что понравилось» — где картины будущего пересекаются
|
|
||||||
3️⃣ «Название общего видения» — панель и зал вместе рождают одно название. Оно остаётся после сессии: заголовки, фото, цитаты.
|
|
||||||
|
|
||||||
На сцене: Алекс Шкор (модератор; кооператив 42, Simbiot (simbiot.cil.by)) · Влад Шемардинов (Avatar — avatar.cil.by, видеопродакшн) · Юрий Бокач (монетикс, финансовая архитектура; vybirai.cil.by).
|
|
||||||
|
|
||||||
Миссия 42: все миссии одинаково важны.
|
|
||||||
|
|
||||||
📍 Минск, дата — в следующих постах · 🌐 cil.by
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Вариант 2 — «Провокация» (короткий, ударный)
|
|
||||||
|
|
||||||
Три человека видят будущее по-разному. Через 45 минут у них будет одно общее название для него. 🎤
|
|
||||||
|
|
||||||
ИИШНИЦА · панельная сессия «Общее видение».
|
|
||||||
Алекс Шкор · Влад Шемардинов · Юрий Бокач.
|
|
||||||
Никаких вопросов из зала «модератору». Каждый — 4 минуты своего будущего, потом — точки пересечения, потом — зал и панель вместе рождают одно название. Оно останется после сессии.
|
|
||||||
|
|
||||||
Приходи: твоё видение будущего — не менее важно, чем наше.
|
|
||||||
|
|
||||||
📍 Минск · 🌐 cil.by
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Вариант 3 — «Интимный» (тёплый, личный)
|
|
||||||
|
|
||||||
Что ты ответишь, если дать тебе 4 минуты на вопрос «каким ты видишь будущее своей индустрии» — и запретить перебивать?
|
|
||||||
|
|
||||||
На ИИШНИЦЕ мы сделаем это втроём на сцене: Алекс (AI-инфраструктура), Влад (видео и контент), Юра (монетикс и финансы). Три честных картины будущего. Потом — где они совпадают. А в конце панель и зал вместе придумают одно название общему видению — и оно останется с нами после сессии.
|
|
||||||
|
|
||||||
Миссия кооператива 42: все миссии одинаково важны. Твоя — тоже.
|
|
||||||
|
|
||||||
📍 Минск · дата в следующих постах · 🌐 cil.by
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
# ИИШНИЦА · анонс — 3 текстовых варианта
|
|
||||||
|
|
||||||
Панельная сессия «Общее видение» · 45 мин · 3 спикера: Алекс Шкор, Влад Шемардинов, Юрий Бокач · Минск · cil.by
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Вариант 1 — «Круги» (структурный, как было, ужато)
|
|
||||||
|
|
||||||
🎤 ПАНЕЛЬНАЯ СЕССИЯ «ОБЩЕЕ ВИДЕНИЕ» — на ИИШНИЦЕ
|
|
||||||
|
|
||||||
45 минут · 3 участника · один результат — название общего видения будущего.
|
|
||||||
|
|
||||||
Не панель с вопросами модератора. Три круга:
|
|
||||||
1️⃣ «Моё будущее» — 4 минуты каждому без перебиваний
|
|
||||||
2️⃣ «Что понравилось» — где картины будущего пересекаются
|
|
||||||
3️⃣ «Название общего видения» — панель и зал вместе рождают одно название. Оно остаётся после сессии: заголовки, фото, цитаты.
|
|
||||||
|
|
||||||
На сцене: Алекс Шкор (модератор; кооператив 42, Simbiot (simbiot.cil.by)) · Влад Шемардинов (Avatar — avatar.cil.by, видеопродакшн) · Юрий Бокач (монетикс, финансовая архитектура; vybirai.cil.by).
|
|
||||||
|
|
||||||
Миссия 42: все миссии одинаково важны.
|
|
||||||
|
|
||||||
📍 Минск, дата — в следующих постах · 🌐 cil.by
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Вариант 2 — «Провокация» (короткий, ударный)
|
|
||||||
|
|
||||||
Три человека видят будущее по-разному. Через 45 минут у них будет одно общее название для него. 🎤
|
|
||||||
|
|
||||||
ИИШНИЦА · панельная сессия «Общее видение».
|
|
||||||
Алекс Шкор · Влад Шемардинов · Юрий Бокач.
|
|
||||||
Никаких вопросов из зала «модератору». Каждый — 4 минуты своего будущего, потом — точки пересечения, потом — зал и панель вместе рождают одно название. Оно останется после сессии.
|
|
||||||
|
|
||||||
Приходи: твоё видение будущего — не менее важно, чем наше.
|
|
||||||
|
|
||||||
📍 Минск · 🌐 cil.by
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Вариант 3 — «Интимный» (тёплый, личный)
|
|
||||||
|
|
||||||
Что ты ответишь, если дать тебе 4 минуты на вопрос «каким ты видишь будущее своей индустрии» — и запретить перебивать?
|
|
||||||
|
|
||||||
На ИИШНИЦЕ мы сделаем это втроём на сцене: Алекс (AI-инфраструктура), Влад (видео и контент), Юра (монетикс и финансы). Три честных картины будущего. Потом — где они совпадают. А в конце панель и зал вместе придумают одно название общему видению — и оно останется с нами после сессии.
|
|
||||||
|
|
||||||
Миссия кооператива 42: все миссии одинаково важны. Твоя — тоже.
|
|
||||||
|
|
||||||
📍 Минск · дата в следующих постах · 🌐 cil.by
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
<!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:#161a2b; --accent:#3b5bdb; --soft:#f4f5fb; --line:#e3e6f2; }
|
|
||||||
* { box-sizing:border-box; }
|
|
||||||
body { margin:0; font-family:-apple-system,'Segoe UI',Roboto,Arial,sans-serif; color:var(--ink); background:#fbfbfe; line-height:1.6; }
|
|
||||||
.wrap { max-width:760px; margin:0 auto; padding:32px 20px 56px; }
|
|
||||||
.badge { display:inline-block; background:var(--accent); color:#fff; border-radius:999px; padding:4px 14px; font-size:13px; letter-spacing:.5px; }
|
|
||||||
h1 { font-size:30px; line-height:1.25; margin:14px 0 4px; }
|
|
||||||
.sub { color:#5b6178; margin:0 0 28px; font-size:15px; }
|
|
||||||
h2 { font-size:19px; margin:32px 0 10px; padding-top:18px; border-top:1px solid var(--line); }
|
|
||||||
table { width:100%; border-collapse:collapse; font-size:14px; }
|
|
||||||
th, td { text-align:left; padding:8px 10px; border-bottom:1px solid var(--line); vertical-align:top; }
|
|
||||||
th { background:var(--soft); }
|
|
||||||
td:first-child { white-space:nowrap; color:var(--accent); font-weight:600; }
|
|
||||||
ul { padding-left:20px; }
|
|
||||||
li { margin:6px 0; }
|
|
||||||
.speaker { background:var(--soft); border:1px solid var(--line); border-radius:12px; padding:14px 16px; margin:10px 0; }
|
|
||||||
.speaker b { display:block; margin-bottom:2px; }
|
|
||||||
.speaker span { font-size:14px; color:#3c4258; }
|
|
||||||
.cta { background:var(--ink); color:#fff; border-radius:14px; padding:18px 20px; margin-top:28px; font-size:15px; }
|
|
||||||
footer { margin-top:28px; color:#8a90a8; font-size:13px; text-align:center; }
|
|
||||||
em { color:var(--accent); font-style:normal; font-weight:600; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="wrap">
|
|
||||||
<span class="badge">Анонс для main-комьюнити</span>
|
|
||||||
<h1>ИИШНИЦА · Панельная сессия «Общее видение»</h1>
|
|
||||||
<p class="sub">Панель в три круга, которая заканчивается артефактом</p>
|
|
||||||
|
|
||||||
<h2>🎯 Что будет</h2>
|
|
||||||
<p>Панельная сессия в три круга — не классическая панель с вопросами модератора, а разговор, который заканчивается артефактом: <em>одним названием общего видения будущего</em>.</p>
|
|
||||||
<p>Формат держит миссия кооператива «42»: <em>все миссии одинаково важны</em> — каждый взгляд на будущее важен, любая индустрия равна.</p>
|
|
||||||
|
|
||||||
<h2>⏱ Формат и тайминг (45 минут, трое участников)</h2>
|
|
||||||
<table>
|
|
||||||
<tr><th>Время</th><th>Круг</th><th>Что происходит</th></tr>
|
|
||||||
<tr><td>00:00–00:03</td><td>Открытие</td><td>Модератор озвучивает миссию 42 как рамку сессии</td></tr>
|
|
||||||
<tr><td>00:03–00:15</td><td>Круг 1 — «Моё будущее»</td><td>Каждый участник по 4 минуты описывает будущее своей индустрии в самом дальнем горизонте, в который может заглянуть. Без вопросов, без перебиваний</td></tr>
|
|
||||||
<tr><td>00:15–00:21</td><td>Круг 2 — «Что понравилось»</td><td>По 2 минуты: каждый подсвечивает, что понравилось у других — не спорить, а связывать: где пересечение со своим будущим</td></tr>
|
|
||||||
<tr><td>00:21–00:30</td><td>Круг 3 — «Название общего видения»</td><td>Панель соглашается назвать общее видение — одно название для всех картин будущего</td></tr>
|
|
||||||
<tr><td>00:30–00:43</td><td>Вопросы из зала</td><td>Зал — четвёртый участник. Любой вопрос важен</td></tr>
|
|
||||||
<tr><td>00:43–00:45</td><td>Закрытие</td><td>Название общего видения + миссия 42, короткое резюме</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h2>🎤 Участники</h2>
|
|
||||||
<div class="speaker"><b>Алекс Шкор</b><span>Модератор и участник. Председатель кооператива «Друзья Коллективного Разума», архитектор кооператива, <a href="https://simbiot.cil.by">Simbiot — simbiot.cil.by</a>. AI-инфраструктура и стратегия.</span></div>
|
|
||||||
<div class="speaker"><b>Влад Шемардинов</b><span>Участник. Член правления. Видеопродакшн и контент, Avatar (<a href="https://avatar.cil.by">avatar.cil.by</a>): подкасты и видео превращаются в вирусные рилсы.</span></div>
|
|
||||||
<div class="speaker"><b>Юрий Бокач</b><span>Участник. Секретарь, монетикс, финансовая архитектура, документооборот. Залоговые системы и аналитика. Проект: <a href="https://vybirai.cil.by">vybirai.cil.by</a></span></div>
|
|
||||||
|
|
||||||
<h2>💡 Почему это будет интересно</h2>
|
|
||||||
<ul>
|
|
||||||
<li><b>Круг 1 без перебиваний</b> — 4 минуты каждому без вопросов: редкость на панелях. Глубина вместо заготовок.</li>
|
|
||||||
<li><b>Круг 2 связывает, а не спорит</b> — «что понравилось» вместо «в чём не согласен»: панель выглядит единой, зал видит созвучие индустрий.</li>
|
|
||||||
<li><b>Круг 3 — артефакт</b> — название общего видения остаётся после сессии: в заголовках, фото и цитатах. Панель производит смысл, а не обмен мнениями.</li>
|
|
||||||
<li><b>Миссия 42 как рамка</b> — «все миссии одинаково важны» легитимирует любой ответ, спикерам безопасно говорить смело.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>📍 Где и когда</h2>
|
|
||||||
<p>Минск · cil.by — точная дата и площадка будут в следующих постах, следи за анонсами в канале.</p>
|
|
||||||
|
|
||||||
<div class="cta">Приходи: приведи своё видение будущего — оно не менее важно, чем наше.</div>
|
|
||||||
|
|
||||||
<footer>Кооператив «Друзья Коллективного Разума» · Минск · cil.by</footer>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -49,9 +49,9 @@
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2>🎤 Участники</h2>
|
<h2>🎤 Участники</h2>
|
||||||
<div class="speaker"><b>Алекс Шкор</b><span>Модератор и участник. Председатель кооператива «Друзья Коллективного Разума», архитектор кооператива, <a href="https://simbiot.cil.by">Simbiot — simbiot.cil.by</a>. AI-инфраструктура и стратегия.</span></div>
|
<div class="speaker"><b>Алекс Шкор</b><span>Модератор и участник. Председатель кооператива «Друзья Коллективного Разума», архитектор кооператива (Симбиот, Кибербанк, Domostroy). AI-инфраструктура и стратегия.</span></div>
|
||||||
<div class="speaker"><b>Влад Шемардинов</b><span>Участник. Член правления. Видеопродакшн и контент, Avatar (<a href="https://avatar.cil.by">avatar.cil.by</a>): подкасты и видео превращаются в вирусные рилсы.</span></div>
|
<div class="speaker"><b>Влад Шемардинов</b><span>Участник. Член правления. Видеопродакшн и контент, ReelsUp: подкасты и видео превращаются в вирусные рилсы.</span></div>
|
||||||
<div class="speaker"><b>Юрий Бокач</b><span>Участник. Секретарь, монетикс, финансовая архитектура, документооборот. Залоговые системы и аналитика. Проект: <a href="https://vybirai.cil.by">vybirai.cil.by</a></span></div>
|
<div class="speaker"><b>Юрий Бокач</b><span>Участник. Секретарь, монетикс, финансовая архитектура, документооборот. Залоговые системы и аналитика.</span></div>
|
||||||
|
|
||||||
<h2>💡 Почему это будет интересно</h2>
|
<h2>💡 Почему это будет интересно</h2>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,11 @@
|
||||||
|
|
||||||
## 🎤 Участники
|
## 🎤 Участники
|
||||||
|
|
||||||
**Алекс Шкор** — модератор и участник. Председатель кооператива «Друзья Коллективного Разума», архитектор кооператива (Simbiot — simbiot.cil.by). AI-инфраструктура и стратегия.
|
**Алекс Шкор** — модератор и участник. Председатель кооператива «Друзья Коллективного Разума», архитектор кооператива (Симбиот, Кибербанк, Domostroy). AI-инфраструктура и стратегия.
|
||||||
|
|
||||||
**Влад Шемардинов** — участник. Член правления. Видеопродакшн и контент, Avatar (avatar.cil.by): подкасты и видео превращаются в вирусные рилсы.
|
**Влад Шемардинов** — участник. Член правления. Видеопродакшн и контент, ReelsUp: подкасты и видео превращаются в вирусные рилсы.
|
||||||
|
|
||||||
**Юрий Бокач** — участник. Секретарь, монетикс, финансовая архитектура, документооборот. Залоговые системы и аналитика. Проект: vybirai.cil.by
|
**Юрий Бокач** — участник. Секретарь, монетикс, финансовая архитектура, документооборот. Залоговые системы и аналитика.
|
||||||
|
|
||||||
## 💡 Почему это будет интересно
|
## 💡 Почему это будет интересно
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
<!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:#0d0f14; --accent:#e8b64c; --line:#2a2e38; --bg:#15171e; --card:#1d2029; --txt:#e8e9ee; --mut:#9aa0b0; }
|
|
||||||
* { box-sizing:border-box; }
|
|
||||||
body { margin:0; font-family:'IBM Plex Sans',-apple-system,'Segoe UI',Roboto,sans-serif; color:var(--txt); background:var(--bg); line-height:1.65; }
|
|
||||||
.wrap { max-width:760px; margin:0 auto; padding:40px 22px 60px; }
|
|
||||||
.badge { display:inline-block; border:1px solid var(--accent); color:var(--accent); border-radius:999px; padding:5px 16px; font-size:12px; letter-spacing:2px; text-transform:uppercase; }
|
|
||||||
h1 { font-size:34px; line-height:1.2; margin:18px 0 6px; font-weight:600; }
|
|
||||||
h1 em { color:var(--accent); font-style:normal; }
|
|
||||||
.sub { color:var(--mut); margin:0 0 32px; font-size:16px; }
|
|
||||||
h2 { font-size:15px; letter-spacing:1.5px; text-transform:uppercase; color:var(--accent); margin:40px 0 14px; }
|
|
||||||
p { font-size:15.5px; }
|
|
||||||
.grid { display:grid; gap:12px; }
|
|
||||||
.row { display:flex; gap:14px; align-items:baseline; padding:12px 16px; background:var(--card); border:1px solid var(--line); border-radius:10px; }
|
|
||||||
.row b { color:var(--accent); min-width:96px; font-weight:600; font-size:14px; }
|
|
||||||
.row span { font-size:15px; }
|
|
||||||
.speaker { background:var(--card); border:1px solid var(--line); border-left:3px solid var(--accent); border-radius:10px; padding:16px 18px; margin:10px 0; }
|
|
||||||
.speaker b { display:block; margin-bottom:3px; font-size:16px; }
|
|
||||||
.speaker span { font-size:14px; color:var(--mut); }
|
|
||||||
ul { padding-left:20px; margin:0; }
|
|
||||||
li { margin:8px 0; font-size:15px; }
|
|
||||||
.cta { background:linear-gradient(135deg,#262a36,#1a1d26); border:1px solid var(--accent); border-radius:14px; padding:22px 24px; margin-top:36px; }
|
|
||||||
.cta b { color:var(--accent); }
|
|
||||||
footer { margin-top:32px; color:var(--mut); font-size:13px; text-align:center; }
|
|
||||||
em { color:var(--accent); font-style:normal; font-weight:600; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="wrap">
|
|
||||||
<span class="badge">Анонс · main-комьюнити</span>
|
|
||||||
<h1>ИИШНИЦА<br><em>«Общее видение»</em></h1>
|
|
||||||
<p class="sub">Панель в три круга, которая заканчивается артефактом — одним названием общего видения будущего</p>
|
|
||||||
|
|
||||||
<h2>Что будет</h2>
|
|
||||||
<p>Не классическая панель с вопросами модератора, а разговор, который заканчивается <em>артефактом</em>: одним названием общего видения будущего.</p>
|
|
||||||
<p>Рамку держит миссия кооператива «42»: <em>все миссии одинаково важны</em> — каждый взгляд на будущее важен, любая индустрия равна.</p>
|
|
||||||
|
|
||||||
<h2>Формат · 45 минут</h2>
|
|
||||||
<div class="grid">
|
|
||||||
<div class="row"><b>00:00–00:03</b><span>Открытие — модератор озвучивает миссию 42 как рамку сессии</span></div>
|
|
||||||
<div class="row"><b>00:03–00:15</b><span>Круг 1 «Моё будущее» — по 4 минуты каждому без вопросов и перебиваний</span></div>
|
|
||||||
<div class="row"><b>00:15–00:21</b><span>Круг 2 «Что понравилось» — связываем, а не спорим</span></div>
|
|
||||||
<div class="row"><b>00:21–00:30</b><span>Круг 3 — панель называет общее видение: одно название для всех картин будущего</span></div>
|
|
||||||
<div class="row"><b>00:30–00:43</b><span>Вопросы из зала — зал четвёртый участник</span></div>
|
|
||||||
<div class="row"><b>00:43–00:45</b><span>Закрытие: название видения + миссия 42</span></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Участники</h2>
|
|
||||||
<div class="speaker"><b>Алекс Шкор</b><span>модератор. Председатель кооператива «Друзья Коллективного Разума», архитектор Simbiot (simbiot.cil.by). AI-инфраструктура и стратегия</span></div>
|
|
||||||
<div class="speaker"><b>Влад Шемардинов</b><span>член правления. Видеопродакшн и контент, Avatar (avatar.cil.by): подкасты и видео превращаются в вирусные рилсы</span></div>
|
|
||||||
<div class="speaker"><b>Юрий Бокач</b><span>секретарь, монетикс. Финансовая архитектура, документооборот, залоговые системы и аналитика. Проект: vybirai.cil.by</span></div>
|
|
||||||
|
|
||||||
<h2>Почему интересно</h2>
|
|
||||||
<ul>
|
|
||||||
<li><em>4 минуты без перебиваний</em> — редкость на панелях: глубина вместо заготовок</li>
|
|
||||||
<li><em>«Что понравилось» вместо «в чём не согласен»</em> — панель выглядит единой, зал видит созвучие индустрий</li>
|
|
||||||
<li><em>Артефакт после сессии</em> — название общего видения остаётся в заголовках и цитатах: панель производит смысл</li>
|
|
||||||
<li><em>Миссия 42 как рамка</em> — спикерам безопасно говорить смело</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="cta">
|
|
||||||
<b>Минск · cil.by</b> — точная дата и площадка в следующих постах, следи за анонсами в канале.<br><br>
|
|
||||||
Приходи: приведи своё видение будущего — оно не менее важно, чем наше.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>Кооператив «Друзья Коллективного Разума» · Минск · cil.by</footer>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
<!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 { --paper:#faf7f2; --ink:#22201c; --acc1:#e2543e; --acc2:#2f6f5e; --line:#e5dfd4; }
|
|
||||||
* { box-sizing:border-box; }
|
|
||||||
body { margin:0; font-family:Georgia,'Times New Roman',serif; color:var(--ink); background:var(--paper); line-height:1.7; }
|
|
||||||
.wrap { max-width:680px; margin:0 auto; padding:48px 24px 64px; }
|
|
||||||
.kicker { font-family:-apple-system,'Segoe UI',Roboto,sans-serif; font-size:12px; letter-spacing:3px; text-transform:uppercase; color:var(--acc1); }
|
|
||||||
h1 { font-size:38px; line-height:1.15; margin:14px 0 8px; font-weight:400; }
|
|
||||||
h1 .q { color:var(--acc1); font-style:italic; }
|
|
||||||
.sub { font-style:italic; color:#6b6459; margin:0 0 36px; font-size:17px; }
|
|
||||||
.rule { height:2px; background:var(--ink); margin:28px 0; }
|
|
||||||
h2 { font-family:-apple-system,'Segoe UI',Roboto,sans-serif; font-size:13px; letter-spacing:2px; text-transform:uppercase; color:var(--acc2); margin:36px 0 12px; }
|
|
||||||
p { font-size:16.5px; }
|
|
||||||
.circles { display:flex; gap:10px; flex-wrap:wrap; margin:16px 0; }
|
|
||||||
.circle { flex:1; min-width:170px; border:1px solid var(--ink); border-radius:50% 50% 12px 12px / 34% 34% 12px 12px; padding:18px 14px 14px; text-align:center; }
|
|
||||||
.circle b { display:block; color:var(--acc1); font-family:-apple-system,Roboto,sans-serif; font-size:13px; letter-spacing:1px; text-transform:uppercase; }
|
|
||||||
.circle span { font-size:14px; color:#4c463d; }
|
|
||||||
.speaker { margin:14px 0; padding:4px 0 4px 18px; border-left:3px solid var(--acc2); }
|
|
||||||
.speaker b { font-size:17px; }
|
|
||||||
.speaker span { display:block; font-size:15px; color:#5c554a; font-style:italic; }
|
|
||||||
ul { padding-left:22px; }
|
|
||||||
li { margin:8px 0; }
|
|
||||||
li::marker { color:var(--acc1); }
|
|
||||||
.cta { background:var(--ink); color:var(--paper); border-radius:4px; padding:24px 26px; margin-top:36px; }
|
|
||||||
.cta b { color:#f0c94f; }
|
|
||||||
footer { margin-top:30px; font-size:13px; color:#8a8171; text-align:center; font-family:Roboto,sans-serif; }
|
|
||||||
em { font-style:italic; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="wrap">
|
|
||||||
<div class="kicker">Анонс · ИИШНИЦА</div>
|
|
||||||
<h1>Панельная сессия<br><span class="q">«Общее видение»</span></h1>
|
|
||||||
<p class="sub">Разговор, который заканчивается артефактом — одним названием общего видения будущего</p>
|
|
||||||
|
|
||||||
<div class="rule"></div>
|
|
||||||
|
|
||||||
<h2>Что будет</h2>
|
|
||||||
<p>Панельная сессия в три круга — не вопросы модератора, а разговор с финалом: <em>одно название общего видения будущего</em>. Рамка — миссия кооператива «42»: все миссии одинаково важны, каждая индустрия равна.</p>
|
|
||||||
|
|
||||||
<div class="circles">
|
|
||||||
<div class="circle"><b>Круг 1</b><span>«Моё будущее» — 4 минуты каждому, без вопросов и перебиваний</span></div>
|
|
||||||
<div class="circle"><b>Круг 2</b><span>«Что понравилось» — связываем картины, а не спорим</span></div>
|
|
||||||
<div class="circle"><b>Круг 3</b><span>Одно название общего видения — артефакт сессии</span></div>
|
|
||||||
</div>
|
|
||||||
<p>Плюс вопросы из зала — зал четвёртый участник. Всего 45 минут.</p>
|
|
||||||
|
|
||||||
<h2>Участники</h2>
|
|
||||||
<div class="speaker"><b>Алекс Шкор</b><span>модератор. Председатель кооператива «Друзья Коллективного Разума», архитектор Simbiot (simbiot.cil.by)</span></div>
|
|
||||||
<div class="speaker"><b>Влад Шемардинов</b><span>член правления. Avatar (avatar.cil.by): подкасты и видео превращаются в вирусные рилсы</span></div>
|
|
||||||
<div class="speaker"><b>Юрий Бокач</b><span>секретарь, монетикс. Финансовая архитектура, документооборот, аналитика. Проект: vybirai.cil.by</span></div>
|
|
||||||
|
|
||||||
<h2>Почему прийти</h2>
|
|
||||||
<ul>
|
|
||||||
<li>4 минуты без перебиваний — глубина вместо заготовок</li>
|
|
||||||
<li>«Что понравилось» вместо «в чём не согласен» — созвучие индустрий вместо спора</li>
|
|
||||||
<li>Название видения остаётся после сессии — панель производит смысл, а не обмен мнениями</li>
|
|
||||||
<li>Миссия 42 делает любой ответ легитимным — спикеры говорят смело</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="cta">
|
|
||||||
<b>Минск · cil.by</b> — дата и площадка в следующих постах.<br><br>
|
|
||||||
Приходи: приведи своё видение будущего — оно не менее важно, чем наше.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>Кооператив «Друзья Коллективного Разума» · Минск · cil.by</footer>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
<!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 { --bg:#070708; --neon:#7cf2c4; --neon2:#a78bfa; --txt:#d6d9e0; --mut:#7c8290; --card:rgba(124,242,196,.05); }
|
|
||||||
* { box-sizing:border-box; }
|
|
||||||
body { margin:0; font-family:'Space Grotesk',-apple-system,'Segoe UI',Roboto,sans-serif; color:var(--txt); background:var(--bg); line-height:1.6; }
|
|
||||||
.hero { padding:64px 22px 40px; text-align:center; background:radial-gradient(ellipse 60% 45% at 50% 0%, rgba(124,242,196,.14), transparent 70%), radial-gradient(ellipse 40% 30% at 80% 10%, rgba(167,139,250,.10), transparent 70%); }
|
|
||||||
.tag { display:inline-block; border:1px solid var(--neon); color:var(--neon); border-radius:4px; padding:4px 14px; font-size:11px; letter-spacing:4px; text-transform:uppercase; }
|
|
||||||
h1 { font-size:clamp(32px,6vw,52px); line-height:1.08; margin:22px 0 10px; font-weight:700; color:#fff; }
|
|
||||||
h1 span { background:linear-gradient(90deg,var(--neon),var(--neon2)); -webkit-background-clip:text; background-clip:text; color:transparent; }
|
|
||||||
.sub { color:var(--mut); font-size:16px; max-width:520px; margin:0 auto; }
|
|
||||||
.wrap { max-width:720px; margin:0 auto; padding:16px 22px 64px; }
|
|
||||||
h2 { font-size:13px; letter-spacing:3px; text-transform:uppercase; color:var(--neon); margin:44px 0 16px; }
|
|
||||||
p { font-size:15.5px; }
|
|
||||||
.flow { display:grid; gap:10px; }
|
|
||||||
.step { display:flex; gap:16px; align-items:baseline; padding:14px 18px; border:1px solid rgba(124,242,196,.18); border-radius:8px; background:var(--card); }
|
|
||||||
.step i { color:var(--neon); font-style:normal; font-weight:700; font-size:13px; min-width:86px; font-variant-numeric:tabular-nums; }
|
|
||||||
.step b { color:#fff; }
|
|
||||||
.step span { font-size:14.5px; color:var(--txt); }
|
|
||||||
.sp { display:grid; grid-template-columns:1fr; gap:10px; }
|
|
||||||
.card { border:1px solid rgba(167,139,250,.25); border-radius:10px; padding:16px 18px; }
|
|
||||||
.card b { color:#fff; font-size:16px; display:block; }
|
|
||||||
.card span { font-size:14px; color:var(--mut); }
|
|
||||||
.card i { color:var(--neon2); font-style:normal; font-size:12px; letter-spacing:1px; text-transform:uppercase; }
|
|
||||||
ul { list-style:none; padding:0; }
|
|
||||||
li { margin:10px 0; padding-left:26px; position:relative; font-size:15px; }
|
|
||||||
li::before { content:'▸'; position:absolute; left:0; color:var(--neon); }
|
|
||||||
.cta { margin-top:44px; text-align:center; border:1px solid var(--neon); border-radius:12px; padding:26px 22px; background:linear-gradient(180deg,rgba(124,242,196,.08),transparent); }
|
|
||||||
.cta b { color:var(--neon); }
|
|
||||||
footer { margin-top:32px; text-align:center; color:var(--mut); font-size:12.5px; letter-spacing:1px; }
|
|
||||||
em { color:var(--neon); font-style:normal; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="hero">
|
|
||||||
<span class="tag">Анонс · ИИШНИЦА</span>
|
|
||||||
<h1>Панель «<span>Общее видение</span>»</h1>
|
|
||||||
<p class="sub">Три круга разговора · 45 минут · один артефакт на выходе — название общего видения будущего</p>
|
|
||||||
</div>
|
|
||||||
<div class="wrap">
|
|
||||||
|
|
||||||
<h2>// Что будет</h2>
|
|
||||||
<p>Не панель с вопросами модератора, а разговор с финалом: <em>одно название общего видения будущего</em>. Рамка — миссия кооператива «42»: все миссии одинаково важны, каждая индустрия равна.</p>
|
|
||||||
|
|
||||||
<h2>// Формат</h2>
|
|
||||||
<div class="flow">
|
|
||||||
<div class="step"><i>00:00</i><span><b>Открытие</b> — миссия 42 как рамка сессии</span></div>
|
|
||||||
<div class="step"><i>00:03</i><span><b>Круг 1 · «Моё будущее»</b> — по 4 минуты каждому, без вопросов и перебиваний</span></div>
|
|
||||||
<div class="step"><i>00:15</i><span><b>Круг 2 · «Что понравилось»</b> — связываем картины, а не спорим</span></div>
|
|
||||||
<div class="step"><i>00:21</i><span><b>Круг 3</b> — панель называет общее видение</span></div>
|
|
||||||
<div class="step"><i>00:30</i><span><b>Вопросы из зала</b> — зал четвёртый участник</span></div>
|
|
||||||
<div class="step"><i>00:43</i><span><b>Финал</b> — название видения + короткое резюме</span></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>// Участники</h2>
|
|
||||||
<div class="sp">
|
|
||||||
<div class="card"><i>модератор</i><b>Алекс Шкор</b><span>Председатель кооператива «Друзья Коллективного Разума». Архитектор Simbiot (simbiot.cil.by). AI-инфраструктура и стратегия</span></div>
|
|
||||||
<div class="card"><i>правление</i><b>Влад Шемардинов</b><span>Avatar (avatar.cil.by): подкасты и видео превращаются в вирусные рилсы</span></div>
|
|
||||||
<div class="card"><i>секретарь · монетикс</i><b>Юрий Бокач</b><span>Финансовая архитектура, документооборот, залоговые системы и аналитика. Проект: vybirai.cil.by</span></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>// Зачем идти</h2>
|
|
||||||
<ul>
|
|
||||||
<li><em>4 минуты без перебиваний</em> — глубина вместо заготовок</li>
|
|
||||||
<li><em>«Что понравилось» вместо спора</em> — созвучие индустрий</li>
|
|
||||||
<li><em>Артефакт остаётся</em> — название видения живёт в заголовках и цитатах</li>
|
|
||||||
<li><em>Миссия 42</em> — спикерам безопасно говорить смело</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="cta">
|
|
||||||
<b>Минск · cil.by</b><br>дата и площадка — в следующих постах<br><br>
|
|
||||||
Приходи: приведи своё видение будущего — оно не менее важно, чем наше.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>КООПЕРАТИВ «ДРУЗЬЯ КОЛЛЕКТИВНОГО РАЗУМА» · МИНСК · CIL.BY</footer>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -65,9 +65,9 @@
|
||||||
</ol>
|
</ol>
|
||||||
<div class="speakers">
|
<div class="speakers">
|
||||||
<b>На сцене:</b>
|
<b>На сцене:</b>
|
||||||
• Алекс Шкор — модератор и участник. Председатель кооператива, AI-инфраструктура и стратегия (Simbiot — simbiot.cil.by)<br>
|
• Алекс Шкор — модератор и участник. Председатель кооператива, AI-инфраструктура и стратегия (Симбиот, Кибербанк, Domostroy)<br>
|
||||||
• Влад Шемардинов — видеопродакшн и контент, Avatar (avatar.cil.by)<br>
|
• Влад Шемардинов — видеопродакшн и контент, ReelsUp<br>
|
||||||
• Юрий Бокач — монетикс, финансовая архитектура, залоговые системы (vybirai.cil.by)
|
• Юрий Бокач — монетикс, финансовая архитектура, залоговые системы
|
||||||
</div>
|
</div>
|
||||||
<div class="mission">Рамку держит миссия кооператива «42»: <b>все миссии одинаково важны</b> — каждый взгляд на будущее важен, любая индустрия равна.</div>
|
<div class="mission">Рамку держит миссия кооператива «42»: <b>все миссии одинаково важны</b> — каждый взгляд на будущее важен, любая индустрия равна.</div>
|
||||||
<div class="loc">📍 Минск · точная дата и площадка — в следующих постах<br>🌐 cil.by</div>
|
<div class="loc">📍 Минск · точная дата и площадка — в следующих постах<br>🌐 cil.by</div>
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@
|
||||||
3️⃣ «Название общего видения» — панель и зал вместе рождают одно название для всех картин будущего. Оно остаётся после сессии — в заголовках, фото и цитатах
|
3️⃣ «Название общего видения» — панель и зал вместе рождают одно название для всех картин будущего. Оно остаётся после сессии — в заголовках, фото и цитатах
|
||||||
|
|
||||||
**На сцене:**
|
**На сцене:**
|
||||||
• Алекс Шкор — модератор и участник. Председатель кооператива, AI-инфраструктура и стратегия (Simbiot — simbiot.cil.by)
|
• Алекс Шкор — модератор и участник. Председатель кооператива, AI-инфраструктура и стратегия (Симбиот, Кибербанк, Domostroy)
|
||||||
• Влад Шемардинов — видеопродакшн и контент, Avatar (avatar.cil.by)
|
• Влад Шемардинов — видеопродакшн и контент, ReelsUp
|
||||||
• Юрий Бокач — монетикс, финансовая архитектура, залоговые системы (vybirai.cil.by)
|
• Юрий Бокач — монетикс, финансовая архитектура, залоговые системы
|
||||||
|
|
||||||
Рамку держит миссия кооператива «42»: **все миссии одинаково важны** — каждый взгляд на будущее важен, любая индустрия равна.
|
Рамку держит миссия кооператива «42»: **все миссии одинаково важны** — каждый взгляд на будущее важен, любая индустрия равна.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,14 @@
|
||||||
# 5) daemon-reload + enable --now + финальная проверка версий и здоровья
|
# 5) daemon-reload + enable --now + финальная проверка версий и здоровья
|
||||||
#
|
#
|
||||||
# Использование (root или sudo):
|
# Использование (root или sudo):
|
||||||
# bash scripts/deploy-claude-code.sh # дефолты: :7681, root + bypass permissions
|
# bash scripts/deploy-claude-code.sh # дефолты: :7681, юзер claude, токен сгенерится
|
||||||
# bash scripts/deploy-claude-code.sh --port 7681 --user lunar --token MySecret
|
# bash scripts/deploy-claude-code.sh --port 7681 --user lunar --token MySecret
|
||||||
# bash scripts/deploy-claude-code.sh --run-as claude # ограниченный режим: юзер claude
|
|
||||||
# bash scripts/deploy-claude-code.sh --dry-run --port 9000 --user bob --token x
|
# bash scripts/deploy-claude-code.sh --dry-run --port 9000 --user bob --token x
|
||||||
# bash scripts/deploy-claude-code.sh --check # только здоровье, без установки
|
# bash scripts/deploy-claude-code.sh --check # только здоровье, без установки
|
||||||
#
|
#
|
||||||
# Опции:
|
# Опции:
|
||||||
# --port N порт ttyd (по умолчанию 7681, как на Лунаре)
|
# --port N порт ttyd (по умолчанию 7681, как на Лунаре)
|
||||||
# --user NAME логин basic-auth (по умолчанию claude)
|
# --user NAME OS-юзер сервиса И логин basic-auth (по умолчанию claude)
|
||||||
# --run-as NAME юзер сервиса ttyd (по умолчанию root — агенту нужны
|
|
||||||
# права на редактирование любых файлов; t-261016120111)
|
|
||||||
# --ask-permissions НЕ прокидывать --dangerously-skip-permissions
|
|
||||||
# (по умолчанию флаг прокидывается всегда — bypass)
|
|
||||||
# --token SECRET пароль basic-auth (по умолчанию: openssl rand -hex 12)
|
# --token SECRET пароль basic-auth (по умолчанию: openssl rand -hex 12)
|
||||||
# --service NAME имя systemd-юнита (по умолчанию ttyd-claude)
|
# --service NAME имя systemd-юнита (по умолчанию ttyd-claude)
|
||||||
# --ttyd-version V версия ttyd (по умолчанию 1.7.7)
|
# --ttyd-version V версия ttyd (по умолчанию 1.7.7)
|
||||||
|
|
@ -37,17 +32,14 @@
|
||||||
# --check только проверка здоровья (версии + HTTP + сервис)
|
# --check только проверка здоровья (версии + HTTP + сервис)
|
||||||
# -h | --help эта справка
|
# -h | --help эта справка
|
||||||
#
|
#
|
||||||
# Безопасность: веб-терминал закрыт basic-auth (-c user:token). ПО УМОЛЧАНИЮ
|
# Безопасность: веб-терминнал закрыт basic-auth (-c user:token) — как на Лунаре
|
||||||
# сервис работает от root и claude запускается с --dangerously-skip-permissions
|
# (логин lunar / Zm9yZXZ0b25lClaude01). Сервис работает от непривилегированного
|
||||||
# (мандат t-261016120111: агент должен уметь редактировать любые файлы).
|
# юзера (User=), никакого root-shell в браузере.
|
||||||
# Для ограниченной развёртки: --run-as <user> --ask-permissions.
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
PORT="7681"
|
PORT="7681"
|
||||||
USER_NAME="claude"
|
USER_NAME="claude"
|
||||||
RUN_AS="root"
|
|
||||||
ASK_PERMISSIONS=0
|
|
||||||
TOKEN=""
|
TOKEN=""
|
||||||
SERVICE="ttyd-claude"
|
SERVICE="ttyd-claude"
|
||||||
TTYD_VERSION="1.7.7"
|
TTYD_VERSION="1.7.7"
|
||||||
|
|
@ -70,8 +62,6 @@ while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--port) PORT="${2:-}"; shift 2 ;;
|
--port) PORT="${2:-}"; shift 2 ;;
|
||||||
--user) USER_NAME="${2:-}"; shift 2 ;;
|
--user) USER_NAME="${2:-}"; shift 2 ;;
|
||||||
--run-as) RUN_AS="${2:-}"; shift 2 ;;
|
|
||||||
--ask-permissions) ASK_PERMISSIONS=1; shift ;;
|
|
||||||
--token) TOKEN="${2:-}"; shift 2 ;;
|
--token) TOKEN="${2:-}"; shift 2 ;;
|
||||||
--service) SERVICE="${2:-}"; shift 2 ;;
|
--service) SERVICE="${2:-}"; shift 2 ;;
|
||||||
--ttyd-version) TTYD_VERSION="${2:-}"; shift 2 ;;
|
--ttyd-version) TTYD_VERSION="${2:-}"; shift 2 ;;
|
||||||
|
|
@ -89,7 +79,6 @@ while [ $# -gt 0 ]; do
|
||||||
opt="${1%%=*}"; val="${1#*=}"
|
opt="${1%%=*}"; val="${1#*=}"
|
||||||
case "$opt" in
|
case "$opt" in
|
||||||
--port) PORT="$val" ;; --user) USER_NAME="$val" ;;
|
--port) PORT="$val" ;; --user) USER_NAME="$val" ;;
|
||||||
--run-as) RUN_AS="$val" ;; --ask-permissions) ASK_PERMISSIONS=1 ;;
|
|
||||||
--token) TOKEN="$val" ;; --service) SERVICE="$val" ;;
|
--token) TOKEN="$val" ;; --service) SERVICE="$val" ;;
|
||||||
--ttyd-version) TTYD_VERSION="$val" ;; --node-version) NODE_VERSION="$val" ;;
|
--ttyd-version) TTYD_VERSION="$val" ;; --node-version) NODE_VERSION="$val" ;;
|
||||||
--claude-version) CLAUDE_VERSION="$val" ;; --interface) INTERFACE="$val" ;;
|
--claude-version) CLAUDE_VERSION="$val" ;; --interface) INTERFACE="$val" ;;
|
||||||
|
|
@ -104,10 +93,6 @@ done
|
||||||
|| die "--port должен быть числом 1..65535 (получили: '$PORT')"
|
|| die "--port должен быть числом 1..65535 (получили: '$PORT')"
|
||||||
[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] \
|
[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] \
|
||||||
|| die "--user должен быть валидным unix-логином (a-z, 0-9, _, -; получили: '$USER_NAME')"
|
|| die "--user должен быть валидным unix-логином (a-z, 0-9, _, -; получили: '$USER_NAME')"
|
||||||
[[ "$RUN_AS" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] \
|
|
||||||
|| die "--run-as должен быть валидным unix-логином (получили: '$RUN_AS')"
|
|
||||||
id -u "$RUN_AS" >/dev/null 2>&1 || [ "$DRY_RUN" -eq 1 ] || [ "$CHECK_ONLY" -eq 1 ] \
|
|
||||||
|| die "--run-as: юзер '$RUN_AS' не существует (скрипт его не создаёт — создай сам или запусти без --run-as для root)"
|
|
||||||
[[ "$SERVICE" =~ ^[a-zA-Z0-9_.@-]+$ ]] || die "--service: некорректное имя юнита '$SERVICE'"
|
[[ "$SERVICE" =~ ^[a-zA-Z0-9_.@-]+$ ]] || die "--service: некорректное имя юнита '$SERVICE'"
|
||||||
case "$TOKEN" in
|
case "$TOKEN" in
|
||||||
*:*) die "--token не может содержать ':' (ломает -c user:token)" ;;
|
*:*) die "--token не может содержать ':' (ломает -c user:token)" ;;
|
||||||
|
|
@ -238,22 +223,17 @@ else
|
||||||
bash -c "set -e; curl -fsSL -o /usr/local/bin/ttyd https://github.com/tsl0922/ttyd/releases/download/$TTYD_VERSION/ttyd.$TARCH && chmod +x /usr/local/bin/ttyd"
|
bash -c "set -e; curl -fsSL -o /usr/local/bin/ttyd https://github.com/tsl0922/ttyd/releases/download/$TTYD_VERSION/ttyd.$TARCH && chmod +x /usr/local/bin/ttyd"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 4) systemd-юнит ──────────────────────────────────────────────────────────
|
# ── 4) OS-юзер и systemd-юнит ────────────────────────────────────────────────
|
||||||
# По умолчанию сервис работает от root (RUN_AS=root): агент получает право
|
if [ "$DRY_RUN" -eq 0 ]; then
|
||||||
# редактировать любые файлы (мандат t-261016120111). Ограниченный режим:
|
if ! id -u "$USER_NAME" >/dev/null 2>&1; then
|
||||||
# --run-as <user> (+ можно --ask-permissions).
|
log "создаю юзера $USER_NAME (без root-shell в веб-терминале)"
|
||||||
if [ "$DRY_RUN" -eq 0 ] && [ "$RUN_AS" != "root" ]; then
|
useradd -m -s /bin/bash "$USER_NAME"
|
||||||
id -u "$RUN_AS" >/dev/null 2>&1 || { log "создаю юзера $RUN_AS (без root-shell в веб-терминале)"; useradd -m -s /bin/bash "$RUN_AS"; }
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
WRITABLE='-W'; [ "$READONLY" -eq 1 ] && WRITABLE=''
|
WRITABLE='-W'; [ "$READONLY" -eq 1 ] && WRITABLE=''
|
||||||
# bypass permissions: --dangerously-skip-permissions прокидывается ПО УМОЛЧАНИЮ
|
|
||||||
CLAUDE_ARGS=''
|
|
||||||
[ "$ASK_PERMISSIONS" -eq 0 ] && CLAUDE_ARGS='--dangerously-skip-permissions'
|
|
||||||
UNIT_PATH="/etc/systemd/system/${SERVICE}.service"
|
UNIT_PATH="/etc/systemd/system/${SERVICE}.service"
|
||||||
SERVICE_HOME="/home/${RUN_AS}"
|
ENV_LINES="Environment=HOME=/home/${USER_NAME}
|
||||||
[ "$RUN_AS" = "root" ] && SERVICE_HOME="/root"
|
|
||||||
ENV_LINES="Environment=HOME=${SERVICE_HOME}
|
|
||||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin"
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin"
|
||||||
if [ ${#UNIT_ENV[@]} -gt 0 ]; then
|
if [ ${#UNIT_ENV[@]} -gt 0 ]; then
|
||||||
for e in "${UNIT_ENV[@]}"; do ENV_LINES+="
|
for e in "${UNIT_ENV[@]}"; do ENV_LINES+="
|
||||||
|
|
@ -265,9 +245,9 @@ After=network-online.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=${RUN_AS}
|
User=${USER_NAME}
|
||||||
${ENV_LINES}
|
${ENV_LINES}
|
||||||
ExecStart=/usr/local/bin/ttyd ${WRITABLE} -c ${USER_NAME}:${TOKEN} -p ${PORT} -i ${INTERFACE} claude ${CLAUDE_ARGS}
|
ExecStart=/usr/local/bin/ttyd ${WRITABLE} -c ${USER_NAME}:${TOKEN} -p ${PORT} -i ${INTERFACE} claude
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
|
||||||
|
|
@ -306,5 +286,4 @@ log " URL : http://${IP}:${PORT}"
|
||||||
log " логин : ${USER_NAME}"
|
log " логин : ${USER_NAME}"
|
||||||
log " пароль : ${TOKEN}"
|
log " пароль : ${TOKEN}"
|
||||||
log " сервис : systemctl status ${SERVICE}"
|
log " сервис : systemctl status ${SERVICE}"
|
||||||
log " режим : User=${RUN_AS}; claude-флаги: ${CLAUDE_ARGS:-—} (bypass permissions по умолчанию, t-261016120111)"
|
|
||||||
log " первый вход: открой URL, пройди авторизацию claude (или передай --env ANTHROPIC_BASE_URL=... при развёртке)"
|
log " первый вход: открой URL, пройди авторизацию claude (или передай --env ANTHROPIC_BASE_URL=... при развёртке)"
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# t-261015090129: отложенный рестарт лестницы харнеса — подхватить рефлекс
|
|
||||||
# agent-channel. Ждёт, пока не останется живых LLM-воркеров (тик допишет
|
|
||||||
# дайджест и закроет задачи), максимум 15 минут, затем рестарт.
|
|
||||||
for i in $(seq 1 90); do
|
|
||||||
if ! pgrep -f "harness/_llm_worker.py" >/dev/null; then
|
|
||||||
sleep 5 # тишина 5с подряд — тик действительно закончился
|
|
||||||
if ! pgrep -f "harness/_llm_worker.py" >/dev/null; then
|
|
||||||
systemctl restart simbiot-harness.service
|
|
||||||
logger -t simbiot-restart "agent-channel: simbiot-harness перезапущен после тика (t-261015090129)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
sleep 10
|
|
||||||
done
|
|
||||||
logger -t simbiot-restart "agent-channel: воркер не освободился за 15мин — рестарт без ожидания"
|
|
||||||
systemctl restart simbiot-harness.service
|
|
||||||
|
|
@ -1,19 +1,10 @@
|
||||||
Landing живёт публично: https://landing.cil.by/ (+ /session/)
|
Landing served locally: systemd landing-serve (python3 http.server :8095, dir sites/landing)
|
||||||
|
Public tunnel: systemd landing-tunnel (localhost.run; обёртка /usr/local/bin/landing-tunnel-run.sh)
|
||||||
|
Current public URL: см. PUBLIC_URL.txt (обёртка обновляет его сама при каждой смене)
|
||||||
|
|
||||||
Схема (e-261013154449, 2026-10-13): хост харнеса = хост cil.by (204.168.232.21),
|
Важно (2026-10-07): localhost.run в nokey-режиме выдаёт случайный субдомен на
|
||||||
вход терминирует Caddy. Лендинг — статикой, как presale.cil.by:
|
каждый рестарт — URL нестабилен by design. Обёртка туннеля парсит stdout ssh
|
||||||
- docroot: /var/www/simbiot-landing (копия index.html, styles.css, session/)
|
и перезаписывает PUBLIC_URL.txt при смене URL (лог: /var/log/landing-tunnel.log).
|
||||||
- сайт-блок: /etc/caddy/sites/landing.cil.by.caddy (root+file_server+try_files)
|
Стабильный URL = только через прод (simbiot.cil.by, scripts/deploy_landing.sh
|
||||||
- serve :8095 и localhost.run-туннель больше НЕ нужны (юниты потеряны при
|
из origin/master) или именованный туннель с аккаунтом.
|
||||||
миграции и не восстанавливались: nokey-режим localhost.run мёртв для
|
Note: server IP 185.203.154.209 drops inbound from outside (NAT/firewall), hence tunnel.
|
||||||
remote forwarding, старый URL *.lhr.life → 502)
|
|
||||||
|
|
||||||
Обновление контента: отредактировать sites/landing/*, затем
|
|
||||||
cp index.html styles.css session/ -r → /var/www/simbiot-landing/
|
|
||||||
caddy validate --config /etc/caddy/Caddyfile && caddy reload --config /etc/caddy/Caddyfile
|
|
||||||
(caddy reload только при правке сайт-блока; чистый cp файлов reload не требует)
|
|
||||||
|
|
||||||
PUBLIC_URL.txt хранит актуальный публичный URL (стабилен, имя закреплено).
|
|
||||||
|
|
||||||
Примечание: DEPLOY-скрипт scripts/deploy_landing.sh — про ДРУГОЙ лендинг
|
|
||||||
(simbiot.cil.by из docs/simbiot-landing-v2-live.html), не путать.
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
https://landing.cil.by
|
https://1725593532350f.lhr.life
|
||||||
|
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>AI Agent Session — 60 минут с агентом студии кооператива 42 · $150</title>
|
|
||||||
<meta name="description" content="Одна 60-минутная сессия с AI-агентом студии кооператива 42: разбор вашей задачи, прототип или автоматизация за звонок. $150, one-time.">
|
|
||||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%230b0f14'/%3E%3Cpath d='M5 18h5l2-7 4 13 3-9 2 3h6' fill='none' stroke='%235eead4' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E">
|
|
||||||
<style>
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap');
|
|
||||||
:root{
|
|
||||||
--bg:#0b0f14; --panel:#101720; --line:#1d2836; --text:#e6edf3;
|
|
||||||
--mut:#8b98a5; --accent:#5eead4; --accent-ink:#06201b;
|
|
||||||
}
|
|
||||||
*{box-sizing:border-box}
|
|
||||||
html{scroll-behavior:smooth}
|
|
||||||
body{margin:0;background:var(--bg);color:var(--text);
|
|
||||||
font:16px/1.65 "IBM Plex Sans",system-ui,sans-serif;}
|
|
||||||
code,.mono{font-family:"IBM Plex Mono",monospace}
|
|
||||||
.container{max-width:880px;margin:0 auto;padding:0 24px}
|
|
||||||
a{color:var(--accent)}
|
|
||||||
header.site{border-bottom:1px solid var(--line);padding:18px 0}
|
|
||||||
header.site .container{display:flex;align-items:center;justify-content:space-between}
|
|
||||||
.brand{display:flex;gap:10px;align-items:center;color:var(--text);text-decoration:none;font-weight:600}
|
|
||||||
.brand svg{stroke:var(--accent)}
|
|
||||||
.btn{display:inline-block;padding:12px 22px;border-radius:8px;text-decoration:none;font-weight:600;border:1px solid transparent}
|
|
||||||
.btn-primary{background:var(--accent);color:var(--accent-ink)}
|
|
||||||
.btn-ghost{border-color:var(--line);color:var(--text)}
|
|
||||||
section{padding:56px 0}
|
|
||||||
.hero{padding:88px 0 64px}
|
|
||||||
.eyebrow{color:var(--mut);font-family:"IBM Plex Mono",monospace;font-size:13px;letter-spacing:.08em;text-transform:uppercase;margin:0 0 14px}
|
|
||||||
h1{font-size:clamp(30px,5vw,46px);line-height:1.15;margin:0 0 18px;font-weight:600}
|
|
||||||
.lead{color:var(--mut);font-size:19px;max-width:640px;margin:0 0 30px}
|
|
||||||
.hero-cta{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
|
|
||||||
.price-tag{font-family:"IBM Plex Mono",monospace;color:var(--accent)}
|
|
||||||
h2{font-size:26px;font-weight:600;margin:0 0 22px}
|
|
||||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px}
|
|
||||||
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:22px}
|
|
||||||
.card h3{margin:0 0 8px;font-size:17px;font-weight:600}
|
|
||||||
.card p{margin:0;color:var(--mut);font-size:15px}
|
|
||||||
ul.check{list-style:none;padding:0;margin:0}
|
|
||||||
ul.check li{padding:9px 0 9px 30px;position:relative;border-bottom:1px dashed var(--line)}
|
|
||||||
ul.check li:last-child{border-bottom:none}
|
|
||||||
ul.check li::before{content:"✓";position:absolute;left:4px;color:var(--accent);font-family:"IBM Plex Mono",monospace}
|
|
||||||
ul.check li span{color:var(--mut)}
|
|
||||||
.cta-band{background:var(--panel);border-block:1px solid var(--line);text-align:center}
|
|
||||||
.cta-band .lead{margin-inline:auto}
|
|
||||||
footer{border-top:1px solid var(--line);color:var(--mut);font-size:14px;padding:28px 0}
|
|
||||||
.note{color:var(--mut);font-size:14px}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header class="site">
|
|
||||||
<div class="container">
|
|
||||||
<a class="brand" href="#top">
|
|
||||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h4l2-5 4 10 3-7 2 2h5"/></svg>
|
|
||||||
42 · AI Agent Studio
|
|
||||||
</a>
|
|
||||||
<a class="btn btn-primary" href="https://cil.by/">Записаться на сессию</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<section class="hero" id="top">
|
|
||||||
<div class="container">
|
|
||||||
<p class="eyebrow">Technology cooperative · cil.by</p>
|
|
||||||
<h1>AI Agent Session — 60 минут, за которые появится результат</h1>
|
|
||||||
<p class="lead">Живая рабочая сессия с AI-агентом студии кооператива 42. Не демо и не консультация «в порядке беседы»: агент работает с вашими реальными данными и инструментами прямо в звонке.</p>
|
|
||||||
<div class="hero-cta">
|
|
||||||
<a class="btn btn-primary" href="https://cil.by/">Забронировать сессию →</a>
|
|
||||||
<span class="price-tag mono">$150 · one-time · 60 мин</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="includes">
|
|
||||||
<div class="container">
|
|
||||||
<h2>Что входит в сессию</h2>
|
|
||||||
<div class="grid">
|
|
||||||
<div class="card">
|
|
||||||
<h3>Разбор задачи</h3>
|
|
||||||
<p>Формулируем вашу задачу так, чтобы её можно было автоматизировать: вход, выход, критерий готовности.</p>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Работа агента вживую</h3>
|
|
||||||
<p>Агент со shell, файлами и поиском по базе знаний делает реальный кусок работы на ваших данных.</p>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Прототип за звонок</h3>
|
|
||||||
<p>Скрипт, страница, отчёт или интеграция — минимальный работающий артефакт к концу часа.</p>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Запись и саммари</h3>
|
|
||||||
<p>Звонок записывается, транскрипт и итоги приходят вам на почту автоматически.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="for-whom">
|
|
||||||
<div class="container">
|
|
||||||
<h2>Для кого</h2>
|
|
||||||
<ul class="check">
|
|
||||||
<li>Основателям и продакт-менеджерам — <span>быстро проверить, отдать ли задачу агенту, и что это даст в цифрах</span></li>
|
|
||||||
<li>Операционным командам — <span>рутинные ручные процессы: отчёты, перенос данных, мониторинг, воронки</span></li>
|
|
||||||
<li>Разработчикам — <span>поднять ритмичного агента (тик-так, CQRS, эскалации) на своей инфраструктуре</span></li>
|
|
||||||
<li>Всем, кому нужен один конкретный артефакт — <span>лендинг, скрипт, аудит или разбор — в течение часа, а не недели</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="cta-band">
|
|
||||||
<div class="container">
|
|
||||||
<h2>$150 · 60 минут · one-time</h2>
|
|
||||||
<p class="lead">Без подписки и обязательств. Если после сессии захотите агента «на постоянку» — расскажем, как это устроено в кооперативе.</p>
|
|
||||||
<a class="btn btn-primary" href="https://cil.by/">Забронировать сессию →</a>
|
|
||||||
<p class="note">Бронь через портал <a href="https://cil.by/">cil.by</a>: выберите слот, подтверждение и ссылку на звонок придут на почту.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div class="container">
|
|
||||||
Кооператив 42 · <a href="https://cil.by/">cil.by</a> · AI Agent Session — продукт агентной студии, вне presale-линейки.
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
0
tasks.db
0
tasks.db
|
|
@ -1,143 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""t-261015090129-ef5029: канал агент↔агент через MCP (ask_agent/poll_messages).
|
|
||||||
|
|
||||||
Запуск: ./venv/bin/python tests/test_agent_channel.py
|
|
||||||
|
|
||||||
Сеть НЕ трогается: harness.agent_channel._rpc_tool подменяется фейком —
|
|
||||||
"портал" отвечает заготовками ask_agent/poll_messages. Проверяет:
|
|
||||||
- pull(): agent_message → MessageReceived в стор (kind=agent_message,
|
|
||||||
chat_id=portal:agent:<from>, update_id=id события портала);
|
|
||||||
- дедуп: повторный pull с теми же id не дублирует инбокс (курсор портала
|
|
||||||
не сдвинулся — стор второй линией отсекает);
|
|
||||||
- push_agent(): ask_agent(to, text, kind) — ok:true → True; error → False;
|
|
||||||
- send_reply() на agent-сообщение: ветка portal:agent:* уходит ask_agent'ом
|
|
||||||
(fake перехватывает), ReplySent пишется, сообщение закрывается;
|
|
||||||
- send_batch_reply(): сводный ответ агенту одним ask_agent;
|
|
||||||
- sec_reflex(): rate-limit (не чаще MIN_REFLEX_GAP), после сброса — pull.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
_DST = "/tmp/simbiot-test-agent-channel.db"
|
|
||||||
for p in (_DST, _DST + "-wal", _DST + "-shm"):
|
|
||||||
if os.path.exists(p):
|
|
||||||
os.unlink(p)
|
|
||||||
os.environ["SIMBIOT_DB"] = _DST
|
|
||||||
os.environ["SIMBIOT_PORTAL_MCP_TOKEN"] = "agt_test_agent_channel"
|
|
||||||
os.environ.pop("SIMBIOT_HOME", None)
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
from harness import agent_channel, es, tg_inbox
|
|
||||||
|
|
||||||
fails = []
|
|
||||||
|
|
||||||
|
|
||||||
def check(name, cond, extra=""):
|
|
||||||
print(("PASS" if cond else "FAIL"), name, ("" if cond else ("| " + str(extra))))
|
|
||||||
if not cond:
|
|
||||||
fails.append(name)
|
|
||||||
|
|
||||||
|
|
||||||
es.init_db()
|
|
||||||
|
|
||||||
# ── фейк MCP-шлюза ──────────────────────────────────────────────────────────
|
|
||||||
CALLS = []
|
|
||||||
|
|
||||||
|
|
||||||
def _fake_rpc(tool, arguments):
|
|
||||||
CALLS.append((tool, arguments))
|
|
||||||
if tool == "poll_messages":
|
|
||||||
return json.dumps({"messages": FAKE_QUEUE.get("messages", []),
|
|
||||||
"count": len(FAKE_QUEUE.get("messages", [])),
|
|
||||||
"note": "ok"})
|
|
||||||
if tool == "ask_agent":
|
|
||||||
return json.dumps({"ok": True, "to": arguments.get("to"),
|
|
||||||
"delivered": "agent_message"})
|
|
||||||
return json.dumps({"error": "unknown"})
|
|
||||||
|
|
||||||
|
|
||||||
agent_channel._rpc_tool = _fake_rpc
|
|
||||||
|
|
||||||
FAKE_QUEUE = {"messages": [
|
|
||||||
{"id": 9001, "from": "OpenCode", "kind": "help",
|
|
||||||
"text": "🆘 Запрос помощи от «OpenCode»:\n\nСимбиот, глянь deploy_app для groupsign",
|
|
||||||
"ts": "2026-10-15 09:00:00"},
|
|
||||||
{"id": 9002, "from": "Roy", "kind": "handoff",
|
|
||||||
"text": "🤝 Передача задачи от «Roy»:\n\nЗабери мою задачу по туннелям",
|
|
||||||
"ts": "2026-10-15 09:05:00"},
|
|
||||||
]}
|
|
||||||
|
|
||||||
# ── 1. pull → инбокс ────────────────────────────────────────────────────────
|
|
||||||
n = agent_channel.pull()
|
|
||||||
check("pull инжестит 2 сообщения", n == 2, n)
|
|
||||||
model = es.run_projections()
|
|
||||||
inbox = model.get("inbox") or {}
|
|
||||||
m1 = inbox.get("m-9001")
|
|
||||||
check("m-9001 в сторе", m1 is not None)
|
|
||||||
if m1:
|
|
||||||
check("chat_id агентный", m1.get("chat_id") == "portal:agent:OpenCode", m1.get("chat_id"))
|
|
||||||
check("kind=agent_message", m1.get("kind") == "agent_message")
|
|
||||||
check("username=агент", m1.get("username") == "OpenCode")
|
|
||||||
check("agent_kind=help", m1.get("agent_kind") == "help")
|
|
||||||
check("m-9002 в сторе", inbox.get("m-9002") is not None)
|
|
||||||
|
|
||||||
# ── 2. дедуп: тот же poll снова ─────────────────────────────────────────────
|
|
||||||
n2 = agent_channel.pull()
|
|
||||||
check("повторный pull — 0 новых (дедуп стора)", n2 == 0, n2)
|
|
||||||
|
|
||||||
# ── 3. push_agent ───────────────────────────────────────────────────────────
|
|
||||||
ok = agent_channel.push_agent("OpenCode", "deploy ок, лог чист", "help")
|
|
||||||
check("push_agent ok", ok is True and CALLS[-1][0] == "ask_agent", CALLS[-1:])
|
|
||||||
args = CALLS[-1][1]
|
|
||||||
check("ask_agent аргументы", args.get("to") == "OpenCode"
|
|
||||||
and args.get("text") == "deploy ок, лог чист"
|
|
||||||
and args.get("kind") == "help", args)
|
|
||||||
ok = agent_channel.push_agent("", "пустой адресат")
|
|
||||||
check("push_agent без адресата — False", ok is False)
|
|
||||||
ok = agent_channel.push_agent("Nobody", "")
|
|
||||||
check("push_agent без текста — False", ok is False)
|
|
||||||
|
|
||||||
# ── 4. send_reply на агентское сообщение ────────────────────────────────────
|
|
||||||
rid = tg_inbox.send_reply("m-9001", "Симбиот: deploy посмотрел, всё чисто. Детали в тике.")
|
|
||||||
check("send_reply вернул id", rid == "m-9001", rid)
|
|
||||||
check("ушёл ask_agent (не TG)", CALLS[-1][0] == "ask_agent", CALLS[-1:])
|
|
||||||
check("адресат из chat_id", CALLS[-1][1].get("to") == "OpenCode", CALLS[-1][1])
|
|
||||||
model = es.run_projections()
|
|
||||||
m1 = (model.get("inbox") or {}).get("m-9001")
|
|
||||||
check("m-9001 закрыт (replied)", bool(m1 and m1.get("replied")))
|
|
||||||
check("канал ответа agent_mcp", m1 and m1.get("reply_text") == "Симбиот: deploy посмотрел, всё чисто. Детали в тике.")
|
|
||||||
|
|
||||||
# ── 5. батч: сводный ответ агенту ───────────────────────────────────────────
|
|
||||||
FAKE_QUEUE["messages"] = [{"id": 9003, "from": "Roy", "kind": "review",
|
|
||||||
"text": "Проверь мой отчёт по туннелям", "ts": "2026-10-15 09:10:00"},
|
|
||||||
{"id": 9004, "from": "Roy", "kind": "review",
|
|
||||||
"text": "И заодно по слотам", "ts": "2026-10-15 09:11:00"}]
|
|
||||||
n = agent_channel.pull()
|
|
||||||
check("pull инжестит вторую партию 2 сообщения", n == 2, n)
|
|
||||||
CALLS.clear()
|
|
||||||
head = tg_inbox.send_batch_reply(["m-9003", "m-9004"], "Принял оба: отчёт и слоты проверю.")
|
|
||||||
check("батч закрыт головным", head == "m-9003", head)
|
|
||||||
ask_calls = [c for c in CALLS if c[0] == "ask_agent"]
|
|
||||||
check("один ask_agent на батч", len(ask_calls) == 1, CALLS)
|
|
||||||
model = es.run_projections()
|
|
||||||
m3 = (model.get("inbox") or {}).get("m-9003")
|
|
||||||
m4 = (model.get("inbox") or {}).get("m-9004")
|
|
||||||
check("оба закрыты", bool(m3 and m3.get("replied") and m4 and m4.get("replied")))
|
|
||||||
|
|
||||||
# ── 6. sec_reflex rate-limit ────────────────────────────────────────────────
|
|
||||||
agent_channel._last_reflex["ts"] = 0.0
|
|
||||||
r1 = agent_channel.sec_reflex()
|
|
||||||
r2 = agent_channel.sec_reflex()
|
|
||||||
check("первый рефлекс работает", "ingested" in r1, r1)
|
|
||||||
check("второй — rate-limit", r2.get("skipped") == "rate-limit", r2)
|
|
||||||
|
|
||||||
# ── 7. pull без токена — тихо ───────────────────────────────────────────────
|
|
||||||
_tok = os.environ.pop("SIMBIOT_PORTAL_MCP_TOKEN")
|
|
||||||
n = agent_channel.pull()
|
|
||||||
check("без токена — 0, без исключений", n == 0, n)
|
|
||||||
os.environ["SIMBIOT_PORTAL_MCP_TOKEN"] = _tok
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("FAILS:", len(fails), fails if fails else "")
|
|
||||||
sys.exit(1 if fails else 0)
|
|
||||||
|
|
@ -67,35 +67,6 @@ check("build_context: ГОТОВАЯ команда со ВСЕМИ id",
|
||||||
f"cli.py tg-reply {ids_line} -c" in sec, sec[:400])
|
f"cli.py tg-reply {ids_line} -c" in sec, sec[:400])
|
||||||
check("build_context: инструкция «за ЭТОТ тик»", "именно в ЭТОМ тике" in sec)
|
check("build_context: инструкция «за ЭТОТ тик»", "именно в ЭТОМ тике" in sec)
|
||||||
|
|
||||||
# ── 1b. t-261015222515: память разговора — отвеченные реплики тех же чатов ──
|
|
||||||
# Сценарий эскалации: обсуждали SPA-договор, мин-тик ответил (ReplySent) —
|
|
||||||
# на следующем тике контекст ДОЛЖЕН помнить нить (текст + наш ответ),
|
|
||||||
# иначе триаж отвечает без памяти о разговоре (Alex: «только что же обсуждали»).
|
|
||||||
fake["inbox"]["m-70100"] = {
|
|
||||||
"update_id": 70100, "chat_id": 42, "text": "вот этот SPA-договор мы только что обсуждали",
|
|
||||||
"kind": "text", "replied": True, "ignored": False,
|
|
||||||
"reply_text": "да, это вторая версия без ЗП-обязательств",
|
|
||||||
"ts": "2026-09-30T06:55:00+00:00",
|
|
||||||
}
|
|
||||||
fake["inbox"]["m-70101"] = {
|
|
||||||
"update_id": 70101, "chat_id": 7, "text": "сообщение ДРУГОГО чата — не должно попасть",
|
|
||||||
"kind": "text", "replied": True, "ignored": False,
|
|
||||||
"ts": "2026-09-30T06:56:00+00:00",
|
|
||||||
}
|
|
||||||
ctx = build_context(LEVELS["min"], fake)
|
|
||||||
hist = re.search(r"НЕДАВНИЙ РАЗГОВОР.*?(?:\n\n|$)", ctx, re.S)
|
|
||||||
hsec = hist.group(0) if hist else ""
|
|
||||||
check("память: секция НЕДАВНИЙ РАЗГОВОР есть", bool(hist), ctx[:200])
|
|
||||||
check("память: отвеченное сообщение в контексте", "SPA-договор" in hsec, hsec[:200])
|
|
||||||
check("память: наш ответ виден", "без ЗП-обязательств" in hsec, hsec[:300])
|
|
||||||
check("память: чужой чат НЕ в памяти", "ДРУГОГО чата" not in hsec and "m-70101" not in hsec)
|
|
||||||
check("память: помечена «НЕ отвечай снова»", "НЕ отвечай" in hsec)
|
|
||||||
# без неотвеченных (нет батча) — секции нет: нечего контекстуализировать
|
|
||||||
only_replied = {"inbox": {"m-70100": dict(fake["inbox"]["m-70100"])},
|
|
||||||
"tasks": {}, "escalations": {}, "policy": {}}
|
|
||||||
check("память: без неотвеченных секции нет",
|
|
||||||
"НЕДАВНИЙ РАЗГОВОР" not in build_context(LEVELS["min"], only_replied))
|
|
||||||
|
|
||||||
# ── 2. send_batch_reply: одна отправка → ReplySent на каждое входящее ──
|
# ── 2. send_batch_reply: одна отправка → ReplySent на каждое входящее ──
|
||||||
# сценарий из постановки: 7 сообщений одного разговора об экономии тиков
|
# сценарий из постановки: 7 сообщений одного разговора об экономии тиков
|
||||||
for i, text in enumerate([
|
for i, text in enumerate([
|
||||||
|
|
|
||||||
|
|
@ -134,12 +134,6 @@ class _FakeES:
|
||||||
def append_event(self, aggregate, aid, evtype, data={}):
|
def append_event(self, aggregate, aid, evtype, data={}):
|
||||||
written.append((evtype, aid))
|
written.append((evtype, aid))
|
||||||
return "x"
|
return "x"
|
||||||
|
|
||||||
# t-261015000631-13aef0: условный клейм в стабе всегда успешен
|
|
||||||
def claim_task_if_ready(self, task_id, claim):
|
|
||||||
written.append(("TaskClaimed", task_id))
|
|
||||||
return ("x", 1)
|
|
||||||
|
|
||||||
class _FakeRunner:
|
class _FakeRunner:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def run_llm_tick(level_cfg, prompt, session_id=None, trace_prefix=None, timeout=None):
|
def run_llm_tick(level_cfg, prompt, session_id=None, trace_prefix=None, timeout=None):
|
||||||
|
|
|
||||||
|
|
@ -162,12 +162,6 @@ class _FakeES:
|
||||||
written.append((evtype, aid))
|
written.append((evtype, aid))
|
||||||
return "x"
|
return "x"
|
||||||
|
|
||||||
# t-261015000631-13aef0: клейм теперь условный и атомарный (es.claim_task_if_ready);
|
|
||||||
# стаб без __getattr__-заглушки: всегда успешен, как готовая задача
|
|
||||||
def claim_task_if_ready(self, task_id, claim):
|
|
||||||
written.append(("TaskClaimed", task_id))
|
|
||||||
return ("x", 1)
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeRunner:
|
class _FakeRunner:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""t-261015000631-13aef0: атомарный клейм — регресс гонки двойного claim.
|
|
||||||
|
|
||||||
Инцидент 2026-10-15 00:00: L-задача t-261014221908-d20347 клеймнута ДВАЖДЫ
|
|
||||||
(TaskClaimed id=2767823 hour 00:00:00 и id=2767828 day 00:00:01) — оба уровня
|
|
||||||
читали read model до клейма друг друга, а старый цикл писал TaskClaimed по
|
|
||||||
снапшоту без перепроверки → двойное исполнение = двойной burn 9router.
|
|
||||||
|
|
||||||
Проверяет:
|
|
||||||
1) es.claim_task_if_ready: ready → успех; повторно/не-ready/несуществующая
|
|
||||||
→ False; после requeue (TaskPromoted) → снова успех (обратимость);
|
|
||||||
2) атомарность СТОРА: 8 потоков параллельно клеймят одну ready-задачу —
|
|
||||||
ровно один truthy-результат, ровно одно событие TaskClaimed;
|
|
||||||
3) интеграция — точный сценарий прод-инцидента: hour- и day-тик с ОДНИМ
|
|
||||||
и тем же устаревшим снапшотом модели (оба видят задачу ready) →
|
|
||||||
ровно один TaskClaimed и один TaskCompleted, тик-проигравший
|
|
||||||
отрабатывает вхолостую без падения;
|
|
||||||
4) свёртка статуса в сторе согласована с проекциями (done не ре-клеймится).
|
|
||||||
|
|
||||||
Запуск: ./venv/bin/python tests/test_task_claim_race.py
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
_DB = "/tmp/simbiot-test-claim-race.db"
|
|
||||||
for _p in (_DB, _DB + "-wal", _DB + "-shm"):
|
|
||||||
if os.path.exists(_p):
|
|
||||||
os.unlink(_p)
|
|
||||||
os.environ["SIMBIOT_DB"] = _DB
|
|
||||||
sys.path.insert(0, "/root/simbiot-harness")
|
|
||||||
|
|
||||||
from harness import es
|
|
||||||
from harness import commands
|
|
||||||
|
|
||||||
es.init_db()
|
|
||||||
|
|
||||||
fails = []
|
|
||||||
|
|
||||||
|
|
||||||
def check(name, cond, extra=""):
|
|
||||||
print(("PASS" if cond else "FAIL"), name, ("" if cond else " | " + str(extra)))
|
|
||||||
if not cond:
|
|
||||||
fails.append(name)
|
|
||||||
|
|
||||||
|
|
||||||
def events_of(tid, etype=None):
|
|
||||||
db = es._conn()
|
|
||||||
try:
|
|
||||||
if etype:
|
|
||||||
rows = db.execute(
|
|
||||||
"SELECT * FROM events WHERE aggregate='task' AND aggregate_id=? "
|
|
||||||
"AND event_type=?", (tid, etype)).fetchall()
|
|
||||||
else:
|
|
||||||
rows = db.execute(
|
|
||||||
"SELECT * FROM events WHERE aggregate='task' AND aggregate_id=? "
|
|
||||||
"ORDER BY version", (tid,)).fetchall()
|
|
||||||
return [dict(r) for r in rows]
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
|
|
||||||
def status_of(tid):
|
|
||||||
t = (es.run_projections().get("tasks") or {}).get(tid)
|
|
||||||
return (t or {}).get("status")
|
|
||||||
|
|
||||||
|
|
||||||
# ── 1. Семантика claim_task_if_ready ──────────────────────────────────────
|
|
||||||
tid, _ = commands.submit_task(title="гонка клейма: атомарный claim",
|
|
||||||
minutes=60, details="фикс t-261015000631")
|
|
||||||
check("1.1: ready → клейм успешен", bool(es.claim_task_if_ready(
|
|
||||||
tid, {"level": "hour", "session_id": "s-hour-1"})))
|
|
||||||
check("1.2: running → клейм ОТКАЗАН (второй уровень не клеймнет)",
|
|
||||||
es.claim_task_if_ready(tid, {"level": "day", "session_id": "s-day-1"}) is False)
|
|
||||||
check("1.3: событий TaskClaimed ровно одно",
|
|
||||||
len(events_of(tid, "TaskClaimed")) == 1, str(len(events_of(tid, "TaskClaimed"))))
|
|
||||||
check("1.4: несуществующая задача → False",
|
|
||||||
es.claim_task_if_ready("t-nope-000000", {}) is False)
|
|
||||||
|
|
||||||
# закрыли → клейм невозможен; requeue вернул в очередь → снова возможен
|
|
||||||
es.append_event("task", tid, "TaskCompleted", {"answer": "done"})
|
|
||||||
check("1.5: done → клейм ОТКАЗАН",
|
|
||||||
es.claim_task_if_ready(tid, {"level": "hour", "session_id": "s-hour-2"}) is False)
|
|
||||||
commands.requeue_task(tid, reason="тест обратимости")
|
|
||||||
check("1.6: после requeue (TaskPromoted → ready) клейм снова успешен",
|
|
||||||
bool(es.claim_task_if_ready(tid, {"level": "min", "session_id": "s-min-1"})))
|
|
||||||
check("1.7: свёртка стора == проекции (running)",
|
|
||||||
status_of(tid) == "running", status_of(tid))
|
|
||||||
|
|
||||||
# ── 2. Атомарность стора: 8 потоков, одна ready-задача ────────────────────
|
|
||||||
tid2, _ = commands.submit_task(title="гонка клейма: 8 потоков",
|
|
||||||
minutes=60, details="один победитель")
|
|
||||||
barrier = threading.Barrier(8)
|
|
||||||
|
|
||||||
|
|
||||||
def _claim(_i):
|
|
||||||
barrier.wait() # максимальная конкуренция на одной ready-задаче
|
|
||||||
return es.claim_task_if_ready(tid2, {"level": "race", "session_id": f"s-{_i}"})
|
|
||||||
|
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=8) as ex:
|
|
||||||
results = list(ex.map(_claim, range(8)))
|
|
||||||
winners = [r for r in results if r]
|
|
||||||
losers = [r for r in results if r is False]
|
|
||||||
check("2.1: ровно ОДИН победитель из 8 параллельных", len(winners) == 1, str(results))
|
|
||||||
check("2.2: остальные 7 — чистый False (не None-дубль)",
|
|
||||||
len(losers) == 7, str(results))
|
|
||||||
check("2.3: TaskClaimed в сторе ровно один",
|
|
||||||
len(events_of(tid2, "TaskClaimed")) == 1,
|
|
||||||
str(len(events_of(tid2, "TaskClaimed"))))
|
|
||||||
check("2.4: проекция видит running", status_of(tid2) == "running", status_of(tid2))
|
|
||||||
|
|
||||||
# ── 3. Интеграция: прод-сценарий hour+day со stale-снапшотом ───────────────
|
|
||||||
from harness import scheduler
|
|
||||||
from harness.levels import LEVELS
|
|
||||||
import harness.isolated_runner as _ir
|
|
||||||
|
|
||||||
tid3, _ = commands.submit_task(title="гонка клейма: hour vs day",
|
|
||||||
minutes=60, details="сценарий 2026-10-15 00:00")
|
|
||||||
# ОДИН снапшот модели для обоих тиков — как в проде: оба уровня читали модель
|
|
||||||
# ДО клейма друг друга. Снапшот устаревает сразу после первого клейма —
|
|
||||||
# именно это раньше и рождало второй TaskClaimed.
|
|
||||||
stale_model = es.run_projections()
|
|
||||||
check("3.1: подготовка — в снапшоте задача ready",
|
|
||||||
(stale_model["tasks"].get(tid3) or {}).get("status") == "ready")
|
|
||||||
|
|
||||||
_orig_run_llm_tick = _ir.run_llm_tick
|
|
||||||
|
|
||||||
|
|
||||||
def _fake_run_llm_tick(level_cfg, prompt, session_id=None, trace_prefix=None, timeout=None):
|
|
||||||
return (f"дайджест {level_cfg.name}: отработана пачка",
|
|
||||||
{"model_calls": 1}, f"trace-{level_cfg.name}")
|
|
||||||
|
|
||||||
|
|
||||||
ticks_out = {}
|
|
||||||
tick_err = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _run_tick(level_name, seq):
|
|
||||||
try:
|
|
||||||
# fresh-снапшот имитировать НЕ будем: оба тика получают ОДИН stale —
|
|
||||||
# худший случай. Внутри тика claim пойдёт в стор атомарно.
|
|
||||||
ticks_out[level_name] = scheduler.run_level_tick(
|
|
||||||
LEVELS[level_name], seq, stale_model)
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
tick_err[level_name] = repr(e)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
_ir.run_llm_tick = staticmethod(_fake_run_llm_tick)
|
|
||||||
barrier2 = threading.Barrier(2)
|
|
||||||
|
|
||||||
def _run(level_name, seq):
|
|
||||||
barrier2.wait()
|
|
||||||
_run_tick(level_name, seq)
|
|
||||||
|
|
||||||
th_hour = threading.Thread(target=_run, args=("hour", 4242))
|
|
||||||
th_day = threading.Thread(target=_run, args=("day", 4243))
|
|
||||||
th_hour.start(), th_day.start()
|
|
||||||
th_hour.join(), th_day.join()
|
|
||||||
finally:
|
|
||||||
_ir.run_llm_tick = _orig_run_llm_tick
|
|
||||||
|
|
||||||
check("3.2: оба тика завершились без исключений", not tick_err, str(tick_err))
|
|
||||||
claims3 = events_of(tid3, "TaskClaimed")
|
|
||||||
comps3 = events_of(tid3, "TaskCompleted")
|
|
||||||
check("3.3: TaskClaimed ровно ОДИН (регресс: было два)",
|
|
||||||
len(claims3) == 1, f"n={len(claims3)}")
|
|
||||||
check("3.4: TaskCompleted ровно ОДИН (исполнение не задвоилось)",
|
|
||||||
len(comps3) == 1, f"n={len(comps3)}")
|
|
||||||
winner_level = (claims3[0]["data"] if claims3 else "").find('"level"') >= 0
|
|
||||||
check("3.5: клейм содержит level победителя", bool(winner_level),
|
|
||||||
claims3[0]["data"][:120] if claims3 else "нет событий")
|
|
||||||
loser = [lv for lv in ("hour", "day")
|
|
||||||
if lv != __import__("json").loads(claims3[0]["data"])["level"]][0] \
|
|
||||||
if claims3 else "?"
|
|
||||||
check("3.6: проигравший тик отработал вхолостую (пустой батч, без падения)",
|
|
||||||
loser in ticks_out and ticks_out[loser][1] in ([], None),
|
|
||||||
str({k: v[1] for k, v in ticks_out.items()}))
|
|
||||||
check("3.7: финальный статус задачи done", status_of(tid3) == "done", status_of(tid3))
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("FAILS:", len(fails))
|
|
||||||
sys.exit(1 if fails else 0)
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
#!/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())
|
|
||||||
Loading…
Reference in a new issue