Repository navigation
feat(memory-sources): default sync sources on with conservative caps, per-source settings & All In #3304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
senamakel
merged 13 commits into
tinyhumansai:main
from
graycyrus:feat/memory-sources-default-on
Jun 4, 2026
Merged
feat(memory-sources): default sync sources on with conservative caps, per-source settings & All In #3304
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
7ebf1cb
feat(memory-sources): default sync sources on with conservative caps,…
graycyrus b959ca6
Merge remote-tracking branch 'upstream/main' into feat/memory-sources…
graycyrus d9a3700
docs(about_app): add memory source sync controls capability
graycyrus da32180
test(memory_sync): add ProviderContext caps fields to integration tes…
graycyrus 43334c6
test(e2e): expose memory_sources_apply_all_in in controller schema ca…
graycyrus 6aab513
fix(memory-sources): render per-source settings panel full-width belo…
graycyrus ec7df62
fix(memory-sources): backend-driven default caps + Unlimited/Maxed UI
graycyrus 8b4a208
feat(memory-sources): info tooltip on Unlimited sync-settings fields
graycyrus 842fcaf
feat(memory-sources): drop unenforced token/cost budget fields from s…
graycyrus 9ab56f3
fix(memory-sync): enforce max_items precisely + centralize cap logic …
graycyrus ab28a7a
test(memory-sync): max_items cap e2e for all 6 providers
graycyrus 597119d
Merge remote-tracking branch 'upstream/main' into feat/memory-sources…
graycyrus 9657a65
fix(memory-sync): address CodeRabbit review on PR #3304
graycyrus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
371 changes: 257 additions & 114 deletions
371
app/src/components/intelligence/MemorySourcesRegistry.tsx
Large diffs are not rendered by default.
Oops, something went wrong.
224 changes: 224 additions & 0 deletions
224
app/src/components/intelligence/SourceSettingsPanel.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| /** | ||
| * Expandable per-source sync-settings panel. | ||
| * | ||
| * Rendered inline below a SourceRow when the gear icon is clicked. | ||
| * Shows only the limit fields relevant to the source's kind, seeded from | ||
| * the current entry values. Empty input = omit the field on save (meaning | ||
| * "use backend default / unlimited"). | ||
| * | ||
| * On Save calls `updateMemorySource` and notifies the parent via | ||
| * `onSaved` with the updated entry. | ||
| */ | ||
| import { useCallback, useState } from 'react'; | ||
|
|
||
| import { useT } from '../../lib/i18n/I18nContext'; | ||
| import { | ||
| type MemorySourceEntry, | ||
| type SourceKind, | ||
| updateMemorySource, | ||
| } from '../../services/memorySourcesService'; | ||
|
|
||
| // Which limit fields are relevant per kind. Order determines display order. | ||
| // Only caps that are actually enforced at sync time are surfaced — the | ||
| // per-sync token/cost budgets are not yet wired into the sync loop, so they | ||
| // are intentionally omitted here. | ||
| const KIND_FIELDS: Record<SourceKind, Array<keyof LimitFields>> = { | ||
| composio: ['sync_depth_days', 'max_items'], | ||
| github_repo: ['max_prs', 'max_issues', 'max_commits', 'sync_depth_days'], | ||
| rss_feed: ['max_items', 'sync_depth_days'], | ||
| twitter_query: ['since_days'], | ||
| web_page: ['sync_depth_days'], | ||
| folder: ['sync_depth_days'], | ||
| }; | ||
|
|
||
| // i18n key for each field label | ||
| const FIELD_LABEL_KEYS: Record<keyof LimitFields, string> = { | ||
| max_prs: 'memorySources.settings.maxPrs', | ||
| max_issues: 'memorySources.settings.maxIssues', | ||
| max_commits: 'memorySources.settings.maxCommits', | ||
| max_items: 'memorySources.settings.maxItems', | ||
| since_days: 'memorySources.settings.sinceDays', | ||
| sync_depth_days: 'memorySources.settings.syncDepthDays', | ||
| }; | ||
|
|
||
| type LimitFields = Pick< | ||
| MemorySourceEntry, | ||
| 'max_prs' | 'max_issues' | 'max_commits' | 'max_items' | 'since_days' | 'sync_depth_days' | ||
| >; | ||
|
|
||
| // Item-count caps where a "Maxed" badge is meaningful (synced count vs cap). | ||
| // Time-window (sync_depth_days/since_days) and budget (tokens/cost) caps don't | ||
| // map to a chunk count, so they never show "Maxed". | ||
| const COUNT_FIELDS = new Set<keyof LimitFields>([ | ||
| 'max_items', | ||
| 'max_prs', | ||
| 'max_issues', | ||
| 'max_commits', | ||
| ]); | ||
|
|
||
| interface SourceSettingsPanelProps { | ||
| source: MemorySourceEntry; | ||
| /** Chunks already synced for this source — drives the "Maxed" badge. */ | ||
| syncedCount?: number; | ||
| onSaved: (updated: MemorySourceEntry) => void; | ||
| onToast?: (toast: { type: 'success' | 'error'; title: string; message?: string }) => void; | ||
| } | ||
|
|
||
| export function SourceSettingsPanel({ | ||
| source, | ||
| syncedCount, | ||
| onSaved, | ||
| onToast, | ||
| }: SourceSettingsPanelProps) { | ||
| const { t } = useT(); | ||
| const fields = KIND_FIELDS[source.kind] ?? []; | ||
|
|
||
| // Hold each field as a string so inputs can be freely edited. Seeded from the | ||
| // entry's stored cap (fetched from the backend — the single source of truth; | ||
| // the caps migration writes conservative defaults onto the entry). An empty | ||
| // field genuinely means "no cap / unlimited". | ||
| const [values, setValues] = useState<Record<string, string>>(() => { | ||
| const init: Record<string, string> = {}; | ||
| for (const f of fields) { | ||
| const stored = source[f as keyof MemorySourceEntry]; | ||
| init[f] = stored != null ? String(stored) : ''; | ||
| } | ||
| return init; | ||
| }); | ||
|
|
||
| const [saving, setSaving] = useState(false); | ||
|
|
||
| const handleChange = useCallback((field: string, value: string) => { | ||
| setValues(prev => ({ ...prev, [field]: value })); | ||
| }, []); | ||
|
|
||
| const handleSave = useCallback(async () => { | ||
| setSaving(true); | ||
| try { | ||
| const patch: Partial<LimitFields> = {}; | ||
| for (const f of fields) { | ||
| const raw = values[f]; | ||
| if (raw !== '' && raw !== undefined) { | ||
| const parsed = Number(raw); | ||
| if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) { | ||
| onToast?.({ type: 'error', title: t('memorySources.settings.saveFailed') }); | ||
| return; | ||
| } | ||
| (patch as Record<string, number>)[f] = parsed; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| // Empty string → omit from patch (backend treats absence as "default") | ||
| } | ||
| const updated = await updateMemorySource(source.id, patch); | ||
| onSaved(updated); | ||
| onToast?.({ type: 'success', title: t('memorySources.settings.saved') }); | ||
| } catch (err) { | ||
| onToast?.({ | ||
| type: 'error', | ||
| title: t('memorySources.settings.saveFailed'), | ||
| message: err instanceof Error ? err.message : String(err), | ||
| }); | ||
| } finally { | ||
| setSaving(false); | ||
| } | ||
| }, [fields, source.id, values, onSaved, onToast, t]); | ||
|
|
||
| if (fields.length === 0) return null; | ||
|
|
||
| // Display name for the tooltip — the toolkit slug (title-cased) for Composio | ||
| // sources, else the source label. | ||
| const toolkitName = source.toolkit | ||
| ? source.toolkit.charAt(0).toUpperCase() + source.toolkit.slice(1) | ||
| : source.label; | ||
| const unlimitedTooltip = t('memorySources.settings.unlimitedTooltip').replace( | ||
| '{toolkit}', | ||
| toolkitName | ||
| ); | ||
|
|
||
| return ( | ||
| <div | ||
| className="mt-2 ml-7 rounded-lg border border-stone-200 bg-stone-50 p-3 dark:border-neutral-700 dark:bg-neutral-800/60" | ||
| data-testid={`source-settings-panel-${source.id}`}> | ||
| <p className="mb-2 text-xs font-semibold text-stone-600 dark:text-neutral-300"> | ||
| {t('memorySources.settings.title')} | ||
| </p> | ||
| <div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> | ||
| {fields.map(field => { | ||
| const cap = Number(values[field]); | ||
| const isUnlimited = (values[field] ?? '') === ''; | ||
| const isMaxed = | ||
| COUNT_FIELDS.has(field) && | ||
| !isUnlimited && | ||
| Number.isFinite(cap) && | ||
| typeof syncedCount === 'number' && | ||
| syncedCount >= cap; | ||
| return ( | ||
| <div key={field}> | ||
| <label | ||
| htmlFor={`src-setting-${source.id}-${field}`} | ||
| className="mb-0.5 flex items-center gap-1.5 text-xs font-medium text-stone-600 dark:text-neutral-400"> | ||
| {t(FIELD_LABEL_KEYS[field])} | ||
| {isMaxed && ( | ||
| <span className="rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700 dark:bg-amber-500/20 dark:text-amber-300"> | ||
| {t('memorySources.settings.maxed')} | ||
| </span> | ||
| )} | ||
| {isUnlimited && ( | ||
| <span | ||
| className="inline-flex cursor-help text-stone-400 dark:text-neutral-500" | ||
| title={unlimitedTooltip} | ||
| aria-label={unlimitedTooltip}> | ||
| <InfoIcon /> | ||
| </span> | ||
| )} | ||
| </label> | ||
| <input | ||
| id={`src-setting-${source.id}-${field}`} | ||
| type="number" | ||
| min={0} | ||
| step={1} | ||
| value={values[field] ?? ''} | ||
| onChange={e => handleChange(field, e.target.value)} | ||
| placeholder={t('memorySources.settings.unlimited')} | ||
| className="w-full rounded-md border border-stone-200 bg-white px-2.5 py-1.5 text-xs font-mono | ||
| text-stone-800 placeholder:text-stone-400 | ||
| dark:border-neutral-600 dark:bg-neutral-900 dark:text-neutral-200 | ||
| dark:placeholder:text-neutral-500 | ||
| focus:outline-none focus:ring-2 focus:ring-primary-200" | ||
| /> | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| <div className="mt-3 flex justify-end"> | ||
| <button | ||
| type="button" | ||
| onClick={() => void handleSave()} | ||
| disabled={saving} | ||
| className="inline-flex items-center gap-1 rounded-md bg-primary-600 px-3 py-1.5 | ||
| text-xs font-semibold text-white shadow-sm transition-colors | ||
| hover:bg-primary-500 disabled:cursor-not-allowed disabled:opacity-50 | ||
| focus:outline-none focus:ring-2 focus:ring-primary-200"> | ||
| {saving ? t('memorySources.settings.saving') : t('memorySources.settings.save')} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function InfoIcon() { | ||
| return ( | ||
| <svg | ||
| width="13" | ||
| height="13" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| aria-hidden="true"> | ||
| <circle cx="12" cy="12" r="10" /> | ||
| <path d="M12 16v-4M12 8h.01" /> | ||
| </svg> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick — stale note. This bullet says new-connection caps are
(None, None)/ bounded only byMAX_PAGES, butbus.rs on_connection_creatednow.unwrap_or_else(|| memory_sync_defaults_for_toolkit(...)), so the first sync is capped by the per-toolkit defaults. The note contradicts the shipped code.