Skip to content

fix: return hits for contextual HSG queries instead of timing out - #213

Open
vincenzopalazzo wants to merge 2 commits into
CaviraOSS:mainfrom
vincenzopalazzo:fix/hsg-contextual-return-hits
Open

fix: return hits for contextual HSG queries instead of timing out#213
vincenzopalazzo wants to merge 2 commits into
CaviraOSS:mainfrom
vincenzopalazzo:fix/hsg-contextual-return-hits

Conversation

@vincenzopalazzo

Copy link
Copy Markdown
Contributor

What this fixes

The default MCP query is type=contextual. It was failing live with the tool
error HSG search timed out after 8000ms (the local fail-soft wrapper doing
its job), so the default path returned no hits at all.

Root cause: hsg_query embeds across all 5 sectors and runs vector search,
then awaits the post-hit reinforcement tail on the same request:
feedback EMA, salience bump, waypoint + associative propagation, and (when
regeneration is on) an embed network call per hit. That tail did not fit in
the MCP query budget, so a perfectly good search surfaced as a timeout.

What changed

Move the reinforcement tail off the request path. hsg_query now returns the
ranked hits as soon as ranking + caching is done and runs the writebacks
fire-and-forget. Each write stays independently guarded, so a failure in one
hit can never surface on the request path. Results are byte-for-byte the same.

  • TS (packages/openmemory-js/src/memory/hsg.ts): extracted
    reinforce_query_hits(top, tids) and call
    reinforce_query_hits(top, tids).catch(() => {}) before cache.set + return.
  • PY (packages/openmemory-py/src/openmemory/memory/hsg.py): extracted
    _reinforce_query_hits(top) and run it via asyncio.create_task(...) (+
    done-callback to consume the exception) before cache[cache_key] + return.

What's intentionally unchanged

  • Fail-soft stays. A failed/timed-out HSG query still returns a readable
    tool error, never Error POSTing to endpoint, and does not drop the
    StreamableHTTP session. That wrapper is local (not in upstream main) and was
    added by the companion commit that stopped the default query from dropping
    HTTP; this PR builds on top of it.
  • list / get / store / delete / factual behavior unchanged. Unbounded
    factual dumps remain capped if that patch is present.

Verification

  • npx tsc --noEmit -> exit 0
  • npx vitest run (openmemory-js) -> 14 files / 65 tests passed
  • python3 -m py_compile on hsg.py + decay.py -> OK
  • No Python test exercises hsg_query, so behavior parity is by inspection.

Out of scope

Issue #180 (not touching that file).

vincenzopalazzo and others added 2 commits August 27, 2026 21:27
Default openmemory_query runs HSG with no timeout, so a hung embed
or vector writeback dropped the Streamable HTTP POST. Catch HSG
failures as MCP tool errors, time out after 12s, and cap unpatterned
factual dumps at k.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hsg_query embedded across all 5 sectors, ran vector search, and then awaited
salience/feedback/waypoint/associative reinforcement (and, when regeneration
is on, an embed network call per hit) on the same request. That tail did not
fit inside the MCP fail-soft query budget, so a perfectly good search surfaced
as 'HSG search timed out after 8000ms'.

Extract the reinforcement tail into a helper and run it fire-and-forget AFTER
ranking + caching is done, so hsg_query returns the hits as soon as they are
ready. Fail-soft is untouched: a failed/timed-out query still returns a
readable tool error and never drops the StreamableHTTP session.

- TS: reinforce_query_hits(top, tids).catch(() => {})
- PY: asyncio.create_task(_reinforce_query_hits(top)) + done callback
- list/get/store/delete/factual behavior unchanged

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ac7aff9cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +64 to +68
} finally {
if (timer) clearTimeout(timer);
// Timed-out work may still reject later; swallow so the HTTP
// session is not taken down by an unhandled rejection.
p.catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel HSG work when its timeout wins

When an embedding or vector-search promise never settles, Promise.race returns the timeout error but leaves p running; the added .catch only observes a later rejection and does not cancel it. Because the underlying hsg_query therefore never reaches its finally block, its active_queries slot remains occupied, and after env.max_active timed-out calls every subsequent contextual query is rejected by the rate limiter until the hung provider recovers or the process restarts.

Useful? React with 👍 / 👎.

patterned = _has_fact_pattern(fp)
fact_limit = max(top_k, 32) if patterned else top_k
try:
facts = await asyncio.wait_for(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run the factual scan outside the event loop

For a large or locked SQLite database, this timeout cannot fire: query_facts_at_time performs its entire query through synchronous db.fetchall at temporal_graph/query.py:38 without yielding, so asyncio.wait_for cannot regain control until the scan and sort have already completed. Consequently a slow factual or unified query still blocks the MCP server beyond OM_MCP_QUERY_TIMEOUT_MS, which defeats the fail-soft behavior this path introduces.

Useful? React with 👍 / 👎.

Comment on lines +678 to +679
_task = asyncio.create_task(_reinforce_query_hits(top))
_task.add_done_callback(lambda t: t.exception())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound detached reinforcement tasks

When completed queries include compressed memories whose on_query_hit must re-embed them, every query now creates a detached task that may remain pending on the embedding provider while new queries continue spawning more tasks. The previous awaited path provided backpressure, but there is now no semaphore, queue, or tracked task set, so sustained distinct queries can accumulate unbounded reinforcement work and overload the provider or process even though the MCP request itself returns promptly.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant