@@ -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" ,
@@ -125,7 +135,7 @@ describe("MemoryContextBuilder", () => {
125135 } ) ;
126136
127137 test ( "formats procedure section correctly" , async ( ) => {
128- const memory = createMockMemorySystem ( {
138+ const { memory } = createMockMemorySystem ( {
129139 procedure : Promise . resolve ( {
130140 id : "proc1" ,
131141 name : "deploy_staging" ,
@@ -170,6 +180,177 @@ describe("MemoryContextBuilder", () => {
170180 expect ( result ) . toContain ( "5 successes" ) ;
171181 } ) ;
172182
183+ test ( "adds durable context on the first turn of a new session" , async ( ) => {
184+ const { memory, recallEpisodes } = createMockMemorySystem ( {
185+ episodes : Promise . resolve ( [
186+ {
187+ id : "ep1" ,
188+ type : "task" as const ,
189+ summary : "Refreshed the deployment runbook" ,
190+ detail : "Full detail" ,
191+ parent_id : null ,
192+ session_id : "s1" ,
193+ user_id : "u1" ,
194+ tools_used : [ "Edit" ] ,
195+ files_touched : [ ] ,
196+ outcome : "success" as const ,
197+ outcome_detail : "" ,
198+ lessons : [ ] ,
199+ started_at : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) ,
200+ ended_at : new Date ( ) . toISOString ( ) ,
201+ duration_seconds : 3600 ,
202+ importance : 0.9 ,
203+ access_count : 3 ,
204+ last_accessed_at : new Date ( ) . toISOString ( ) ,
205+ decay_rate : 1.0 ,
206+ } ,
207+ {
208+ id : "ep2" ,
209+ type : "interaction" as const ,
210+ summary : "Discussed rollout timing for tomorrow" ,
211+ detail : "Full detail" ,
212+ parent_id : null ,
213+ session_id : "s2" ,
214+ user_id : "u1" ,
215+ tools_used : [ ] ,
216+ files_touched : [ ] ,
217+ outcome : "partial" as const ,
218+ outcome_detail : "" ,
219+ lessons : [ ] ,
220+ started_at : new Date ( Date . now ( ) - 7200000 ) . toISOString ( ) ,
221+ ended_at : new Date ( ) . toISOString ( ) ,
222+ duration_seconds : 1800 ,
223+ importance : 0.7 ,
224+ access_count : 1 ,
225+ last_accessed_at : new Date ( ) . toISOString ( ) ,
226+ decay_rate : 1.0 ,
227+ } ,
228+ ] ) ,
229+ durableEpisodes : Promise . resolve ( [
230+ {
231+ id : "ep1" ,
232+ type : "task" as const ,
233+ summary : "Refreshed the deployment runbook" ,
234+ detail : "Full detail" ,
235+ parent_id : null ,
236+ session_id : "s1" ,
237+ user_id : "u1" ,
238+ tools_used : [ "Edit" ] ,
239+ files_touched : [ ] ,
240+ outcome : "success" as const ,
241+ outcome_detail : "" ,
242+ lessons : [ ] ,
243+ started_at : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) ,
244+ ended_at : new Date ( ) . toISOString ( ) ,
245+ duration_seconds : 3600 ,
246+ importance : 0.9 ,
247+ access_count : 3 ,
248+ last_accessed_at : new Date ( ) . toISOString ( ) ,
249+ decay_rate : 1.0 ,
250+ } ,
251+ ] ) ,
252+ facts : Promise . resolve ( [
253+ {
254+ id : "f1" ,
255+ subject : "user" ,
256+ predicate : "prefers" ,
257+ object : "small PRs" ,
258+ natural_language : "The user prefers small PRs" ,
259+ source_episode_ids : [ ] ,
260+ confidence : 0.9 ,
261+ valid_from : new Date ( ) . toISOString ( ) ,
262+ valid_until : null ,
263+ version : 1 ,
264+ previous_version_id : null ,
265+ category : "user_preference" as const ,
266+ tags : [ ] ,
267+ } ,
268+ {
269+ id : "f2" ,
270+ subject : "repo" ,
271+ predicate : "uses" ,
272+ object : "Bun" ,
273+ natural_language : "This repo uses Bun for task execution" ,
274+ source_episode_ids : [ ] ,
275+ confidence : 0.6 ,
276+ valid_from : new Date ( ) . toISOString ( ) ,
277+ valid_until : null ,
278+ version : 1 ,
279+ previous_version_id : null ,
280+ category : "codebase" as const ,
281+ tags : [ ] ,
282+ } ,
283+ ] ) ,
284+ } ) ;
285+
286+ const builder = new MemoryContextBuilder ( memory , TEST_CONFIG ) ;
287+ const result = await builder . build ( "help me deploy" , { isNewSession : true } ) ;
288+
289+ expect ( recallEpisodes ) . toHaveBeenCalledTimes ( 2 ) ;
290+ expect ( result ) . toContain ( "## Durable Context" ) ;
291+ expect ( result ) . toContain ( "Fact: The user prefers small PRs" ) ;
292+ expect ( result ) . toContain ( "Memory: [task] Refreshed the deployment runbook" ) ;
293+ expect ( result ) . toContain ( "## Known Facts" ) ;
294+ expect ( result ) . toContain ( "This repo uses Bun for task execution" ) ;
295+ expect ( result ) . toContain ( "## Recent Memories" ) ;
296+ expect ( result ) . toContain ( "Discussed rollout timing for tomorrow" ) ;
297+ expect ( result . split ( "The user prefers small PRs" ) . length - 1 ) . toBe ( 1 ) ;
298+ expect ( result . split ( "Refreshed the deployment runbook" ) . length - 1 ) . toBe ( 1 ) ;
299+ } ) ;
300+
301+ test ( "skips durable startup context on resumed turns" , async ( ) => {
302+ const { memory, recallEpisodes } = createMockMemorySystem ( {
303+ episodes : Promise . resolve ( [ ] ) ,
304+ durableEpisodes : Promise . resolve ( [
305+ {
306+ id : "ep1" ,
307+ type : "task" as const ,
308+ summary : "Should not be recalled durably" ,
309+ detail : "Full detail" ,
310+ parent_id : null ,
311+ session_id : "s1" ,
312+ user_id : "u1" ,
313+ tools_used : [ ] ,
314+ files_touched : [ ] ,
315+ outcome : "success" as const ,
316+ outcome_detail : "" ,
317+ lessons : [ ] ,
318+ started_at : new Date ( ) . toISOString ( ) ,
319+ ended_at : new Date ( ) . toISOString ( ) ,
320+ duration_seconds : 60 ,
321+ importance : 0.9 ,
322+ access_count : 0 ,
323+ last_accessed_at : "" ,
324+ decay_rate : 1.0 ,
325+ } ,
326+ ] ) ,
327+ facts : Promise . resolve ( [
328+ {
329+ id : "f1" ,
330+ subject : "user" ,
331+ predicate : "prefers" ,
332+ object : "small PRs" ,
333+ natural_language : "The user prefers small PRs" ,
334+ source_episode_ids : [ ] ,
335+ confidence : 0.9 ,
336+ valid_from : new Date ( ) . toISOString ( ) ,
337+ valid_until : null ,
338+ version : 1 ,
339+ previous_version_id : null ,
340+ category : "user_preference" as const ,
341+ tags : [ ] ,
342+ } ,
343+ ] ) ,
344+ } ) ;
345+
346+ const builder = new MemoryContextBuilder ( memory , TEST_CONFIG ) ;
347+ const result = await builder . build ( "help me deploy" ) ;
348+
349+ expect ( recallEpisodes ) . toHaveBeenCalledTimes ( 1 ) ;
350+ expect ( result ) . not . toContain ( "## Durable Context" ) ;
351+ expect ( result ) . toContain ( "## Known Facts" ) ;
352+ } ) ;
353+
173354 test ( "respects token budget and truncates" , async ( ) => {
174355 // Create many facts that would exceed a tiny budget
175356 const manyFacts = Array . from ( { length : 100 } , ( _ , i ) => ( {
@@ -188,7 +369,7 @@ describe("MemoryContextBuilder", () => {
188369 tags : [ ] ,
189370 } ) ) ;
190371
191- const memory = createMockMemorySystem ( {
372+ const { memory } = createMockMemorySystem ( {
192373 facts : Promise . resolve ( manyFacts ) ,
193374 } ) ;
194375
@@ -204,7 +385,7 @@ describe("MemoryContextBuilder", () => {
204385 } ) ;
205386
206387 test ( "handles errors from memory system gracefully" , async ( ) => {
207- const memory = createMockMemorySystem ( {
388+ const { memory } = createMockMemorySystem ( {
208389 episodes : Promise . reject ( new Error ( "Qdrant down" ) ) ,
209390 facts : Promise . reject ( new Error ( "Qdrant down" ) ) ,
210391 procedure : Promise . reject ( new Error ( "Qdrant down" ) ) ,
0 commit comments