Skip to content

perf(state): narrow FTS UPDATE triggers with AFTER UPDATE OF (salvage #73639) - #77628

Merged
kshitijk4poor merged 4 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/73639-fts-update-of
Aug 3, 2026
Merged

perf(state): narrow FTS UPDATE triggers with AFTER UPDATE OF (salvage #73639)#77628
kshitijk4poor merged 4 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/73639-fts-update-of

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Context

Every UPDATE messages SET ... fires the FTS sync triggers today, because they are broad AFTER UPDATE ON messages. Status-only writes — marking rows inactive/compacted/observed during compaction, recovery, and housekeeping — pay trigger WHEN-clause evaluation for every row touched even though no indexed column changed, which on large state.dbs saturates I/O (#68858). WHO benefits: anyone with a large session DB, during compaction/optimize passes and bulk status updates.

Measured impact

10,000 status-only UPDATEs on a synthetic FTS-enabled DB (sqlite trace-counted):

metric before (broad) after (OF) delta
trigger WHEN evaluations 10,000 0 eliminated
wall time 20.9 ms 8.6 ms 59% faster
content-update reindex works works (WHEN-evals=1, fts row updated) unchanged

Honest caveat: the win applies to status-only UPDATE statements; content/tool_name/tool_calls updates still (correctly) fire the triggers. Every UPDATE messages SET site on main was enumerated — none can change indexed columns without listing them in SET, so AFTER UPDATE OF is semantically safe.

Provenance

Salvage of #73639 by @smfworks (3 commits, authorship preserved, incl. the co-contributed fail-closed CJK migration handling). Migration audited: _migrate_broad_fts_update_triggers is idempotent (second open drops 0), costs two µs-scale sqlite_master point reads per writer open in steady state, and the legacy-layout branch recreates everything it drops. The CJK quarantine path uses the existing in-transaction set_meta(..., cursor=) variant — no nested-transaction hazard. Simplify-pass fold: removed an unreachable regex allowlist guard whose continue would have miscounted a skipped trigger (names come from a literal allowlist via IN binding).

Verification

  • 9 passed (tests/test_fts_update_of_narrowing.py) + 21 passed (FTS suite in tests/test_hermes_state.py), -p no:randomly.
  • Mutation check: reverting the three production files turns all 9 new tests RED; restore -> green.
  • OF-column audit: role is included in the OF lists where the WHEN clause inspects it (trigram/cjk role <> 'tool').
  • Simplify pass: 1 low finding folded (dead guard); REUSE verified (_drop_fts_triggers is a false consolidation target — it drops insert/delete triggers too).

Closes #73639.

@kshitijk4poor
kshitijk4poor enabled auto-merge (rebase) August 3, 2026 11:59
@alt-glitch alt-glitch added type/perf Performance improvement or optimization P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 3, 2026
@kshitijk4poor

Copy link
Copy Markdown
Collaborator Author

/rerun

Jasmine Naderi and others added 4 commits August 3, 2026 20:10
Retarget NousResearch#73639 onto the SessionDB mixin split (hermes_state_common /
hermes_state_schema). Fresh installs create UPDATE OF content/tool_*
triggers; existing broad AFTER UPDATE triggers are inspected and
replaced under schema init without an FTS rebuild (WHEN clauses already
guarded content correctness; OF skips non-content status writes that
saturated disk I/O on large state.db).

Tests: tests/test_fts_update_of_narrowing.py (4)
_ensure_fts_cjk_schema never raises on OperationalError; post-condition
after dropping messages_fts_cjk_update now requires a narrowed UPDATE
trigger or durable fts_cjk_stale + unavailable. Covers the production
soft-fail path the raise-only handler missed.
Simplify-pass fold: to_drop names come from the literal update_names\nallowlist via IN binding, so the [A-Za-z0-9_]+ fullmatch could never\nfail — and if it somehow did, its `continue` would miscount (the\nskipped trigger stayed in len(to_drop)/the log while CREATE TRIGGER\nIF NOT EXISTS silently kept the broad variant). Delete the guard and\nits function-local re import; keep the invariant as a comment.
@kshitijk4poor
kshitijk4poor force-pushed the salvage/73639-fts-update-of branch from 836fc0d to 044a0ba Compare August 3, 2026 14:40
@kshitijk4poor
kshitijk4poor merged commit ae17163 into NousResearch:main Aug 3, 2026
38 checks passed
@kshitijk4poor
kshitijk4poor deleted the salvage/73639-fts-update-of branch August 5, 2026 07:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/perf Performance improvement or optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants