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."""