Skip to content

[Feat]: Make the semantic ingestion poll interval configurable (currently hardcoded at 2s) #1699

Description

@leomem

Is your feature request related to a problem?

The semantic memory background ingestion loop polls on a fixed 2-second interval that cannot be changed by configuration. Every sibling knob that governs the same loop is configurable; this one is the exception.

SemanticService.Params declares the value with a default at semantic_memory.py:98:

feature_update_interval_sec: float = 2.0

It reaches the loop via semantic_memory.py:124, and the loop sleeps on it at :808 (idle path) and :836 (clean-pass path).

The only production construction site is semantic_manager.py:245, which passes the three neighbouring knobs and omits this one:

uningested_time_limit=self._conf.ingestion_trigger_age,
uningested_message_limit=self._conf.ingestion_trigger_messages,
max_features_per_update=self._conf.max_features_per_update,

So the 2.0 default always wins. Grepping the tree for feature_update_interval_sec returns the declaration, that one assignment, and three test files that set it to 0.05 either through Params directly or by assigning the private attribute (test_memmachine_integration.py:165). There is no config key, no environment variable, and no field on the v2 config API.

Operators therefore cannot trade ingestion latency against database load. Every deployment scans for dirty sets twice a second whether or not there is work, and the only way to change that is to patch the source.

Describe the solution you'd like

Add a poll interval to SemanticMemoryConf alongside the triggers it belongs with (common/configuration/__init__.py:167-183) and wire it through the manager:

ingestion_poll_interval_seconds: float = Field(
    default=2.0,
    description=(
        "How often the background ingestion loop checks for sets with "
        "uningested messages. Larger values reduce idle database load at "
        "the cost of ingestion latency."
    ),
    gt=0,
)
# semantic_manager.py
feature_update_interval_sec=self._conf.ingestion_poll_interval_seconds,

Keeping 2.0 as the default makes this backward compatible. ingestion_trigger_messages and ingestion_trigger_age_seconds are already exposed over the v2 config API (config_spec.py:562-566), so exposing this one there too would be consistent, though a plain config key covers the need.

Describe alternatives you've considered

Leaving it hardcoded and relying on the work already done for #1251. That issue landed four of its five proposed fixes, and they help a great deal: the GROUP BY/HAVING rewrite (#1253), the (set_id, ingested) composite index at sqlalchemy_pgvector_semantic.py:168, purge_ingested_rows in the loop, and the error backoff that doubles to a 60s ceiling. What none of them give an operator is a way to say "poll less often". The per-query cost dropped; the query rate did not.

Patching the constant in a fork works but does not survive upgrades.

Additional context

Context from prior reports:

  • Background ingestion polling causes unbounded database load growth #1251 (closed, priority: high, v0.3.3) measured the cost of this cadence in production: 14,055 rows in set_ingested_history, Aurora ACU climbing from 2.5 to 12.3 and hitting the 16 ACU cap, with roughly $416/month attributed to the polling loop while no users were active. Its fifth proposed fix, running one ingestion loop instead of one per uvicorn worker, was set aside because MemMachine assumes a single server instance per database, so a configurable interval is the remaining lever for anyone whose load profile does not suit a 2-second poll.
  • [Bug]: Excessive background semantic/profile memory LLM calls #1453 (open) reports background extraction generating around $200 in LLM cost after roughly 40 questions over four PDFs. That is about call volume rather than poll cadence, though the two compound.
  • [Bug]: Ingest Interval not triggered anymore #753 (closed) is a reminder that the current value is not discoverable. The reporter guessed "(I think it used to be) 2min" while filing a bug against this very loop.

Secondary finding, same area and small enough to fold into the same change. consolidation_threshold (semantic_memory.py:96, default 20) is also absent from the semantic_manager.py construction site, and SemanticService assigns it to self._consolidation_threshold at :134 and never reads it again. The IngestionService.Params built at :786 does not pass consolidated_threshold, so the ingestion service falls back to its own default of 20 (semantic_ingestion.py:82). The attribute on SemanticService is dead. Either wire it through or delete it.

Verified against 73937ac on main.

Activity

  1. honggyukim commented on Sep 21, 2026

    @honggyukim
    Collaborator

    @leomem Thanks for raising this issue!

  2. added 4 commits that reference this issue on Sep 21, 2026
    7fbbe69
    cb32838
    6bb3859
    378abdc
  3. yiweizh-memverge commented on Sep 22, 2026

    @yiweizh-memverge

    I've looked into this feat request and created a PR for this.

  4. added a commit that references this issue on Sep 28, 2026
    2fce7e6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions