Skip to content

fix(runtime): make the recall embedding budget configurable and log its fallback - #2042

Merged
hidb4ai merged 2 commits into
oceanbase:masterfrom
Fengzdadi:fix/recall-embedding-timeout-setting
Oct 20, 2026
Merged

hidb4ai merged 2 commits into
oceanbase:masterfrom
Fengzdadi:fix/recall-embedding-timeout-setting

Conversation

@Fengzdadi

Copy link
Copy Markdown
Contributor

Which issue or RFC does this PR close?

Part of #2039. The issue also asks for a reason on PreparedContext and for a decision on the default value; both are left for discussion there (see below).

Rationale for this change

POST /v1/context/prepare bounds the query embedding of Atomic Memory and Topic Memory retrieval with two module constants of 0.25 s. A remote embedding provider needs longer (0.27–0.41 s measured through OpenRouter; 315–1019 ms in #1700), so _resolve_query_embedding quietly continued with lexical matching only, and a long Hook query returned empty although the Scope held the matching Memory. #1700 gave the host Hooks a 3 s request budget for exactly this round trip; the Server-side cap introduced with #1857 recreated the failure where the Hook budget cannot help, and nothing recorded it.

This PR makes the budget an operator setting and makes the degradation visible. The default stays 0.25 s, so existing deployments behave exactly as before until an operator raises it.

What changes are included in this PR?

  • RuntimeConfig.recall_embedding_timeout_seconds, read as POWERCONTEXT_SERVER_RUNTIME_RECALL_EMBEDDING_TIMEOUT_SECONDS (default 0.25, 0 < value <= 60, booleans rejected), replaces both constants. open_builtin_runtime passes it to BuiltinRuntime, which uses it for the Atomic Memory search and the Topic Memory search of a preparation. The budget applies to each search; a preparation searches the Scope and each of its context references in turn, so the waits add up. The configuration row says so.
  • Atomic Memory retrieval logs memory.search.embedding_fallback at WARNING when the budget expires or the provider is unavailable, with outcome=fallback, mode=text, error_code=inference_timeout or inference_unavailable, and unit=memory. It mirrors the existing topic_memory.search.embedding_fallback: fixed message, no query, exception or provider text, and only fields the operational allowlist already carries, so the formatter and the field contract are unchanged.
  • Documentation: one row in the configuration reference, English and Chinese.
  • Tests:
    • tests/e2e/test_recall_embedding_timeout.py drives prepare through open_builtin_runtime with an embedding stub that only slows or fails the query embedding: an expired budget still recalls the Memory lexically and emits the event while the query embedding never completes; a provider failure is classified by error_code and the JSON-serialized record carries no query or exception; a sufficient budget waits for the embedding and emits nothing.
    • tests/builtin/runtime/test_recall_embedding_timeout_config.py covers the default, the bounds, boolean rejection and the environment mapping.

Are there any user-facing changes?

  • New Server setting. The default is unchanged, so behavior is identical until an operator raises it. A deployment with a remote embedding provider should raise it and keep the summed waits inside the host's Hook request timeout (3 s on most hosts).
  • New WARNING event. With a persistently slow provider it fires once per Atomic Memory search, that is once per searched Scope, plus once for Topic Memory, per preparation.
  • No API, persisted format, or migration change.

Deliberately not in this PR, for discussion on #2039: the default value, and a reason field on PreparedContext. Every host plugin validates the prepare response strictly and rejects unknown fields, so the field needs the hosts updated first.

How was this change tested?

  • make check (lock, pre-commit hooks, ty check) in the branch's worktree.
  • uv run pytest tests/e2e/test_recall_embedding_timeout.py tests/builtin/runtime/test_recall_embedding_timeout_config.py tests/builtin/runtime/test_recall_channel_weights.py -q and, before the final review amendments, uv run pytest tests/builtin/runtime tests/test_server.py tests/test_server_logging.py tests/e2e/test_worker_logging.py -q.
  • The measurements in bug: prepare_context abandons the query embedding after 0.25 s, so Hook recall silently degrades to lexical search with a remote embedding provider #2039 (ten prepare replays: empty 0/10 behind a 300 ms delay on master, ready 10/10 with the budget at 2 s; paired harness ON sessions then injecting context instead of the model calling pc_search) were taken with the constant patched locally on the same call sites this setting now controls.
  • Not validated: the Docker-based paired harness and the host acceptance workflows were not run on this branch, and no OceanBase or seekdb backend run was made (the change does not touch storage).

AI usage statement

Claude Code (Claude Fable 5.1) helped investigate the recall path, measure the embedding latency, write the change, tests and documentation, run the local checks, review the result in a separate agent pass, and draft this description. I reviewed the code, the measurements and this text.

…ts fallback

prepare_context bounds the query embedding of Atomic Memory and Topic Memory
retrieval with a module constant of 0.25 s. A remote embedding provider needs
longer, so the preparation silently continued with lexical matching only and a
long Hook query returned empty although the Memory existed (oceanbase#2039).

- RuntimeConfig.recall_embedding_timeout_seconds, read as
  POWERCONTEXT_SERVER_RUNTIME_RECALL_EMBEDDING_TIMEOUT_SECONDS, replaces both
  constants. The default stays 0.25, so behavior is unchanged until an operator
  raises it.
- Atomic Memory retrieval logs memory.search.embedding_fallback at WARNING when
  the budget expires or the provider is unavailable, with the same fields as the
  Topic Memory event and without the query.
- Tests cover the fallback event, the setting's bounds and environment mapping,
  and the budget reaching the search through open_builtin_runtime.
- The configuration reference lists the setting in both locales.

Refs oceanbase#2039
Copilot AI balanced review requested due to automatic review settings October 17, 2026 07:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@AlexStocks AlexStocks added the 1.4.1 v1.4.1 release tracking label Oct 17, 2026

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

…ecall-embedding-timeout-setting

# Conflicts:
#	src/powercontext/builtin/runtime/atomic_memory.py
Copilot AI balanced review requested due to automatic review settings October 19, 2026 09:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@hireview001 hireview001 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  • CI failed: validate (failure).
  • CI failed: E2E harness (failure). Failed steps: validate / Validate harness and Compose definitions.

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@hidb4ai
hidb4ai merged commit bb9b075 into oceanbase:master Oct 20, 2026
39 of 41 checks passed
jasondeng1997 added a commit to jasondeng1997/powercontext that referenced this pull request Oct 21, 2026
…ceanbase#2039)

`prepare_context` bounded the query embedding, then said nothing when the bound
expired: `status: "empty"` read the same whether nothing matched or the semantic
path never ran, and oceanbase#2042's WARNING only reached the server log.

Add a nullable `reason` to `PreparedContext`, mirroring `BootstrapContext.reason`:

- `embedding_timeout` when `recall_embedding_timeout_seconds` expired,
  `embedding_unavailable` when the provider failed, `null` otherwise. The field
  stays outside the spec's `required` list, so a client written before it
  existed still parses a degraded response.
- The marker is filled from the same classification that emits the fallback
  event, carried from `_resolve_query_embedding` and the Topic Memory searcher
  through the recall round into the preparation; the log and the response cannot
  disagree about the cause.
- A preparation that never wanted vectors stays `null`: a keyword-only
  deployment is not degraded, only one that lost a channel it asked for.
- When both channels fail, a timeout outranks an unavailable provider, because
  the budget is the one an operator can raise.
jasondeng1997 added a commit to jasondeng1997/powercontext that referenced this pull request Oct 21, 2026
…ceanbase#2039)

`prepare_context` bounded the query embedding, then said nothing when the bound
expired: `status: "empty"` read the same whether nothing matched or the semantic
path never ran, and oceanbase#2042's WARNING only reached the server log.

Add a nullable `reason` to `PreparedContext`, mirroring `BootstrapContext.reason`:

- `embedding_timeout` when `recall_embedding_timeout_seconds` expired,
  `embedding_unavailable` when the provider failed, `null` otherwise. The field
  stays outside the spec's `required` list, so a client written before it
  existed still parses a degraded response.
- The marker is filled from the same classification that emits the fallback
  event, carried from `_resolve_query_embedding` and the Topic Memory searcher
  through the recall round into the preparation; the log and the response cannot
  disagree about the cause.
- A preparation that never wanted vectors stays `null`: a keyword-only
  deployment is not degraded, only one that lost a channel it asked for.
- When both channels fail, a timeout outranks an unavailable provider, because
  the budget is the one an operator can raise.
jasondeng1997 added a commit to jasondeng1997/powercontext that referenced this pull request Oct 21, 2026
…oceanbase#2039)

`prepare_context` bounded the query embedding, then said nothing when the bound
expired: `status: "empty"` read the same whether nothing matched or the semantic
path never ran, and oceanbase#2042's WARNING only reached the server log.

A request can now ask with `"explain": true`, and the response then carries
`reason`: `embedding_timeout` when `recall_embedding_timeout_seconds` expired,
`embedding_unavailable` when the provider failed.

- The field is opt-in, and absent even for an explaining request when nothing was
  lost. Every Host validates the injected envelope -- most by exact key set
  (zcode, opencode, dsh, pi, claude-code, codex, workbuddy, kimi, openclaw) -- so
  a fifth key on an ordinary preparation would break all of them for a fact none
  of them reads. RFC 1435 states the same rule for the injection value.
- The marker is filled from the same classification that emits the fallback
  event, carried from `_resolve_query_embedding` and the Topic Memory searcher
  through the recall round into the preparation, so the log and the response
  cannot disagree about the cause.
- A preparation that never wanted vectors stays silent: a keyword-only
  deployment is not degraded, only one that lost a channel it asked for.
- When both channels fail, a timeout outranks an unavailable provider, because
  the budget is the one an operator can raise.
jasondeng1997 added a commit to jasondeng1997/powercontext that referenced this pull request Oct 21, 2026
…oceanbase#2039)

`prepare_context` bounded the query embedding, then said nothing when the bound
expired: `status: "empty"` read the same whether nothing matched or the semantic
path never ran, and oceanbase#2042's WARNING only reached the server log.

A request can now ask with `"explain": true`, and the response then carries
`reason`: `embedding_timeout` when `recall_embedding_timeout_seconds` expired,
`embedding_unavailable` when the provider failed.

- The field is opt-in, and absent even for an explaining request when nothing was
  lost. Every Host validates the injected envelope -- most by exact key set
  (zcode, opencode, dsh, pi, claude-code, codex, workbuddy, kimi, openclaw) -- so
  a fifth key on an ordinary preparation would break all of them for a fact none
  of them reads. RFC 1435 states the same rule for the injection value.
- The marker is filled from the same classification that emits the fallback
  event, carried from `_resolve_query_embedding` and the Topic Memory searcher
  through the recall round into the preparation, so the log and the response
  cannot disagree about the cause.
- A preparation that never wanted vectors stays silent: a keyword-only
  deployment is not degraded, only one that lost a channel it asked for.
- When both channels fail, a timeout outranks an unavailable provider, because
  the budget is the one an operator can raise.
jasondeng1997 added a commit to jasondeng1997/powercontext that referenced this pull request Oct 21, 2026
…oceanbase#2039)

`prepare_context` bounded the query embedding, then said nothing when the bound
expired: `status: "empty"` read the same whether nothing matched or the semantic
path never ran, and oceanbase#2042's WARNING only reached the server log.

A request can now ask with `"explain": true`, and the response then carries
`reason`: `embedding_timeout` when `recall_embedding_timeout_seconds` expired,
`embedding_unavailable` when the provider failed.

- The field is opt-in, and absent even for an explaining request when nothing was
  lost. Every Host validates the injected envelope -- most by exact key set
  (zcode, opencode, dsh, pi, claude-code, codex, workbuddy, kimi, openclaw) -- so
  a fifth key on an ordinary preparation would break all of them for a fact none
  of them reads. RFC 1435 states the same rule for the injection value.
- The marker is filled from the same classification that emits the fallback
  event, carried from `_resolve_query_embedding` and the Topic Memory searcher
  through the recall round into the preparation, so the log and the response
  cannot disagree about the cause.
- A preparation that never wanted vectors stays silent: a keyword-only
  deployment is not degraded, only one that lost a channel it asked for.
- When both channels fail, a timeout outranks an unavailable provider, because
  the budget is the one an operator can raise.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.4.1 v1.4.1 release tracking

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants