22 * Shared core for Reflect Memory browser extension.
33 *
44 * Architecture: "Lazy Priming" with selective write-back.
5- *
6- * READ: The extension waits for the user to submit their first message,
7- * searches for relevant memories, and only if matches exist, sends a
8- * hidden priming message before the user's real message. If nothing
9- * is relevant, zero interference.
10- *
11- * WRITE: Only conversations where priming fired (meaning the topic is
12- * related to stored context) have their exchanges captured back to
13- * Reflect Memory. Unrelated conversations are never written.
14- *
15- * Each vendor script implements a VendorAdapter:
16- * getInputElement() - returns the chat textarea/contenteditable
17- * getInputValue() - reads current user input text
18- * setInputValue(text) - sets the input text
19- * getMessages() - returns [{ role, text }] from the DOM
20- * onNewMessage(cb) - calls cb(messages) on DOM changes
21- * triggerSend() - clicks the send button programmatically
22- * isNewConversation() - true if the chat has zero messages
23- * hideLastExchange() - hides the priming message + response from view
245 */
256
267const PRIMING_MARKER = "[[REFLECT_MEMORY_PRIME]]" ;
278const PRIMED_KEY = "reflect_memory_primed" ;
289const WRITE_ENABLED_KEY = "reflect_memory_write_enabled" ;
10+ const DEBUG = true ;
2911
3012let isPriming = false ;
3113let lastCapturedCount = 0 ;
3214
15+ function log ( ...args ) {
16+ if ( DEBUG ) console . log ( "[Reflect Memory]" , ...args ) ;
17+ }
18+
3319async function sendToBackground ( message ) {
3420 return new Promise ( ( resolve ) => {
3521 chrome . runtime . sendMessage ( message , resolve ) ;
@@ -84,21 +70,21 @@ function enableWrite() {
8470 sessionStorage . setItem ( getSessionKey ( WRITE_ENABLED_KEY ) , "true" ) ;
8571}
8672
87- /**
88- * Intercepts the user's first message in a new conversation.
89- * Searches for relevant memories. If found, primes the AI first,
90- * then sends the user's real message. If not found, sends normally
91- * and write-back stays disabled for this conversation.
92- */
9373async function interceptFirstMessage ( adapter , userMessage ) {
94- if ( isPriming || isAlreadyPrimed ( ) ) return false ;
74+ if ( isPriming || isAlreadyPrimed ( ) ) {
75+ log ( "Skipping: already primed or priming in progress" ) ;
76+ return false ;
77+ }
9578 if ( ! adapter . isNewConversation ( ) ) {
79+ log ( "Skipping: not a new conversation" ) ;
9680 markAsPrimed ( ) ;
9781 return false ;
9882 }
9983
10084 const authCheck = await sendToBackground ( { type : "CHECK_AUTH" } ) ;
85+ log ( "Auth check:" , authCheck ) ;
10186 if ( ! authCheck ?. authenticated ) {
87+ log ( "Not authenticated. Check your agent key." ) ;
10288 markAsPrimed ( ) ;
10389 return false ;
10490 }
@@ -107,11 +93,14 @@ async function interceptFirstMessage(adapter, userMessage) {
10793
10894 try {
10995 const searchTerm = userMessage . slice ( 0 , 200 ) ;
96+ log ( "Searching memories for:" , searchTerm ) ;
11097 const response = await sendToBackground ( {
11198 type : "SEARCH_MEMORIES" ,
11299 term : searchTerm ,
113100 } ) ;
114101
102+ log ( "Search result:" , response ?. memories ?. length || 0 , "memories found" ) ;
103+
115104 if ( ! response ?. memories ?. length ) {
116105 isPriming = false ;
117106 markAsPrimed ( ) ;
@@ -125,20 +114,29 @@ async function interceptFirstMessage(adapter, userMessage) {
125114 return false ;
126115 }
127116
117+ log ( "Setting priming text..." ) ;
128118 adapter . setInputValue ( primingText ) ;
129- await new Promise ( ( r ) => setTimeout ( r , 100 ) ) ;
119+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
120+
121+ log ( "Sending priming message..." ) ;
130122 adapter . triggerSend ( ) ;
131123
124+ log ( "Waiting for AI response..." ) ;
132125 await waitForResponse ( adapter ) ;
126+
127+ log ( "Hiding priming exchange..." ) ;
133128 adapter . hideLastExchange ( ) ;
134129 markAsPrimed ( ) ;
135130 enableWrite ( ) ;
136131
132+ log ( "Sending user's real message:" , userMessage . slice ( 0 , 50 ) ) ;
137133 adapter . setInputValue ( userMessage ) ;
138- await new Promise ( ( r ) => setTimeout ( r , 100 ) ) ;
134+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
139135 adapter . triggerSend ( ) ;
140136
141- } catch {
137+ log ( "Priming complete." ) ;
138+ } catch ( err ) {
139+ log ( "Error during priming:" , err ) ;
142140 adapter . setInputValue ( userMessage ) ;
143141 await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
144142 adapter . triggerSend ( ) ;
@@ -162,6 +160,7 @@ function waitForResponse(adapter) {
162160
163161 if ( hasResponse || checks >= maxChecks ) {
164162 clearInterval ( interval ) ;
163+ log ( "Response wait done. Checks:" , checks , "Messages:" , current . length ) ;
165164 setTimeout ( resolve , 300 ) ;
166165 }
167166 } , 500 ) ;
@@ -207,6 +206,7 @@ async function captureAsMemory(messages, vendor) {
207206 `Response: ${ lastAI . slice ( 0 , 600 ) } ` ,
208207 ] . join ( "\n\n" ) ;
209208
209+ log ( "Writing memory:" , title ) ;
210210 await sendToBackground ( {
211211 type : "WRITE_MEMORY" ,
212212 data : {
@@ -218,14 +218,33 @@ async function captureAsMemory(messages, vendor) {
218218}
219219
220220function initVendor ( adapter , vendorName ) {
221+ log ( `Initializing ${ vendorName } adapter...` ) ;
221222 let debounceTimer = null ;
222223 let sendIntercepted = false ;
223224
224225 function hookSendInterception ( ) {
225226 const el = adapter . getInputElement ( ) ;
226- if ( ! el || sendIntercepted ) return ;
227+ if ( ! el ) {
228+ log ( "Input element NOT found. Selectors need updating." ) ;
229+ log ( "Scanning page for contenteditable elements..." ) ;
230+ const allEditable = document . querySelectorAll ( "[contenteditable='true']" ) ;
231+ log ( `Found ${ allEditable . length } contenteditable elements:` ) ;
232+ allEditable . forEach ( ( e , i ) => {
233+ log ( ` [${ i } ] tag=${ e . tagName } class="${ e . className . slice ( 0 , 80 ) } " role=${ e . getAttribute ( "role" ) } placeholder=${ e . getAttribute ( "data-placeholder" ) || e . getAttribute ( "aria-placeholder" ) } ` ) ;
234+ } ) ;
235+ const allTextareas = document . querySelectorAll ( "textarea" ) ;
236+ log ( `Found ${ allTextareas . length } textarea elements:` ) ;
237+ allTextareas . forEach ( ( e , i ) => {
238+ log ( ` [${ i } ] placeholder="${ e . placeholder ?. slice ( 0 , 50 ) } " name=${ e . name } ` ) ;
239+ } ) ;
240+ return ;
241+ }
242+ if ( sendIntercepted ) return ;
227243 sendIntercepted = true ;
228244
245+ log ( "Input element found:" , el . tagName , el . className ?. slice ( 0 , 60 ) ) ;
246+ log ( "Keydown listener attached. Waiting for first Enter press." ) ;
247+
229248 el . addEventListener ( "keydown" , ( e ) => {
230249 if ( e . key !== "Enter" || e . shiftKey ) return ;
231250 if ( isPriming ) return ;
@@ -234,11 +253,13 @@ function initVendor(adapter, vendorName) {
234253 const userMessage = adapter . getInputValue ( ) ?. trim ( ) ;
235254 if ( ! userMessage ) return ;
236255
256+ log ( "Enter intercepted. Message:" , userMessage . slice ( 0 , 50 ) ) ;
237257 e . preventDefault ( ) ;
238258 e . stopImmediatePropagation ( ) ;
239259
240260 interceptFirstMessage ( adapter , userMessage ) . then ( ( handled ) => {
241261 if ( ! handled ) {
262+ log ( "No priming needed. Sending original message." ) ;
242263 adapter . setInputValue ( userMessage ) ;
243264 setTimeout ( ( ) => adapter . triggerSend ( ) , 50 ) ;
244265 }
@@ -254,7 +275,13 @@ function initVendor(adapter, vendorName) {
254275 }
255276 } , 800 ) ;
256277
257- setTimeout ( ( ) => clearInterval ( waitForInput ) , 30000 ) ;
278+ setTimeout ( ( ) => {
279+ clearInterval ( waitForInput ) ;
280+ if ( ! sendIntercepted ) {
281+ log ( "TIMEOUT: Input element never found after 30s. Running diagnostic..." ) ;
282+ hookSendInterception ( ) ;
283+ }
284+ } , 30000 ) ;
258285
259286 const bodyObserver = new MutationObserver ( ( ) => {
260287 if ( ! sendIntercepted || ! adapter . getInputElement ( ) ) {
0 commit comments