| 2026-11-08 |
overlay: ack the config again after a restart
...
A document that needed a restart was acked as 'staged, restart required'
and never updated afterwards: the machine restarted into the new config but
the hive kept showing the old ack. Startup now re-reports what it is running,
so the status view reflects reality instead of the last live refresh.
Eugene Sukhodolskiy
committed
6 hours ago
|
overlay: report llm and mcp changes as restart_required
...
A live refresh cannot swap LLM backends or the MCP pool — both are built
with the container — so a document that changes those sections was reported
as applied while the old wiring kept running. Compare the sections against
the previous document and stage them by name.
Eugene Sukhodolskiy
committed
7 hours ago
|
| 2026-11-07 |

navi: overlay the hive config onto the instance's own
...
An instance that knows a hive with the swarm key starts from the hive
document: layers that apply to it are merged over its local config, fields
the hive does not mention stay local, and an empty answer means 'run exactly
your own config'. Settings, profiles, LLM routes and MCP servers are covered;
database URL, ports, auth switches and the channel's own settings can never be
overridden.
Startup fetches with a short timeout, falls back to the last validated overlay
on disk and refuses to crash-loop on a document that killed the previous start.
A live refresh applies secrets and timeouts in place (the frozen singleton is
written through one deliberate escape hatch), rebuilds profiles from their
local base and reports everything else as restart_required. The hive reaches
the instance by push, by the announce reply and by a retry sweep; every apply
is acked back, which is what the hive's status view reads.
Eugene Sukhodolskiy
committed
19 hours ago
|
hive: versioned central config with layered targeting
...
The hive now stores one config document for the fleet: a list of layers,
each with an optional match (names/instance_ids/tags), appended as immutable
versions. Publish, history, rollback and status are admin-only (admin key or
loopback); the swarm key reads config, so a compromised instance cannot
rewrite what the fleet runs.
Delivery has three paths that degrade into each other: a push notification,
a 'you are behind' hint in the announce reply, and a periodic sweep over
instances that still report an older hash. Acks are tracked per instance and
version, so the status view answers who took the config and who needs a nudge.
A layer list is validated as a whole; an empty list is a legal document that
means 'no overlay'.
Eugene Sukhodolskiy
committed
19 hours ago
|
profiles: teach the agent to scout profiles and tools before switching
...
Persona gains a reconnaissance order (list_profiles, then list_tools for the
real toolset, then tool_manual) and the switch-vs-delegate rule: switch when
the task itself is another profile's work, delegate a side piece with
spawn_agent. The profiles block is built in one place and shared with the
admin prompt debug view, and the stale 'next user message' claim is fixed.
list_tools applies the admin-only role gate to an explicitly named profile,
the same one list_profiles uses, so it cannot enumerate hidden profiles.
Eugene Sukhodolskiy
committed
23 hours ago
|
skills: re-authorize a replacement when the proposal is accepted
...
A proposal proved ownership when it was written, but the protocol can be
promoted to shared while it sits pending. Check again at the click: a shared
protocol is now admins only, a personal one belongs to its owner.
Eugene Sukhodolskiy
committed
23 hours ago
|
webclient: make the skills catalog panels say what they are
...
The panel used three CSS variables the kit never defines, so its borders,
hover fills and dimmed text were silently dropped — the two columns had no
visible edge. Use the real tokens.
Around that, name the section and keep its controls with the list they act
on: a title and one-line explanation, then a list pane (search, area
filter, New, count) and a document pane, each a bordered surface. Rows get
a caret and hover/focus states; the reading pane shows a loader while the
next entry travels, moves the read-only marker into the meta line, and
swaps Delete for the kit's icon-only button. The editor uses the whole
column instead of the kit's 600px control cap.
Eugene Sukhodolskiy
committed
1 day ago
|
webclient: read the skills catalog as documents, split it into two tabs
...
Settings had one long panel mixing skills and protocols as raw markdown,
which was hard to read and wasted the width. Skills and Protocols are now
two tabs over one panel keyed by kind.
Selecting an entry shows the parsed document — triggers, preconditions,
steps, post-checks, escape hatches, body — and an Edit button switches to
the source, so the catalog is read the way the agent loads it. The
single-entry payload carries the parsed document; the list still does not,
so it stays cheap. SkillDocument is shared with the chat proposal card,
which now only draws its own frame and header.
Eugene Sukhodolskiy
committed
1 day ago
|
skills: name the kind when a document already had one implied
...
A proposal is parsed as a protocol when it is stored, but accepting it
re-parsed the same text with no override, so a front matter that left
'kind' out made the catalog refuse a document the user had just agreed
to. The create path and the agent's own skill writes had the same gap.
Eugene Sukhodolskiy
committed
1 day ago
|
webclient: make the protocol proposal card a document, not a kit tile
...
GnCard is a 340px tile with an uppercased title and GnSteps uppercases
every step, so a protocol document came out as a narrow column of caps with
its body shown as raw markdown. The card is a chat domain card now: an
app-owned full-width frame (accented left edge for pending/accepted/
rejected) with the kit's buttons, alert, chips, badge and loader.
- shows what was missing: when to use it (triggers), preconditions, steps
with their action and check, post-checks, and the body as rendered
markdown under "Context";
- unwraps a step the model wrote as a YAML mapping, which the parser turns
into Python repr text, so the step reads instead of leaking {'id': ...};
- says "Add to catalog" and shows an "In the catalog" badge once decided.
Eugene Sukhodolskiy
committed
1 day ago
|
webclient: reach the socket from cards, and draw the proposal card with the kit
...
The skill-proposal card sent its decision through inject('wsSend'), but no
ancestor ever provided it — the click went nowhere and the card reported
"No connection". Form.vue carried the same latent bug since 9dfdb48.
- wsBridge.js: a module-level bridge. useWebSocket registers its send on
setup and clears it on unmount; cards call wsSend() and get false back
instead of silently dropping the payload.
- Form.vue uses the bridge and reports a refused send without marking the
form submitted.
- SkillProposal.vue is rebuilt on the kit (GnCard, GnAlert, GnChip, GnList,
GnSteps, GnDescriptionList, GnLoader, GnButton) and exports `bare`, so
UiComponentCard no longer wraps it in a second frame with a dev header.
- registry.js tracks bare components; UiComponentCard renders them as-is.
Eugene Sukhodolskiy
committed
1 day ago
|
prompt: give the methodologies catalog its rules in the system prompt
Eugene Sukhodolskiy
committed
1 day ago
|
profiles: give every working profile the skills and protocol tools
Eugene Sukhodolskiy
committed
1 day ago
|

protocol: run a protocol as a committed plan, with a log of how it ended
...
A protocol was only text until now. The new `protocol` tool starts one: its
steps are spliced into the session's todo plan after the agent's current
position (nothing already done is reordered), a row appears in `protocol_runs`
with a snapshot of the version, steps and post-checks, and the run stays
`running` until it is closed.
`finish` refuses while any step of the run is open, refuses outright when one
failed, and requires a report — it is the run's record for the user. `cancel`
and `fail` require a reason and mark the rest of the run's steps `skipped`, so
the plan does not keep work that was never done. One run per session is active
(a partial unique index enforces it).
The commitment also has to survive the context: every model turn while a run is
open carries a system reminder with the protocol, its steps and the way to close
it, and the final-turn intercept now fires for an open run even when the todo
shows no open steps, so a bare-text turn cannot leave one running.
`protocol` is wired into assistant, developer, discuss and server_admin next to
`skills`; sub-agents get neither, because a protocol belongs to the session's
plan. The run log has no UI yet — `protocol(op='status')` is the current view.
Eugene Sukhodolskiy
committed
1 day ago
|

skills: agent-written skills and protocol proposals with chat buttons
...
A skill the agent writes now lands in the catalog on its own; a protocol
never does. `skills(op="propose")` stores the document in `skill_proposals`
and returns a card through the tool result's `ui_component` metadata — no
`render_component` call, so profiles without `navi_ui` get the card too.
The decision is the server's: the browser sends `skill_proposal` over the
WebSocket, `SkillStore.decide_proposal` writes the catalog row from the
*stored* document, names the clicking user as the author, and answers with
`skill_proposal_decided`. The agent learns the outcome as a hidden user
message, the same path a `navi_ui` form submission takes.
`create`/`update` refuse protocol documents and point at `propose` with
`replaces_id`, so the version history of a protocol always starts from a
user's yes. The chat store now renders `ui_component` from any tool whose
result carries one, not only from `mcp__navi_ui__render_component`.
Eugene Sukhodolskiy
committed
1 day ago
|

skills: catalog of skills and protocols with a settings tab
...
One registry for reusable methodologies: markdown with YAML front matter,
stored in PostgreSQL alongside a full edit history. A row is either a
`skill` (advice, adapt as needed) or a `protocol` (a committed order of
actions), and a protocol is refused on save unless it declares its steps,
post-checks and the three escape hatches — on_deviation, on_blocked,
on_already_done — because an honest run needs a way out when reality
differs from the plan.
- `SkillStore`: lazy DDL, uniqueness on (owner_id, kind, name), version
bump with an appended `skill_versions` row, `content_hash` as the only
hive-sync hook. Personal rows carry an owner; shared rows have an empty
one and are writable by an admin.
- REST `/skills` (list/get/create/update/delete/versions) and the agent
tool `skills` (search/load) that renders a compact document — numbered
steps and escape hatches for a protocol, the instructions for a skill.
- Settings → Скилы и протоколы: catalog with search and kind/area
filters, a document editor that shows the server's validation error
verbatim, create from a template, delete, edit history, and the
personal/shared switch for an admin.
Manuals stay where they are: tool knowledge, separate from this catalog.
Eugene Sukhodolskiy
committed
1 day ago
|

todo: give plans their own table and recover them after a crash
...
Todo already survived restarts inside the generic session_store KV row, but
the plan there was opaque JSON with no timestamp: after a server crash the
agent resumed a step marked in_progress with no way to tell whether it had
ever been done.
session_todos (user_id, session_id, tasks JSONB, revision, updated_at) now
holds the plan. Existing rows are migrated on first use and the KV source is
deleted in the same step, so clearing a plan is not undone by the next
process start.
Each step gains provenance fields (source, step_id, protocol_run_id) for the
protocol work, and insert_steps() splices a block after the step the agent
has already reached instead of making it re-issue the whole plan.
get_progress_message() now flags a plan whose in_progress step predates this
process once per (session, revision): re-check the real result before
marking it done.
The todo op "clear" now uses the same user scope as load/save; it used to
prefer ctx.user_id and could delete a different row than the one written.
Eugene Sukhodolskiy
committed
1 day ago
|
| 2026-11-05 |
ngram: измерить хост .135 и закрыть направление VRAM
...
Замеры на агентских промптах показали, что .135 ограничен арифметикой на
четырёх ядрах Ivy Bridge (351% CPU, 20% GPU busy), а не полосой памяти.
Отсюда два вывода с числами:
- перенос экспертов в видеопамять проигрывает в обеих раскладках:
--n-cpu-moe 32 даёт 7,7 t/s против 10,2, целые слои (-ngl 4..12) — 5,5..6,4;
- запас нашёлся в другом: IQ2_M даёт +19% генерации за +1,9% perplexity,
а -ub 2048 — +71% приёма промпта.
Добавлены зонды busy_probe и pp_probe, NGRAM_TASK.md дополнен разделом 9
и поправкой к 8.5.
Eugene Sukhodolskiy
committed
3 days ago
|
| 2026-11-04 |

export: token-served age snapshot of dialogue for an n-gram consumer
...
Navi had no way to hand its history to another machine. A database dump
was the obvious idea and the wrong one: prod's 597 MB is mostly things
that must never leave (Fernet-encrypted MCP keys, user rows, attachments),
and the dialogue an n-gram table wants is 14 MB of it.
So the export is a fixed, versioned SELECT — role, content, session, seq,
ts — over both message tables. Default corpus is user+assistant only:
tool output is 52 MB of scraped web text and command logs, and system
messages are near-duplicate boilerplate; either would dominate the counts.
A rolling window takes whole sessions, newest first, up to 150 MB, so the
snapshot stays bounded as history grows.
The snapshot is age-encrypted to a recipient whose private half never
reaches this server, so Navi cannot decrypt what it exports. The plaintext
gzip is deterministic, and its digest becomes content_id: an unchanged
corpus keeps the ciphertext and its ETag, so a consumer that polls every
few minutes downloads nothing until something actually changed.
Endpoints sit behind a bearer token (machine credential, not user auth):
/export/v1/manifest describes, /export/v1/messages streams and answers 304
on a matching If-None-Match. /health carries an informational freshness
object that never moves the aggregate status.
scripts/navi_export.py is the consumer side: keygen, then pull — manifest
first, download, checksum, decrypt, gzip-verify, atomic rename; exit 3
when nothing changed.
Eugene Sukhodolskiy
committed
4 days ago
|
| 2026-11-03 |
webclient: drop the separate queue counter, mute the queued badge
...
The input area carried a chip with the queue's own count next to the
per-message "queued" mark. Two views of one queue disagreed more than they
helped, and the count said nothing the transcript did not: the chip, its
i18n keys and the store state behind it (queuedMessages, queuedCount) are
gone. message_queued still lands in the store — it drops the "sending" hint —
but nothing is counted.
The badge on a queued message took --gn-color-accent (#FF9E64). On the user's
blue bubble, already faded to 0.75 while queued, that measured ~1.9:1 and read
as a warning sign. It is now the palette's grey (#414868): muted, legible on
that blue, and clearly secondary to the message text — the darker neutrals
look like the message itself.
Eugene Sukhodolskiy
committed
5 days ago
|

navi-web: make the batch shape the schema, drop Brave
...
Every recorded web_search and web_view call in production carried a scalar
(20 of 20, 8 of 8), including sessions where three searches on one topic went
out one after another. The recommendation was already written in three
places — the parameter description, the tool docstring and the server's
summary line — so prose was not the problem: `str | list[str]` renders as
`anyOf: [string, array]`, and the model took the first branch.
The parameter is now a pure array with minItems/maxItems and an examples
entry. A BeforeValidator folds a bare "query": "…" into a one-item list, so a
laxer caller still works; the cap rides in json_schema_extra rather than
pydantic's max_length, so four queries reach the code that runs three and
names the rest instead of failing validation.
Brave is gone on both paths: the keyed Search API (signup answers 403, so a
third backend that can never be enabled) and the ddgs engine of the same name,
which answers nothing from this host. The chain is SearXNG -> ddgs.
The persona's parallelism rule now separates "several items in one call" from
"several calls in one message" — it used to read "when the profile allows
parallel tool calls", which the model could only read as "not here".
Eugene Sukhodolskiy
committed
5 days ago
|

navi-web: pin the ddgs engines, drop bing, and give the fallback room
...
SearXNG answered the session's searches with noise. Bing, asked explicitly,
returns trending/homepage blocks for queries it cannot match (Block Blast for
a motherboard query); because the list is non-empty the chain accepted it as an
answer and never reached the working fallback. The request now pins
google,duckduckgo and asks for language=all — the pinned uk-UA was what made the
instance's DDG engine return CAPTCHA (kl=ua-uk) on every request.
The ddgs leg had a quieter bug. ddgs v9 has no "html" backend, and an unknown
name does not fail: it silently means "auto" — all engines shuffled together,
yandex and grokipedia included. That is where the slow, flaky, mixed-provenance
results came from. The engine set is now explicit and yandex-free. The 4s client
timeout and 12s chain budget also could not fit a real 1-13s multi-engine scrape,
so they become 10s and 20s.
Wire the Brave key placeholder the docs already described: optional, mapped
with a ":-" fallback so an empty value means off.
Eugene Sukhodolskiy
committed
5 days ago
|
subagents: a stopped run closes the tool it had opened
...
A tool card is drawn from tool_started and only its tool_call closes it.
When the user stopped a run while a sub-agent was mid-tool, the sub-agent
was cancelled, the parent dropped its sink, and the matching tool_call was
never sent — the card spun until the page was reloaded.
The sub-agent now reports its in-flight tool from the CancelledError
handler, and both batch paths (sequential and parallel) drain the sink
after cancelling instead of discarding it, so that event reaches the
client ahead of the parent's synthetic "stopped by the user" result.
The webclient settles anything still pending at stream_end,
stream_stopped and error as a safety net for a lost frame or a dying
server, with a visible "interrupted" note rather than a silent close.
The TUI needs nothing: it renders tool_call as a result panel and carries
no pending state, so it gets the fix through the backend alone.
Eugene Sukhodolskiy
committed
5 days ago
|
deploy: MCP servers' own venvs are synced by the deploy, not by memory
...
Each server under mcp-servers/ runs from its own .venv — the interpreter its
config in mcp_servers.d/*.json names — and Navi's venv does not carry those
dependencies. When a server's pyproject.toml gained one (trafilatura, Nov
2026) and the host was only pulled and restarted, the server died at import:
the manager could not connect, its tools silently left every profile's tool
list, and nothing reached the chat. SearXNG was healthy the whole time.
sync-mcp-venvs.sh installs every mcp-servers/*/pyproject.toml into its own
venv, stamped by a sha256 of the pyproject so an unchanged server is skipped
and a re-run is cheap. install.sh calls it as a numbered step; UPDATE.md
runs it unconditionally after a pull and verifies the MCP connection in the
journal; docs/mcp.md no longer reads as "install it yourself by hand".
Eugene Sukhodolskiy
committed
5 days ago
|
tests: keep the theme tests off a live backend
...
They mounted NaviCodeTui without mocking the terminal API. The mount worker
resolves a session and replays its history with load_history(), which rebuilds
the chat; against a running Navi on :8099 that replay landed after the test had
injected stream_delta and wiped the bubble, so the test passed only while no
server happened to be listening. Mock the same calls the neighbouring TUI test
modules do.
Eugene Sukhodolskiy
committed
5 days ago
|
tests: backdate idle eviction relative to the host's uptime
...
The test set last-used to 0.0 and assumed monotonic() had already passed the
1800s idle threshold, so it passed only on hosts that had been up that long.
Eugene Sukhodolskiy
committed
5 days ago
|
subagents: a worker persona, a live prompt fallback, and guards that scale
...
- assistant and designer_3d get their own subagent_system_prompt.txt; without
one a sub-agent ran on the orchestrator's prompt, or since ce55325 on nothing
but the scope anchor, and rarely finished the step it was given
- build the sub-agent system prompt in one place; the documented fallback to
profile.system_prompt had become dead code
- the thinking-stall guard killed healthy cloud reasoners at 12k chars after
13-35s; raise the char bound to 48k, keep the 60s wall, and make both settings
- a 300s run cap cut long ops work in half; raise it to 600s, configurable
Eugene Sukhodolskiy
committed
5 days ago
|
| 2026-11-02 |
webclient: a published card appears while the turn is still running
...
The tool index kept the raw card object, so applying a result wrote past
Vue's proxy: nothing that read the card through the array was invalidated
and a running content_publish stayed an error card until the turn's
message was rebuilt. Index the array's proxy instead, and show a pending
publish as an ordinary tool card rather than a failed one.
Eugene Sukhodolskiy
committed
6 days ago
|
webclient: a recall you can inspect, skip and cancel
...
Clicking the banner opens a dialog with the call's kind, when it was created,
a live countdown to the next fire, the repeat interval and the system message
the agent wrote for itself; skip and cancel live there too.
The cancel button in the banner and the sidebar never worked: ph-clock-slash
is not a Phosphor icon, so the glyph was empty and the button collapsed to
8x4px. It is ph-x-circle now, with a larger hit area.
/recall payloads carry created_at, and the countdown picks days or hours
instead of counting out "43230 min".
Eugene Sukhodolskiy
committed
6 days ago
|
sessions: the info dialog shows a pending recall
...
GET /sessions/{id}/info now carries the session's pending recall, and
GET /sessions/{id}/recall answers in one shape ({"recall": {...} | null})
instead of a flat object when found and a wrapper when not. The lookup is a
single indexed row rather than the newest 50 recalls filtered in Python.
Eugene Sukhodolskiy
committed
6 days ago
|