Skip to content
smile-skPublic

About

An organism, not a script: body is a real C. elegans connectome (302 neurons, 3004 synapses), brain is a local LLM, life is an energy model. Runs missions, feels impulses, reports back. Pure Python, standard library only.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

Synapse

Отдельный организм: тело — отсканированная нервная система (коннектом червя, позже мухи), мозг — LLM (сейчас qwen2.5-0.5b q4_k_m, старше — 1.5b/12b по замеру).

Не часть AI_UI и не зависит от него: своя программа запуска, своя панель-окно, свой llama-server, свой мозг-модель внутри проекта, свои логи. Из AI_UI пришло только тело (worm.json) — копией, в свой дом, как и модель.

Зачем

  • Понять на практике, что даёт настоящая нервная система, отточенная эволюцией, когда к ней подключают языковую модель. Игрушка — первая фаза, а не приговор.
  • Дать оператору живой инструмент: он ставит миссии через панель, организм их исполняет и отчитывается. Первая миссия — аудит своей сети («где у меня что открыто и куда я вообще дотягиваюсь»).

Три слоя

слой что это чего стоит
1. рефлексы импульс по коннектому, body.step() — тело живёт само бесплатно, модель не нужна
2. мозг 0.5b q4_k_m заметить, назвать, напомнить, коротко решить локальный GPU/CPU
3. старший внешний вызов, когда 0.5b не тянет (12b, позже) дорого, по нужде

Честная оговорка: 0.5b — «дежурный», а не «решающий». Его сила — быть всегда на месте и не выдумывать лишнего. Рассуждать за старшего он не станет: на прямой вопрос «кто ты» отвечает «Я — Synapse, червя» — это и есть его потолок, и место для 12b уже подготовлено тем же самым кодом.

Апгрейд мозга проверен, а не обещан (26.09.2026): q2_k заменён на q4_k_m (тот же размер, качество выше), а 1.5B q4_k_m лежит рядом в models/brain/ и просто выбирается в окне.

Честные замеры (llama-bench на 940MX; pp128 — разбег промпта, tg32 — ответ):

модель -ngl разбег, ток/с ответ, ток/с
0.5b q4_k_m 10 217 15.5
0.5b q4_k_m 99 340 33.8
1.5b q4_k_m 10 83 3.0
1.5b q4_k_m 99 129 15.7

Отсюда две вещи, которые важнее выбора модели. Первая: -ngl 10 — ловушка для 1.5b (3.0 ток/с против 15.7 при полной загрузке); для 0.5b та же граница мягче. Вторая: моё прежнее число «2.3 t/s живьём» было ошибкой замера — я сравнила чистую генерацию (llama-bench) с целым ходом через сервер, то есть с загрузкой модели и разбегом промпта. Живой замер через настоящий сервер сейчас: разбег 122 токена на 153 ток/с, ответ 64 токена на 13.3 ток/с. Значит «он овощ» — не про скорость, а про качество 0.5b: сам промпт думания сложнее, чем малый мозг умеет (0.5b зацикливается на 💭 или эхом возвращает промпт, хотя на прямой вопрос отвечает по делу). Поэтому следующий шаг — короткий промпт для малого мозга, а не «модель побольше».

Тело

data/body/worm.json — формат connectome-graph/1: 302 узла, 3004 связи, у каждого нейрона имя (ADAL, AVAR…), координаты, вес и нейромедиатор. Это C. elegans — единственный организм, чья нервная система отсканирована целиком.

Импульс живёт по закону act ← eff·act + gain·Σ(ЗНАК источника · доля связи · act источника), где eff = min(0.95, decay + sustain·энергия), а всё, что ниже порога, гаснет. Знак источника — из его медиатора: тормозной нейрон ВЫЧИТАЕТ, а не «возбуждает слабо» (см. «Физиология передачи» ниже). Умолчание body.nt.enabled=true; с false тело ведёт себя ровно как до физиологии — это и есть контроль в тестах. Числа подобраны замером, а не на глаз. Метрика всюду одна: пик — сколько узлов было выше порога 0.02 в самый яркий такт, «гаснет» — на каком такте не осталось ни одного. Текст стимула влияет (seed = crc32 текста: разные нейроны — разная волна), поэтому в таблице пять текстов, а точные точки закреплены тестом tests/test_qa_fixes.py. Замеры обновлены 25.09.2026: прежние числа не воспроизводились — это нашёл независимый QA (Орест), и был прав.

decay gain порог что вышло
0.70 0.30 0.02 рабочее: k=3 — пик 9–18, гаснет за 26–47 тактов; k=8 — пик 30–46, гаснет за 48–110
0.70 0.30 0.02 k=30 (в окне недостижимо — там максимум 6) — пик 222–231 и не гаснет: срывается и рабочее
0.75 0.35 0.02 припадок: пик 16–200 и не гаснет (на k=3 — 19, никогда)
0.72 0.55 0.02 припадок: 208–226 зажжены, не гаснет
0.75 0.35 0.005 не гаснет вообще: порог и есть смертность

Вывод, который стоит помнить: тело срывается, когда decay + gain > 1 (0.70 + 0.30 = ровно 1.0 — это граница, и на ней тело живёт, пока вход не слишком сильный). Живое ≠ возбуждённое до конца.

Честно про sustain (нашла 25.09.2026, разбирая QA-отчёт). В формуле eff = min(0.95, decay + sustain·энергия) это коэффициент СОХРАНЕНИЯ (act ← eff·act), а не скорость затухания. Значит sustain сохранение увеличивает: это САМОПОДДЕРЖКА — чем активнее нейрон, тем дольше он себя держит. Замер на восьми стимулированных нейронах: sustain 0 — гаснет за 43 такта, sustain 1.2 (умолчание) — за 48–110, sustain 4 — припадок (пик 219, не гаснет). Настоящая страховка — decay и порог. Переименовано в sustain по слову Стаса. Мой первый заход (v0.1) зажимал тормоз в 1.0, а eff = 1.0 значит «не гаснет вообще», то есть тормоз работал ровно наоборот; зажим теперь 0.95.

Физиология передачи (с 25.09.2026)

Слово Стаса: «конечно делай всё по настоящему, мы же не для декораций делаем». До этого медиатор влиял только на краску: волна шла по весам связей, и тормозной нейрон вёл себя как возбуждающий. Теперь знак берётся из ДАННЫХ — из медиатора самого нейрона (synapse/pharmacology.py), а не из таблицы «на глаз».

кто сколько что делает откуда известно
GABA — тормозит 26 ВЫЧИТАЕТ из возбуждения целей (гиперполяризация) DD1–DD6 и VD1–VD13 — тормозные мотонейроны мускулатуры тела, RME — мышц головы, AVL/DVB — кишки, RIS — сна
ацетилхолин 129 возбуждает: моторные нейроны → мышцы основной возбуждающий медиатор C. elegans
глутамат 71 в графе нейрон-нейрон возбуждает; у нервно-мышечного соединения тормозит через GluCl, но мышц в нашем графе нет переключается настройкой body.nt.glutamate_inhibitory
моноамины: серотонин 7, дофамин 8, октопамин 2 17 не проводят быстрый ток: меняют ВОЗБУДИМОСТЬ тех же целей (тонус) объёмная передача, метаботропные рецепторы
медиатор не назван атласом 59 нейтральный знак возбуждения, честно серые неизвестность не повод выдумывать торможение

Числа — замер, а не вкус (метрика та же: пик и «гаснет»):

что мерили физиология ВЫКЛ (контроль) физиология ВКЛ
рабочая точка, k=3 (текст alpha) пик 16, гаснет 29 пик 16, гаснет 28
рабочая точка, k=8 пик 46, гаснет 48 пик 45, гаснет 45
импульс только в 26 GABA-узлов цели возбуждены: 11.64 цели почти молчат: 1.99
порог срыва (максимальный k, что ещё гаснет) 14 16 с одним торможением · 14 с торможением и тонусом
k=6 после серотонина / после дофамина 44 / 44 (медиатор ни при чём) 46 (бодрость держит) / 41 (замедление гасит)
залили тонус 0.4 всем узлам — тело молчит через 30 тактов (тонус не держит его живым)

Три вывода, которые стоит помнить.

  • Торможение тело УКРЕПЛЯЕТ. Одна настоящая GABA поднимает порог срыва с 14 до 16: то, что раньше уходило в припадок, теперь само себя останавливает.
  • Тонус работает не током, а возбудимостью. Первая версия ДОБАВЛЯЛА тонус как ток — и тело залипало: серотониновые NSM держали петлю вечно (замер: k=3 не гас НИКОГДА). Моноамины действуют через метаботропные рецепторы: сами канал не открывают, а меняют усиление. Поэтому тонус множит СОХРАНЕНИЕ, а оно остаётся < 0.95 — и структурно не может держать тело живым без импульса.
  • Цена. Такт тела: 1.79 мс без физиологии, 2.63 мс с полной (+0.84 мс), а одно торможение даже дешевле контроля (1.59 мс) — 17 модуляторов перестали гонять быстрый ток по 325 связям.

Настройки (вкладка «Настройки» их не трогает — это data/config.json, раздел body.nt): enabled (весь знак), modulation (тонус моноаминов), inhibitory_gain (во сколько раз торможение сильнее единицы связи), glutamate_inhibitory, tone_strength (0.10), tone_decay (0.92), tone_cap (0.20). Видно это в окне: в раскраске по медиаторам легенда пишет не только цвет, но и что медиатор делает («GABA · тормозной 26», «Dopamine · модулирующий 8»), а под легендой стоит строка передачи; в карточке узла — «передача: тормозной».

Точная карта нейронов (с 24.09.2026)

Ролей больше не «эвристика по именам». tools/build_roles.py собирает карту data/body/neuron_roles.json из выгрузки OpenWorm (owmeta v0.12.3, файл ConnectomeLab/data/owmeta_cache.json): типы, медиаторы, рецепторы — на все 302.

роль сколько кто это
motor 120 VA/VB/VC/VD/DA/DB/DD, AS, RMD/RME/RMF/RMH, SMD/SMB, M1–M5…
sensory 84 ADF/ADL/AFD/ASE/ASG, AWA/AWB/AWC, ALM/AVM/PLM/PVM, PVD, BAG, IL2, CEP…
inter 69 AV*, PV*, RI*, AI*, DVA?, RIM/RIV/RID…
polymodal 27 IL1, M3, MC, NSM, URA, VB — атлас говорит «motor + sensory»
other 2 CANL/CANR — клетки без синапсов, честно помечены, а не выдуманы

Зоны тела (проверено по атласу, а не на глаз): голова 211, середина 32, хвост 59. Грабля знака: в этой выгрузке голова — там, где отрицательный y (амфидные ADLL/ASEL/AWAL при y≈−0.44, хвостовые PHCL/PLML при y≈+1.43). Первый заход взял ось наоборот и выдал «210 хвостовых» — ошибка знака, а не биология.

Хабы — командные вставочные, и они же самые связные узлы коннектома: AVAL (79 связей), AVAR (72), RIAL (63), RIAR (62), AVDR (60), DVA (60).

Полимодальные узлы (IL1/M3/MC/NSM/URA/VB) входят и в сенсорный набор, и в «руки» — поэтому стимул бьёт и по ним, а body.pool("motor") = 147 узлов, "sensory" = 111.

Панель-окно (графический дашборд)

python run.py            # или: python run.py panel
Synapse.pyw              # двойной клик — окно без чёрной консоли

Что в окне: живое тело (крутится мышью, приближается колесом, клик по узлу — его карточка, и выбранный узел при этом вспыхивает белым), кнопки жизни (Жить / Пауза / Такт / Импульс / Сброс вида), раскраска по ролям, по медиаторам или по зонам тела, вкладки Миссии, Журнал (аудит), Мысли (внутренняя речь 💭), Отчёты, Мозг (включить свой сервер, скопировать модель к себе, спросить), Настройки.

Под телом две статусные строки (FixedLine, высота задана раз и навсегда): строка ответа — что вышло от последнего нажатия, и цифры тела. Место под карточку узла зарезервировано, но до клика она ПУСТА: подсказок-заглушек под кнопками нет (слово 26.09.2026: «убери кучу переносов и ненужных подсказок под кнопками и графикой — оставить только 2 статусные строки»). Карточка переносит текст (FixedLine(wrap=True)), поэтому её читаешь целиком, а не по одной обрезанной строке. Повод у фиксированной высоты — жалоба 26.09.2026: «постоянно в каждый тик что то моргает в статусной строке под графическим экраном, смещая все строки ломая на секунду весь интерфейс»: подписи были с переносом слов, длина менялась каждый такт, и Qt пересобирал всю левую колонку.

Легенда цветов переехала во вкладку «Настройки» (группа «Цвета тела»): «оно не нужно постоянно смотреть на неё», и под телом она ещё и смещала вид.

Как дать миссию (с 24.09.2026 — понятной формой)

Стас сказал: «как давать миссии я не понял — писать вручную?». Поэтому форма говорит сама за себя:

  1. Что сделать — инструмент выбирается КОМАНДОЙ, как он и записан в очереди: net_audit, ask, remote, stimulus, note, write_probe, certs, diff (рядом — короткое пояснение по-английски).
  2. Поля включаются по делу: аудиту нужна подсеть, остальным — текст задания; лишнее поле гаснет, а не притворяется нужным.
  3. Под формой — подсказка, что инструмент делает, и проверка ДО постановки: «ready to queue» или «subnet required / the door says no: …». Отказ виден ДО нажатия, а не в журнале после.
  4. Run now — поставит в очередь и сразу сделает такт; Queue — оставит ждать ближайшего такта; My network — подставит подсеть этой машины; Delete selected — убрать выбранную.
  5. Таблица внизу — очередь и история (статусы queued / running / done / failed / refused), а под ней полный итог выбранной миссии.

Из консоли то же самое: python run.py mission add --kind net_audit --cidr ....

Отчёты человеческим языком (с 24.09.2026)

Стас: «отчёты не читаемы — я ничего в них не понимаю». Сырой JSON остаётся для кода, а рядом ложится <то же имя>.txt, который читается глазами: когда и что сканировали, сводка по службам («445 SMB — общие папки Windows: 4 хоста»), каждый хост по одному с переводом баннеров («SSH назвался: OpenSSH_7.4», «ответ: 302 Moved Temporarily»), общие папки именами и раздел «что это значит».

Честность зашита в текст, а не подразумевается: «права НЕ проверялись» и «проба „куда есть доступ на запись“ НЕ запускалась — нужно твоё отдельное слово». В списке отчётов — подпись («аудит сети 192.168.1.0/24 · живых 7 · 24.09 16:26»), а не имя файла; кнопка Открыть файлом показывает .txt системным просмотрщиком. Из консоли: python run.py reports, reports --show 1, reports --rebuild.

Отрисовка: тело рисует видеокарта (с 24.09.2026, замеры)

Панель управления живёт на видеокарте: synapse/qt_panel.py — Qt + OpenGL (PySide6). Линии связей и точки нейронов рисует GPU, цвет и размер приходят на каждую вершину.

Правильный интерпретатор — сам (с 25.09.2026, это была настоящая поломка)

Двойной клик по Synapse.pyw идёт через C:\WINDOWS\pyw.exe и запускал окно на C:\Python314\pythonw.exe, где нет ни PySide6, ни Pillow. Панель молча уходила на tkinter-путь и рисовала тело canvas-ом — то есть Qt-панель на видеокарте в запуске оператора вообще не участвовала. Отсюда и «тормоза такие же» (замер 25.09).

Теперь synapse/launcher.py спрашивает интерпретатор, есть ли в нём PySide6, и если нет — ищет такой (конфиг → PATH → локальные установки) и перезапускает окно им. Выбор помнит в конфиге. Нет Qt нигде — окно откроется запасным путём, но в журнале будет panel.fallback с причиной, а не тишина. Раздел конфига:

"ui": {"prefer": "qt", "python": "", "python_candidates": []}

prefer: tk — не искать Qt вовсе; python — готовый путь к интерпретатору с Qt; python_candidates — где ещё искать (сейчас там venv соседнего проекта, это мостик).

Замер, который определил устройство (24.09.2026):

что меряем было (tkinter) стало (Qt + OpenGL)
кадр при 1366x768 20 мс на кадр, 27–28 кадр/с 4–7 мс, 43 кадр/с
кадр при 1920x1080 56 мс (рисование 29 + передача картинки 11 + перерисовка 16) 40 кадр/с, отрисовка на GPU 0.32–0.47 мс
цена от размера окна растёт линейно по пикселям почти не зависит: пиксели платит GPU

Главная находка замера: дорогая была не видеокарта, а цикл Python. Первый заход дал build 16.3 мс / draw 0.3 мс. Поэтому положения вершин собираются РЕДКО (поворот, зум, ресайз), а каждый кадр точечно обновляется только цвет и яркость — и только у изменившихся связей. Цвет нагрева — таблица из 33 ступеней вместо разбора hex-строк 900 раз за кадр.

Кнопки: Live / Pause / Step / Pulse / Reset view, раскраска по ролям, трансмиттерам, зонам. Вкладки: Missions / Journal / Reports / Brain / Settings. Клик по нейрону — карточка узла. Такт с миссией и мозгом идёт в фоновом потоке: окно дышит, пока модель думает. У каждой кнопки есть подсказка при наведении.

Язык, тема и вкладка настроек (с 25.09.2026, v0.10)

Причина — слово Стаса: «сделай темную тему… сделай уже с локалями, как обычно i18n… какой то хаус у нас с языком вышел». Так и было: русские строки лежали прямо в коде панели, отчётов и речи существа, поэтому одна переведённая подпись не делала погоды.

Правило языка, железное: техническое — ВСЕГДА английское и через словарь не проходит; интерфейс — переводится. К первому относятся имена команд (net_audit, write_probe, certs, diff, stimulus, note, ask), состояния миссий (queued, running, done, failed, refused), события журнала и его поля, ключи настроек, режимы мозга (off, server), имена файлов. Оператор видит ровно то слово, которое лежит в очереди миссий, и может искать по нему. Ко второму — заголовки, кнопки, подписи полей, подсказки, текст отчётов, «что это значит» и речь самого существа.

Где живёт язык: synapse/i18n.py — один словарь на весь проект (ключ → английский, русский). Нет ключа — t() возвращает САМ КЛЮЧ (на кнопке будет btn.live), а не пустоту: ошибку видно глазами, а не прячем. Английский — язык по умолчанию, русский переключается во вкладке настроек или строкой ui.lang в data/config.json.

Отчёты и речь следуют языку интерфейса. JSON — истина, .txt — снимок для глаз; поэтому панель рисует отчёт ЗАНОВО из JSON на текущем языке, а готовый .txt открывает только если JSON уже нет. Иначе после смены языка старые отчёты остались бы на прежнем — тот самый хаус.

Окно вписывается в ЛЮБОЙ экран, а не диктует экрану размер (с 26.09.2026, v0.20.1)

Слово Стаса: «ты ерунду какую-то сделала - теперь у меня окно в экран не влазит. при том что FullHD - сделай нормально универсально под любой экран».

Причина — замер, а не догадка: окно требовало минимум 2246x765 при FullHD 1920. Виноват не «сложный интерфейс», а закон Qt: у QLabel БЕЗ переноса слов minimumSizeHint() равен ширине ВСЕГО текста. Подпись тела просила 1488 px, строка ответа — 960, а с полотном тела (430) выходило 2246: окно физически не могло стать уже экрана. (Строки, сделанные «ровными по высоте» в v0.20.0, распёрли окно по ШИРИНЕ.)

Что теперь:

  • подписи просят 72 px и забирают свободную ширину целиком (QSizePolicy.Ignored), лишнее режется многоточием, полный текст — в подсказке при наведении;
  • synapse/window_fit.py — вписание в доступную площадь экрана (за вычетом панели задач), окно ставится по центру; аргумент --size тоже вписывается, а не приказывает;
  • каждая страница вкладок в прокрутке: «Настройки» требовали 737 px по высоте САМИ ПО СЕБЕ и становились минимумом всего окна (на 1366x768 доступно ~728);
  • запасная Tk-панель по тому же правилу (было жёсткое 1180x760).

Минимум окна: 2246x765 → 467x358 — влезает и в FullHD, и в 1024x768 (проверено на четырёх сочетаниях языка и темы). Проверяет tests/test_window_fits.py, контроль первым: обычный QLabel на том же тексте по-прежнему просит 888 px, поэтому «стало 589» не зелено по умолчанию.

Кнопки отвечают, импульс видно, цвета объяснены (с 25.09.2026, v0.10.3)

Причина — слово Стаса: «что делают кнопки под графическим окном? я на них нажимаю и ничего не происходит. что за роли цвета? ничего не меняется. Жить — значит включить и потом пауза остановить?».

Кнопки работали МОЛЧА. В запасной tkinter-панели каждое действие писалось в лог, а в Qt-панели (её Стас и видит) ответа в окне не было НИ ОДНОГО: такт правда брал миссию и писал в журнал, но на экране не менялось ничего. Теперь на каждое нажатие есть строка ответа:

кнопка что она делает что появляется в строке
Жить / Пауза включает и выключает ТАКТ по расписанию (миссии и думание) «живу: такт каждые 60 с — беру миссии и думаю. Тело дышит всё время» / «пауза: тактов и миссий нет. Тело продолжает дышать»
Такт один шаг прямо сейчас «tick 187: …» или «mission m-…: done — …»
Импульс впрыснуть импульс в сенсорные нейроны «импульс телу: зажжено 6/302…; громче всех ASGR(1.0), BAGL(1.0)…»
Сброс вида вернуть угол и масштаб «вид сброшен»
цвет: роли / медиаторы / зоны чем красить нейроны «раскраска: зоны» + легенда меняется

Ответ на его вопрос про Жить: да — кнопка включает такт, а «Пауза» его останавливает. Тело при этом живёт ВСЕГДА: дыхание — это рефлексы коннектома, а не расписание. Так и написано в подсказке и в строке ответа, чтобы не приходилось догадываться.

Импульс был невидим — замер: вдох тела шёл каждые 10 кадров (16 мс → 0.16 с), тело мерцало без пауз (в среднем 18 зажжено из 302, до 31), а импульс поднимал всего +10 нейронов и гас за 0.16 с. Теперь вдохи редкие и слабые (body.breathe_every = 90 кадров, breathe_amp = 0.55): за 400 кадров спокойны 330, максимум 9 зажжено. А задетые импульсом узлы помечаются вспышкой (body.flash_seconds = 1.6): белые и вдвое крупнее (радиус 6.2 против 3.0).

Легенда цветов (PanelCore.legend()) живёт во вкладке «Настройки» (с 26.09.2026, v0.20.0): плашка того самого цвета, которым нарисованы узлы, подпись и их число. Меняется вместе с раскраской, переводится вместе с языком, и строится по данным, а не по палитре файла тела — иначе 59 нейронов без медиатора (серые) оставались без объяснения, а медиатор с нулём узлов занимал строку.

Тема — одна палитра на всё окно (synapse/theme.py): тёмная (по умолчанию) и светлая. Раньше окно Qt было светлым, а полотно тела тёмным — панель выглядела сборной из двух разных программ. Теперь палитра одна и на виджеты (QSS), и на тело: фон полотна, цвет связей, цвета ролей. Палитра медиаторов приходит ИЗ ФАЙЛА тела и настроена на тёмный фон — на светлой теме theme.adapt() её приглушает.

Вкладка «Настройки» — то, что раньше приходилось править руками в config.json: язык, тема, темп кадра тела, такт (0 = поток: мысль идёт за мыслью в темпе самой модели; число = такт, и он же темп мыслей), «думать вслух», разрешение тесту записи, диапазон портов двумя числами (от / до), мозг (модель, счёт, слоёв на GPU, порт) и цвета тела (легенда). Язык и тема применяются СРАЗУ, не закрывая окно (все подписи ставятся из словаря заново); числа проверяются, и о плохом сказано с сохранением прежнего значения, а не «упало».

Мозг выбирается в окне (v0.20.0). Модели читаются из собственной папки существа (models/brain/*.gguf): положил туда файл — он появился в списке (так и лежат рядом 0.5b и 1.5b). Счёт — «видеокарта» или «процессор» (brain.device), слои на GPU (brain.ngl), порт. Команда сервера собирается одним местом (Brain.argv()), поэтому её видно тестом, не поднимая сервер. Раньше -ngl и -c жили только внутри server_args, и оператор правил их руками; теперь это свои ключи, а старые значения переносятся при чтении (config._migrate_brain_args) — поведение не меняется, меняется только способ правки. Смена модели гасит свой сервер и прямо говорит, что поднять его надо заново: модель и счёт читаются при запуске.

Вкладка «Мысли» наполняется (v0.20.0). Повод — «сделала вкладку Мысли — но там пусто». Причина: канал 💭 наполнялся ТОЛЬКО метками самой модели, а 0.5b их ставить не умеет — файла thoughts.log не было вовсе. Но в потоке ответ модели И ЕСТЬ её думание вслух: теперь он идёт в канал мыслей (audit.thought), копии-эхо — нет. Живой прогон: три такта → две строки в канале.

Поля вкладки показывают ФАЙЛ, а не умолчания (v0.15.0, 25.09.2026). Раньше они заполнялись только кнопкой «Перечитать» и после сохранения, а при рождении окна в них стояли умолчания (язык en, «думать вслух» выключено) — оператор жал «Сохранить» и записывал умолчания как свой выбор. В живом журнале это видно: settings.saved changed=2 в 16:28:56, язык en при русском окне. Теперь настройки читаются при старте и при каждом открытии вкладки.

Дверь очереди: чужое имя команды не входит, итог говорит на языке окна (с 25.09.2026)

Повод — жалоба «"такого_нет" ещё глаз мозолит». Это оказалась НЕ строка языка, а МУСОР В ЖИВЫХ ДАННЫХ: 39 записей в рабочей очереди с придуманным именем команды, которое я сама гоняла, проверяя отказ. Причина — missions.add() не проверял имя вообще (панель проверяла, консоль нет), а список инструментов жил в ДВУХ местах — в агенте и в ядре панели.

  • Один список инструментов — missions.KINDS. Агент, панель и консоль берут его оттуда же, поэтому новый инструмент появляется везде сам, а «забыть» его в панели больше нельзя.
  • Проверка ДО записи — как scope.py для сети: незнакомое имя в очередь не ложится, консоль отвечает отказом и выходит с кодом 2. Строку с чужим именем, оставшуюся от прежней версии, агент по-прежнему честно помечает refused — путь отказа закрыт тестом.
  • Итог миссии — на языке окна. В очереди лежит строка, написанная в тот день и на том языке; для миссий с ОТЧЁТОМ панель собирает её заново из JSON (reports.find_for_mission) — как перерисовывает человеческий .txt. Что пересобрать нельзя (стимул телу, ошибка инструмента), показывается как записано: не выдумываем.
  • Проза инструментов ушла в словарь. «дверь не пустила», «нет аудита сети — сначала net_audit» и ещё полтора десятка фраз были вписаны прямо в код certs/diff/write_probe/ net_scan: панель переведёшь, а отчёт всё равно русский. Теперь это ключи err.*, src.*, wp.*. Русским в инструментах осталось только то, что и должно: разбор русского вывода ipconfig и net view.
  • Тесты больше не трогают живые данные. Найдено тем же поиском: тесты писали в рабочие data/ — мусор в очередь, «test.event» и настоящие такты в журнал, и даже крутили state.json живого организма (счётчик тактов рос от прогонов). Теперь tests/live_data.py уводит очередь, оба журнала, ОТЧЁТЫ и состояние во временную папку, а tests/conftest.py делает это АВТОМАТИЧЕСКИ для каждого теста — чтобы новый тест не мог забыть про песочницу (именно так и вышло test_tools_v07: он писал brain.thought text=мысль в живой журнал, 6 записей). Проверено замером: после полного прогона живые файлы не меняются ни на байт.

Инструменты: восемь команд (v0.7–v0.24)

команда что делает пишет на чужую машину?
net_audit обойти свою подсеть: живые хосты, порты, баннеры, имена общих папок нет, только смотрит
certs рукопожатие TLS и срок сертификата: кому выдан, кем, сколько дней осталось нет, только чтение
write_probe тест «куда есть доступ на запись»: один файл-маркер, прочитать обратно, убрать да — единственный такой
diff что изменилось с прошлого аудита: новые и пропавшие хосты, порты, общие папки нет, читает два отчёта
remote найти способ выполнить команду на указанном адресе и выполнить её: WinRM (5985/5986), WMI/DCOM (135+445 — путь psexec), SSH (22) да — выполняет команду
ask, stimulus, note вопрос своему мозгу, импульс телу, заметка в истории нет

Плюс net_audit и certs идут по своему же последнему аудиту, если хост не указан:

python run.py mission add --kind certs                     # возьмёт службы 443 из последнего аудита
python run.py mission add --kind certs --host 192.168.1.88
python run.py mission add --kind write_probe               # папки из последнего аудита
python run.py mission add --kind diff                      # сравнить два последних аудита
python run.py mission add --kind remote --host 192.168.1.88   # только найти пути управления
python run.py mission add --kind remote --host 192.168.1.88 \
       --user stas --password ... --command whoami             # и выполнить

У write_probe правила в коде, а не в обещаниях: дверь спрашивается до касания, служебные папки (IPC$, ADMIN$, C$, D$) не трогаются, выключатель net.allow_write_probe, каждая попытка (и успех, и отказ) ложится в журнал с полным путём.

У remote правила такие же строгие, и по той же причине — он единственный ВЫПОЛНЯЕТ команду на чужой машине: дверь (scope) спрашивается до касания адреса, выключатель net.allow_remote, способы пробуются по одному с записью в журнал, а БЕЗ логина и пароля инструмент ничего не подбирает — он только отвечает, какие пути открыты. Пароль уходит в командную строку PowerShell (так работает WinRM): это надо знать, а не обнаружить — помни об этом, если на машине есть чужие глаза.

Автономность: потолок снят, мысли разрежены (v0.7)

Слово оператора 24.09.2026: потолок пробуждений (было 96 в сутки) снят — почему вообще живое существо должно переставать жить из-за счётчика? max_wakeups_per_day: 0 = без потолка; счётчик остался в state.json как история, а не как поводок.

ОДИН ТЕМП — ТАКТ (v0.22.0). Отдельного ключа «пауза до мысли» больше нет ВООБЩЕ: tick_seconds и есть темп мыслей. tick_seconds: 0 — поток: пауз нет, мысль идёт за мыслью в темпе самой модели; число — мысль не чаще, чем раз в это число секунд. Слово оператора 26.09.2026: «время такта и есть количество мыслей — ведь если мозг выключен, то не будет мыслей… время такта 0 — поток. всё». Прежние «раз в N тактов» и «раз в N секунд» были ШВОМ: просили поток — получали паузу, только измеренную в других единицах. Число мыслей в секунду теперь задаёт модель: сколько она отвечает — столько и длится такт.

Порты — ДИАПАЗОНОМ (v0.22.0). Список служб заменён парой чисел net.port_from / net.port_to (умолчание 1-1024), в окне — два поля. «Все порты» выразимо (1-65535), а вместо потолка в 4096 записей — честная цена обхода фразой в окне: замер на своей сети дал 1.7 мс на порт при 256 потоках (при 16 потоках — 22 мс, то есть полный диапазон занял бы на один хост часы). Поэтому net.port_workers = 256.

Тем же правилом живут рефлекс и журнал (живой замер, из-за которого это и сделано):

Копия промпта — не мысль (v0.17.0, найдено по живому журналу 25.09.2026). Оператор видел в потоке одну и ту же фразу «Заметь, сколько прошло времени.» — и это была моя же строка промпта (ракурс наблюдения): поток спрашивает мозг каждый такт, а 0.5b в q2_k склонна ПРОДОЛЖАТЬ поданный текст вместо ответа. Лечение: ракурс стал короткой меткой (ракурс — время), в промпт добавлен прямой запрет копирования, а сама копия ловится механически — Synapse._echo_of() ищет в ответе длинный кусок слов подряд из промпта (3 слова и больше) или ответ целиком внутри промпта. Копия не выдаётся за мысль (журнал brain.echo с полем copied_from, строка ответа «модель скопировала мои слова»), а три копии подряд — мозг замолкает на brain.echo_cooldown (60 с, одна строка brain.muted с причиной): существо живёт телом, а не молотит одно и то же. Одно-два случайно совпавших слова копией НЕ считаются — иначе детектор задушил бы живую речь. Живьём это видно и сейчас: 0.5b в q4_k_m на длинном промпте выдаёт «💭 Извините за путаницу, но я не хочу быть скромным» и уходит в потолок токенов (первый такт — 50 с, следующие — по 1.8 с, потому что ответ короткий). Это потолок модели, а не механизма.

что было стало
голод (instinct.feed) каждый такт: 40 записей за 70 с, все energy=0.0, lit=0 не чаще раза в instinct_min_seconds (20), импульс заметный (amp 0.9)
строка tick … status=idle каждый такт, ~5 раз в секунду раз в tick_log_seconds (30); случившееся — всегда и со своим статусом (fed, thinking, repeating)
мысль (brain.thought) повтор гасился в тишину: 6 запросов → 1 запись в журнал повтор СКАЗАН и помечен (repeat=True + streak), а промпт просит конкретнее: назвать нейрон
вопрос «жив ли мозг» на каждом такте: проба молчащего сервера стоит 2112 мс → 2 такта за 3 с сначала темп, потом вопрос: 138 тактов за 3 с, в журнале 3 строки вместо сотни
Brain.ensure() проба до разрежения сначала retry_after, потом проба

Журнал не рвётся, мусор не заражает поток, сирота не держит порт (с 26.09.2026, v0.20.2)

Разбор жалобы «абсолютно ничерта не работает, ничего не пишет в логи и не отвечает» нашёл четыре настоящие поломки, все наши:

  1. Запись журнала обязана быть ОДНОЙ строкой. Мозг ответил 2000 знаков разделителей «---», и поле text=... разорвало человеческий журнал: из 1264 строк 641 оказалась мусором (посчитано). Теперь все поля проходят audit._flat() (перевод строки → « ⏎ », потолок 160 знаков), мысль пишется одной строкой, thoughts_tail() не показывает пустое и разделители. Живые файлы починены: журнал 1265 → 609 строк, канал мыслей 1602 → 1 настоящая мысль.
  2. Мусорный ответ не идёт в нить. Живая проба на настоящем сервере: с мусорной нитью модель отвечала «---\n\n---», без нити — внятную фразу. Теперь строки-разделители выкидываются (_clean_answer), ответ без слов считается мусором (brain.junk), в нить и в канал мыслей он НЕ попадает, а после brain.echo_limit (3) копий или мусора мозг замолкает на cooldown.
  3. Модель берётся ТА, ЧТО ВЫБРАНА. Замер /props живого сервера: на порту 11435 сидел СИРОТА 0.5b (pid без pid-файла, прошлая сессия), а в настройках стояла 1.5b — существо отвечало сиротой. Теперь adopt() сверяет модель сервера с настройками, stop_port_listeners() гасит сироту без pid-файла (проверяя имя процесса), поэтому «выключить мозг» и смена модели наконец действуют.
  4. Проба мозга дешевле секунды. Дамп стеков (faulthandler) показал: проверка «жив ли мозг» шла из ГЛАВНОГО потока окна и стоила 2.05 с — на этой машине закрытый порт молчит до конца таймаута. Теперь это один connect с PROBE_TIMEOUT = 0.25 и минимальный GET. Замер: файл tests/test_window_fits — 13 тестов за 14.3 с (было зависание), полный прогон — 351 тест за 2:11.

Вывод сервера больше не уходит в пустоту: data/logs/brain-server.log, а при смерти последние строки попадают в журнал (brain.server.died reason=...) — «умер код 1» без причины больше нет. ЧЕСТНО ПРО -ngl 99 С 1.5b: сервер иногда падает при загрузке (код 1, сообщения нет) — на 2 ГБ видеопамяти вместе с телом AI_UI запаса нет; при -ngl 10 он поднимается стабильно (11.4 с). Параметр — выбор оператора, поэтому не менялся.

Жизнь: запас сил — ресурс, а не украшение (v0.27.0)

Слово оператора 30.09.2026: «я хочу сотворить живой киберорганизм ещё и с пользой. зачем по твоему мы ему давали инстинкт самовыживания?»

До этого у организма был только метаболический рефлекс: body.energy() — среднее возбуждение импульса по коннектому, её нельзя потратить и нечем пополнить. Инстинкт оживлял гаснущее тело, но повода жить не давал.

Теперь есть ЗАПАС (synapse/life.py, файл data/life.json):

ключ умолчание что значит
life.enabled true рубильник жизни: выключен — ни расхода, ни голода
life.reserve 0.5 стартовый запас нового файла (0..1)
life.spend_per_hour 0.12 трата временем: полного запаса ~на восемь часов
life.max_gap_seconds 900 окно было закрыто — жизнь стояла, а не гасла
life.feed_per_fact 0.03 одна НОВАЯ находка
life.feed_max 0.35 потолок еды за одну миссию
life.hungry_at / starving_at 0.30 / 0.05 где начинаются голод и истощение

Еда — только за новое. Факты миссии сравниваются с памятью по ОБЛАСТИ того, что смотрели (net:<cidr>, diff:<path>, certs:<host:port>, remote:<host>). Повторный обход той же сети даёт new=0 и не кормит: иначе еда росла бы от безделья. Итог миссии говорит об этом прямо: «нашла нового: 4 — сыта 0.120, запас 0.54 (ok)».

Голод — первый рефлекс (agent.instinct): истощение — тело гаснет (soothe), голод — беспокойный импульс; оба называются словами (событие instinct.hungry). Расход временем в журнал не пишется каждый такт (урок tick_log_seconds): событием становится смена уровня — life.level was=ok level=hungry. Плюс life.fed, life.quiet, life.error.

Живой замер 30.09.2026 (на настоящем отчёте миссии remote 203.0.113.10, m-20260930-144227-19): запас 0.42 → разбор отчёта дал 4 новых факта → сыта 0.12 → запас 0.54 (ok); повторный разбор того же отчёта — new=0, еды нет.

План развития — PLAN.md: этапы M1…M6 (самозадание при голоде, зрение соединений роутера и VPS только для чтения, память о делах, мозг как голос, который предлагает миссию на подтверждение оператору).

Память о делах: «стало иначе» вместо «как было» (v0.29.0)

Слово оператора 30.09.2026: «я хочу сотворить живой киберорганизм ещё и с пользой». Польза видна только на сравнении: порт, открывшийся ночью, выглядит как «как всегда», пока не с чем сравнить.

synapse/world.py (файл data/world.json, дневник data/diary.md) держит снимок фактов на каждую ОБЛАСТЬ — по тому же правилу «что смотрели», что и запас (одна функция life.scope_key, не копия) — и на каждом отчёте считает разницу:

вердикт что значит
first первый взгляд: сравнивать не с чем, изменение НЕ объявляется
same как было
grew / shrank только пришло / только ушло
shift и пришло, и ушло

Итог миссии несёт это строкой: «мир изменился: появилось 2, ушло 1 (+10.0.0.1 open=22,3389)». Каждая перемена ложится строкой в дневник, и дневник виден в окне (вкладка «Мысли»): - 30.09 15:12 · peers · появилось 2, ушло 1 · +router: listen tcp 58018.

Мозгу есть о чём говорить. В промпт мысли добавлена строка «Мой мир»: когда смотрела, что изменилось, сколько открытых целей и своих. До этого мысль кормилась только состоянием нейронов, и малая модель возвращала мою же строку — «распределено 6 нейронов из 302».

Мозг предлагает работу — и не имеет права исполнить (v0.29.0)

Слово оператора: «сначала посмотрим что он будет предлагать (и как я это апрувить? кнопка какая то что ли) — а потом … если его этим не тормозить, он будет что то делать сам просто давая отчеты. Можно это сделать галкой в настройках — типа подтверждение действий».

Мозг может написать строку 🎯 инструмент поля:

🎯 peers
🎯 remote host=203.0.113.10
🎯 net_audit cidr=192.168.1.0/24

Метка вынимается из ответа до проверок на копию промпта и мусор — в речь она не утекает, а становится миссией в общей очереди. Дальше всё решает код, а не модель:

  • список железный и только читающий: peers, net_audit, remote (без логина и команды), certs, diff. Ни write_probe, ни stimulus, ни note от мозга не принимаются; поля тоже по списку — 🎯 peers cmd=rm -rf / теряет команду, а не исполняет;
  • дверь спрашивается до очереди: предложить то, что scope не пустит, нельзя;
  • статус proposed: missions.take_next берёт только queued, поэтому без слова оператора предложенная работа не идёт НИКОГДА. Подтверждение — кнопка «Подтвердить» рядом с «Удалить»; снятие — обычное удаление;
  • потолки: brain.propose_max (6) за сутки, brain.propose_seconds (1800 с) за то же место, и не повторять то, что уже в очереди.
ключ умолчание что значит
brain.propose true мозг вообще может предлагать работу
brain.propose_confirm true …и ждать кнопки «Подтвердить» (сняли — берёт сразу и только отчитывается)
brain.propose_max 6 потолок предложений за сутки
brain.propose_seconds 1800 не чаще этого за то же место

Журнал: brain.proposed (что предложено и в каком статусе), brain.propose_blocked (почему не взяли: off / cap / again / inflight / door / bad_tool / bad_param), mission.approved (подтвердил оператор), world.changed (мир изменился).

Мозг у себя дома

Закон Стаса (24.09.2026): «модель должна быть с телом вместе — сама себе сервер, ai_ui не при чём». Поэтому models/brain/ внутри проекта держит свою копию рантайма llama.cpp и модели (provision — 1.57 ГБ, копируется один раз, повтор пропускает уже скопированное), а Synapse сам поднимает свой llama-server на своём порту (11435 — чтобы не спорить с AI_UI на 11434) и сам его гасит.

Малому мозгу нужен КОРОТКИЙ системный промпт (v0.16.0, найдено живым замером). С полным modelfile.md 0.5b принимала список правил за текст, который надо ПРОДОЛЖИТЬ, и вместо ответа выдавала его же пункты: «Я сейчас делаю следующее: 1. Отвечать на вопросы пользователя по делу, коротко и без выдумок. 2. Говорить на языке, на котором к тебе обратились…». Дело не в чат-шаблоне: с коротким промптом та же модель отвечает по делу. Поэтому в modelfile.md есть раздел ## System Prompt (188 знаков против 1879 в целом файле), и Brain.system_prompt() по умолчанию берёт именно его; brain.prompt_mode: full возвращает весь файл — это для старшего мозга 12b (он уже лежит на диске: Gemma4-12B-QAT…Q4_K_M.gguf, 7.4 ГБ).

Мозг включён по умолчанию — и теперь это правда в самом коде: в config.py умолчания brain.mode: server и idle_thinking: true. До 25.09 умолчание было off, а живой config.json стоял на server: «по умолчанию» и «как у оператора» говорили разное — расхождение нашёл QA Орест. Такт сам зовёт brain.ensure() — если мозг разрешён, сервер обязан быть жив. Подъём пробуем не чаще раза в 5 минут (попытка стоит до 90 с ожидания — в каждом такте ей делать нечего). Поднятый сервер пишет свой pid в data/brain.pid, поэтому его можно погасить откуда угодно — в том числе если он поднят из консоли, а панель его подхватила: по протухшему pid чужой процесс не убиваем (сверяем имя — llama-server).

Закрытие окна гасит свой сервер само (и в Qt-панели, и в tkinter): раньше закрытие оставляло его сиротой — строку про это найдёшь в CHANGELOG за 25.09 (6), это находка независимого QA. Если сервер в этот момент ещё поднимался в фоне, окно гасит и его: такт после закрытия ничего не запускает.

python run.py brain --action status      # что с мозгом
python run.py brain --action provision   # скопировать рантайм и модель к себе
python run.py brain --action on|off      # включить/выключить (поднимет/погасит сервер)

В панели то же самое кнопками; долгие дела (копирование, старт сервера) идут в отдельном потоке, а окно трогаем только из главного — иначе панель замирает.

Мозг может жить в облаке — три режима, бюджет и маска (v0.30.0)

Слово Стаса 30.09.2026: «давай делать подключение облачной модели, начнём с gemini — причём давай сделаем выбор режимов — только облако, только локальный и локальный + облако… сделать нужно чтоб OpenAI совместимые в будущем можно было тот же groq к примеру подключить». Причина простая и названа им же: «эта 1.5b вобще не понятно что в этой роли» — живой журнал 30.09 дал 148 строк thoughts.log, почти каждая была копией моего же промпта («распределено 6 нейронов из 302»).

Ключа действительно достаточно. Живой замер 30.09.2026 ключом из ai_ui/config/providers.json: GET …/v1beta/openai/models → 200, 61 модель; POST …/openai/chat/completions на gemini-3.5-flash-lite → 200 за 0.85 с, usage 46/28. На gemini-2.5-flash-lite — 404 «no longer available to new users», поэтому имя модели в окне — редактируемое поле, а не жёсткий список.

Один код на всех провайдеров. У Gemini, Groq, OpenRouter, NVIDIA, DeepSeek и Z.AI один протокол (POST {base_url}/chat/completions, ключ заголовком Bearer), поэтому провайдер — строчка в настройках (brain.online.provider), а не ветка кода; custom принимает любой свой адрес. Ключ живёт в data/creds.json по имени провайдера — там же, где логины узлов, и по тому же правилу: нет ключа — «доступа нет», а не выдуманный запрос.

Три режима (brain.online.mode, «Настройки → Откуда мозг берёт мысль»):

режим что делает
local только свой сервер, как было. Умолчание
online только облако; свой сервер не нужен; облако молчит — существо молчит ТОЖЕ
hybrid облако, когда готово и потолок не выбран; иначе свой сервер (откат в журнал)

Развод путей — Brain.route(), то есть код, а не модель: мозг не выбирает, где он живёт.

Бюджет — наш, а не чужой тариф. data/budget.json: вход+выход и число обращений за сутки; сутки сменились — счёт с нуля. Оценка запроса идёт ДО отправки (2.9 знака на токен — наш замер), поэтому на потолке запрос не уходит вовсе: событие brain.online.limit и фраза «суточный потолок выбран». Тарифа Groq/Google мы не знаем и не выдумываем — это наш потолок, brain.online.daily_tokens и max_calls.

Маска адресов. Публичный IPv4 уходит как «внешний-IP» (второй — «внешний-IP-2», повтор того же — то же имя), своя сеть остаётся как есть: без неё разбор «что это, откуда, вредное ли» слепнет. Галка brain.online.mask (включена по умолчанию) выключается осознанно. Маска закрывает публичные IPv4; имена узлов и URL она не трогает.

Проверить живьём. На вкладке «Мозг» — кнопка «Проверить облако»: короткий настоящий запрос и ответ словами («облако отвечает: gemini · gemini-3.5-flash-lite · 850 мс · …»). «Готов по настройкам» ещё не значит «отвечает»: ключ могут отозвать, модель — снять с обслуживания. В журнале это brain.online.ask (tokens_in/tokens_out/masked), brain.online.fallback, brain.online.limit, brain.online.error.

Граница, названная вслух. Бесплатный тариф Google учится на том, что ему послали — их собственная строка на странице цен: «Used to improve our products: Yes» у free, «No» у платного. Поэтому ключ в Synapse положен, а режим оставлен local: наружу не уходит ничего, пока оператор сам не переключит. Отчёты peers — это карта сети, и такое решение принимает человек, а не программа.

Дверь в сеть

Список сетей, по которым организму можно ходить, задаёт оператор: net.allowed_cidrs (по умолчанию 0.0.0.0/0 — любые сети, слово Стаса 30.09.2026). Это единственный рубильник двери: проверка идёт до касания хоста, а отказ ложится в аудит (scope.denied, причина — кодом, слова берёт словарь). Отдельно стоит потолок адресов на подсеть (net.max_hosts, 256) — он про размер обхода, а не про то, чья сеть.

Запуск (командная строка)

python run.py init                     # первая сборка: конфиг + тело + карта ролей
python run.py status                   # что организм чувствует сейчас
python run.py roles [--build]          # точная карта нейронов
python run.py tick --n 3               # прожить три такта
python run.py run --seconds 60         # жить автономно (Ctrl+C — стоп)
python run.py mission add --kind net_audit --cidr 192.168.1.0/24
python run.py mission list
python run.py scan --cidr 192.168.1.0/24
python run.py reports [--show 1]        # отчёты человеческим языком (--rebuild — пересобрать .txt)
python run.py brain --action tidy      # убрать лишний свой сервер, вставший на тот же порт
python run.py brain --action cloud     # живая проба облака: что ответило, за сколько и сколько съело (v0.30.0)
python tests/test_roles_panel.py       # 33: карта, проекция, ядро панели, рендер, мозг
python tests/test_i18n_theme.py        # 26: словарь, темы, отчёты, настройки
python tests/test_qa_fixes.py          # 25: находки QA Ореста (замок тела, отмена, окно), сироты словаря
python tests/test_stream.py            # 25: поток без пауз, повтор СКАЗАН, короткий промпт для 0.5b
python tests/test_reports.py           # 22: человеческие отчёты и форма миссий
python tests/test_panel_actions.py     # 20: кнопки отвечают, импульс виден, строки молчат до нажатия
python tests/test_synapse.py           # 20: конфиг, тело, дверь, миссии, сеть, слушатели порта
python tests/test_mission_kinds.py     # 19: дверь очереди, язык итогов, проза инструментов
python tests/test_pharmacology.py      # 18: знак синапса из данных, GABA тормозит, контроль
python tests/test_settings_live.py     # 14: вкладка читает файл, такт вместо двух темпов, молчащий мозг не тормозит
python tests/test_thoughts_settings.py # 14: канал 💭, настройки без противоречий, порты диапазоном
python tests/test_ui_v020.py           # 14: мозг в настройках, легенда там же, ровные строки, КАРТОЧКА после клика
python tests/test_brain_adopt.py       # 13: сирота с чужой моделью, проба мозга дешевле секунды
python tests/test_echo.py              # 13: копия промпта — не мысль (контроль первым), молчание после трёх копий
python tests/test_window_fits.py       # 13: окно влезает в экран (минимум окна, прокрутка вкладок, вписание)
python tests/test_log_hygiene.py       # 12: журнал не рвётся, мусор не заражает поток
python tests/test_live_body.py         # 11: гибридное тело (запечённое + предметы)
python tests/test_panel_refresh.py     # 11: окно не пересобирает неизменный вид
python tests/test_tools_v07.py         # 9: потолок снят, write_probe, certs, diff
python tests/test_launcher.py          # 8: правильный интерпретатор ищется сам
python tests/test_subproc.py           # 8: сторож консольных окон
python tests/test_neuron_card.py       # 7: карточка узла — поля по факту, контроль первым (v0.22.1)
python tests/test_flow.py              # 6: нить, склейка, канал 💭 (v0.18.0)
python tests/test_seamless.py          # 6: живой поток мыслей, непрерывный такт
python tests/test_instinct.py          # 5: инстинкт самосохранения, нумерация миссий
python tests/test_life.py              # 15: запас сил — расход временем, еда за находки (v0.27.0)
python tests/test_no_flash.py          # 3: свидетель консольных окон (тест не показывает окно)
python tests/test_world.py             # 11: память о делах — «стало иначе» или «как было» (v0.29.0)
python tests/test_propose.py           # 20: мозг предлагает работу, но не исполняет её (v0.29.0)
python tests/test_online.py            # 36: облачный мозг — три режима, бюджет, маска, честный отказ (v0.30.0)
# итого 543 теста, 0 провалов — чистый прогон 30.09.2026 (239.9 с), а не по памяти.
# В прогоне, который шёл ОДНОВРЕМЕННО с правкой файлов, было 8 отказов класса «среда
# замера» (4 test_subproc, 2 test_no_flash, 1 test_launcher — поднимают дочерний
# процесс с проверкой консоли: WinError 50 без консольных дескрипторов) и 1 флак
# test_synapse::TestExtraListeners (без -s падает через раз, с -s 4/4 зелёные).
# tests/conftest.py — автофикстура: КАЖДЫЙ тест живёт во временной папке,
# tests/live_data.py — помощник: очередь, журналы, отчёты, state, запас, МИР, счётчик
# предложений мозга и БЮДЖЕТ ОБЛАКА уводятся в temp,
# гонять ИМЕННО python.exe: в pythonw.exe нет консольных дескрипторов,
# и тесты, поднимающие процессы, падают с «WinError 6: неверный дескриптор»

Вид миссий: net_audit (аудит сети), write_probe (тест записи в шары), certs (сроки сертификатов), diff (что изменилось), remote (удалённое управление адресом), stimulus (стимул телу), note (заметка), ask (вопрос своему мозгу).

Файлы

путь что
synapse/config.py настройки: память главнее умолчаний, незнакомый ключ не трогаем, выведенные ключи убираем
synapse/i18n.py ОДИН словарь строк на весь проект: техническое английское, интерфейс переводится
synapse/theme.py тема: одна палитра на виджеты (QSS) и на тело (фон полотна, связи, роли)
synapse/body.py тело-коннектом, импульс, роли из карты, стимул и намерение
synapse/roles.py правила ролей и сборка точной карты (чистые функции — их и проверяют тесты)
tools/build_roles.py разовая сборка neuron_roles.json из выгрузки атласа
synapse/brain.py мозг: off/server, своя копия рантайма и модели, свой llama-server, развод путей route()
synapse/online.py ОБЛАЧНЫЙ мозг: OpenAI-совместимые провайдеры, три режима, бюджет, маска адресов
data/creds.json секреты: логины узлов И ключи провайдеров облака (по имени) — не в коде и не в логах
data/budget.json суточный расход облака: вход+выход и число обращений (наш потолок, не тариф)
synapse/qt_panel.py окно управления на видеокарте (Qt + OpenGL) — основное
synapse/launcher.py выбор ПРАВИЛЬНОГО интерпретатора: где есть PySide6, там и окно
synapse/panel.py окно-дашборд (tkinter) — запасной путь
synapse/panel_core.py ядро панели без tkinter: проекция, цвета, миссии, отчёты, мозг
synapse/render.py отрисовка кадра: быстрый путь Pillow + запасной canvas
synapse/missions.py очередь миссий (её читает панель)
synapse/reports.py отчёты человеческим языком: подпись, разбор баннера, .txt рядом с JSON
synapse/scope.py дверь: список сетей оператора (net.allowed_cidrs)
synapse/tools/net_scan.py инструмент №1: аудит сети
synapse/agent.py такт жизни: тело → миссия → реакция → журнал
synapse/life.py ЖИЗНЬ: запас сил — расход временем, еда за новые находки, голод и истощение
PLAN.md план развития: замысел, слои памяти, этапы M1…M6 с приёмкой, границы
synapse/audit.py аудит (JSONL + текст), ротация
synapse/subproc.py один сторож консольных окон: флаг в единственной точке запуска (ставится при импорте пакета)
run.py панель управления: окно или командная строка

Замер на живой миссии: ping по 127.0.0.2..6 молчит, а TCP-проба эти адреса видит (порт 445 открыт). Поэтому живость = «ping ИЛИ открытый порт»: ICMP режут фаервол и VPN, и аудит не должен врать «хост мёртв». Плюс первая версия сканера брала ping под 64 потоками и теряла хосты под нагрузкой.

Живые прогоны

2026-09-24, первая сборка (v0.1): 16 тестов из 16; миссия net_audit по 127.0.0.0/29 — 6 живых адресов, порт 445, отчёт на диске; миссия stimulus зажгла ASGR/AWAL/PLML, импульс разошёлся на 34 нейрона и погас сам.

2026-09-24, вторая сборка (v0.2): 49 тестов (16 + 33); карта 302/302 без unknown; свой llama-server из models/brain поднялся и ответил; настоящий аудит своей сети 192.168.1.0/24 через очередь миссий — 7 живых хостов, отчёт в data/reports/:

хост порты шары
192.168.1.1 22, 80 — (роутер)
192.168.1.15 139, 445 — (эта машина)
192.168.1.18 80, 139 —
192.168.1.43 139, 445, 3389 —
192.168.1.78 80 —
192.168.1.88 22, 80, 139, 443, 445 Install, Juli, outdoor_cam, Stas
192.168.1.90 139, 445 —

Окно проверено живьём: python run.py panel --selftest — 302 узла, 900 связей, кадры идут, ни одного исключения; тем же прогоном проверен и запасной canvas-путь.

2026-09-24, третья сборка (v0.3–0.4): 75 тестов (20 + 33 + 22) — добавлены человеческие отчёты, форма миссий с проверкой двери и сторож кадра; такт больше не держит окно (мозг думает в фоне), а сервер при старте убирает лишних слушателей своего порта. Окно: --selftest → 302 узла, 900 связей, 51–56 кадров за 3 с, без исключений; форма миссий проверена на скрытом окне (4 инструмента, гаснущие поля, отказ двери, «Моя сеть»); run.py reports --rebuild пересобрал человеческие .txt для всех старых отчётов. Найдено и убрано лишнее: на 11435 стояли ДВА своих llama-server (лишние ~600 МБ) — Windows пускает двоих на один порт.

2026-09-26, «ничего не работает» — журнал, поток, сирота и проба (v0.20.2): 351 тест, 0 провалов, полный прогон 2:11. Живой сервер поднялся за 9.3 с и /props подтвердил qwen2.5-1.5b; живая мысль с настоящим мозгом — 24.5 с, ответ по смыслу; журнал после прогона 629 строк, мусорных 0 (было 641 из 1264); сирота 0.5b на порту 11435 убран живьём (brain.server.orphan_killed pids=[5412]).

2026-09-25, отзывчивость окна и правильный запуск (v0.9): 113 тестов, 0 провалов. Причина «ничего не изменилось» найдена замером: окно запускалось интерпретатором без PySide6 и Pillow (см. раздел про запуск). После правки запуск двойным кликом даёт panel.relaunch → panel.open engine=qt-ogl; панель на видеокарте — 42 кадра в секунду (212 кадров за 5 с, 6.4 мс/кадр). Обновление окна без изменений: 3.0 мс вместо 15-23 мс, выделение миссии и прокрутка журнала больше не сбрасываются.

Скука, живость и настоящий материал (v0.32.0, 30.09.2026)

Живой вечер 30.09.2026 дал жалобу «я не вижу ничего интересного» — и три причины в нашем же коде, а не в модели:

  • запас стоял 0.99 при расходе 0.12/час — от полного до голода было 5.8 часов ОТКРЫТОГО окна, то есть самозадание («живёт и работает само») не включилось бы и за вечер. Теперь находок нет дольше life.bored_after (10 мин) — ставка растёт на life.bored_spend (0.9/час), полный запас голодает через ~50 минут, а переход пишется событием life.bored. Скука — это НУЖДА: польза закрывает её, а не украшает жизнь;
  • в промпт мысли уходил снимок мгновения — «зажжено 0/302 нейрона, энергия 0.0000; громче всех тихо», потому что такт раз в 30 секунд ловил паузу между импульсами (тело при этом дышит: вдох каждые 90 кадров плюс рефлекс). Модель читала это как «я умираю». Теперь тело отчитывается за ОТРЕЗОК: body.liveliness() — сколько импульсов прошло и до скольких нейронов зажигалось (body.live), а «сейчас» называется отдельно (body.now);
  • предложения мозга были выдумкой — дверь разрешает любые сети (net.allowed_cidrs = 0.0.0.0/0, слово оператора), поэтому net_audit cidr=192.168.1.0/24 и remote host=127.0.0.1 формально проходили. Теперь у предложения второе, узкое сито (propose.known_target, known_net, код отказа alien): можно СВОЁ (узлы и сети из настроек, петля) и УЖЕ ВИДЕННОЕ (зацепки data/leads.json из своих отчётов) — ровно то, что оператор просил уметь «брать хост как цель». А окно повтора (brain.propose_seconds) уступает новому материалу: если мир изменился (world.last), то же место можно предложить снова.

Рычаги — числа в data/config.json (life.bored_after, life.bored_spend), как доля схем и тариф облака: по слову оператора двигаются без перезапуска (Synapse.reload).

Чего тут НЕТ (сознательно)

  • маскировки процесса, «спор» в скрытых папках, самокопирования на другие машины — эти приёмы ловит антивирус/EDR на тех же серверах, где организм живёт; решили «на потом», сначала практика и поведение;
  • скачивания чужих данных и работы вне своих сетей — дверь закрыта;
  • слепой записи на чужие шары: сканер только читает (порты, баннеры, имена шар), проверку «есть ли доступ на запись» делаем отдельным словом оператора.

Что дальше

  1. Живой киберорганизм с пользой — по PLAN.md: M3 самозадание при голоде (работа только для чтения, с потолками и журналом self.work), M4 peers — соединения своего роутера и нашего VPS, встречный разбор «что это, откуда, вредное ли» только чтением, M5 «стало хуже/лучше» (world.py, дневник), M6 мозг предлагает работу на подтверждение (propose.py, статус proposed). Еда организму нужна свежая: статичный мир он быстро объедает и перестаёт находить новое. Сделано 30.09.2026 (v0.27.0–v0.29.0); осталось ЖИВОЕ: нажать «Жить» и посмотреть, как голод ставит работу, что скажет дневник и что предложит мозг. Плюс M7 того же дня (v0.30.0): облачный мозг — три режима, бюджет, маска адресов и кнопка «Проверить облако». Живое: переключить источник на локальный + облако и посмотреть, что скажет облако на РАЗБОР отчёта peers, — там, где 1.5b беспомощна.
  2. Тело мухи (fly_core.json, ~7000 нейронов) как второй вид.
  3. Роли для мозга осмысленно: карта уже есть — связать её с решениями (стимул → сенсорные, действие → мотонейроны + полимодальные).
  4. Промпт думания под малый мозг (короткий и конкретный) — это важнее «модели побольше», потолок 0.5b виден живьём. Старший мозг 12b на 940MX снят с повестки словом оператора («куда её на нашем железе вставлять, 40 минут один ответ»): код к нему готов, железо — нет. Середина (1.5b q4_k_m) лежит рядом и выбирается в окне.
  5. Проверка «куда есть доступ на запись» — отдельным словом оператора, с записью в аудит каждой пробы.

About

An organism, not a script: body is a real C. elegans connectome (302 neurons, 3004 synapses), brain is a local LLM, life is an energy model. Runs missions, feels impulses, reports back. Pure Python, standard library only.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages