| 2026-11-09 |
webclient: split the chat store into area modules
Eugene Sukhodolskiy
committed
1 day ago
|
webclient: split the app styles into partials
Eugene Sukhodolskiy
committed
1 day ago
|
core: split the agent and context builder into packages
...
Agent becomes one class assembled from mixins: runtime (turn lifecycle),
streaming (the turn loop), tools (tool execution), compression (context
gates) and context (tool lists, backend lookup, window). The module-level
helpers and sentinels move to helpers.py.
ContextBuilder splits into helpers (pure truncation/freshness/prompt
blocks) and builder (the class). Both old modules become packages with
the same import surface, so navi.core.agent.Agent and
navi.core.context_builder.ContextBuilder keep working.
Method bodies are unchanged; tests now patch settings/log/should_compress
on the mixin that actually reads them.
Eugene Sukhodolskiy
committed
1 day ago
|
api: split the admin routes into a package
...
The 796-line module becomes one /admin router plus area modules: common,
sessions, users, memory, profiles, mcp, runtime and recalls. The package
re-exports every view so navi.main and existing imports keep working.
Route set, order and status codes are unchanged.
Eugene Sukhodolskiy
committed
1 day ago
|
api: split the sessions routes into a package
...
The 1064-line module becomes one /sessions router plus area modules:
common (models and helpers), listing, detail, files, messages, naming,
manage, recall. The package re-exports every view so existing imports
keep working.
Route set, order and status codes are unchanged; tests now patch
settings/asyncio where they are actually read (the submodules).
Eugene Sukhodolskiy
committed
1 day ago
|
tools: split the filesystem tool into a package
...
`tools/filesystem.py` was 1145 lines: path/format/diff/AI-text helpers at the
top, then a `FilesystemTool` class with every action handler. Move the pure
layer out into `navi/tools/_filesystem/`:
* `helpers.py` — path checks, formatting, line numbering, unified-diff
annotation, chunking, edit-op validation/application, AI prompts;
* `tool.py` — the dispatcher and its read/write/AI handlers.
The package is underscore-prefixed because `navi/tools/filesystem` also has
to stay available as the compatibility shim at the same import path. No
behavior change: the shim re-exports every top-level name the module had, and
the tests patch `settings` on the modules that now read it.
Eugene Sukhodolskiy
committed
1 day ago
|
core: split the Postgres session store into a package
...
`pg_session_store.py` mixed three layers: schema DDL and migrations, the
row <-> `Message` mapping with its archive-aware hydration, and the store
itself. Split them into `navi/core/sessions/`:
* `ddl.py` — table/index definitions and the one-shot migrations;
* `rows.py` — serialization, dangling-tool-call repair, session hydration;
* `store.py` — `PgSessionStore` and the pending-sweep loop.
`navi/core/pg_session_store.py` stays as a compatibility shim re-exporting
both the public surface and the private helpers the tests use. No behavior
change: every top-level function and every store method is preserved.
Eugene Sukhodolskiy
committed
1 day ago
|

core: split the compressor into a package
...
`navi/core/compressor.py` was 1355 lines covering three different jobs:
deciding what a pass summarizes, building and calling the summarizer, and
the retry/truncate/persist wrapper around it. Move each to its own module:
* `compression/partition.py` — pure partitioning and the single
`_plan_compression` decision shared with `would_compress`;
* `compression/summarize.py` — prompt formatting, the KEYS line, the
meta-summary and `compress_context`;
* `compression/compressor.py` — `ContextCompressor` and its fallbacks.
`navi/core/compressor.py` stays as a compatibility shim that re-exports the
public surface, so imports elsewhere are untouched. Because each module now
binds its own `settings`, the tests that override settings patch all of them
(and `_meta_summarize`/`compress_context` are patched where they are read,
not on the shim). Adds a regression test for the hard-truncate trim inside a
single oversized turn, which the moved `_split_turn_tail` import silently
broke (ruff F821, no test hit that branch).
Eugene Sukhodolskiy
committed
1 day ago
|
memory: fix review findings in unified search
...
- index each compression-dropped turn once: messages and context share
objects, and earlier drops stay in the display history
- label raw excerpts against facts even when recall asks for scope=raw
- top up short vector results from the lexical index (NULL embeddings)
- count whole content words only and cap query terms at 12
- carry source_session_id on fact hits
- cap injected_chunk_ids at 100 entries
- keep a session delete successful when chunk cleanup fails
- stop logging query text in the memory paths
- stop fabricating similarity scores for recent chunks
- consult session_messages_archive in latest_summary_at
Eugene Sukhodolskiy
committed
1 day ago
|

memory: one search across facts, raw history and sketches
...
Compression removes messages from the model's window, not from the
conversation: every compression path now writes the turns it demotes into
`memory_chunks` — verbatim, append-only, with the turn range, timestamps and
breadcrumb keys (the summarizer's `KEYS:` line plus a deterministic extractor,
which also covers the hard-truncate fallback that never sees a summarizer).
`memory_search` is the single read path over the three stores: curated facts
first, then raw excerpts by relevance, then the current session's sketches.
Every hit carries provenance (source, session, turn range, timestamp), and a
raw excerpt that a newer curated fact overrides is labelled outdated rather
than hidden — "we changed our mind, the old value was X" is often the
question. The agent also gets up to two relevant excerpts injected per turn
once the context holds a summary, deduped across turns.
Fixes found by running the pilot against a real database:
- `search_facts` returns ISO-string timestamps while chunks return datetimes;
the freshness comparison raised TypeError and took the whole search down.
- an existing `memory_chunks` table created without pgvector never gained the
`embedding` column (CREATE TABLE IF NOT EXISTS does not alter), and the
INSERT failed on it; the DDL now adds the column, and a database without
pgvector writes text-only rows found through FTS.
Eugene Sukhodolskiy
committed
1 day ago
|

context: price images by area and detect backend-truncated prompts
...
Every window decision priced an inline image at a flat 500 tokens, while a
1024x1024 screenshot costs ~1026 on qwen3.6 and a 2048x2048 one ~4098. The
undercount kept sessions below the 90% trigger; Ollama then silently truncated
the prompt to about half the window (dropping the system prompt), and the
truncated prompt_eval_count was cached as the session baseline, pinning the
estimate at ~50% for good - the session never compressed while the model was
already told the context was full.
- image_tokens.py prices each image from its header (one token per 32x32 px,
downscaled to <=2048^2, clamped to 256..4096); unreadable headers fall back
to CONTEXT_IMAGE_TOKENS. Used by both estimators, the delta and
check_context_size; the per-turn image budget fits images by their own cost.
- A prompt reported far below what was sent is a truncation: it is not cached
as the baseline, and the post-turn worker compresses regardless of the
threshold. The session keeps the honest token count.
- The pre-turn gate prices the incoming message and its images, which are not
in session.context yet.
- The summarizer receives images under a window-scaled budget, and prompts
carrying images stay out of the calibration fit (their bytes are not in
chars and would skew it).
- Oversized user messages are capped for the LLM view (at least num_ctx/3),
so a paste bigger than the window no longer dead-ends a session.
- The in-flight partition also trims by token budget: a gate that fires can
now actually shrink a turn made of a few huge messages.
Eugene Sukhodolskiy
committed
1 day ago
|
webclient: reword the Synapse not-configured hint
...
A navi may receive Synapse events without being an event source: the old
text claimed the whole integration was off. Say that outgoing events are
disabled while incoming events, reactions and app delivery keep working.
Rebuilt dist.
Eugene Sukhodolskiy
committed
1 day ago
|
| 2026-11-08 |
chat: delete the session from the header menu
Eugene Sukhodolskiy
committed
2 days ago
|
chat: fold session info and sharing into one header menu
...
Two icon buttons next to the context chip left the phone header without
room for the title. They now sit behind a GnDropdown triggered by a single
"more" icon, with the panel anchored to the right edge it opens from.
Sharing stays in the menu only while a session is open; the item labels are
computed so they follow the interface language.
Eugene Sukhodolskiy
committed
2 days ago
|

sessions: share a chat as a public read-only link
...
A session's owner can hand out one capability link that opens the display
history without an account: no input, no chat list, no artifacts panel.
The token lives in the URL hash, so it never reaches the server in a request
line or a Referer header.
The link is permanent by decision - no expiry, no revoke. Deleting the chat
removes the share row (FK cascade) and closes it, which is the only switch.
Backend: session_shares table (unique token, FK cascade), POST
/sessions/{id}/share issuing the session's one link (owner or admin; a
read-all permission may read another user's chat but not publish it), and
public GET /shared/{token}[/messages] serving display history only, with
session and owner ids left out of the payload.
Webclient: #share=<token> renders a standalone page with the owner's cards,
rating off, interactive UI components filtered out and scroll-up paging; a
share button in the chat header opens a dialog that creates the link.
Also fixes ChatHeader's rename, which called an undefined `api` and failed
silently.
Eugene Sukhodolskiy
committed
2 days ago
|
llm: rotate the routing config under a running process
...
A key that lives in the central config is only worth centralizing if
publishing it does not cost a fleet restart. The backend bakes host and
Authorization header into its clients, so it now accepts a fresh
LLMConfig: clients are dropped and rebuilt on the next call, health
stays keyed by target name, and the probe follows the new hosts too.
The overlay's llm section is no longer always staged. It is re-parsed
against the local document and swapped live; only a backend without the
swap method or a document that fails to parse falls back to
restart_required.
Eugene Sukhodolskiy
committed
2 days ago
|
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
2 days 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
2 days 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
2 days 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
2 days 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
3 days 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
3 days 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
3 days 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
3 days 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
3 days 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
3 days 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
3 days ago
|
prompt: give the methodologies catalog its rules in the system prompt
Eugene Sukhodolskiy
committed
3 days ago
|
profiles: give every working profile the skills and protocol tools
Eugene Sukhodolskiy
committed
3 days 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
3 days ago
|