Skip to content

fix(*): close the embedding pin's upgrade paths and its reporting - #453

Open
gloryfromca wants to merge 6 commits into
mainfrom
fix/embedding_pin_upgrade_paths
Open

gloryfromca wants to merge 6 commits into
mainfrom
fix/embedding_pin_upgrade_paths

Conversation

@gloryfromca

Copy link
Copy Markdown
Member

Summary

Five upgrade-path and reporting defects in the embedding pin that #414 landed. Three were named as bounded follow-ups in that PR's final review; two came out of driving the acceptance environment afterwards, and both of those were invisible to the unit suite because every fixture already held the new shape.

The two found by acceptance:

  • A config still holding the retired endpoint shape does not load at all. The block used to carry the address and the key; EmbeddingConfig forbids extras, so raven doctor ends in a traceback rather than in a degraded feature, and so does every other command that reads the extension blocks. The migration takes those keys now: the address is adopted onto whichever configured provider answers there, and when nothing answers the whole block goes rather than half of it, with the dropped address named in the log. A model left with no provider is a pin every reader resolves to nothing while every screen reads it as configured.
  • A pin could not name a provider this config holds. The provider half was checked against the registry alone, and Raven carries no spec for every vendor LiteLLM can reach. A pin the wizard had stored and every reader resolved came back from the settings page as a provider that does not exist, leaving the endpoint uneditable on the page that exists to edit it. A section the operator wrote counts as proof the vendor exists; a name nothing holds is still refused as a typo.

The three from review:

  • Clearing the pin was a no-op. The picker's inherit option sends both halves empty and the writer dropped empty values before deciding anything, so the call returned applied while the file kept the old pair.
  • Doctor reported a move it had not made. The endpoint only moves when a configured provider answers at the retired address, and the return value was discarded. The unmade move stays in the remaining list now, with a line saying why.
  • The canonical SkillForge glossary entry still said skillForge.everos and attributed the local extraction pipeline to an embedded copy of the memory backend.

Type

  • Fix

Verification

  • uv run --all-extras pytest -q -- 23058 passed, 110 skipped, 4 failed; the four are the same pre-existing failures main carries (test_agent_loop_token_budget, test_agents_code_launcher, test_agents_oncall_launcher, test_ppt_engine_image_search), matched by test id against a run on the merge base.

  • uv run ruff check . and uv run ruff format --check . -- clean.

  • make check-commits, make check-source-language, make check-large-files -- clean.

  • Every fix was reproduced before and after against a real config or a real gateway, not only through its test: the unloadable config came from the acceptance home itself and raven doctor was run on it before and after; the provider refusal was reproduced over a real WebSocket to a running gateway; clearing and the doctor report were each run end to end.

  • Each new test was checked by removing its fix and confirming it fails.

  • Wider acceptance on the running product, with the memory plugin and without it: a gateway with no memory plugin installed built a knowledge base whose width (4096) was measured from the live endpoint and answered a query sharing almost no keywords with the source (score 0.728); the real TUI recalled a fact stored in an earlier turn, with the recall payload in the audit artifact as evidence rather than the model's answer.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

Two changes widen what is accepted rather than narrowing it: a provider section counts alongside the registry, and a migration edits a block it previously left alone. The migration only runs for a config carrying the retired keys and is stamped, so it runs once; a config already in the current shape is untouched. Rolling back any single commit is safe -- they do not depend on each other.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Blocking: the retired endpoint repair must run for configs that have already consumed migration generation 8.

I reviewed the complete github/main...HEAD diff, the config/provider/settings/doctor callers, the history that introduced migration generation 8, the repository rules and runtime vocabulary, backward compatibility, test changes, and the provider/config architecture boundary. One upgrade-path defect is blocking inline.

Verification:

  • uv run --extra dev pytest tests/test_cli_doctor_commands.py tests/test_config_raven_loader.py tests/test_rpc_settings.py -x: 158 passed.
  • uv run --extra dev pytest tests/test_config_loader.py tests/test_config_update_providers.py tests/test_knowledge_embedding.py tests/test_cli_onboard_commands.py -x: 567 passed.
  • git diff --check, Ruff check/format, source-language, large-file, and commit-message checks passed.
  • The first default-environment test attempt stopped with four setup errors because the optional raven_everos workspace package was absent; rerunning with the repository's dev extra installed that package and produced the passing results above.

I also traced two narrower candidates and did not make them blockers: provider-name spelling aliases remain a pre-existing corner that this change improves for ordinary raw slugs, and endpoint-address ambiguity requires multiple configured providers sharing one URL. Neither outweighs the concrete v8 upgrade failure.

Comment thread raven/config/loader.py Outdated

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

The generation-9 split fixes the only blocking upgrade path from the previous review. I re-read the new delta in the context of the full github/main...HEAD change and covered the migration callers/history, project rules and runtime vocabulary, backward compatibility, test changes, and the provider/config architecture boundary. The added regression exercises the formerly failing v8-stamped state without weakening existing coverage.

Verification:

  • Exact v8-stamped reproduction: migrated to {model, provider}, stamp advanced to 9, and both Raven/base loads succeeded.
  • uv run --extra dev pytest tests/test_config_raven_loader.py tests/test_config_loader.py tests/test_config_update_providers.py tests/test_knowledge_embedding.py tests/test_rpc_settings.py tests/test_cli_doctor_commands.py tests/test_cli_onboard_commands.py -x: 726 passed.
  • git diff --check, Ruff check/format, source-language, large-file, and commit-message checks passed.

The previously reported CI shard signal is a timing-gate-only result with zero test failures in an untouched area, so it does not hold this change. The blocker thread is replied to and resolved.

@gloryfromca
gloryfromca force-pushed the fix/embedding_pin_upgrade_paths branch from 7f4c317 to 6dd6db7 Compare September 17, 2026 06:35

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

Reviewed the full github/main...HEAD diff and the new post-rebase commit, including the config/RPC/UI callers, migration and load paths, relevant history, backward compatibility, and the architecture terms in CONTEXT-MAP.md / CONTEXT.md. I also checked the repository rules in AGENTS.md / CLAUDE.md and looked for weakened tests; the added regressions exercise the echoed provider address and the warning returned to the save path.

Verification:

  • uv run --extra dev pytest tests/test_rpc_settings.py tests/test_config_raven_loader.py tests/test_cli_doctor_commands.py tests/test_config_loader.py tests/test_config_update_providers.py tests/test_knowledge_embedding.py tests/test_cli_onboard_commands.py -x: 730 passed.
  • npm test -- src/features/settings/SettingsPage.test.tsx: 123 passed.
  • npm run type-check, npm run gen:check, and node --check src/live/120-settings.js: passed.
  • Ruff check/format, git diff --check, source-language, large-file, and commit-message checks: passed.

npm ci emitted Vitest's engine warning for the local Node 23.10.0 runtime, but dependency installation and all web checks above completed successfully.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

The new delta removes a stale instruction to configure the retired EverOS embedding block. KnowledgeManager still obtains its endpoint through Raven's embedding pin, so directing the operator to choose the model and its provider matches the active configuration path.

Reviewed the complete github/main...HEAD diff, the affected callers and history, backward compatibility, test integrity, and the repository and architecture rules in AGENTS.md / CLAUDE.md / CONTEXT-MAP.md / CONTEXT.md. No tests were weakened.

Verification: uv run --extra dev pytest tests/test_knowledge_manager.py tests/test_knowledge_embedding.py -x passed 73 tests. Ruff check/format, git diff --check, and the source-language, large-file, and commit-message checks also passed.

gloryfromca and others added 6 commits September 17, 2026 20:24
The block used to carry the address and the key itself. `EmbeddingConfig`
forbids extras, so a config still holding them does not load at all --
`raven doctor` ends in a traceback rather than in a degraded feature, and
so does every other command that reads the extension blocks. A config
written by an earlier build of this change is exactly that shape, which
is how the acceptance environment found it.

The migration takes them now. The address is adopted onto whichever
configured provider answers there, which is the same question doctor's
own migration asks, so the reverse lookup moves to where the providers
live and both callers use it. When nothing answers there, the whole
block goes rather than half of it: a model left with no provider is a
pin every reader resolves to nothing while every screen reads it as
configured, and the dropped address is named in the log so it can be
configured again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Raven carries no spec for every vendor LiteLLM can reach, and the pin's
provider half was checked against the registry alone. So a pin the
wizard had stored and every reader resolved -- `provider: deepinfra`,
with its key and address sitting in the config -- came back from the
settings page as a provider that does not exist. The endpoint was
uneditable on the page that exists to edit it.

A section the operator wrote is proof the vendor exists, so it counts
alongside the registry. The check still earns its keep: a name nothing
holds is a typo, and it is still refused rather than surfacing later as
a silent fallback to the conversation's model.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clearing the pin was a no-op. The picker's inherit option sends both
halves empty, and the writer dropped empty values before deciding
anything -- so the call returned applied, the file kept the old pair, and
the picker snapped back on the next load. Editing the file by hand was
the only way to unset it. Both halves empty now removes the block and
answers with what it held.

Doctor reported a move it had not made. The endpoint only moves when a
configured provider answers at the address the retired block named, and
the return value was discarded: an operator was told the endpoint had
been carried across while it sat exactly where it was, and the next run
offered the same fix again. The unmade move stays in the remaining list
now, with a line saying why and what to do about it.

And the canonical SkillForge entry still called the local extraction
pipeline `skillForge.everos` and said it belonged to an embedded copy of
the memory backend. It calls no service and needs no plugin, which is
why the key was renamed; the glossary now says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…med v8

Generation 8 is on main, and it shipped the move without this repair. So a
config can carry the stamp and the shape that cannot load at the same
time: the base loader pops the extension blocks, validates what is left,
and stamps 8, while the extension read then fails on the two retired
fields. A repair gated on 8 never reaches that install, which is every
install of the merged build whose config was written by an earlier one.

Its own generation, so those get it. Reproduced on a config stamped 8:
before, `raven doctor` ended in a traceback and the file was unchanged;
after, the endpoint is adopted onto the provider that answers at its
address, or the block goes whole, and the stamp moves to 9.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three defects, all found by driving the real screens rather than the
layer underneath them.

The sentence was the one English paragraph on a translated screen: it
was built by interpolating two model names into a finished string, so
the catalogue had nothing to look up. It is a catalogue key with
placeholders now, which also gives the settings page a translated
warning for free.

And the page threw it away. Both save paths awaited the call without
taking its answer and toasted a fixed "saved", so the field the server
had started returning reached nobody. The memory card's embedding row
also carries the standing note the default-models row already had, in
the same wording, so the risk is stated before a save as well as after
one.

Saving that row was refused outright. It renders the resolved address as
a defaultValue, so every save carries it back whether or not anyone
touched it; changing only the model came back as an error naming a field
the reader had not edited. An address equal to what the provider already
answers with is this page echoing what it was shown -- the same shape as
the redacted key the borrow path already drops. One that differs is an
instruction, and is still refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ure it

It said to set `[embedding]` in the EverOS config -- a file nothing reads
for this any more, and a file a deployment without the memory plugin does
not have at all. The endpoint is a model and the provider that serves it,
so the error says that instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gloryfromca
gloryfromca force-pushed the fix/embedding_pin_upgrade_paths branch from 582a977 to dcbf0f5 Compare September 17, 2026 12:25

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

After refreshing github/main, this revision is a rebase-only update: git range-diff marks all six PR commits patch-identical to the previously reviewed revision, with no conflict-resolution changes.

Rechecked the complete diff, affected callers and history, backward compatibility, test integrity, and the repository/architecture rules in AGENTS.md / CLAUDE.md / CONTEXT-MAP.md / CONTEXT.md. The previously settled migration thread remains resolved.

Verification:

  • The focused Python suite passed all 772 tests in parallel. That run emitted one BaseEventLoop.__del__ invalid-file-descriptor warning during teardown; the identical suite then passed all 772 tests serially without reproducing the warning.
  • The settings UI suite passed 123 tests; TypeScript, generated-client consistency, and JavaScript syntax checks passed.
  • Ruff check/format, git diff --check, source-language, large-file, and commit-message checks passed.

@0xKT

0xKT commented Sep 17, 2026

Copy link
Copy Markdown
Member

**Not a blocker -- the pin paths are closed and I could not find one left open. One composition deletes the whole pin where base refused the same call, and says "Saved".

Two new, individually reasonable changes compose into a silent delete

raven/rpc/methods/console.py:1257-1258, new here, pops the echoed address:

shown = host_embedding_section().get("base_url", "")
if shown and clean.get("base_url", "").rstrip("/") == shown.rstrip("/"):
    clean.pop("base_url")

raven/config/update.py:463-465, also new here, clears the pin when the caller sent fields but all of them were empty:

if given and not clean:
    return _clear_embedding_pin(path)

In between, console.py:1268 turns an empty clean into pin = {"model": None, "provider": None}. So: the address is popped, clean becomes {}, the pin becomes two Nones, given is truthy and clean is empty -- and the whole embedding block is removed. The response is {"applied": True} with no warning key, and 120-settings.js:217 toasts the ordinary saved string.

It is silent by construction: embedding_model_change:574 is if not was or not now or was == now: return '', so the sentence this PR exists to deliver -- "the cost of moving the embedding model reaches the person" -- can never fire on the largest move of all, the model going to nothing.

And it bypasses the module's own rule. _refuse_a_pin_that_cannot_embed (update.py:519-525) says:

Both halves or neither. [...] A provider with nothing to run is the same write from the other end: accepted, reported as saved, and storing nothing anyone can use.

That guard is reached at update.py:471. The clear branch returns at :465, six lines before it.

Base refuses the same call. On de3870a5 the identical payload raises ConfigValidationError: base_url belongs to the provider, not to raven's embedding endpoint and the pin survives. So this is a behaviour change introduced here, not a pre-existing shape.

Where I disagree with how reachable this is

The pass described the user as one who "never touched that field", with "the model input left empty". I checked, and that is not the resting state of the form:

<input ref={model} type="text" defaultValue={cur.model || ''} ... />   // SettingsPage.tsx:2281
<input ref={base}  type="text" defaultValue={cur.base_url || ''} ... /> // SettingsPage.tsx:2295

Both inputs are pre-filled. On a Save with nothing touched, save() includes fields.model (:2253), so clean is not empty after the pop and the clear branch never runs. I walked the neighbouring states too:

  • provider credential unusable -> host_embedding_section() returns {} (its docstring: "Empty when nothing is pinned or the provider has no usable credential"), so shown is empty, the pop is guarded off, and all three inputs render blank -- the empty-save guard at :2259 then stops the call before it leaves the page.
  • only an API key typed -> stray catches it and the call is refused.

So the real path is: a user who clears the model box and saves. That is a plausible thing to do when you mean change the model, and what you get is the entire pin deleted with a success toast. It is narrower than "an untouched save", which is why I am not holding the merge button -- and it is still worth closing, because the difference between "unset the pin" and "I was about to type a new model" is exactly what a confirmation or a warning exists to distinguish.

Letting _clear_embedding_pin go through embedding_model_change (or giving the clear its own sentence) would make this PR's own promise cover its own largest case.

@gloryfromca

Copy link
Copy Markdown
Member Author

No blockers; suggestions only, and they are marked inline.

Confirmed. With an existing {model, provider} pin and the provider's resolved address, the exact RPC payload {"section":"embedding","fields":{"base_url":"<shown address>"}} returns {"applied":true}, removes the whole embedding block, and supplies no warning. That is the payload the form produces when the operator clears the prefilled model but leaves the prefilled address untouched.

This changes the standing stance from clean to nonblocking, agreeing with the report's classification. A follow-up should pin this exact composition and either require an explicit clear action or return a warning that says the endpoint was removed, rather than the ordinary Saved toast.

Verification: the direct temporary-config reproduction observed the deletion and response above; tests/test_rpc_settings.py passed 56 tests and the settings UI suite passed 123 tests.

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.

2 participants