@@ -182,7 +182,7 @@ describe("context-engine afterTurn()", () => {
182182 ) ;
183183 } ) ;
184184
185- it ( "stores new messages via addSessionMessage" , async ( ) => {
185+ it ( "stores new messages via addSessionMessage with proper roles " , async ( ) => {
186186 const { engine, client } = makeEngine ( ) ;
187187
188188 const messages = [
@@ -198,10 +198,13 @@ describe("context-engine afterTurn()", () => {
198198 prePromptMessageCount : 1 ,
199199 } ) ;
200200
201- expect ( client . addSessionMessage ) . toHaveBeenCalledTimes ( 1 ) ;
202- const storedContent = client . addSessionMessage . mock . calls [ 0 ] [ 2 ] as string ;
203- expect ( storedContent ) . toContain ( "hello world" ) ;
204- expect ( storedContent ) . toContain ( "hi there" ) ;
201+ expect ( client . addSessionMessage ) . toHaveBeenCalledTimes ( 2 ) ;
202+ // First call: user message
203+ expect ( client . addSessionMessage . mock . calls [ 0 ] [ 1 ] ) . toBe ( "user" ) ;
204+ expect ( client . addSessionMessage . mock . calls [ 0 ] [ 2 ] ) . toContain ( "hello world" ) ;
205+ // Second call: assistant message
206+ expect ( client . addSessionMessage . mock . calls [ 1 ] [ 1 ] ) . toBe ( "assistant" ) ;
207+ expect ( client . addSessionMessage . mock . calls [ 1 ] [ 2 ] ) . toContain ( "hi there" ) ;
205208 } ) ;
206209
207210 it ( "passes the latest non-system message timestamp to addSessionMessage as ISO string" , async ( ) => {
@@ -220,12 +223,14 @@ describe("context-engine afterTurn()", () => {
220223 prePromptMessageCount : 1 ,
221224 } ) ;
222225
223- expect ( client . addSessionMessage ) . toHaveBeenCalledTimes ( 1 ) ;
224- const createdAt = client . addSessionMessage . mock . calls [ 0 ] [ 4 ] as string ;
226+ // user + assistant + toolResult(→user) = 3 calls (toolResult merges with no adjacent user)
227+ expect ( client . addSessionMessage ) . toHaveBeenCalled ( ) ;
228+ const lastCallIdx = client . addSessionMessage . mock . calls . length - 1 ;
229+ const createdAt = client . addSessionMessage . mock . calls [ lastCallIdx ] [ 4 ] as string ;
225230 expect ( createdAt ) . toBe ( "2026-04-01T10:03:00.000Z" ) ;
226231 } ) ;
227232
228- it ( "sanitizes <relevant-memories> from stored content" , async ( ) => {
233+ it ( "sanitizes <relevant-memories> from user content but not from assistant " , async ( ) => {
229234 const { engine, client } = makeEngine ( ) ;
230235
231236 const messages = [
@@ -243,6 +248,7 @@ describe("context-engine afterTurn()", () => {
243248 } ) ;
244249
245250 expect ( client . addSessionMessage ) . toHaveBeenCalledTimes ( 1 ) ;
251+ expect ( client . addSessionMessage . mock . calls [ 0 ] [ 1 ] ) . toBe ( "user" ) ;
246252 const storedContent = client . addSessionMessage . mock . calls [ 0 ] [ 2 ] as string ;
247253 expect ( storedContent ) . not . toContain ( "relevant-memories" ) ;
248254 expect ( storedContent ) . not . toContain ( "injected memory data" ) ;
@@ -391,10 +397,12 @@ describe("context-engine afterTurn()", () => {
391397 prePromptMessageCount : 0 ,
392398 } ) ;
393399
394- const storedContent = client . addSessionMessage . mock . calls [ 0 ] [ 2 ] as string ;
395- expect ( storedContent ) . toContain ( "src/app.ts" ) ;
396- expect ( storedContent ) . toContain ( "npm install" ) ;
397- expect ( storedContent ) . toContain ( "export const x = 1" ) ;
400+ expect ( client . addSessionMessage ) . toHaveBeenCalledTimes ( 2 ) ;
401+ const userContent = client . addSessionMessage . mock . calls [ 0 ] [ 2 ] as string ;
402+ const assistantContent = client . addSessionMessage . mock . calls [ 1 ] [ 2 ] as string ;
403+ expect ( userContent ) . toContain ( "src/app.ts" ) ;
404+ expect ( userContent ) . toContain ( "npm install" ) ;
405+ expect ( assistantContent ) . toContain ( "export const x = 1" ) ;
398406 } ) ;
399407
400408 it ( "passes agentId to addSessionMessage" , async ( ) => {
@@ -428,6 +436,143 @@ describe("context-engine afterTurn()", () => {
428436 expect ( client . getSession ) . toHaveBeenCalled ( ) ;
429437 } ) ;
430438
439+ it ( "maps toolResult to user role" , async ( ) => {
440+ const { engine, client } = makeEngine ( ) ;
441+
442+ const messages = [
443+ { role : "assistant" , content : [
444+ { type : "text" , text : "running tool" } ,
445+ { type : "toolUse" , name : "bash" , input : { cmd : "ls" } } ,
446+ ] } ,
447+ { role : "toolResult" , toolName : "bash" , content : "file1.txt\nfile2.txt" } ,
448+ { role : "assistant" , content : "done" } ,
449+ ] ;
450+
451+ await engine . afterTurn ! ( {
452+ sessionId : "s1" ,
453+ sessionFile : "" ,
454+ messages,
455+ prePromptMessageCount : 0 ,
456+ } ) ;
457+
458+ expect ( client . addSessionMessage ) . toHaveBeenCalledTimes ( 3 ) ;
459+ // assistant → user(toolResult) → assistant
460+ expect ( client . addSessionMessage . mock . calls [ 0 ] [ 1 ] ) . toBe ( "assistant" ) ;
461+ expect ( client . addSessionMessage . mock . calls [ 1 ] [ 1 ] ) . toBe ( "user" ) ;
462+ expect ( client . addSessionMessage . mock . calls [ 1 ] [ 2 ] ) . toContain ( "[bash result]:" ) ;
463+ expect ( client . addSessionMessage . mock . calls [ 1 ] [ 2 ] ) . toContain ( "file1.txt" ) ;
464+ expect ( client . addSessionMessage . mock . calls [ 2 ] [ 1 ] ) . toBe ( "assistant" ) ;
465+ } ) ;
466+
467+ it ( "merges adjacent same-role messages" , async ( ) => {
468+ const { engine, client } = makeEngine ( ) ;
469+
470+ const messages = [
471+ { role : "user" , content : "first question" } ,
472+ { role : "user" , content : "second question" } ,
473+ { role : "assistant" , content : "answer" } ,
474+ ] ;
475+
476+ await engine . afterTurn ! ( {
477+ sessionId : "s1" ,
478+ sessionFile : "" ,
479+ messages,
480+ prePromptMessageCount : 0 ,
481+ } ) ;
482+
483+ expect ( client . addSessionMessage ) . toHaveBeenCalledTimes ( 2 ) ;
484+ expect ( client . addSessionMessage . mock . calls [ 0 ] [ 1 ] ) . toBe ( "user" ) ;
485+ expect ( client . addSessionMessage . mock . calls [ 0 ] [ 2 ] ) . toContain ( "first question" ) ;
486+ expect ( client . addSessionMessage . mock . calls [ 0 ] [ 2 ] ) . toContain ( "second question" ) ;
487+ expect ( client . addSessionMessage . mock . calls [ 1 ] [ 1 ] ) . toBe ( "assistant" ) ;
488+ } ) ;
489+
490+ it ( "merges adjacent toolResults into one user group" , async ( ) => {
491+ const { engine, client } = makeEngine ( ) ;
492+
493+ const messages = [
494+ { role : "assistant" , content : [
495+ { type : "text" , text : "calling tools" } ,
496+ { type : "toolUse" , name : "read" , input : { path : "a.txt" } } ,
497+ ] } ,
498+ { role : "toolResult" , toolName : "read" , content : "content of a" } ,
499+ { role : "toolResult" , toolName : "write" , content : "ok" } ,
500+ { role : "assistant" , content : "all done" } ,
501+ ] ;
502+
503+ await engine . afterTurn ! ( {
504+ sessionId : "s1" ,
505+ sessionFile : "" ,
506+ messages,
507+ prePromptMessageCount : 0 ,
508+ } ) ;
509+
510+ expect ( client . addSessionMessage ) . toHaveBeenCalledTimes ( 3 ) ;
511+ expect ( client . addSessionMessage . mock . calls [ 0 ] [ 1 ] ) . toBe ( "assistant" ) ;
512+ // Two toolResults merged into one user call
513+ expect ( client . addSessionMessage . mock . calls [ 1 ] [ 1 ] ) . toBe ( "user" ) ;
514+ expect ( client . addSessionMessage . mock . calls [ 1 ] [ 2 ] ) . toContain ( "[read result]:" ) ;
515+ expect ( client . addSessionMessage . mock . calls [ 1 ] [ 2 ] ) . toContain ( "[write result]:" ) ;
516+ expect ( client . addSessionMessage . mock . calls [ 2 ] [ 1 ] ) . toBe ( "assistant" ) ;
517+ } ) ;
518+
519+ it ( "does not sanitize <relevant-memories> from assistant content" , async ( ) => {
520+ const { engine, client } = makeEngine ( ) ;
521+
522+ const messages = [
523+ { role : "user" , content : "question" } ,
524+ { role : "assistant" , content : "Here is context <relevant-memories>data</relevant-memories> end" } ,
525+ ] ;
526+
527+ await engine . afterTurn ! ( {
528+ sessionId : "s1" ,
529+ sessionFile : "" ,
530+ messages,
531+ prePromptMessageCount : 0 ,
532+ } ) ;
533+
534+ expect ( client . addSessionMessage ) . toHaveBeenCalledTimes ( 2 ) ;
535+ const assistantContent = client . addSessionMessage . mock . calls [ 1 ] [ 2 ] as string ;
536+ expect ( assistantContent ) . toContain ( "relevant-memories" ) ;
537+ } ) ;
538+
539+ it ( "skips heartbeat messages from being stored" , async ( ) => {
540+ const { engine, client } = makeEngine ( ) ;
541+
542+ const messages = [
543+ { role : "user" , content : "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK." } ,
544+ { role : "assistant" , content : "HEARTBEAT_OK" } ,
545+ ] ;
546+
547+ await engine . afterTurn ! ( {
548+ sessionId : "s1" ,
549+ sessionFile : "" ,
550+ messages,
551+ prePromptMessageCount : 0 ,
552+ } ) ;
553+
554+ expect ( client . addSessionMessage ) . not . toHaveBeenCalled ( ) ;
555+ } ) ;
556+
557+ it ( "skips heartbeat via isHeartbeat flag" , async ( ) => {
558+ const { engine, client } = makeEngine ( ) ;
559+
560+ const messages = [
561+ { role : "user" , content : "regular message" } ,
562+ { role : "assistant" , content : "reply" } ,
563+ ] ;
564+
565+ await engine . afterTurn ! ( {
566+ sessionId : "s1" ,
567+ sessionFile : "" ,
568+ messages,
569+ prePromptMessageCount : 0 ,
570+ isHeartbeat : true ,
571+ } ) ;
572+
573+ expect ( client . addSessionMessage ) . not . toHaveBeenCalled ( ) ;
574+ } ) ;
575+
431576 it ( "skips store when all new messages are system only" , async ( ) => {
432577 const { engine, client } = makeEngine ( ) ;
433578
0 commit comments