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.
hidb4ai pushed a commit that referenced this pull request Oct 23, 2026
…#2039) (#2144)

* feat(context): report why a preparation lost its semantic recall path (#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 #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.

* fix(context): spell the lost-channel reason in the RFC 1435 vocabulary

`PreparedContext.reason` said `embedding_timeout` / `embedding_unavailable`
while the fallback events and RFC 1435's `SearchGroup.fallback_reason` already
said `inference_timeout` / `inference_unavailable`. One classification ended up
with two spellings on two surfaces of the same Server, which is exactly the
divergence the Receipt work would have to reconcile later.

Both now come from one function: the event `error_code` and the response
`reason` are the same value, and the two translation tables that only existed
to paper over the split are gone. The test that used to assert the two
spellings separately now asserts they are equal.

The failure belongs to the inference provider, not to the channel it was going
to feed, so the response names the provider's problem too.

* chore(ci): retrigger after an OceanBase lock-wait flake

`Acceptance (oceanbase)` failed this revision on
`test_atomic_memory_read_keeps_content_and_state_in_one_snapshot[oceanbase-get]`
with `(1205, 'Lock wait timeout exceeded')`, and the OceanBase migration job then
sat for 40 minutes. Both predate this branch and are unrelated to a vocabulary
change that touches no storage path; the same jobs passed on 1a3db93.

An author cannot re-run a fork PR's jobs, so this commit only retriggers CI.

* fix(client): keep `explain` off the wire until a Server knows it

`PREPARE_CONTEXT` serializes its defaults — an older Server needs the fields it
already understands — so the new `explain: false` travelled on every ordinary
preparation. A client upgraded ahead of its Server then got `422 invalid_request`
instead of a prepared context; the same payload without the key returns 200.
`false` and absence ask for exactly the same thing, so the client drops the key
unless the caller actually opted in, the way it already handles `assembly`.

Also stop calling the vocabulary "RFC 1435 minus `none`". The RFC lists four
`fallback_reason` spellings; this field publishes the two inference failures.
`none` travels as absence, and `reused_fts_fallback` is not a cause at this
level — a later round reusing a failed round's keyword-only outcome is still
explained by that round's `inference_*` value, which outranks it.

* fix(desktop): restore the license header in the regenerated contract files

The merge staged the first, broken regeneration and committed it: that run read
the generator script itself for the license block, so both files led with the
script's source instead of the Apache header. `License Check` flagged them and
`windows` failed to parse them (`Unterminated string` at line 15, statements in
an ambient context).

The fixed files — header taken from `desktop/scripts/generate.mjs`, digest of the
merged contract — were in the working tree but never added. This commits them.
`operations.ts` and `operations.json` now differ from master only in
`contractSha256`; `api.d.ts` only by `PreparedContext.reason`,
`PrepareContextRequest.explain` and the `PreparedContextReason` enum.
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