Repository navigation
fix(workspace): index the unread count so a poll doesn't scan every message (#3064) - #3076
Merged
Merged
Conversation
…essage (#3064) count_unread_by_session runs on every 20s Workspace poll, per open tab. Its message arm filters enterprise_portal_messages by the viewer, role = 'assistant', the thread and created_at past a read cursor. No index led on client_email, so the plan read every message in the install: - SQLite: `SCAN m USING INDEX idx_portal_messages_session`. - PostgreSQL: a Seq Scan once rows span more than one agent. New index idx_portal_messages_unread ON enterprise_portal_messages(client_email, role, session_id, created_at): the equality columns first, then the GROUP BY key, then the range. Index-only; no query or behaviour change. Dual track (Rule #9): - db/schema.py INDEXES, for fresh installs on both engines; - the versioned SQLite migration `portal_messages_unread_index`; - Alembic `0081_portal_messages_unread_idx` on `0080_agent_skill_sets` (single head). Plans measured on the live query: - SQLite: SCAN m → SEARCH m USING COVERING INDEX idx_portal_messages_unread (client_email=? AND role=?). - PostgreSQL 16 (120k rows, 400 viewers, 20 agents, ANALYZEd): Seq Scan with 119,859 rows filtered, 2,269 buffers, 6.45 ms → Bitmap Index Scan with Index Cond (client_email, role), 143 buffers, 0.87 ms. Test: test_3064 EXPLAINs the SQL count_unread_by_session actually sends, which is captured at the cursor rather than copied. It asserts no scan of the message table, with a negative control proving the assertion can fail. It also covers the SQLite migration on an install that predates the index (idempotent) and the Alembic revision. It was red before the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
obasilakis
approved these changes
Sep 29, 2026
obasilakis
left a comment
Contributor
There was a problem hiding this comment.
Validated. Index matches the count_unread_by_session predicate (seek on client_email + role, covering session_id/created_at), both migration tracks present, single Alembic head, pg-migrations green. Mutation check: reverting the fix turns 3/4 of test_3064 red (the 4th is the negative control). Merge-order note: #2984/#3021/#3035 also add 0081 on 0080 — disjoint tables, mechanical re-parent for whichever lands later.
6 tasks done
dolho
added a commit
that referenced
this pull request
Sep 29, 2026
…/ent703-pull-heartbeat dev gained 0081_portal_messages_unread_idx (#3076). Resolve: - db/migrations.py: keep dev's portal_messages_unread_index entry, then pull_sync. - Alembic: 0081_pull_sync -> 0082_pull_sync, down_revision 0081_portal_messages_unread_idx, so the version line keeps a single head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dolho
added a commit
that referenced
this pull request
Sep 29, 2026
dev gained 0081_portal_messages_unread_idx (#3076). Resolve: - db/migrations.py: dev's portal_messages_unread_index entry, then seat_ask_class_state_table. - Alembic: 0081_seat_ask_class_state -> 0082_seat_ask_class_state, chained off 0081_portal_messages_unread_idx (single head); references updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dolho
added a commit
that referenced
this pull request
Sep 29, 2026
…on to 0082 dev added 0081_portal_messages_unread_idx (#3076) off the same parent. Rename the Alembic revision to 0082_ent720_email_identity, parented on 0081_portal_messages_unread_idx, and order the SQLite entry after dev's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
count_unread_by_sessionruns on every 20s Workspace poll, per open tab. Its message arm filtersenterprise_portal_messagesby the viewer,role = 'assistant', the thread andcreated_atpast a read cursor, and no index led onclient_email. The plan read every message in the install.idx_portal_messages_unread ON enterprise_portal_messages(client_email, role, session_id, created_at). Equality columns come first, then theGROUP BYkey, then the range column. Index-only: no query change, no behaviour change.Dual track (Rule #9)
db/schema.pyINDEXES, for fresh installs on both engines.portal_messages_unread_indexindb/migrations.py(CREATE INDEX IF NOT EXISTS, idempotent).0081_portal_messages_unread_idx,down_revision = "0080_agent_skill_sets".check_alembic_heads.pyreports 82 revisions and 1 head, andcheck_alembic_parity.py origin/dev HEADpasses.db/tables.pyis unchanged: the portal indexes are declared inschema.pyDDL, not asIndexobjects, matchingidx_portal_messages_convoandidx_portal_messages_session.Query plans (the live query, captured at the cursor)
SQLite
PostgreSQL 16: scratch container, 120k messages, 400 viewers, 20 agents,
ANALYZEd,EXPLAIN (ANALYZE, BUFFERS).With a single agent in the table, PostgreSQL already used
idx_portal_messages_convo(a non-leadingclient_emailcondition, so it still walks the whole index). The seq scan appears once messages span several agents, which is the production shape.Test Plan
tests/unit/test_3064_portal_unread_index.py(new; red before the change):count_unread_by_sessionactually sends, captured viabefore_cursor_execute, on a sqlite built from the realschema.pyDDL, and asserts the message table is not scanned and the new index is used.schema_parity,migrations,alembic_parity_guard,alembic_revision_id_length, and the unread suitesent359_portal_chat_stateandent557_unread_never_opened_chat. Unread counts are unchanged.pg-migrationsCI runs the realalembic upgrade head.Merge-order note: open PRs #2984, #3021 and #3035 also add an
0081_*revision on0080_agent_skill_sets. Whichever merges second re-chains onto the live head;alembic-head-watchwill flag it.Fixes #3064
🤖 Generated with Claude Code