Skip to content

Commit d5dc5a8

Browse files
committed
Update bloat_min_bytes configuration and enhance MaintenanceSection
- Increased the default value of `bloat_min_bytes` from 2048 to 2400 to improve memory management. - Updated the diagnostic view and related logic to reflect the new threshold. - Enhanced the MaintenanceSection in the frontend to include preset options for bloat thresholds and improved user interface elements. - Modified localization files to update labels and descriptions related to memory audit and bloat thresholds. - Added tests to validate the new functionality and ensure proper behavior of the MaintenanceSection.
1 parent 17fbbba commit d5dc5a8

7 files changed

Lines changed: 207 additions & 58 deletions

File tree

backend/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def _default_database_url() -> str:
5656
"cors_origins": None,
5757
"public_readonly_mcp": False,
5858
"locale": None,
59-
"bloat_min_bytes": 2048,
59+
"bloat_min_bytes": 2400,
6060
}
6161

6262
_ENV_MAP: dict[str, str] = {

backend/system_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ async def generate_diagnostic_view(
401401
import config
402402

403403
if bloat_min_bytes is None:
404-
bloat_min_bytes = config.get("bloat_min_bytes") or 2048
404+
bloat_min_bytes = config.get("bloat_min_bytes") or 2400
405405

406406
graph = get_graph_service()
407407

backend/tests/api/test_api_routes.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,10 @@ def test_config_backfills_bloat_min_bytes(tmp_path, monkeypatch):
326326
monkeypatch.setattr(config, "CONFIG_PATH", test_config_path)
327327
config._invalidate()
328328

329-
# 内存补齐生效:config.get() 和 config.get_all() 均可获取默认值 2048
329+
# 内存补齐生效:config.get() 和 config.get_all() 均可获取默认值 2400
330330
val = config.get("bloat_min_bytes")
331-
assert val == 2048
332-
assert config.get_all().get("bloat_min_bytes") == 2048
331+
assert val == 2400
332+
assert config.get_all().get("bloat_min_bytes") == 2400
333333

334334
# 旧的配置文件在磁盘上保持原样,未进行物理写入,避免只读文件系统等问题
335335
saved_data = json.loads(test_config_path.read_text())
@@ -346,7 +346,7 @@ async def test_settings_api_bloat_min_bytes(api_client, monkeypatch, tmp_path):
346346

347347
get_res = await api_client.get("/settings")
348348
assert get_res.status_code == 200
349-
assert get_res.json()["settings"]["bloat_min_bytes"] == 2048
349+
assert get_res.json()["settings"]["bloat_min_bytes"] == 2400
350350

351351
invalid_res = await api_client.put("/settings", json={"bloat_min_bytes": 0})
352352
assert invalid_res.status_code == 422

frontend/src/features/settings/MaintenanceSection.jsx

Lines changed: 115 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import React, { useState, useEffect } from 'react';
2-
import { Save, AlertTriangle } from 'lucide-react';
2+
import { Save, AlertTriangle, Sparkles, Check, FileText } from 'lucide-react';
33
import { useTranslation } from 'react-i18next';
44
import { toast } from '../../components/Toast';
55

66
export default function MaintenanceSection({ settings, onSave }) {
77
const { t } = useTranslation();
8-
const [bloatMinBytes, setBloatMinBytes] = useState('2048');
8+
const [bloatMinBytes, setBloatMinBytes] = useState(
9+
settings?.bloat_min_bytes != null ? String(settings.bloat_min_bytes) : ''
10+
);
911
const [dirty, setDirty] = useState(false);
1012
const [saving, setSaving] = useState(false);
1113

1214
useEffect(() => {
1315
if (settings?.bloat_min_bytes != null) {
1416
setBloatMinBytes(String(settings.bloat_min_bytes));
17+
setDirty(false);
1518
}
1619
}, [settings?.bloat_min_bytes]);
1720

@@ -29,6 +32,13 @@ export default function MaintenanceSection({ settings, onSave }) {
2932
return `${bytes} ${t('settings.maintenance.bytes_unit')}`;
3033
};
3134

35+
const getCharEstimate = (bytes) => {
36+
if (isNaN(bytes) || bytes < 1) return null;
37+
const zhCount = Math.round(bytes / 3);
38+
const enCount = bytes;
39+
return t('settings.maintenance.char_estimate', { zhCount, enCount });
40+
};
41+
3242
const handleSave = async () => {
3343
if (!isValid) {
3444
toast(t('settings.maintenance.invalid_bloat_min_bytes'), 'error');
@@ -50,50 +60,124 @@ export default function MaintenanceSection({ settings, onSave }) {
5060
}
5161
};
5262

63+
const PRESETS = [
64+
{ bytes: 1200, labelKey: 'preset_strict', descKey: 'preset_strict_desc' },
65+
{ bytes: 2400, labelKey: 'preset_standard', descKey: 'preset_standard_desc' },
66+
{ bytes: 4800, labelKey: 'preset_relaxed', descKey: 'preset_relaxed_desc' },
67+
];
68+
69+
const handleApplyPreset = (bytes) => {
70+
setBloatMinBytes(String(bytes));
71+
const savedVal = settings?.bloat_min_bytes != null ? String(settings.bloat_min_bytes) : '';
72+
setDirty(String(bytes) !== savedVal);
73+
};
74+
5375
return (
54-
<div className="space-y-4 pt-4">
55-
<div className="space-y-2">
56-
<label className="block text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center justify-between">
57-
<span>{t('settings.maintenance.bloat_min_bytes_label')}</span>
58-
{isValid && (
59-
<span className="text-[11px] font-mono text-indigo-400 font-normal">
60-
{formatByteHint(parsedVal)}
61-
</span>
62-
)}
63-
</label>
64-
<div className="flex items-center gap-2">
65-
<input
66-
type="number"
67-
min="1"
68-
value={bloatMinBytes}
69-
onChange={(e) => {
70-
setBloatMinBytes(e.target.value);
71-
setDirty(true);
72-
}}
73-
placeholder="2048"
74-
className="bg-slate-950 border border-slate-700 text-slate-200 rounded-lg px-3 py-2 text-sm w-44 font-mono focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 shadow-inner"
75-
/>
76-
<span className="text-xs text-slate-500 font-mono">
77-
{t('settings.maintenance.bytes_unit')}
78-
</span>
76+
<div className="space-y-5 pt-2">
77+
{/* Explanation Banner */}
78+
<div className="p-4 rounded-xl bg-indigo-950/40 border border-indigo-800/40 text-slate-300 text-xs leading-relaxed space-y-2 shadow-inner">
79+
<div className="flex items-center gap-2 text-indigo-300 font-semibold text-sm">
80+
<Sparkles size={16} className="text-indigo-400 flex-shrink-0" />
81+
<span>{t('settings.maintenance.explanation_title')}</span>
7982
</div>
80-
<p className="text-xs text-slate-500 leading-relaxed pt-1">
81-
{t('settings.maintenance.bloat_min_bytes_desc')}
83+
<p className="text-slate-300/90 leading-normal">
84+
{t('settings.maintenance.explanation_text')}
8285
</p>
86+
<div className="pt-1 flex items-center gap-1.5 text-[11px] text-indigo-300/80 font-mono">
87+
<FileText size={13} className="text-indigo-400 flex-shrink-0" />
88+
<span>docs/skills/memory-audit/SKILL.md &rarr; system://diagnostic</span>
89+
</div>
90+
</div>
91+
92+
{/* Main Setting Input & Presets */}
93+
<div className="space-y-3 pt-1">
94+
<div className="space-y-1">
95+
<label className="block text-xs font-medium text-slate-300 flex items-center justify-between">
96+
<span className="font-semibold text-slate-200">{t('settings.maintenance.bloat_min_bytes_label')}</span>
97+
{isValid && (
98+
<span className="text-[11px] font-mono text-indigo-400 font-normal">
99+
{formatByteHint(parsedVal)} {getCharEstimate(parsedVal)}
100+
</span>
101+
)}
102+
</label>
103+
<p className="text-xs text-slate-400 leading-relaxed">
104+
{t('settings.maintenance.bloat_min_bytes_desc')}
105+
</p>
106+
</div>
107+
108+
{/* Preset Chips */}
109+
<div className="space-y-1.5 pt-1">
110+
<div className="text-[11px] font-medium text-slate-400">
111+
{t('settings.maintenance.presets_label')}
112+
</div>
113+
<div className="grid grid-cols-3 gap-2">
114+
{PRESETS.map((p) => {
115+
const isSelected = isValid && parsedVal === p.bytes;
116+
return (
117+
<button
118+
key={p.bytes}
119+
type="button"
120+
onClick={() => handleApplyPreset(p.bytes)}
121+
className={`p-2 rounded-lg border text-left transition-all flex flex-col justify-between ${
122+
isSelected
123+
? 'bg-indigo-600/20 border-indigo-500/80 text-indigo-200 ring-1 ring-indigo-500/30'
124+
: 'bg-slate-900/60 border-slate-800 text-slate-300 hover:bg-slate-800/60 hover:border-slate-700'
125+
}`}
126+
>
127+
<div className="flex items-center justify-between w-full font-medium text-xs">
128+
<span>{t(`settings.maintenance.${p.labelKey}`)}</span>
129+
{isSelected && <Check size={12} className="text-indigo-400 flex-shrink-0" />}
130+
</div>
131+
<div className="text-[10px] text-slate-400 mt-1">
132+
{t(`settings.maintenance.${p.descKey}`)}
133+
</div>
134+
</button>
135+
);
136+
})}
137+
</div>
138+
</div>
139+
140+
{/* Manual Input */}
141+
<div className="pt-2">
142+
<div className="flex items-center gap-2">
143+
<input
144+
type="number"
145+
min="1"
146+
value={bloatMinBytes}
147+
onChange={(e) => {
148+
const val = e.target.value;
149+
setBloatMinBytes(val);
150+
const savedVal = settings?.bloat_min_bytes != null ? String(settings.bloat_min_bytes) : '';
151+
setDirty(val !== savedVal);
152+
}}
153+
placeholder={settings?.bloat_min_bytes != null ? String(settings.bloat_min_bytes) : ''}
154+
className="bg-slate-950 border border-slate-700 text-slate-200 rounded-lg px-3 py-2 text-sm w-44 font-mono focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 shadow-inner"
155+
/>
156+
<span className="text-xs text-slate-500 font-mono">
157+
{t('settings.maintenance.bytes_unit')}
158+
</span>
159+
</div>
160+
</div>
161+
83162
{!isValid && dirty && (
84163
<p className="text-xs text-rose-400 flex items-center gap-1 pt-1">
85164
<AlertTriangle size={12} />
86165
{t('settings.maintenance.invalid_bloat_min_bytes')}
87166
</p>
88167
)}
168+
169+
{/* Tip */}
170+
<p className="text-[11px] text-slate-400/80 bg-slate-900/40 p-2.5 rounded-lg border border-slate-800/50 leading-relaxed">
171+
{t('settings.maintenance.impact_hint')}
172+
</p>
89173
</div>
90174

91175
{dirty && (
92-
<div className="flex items-center justify-end pt-1">
176+
<div className="flex items-center justify-end pt-2">
93177
<button
94178
onClick={handleSave}
95179
disabled={saving || !isValid}
96-
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white rounded-lg text-sm font-medium flex items-center gap-2 transition-colors"
180+
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white rounded-lg text-sm font-medium flex items-center gap-2 transition-colors shadow-lg shadow-indigo-900/20"
97181
>
98182
<Save size={14} />
99183
{saving ? t('settings.maintenance.saving') : t('settings.maintenance.save')}

frontend/src/features/settings/MaintenanceSection.test.jsx

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,83 @@ vi.mock('../../components/Toast', () => ({
88
}));
99

1010
describe('MaintenanceSection', () => {
11-
it('renders initial bloat_min_bytes and byte conversion hint', () => {
12-
render(<MaintenanceSection settings={{ bloat_min_bytes: 2048 }} onSave={vi.fn()} />);
11+
it('renders initial bloat_min_bytes, byte conversion hint, and explanation callout', () => {
12+
render(<MaintenanceSection settings={{ bloat_min_bytes: 2400 }} onSave={vi.fn()} />);
1313

14-
const input = screen.getByPlaceholderText('2048');
15-
expect(input.value).toBe('2048');
16-
expect(screen.getByText('≈ 2.0 KB')).toBeInTheDocument();
14+
const input = screen.getByPlaceholderText('2400');
15+
expect(input.value).toBe('2400');
16+
expect(screen.getByText(/2.3 KB/)).toBeInTheDocument();
17+
expect(screen.getByText('What is Memory Audit & Bloat Threshold?')).toBeInTheDocument();
1718
});
1819

1920
it('shows validation error for values less than 1', async () => {
20-
render(<MaintenanceSection settings={{ bloat_min_bytes: 2048 }} onSave={vi.fn()} />);
21+
render(<MaintenanceSection settings={{ bloat_min_bytes: 2400 }} onSave={vi.fn()} />);
2122

22-
const input = screen.getByPlaceholderText('2048');
23+
const input = screen.getByPlaceholderText('2400');
2324
fireEvent.change(input, { target: { value: '0' } });
2425

2526
expect(screen.getByText('Threshold must be a positive integer of at least 1')).toBeInTheDocument();
2627
expect(screen.queryByRole('button', { name: /save/i })).toBeDisabled();
2728
});
2829

29-
it('calls onSave with parsed integer when valid value is saved', async () => {
30+
it('allows selecting presets (e.g. Compact 1200 B) to update threshold', async () => {
3031
const onSaveMock = vi.fn().mockResolvedValue({ success: true });
31-
render(<MaintenanceSection settings={{ bloat_min_bytes: 2048 }} onSave={onSaveMock} />);
32+
render(<MaintenanceSection settings={{ bloat_min_bytes: 2400 }} onSave={onSaveMock} />);
3233

33-
const input = screen.getByPlaceholderText('2048');
34-
fireEvent.change(input, { target: { value: '4096' } });
34+
const compactPreset = screen.getByText('Compact (1200 B)');
35+
fireEvent.click(compactPreset);
3536

36-
expect(screen.getByText('≈ 4.0 KB')).toBeInTheDocument();
37+
const input = screen.getByPlaceholderText('2400');
38+
expect(input.value).toBe('1200');
3739

3840
const saveButton = screen.getByRole('button', { name: /save/i });
3941
expect(saveButton).not.toBeDisabled();
4042

4143
fireEvent.click(saveButton);
4244

4345
await waitFor(() => {
44-
expect(onSaveMock).toHaveBeenCalledWith({ bloat_min_bytes: 4096 });
46+
expect(onSaveMock).toHaveBeenCalledWith({ bloat_min_bytes: 1200 });
4547
});
4648
});
49+
50+
it('calls onSave with parsed integer when valid value is saved manually', async () => {
51+
const onSaveMock = vi.fn().mockResolvedValue({ success: true });
52+
render(<MaintenanceSection settings={{ bloat_min_bytes: 2400 }} onSave={onSaveMock} />);
53+
54+
const input = screen.getByPlaceholderText('2400');
55+
fireEvent.change(input, { target: { value: '4800' } });
56+
57+
expect(screen.getByText(/4.7 KB/)).toBeInTheDocument();
58+
59+
const saveButton = screen.getByRole('button', { name: /save/i });
60+
expect(saveButton).not.toBeDisabled();
61+
62+
fireEvent.click(saveButton);
63+
64+
await waitFor(() => {
65+
expect(onSaveMock).toHaveBeenCalledWith({ bloat_min_bytes: 4800 });
66+
});
67+
});
68+
69+
it('does not show save button when selecting preset that matches saved value', async () => {
70+
render(<MaintenanceSection settings={{ bloat_min_bytes: 2400 }} onSave={vi.fn()} />);
71+
72+
// Click the 2400 preset
73+
const standardPreset = screen.getByText(/Standard \(2400 B/i);
74+
fireEvent.click(standardPreset);
75+
76+
// Should not show save button
77+
expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
78+
});
79+
80+
it('does not show save button when manually entering saved value', async () => {
81+
render(<MaintenanceSection settings={{ bloat_min_bytes: 2400 }} onSave={vi.fn()} />);
82+
83+
const input = screen.getByPlaceholderText('2400');
84+
fireEvent.change(input, { target: { value: '1200' } });
85+
expect(screen.queryByRole('button', { name: /save/i })).toBeInTheDocument();
86+
87+
fireEvent.change(input, { target: { value: '2400' } });
88+
expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
89+
});
4790
});

frontend/src/i18n/en.json

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"section_database": "Database Connection",
2323
"section_boot_uris": "Boot URIs",
2424
"section_presets": "Boot Presets",
25-
"section_maintenance": "Memory Maintenance Thresholds",
25+
"section_maintenance": "Memory Audit Bloat Threshold",
2626
"section_domains": "Memory Domains"
2727
},
2828
"error": {
@@ -185,15 +185,26 @@
185185
"save_failed": "Save failed"
186186
},
187187
"maintenance": {
188-
"bloat_min_bytes_label": "Node Bloat Threshold (bloat_min_bytes)",
189-
"bloat_min_bytes_desc": "When a single memory's UTF-8 content size exceeds this threshold, memory diagnostic and cleanup views flag it as bloated for summarization or splitting.",
188+
"bloat_min_bytes_label": "Memory Audit Bloat Threshold (bloat_min_bytes)",
189+
"bloat_min_bytes_desc": "When a single memory's UTF-8 content size exceeds this threshold, the AI memory-audit workflow flags it as a Bloated Memory via system://diagnostic, prompting the AI to split or summarize it.",
190190
"bytes_unit": "Bytes",
191191
"equiv_kb": "≈ {{kb}} KB",
192192
"equiv_mb": "≈ {{mb}} MB",
193+
"char_estimate": "(≈ {{zhCount}} Chinese chars / {{enCount}} English chars)",
193194
"invalid_bloat_min_bytes": "Threshold must be a positive integer of at least 1",
194195
"save_failed": "Failed to save maintenance threshold",
195196
"saving": "Saving...",
196-
"save": "Save"
197+
"save": "Save",
198+
"explanation_title": "What is Memory Audit & Bloat Threshold?",
199+
"explanation_text": "When the AI performs self-reflection and memory quality auditing (memory-audit skill), it queries system://diagnostic to identify memory graph issues. Oversized memories consume excessive context bandwidth and dilute key takeaways. This threshold defines when a memory node is diagnosed as 'bloated'.",
200+
"presets_label": "Recommended Presets",
201+
"preset_strict": "Compact (1200 B)",
202+
"preset_strict_desc": "≈ 400 chars · Aggressive split",
203+
"preset_standard": "Standard (2400 B · Default)",
204+
"preset_standard_desc": "≈ 800 chars · Recommended default",
205+
"preset_relaxed": "Relaxed (4800 B)",
206+
"preset_relaxed_desc": "≈ 1600 chars · Allows long nodes",
207+
"impact_hint": "💡 Tip: Lowering the threshold prompts the AI to aggressively decompose bloated memories during audits; raising it allows longer single nodes."
197208
}
198209
},
199210

0 commit comments

Comments
 (0)