Backend (runtime.uc):
- query_llm() accepts model_override (5th param), falls back to provider default
- ai_doctor() reads ai_doctor_model from UCI and passes to query_llm
Frontend (renderAdvancedSettings.ts):
- aiDoctorModel state field (type / init / load / save)
- Model dropdown per provider with sensible defaults:
openai: gpt-4o-mini(default)/gpt-4o/gpt-4.1-mini/gpt-4.1/o4-mini
anthropic: claude-3-5-haiku(default)/claude-3-5-sonnet/claude-3-7-sonnet
deepseek: deepseek-chat(default)/deepseek-reasoner
custom: free-text input
- Switching provider resets model to '' (use new provider default)
Legacy LuCI (settings.js):
- ai_doctor_model Value field with placeholder '(provider default)'
With set -eo pipefail, when grep -qx exits early after finding a match
printf gets SIGPIPE and returns non-zero, which with pipefail triggers the
|| fail branch even though grep returned 0 (found).
Here-string avoids the pipe entirely.
- watchdog: удалён дублирующий check_tachyon_cli_running(), везде используется controller.check_tachyon_cli_running()
- watchdog: smart_detect_process_pending использует controller.proxy_port() вместо повторного парсинга config.json
- watchdog: удалена однострочная обёртка is_dns_dead() — заменена is_dns_working() напрямую
- watchdog: heal_proxy_connectivity требует streak >= 2 перед очисткой cache.db — устраняет ложные ночные уведомления при кратких просадках прокси-ноды
- event_controller: push_history переведён на slice() вместо ручного цикла
- event_controller: direct_ok использует тот же check_url что и proxy check — корректность при кастомном ai_proxy_health_url
- event_controller: добавлен subscription_fail_streak — одиночный таймаут сети больше не триггерит Telegram
- telegram: tg_request больше не создаёт временные файлы в /tmp — JSON передаётся напрямую через curl -d, устранена утечка tg_payload_*.json при kill процесса
- telegram: worker() при старте удаляет оставшиеся tg_payload_*.json от предыдущих сессий
- tests: исправлен шаблон mktemp в background_spawn_fds.sh — busybox не поддерживает двойное расширение (.XXXXXX.uc)
- В сборках OpenWrt без c-ares libcurl не поддерживает --dns-servers,
что заставляло curl использовать системный DNS (dnsmasq).
- Во время рестарта или перед стартом sing-box dnsmasq был перенаправлен
на неработающий порт sing-box 5335, из-за чего curl получал Could not
resolve host (exit 6), создавая замкнутый круг и бесконечный падеж
сервиса при скачивании подписок/списков.
- Добавлен автоматический фолбэк на DoH (--doh-url https://1.1.1.1/dns-query),
который работает без c-ares и считывает IP адреса серверов подписок/списков
напрямую в обход падающего системного DNS.
- При получении 'Another component action is already running' или сетевого
RPC-сбоя спиннер на нажатой кнопке больше не сбрасывается сразу.
- Добавлено повторное связывание (fallback lookups) с 800ms задержкой, чтобы
бэкенд успевал прописать запущенный job в state до снятия индикатора.
- При отслеживании запущенного задания теперь учитываются любые фоновые
акшны для того же компонента.
- В waitComponentActionJob добавлен минимальный порог ожидания (10с)
перед срабатыванием fallback проверки версии Tachyon. При same-release
обновлениях (одинаковый номер версии, но новый SHA) совпадение версии
больше не приводит к мгновенному (за 1с) снятию спиннера loading, пока
бэкенд на самом деле только скачивает и ставит пакет.
- Расширен список транзиентных RPC ошибок (isTransientRpcError) для
надежного перешивания перезапуска uhttpd/rpcd/ubus во время переустановки.
- Увеличен COMPONENT_ACTION_TRANSIENT_RPC_GRACE_MS до 60с.
Lock contention ('Another component action is already running') is an
expected transient state — user clicked a button while a previous action
was still in flight. Showing [error] toast confuses users into thinking
something broke. Filter it from the log watcher notification pipeline.
Also suppress DNS retry debug lines (issue #24 retry logs) from showing
as error toasts.
Added tests for both suppressed patterns.
- Guard --dns-servers behind runtime capability check (requires libcurl
with c-ares). OpenWrt builds without c-ares now skip the flag silently
instead of crashing curl with 'option not supported'.
Affects: subscription/cache.uc, components/updates.uc
- On DNS resolution failure (curl exit 6), retry with increasing backoff
(5s, 10s, 15s) before giving up, instead of immediately breaking.
Fixes transient 'Could not resolve host' during sing-box restart when
Tachyon's intercepting DNS resolver is briefly unavailable (issue #24).
- Fix stale component-action lock deadlock: pid_running now validates
/proc/<pid>/cmdline against ucode/tachyon/sh pattern + 300s TTL.
Background check_update exits cleanly on lock contention instead of
emitting [error] toasts (action.uc, state.uc, initd.uc).
- Проверять /proc/<pid>/cmdline при валидации PID (исключает ложное определение dnsmasq/sing-box как владельца блокировки при рециркуляции PID в Linux)
- Добавить автоматическую очистку зависших блокировок старше 300 секунд (TTL)
- При фоновой автоматической проверке обновлений (action == check_update) пропускать занятые блокировки без записи [error] в syslog и появления всплывающих нотификаций
- В query_llm (runtime.uc) добавлена интеграция с Anthropic Messages API (https://api.anthropic.com/v1/messages) с заголовком x-api-key
- В настройки Settings и Dashboard добавлен выпадающий выбор провайдера 'Anthropic (Claude API)'
- Добавлен русский перевод для опций AI Doctor
- Добавлена опция agent_api_token (AI Agent REST API Bearer Token) в CBI-форму настроек settings.js
- Сохраняет токен через стандартную кнопку Save & Apply в LuCI
- В Advanced Settings (Dashboard) добавлен новый блок 'AI Agent REST API (12 Endpoints)'
- Позволяет просматривать, изменять и генерировать Bearer Auth Token (agent_api_token) для REST API из веб-интерфейса
- Сохраняет токен в UCI (tachyon.settings.agent_api_token)
- kill_matching_command и цикл зачистки fd/1000 прокси в package.uc теперь пропускют процессы ucode (action.uc / updates.uc) и их предков, предотвращая выбивание воркера обновлений по SIGKILL во время prerm
- isTransientRpcError теперь учитывает 'failed to execute' в качестве транзиентной ошибки RPC при перезапуске rpcd / служб во время обновления
run_deferred_subscription_bootstrap теперь после проверки что процесс
sing-box запущен дополнительно проверяет что proxy-порт реально принимает
соединения (до 10x3s).
Без этого: если порт не готов в момент вызова — fallback на прямой
download, что провалится для URL доступных только через proxy.
При неудаче через 30s запускается retry worker (поведение сохранено).
Также: get_subscription_download_proxy_address делает одиночную проверку
порта — retry worker является safety net для edge cases.
При обновлении sing-box Tachyon перезапускается, запускает sing-box
и сразу пытался загрузить списки/подписки через proxy 127.0.0.1:4534,
но sing-box ещё не принял соединения на этом порту.
Исправления:
- updates.uc: dns_probe_passed теперь проверяет TCP-доступность proxy
вместо немедленного return true; ждёт до 30s (10 попыток по 3s)
- updates.uc: download_to_file при неудаче через proxy делает fallback
на прямой download с bootstrap DNS 8.8.8.8/1.1.1.1
- cache.uc: get_subscription_download_proxy_address проверяет что proxy-порт
реально принимает соединения, иначе переходит на прямой download
- cache.uc: download_subscription использует bootstrap DNS при прямом
скачивании подписок (когда proxy недоступен или не настроен)
Older sing-box versions fail on 'close_timeout' in MTProto inbound
(added in Tachyon to fix SIGHUP hot-reload crash, issue #15).
Instead of hard-failing, runtime.uc now detects 'json: unknown field'
errors from sing-box check, strips the offending inbound field from
all inbounds in the generated JSON, and retries the check once.
This makes Tachyon forward-compatible: new fields for newer sing-box
versions degrade gracefully on older installs without requiring manual
version detection or config changes.
Самообновление Tachyon — это компонентное задание, которое устанавливает
пакет, чей postinst и вызывает remove_component_update_cache(). Очистка
стирала каталог component-actions целиком, включая файл состояния того
самого задания, которое ещё выполнялось. LuCI опрашивал задание, не
находил его, не получал ни message, ни stderr и показывал фолбэк
"Failed to execute" поверх успешно прошедшего обновления.
Теперь очистка пропускает задания, помеченные running. Устаревшие
результаты и кэш проверок обновлений убираются как раньше. Зависшее
задание не залипнет: детект по PID и started_at помечает такие stale.
Пути каталогов состояния стали переопределяемыми через окружение, чтобы
тест мог прогнать luci-postinst, не трогая /var/run.
Коммит 3d84d96e добавил constants.TACHYON_COMMIT_SHA в get_system_status()
без соответствующего require(). В результате constants == null и каждый
вызов api.get_system_status() крашил с 'left-hand side expression is null'.
Это ломало буквально все команды Telegram-бота (/menu, /status, и т.д.)
так как каждая из них вызывает view_menu() -> api.get_system_status().
Воркер компонентного действия перенаправлял stderr в тот же файл, что и
stdout (2>&1). Действие печатает результат в stdout одним JSON-объектом,
а stderr — предупреждения apk/opkg, вывод init-скрипта при рестарте —
попадал в тот же файл после JSON. Разбор всего файла падал, построчный
фолбэк не находил объекта, и finish_component_job писал синтетический
success: false с последней строкой лога в качестве сообщения.
Особенно надёжно это ломало самообновление Tachyon: install_tachyon
рестартует сервис, будучи предком воркера, поэтому вывод рестарта
гарантированно ложился поверх канала результата.
Теперь stderr пишется в отдельный <job>.out.stderr, он же используется
как источник сообщения, когда воркер действительно умер, не напечатав
результат. Файл убирается вместе с заданием и подметается orphan-очисткой.
Заодно валидация sing-box перестала судить о бинарнике по коду возврата:
popen().close() отдаёт сырой wait-статус, а command_output отбрасывал
stdout при любом ненулевом статусе. `sing-box version` печатает версию и
может завершиться ненулевым кодом или получить сигнал от остановки
сервиса перед установкой — валидация получала "", объявляла рабочий
пакет битым и откатывала установку с exit_code 129 (128+SIGHUP). Добавлен
command_output_lenient, он же чинит показ версии в дашборде.
Добавлена проверка 15b в run_doctor_checks():
- Итерирует все включённые секции через uci_core.section_objects()
- Для каждого community_list проверяет существование .srs-файла
в TMP_RULESET_FOLDER (/tmp/sing-box/rulesets/)
- При наличии пропущенных файлов запускает tachyon list_update и
перепроверяет результат (FIXED / остались отсутствующие)
- Правильно использует section_objects() вместо get_all() который
всегда возвращает null при вызове без section_name
- get_proxy_args() теперь поддерживает опцию UCI bot_proxy_section:
если sing-box запущен и секция указана, переключает GLOBAL outbound через REST API
- Исправлен краш 'left-hand side expression is null' при обработке
callback_query без поля from (например channel_post callbacks)
- В LuCI settings.js добавлен ListValue 'bot_proxy_section' с выпадающим
списком активных секций (паттерн аналогичен download_via_proxy_section)
- Локализация: ru/tachyon.po и templates/tachyon.pot синхронизированы
- Пересборка main.js через npm run build
The dashboard showed a sing-box version that never changed. build_system_info()
preferred /etc/tachyon/sing-box-version for every "extended" variant, and that
state file is written by the component action alone — a sing-box installed via
install.sh, opkg or by hand leaves it pinned at whatever was recorded last. The
UI then reported the stale version indefinitely, and the update badge compared
the release against it rather than against the binary actually on disk.
The state file only exists for variants whose binary cannot be executed to print
a version: extended-compressed is a self-extracting stub, and lx needs a runtime
that may be absent. Plain extended answers `sing-box version` fine — singbox
/runtime.uc:259 and subscription/cache.uc:1482 both gate on the compressed
variants only. Covering plain extended was a regression from 46ef97fe.
Both call sites now share sing_box_resolved_version(), which also falls through
to the live probe when the state file is missing or empty instead of reporting
"unknown".
Verified on a router: with the state file still holding 1.13.16-extended-2.6.2,
system-info now reports the installed 1.13.18-extended-2.6.3.
build.sh assembles the APK and IPK packages itself rather than going through
OpenWrt's packaging, so it carries its own copies of the prerm-family hooks.
Bounding Package/tachyon/prerm in tachyon/Makefile therefore never reached the
released artifacts: 1.2.66 shipped with
exit(system("/usr/bin/tachyon package_prerm upgrade >/dev/null 2>&1"));
which is both unbounded and status-propagating. `exit(system(...))` returns a raw
wait status, so a cleanup that was merely killed took the upgrade down with it —
on the router that surfaced as
ERROR: lib/apk/exec/tachyon-1.2.66.pre-upgrade: exited with error 247
247 being 128+119, the cleanup SIGKILLed by its own sweep.
All three hooks written here (APK pre-upgrade, APK pre-deinstall, backend IPK
prerm) now probe the BusyBox timeout spelling against /bin/true, bound the
cleanup at 90s, and exit 0 unconditionally — matching the Makefile.
tests/package_lifecycle.sh gains contracts over all three: each must be bounded,
must not propagate the cleanup's status, and must compile. They live inside
heredocs, so ucode_syntax_lint.sh never sees them and a syntax error would only
surface on a router mid-upgrade.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ps | grep '/usr/bin/tachyon'` matches the hook itself, because
/usr/bin/tachyon is what invoked it. The sweep therefore killed its own
parent, the parent died on SIGKILL, and apk saw
`tachyon-<version>.pre-upgrade: exited with error 247` (128+119) and
reported a failed hook.
The sweep now walks /proc/<pid>/stat to collect its own ancestor chain and
skips those pids.
Holders of procd's serialization lock are also cleared by descriptor rather
than by name. procd_lock() does `exec 1000>` on
/var/lock/procd_tachyon.lock and then `flock 1000` with no -w, so any
background spawn inheriting fd 1000 keeps the lock held. On a live router
the holders were watchdog workers, logread, the zapret2 supervisors, nfqws2
and a stray curl — none of which match a tachyon name pattern. While any of
them lives, every init.d call blocks in flock until the package manager
gives up on the hook.
Same descriptor-based sweep added to install.sh, which faces the same lock
left behind by pre-fix builds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
installed_backend_sha() rejects the unsubstituted __COMPILED_COMMIT_SHA__
placeholder and the literal "unknown", so a stale build on disk reports no SHA at
all. install_backend_package treated that empty value as "fresh install" and fell
through to the presence check, which passes trivially after an apk rollback — the
package and its version are still there. The run printed
WARN Package manager reported errors during installation, but tachyon is
installed — continuing
directly after the backend install had been rolled back, and then continued
through the remaining steps to a green banner over a failed upgrade.
Record whether tachyon was installed before the install and treat "was present,
still reports no SHA afterwards" as the failure it is. A genuine first install has
no package present beforehand and is unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
procd_lock() in /lib/functions/procd.sh does `exec 1000>/var/lock/procd_<svc>.lock`
followed by `flock 1000` with no -w timeout, so descriptor 1000 IS the procd
serialization lock. The spawn prologue capped its /proc/self/fd sweep at 999 and
therefore skipped exactly that descriptor: every background spawn inherited the
held lock and kept it alive long after the init script that took it had exited.
On the router eleven processes were found pinning /var/lock/procd_tachyon.lock
that way — watchdog workers, logread -f, both zapret2 supervisors and their sh
wrappers, both nfqws2 instances, the telegram worker and a stray curl. With the
lock permanently held, every later /etc/init.d/tachyon call blocked forever in
flock. That is what hung package_prerm until apk killed the pre-upgrade hook and
rolled the whole transaction back, leaving the old build installed and
__COMPILED_COMMIT_SHA__ unsubstituted in the installed constants module.
Killing the flock processes could not fix it: the descriptor lives on in every
inheriting child.
Raise the ceiling past 1000 in all three copies of the prologue (core/common.uc
and the two early-boot duplicates that deliberately carry their own helpers).
tests/background_spawn_fds.sh gains a structural check that every __tfd ceiling
covers 1000 — the runtime probe only runs on a shell that can close high
descriptors, and dash cannot, so CI would never reach it — plus a runtime case
asserting fd 1000 does not survive the prologue where the shell allows it.
Verified on the router: lock holders 0, package_prerm no longer blocks,
the 1.2.66 rebuild installs and constants.uc reports 2fc57d38.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bounding the hooks fixes future upgrades, but the hook that runs during
THIS one belongs to the build already installed, and every release up to
1.2.66 shipped a prerm with no timeout. It blocks on rc.common's
flock -w 1000, which procd keeps re-populating with retry_start_on_wan_up
waiters between pre- and post-upgrade, so apk's pre-upgrade outlives its
patience and the transaction is rolled back.
cleanup_legacy already stops and disables the service; it now also kills
the flock waiters, so the old unbounded hook has nothing left to wait on.
The router upgrade died at the installer's 120s ceiling while apk was
running tachyon-1.2.66.pre-upgrade, so apk rolled the transaction back
and the old build stayed installed.
prerm reached INIT_PATH status through rc.common, which waits on
flock -w 1000. A stuck start or retry_start_on_wan_up holding that lock
stalls the hook for up to a quarter of an hour. Every init call in the
hooks is now bounded, the status probe moved after the flock waiters are
killed, and the Makefile hook bounds the old version's cleanup from the
outside since that code cannot be trusted to bound itself.
The installer also called a rollback a success: a rebuilt tag keeps its
version, so both the presence and version checks still passed. It now
compares the commit SHA baked into the installed constants module and
fails loudly when it did not move. Package installs get 300s, since a
kill mid-transaction is what caused the rollback.
Переводы для "Rebuilt release" и "Installed build → Available build"
были добавлены в исходный каталог после locales:distribute, поэтому
копия в luci-app-tachyon/po/ru разошлась и тест luci_localization
падал на cmp исходного и упакованного каталогов.
opkg считает пакет уже установленным при равенстве версий, поэтому
пересборка под тем же тегом молча не применялась: задание завершалось
успехом, билд оставался старым. Добавлен --force-reinstall в
pkg_install_files_command (opkg) и в install.sh; apk не тронут, он
всегда пишет переданный файл.
Детект пересборки больше не зависит только от SHA, которого нет в
релизах старше f974e4a0: добавлен отпечаток билда по published_at и
метаданным ipk-ассета, решение вынесено в tachyon_build_differs с
префиксным сравнением SHA разной длины. Отпечаток установленного
билда пишется в state-файл и попадает в extra.
Telegram уведомляет о пересборках: check_notified_updates пропускает
outdated_same_release, дедуп идёт по версии и SHA, текст показывает
короткие SHA вместо пары одинаковых версий.
Кэш проверки обновлений сохраняет current_sha из результата действия
вместо безусловного latest, иначе неудачная установка выглядела как
"актуально".
Фронт пересобран из TS, ручная правка main.js снята. Добавлен
describeSameReleaseBuild с фолбэк-текстом, когда SHA недоступен.
- postinst_restore: switch from BIN_PATH start to INIT_PATH start so
the test-injected init script is exercised (the test sets TACHYON_INIT
to a fake init that writes to a log; BIN_PATH was never the fake init)
- postinst_restore: guard process-killing with !PACKAGE_TEST_MODE,
consistent with prerm_cleanup — the start call itself is always made
- flock deadlock risk is gone: prerm_cleanup now kills all waiters
before removing the routing table entry, so INIT_PATH start is safe