| 2026-11-10 |
core: cheaper session heads, no JSON parsing in list views
...
`Session`'s private write baseline used PrivateAttr(default_factory=dict),
and pydantic re-inspects a private attr's factory on every construction: 16 µs
per row against 0.6 µs for a plain default, which dominated building a
1.3k-row session list. It is now created on first use behind
`saved_fingerprints`.
List views also stop parsing planning_logs and session_metadata. No list view
reads them (the planning endpoint goes through get()), and the columns reach
~95 kB per row on prod, so a sidebar load paid for JSON nobody looked at.
Eugene Sukhodolskiy
committed
21 hours ago
|
core: index-only counts and search positions
...
The sidebar's per-session counts and the search page's match positions ran
over the message heap: rows are wide (content, thinking) and the session
index has no is_display, so every counted row cost a heap fetch. A covering
index on (session_id, sequence_number) INCLUDE (is_display) turns both into
index-only scans.
The match query now numbers positions over that projection and reads content
only for the rows the trigram index returns, instead of numbering every
display row together with its content. On prod (1.3k sessions, 77k messages)
a common term across the largest sessions went from ~1.7 s to ~0.1 s, and the
summary aggregate from ~0.5 s to the index.
Eugene Sukhodolskiy
committed
1 day ago
|
core: session lists read aggregates, search matches from SQL
...
GET /sessions without paging hydrated every hot message of every listed
session just to return a count and a preview: 313 ms locally, and the whole
hot history allocated. `list_all_summaries` returns the same rows in the same
order with the count and snippet from one aggregate — 30 ms.
`search_list` now returns summaries like `list_page`; "where does the term
appear" comes from `search_matches`, one aggregate that reproduces the display
indices clients scroll to (verified equal to the old Python scan on 105
sessions). The API search path drops from ~500 ms to ~150 ms, most of which is
the ILIKE scan itself — see docs/sessions.md for the pg_trgm note.
The stale-session sweep on POST /sessions takes its candidates from summaries
and loads a history only for a session that actually needs extraction.
Eugene Sukhodolskiy
committed
1 day ago
|
protocol, clients: acks, queue notices and reconnect without duplicates
...
- client_id is echoed in message_accepted / message_queued; queue overflow
broadcasts message_dropped to every socket watching the session
- stream_end / stream_stopped carry message_index, the global display rank
that also stamps history rows
- webclient: cheap session_sync via /sessions/{id}/meta, the ack deadline is
armed at the write and re-armed while a run is active, fatal 4003/4004 stop
the reconnect loop instead of retrying forever
- TUI: WsBridge posts WsResumed / MessageWritten / SessionUnavailable, the app
drops the half-written turn on reconnect, shows queued and not-delivered
markers on the bubble, and reloads history only when the meta token changes
- dist rebuilt
Eugene Sukhodolskiy
committed
1 day ago
|

core, webclient: cheap session lists, changed-row saves, smaller bundle
...
The sidebar asked for a count and a 60-char snippet and got every hot
message of every listed session: 2,097 rows / 4.4 MB for 507 sessions,
plus a full Pydantic hydration. list_page now aggregates counts and the
last displayed line in SQL (4.0 ms, 30 rows); get_meta folds its COUNT
into the same round trip.
save() runs after every streaming step and rewrote the whole hot
history each time — 1,016 UPDATEs, 46.8 ms, per call, growing with the
session. A fingerprint of the mutable columns lets it touch only the
rows that changed, which is normally the tail.
HTTP: content-hashed /assets are immutable for a year, gzip drops to
level 6 (same ratio within a couple of percent on JSON, half the CPU),
and session search gets pg_trgm GIN indexes when the extension exists.
Client: one shared highlight.js on lib/common plus the languages we
actually render (openscad included) instead of the full ~190-language
entry point, and the unused @tanstack/vue-virtual is gone.
Bundle 1566 -> 760 kB (gzip 495 -> 238 kB).
Eugene Sukhodolskiy
committed
1 day ago
|
| 2026-11-09 |
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
|