@@ -15,6 +15,12 @@ const mockNewSession = vi.hoisted(() =>
1515 configOptions : [ ] ,
1616 } ) ,
1717) ;
18+ const mockResumeSession = vi . hoisted ( ( ) =>
19+ vi . fn ( ) . mockResolvedValue ( { configOptions : [ ] } ) ,
20+ ) ;
21+ const mockPrompt = vi . hoisted ( ( ) =>
22+ vi . fn ( ) . mockResolvedValue ( { stopReason : "end_turn" } ) ,
23+ ) ;
1824
1925const mockAcpClient = vi . hoisted ( ( ) => ( {
2026 current : undefined as
@@ -40,7 +46,8 @@ const mockClientSideConnection = vi.hoisted(() =>
4046 this . initialize = vi . fn ( ) . mockResolvedValue ( { } ) ;
4147 this . newSession = mockNewSession ;
4248 this . loadSession = vi . fn ( ) . mockResolvedValue ( { configOptions : [ ] } ) ;
43- this . resumeSession = vi . fn ( ) . mockResolvedValue ( { configOptions : [ ] } ) ;
49+ this . resumeSession = mockResumeSession ;
50+ this . prompt = mockPrompt ;
4451 this . setSessionConfigOption = vi . fn (
4552 async ( { value } : { value : string } ) => ( {
4653 configOptions : [
@@ -70,6 +77,12 @@ const mockAgentRun = vi.hoisted(() =>
7077 ) ,
7178) ;
7279
80+ const mockResumeFromLog = vi . hoisted ( ( ) => vi . fn ( ) ) ;
81+ const mockFormatConversationForResume = vi . hoisted ( ( ) => vi . fn ( ) ) ;
82+ const mockHydrateSessionJsonl = vi . hoisted ( ( ) =>
83+ vi . fn ( ) . mockResolvedValue ( { hasSession : false } ) ,
84+ ) ;
85+
7386const mockAgentConstructor = vi . hoisted ( ( ) =>
7487 vi . fn ( ) . mockImplementation ( function ( this : Record < string , unknown > ) {
7588 this . run = mockAgentRun ;
@@ -103,6 +116,11 @@ vi.mock("@posthog/agent/posthog-api", () => ({
103116 getLlmGatewayUrl : vi . fn ( ( ) => "https://gateway.example.com" ) ,
104117} ) ) ;
105118
119+ vi . mock ( "@posthog/agent/resume" , ( ) => ( {
120+ resumeFromLog : mockResumeFromLog ,
121+ formatConversationForResume : mockFormatConversationForResume ,
122+ } ) ) ;
123+
106124vi . mock ( "@posthog/agent/gateway-models" , ( ) => ( {
107125 DEFAULT_GATEWAY_MODEL : "claude-opus-4-8" ,
108126 DEFAULT_CODEX_MODEL : "gpt-5.5" ,
@@ -113,7 +131,7 @@ vi.mock("@posthog/agent/gateway-models", () => ({
113131} ) ) ;
114132
115133vi . mock ( "@posthog/agent/adapters/claude/session/jsonl-hydration" , ( ) => ( {
116- hydrateSessionJsonl : vi . fn ( ) . mockResolvedValue ( undefined ) ,
134+ hydrateSessionJsonl : mockHydrateSessionJsonl ,
117135} ) ) ;
118136
119137vi . mock ( "node:fs" , async ( importOriginal ) => {
@@ -334,6 +352,148 @@ describe("AgentService", () => {
334352 } ) ;
335353 } ) ;
336354
355+ describe ( "reconnect" , ( ) => {
356+ it ( "preserves conversation context when native reconnect fails" , async ( ) => {
357+ const apiClient = { } ;
358+ mockAgentConstructor . mockImplementationOnce ( function (
359+ this : Record < string , unknown > ,
360+ ) {
361+ this . run = mockAgentRun ;
362+ this . cleanup = vi . fn ( ) . mockResolvedValue ( undefined ) ;
363+ this . getPosthogAPI = vi . fn ( ( ) => apiClient ) ;
364+ this . flushAllLogs = vi . fn ( ) . mockResolvedValue ( undefined ) ;
365+ } ) ;
366+ mockResumeFromLog . mockResolvedValue ( { conversation : [ { role : "user" } ] } ) ;
367+ mockFormatConversationForResume . mockReturnValue ( "User: previous request" ) ;
368+ mockResumeSession . mockRejectedValueOnce ( new Error ( "not found" ) ) ;
369+ await service . reconnectSession ( {
370+ ...baseSessionParams ,
371+ adapter : "codex" ,
372+ sessionId : "old-session" ,
373+ } ) ;
374+
375+ await service . prompt ( "run-1" , [ { type : "text" , text : "next request" } ] ) ;
376+ await service . prompt ( "run-1" , [ { type : "text" , text : "later request" } ] ) ;
377+
378+ expect ( mockPrompt . mock . calls [ 0 ] [ 0 ] . prompt ) . toEqual ( [
379+ expect . objectContaining ( {
380+ type : "text" ,
381+ text : expect . stringContaining ( "previous request" ) ,
382+ } ) ,
383+ { type : "text" , text : "next request" } ,
384+ ] ) ;
385+ expect ( mockPrompt . mock . calls [ 1 ] [ 0 ] . prompt ) . toEqual ( [
386+ { type : "text" , text : "later request" } ,
387+ ] ) ;
388+ } ) ;
389+
390+ it ( "preserves conversation context when reconnect has no session ID" , async ( ) => {
391+ mockAgentConstructor . mockImplementationOnce ( function (
392+ this : Record < string , unknown > ,
393+ ) {
394+ this . run = mockAgentRun ;
395+ this . cleanup = vi . fn ( ) . mockResolvedValue ( undefined ) ;
396+ this . getPosthogAPI = vi . fn ( ( ) => ( { } ) ) ;
397+ this . flushAllLogs = vi . fn ( ) . mockResolvedValue ( undefined ) ;
398+ } ) ;
399+ mockResumeFromLog . mockResolvedValue ( { conversation : [ { role : "user" } ] } ) ;
400+ mockFormatConversationForResume . mockReturnValue ( "User: previous request" ) ;
401+
402+ await service . reconnectSession ( {
403+ ...baseSessionParams ,
404+ adapter : "codex" ,
405+ } ) ;
406+ await service . prompt ( "run-1" , [ { type : "text" , text : "next request" } ] ) ;
407+
408+ expect ( mockPrompt . mock . calls [ 0 ] [ 0 ] . prompt [ 0 ] . text ) . toContain (
409+ "previous request" ,
410+ ) ;
411+ } ) ;
412+
413+ it ( "reuses hydrated conversation when Claude resume fails" , async ( ) => {
414+ mockAgentConstructor . mockImplementationOnce ( function (
415+ this : Record < string , unknown > ,
416+ ) {
417+ this . run = mockAgentRun ;
418+ this . cleanup = vi . fn ( ) . mockResolvedValue ( undefined ) ;
419+ this . getPosthogAPI = vi . fn ( ( ) => ( { } ) ) ;
420+ this . flushAllLogs = vi . fn ( ) . mockResolvedValue ( undefined ) ;
421+ } ) ;
422+ mockHydrateSessionJsonl . mockResolvedValueOnce ( {
423+ hasSession : true ,
424+ conversation : [ { role : "user" , content : [ ] } ] ,
425+ } ) ;
426+ mockFormatConversationForResume . mockReturnValue ( "User: hydrated request" ) ;
427+ mockResumeSession . mockRejectedValueOnce ( new Error ( "not found" ) ) ;
428+
429+ await service . reconnectSession ( {
430+ ...baseSessionParams ,
431+ adapter : "claude" ,
432+ sessionId : "old-session" ,
433+ } ) ;
434+ await service . prompt ( "run-1" , [ { type : "text" , text : "next request" } ] ) ;
435+
436+ expect ( mockResumeFromLog ) . not . toHaveBeenCalled ( ) ;
437+ expect ( mockPrompt . mock . calls [ 0 ] [ 0 ] . prompt [ 0 ] . text ) . toContain (
438+ "hydrated request" ,
439+ ) ;
440+ } ) ;
441+
442+ it ( "does not resend hydrated conversation after native resume succeeds" , async ( ) => {
443+ mockAgentConstructor . mockImplementationOnce ( function (
444+ this : Record < string , unknown > ,
445+ ) {
446+ this . run = mockAgentRun ;
447+ this . cleanup = vi . fn ( ) . mockResolvedValue ( undefined ) ;
448+ this . getPosthogAPI = vi . fn ( ( ) => ( { } ) ) ;
449+ this . flushAllLogs = vi . fn ( ) . mockResolvedValue ( undefined ) ;
450+ } ) ;
451+ mockHydrateSessionJsonl . mockResolvedValueOnce ( {
452+ hasSession : true ,
453+ conversation : [ { role : "user" , content : [ ] } ] ,
454+ } ) ;
455+ mockFormatConversationForResume . mockReturnValue ( "User: hydrated request" ) ;
456+
457+ await service . reconnectSession ( {
458+ ...baseSessionParams ,
459+ adapter : "claude" ,
460+ sessionId : "old-session" ,
461+ } ) ;
462+ await service . prompt ( "run-1" , [ { type : "text" , text : "next request" } ] ) ;
463+
464+ expect ( mockPrompt . mock . calls [ 0 ] [ 0 ] . prompt ) . toEqual ( [
465+ { type : "text" , text : "next request" } ,
466+ ] ) ;
467+ } ) ;
468+
469+ it ( "retries recovered context after prompt failure" , async ( ) => {
470+ mockAgentConstructor . mockImplementationOnce ( function (
471+ this : Record < string , unknown > ,
472+ ) {
473+ this . run = mockAgentRun ;
474+ this . cleanup = vi . fn ( ) . mockResolvedValue ( undefined ) ;
475+ this . getPosthogAPI = vi . fn ( ( ) => ( { } ) ) ;
476+ this . flushAllLogs = vi . fn ( ) . mockResolvedValue ( undefined ) ;
477+ } ) ;
478+ mockResumeFromLog . mockResolvedValue ( { conversation : [ { role : "user" } ] } ) ;
479+ mockFormatConversationForResume . mockReturnValue ( "User: previous request" ) ;
480+ mockPrompt . mockRejectedValueOnce ( new Error ( "connection lost" ) ) ;
481+
482+ await service . reconnectSession ( {
483+ ...baseSessionParams ,
484+ adapter : "codex" ,
485+ } ) ;
486+ await expect (
487+ service . prompt ( "run-1" , [ { type : "text" , text : "first attempt" } ] ) ,
488+ ) . rejects . toThrow ( "connection lost" ) ;
489+ await service . prompt ( "run-1" , [ { type : "text" , text : "retry" } ] ) ;
490+
491+ expect ( mockPrompt . mock . calls [ 1 ] [ 0 ] . prompt [ 0 ] . text ) . toContain (
492+ "previous request" ,
493+ ) ;
494+ } ) ;
495+ } ) ;
496+
337497 describe ( "MCP servers" , ( ) => {
338498 it ( "marks desktop sessions as local even though they have a taskRunId" , async ( ) => {
339499 await service . startSession ( {
0 commit comments