| 2026-11-09 |

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
21 hours 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
21 hours ago
|
| 2026-07-12 |
compression: profile-aware worker + real-token baseline estimator
...
Item 2 — thread the active profile into CompressionWorker so
compress_context applies per-profile overrides (compression_keep_recent,
compression_max_tokens, compression_prompt_file). navi_code now compresses
with keep_recent=12 instead of the global 8.
Item 1 — estimate the next LLM call's context from the *real* prompt_tokens
of the previous call (bulk) plus a heuristic delta for messages appended
since, replacing the chars//3 estimate that undercounts code-heavy tool
output and fired midturn compression too late (Navi kept working until the
window was exhausted). Baseline is recorded after each stream and cleared
after compression; check_context_size and the midturn gate use it, with a
heuristic fallback when no baseline exists or the context shrank.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 12 Jul
|
| 2026-04-08 |
Review fixes: events module, circular imports, deps, vision-aware compression
...
- Extract all AgentEvent dataclasses to navi/core/events.py; import from
there in agent.py and __init__.py — eliminates circular import between
workers and core
- workers/compressor.py: remove runtime import hack, use navi.core.events
- workers/base.py: WorkerResult.events typed as list[AgentEvent] (was Any)
- api/deps.py: replace @lru_cache on mutable list with module-level
singletons (_registries, _workers)
- core/compressor.py: _format_for_summary returns (text, images); images
passed to summarization LLM so vision models describe them in summary;
non-vision models silently ignore the images field; docstring updated
- client/js/app.js: add comment explaining is_summary backward compat branch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 8 Apr
|

Separate display history from LLM context; formalize worker system
...
Architecture change:
- session.messages: full display history, never modified by compression
- session.context: what the LLM sees, may be compressed by workers
- System messages go only into context (not display history)
- Image injections (synthetic) go only into context
- User/assistant/tool messages go into both
SQLite: add context column with backward-compat migration
(empty context → initialized from messages on load)
Workers (navi/workers/):
- Worker ABC + WorkerContext + WorkerResult (base.py)
- CompressionWorker: compresses session.context when above threshold
- build_default_workers() returns [CompressionWorker()]
- Agent accepts workers list, runs them after StreamEnd
- Workers injected via deps.py get_workers() (lru_cached singleton)
- WebSocket agent construction also receives workers
Compressor: compress_context() now takes context[], not messages[]
Config: context_keep_recent 6 → 10
Agent: _run_workers() collects events from all workers and yields them
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 8 Apr
|