@@ -1582,6 +1582,189 @@ describe("active-memory plugin", () => {
15821582 ] ) ;
15831583 } ) ;
15841584
1585+ it ( "fast-fails empty recall when memory_search reports zero hits" , async ( ) => {
1586+ api . pluginConfig = {
1587+ agents : [ "main" ] ,
1588+ timeoutMs : 10_000 ,
1589+ logging : true ,
1590+ } ;
1591+ plugin . register ( api as unknown as OpenClawPluginApi ) ;
1592+ const sessionKey = "agent:main:empty-search-fast-fail" ;
1593+ hoisted . sessionStore [ sessionKey ] = {
1594+ sessionId : "s-empty-search-fast-fail" ,
1595+ updatedAt : 0 ,
1596+ } ;
1597+ let aborted = false ;
1598+ runEmbeddedPiAgent . mockImplementationOnce (
1599+ ( params : { abortSignal ?: AbortSignal ; onToolResult ?: ( payload : unknown ) => void } ) => {
1600+ const pending = new Promise < never > ( ( _resolve , reject ) => {
1601+ params . abortSignal ?. addEventListener (
1602+ "abort" ,
1603+ ( ) => {
1604+ aborted = true ;
1605+ const error = new Error ( "aborted by fast-fail" ) ;
1606+ error . name = "AbortError" ;
1607+ reject ( error ) ;
1608+ } ,
1609+ { once : true } ,
1610+ ) ;
1611+ } ) ;
1612+ params . onToolResult ?.( {
1613+ text : `🧠 Memory Search\n\`\`\`txt\n${ JSON . stringify (
1614+ {
1615+ results : [ ] ,
1616+ debug : {
1617+ backend : "qmd" ,
1618+ configuredMode : "search" ,
1619+ effectiveMode : "query" ,
1620+ searchMs : 12 ,
1621+ hits : 0 ,
1622+ } ,
1623+ } ,
1624+ null ,
1625+ 2 ,
1626+ ) } \n\`\`\``,
1627+ } ) ;
1628+ return pending ;
1629+ } ,
1630+ ) ;
1631+
1632+ const result = await hooks . before_prompt_build (
1633+ { prompt : "what do you remember about my preferences?" , messages : [ ] } ,
1634+ { agentId : "main" , trigger : "user" , sessionKey, messageProvider : "webchat" } ,
1635+ ) ;
1636+
1637+ expect ( result ) . toBeUndefined ( ) ;
1638+ expect ( aborted ) . toBe ( true ) ;
1639+ const lines = getActiveMemoryLines ( sessionKey ) ;
1640+ expect ( lines ) . toEqual (
1641+ expect . arrayContaining ( [
1642+ expect . stringContaining ( "🧩 Active Memory: status=empty" ) ,
1643+ expect . stringContaining ( "hits=0" ) ,
1644+ ] ) ,
1645+ ) ;
1646+ } ) ;
1647+
1648+ it ( "fast-fails unavailable recall when memory_search is denied by scope" , async ( ) => {
1649+ api . pluginConfig = {
1650+ agents : [ "main" ] ,
1651+ allowedChatTypes : [ "channel" ] ,
1652+ timeoutMs : 10_000 ,
1653+ logging : true ,
1654+ } ;
1655+ plugin . register ( api as unknown as OpenClawPluginApi ) ;
1656+ const sessionKey = "agent:main:discord:channel:1488793123260862544" ;
1657+ hoisted . sessionStore [ sessionKey ] = {
1658+ sessionId : "s-scope-denied-fast-fail" ,
1659+ updatedAt : 0 ,
1660+ } ;
1661+ let aborted = false ;
1662+ runEmbeddedPiAgent . mockImplementationOnce (
1663+ ( params : { abortSignal ?: AbortSignal ; onToolResult ?: ( payload : unknown ) => void } ) => {
1664+ const pending = new Promise < never > ( ( _resolve , reject ) => {
1665+ params . abortSignal ?. addEventListener (
1666+ "abort" ,
1667+ ( ) => {
1668+ aborted = true ;
1669+ const error = new Error ( "aborted by unavailable fast-fail" ) ;
1670+ error . name = "AbortError" ;
1671+ reject ( error ) ;
1672+ } ,
1673+ { once : true } ,
1674+ ) ;
1675+ } ) ;
1676+ params . onToolResult ?.( {
1677+ text : `🧠 Memory Search\n\`\`\`txt\n${ JSON . stringify (
1678+ {
1679+ results : [ ] ,
1680+ disabled : true ,
1681+ unavailable : true ,
1682+ error : "qmd search denied by scope" ,
1683+ warning : "Memory search is unavailable due to an embedding/provider error." ,
1684+ action : "Check embedding provider configuration and retry memory_search." ,
1685+ debug : {
1686+ error : "qmd search denied by scope" ,
1687+ warning : "Memory search is unavailable due to an embedding/provider error." ,
1688+ action : "Check embedding provider configuration and retry memory_search." ,
1689+ } ,
1690+ } ,
1691+ null ,
1692+ 2 ,
1693+ ) } \n\`\`\``,
1694+ } ) ;
1695+ return pending ;
1696+ } ,
1697+ ) ;
1698+
1699+ const result = await hooks . before_prompt_build (
1700+ { prompt : "Testing 1, 2, 3" , messages : [ ] } ,
1701+ {
1702+ agentId : "main" ,
1703+ trigger : "user" ,
1704+ sessionKey,
1705+ messageProvider : "discord" ,
1706+ channelId : "1488793123260862544" ,
1707+ } ,
1708+ ) ;
1709+
1710+ expect ( result ) . toBeUndefined ( ) ;
1711+ expect ( aborted ) . toBe ( true ) ;
1712+ const lines = getActiveMemoryLines ( sessionKey ) ;
1713+ expect ( lines ) . toEqual (
1714+ expect . arrayContaining ( [
1715+ expect . stringContaining ( "🧩 Active Memory: status=unavailable" ) ,
1716+ expect . stringContaining ( "Memory search is unavailable" ) ,
1717+ ] ) ,
1718+ ) ;
1719+ } ) ;
1720+
1721+ it ( "does not fast-fail unavailable output from memory_get" , async ( ) => {
1722+ api . pluginConfig = {
1723+ agents : [ "main" ] ,
1724+ timeoutMs : 10_000 ,
1725+ logging : true ,
1726+ } ;
1727+ plugin . register ( api as unknown as OpenClawPluginApi ) ;
1728+ const sessionKey = "agent:main:memory-get-unavailable-not-terminal" ;
1729+ hoisted . sessionStore [ sessionKey ] = {
1730+ sessionId : "s-memory-get-unavailable-not-terminal" ,
1731+ updatedAt : 0 ,
1732+ } ;
1733+ let aborted = false ;
1734+ runEmbeddedPiAgent . mockImplementationOnce (
1735+ async ( params : { abortSignal ?: AbortSignal ; onToolResult ?: ( payload : unknown ) => void } ) => {
1736+ params . abortSignal ?. addEventListener (
1737+ "abort" ,
1738+ ( ) => {
1739+ aborted = true ;
1740+ } ,
1741+ { once : true } ,
1742+ ) ;
1743+ params . onToolResult ?.( {
1744+ text : `📓 Memory Get\n\`\`\`txt\n${ JSON . stringify (
1745+ {
1746+ disabled : true ,
1747+ error : "wiki corpus result not found" ,
1748+ } ,
1749+ null ,
1750+ 2 ,
1751+ ) } \n\`\`\``,
1752+ } ) ;
1753+ return { payloads : [ { text : "Useful search summary after memory_get miss." } ] } ;
1754+ } ,
1755+ ) ;
1756+
1757+ const result = await hooks . before_prompt_build (
1758+ { prompt : "what should I remember?" , messages : [ ] } ,
1759+ { agentId : "main" , trigger : "user" , sessionKey, messageProvider : "webchat" } ,
1760+ ) ;
1761+
1762+ expect ( aborted ) . toBe ( false ) ;
1763+ expect ( ( result as { prependContext ?: string } | undefined ) ?. prependContext ) . toContain (
1764+ "Useful search summary after memory_get miss." ,
1765+ ) ;
1766+ } ) ;
1767+
15851768 it ( "returns nothing when the subagent says none" , async ( ) => {
15861769 runEmbeddedPiAgent . mockResolvedValueOnce ( {
15871770 payloads : [ { text : "NONE" } ] ,
@@ -1600,6 +1783,37 @@ describe("active-memory plugin", () => {
16001783 expect ( result ) . toBeUndefined ( ) ;
16011784 } ) ;
16021785
1786+ it ( "treats embedded timeout boilerplate as timeout instead of memory context" , async ( ) => {
1787+ api . pluginConfig = {
1788+ agents : [ "main" ] ,
1789+ timeoutMs : 10_000 ,
1790+ logging : true ,
1791+ } ;
1792+ plugin . register ( api as unknown as OpenClawPluginApi ) ;
1793+ const sessionKey = "agent:main:timeout-boilerplate" ;
1794+ hoisted . sessionStore [ sessionKey ] = {
1795+ sessionId : "s-timeout-boilerplate" ,
1796+ updatedAt : 0 ,
1797+ } ;
1798+ runEmbeddedPiAgent . mockResolvedValueOnce ( {
1799+ payloads : [
1800+ {
1801+ text : "Request timed out before a response was generated. Please try again, or increase `agents.defaults.timeoutSeconds` in your config." ,
1802+ } ,
1803+ ] ,
1804+ } ) ;
1805+
1806+ const result = await hooks . before_prompt_build (
1807+ { prompt : "what wings should i order? timeout boilerplate" , messages : [ ] } ,
1808+ { agentId : "main" , trigger : "user" , sessionKey, messageProvider : "webchat" } ,
1809+ ) ;
1810+
1811+ expect ( result ) . toBeUndefined ( ) ;
1812+ const lines = getActiveMemoryLines ( sessionKey ) ;
1813+ expect ( lines ) . toEqual ( [ expect . stringContaining ( "🧩 Active Memory: status=timeout" ) ] ) ;
1814+ expect ( lines . join ( "\n" ) ) . not . toContain ( "Request timed out before a response was generated" ) ;
1815+ } ) ;
1816+
16031817 it ( "returns partial transcript text on timeout when the subagent has already written assistant output" , async ( ) => {
16041818 __testing . setMinimumTimeoutMsForTests ( 1 ) ;
16051819 __testing . setSetupGraceTimeoutMsForTests ( 0 ) ;
@@ -2175,7 +2389,7 @@ describe("active-memory plugin", () => {
21752389 ) . toBe ( true ) ;
21762390 } ) ;
21772391
2178- it ( "does not spend the model timeout budget on active-memory subagent setup" , async ( ) => {
2392+ it ( "does not extend the user-visible recall budget with setup grace " , async ( ) => {
21792393 const CONFIGURED_TIMEOUT_MS = 10 ;
21802394 __testing . setMinimumTimeoutMsForTests ( 1 ) ;
21812395 __testing . setSetupGraceTimeoutMsForTests ( 100 ) ;
@@ -2200,19 +2414,19 @@ describe("active-memory plugin", () => {
22002414 } ,
22012415 ) ;
22022416
2203- expect ( result ?. prependContext ) . toContain ( "remember the ramen place" ) ;
2417+ expect ( result ) . toBeUndefined ( ) ;
22042418 expect ( runEmbeddedPiAgent . mock . calls . at ( - 1 ) ?. [ 0 ] ?. timeoutMs ) . toBe ( CONFIGURED_TIMEOUT_MS ) ;
22052419 const infoLines = vi
22062420 . mocked ( api . logger . info )
22072421 . mock . calls . map ( ( call : unknown [ ] ) => String ( call [ 0 ] ) ) ;
2208- expect ( infoLines . some ( ( line : string ) => line . includes ( "status=timeout" ) ) ) . toBe ( false ) ;
2422+ expect ( infoLines . some ( ( line : string ) => line . includes ( "status=timeout" ) ) ) . toBe ( true ) ;
22092423 } ) ;
22102424
22112425 it ( "returns timeout within a hard deadline even when the subagent never checks the abort signal" , async ( ) => {
22122426 const CONFIGURED_TIMEOUT_MS = 200 ;
22132427 const HARD_DEADLINE_MARGIN_MS = 4_800 ;
22142428 __testing . setMinimumTimeoutMsForTests ( 1 ) ;
2215- __testing . setSetupGraceTimeoutMsForTests ( 0 ) ;
2429+ __testing . setSetupGraceTimeoutMsForTests ( 5_000 ) ;
22162430 api . pluginConfig = {
22172431 agents : [ "main" ] ,
22182432 timeoutMs : CONFIGURED_TIMEOUT_MS ,
0 commit comments