From 651cff4273572c2c99500e9a08a458463a69d395 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:38:21 -0700 Subject: [PATCH] fix(desktop): treat built-in memory as built-in in provider panel (#49513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Built-in memory (MEMORY.md/USER.md) is controlled by memory_enabled, not memory.provider — but the desktop dropdown offered 'builtin' as a normal provider-plugin value and gave it plugin-shaped affordances (config panel, OAuth connect row), and the empty sentinel rendered as '(none)' even though built-in memory was active. - Label the empty memory.provider option 'Built-in only' (all locales). - Drop the literal 'builtin' option from the desktop ENUM_OPTIONS and the backend config-schema select; _normalize_memory_provider_name already maps legacy builtin/built-in/none values to ''. A stored legacy literal stays visible via enumOptionsFor's current-value passthrough. - Gate MemoryConnect and ProviderConfigPanel behind a new isExternalMemoryProvider() helper so built-in aliases never get provider-plugin affordances. --- .../src/app/settings/config-settings.tsx | 17 +++++++++---- apps/desktop/src/app/settings/constants.ts | 5 +++- apps/desktop/src/app/settings/helpers.test.ts | 24 ++++++++++++++++++- apps/desktop/src/app/settings/helpers.ts | 13 ++++++++++ apps/desktop/src/i18n/en.ts | 1 + apps/desktop/src/i18n/ja.ts | 1 + apps/desktop/src/i18n/types.ts | 1 + apps/desktop/src/i18n/zh-hant.ts | 1 + apps/desktop/src/i18n/zh.ts | 1 + hermes_cli/web_server.py | 9 ++++--- tests/hermes_cli/test_web_server.py | 8 ++++--- 11 files changed, 69 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/app/settings/config-settings.tsx b/apps/desktop/src/app/settings/config-settings.tsx index 46732d21bd14a..571a3812b89bc 100644 --- a/apps/desktop/src/app/settings/config-settings.tsx +++ b/apps/desktop/src/app/settings/config-settings.tsx @@ -21,7 +21,14 @@ import { PanelEmpty } from '../overlays/panel' import { CONTROL_TEXT, EMPTY_SELECT_VALUE, FIELD_DESCRIPTIONS, FIELD_LABELS } from './constants' import { FallbackModelsField } from './fallback-models-field' import { fieldCopyForSchemaKey } from './field-copy' -import { enumOptionsFor, getNested, prettyName, sectionFieldEntries, setNested } from './helpers' +import { + enumOptionsFor, + getNested, + isExternalMemoryProvider, + prettyName, + sectionFieldEntries, + setNested +} from './helpers' import { MemoryConnect } from './memory/connect' import { ProviderConfigPanel } from './memory/provider-config-panel' import { ModelSettings, ModelSettingsSkeleton } from './model-settings' @@ -134,7 +141,9 @@ function ConfigField({ ? (optionLabels?.[option] ?? prettyName(option)) : schemaKey === 'display.personality' ? c.none - : c.noneParen} + : schemaKey === 'memory.provider' + ? c.builtinOnly + : c.noneParen} ))} @@ -457,7 +466,7 @@ export function ConfigSettings({
) : undefined } @@ -472,7 +481,7 @@ export function ConfigSettings({ schemaKey={key} value={getNested(config, key)} /> - {key === 'memory.provider' && typeof getNested(config, key) === 'string' && getNested(config, key) ? ( + {key === 'memory.provider' && isExternalMemoryProvider(getNested(config, key)) ? ( ) : null}
diff --git a/apps/desktop/src/app/settings/constants.ts b/apps/desktop/src/app/settings/constants.ts index cb87dd2b953d4..2eecd030b0e06 100644 --- a/apps/desktop/src/app/settings/constants.ts +++ b/apps/desktop/src/app/settings/constants.ts @@ -247,7 +247,10 @@ export const ENUM_OPTIONS: Record = { 'code_execution.mode': ['project', 'strict'], 'context.engine': ['compressor', 'default', 'custom'], 'delegation.reasoning_effort': ['', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], - 'memory.provider': ['', 'builtin', 'honcho', 'hindsight'], + // Built-in memory is not a provider plugin: the empty sentinel renders as + // "Built-in only" and a legacy literal `builtin` value is only kept visible + // via enumOptionsFor's current-value passthrough (#49513). + 'memory.provider': ['', 'honcho', 'hindsight'], // Terminal execution backends — kept in sync with the dispatch ladder in // tools/terminal_tool.py::_create_environment (local/docker/singularity/ // modal/daytona/ssh). Remote backends need extra env (image, tokens, host). diff --git a/apps/desktop/src/app/settings/helpers.test.ts b/apps/desktop/src/app/settings/helpers.test.ts index a010076cdfffd..7b618822e8366 100644 --- a/apps/desktop/src/app/settings/helpers.test.ts +++ b/apps/desktop/src/app/settings/helpers.test.ts @@ -6,6 +6,7 @@ import { defineFieldCopy, fieldCopyForSchemaKey, schemaKeyToFieldCopyKey } from import { enumOptionsFor, getNested, + isExternalMemoryProvider, providerGroup, sectionFieldEntries, setNested, @@ -17,7 +18,28 @@ describe('settings helpers', () => { it('lists the desktop memory provider options in their declared order', () => { const options = enumOptionsFor('memory.provider', '', {}) - expect(options).toEqual(['', 'builtin', 'honcho', 'hindsight']) + // Built-in memory is not a provider plugin; the empty sentinel is the + // only built-in-shaped entry (#49513). + expect(options).toEqual(['', 'honcho', 'hindsight']) + }) + + it('keeps a legacy literal builtin value visible as the current selection', () => { + const options = enumOptionsFor('memory.provider', 'builtin', {}) + + expect(options).toEqual(['', 'honcho', 'hindsight', 'builtin']) + }) + + describe('isExternalMemoryProvider', () => { + it('treats only real plugin names as external providers', () => { + expect(isExternalMemoryProvider('honcho')).toBe(true) + expect(isExternalMemoryProvider('hindsight')).toBe(true) + }) + + it('treats built-in aliases and empty values as not external', () => { + for (const value of ['', 'builtin', 'built-in', 'Builtin', 'none', ' ', undefined, null, 7]) { + expect(isExternalMemoryProvider(value)).toBe(false) + } + }) }) describe('defineFieldCopy', () => { diff --git a/apps/desktop/src/app/settings/helpers.ts b/apps/desktop/src/app/settings/helpers.ts index 16bee76b4ea50..2c3b8e9b01d1d 100644 --- a/apps/desktop/src/app/settings/helpers.ts +++ b/apps/desktop/src/app/settings/helpers.ts @@ -182,3 +182,16 @@ export function enumOptionsFor( return current && !opts.includes(current) ? [...opts, current] : opts } + +// Built-in memory (MEMORY.md/USER.md) is controlled by memory_enabled, not +// memory.provider — only a real external plugin name gets provider-shaped +// affordances (config panel, OAuth connect). See #49513. +export function isExternalMemoryProvider(value: unknown): value is string { + if (typeof value !== 'string') { + return false + } + + const normalized = value.trim().toLowerCase() + + return normalized !== '' && normalized !== 'builtin' && normalized !== 'built-in' && normalized !== 'none' +} diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index 62bf969322515..4fc5b8ebbeae2 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -513,6 +513,7 @@ export const en: Translations = { config: { none: 'None', noneParen: '(none)', + builtinOnly: 'Built-in only', notSet: 'Not set', commaSeparated: 'comma-separated values', loading: 'Loading Hermes configuration...', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 31f6d486208a4..e7fc815eb238f 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -606,6 +606,7 @@ export const ja = defineLocale({ config: { none: 'なし', noneParen: '(なし)', + builtinOnly: '内蔵のみ', notSet: '未設定', commaSeparated: 'カンマ区切りの値', loading: 'Hermes の設定を読み込み中...', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index 851c794c20913..87b50a2f93822 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -424,6 +424,7 @@ export interface Translations { config: { none: string noneParen: string + builtinOnly: string notSet: string commaSeparated: string loading: string diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index c8e3f04a33ccc..431f9d02b45cb 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -594,6 +594,7 @@ export const zhHant = defineLocale({ config: { none: '無', noneParen: '(無)', + builtinOnly: '僅內建', notSet: '未設定', commaSeparated: '逗號分隔的值', loading: '正在載入 Hermes 設定...', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 8928b646158ff..5b051c6cd812b 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -705,6 +705,7 @@ export const zh: Translations = { config: { none: '无', noneParen: '(无)', + builtinOnly: '仅内置', notSet: '未设置', commaSeparated: '逗号分隔的值', loading: '正在加载 Hermes 配置...', diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 9280b97ec51fd..f782c4e61f035 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -622,10 +622,13 @@ def _memory_provider_options() -> List[str]: """Discovered memory providers for the ``memory.provider`` select. Directory-scan only (no provider imports), so it's safe at module import - time. ``""`` (built-in) is always first; discovery failures degrade to the - bundled defaults rather than dropping the field. + time. ``""`` (built-in only) is always first; discovery failures degrade to + the bundled defaults rather than dropping the field. The literal + ``builtin`` alias is deliberately NOT offered — built-in memory is not a + provider plugin, and ``_normalize_memory_provider_name`` already maps any + legacy ``builtin``/``built-in``/``none`` value back to ``""`` (#49513). """ - options = ["", "builtin"] + options = [""] try: from plugins.memory import list_memory_provider_names diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index fd9a414d948d9..07ba06a25db85 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -4148,10 +4148,12 @@ class TestBuildSchemaFromConfig: assert entry["type"] == "select" assert entry["category"] == "memory" options = entry["options"] - # Built-in sentinel first, plus at least one discovered provider. + # Built-in-only sentinel first, plus at least one discovered provider. + # The literal "builtin" alias must NOT be offered — built-in memory is + # not a provider plugin (#49513). assert options[0] == "" - assert "builtin" in options - assert len(options) >= 3 + assert "builtin" not in options + assert len(options) >= 2 def test_memory_provider_options_cover_discovered_providers(self): """Every provider the /api/memory endpoint can activate is selectable."""