@@ -14,37 +14,47 @@ const TEST_CONFIG: MemoryConfig = {
1414function createMockMemorySystem ( overrides ?: {
1515 ready ?: boolean ;
1616 episodes ?: ReturnType < MemorySystem [ "recallEpisodes" ] > ;
17+ durableEpisodes ?: ReturnType < MemorySystem [ "recallEpisodes" ] > ;
1718 facts ?: ReturnType < MemorySystem [ "recallFacts" ] > ;
1819 procedure ?: ReturnType < MemorySystem [ "findProcedure" ] > ;
19- } ) : MemorySystem {
20- const ms = {
20+ } ) {
21+ const recallEpisodes = mock ( ( _query : string , options ?: { strategy ?: string } ) => {
22+ if ( options ?. strategy === "metadata" ) {
23+ return overrides ?. durableEpisodes ?? Promise . resolve ( [ ] ) ;
24+ }
25+
26+ return overrides ?. episodes ?? Promise . resolve ( [ ] ) ;
27+ } ) ;
28+ const recallFacts = mock ( ( ) => overrides ?. facts ?? Promise . resolve ( [ ] ) ) ;
29+ const findProcedure = mock ( ( ) => overrides ?. procedure ?? Promise . resolve ( null ) ) ;
30+ const memory = {
2131 isReady : ( ) => overrides ?. ready ?? true ,
22- recallEpisodes : mock ( ( ) => overrides ?. episodes ?? Promise . resolve ( [ ] ) ) ,
23- recallFacts : mock ( ( ) => overrides ?. facts ?? Promise . resolve ( [ ] ) ) ,
24- findProcedure : mock ( ( ) => overrides ?. procedure ?? Promise . resolve ( null ) ) ,
32+ recallEpisodes,
33+ recallFacts,
34+ findProcedure,
2535 } as unknown as MemorySystem ;
26- return ms ;
36+ return { memory , recallEpisodes , recallFacts , findProcedure } ;
2737}
2838
2939describe ( "MemoryContextBuilder" , ( ) => {
3040 test ( "returns empty string when memory system is not ready" , async ( ) => {
31- const memory = createMockMemorySystem ( { ready : false } ) ;
41+ const { memory } = createMockMemorySystem ( { ready : false } ) ;
3242 const builder = new MemoryContextBuilder ( memory , TEST_CONFIG ) ;
3343
3444 const result = await builder . build ( "test query" ) ;
3545 expect ( result ) . toBe ( "" ) ;
3646 } ) ;
3747
3848 test ( "returns empty string when no memories found" , async ( ) => {
39- const memory = createMockMemorySystem ( ) ;
49+ const { memory } = createMockMemorySystem ( ) ;
4050 const builder = new MemoryContextBuilder ( memory , TEST_CONFIG ) ;
4151
4252 const result = await builder . build ( "test query" ) ;
4353 expect ( result ) . toBe ( "" ) ;
4454 } ) ;
4555
4656 test ( "formats facts section correctly" , async ( ) => {
47- const memory = createMockMemorySystem ( {
57+ const { memory } = createMockMemorySystem ( {
4858 facts : Promise . resolve ( [
4959 {
5060 id : "f1" ,
@@ -89,7 +99,7 @@ describe("MemoryContextBuilder", () => {
8999 } ) ;
90100
91101 test ( "formats episodes section correctly" , async ( ) => {
92- const memory = createMockMemorySystem ( {
102+ const { memory } = createMockMemorySystem ( {
93103 episodes : Promise . resolve ( [
94104 {
95105 id : "ep1" ,
@@ -180,7 +190,7 @@ describe("MemoryContextBuilder", () => {
180190 } ) ;
181191
182192 test ( "formats procedure section correctly" , async ( ) => {
183- const memory = createMockMemorySystem ( {
193+ const { memory } = createMockMemorySystem ( {
184194 procedure : Promise . resolve ( {
185195 id : "proc1" ,
186196 name : "deploy_staging" ,
@@ -225,6 +235,177 @@ describe("MemoryContextBuilder", () => {
225235 expect ( result ) . toContain ( "5 successes" ) ;
226236 } ) ;
227237
238+ test ( "adds durable context on the first turn of a new session" , async ( ) => {
239+ const { memory, recallEpisodes } = createMockMemorySystem ( {
240+ episodes : Promise . resolve ( [
241+ {
242+ id : "ep1" ,
243+ type : "task" as const ,
244+ summary : "Refreshed the deployment runbook" ,
245+ detail : "Full detail" ,
246+ parent_id : null ,
247+ session_id : "s1" ,
248+ user_id : "u1" ,
249+ tools_used : [ "Edit" ] ,
250+ files_touched : [ ] ,
251+ outcome : "success" as const ,
252+ outcome_detail : "" ,
253+ lessons : [ ] ,
254+ started_at : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) ,
255+ ended_at : new Date ( ) . toISOString ( ) ,
256+ duration_seconds : 3600 ,
257+ importance : 0.9 ,
258+ access_count : 3 ,
259+ last_accessed_at : new Date ( ) . toISOString ( ) ,
260+ decay_rate : 1.0 ,
261+ } ,
262+ {
263+ id : "ep2" ,
264+ type : "interaction" as const ,
265+ summary : "Discussed rollout timing for tomorrow" ,
266+ detail : "Full detail" ,
267+ parent_id : null ,
268+ session_id : "s2" ,
269+ user_id : "u1" ,
270+ tools_used : [ ] ,
271+ files_touched : [ ] ,
272+ outcome : "partial" as const ,
273+ outcome_detail : "" ,
274+ lessons : [ ] ,
275+ started_at : new Date ( Date . now ( ) - 7200000 ) . toISOString ( ) ,
276+ ended_at : new Date ( ) . toISOString ( ) ,
277+ duration_seconds : 1800 ,
278+ importance : 0.7 ,
279+ access_count : 1 ,
280+ last_accessed_at : new Date ( ) . toISOString ( ) ,
281+ decay_rate : 1.0 ,
282+ } ,
283+ ] ) ,
284+ durableEpisodes : Promise . resolve ( [
285+ {
286+ id : "ep1" ,
287+ type : "task" as const ,
288+ summary : "Refreshed the deployment runbook" ,
289+ detail : "Full detail" ,
290+ parent_id : null ,
291+ session_id : "s1" ,
292+ user_id : "u1" ,
293+ tools_used : [ "Edit" ] ,
294+ files_touched : [ ] ,
295+ outcome : "success" as const ,
296+ outcome_detail : "" ,
297+ lessons : [ ] ,
298+ started_at : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) ,
299+ ended_at : new Date ( ) . toISOString ( ) ,
300+ duration_seconds : 3600 ,
301+ importance : 0.9 ,
302+ access_count : 3 ,
303+ last_accessed_at : new Date ( ) . toISOString ( ) ,
304+ decay_rate : 1.0 ,
305+ } ,
306+ ] ) ,
307+ facts : Promise . resolve ( [
308+ {
309+ id : "f1" ,
310+ subject : "user" ,
311+ predicate : "prefers" ,
312+ object : "small PRs" ,
313+ natural_language : "The user prefers small PRs" ,
314+ source_episode_ids : [ ] ,
315+ confidence : 0.9 ,
316+ valid_from : new Date ( ) . toISOString ( ) ,
317+ valid_until : null ,
318+ version : 1 ,
319+ previous_version_id : null ,
320+ category : "user_preference" as const ,
321+ tags : [ ] ,
322+ } ,
323+ {
324+ id : "f2" ,
325+ subject : "repo" ,
326+ predicate : "uses" ,
327+ object : "Bun" ,
328+ natural_language : "This repo uses Bun for task execution" ,
329+ source_episode_ids : [ ] ,
330+ confidence : 0.6 ,
331+ valid_from : new Date ( ) . toISOString ( ) ,
332+ valid_until : null ,
333+ version : 1 ,
334+ previous_version_id : null ,
335+ category : "codebase" as const ,
336+ tags : [ ] ,
337+ } ,
338+ ] ) ,
339+ } ) ;
340+
341+ const builder = new MemoryContextBuilder ( memory , TEST_CONFIG ) ;
342+ const result = await builder . build ( "help me deploy" , { isNewSession : true } ) ;
343+
344+ expect ( recallEpisodes ) . toHaveBeenCalledTimes ( 2 ) ;
345+ expect ( result ) . toContain ( "## Durable Context" ) ;
346+ expect ( result ) . toContain ( "Fact: The user prefers small PRs" ) ;
347+ expect ( result ) . toContain ( "Memory: [task] Refreshed the deployment runbook" ) ;
348+ expect ( result ) . toContain ( "## Known Facts" ) ;
349+ expect ( result ) . toContain ( "This repo uses Bun for task execution" ) ;
350+ expect ( result ) . toContain ( "## Recent Memories" ) ;
351+ expect ( result ) . toContain ( "Discussed rollout timing for tomorrow" ) ;
352+ expect ( result . split ( "The user prefers small PRs" ) . length - 1 ) . toBe ( 1 ) ;
353+ expect ( result . split ( "Refreshed the deployment runbook" ) . length - 1 ) . toBe ( 1 ) ;
354+ } ) ;
355+
356+ test ( "skips durable startup context on resumed turns" , async ( ) => {
357+ const { memory, recallEpisodes } = createMockMemorySystem ( {
358+ episodes : Promise . resolve ( [ ] ) ,
359+ durableEpisodes : Promise . resolve ( [
360+ {
361+ id : "ep1" ,
362+ type : "task" as const ,
363+ summary : "Should not be recalled durably" ,
364+ detail : "Full detail" ,
365+ parent_id : null ,
366+ session_id : "s1" ,
367+ user_id : "u1" ,
368+ tools_used : [ ] ,
369+ files_touched : [ ] ,
370+ outcome : "success" as const ,
371+ outcome_detail : "" ,
372+ lessons : [ ] ,
373+ started_at : new Date ( ) . toISOString ( ) ,
374+ ended_at : new Date ( ) . toISOString ( ) ,
375+ duration_seconds : 60 ,
376+ importance : 0.9 ,
377+ access_count : 0 ,
378+ last_accessed_at : "" ,
379+ decay_rate : 1.0 ,
380+ } ,
381+ ] ) ,
382+ facts : Promise . resolve ( [
383+ {
384+ id : "f1" ,
385+ subject : "user" ,
386+ predicate : "prefers" ,
387+ object : "small PRs" ,
388+ natural_language : "The user prefers small PRs" ,
389+ source_episode_ids : [ ] ,
390+ confidence : 0.9 ,
391+ valid_from : new Date ( ) . toISOString ( ) ,
392+ valid_until : null ,
393+ version : 1 ,
394+ previous_version_id : null ,
395+ category : "user_preference" as const ,
396+ tags : [ ] ,
397+ } ,
398+ ] ) ,
399+ } ) ;
400+
401+ const builder = new MemoryContextBuilder ( memory , TEST_CONFIG ) ;
402+ const result = await builder . build ( "help me deploy" ) ;
403+
404+ expect ( recallEpisodes ) . toHaveBeenCalledTimes ( 1 ) ;
405+ expect ( result ) . not . toContain ( "## Durable Context" ) ;
406+ expect ( result ) . toContain ( "## Known Facts" ) ;
407+ } ) ;
408+
228409 test ( "respects token budget and truncates" , async ( ) => {
229410 // Create many facts that would exceed a tiny budget
230411 const manyFacts = Array . from ( { length : 100 } , ( _ , i ) => ( {
@@ -243,7 +424,7 @@ describe("MemoryContextBuilder", () => {
243424 tags : [ ] ,
244425 } ) ) ;
245426
246- const memory = createMockMemorySystem ( {
427+ const { memory } = createMockMemorySystem ( {
247428 facts : Promise . resolve ( manyFacts ) ,
248429 } ) ;
249430
@@ -259,7 +440,7 @@ describe("MemoryContextBuilder", () => {
259440 } ) ;
260441
261442 test ( "handles errors from memory system gracefully" , async ( ) => {
262- const memory = createMockMemorySystem ( {
443+ const { memory } = createMockMemorySystem ( {
263444 episodes : Promise . reject ( new Error ( "Qdrant down" ) ) ,
264445 facts : Promise . reject ( new Error ( "Qdrant down" ) ) ,
265446 procedure : Promise . reject ( new Error ( "Qdrant down" ) ) ,
0 commit comments