@@ -357,8 +357,11 @@ function ConfigForm(props: {
357357 // Merge form data with original to preserve unknown keys
358358 const original = parsed ( ) ;
359359 const merged = { ...original , ...formData ( ) } ;
360- // Deep merge for nested objects
361- for ( const key of [ "embedding" , "memory" ] ) {
360+ // Deep merge for nested objects so we don't blow away sub-keys the form
361+ // doesn't currently expose (e.g. user-set values inside `experimental.*`
362+ // sub-objects that the UI doesn't render). The shallow `...formData()`
363+ // above would otherwise replace the whole sub-tree.
364+ for ( const key of [ "embedding" , "memory" , "experimental" ] ) {
362365 if ( typeof formData ( ) [ key ] === "object" && formData ( ) [ key ] != null ) {
363366 merged [ key ] = {
364367 ...( ( original [ key ] as Record < string , unknown > ) ?? { } ) ,
@@ -1530,6 +1533,346 @@ function ConfigForm(props: {
15301533 </ div >
15311534 </ div >
15321535
1536+ { /* Experimental — opt-in features gated behind `experimental.*` flags.
1537+ May change between releases. Rendered before Tags & Cleanup so the
1538+ "advanced" surface stays at the bottom of the form. Each feature is
1539+ a master toggle; child controls only appear when the feature is on,
1540+ keeping the surface compact for users who never enable any of them. */ }
1541+ { ( ( ) => {
1542+ const exp = ( ) =>
1543+ ( getNestedValue ( formData ( ) , "experimental" ) as
1544+ | Record < string , Record < string , unknown > | boolean >
1545+ | undefined ) ?? { } ;
1546+ const setExperimentalKey = ( key : string , value : unknown ) => {
1547+ handleFieldChange ( "experimental" , { ...exp ( ) , [ key ] : value } ) ;
1548+ } ;
1549+
1550+ // temporal_awareness — boolean (no children)
1551+ const temporalAwareness = ( ) => Boolean ( exp ( ) . temporal_awareness ) ;
1552+
1553+ // git_commit_indexing — { enabled, since_days, max_commits }
1554+ const gitCommit = ( ) =>
1555+ ( exp ( ) . git_commit_indexing as
1556+ | { enabled ?: boolean ; since_days ?: number ; max_commits ?: number }
1557+ | undefined ) ?? { } ;
1558+ const gitCommitEnabled = ( ) => Boolean ( gitCommit ( ) . enabled ) ;
1559+ const gitCommitSinceDays = ( ) => gitCommit ( ) . since_days ?? 365 ;
1560+ const gitCommitMaxCommits = ( ) => gitCommit ( ) . max_commits ?? 2000 ;
1561+
1562+ // auto_search — { enabled, score_threshold, min_prompt_chars }
1563+ const autoSearch = ( ) =>
1564+ ( exp ( ) . auto_search as
1565+ | { enabled ?: boolean ; score_threshold ?: number ; min_prompt_chars ?: number }
1566+ | undefined ) ?? { } ;
1567+ const autoSearchEnabled = ( ) => Boolean ( autoSearch ( ) . enabled ) ;
1568+ const autoSearchScoreThreshold = ( ) => autoSearch ( ) . score_threshold ?? 0.55 ;
1569+ const autoSearchMinChars = ( ) => autoSearch ( ) . min_prompt_chars ?? 20 ;
1570+
1571+ // caveman_text_compression — { enabled, min_chars }
1572+ const caveman = ( ) =>
1573+ ( exp ( ) . caveman_text_compression as
1574+ | { enabled ?: boolean ; min_chars ?: number }
1575+ | undefined ) ?? { } ;
1576+ const cavemanEnabled = ( ) => Boolean ( caveman ( ) . enabled ) ;
1577+ const cavemanMinChars = ( ) => caveman ( ) . min_chars ?? 500 ;
1578+
1579+ const ctxReduceEnabled = ( ) => {
1580+ const v = getNestedValue ( formData ( ) , "ctx_reduce_enabled" ) ;
1581+ return v == null ? true : Boolean ( v ) ;
1582+ } ;
1583+
1584+ return (
1585+ < div class = "config-card full-width" >
1586+ < div class = "config-card-header" >
1587+ < span class = "config-card-icon" > 🧪</ span >
1588+ < span class = "config-card-title" > Experimental</ span >
1589+ </ div >
1590+ < div class = "config-card-content" >
1591+ < div
1592+ class = "config-field-desc"
1593+ style = { { "margin-bottom" : "8px" , opacity : "0.85" } }
1594+ >
1595+ Opt-in features that may change between releases. Disabled by default.
1596+ </ div >
1597+
1598+ { /* Temporal awareness */ }
1599+ < div class = "config-field" >
1600+ < div class = "config-field-header" >
1601+ < span class = "config-field-label" > Temporal Awareness</ span >
1602+ < span class = "config-field-key" > experimental.temporal_awareness</ span >
1603+ </ div >
1604+ < span class = "config-field-desc" >
1605+ Inject elapsed-time markers (e.g. < code > +12m</ code > , < code > +3d 4h</ code > )
1606+ between user messages with >5 min gaps, and add{ " " }
1607+ < code > start-date</ code > /< code > end-date</ code > attributes on rendered
1608+ compartments. Helps the agent reason about session pacing across long-running
1609+ and multi-day sessions.
1610+ </ span >
1611+ < label class = "toggle-switch" >
1612+ < input
1613+ type = "checkbox"
1614+ checked = { temporalAwareness ( ) }
1615+ onChange = { ( e ) =>
1616+ setExperimentalKey ( "temporal_awareness" , e . currentTarget . checked )
1617+ }
1618+ />
1619+ < span class = "toggle-slider" />
1620+ < span class = "toggle-label" >
1621+ { temporalAwareness ( ) ? "Enabled" : "Disabled" }
1622+ </ span >
1623+ </ label >
1624+ </ div >
1625+
1626+ { /* Git commit indexing */ }
1627+ < div class = "config-field" >
1628+ < div class = "config-field-header" >
1629+ < span class = "config-field-label" > Git Commit Indexing</ span >
1630+ < span class = "config-field-key" > experimental.git_commit_indexing.enabled</ span >
1631+ </ div >
1632+ < span class = "config-field-desc" >
1633+ Index < code > HEAD</ code > non-merge commits into < code > ctx_search</ code > as a
1634+ 4th source alongside memories, facts, and message history. Useful for
1635+ agents recalling regressions, prior fixes, and decisions without running{ " " }
1636+ < code > git log</ code > manually.
1637+ </ span >
1638+ < label class = "toggle-switch" >
1639+ < input
1640+ type = "checkbox"
1641+ checked = { gitCommitEnabled ( ) }
1642+ onChange = { ( e ) =>
1643+ setExperimentalKey ( "git_commit_indexing" , {
1644+ ...gitCommit ( ) ,
1645+ enabled : e . currentTarget . checked ,
1646+ } )
1647+ }
1648+ />
1649+ < span class = "toggle-slider" />
1650+ < span class = "toggle-label" >
1651+ { gitCommitEnabled ( ) ? "Enabled" : "Disabled" }
1652+ </ span >
1653+ </ label >
1654+ </ div >
1655+
1656+ < Show when = { gitCommitEnabled ( ) } >
1657+ < div class = "config-field" >
1658+ < div class = "config-field-header" >
1659+ < span class = "config-field-label" > History Window (days)</ span >
1660+ < span class = "config-field-key" >
1661+ experimental.git_commit_indexing.since_days
1662+ </ span >
1663+ </ div >
1664+ < span class = "config-field-desc" >
1665+ Days of HEAD history to index. Older commits are excluded from search.
1666+ Range 7–3650, default 365.
1667+ </ span >
1668+ < input
1669+ class = "config-input"
1670+ type = "number"
1671+ min = { 7 }
1672+ max = { 3650 }
1673+ value = { gitCommitSinceDays ( ) }
1674+ onInput = { ( e ) => {
1675+ const v = e . currentTarget . value ;
1676+ setExperimentalKey ( "git_commit_indexing" , {
1677+ ...gitCommit ( ) ,
1678+ since_days : v ? Math . max ( 7 , Math . min ( 3650 , Number ( v ) ) ) : 365 ,
1679+ } ) ;
1680+ } }
1681+ />
1682+ </ div >
1683+ < div class = "config-field" >
1684+ < div class = "config-field-header" >
1685+ < span class = "config-field-label" > Max Commits</ span >
1686+ < span class = "config-field-key" >
1687+ experimental.git_commit_indexing.max_commits
1688+ </ span >
1689+ </ div >
1690+ < span class = "config-field-desc" >
1691+ Maximum commits kept per project. Oldest evicted when the cap is reached.
1692+ Range 100–20000, default 2000.
1693+ </ span >
1694+ < input
1695+ class = "config-input"
1696+ type = "number"
1697+ min = { 100 }
1698+ max = { 20000 }
1699+ value = { gitCommitMaxCommits ( ) }
1700+ onInput = { ( e ) => {
1701+ const v = e . currentTarget . value ;
1702+ setExperimentalKey ( "git_commit_indexing" , {
1703+ ...gitCommit ( ) ,
1704+ max_commits : v ? Math . max ( 100 , Math . min ( 20000 , Number ( v ) ) ) : 2000 ,
1705+ } ) ;
1706+ } }
1707+ />
1708+ </ div >
1709+ </ Show >
1710+
1711+ { /* Auto search */ }
1712+ < div class = "config-field" >
1713+ < div class = "config-field-header" >
1714+ < span class = "config-field-label" > Auto Search Hint</ span >
1715+ < span class = "config-field-key" > experimental.auto_search.enabled</ span >
1716+ </ div >
1717+ < span class = "config-field-desc" >
1718+ On each new user message, run < code > ctx_search</ code > in the background and
1719+ append a compact < code > <ctx-search-hint></ code > block of vague
1720+ fragments when the top hit clears the score threshold. Does NOT inject full
1721+ content — just nudges the agent to run < code > ctx_search</ code > for the real
1722+ result if relevant. Adds one embedding round-trip per new user turn.
1723+ </ span >
1724+ < label class = "toggle-switch" >
1725+ < input
1726+ type = "checkbox"
1727+ checked = { autoSearchEnabled ( ) }
1728+ onChange = { ( e ) =>
1729+ setExperimentalKey ( "auto_search" , {
1730+ ...autoSearch ( ) ,
1731+ enabled : e . currentTarget . checked ,
1732+ } )
1733+ }
1734+ />
1735+ < span class = "toggle-slider" />
1736+ < span class = "toggle-label" >
1737+ { autoSearchEnabled ( ) ? "Enabled" : "Disabled" }
1738+ </ span >
1739+ </ label >
1740+ </ div >
1741+
1742+ < Show when = { autoSearchEnabled ( ) } >
1743+ < div class = "config-field" >
1744+ < div class = "config-field-header" >
1745+ < span class = "config-field-label" > Score Threshold</ span >
1746+ < span class = "config-field-key" >
1747+ experimental.auto_search.score_threshold
1748+ </ span >
1749+ </ div >
1750+ < span class = "config-field-desc" >
1751+ Minimum top-hit score for the hint to fire. Higher = fewer but more
1752+ relevant hints. Range 0.30–0.95, default 0.55.
1753+ </ span >
1754+ < input
1755+ class = "config-input"
1756+ type = "number"
1757+ min = { 0.3 }
1758+ max = { 0.95 }
1759+ step = { 0.05 }
1760+ value = { autoSearchScoreThreshold ( ) }
1761+ onInput = { ( e ) => {
1762+ const v = e . currentTarget . value ;
1763+ setExperimentalKey ( "auto_search" , {
1764+ ...autoSearch ( ) ,
1765+ score_threshold : v ? Math . max ( 0.3 , Math . min ( 0.95 , Number ( v ) ) ) : 0.55 ,
1766+ } ) ;
1767+ } }
1768+ />
1769+ </ div >
1770+ < div class = "config-field" >
1771+ < div class = "config-field-header" >
1772+ < span class = "config-field-label" > Min Prompt Chars</ span >
1773+ < span class = "config-field-key" >
1774+ experimental.auto_search.min_prompt_chars
1775+ </ span >
1776+ </ div >
1777+ < span class = "config-field-desc" >
1778+ Skip the hint when a user message is shorter than this. Avoids embedding
1779+ cost on trivial replies. Range 5–500, default 20.
1780+ </ span >
1781+ < input
1782+ class = "config-input"
1783+ type = "number"
1784+ min = { 5 }
1785+ max = { 500 }
1786+ value = { autoSearchMinChars ( ) }
1787+ onInput = { ( e ) => {
1788+ const v = e . currentTarget . value ;
1789+ setExperimentalKey ( "auto_search" , {
1790+ ...autoSearch ( ) ,
1791+ min_prompt_chars : v ? Math . max ( 5 , Math . min ( 500 , Number ( v ) ) ) : 20 ,
1792+ } ) ;
1793+ } }
1794+ />
1795+ </ div >
1796+ </ Show >
1797+
1798+ { /* Caveman text compression */ }
1799+ < div class = "config-field" >
1800+ < div class = "config-field-header" >
1801+ < span class = "config-field-label" > Caveman Text Compression</ span >
1802+ < span class = "config-field-key" >
1803+ experimental.caveman_text_compression.enabled
1804+ </ span >
1805+ </ div >
1806+ < span class = "config-field-desc" >
1807+ Age-tiered compression for long user/assistant text parts.{ " " }
1808+ < strong >
1809+ Only active when Agent Controlled Reduction (
1810+ < code > ctx_reduce_enabled</ code > ) is OFF.
1811+ </ strong > { " " }
1812+ Outside the protected tail, oldest 20% of eligible tags get ultra
1813+ compression, next 20% full, next 20% lite, newest 40% untouched. Always
1814+ compresses from the original source, so depth shifts are equivalent to
1815+ compressing the original text directly.
1816+ </ span >
1817+ < Show when = { ctxReduceEnabled ( ) && cavemanEnabled ( ) } >
1818+ < div
1819+ class = "config-field-desc"
1820+ style = { { color : "var(--color-warning, #c8881f)" , "margin-bottom" : "4px" } }
1821+ >
1822+ ⚠️ Caveman compression has no effect while Agent Controlled Reduction is
1823+ enabled. Disable < code > ctx_reduce_enabled</ code > in General to use this.
1824+ </ div >
1825+ </ Show >
1826+ < label class = "toggle-switch" >
1827+ < input
1828+ type = "checkbox"
1829+ checked = { cavemanEnabled ( ) }
1830+ onChange = { ( e ) =>
1831+ setExperimentalKey ( "caveman_text_compression" , {
1832+ ...caveman ( ) ,
1833+ enabled : e . currentTarget . checked ,
1834+ } )
1835+ }
1836+ />
1837+ < span class = "toggle-slider" />
1838+ < span class = "toggle-label" > { cavemanEnabled ( ) ? "Enabled" : "Disabled" } </ span >
1839+ </ label >
1840+ </ div >
1841+
1842+ < Show when = { cavemanEnabled ( ) } >
1843+ < div class = "config-field" >
1844+ < div class = "config-field-header" >
1845+ < span class = "config-field-label" > Min Chars</ span >
1846+ < span class = "config-field-key" >
1847+ experimental.caveman_text_compression.min_chars
1848+ </ span >
1849+ </ div >
1850+ < span class = "config-field-desc" >
1851+ Text parts shorter than this are left untouched. Range 100–10000,
1852+ default 500.
1853+ </ span >
1854+ < input
1855+ class = "config-input"
1856+ type = "number"
1857+ min = { 100 }
1858+ max = { 10000 }
1859+ step = { 50 }
1860+ value = { cavemanMinChars ( ) }
1861+ onInput = { ( e ) => {
1862+ const v = e . currentTarget . value ;
1863+ setExperimentalKey ( "caveman_text_compression" , {
1864+ ...caveman ( ) ,
1865+ min_chars : v ? Math . max ( 100 , Math . min ( 10000 , Number ( v ) ) ) : 500 ,
1866+ } ) ;
1867+ } }
1868+ />
1869+ </ div >
1870+ </ Show >
1871+ </ div >
1872+ </ div >
1873+ ) ;
1874+ } ) ( ) }
1875+
15331876 { /* Tags & Cleanup — rendered after agent cards */ }
15341877 { ( ( ) => {
15351878 const tagsFields = sections ( ) . find ( ( [ name ] ) => name === "Tags & Cleanup" ) ;
0 commit comments