| 2026-10-08 |

startup, llm, profiles, todo: five more the prod logs gave up
...
The same 5-day sweep of prod's journal as 5e51f87, continued. Every one of
these failed silently: no error in the UI, just a line in the log or a hole
in the database.
complete()/embed() retry a connection error before falling back.
stream_complete has always given its first chunk two attempts, but the
non-streaming path made exactly one per (server, model), and with a single
configured server (ollama.com) one ReadTimeout raised "All backends
exhausted: ReadTimeout" outright. That is the whole story of 2026-10-08,
when 27 calls died that way in a day — 23 of them memory summarisation, the
rest planning. Attempts now come from settings.llm_complete_retries (2) with
llm_retry_backoff_sec (2.0) between them; a missing model is still not
retried (a 404 is an answer, not a hiccup), and a lone server is still never
blacklisted — blacklisting it would block the next request for _TTL.
Startup waits for Postgres. create_container() opens the pool eagerly and
the retry loop for the DDL tables sits below it, so a host that comes up
before docker takes the whole lifespan with it. On 2026-10-07 15:19:40 the
agent ran `sudo reboot` on itself at the user's request ("Перезагрузи
себя"); the host booted 15:20:03, navi started at 15:20:18, hit
ConnectionRefusedError on the pool and logged "Application startup failed.
Exiting." Connection errors now retry for ~30s; a genuinely broken config
(a missing DATABASE_URL) still fails fast.
The profile loader reports its tally. A profile dropped by an unreadable
config.json is skipped by design so it cannot take the server down, but on
that same 15:20 boot four of them — modeler_3d, navi_code, secretary,
server_admin — were dropped with `Extra data: line 125 column 1`, and the
only trace was one error line each: the UI simply showed four profiles
fewer.
todo recovers a dropped `op`. Of the 3385 todo calls stored on prod, 73
arrived with the discriminator flattened away ({"index": 1, "status":
"in_progress"}, {"action": "view"}, {"": "add", "tasks": [...]}) and got a
bare "Unknown op: None" — a wasted round trip for an intent the remaining
arguments state plainly. The error message now names the five ops and what
each needs.
Message.created_at is stamped at creation. It defaulted to None and only
some construction sites filled it, so 85% of stored rows had no timestamp at
all (every tool message, 14 168 of 14 168) and no query could slice history
by time or measure a pause. Loading a row passes the column through
explicitly, so NULL rows stay NULL instead of being restamped with "now".
Every new test was checked to fail with its fix reverted. Full suite green
(1466 passed, 1 skipped). No frontend or dependency change, so the deploy is
a pull and a restart.
Eugene Sukhodolskiy
committed
6 hours ago
|
| 2026-07-14 |

todo: add op — append steps discovered mid-task (preserve statuses)
...
The todo tool only had set/view/update/clear. Adding a step that surfaced
mid-task meant either 'set' (which replaces the plan and resets every status
to pending) or 'replan' (1–3 LLM calls) — both heavy for a single new step, so
the agent usually skipped recording it. Add a cheap 'add' op:
- navi/tools/todo.py: op 'add' (tasks: list[str]) appends new _Task entries to
the end of the existing plan in pending status; existing steps and their
statuses are untouched. Requires an existing plan (points to 'set'
otherwise). Schema enum + op description + tool description updated.
- system_prompt.txt: tell the agent to record new subtasks with 'todo add'
right away (not held in head, not a replan); clarify the 'small todo edit'
line — add via 'todo add', drop/merge/reorder via 'set' (re-apply statuses).
- renderers/todo.py: TodoStartedRenderer renders the add card
('→ todo · add (N)') listing the new pending steps, not a JSON dump.
Tests: add appends + preserves statuses / requires existing plan / requires
tasks; renderer add card + empty-tasks. Full suite: 1005 passed, 1 skipped.
Eugene Sukhodolskiy
committed
on 14 Jul
|
| 2026-07-13 |

Автономность мелких моделей: OUTPUT DISCIPLINE, milestone-todo, перехват финала хода
...
Два последовательных захода над одной проблемой — мелкие модели (12–30B) в navi_code
теряют автономность на трудных шагах: «остановился поболтать» вместо действия и
слишком большое расстояние между пунктами плана.
Заход 1 (З1–З3):
- З1: OUTPUT DISCIPLINE в системном промпте navi_code — «act, don't announce»,
без few-shot антипримеров. Контракт хода: ответ без tool_calls = конец хода, поэтому
объявление намерения текстом убивает автономность; правила заставляют вызывать
инструмент в том же ходе.
- З2: плоский todo + метка группы milestone + декомпозиция. _parse_plan_steps
возвращает list[tuple[milestone, text]]; milestone — метка группировки (не сущность,
без статуса), «done» вычисляется при рендеринге; подшаги = больше плоских шагов
(без вложенности). TUI side-panel группирует по milestone (плоский фолбэк при пустом
milestone). Plan depth: max 15→20 + правило декомпозиции.
- З3: adaptive re-plan «длинный шаг» — nudge «разбей шаг» при in_progress ≥ порога
итераций без смены todo (порог 4, раньше общего anti-stall warning на 8).
Заход 2 (шаги 1–3, после cloud-теста 31b vs 12b):
Корневая структурная причина: весь спасательный механизм (anti-stall warning с явным
предложением reflect, adaptive re-plan) живёт только внутри tool-цикла — nudge
инжектируется в pre_turn *следующей* итерации, которой при «остановился поболтать»
нет (ход закрылся по return до post_turn). 31b застревала через «продолжаю
tool-итерации» → дожала до warning → спаслась; 12b — через «замолчала текстом» → мимо
всех nudge.
- Шаг 1: перехват финала хода. Если модель выдала bare-text, но в todo есть открытые
шаги (pending/in_progress) и лимит не исчерпан — НЕ эмитить StreamEnd, а сохранить
ассистентский текст в session.context, поставить системный nudge и continue
(без StreamEnd, без workers — консистентно с multi-iteration tool-турами). Счётчик
final_interceptions на AgentTurnContext, лимит final_intercept_limit (default 2),
эскалация жёсткости (мягкий → «second stop»). has_open_steps в todo.py: пустой
todo → False (защита casual-сообщений), failed/skipped терминальны. Профильные
флаги final_intercept_enabled/limit в base.py + loader + admin.
- Шаг 2: жёсткий reflect-триггер в промпте — «~3 tool attempts on the same step
without progress → call reflect IN THIS TURN (tool call, not reasoning aloud)».
- Шаг 3: открыть replan для застревания — «call replan when reflect showed the whole
approach is dead (not one failed step, but the approach itself)».
Тесты: 874 passed, 1 skipped. Новые — has_open_steps (5), final intercept (5),
milestone-группировка, adaptive long-step nudge, парсер шагов с milestone-маркером.
cloud: navi_code model → gemma4:31b-cloud для тестирования догадки (31b признала
застревание, 12b — нет); .env cloud-host уже gitignored.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 13 Jul
|
| 2026-07-11 |

tui: show the changed step's text in the todo update call card
...
The todo update call card (→ todo · #3 → done) now shows the text of the
step being changed, plus the validation when present. The LLM's update args
carry only the index, not the task text, so the text is read from the
current plan row before the tool runs and attached to ToolStarted.metadata
(client-rendering only, backward-compatible).
Backend:
- events.ToolStarted gains a metadata dict (mirrors ToolEvent) → to_wire.
- navi/tools/todo: step_text_for_update(index, ctx) resolves the step text
via _sid/_uid (so sub-agent isolation holds), started_metadata_for_call
wraps it for both emit sites.
- agent.py (parent) and subagent_runner.py (sub-agent) enrich ToolStarted
via the shared helper before emitting.
Renderer:
- TodoStartedRenderer update card reads msg.metadata.step_text → shows the
step text + validation; falls back to 'no validation' on history replay.
- Removed the step-text line from the result card (it now lives in the call
card) — result card is back to the compact 'plan · X/N done' summary.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 11 Jul
|

Isolate sub-agent todo + render live todo in TUI side panel
...
Phase 1 — sub-agent todo isolation (backend):
- Add current_todo_session_id ContextVar; subagent_runner scopes it to the
sub-agent's ephemeral run id so its auto-populated plan and todo updates
land in an isolated KV row instead of clobbering the parent session's todo
(which the parent's goal-anchoring reads every iteration).
- todo._sid() and planning.set_tasks prefer current_todo_session_id; the
parent run leaves it unset, so all existing todo consumers (anti-stall,
goal anchor, get_progress_message) behave exactly as before.
Phase 2 — live todo in the TUI right column:
- TodoUpdated event + emit it from the agent loop after planning auto-populate
and after each tool-execution turn.
- GET /sessions/{id}/todos reads the parent session's todo KV row (explicit
user_id/session_id, optional injected kv).
- api.get_todos + TodoList/TodoPanel widgets: status-coloured markers
(pending dim, in_progress accent+bold, done success, failed error, skipped
dim), progress header, scrollable panel below the auto-height info block.
- Hybrid delivery: REST seeds the panel on attach/switch, todo_updated WS
events update it live.
Sub-agent todos as nested sub-lists is deferred to a later phase.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 11 Jul
|
| 2026-05-23 |
Pass explicit ToolContext to tools instead of hidden ContextVars
...
Add ToolContext dataclass (session_id, event_sink, stop_event, model,
user_id, user_role, user_info) and thread it through the execution chain:
Agent._execute_tools_with_sink → ToolExecutor → tool.execute().
All ~25 tools updated to accept ctx parameter. Tools that previously
read ContextVar now prefer ctx when provided, falling back to
ContextVar for backward compatibility.
Tests updated to pass ToolContext explicitly — no more test fixtures
that set current_session_id / current_user_id ContextVars.
ContextVar setters remain as fallback for non-tool consumers
(ai_helper, context_builder, planning) and will be removed in a
follow-up refactor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 23 May
|
| 2026-05-16 |
Extract ContextCompressor, fix STL viewer, expand test suite, add architecture audit docs
...
- Extract ContextCompressor from agent.py (Step 1 of god-object refactor)
- Add retry + hard-truncate fallback logic to ContextCompressor
- Add unit tests: agent loop (14), compressor (18), KV store (8),
auth encrypt (3), auth deps (13), todo/scratchpad/image_view/memory
- Fix WebGL STL viewer: allow-same-origin sandbox + graceful fallback
- Add CompressionStarted event and client-side compression notice
- Add docs/architecture_weak_spots.md and plan_01_god_object_agent.md
- Update test_events.py and test_agent_context_size.py for new logic
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 16 May
|