@@ -324,6 +324,126 @@ func TestHandleSaveRecordsActivityForExplicitSessionID(t *testing.T) {
324324 }
325325}
326326
327+ // TestHandleSaveResolvesActiveSessionFromStore reproduces issue #386: the
328+ // SessionStart hook registers a UUID session via POST /sessions (a separate
329+ // process from the MCP server, sharing only the SQLite store). A later
330+ // mem_save with no explicit session_id must attach to that UUID session,
331+ // resolved from the persisted sessions table — NOT fall back to
332+ // manual-save-{project}. The two processes never share in-memory state, so
333+ // store-based resolution is the only thing that survives the process split.
334+ func TestHandleSaveResolvesActiveSessionFromStore (t * testing.T ) {
335+ s := newMCPTestStore (t )
336+
337+ // Simulate the SessionStart hook registering a UUID session (POST /sessions
338+ // ultimately calls store.CreateSession).
339+ const uuidSession = "0c8e7f2a-1b34-4d9e-9a77-aaaabbbbcccc"
340+ if err := s .CreateSession (uuidSession , "engram" , "/work/engram" ); err != nil {
341+ t .Fatalf ("create UUID session: %v" , err )
342+ }
343+
344+ // mem_save with NO session_id — exactly what the proactive protocol does.
345+ h := handleSave (s , MCPConfig {}, NewSessionActivity (10 * time .Minute ))
346+ res , err := h (context .Background (), mcppkg.CallToolRequest {Params : mcppkg.CallToolParams {Arguments : map [string ]any {
347+ "title" : "Active session resolution" ,
348+ "content" : "**What**: saved without session_id\n **Why**: repro for #386" ,
349+ "type" : "bugfix" ,
350+ "project" : "engram" ,
351+ }}})
352+ if err != nil {
353+ t .Fatalf ("handler error: %v" , err )
354+ }
355+ if res .IsError {
356+ t .Fatalf ("unexpected save error: %s" , callResultText (t , res ))
357+ }
358+
359+ obs , err := s .RecentObservations ("engram" , "project" , 5 )
360+ if err != nil {
361+ t .Fatalf ("recent observations: %v" , err )
362+ }
363+ if len (obs ) == 0 {
364+ t .Fatalf ("expected at least one observation, got none" )
365+ }
366+ if obs [0 ].SessionID != uuidSession {
367+ t .Fatalf ("expected observation to attach to active UUID session %q, got %q (regression #386: fell back to manual-save)" , uuidSession , obs [0 ].SessionID )
368+ }
369+ }
370+
371+ // TestHandleSaveFallsBackToManualSaveWhenNoActiveSession is the regression
372+ // guard for the preserved behavior: when there is no un-ended session for the
373+ // project, mem_save with no session_id must still use manual-save-{project}.
374+ func TestHandleSaveFallsBackToManualSaveWhenNoActiveSession (t * testing.T ) {
375+ s := newMCPTestStore (t )
376+
377+ h := handleSave (s , MCPConfig {}, NewSessionActivity (10 * time .Minute ))
378+ res , err := h (context .Background (), mcppkg.CallToolRequest {Params : mcppkg.CallToolParams {Arguments : map [string ]any {
379+ "title" : "No active session" ,
380+ "content" : "**What**: saved with no active session\n **Why**: fallback regression guard" ,
381+ "type" : "bugfix" ,
382+ "project" : "engram" ,
383+ }}})
384+ if err != nil {
385+ t .Fatalf ("handler error: %v" , err )
386+ }
387+ if res .IsError {
388+ t .Fatalf ("unexpected save error: %s" , callResultText (t , res ))
389+ }
390+
391+ obs , err := s .RecentObservations ("engram" , "project" , 5 )
392+ if err != nil {
393+ t .Fatalf ("recent observations: %v" , err )
394+ }
395+ if len (obs ) == 0 {
396+ t .Fatalf ("expected at least one observation, got none" )
397+ }
398+ if want := defaultSessionID ("engram" ); obs [0 ].SessionID != want {
399+ t .Fatalf ("expected fallback to %q with no active session, got %q" , want , obs [0 ].SessionID )
400+ }
401+ }
402+
403+ // TestHandleSaveResolvesMostRecentActiveSession covers the multi-session edge
404+ // case: two un-ended sessions exist; mem_save must attach to the most recent.
405+ func TestHandleSaveResolvesMostRecentActiveSession (t * testing.T ) {
406+ s := newMCPTestStore (t )
407+
408+ if err := s .CreateSession ("uuid-older" , "engram" , "/work/engram" ); err != nil {
409+ t .Fatalf ("create older session: %v" , err )
410+ }
411+ if _ , err := s .DB ().Exec (`UPDATE sessions SET started_at = ? WHERE id = ?` , "2025-01-01 00:00:00" , "uuid-older" ); err != nil {
412+ t .Fatalf ("backdate older session: %v" , err )
413+ }
414+ if err := s .CreateSession ("uuid-newer" , "engram" , "/work/engram" ); err != nil {
415+ t .Fatalf ("create newer session: %v" , err )
416+ }
417+ if _ , err := s .DB ().Exec (`UPDATE sessions SET started_at = ? WHERE id = ?` , "2025-06-01 00:00:00" , "uuid-newer" ); err != nil {
418+ t .Fatalf ("set newer session started_at: %v" , err )
419+ }
420+
421+ h := handleSave (s , MCPConfig {}, NewSessionActivity (10 * time .Minute ))
422+ res , err := h (context .Background (), mcppkg.CallToolRequest {Params : mcppkg.CallToolParams {Arguments : map [string ]any {
423+ "title" : "Most recent active session" ,
424+ "content" : "**What**: saved with two active sessions\n **Why**: multi-session edge case" ,
425+ "type" : "bugfix" ,
426+ "project" : "engram" ,
427+ }}})
428+ if err != nil {
429+ t .Fatalf ("handler error: %v" , err )
430+ }
431+ if res .IsError {
432+ t .Fatalf ("unexpected save error: %s" , callResultText (t , res ))
433+ }
434+
435+ obs , err := s .RecentObservations ("engram" , "project" , 5 )
436+ if err != nil {
437+ t .Fatalf ("recent observations: %v" , err )
438+ }
439+ if len (obs ) == 0 {
440+ t .Fatalf ("expected at least one observation, got none" )
441+ }
442+ if obs [0 ].SessionID != "uuid-newer" {
443+ t .Fatalf ("expected most recent active session uuid-newer, got %q" , obs [0 ].SessionID )
444+ }
445+ }
446+
327447func TestHandleSaveWithNilActivityStillSucceeds (t * testing.T ) {
328448 s := newMCPTestStore (t )
329449 h := handleSave (s , MCPConfig {}, nil )
0 commit comments