@@ -1779,6 +1779,31 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
17791779 { name : "network_skill_pull" } ,
17801780 ) ;
17811781
1782+ // ─── Inject recall context as system message (hidden from chat UI) ───
1783+ // `prependContext` (from before_agent_start) is prepended to user messages and
1784+ // therefore visible in the chat box. To keep injected memories invisible to the
1785+ // user while still feeding them to the model, we stash the recall result in
1786+ // `pendingRecallContext` and inject it as a system-level message via the
1787+ // `before_context_send` hook, which fires synchronously during prompt assembly.
1788+ let pendingRecallContext : string | null = null ;
1789+
1790+ api . on ( "before_context_send" , ( event : { messages : Array < { role : string ; content : string | unknown } > } ) => {
1791+ if ( ! pendingRecallContext ) return ;
1792+ const memoryContext = pendingRecallContext ;
1793+ pendingRecallContext = null ;
1794+ // Insert after the last system message (before the first user/assistant turn).
1795+ // This keeps the static system prompt at the very beginning of the sequence so
1796+ // KV-cache prefixes stay stable across requests — the provider can reuse the
1797+ // cached keys/values for the system prompt even though the memory block changes.
1798+ const firstNonSystemIdx = event . messages . findIndex ( ( m ) => m . role !== "system" ) ;
1799+ if ( firstNonSystemIdx === - 1 ) {
1800+ event . messages . push ( { role : "system" , content : memoryContext } ) ;
1801+ } else {
1802+ event . messages . splice ( firstNonSystemIdx , 0 , { role : "system" , content : memoryContext } ) ;
1803+ }
1804+ ctx . log . info ( `before_context_send: injected recall context as system message at idx=${ firstNonSystemIdx === - 1 ? event . messages . length - 1 : firstNonSystemIdx } (${ memoryContext . length } chars)` ) ;
1805+ } ) ;
1806+
17821807 // ─── Auto-recall: inject relevant memories before agent starts ───
17831808
17841809 api . on ( "before_agent_start" , async ( event : { prompt ?: string ; messages ?: unknown [ ] } , hookCtx ?: { agentId ?: string ; sessionKey ?: string } ) => {
@@ -1884,7 +1909,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
18841909 "\n\nYou SHOULD call `skill_get` to retrieve the full guide before attempting the task." ;
18851910 ctx . log . info ( `auto-recall-skill (no-memory path): injecting ${ topSkills . length } skill(s)` ) ;
18861911 try { store . recordApiLog ( "skill_search" , { type : "auto_recall_skill" , query } , JSON . stringify ( topSkills ) , dur , true ) ; } catch { /* best-effort */ }
1887- return { prependContext : skillContext } ;
1912+ pendingRecallContext = skillContext ;
1913+ return { } ;
18881914 }
18891915 } catch ( err ) {
18901916 ctx . log . debug ( `auto-recall-skill (no-memory path): failed: ${ err } ` ) ;
@@ -1893,11 +1919,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
18931919
18941920 if ( query . length > 50 ) {
18951921 const noRecallHint =
1896- "## Memory system — ACTION REQUIRED \n\n" +
1922+ "## Memory system\n\n" +
18971923 "Auto-recall found no results for a long query. " +
1898- "You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
1899- "Do NOT skip this step. Do NOT answer without searching first." ;
1900- return { prependContext : noRecallHint } ;
1924+ "Call `memory_search` with a shortened query (2-5 key words) before answering." ;
1925+ pendingRecallContext = noRecallHint ;
1926+ return { } ;
19011927 }
19021928 return ;
19031929 }
@@ -1933,7 +1959,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
19331959 } ) , dur , true ) ;
19341960 if ( query . length > 50 ) {
19351961 const noRecallHint =
1936- "## Memory system — ACTION REQUIRED \n\n" +
1962+ "## Memory system\n\n" +
19371963 "Auto-recall found no relevant results for a long query. " +
19381964 "You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
19391965 "Do NOT skip this step. Do NOT answer without searching first." ;
@@ -1985,11 +2011,9 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
19852011 const tipsText = "\n\nAvailable follow-up tools:\n" + tips . join ( "\n" ) ;
19862012
19872013 const contextParts = [
1988- "## User's conversation history (from memory system) " ,
2014+ "## Recalled memories " ,
19892015 "" ,
1990- "IMPORTANT: The following are facts from previous conversations with this user." ,
1991- "You MUST treat these as established knowledge and use them directly when answering." ,
1992- "Do NOT say you don't know or don't have information if the answer is in these memories." ,
2016+ "The following facts were retrieved from previous conversations with this user. Treat them as established knowledge." ,
19932017 "" ,
19942018 lines . join ( "\n\n" ) ,
19952019 ] ;
@@ -2073,18 +2097,18 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
20732097 } ) , recallDur , true ) ;
20742098 telemetry . trackAutoRecall ( filteredHits . length , recallDur ) ;
20752099
2076- ctx . log . info ( `auto-recall: returning prependContext (${ context . length } chars), sufficient=${ sufficient } , skills=${ skillSection ? "yes" : "no" } ` ) ;
2100+ ctx . log . info ( `auto-recall: stashing recall context for system message injection (${ context . length } chars), sufficient=${ sufficient } , skills=${ skillSection ? "yes" : "no" } ` ) ;
20772101
20782102 if ( ! sufficient ) {
20792103 const searchHint =
20802104 "\n\nIf these memories don't fully answer the question, " +
20812105 "call `memory_search` with a shorter or rephrased query to find more." ;
2082- return { prependContext : context + searchHint } ;
2106+ pendingRecallContext = context + searchHint ;
2107+ return { } ;
20832108 }
20842109
2085- return {
2086- prependContext : context ,
2087- } ;
2110+ pendingRecallContext = context ;
2111+ return { } ;
20882112 } catch ( err ) {
20892113 const dur = performance . now ( ) - recallT0 ;
20902114 store . recordToolCall ( "memory_search" , dur , false ) ;
0 commit comments