@@ -23,6 +23,13 @@ import { logger } from "../lib/logger";
2323
2424const log = logger . scope ( "session-manager" ) ;
2525
26+ function isAuthError ( error : unknown ) : boolean {
27+ return (
28+ error instanceof Error &&
29+ error . message . startsWith ( "Authentication required" )
30+ ) ;
31+ }
32+
2633type MessageCallback = ( message : unknown ) => void ;
2734
2835class NdJsonTap {
@@ -135,6 +142,7 @@ export interface ManagedSession {
135142 createdAt : number ;
136143 lastActivityAt : number ;
137144 mockNodeDir : string ;
145+ config : SessionConfig ;
138146}
139147
140148function getClaudeCliPath ( ) : string {
@@ -146,7 +154,7 @@ function getClaudeCliPath(): string {
146154
147155export class SessionManager {
148156 private sessions = new Map < string , ManagedSession > ( ) ;
149- private sessionTokens = new Map < string , string > ( ) ;
157+ private currentToken : string | null = null ;
150158 private getMainWindow : ( ) => BrowserWindow | null ;
151159 private onLog : OnLogCallback ;
152160
@@ -155,23 +163,20 @@ export class SessionManager {
155163 this . onLog = onLog ;
156164 }
157165
158- public updateSessionToken ( taskRunId : string , newToken : string ) : void {
159- this . sessionTokens . set ( taskRunId , newToken ) ;
160- log . info ( "Session token updated" , { taskRunId } ) ;
166+ public updateToken ( newToken : string ) : void {
167+ this . currentToken = newToken ;
168+ log . info ( "Session token updated" ) ;
161169 }
162170
163- private getSessionToken ( taskRunId : string , fallback : string ) : string {
164- return this . sessionTokens . get ( taskRunId ) || fallback ;
171+ private getToken ( fallback : string ) : string {
172+ return this . currentToken || fallback ;
165173 }
166174
167- private buildMcpServers (
168- credentials : PostHogCredentials ,
169- taskRunId : string ,
170- ) : AcpMcpServer [ ] {
175+ private buildMcpServers ( credentials : PostHogCredentials ) : AcpMcpServer [ ] {
171176 const servers : AcpMcpServer [ ] = [ ] ;
172177
173178 const mcpUrl = this . getPostHogMcpUrl ( credentials . apiHost ) ;
174- const token = this . getSessionToken ( taskRunId , credentials . apiKey ) ;
179+ const token = this . getToken ( credentials . apiKey ) ;
175180
176181 servers . push ( {
177182 name : "posthog" ,
@@ -211,6 +216,7 @@ export class SessionManager {
211216 private async getOrCreateSession (
212217 config : SessionConfig ,
213218 isReconnect : boolean ,
219+ isRetry = false ,
214220 ) : Promise < ManagedSession | null > {
215221 const {
216222 taskId,
@@ -222,9 +228,11 @@ export class SessionManager {
222228 model,
223229 } = config ;
224230
225- const existing = this . sessions . get ( taskRunId ) ;
226- if ( existing ) {
227- return existing ;
231+ if ( ! isRetry ) {
232+ const existing = this . sessions . get ( taskRunId ) ;
233+ if ( existing ) {
234+ return existing ;
235+ }
228236 }
229237
230238 const channel = `agent-event:${ taskRunId } ` ;
@@ -234,7 +242,7 @@ export class SessionManager {
234242 const agent = new Agent ( {
235243 workingDirectory : repoPath ,
236244 posthogApiUrl : credentials . apiHost ,
237- posthogApiKey : credentials . apiKey ,
245+ getPosthogApiKey : ( ) => this . getToken ( credentials . apiKey ) ,
238246 posthogProjectId : credentials . projectId ,
239247 debug : ! app . isPackaged ,
240248 onLog : this . onLog ,
@@ -256,11 +264,9 @@ export class SessionManager {
256264 clientCapabilities : { } ,
257265 } ) ;
258266
259- const mcpServers = this . buildMcpServers ( credentials , taskRunId ) ;
267+ const mcpServers = this . buildMcpServers ( credentials ) ;
260268
261269 if ( isReconnect ) {
262- // Use our custom extension method to resume without replaying history.
263- // Client fetches history from S3 directly.
264270 await connection . extMethod ( "_posthog/session/resume" , {
265271 sessionId : taskRunId ,
266272 cwd : repoPath ,
@@ -290,38 +296,82 @@ export class SessionManager {
290296 createdAt : Date . now ( ) ,
291297 lastActivityAt : Date . now ( ) ,
292298 mockNodeDir,
299+ config,
293300 } ;
294301
295302 this . sessions . set ( taskRunId , session ) ;
303+ if ( isRetry ) {
304+ log . info ( "Session created after auth retry" , { taskRunId } ) ;
305+ }
296306 return session ;
297307 } catch ( err ) {
298308 this . cleanupMockNodeEnvironment ( mockNodeDir ) ;
309+ if ( ! isRetry && isAuthError ( err ) ) {
310+ log . warn (
311+ `Auth error during ${ isReconnect ? "reconnect" : "create" } , retrying` ,
312+ { taskRunId } ,
313+ ) ;
314+ return this . getOrCreateSession ( config , isReconnect , true ) ;
315+ }
299316 log . error (
300- `Failed to ${ isReconnect ? "reconnect" : "create" } session` ,
317+ `Failed to ${ isReconnect ? "reconnect" : "create" } session${ isRetry ? " after retry" : "" } ` ,
301318 err ,
302319 ) ;
303320 if ( isReconnect ) return null ;
304321 throw err ;
305322 }
306323 }
307324
325+ private async recreateSession ( taskRunId : string ) : Promise < ManagedSession > {
326+ const existing = this . sessions . get ( taskRunId ) ;
327+ if ( ! existing ) {
328+ throw new Error ( `Session not found for recreation: ${ taskRunId } ` ) ;
329+ }
330+
331+ log . info ( "Recreating session due to auth error" , { taskRunId } ) ;
332+
333+ // Store config and cleanup old session
334+ const config = existing . config ;
335+ this . cleanupSession ( taskRunId ) ;
336+
337+ // Reconnect to preserve Claude context via sdkSessionId
338+ const newSession = await this . getOrCreateSession ( config , true ) ;
339+ if ( ! newSession ) {
340+ throw new Error ( `Failed to recreate session: ${ taskRunId } ` ) ;
341+ }
342+
343+ return newSession ;
344+ }
345+
308346 async prompt (
309347 taskRunId : string ,
310348 prompt : ContentBlock [ ] ,
311349 ) : Promise < { stopReason : string } > {
312- const session = this . sessions . get ( taskRunId ) ;
350+ let session = this . sessions . get ( taskRunId ) ;
313351 if ( ! session ) {
314352 throw new Error ( `Session not found: ${ taskRunId } ` ) ;
315353 }
316354
317355 session . lastActivityAt = Date . now ( ) ;
318356
319- const result = await session . connection . prompt ( {
320- sessionId : taskRunId , // Use taskRunId as ACP sessionId
321- prompt,
322- } ) ;
323-
324- return { stopReason : result . stopReason } ;
357+ try {
358+ const result = await session . connection . prompt ( {
359+ sessionId : taskRunId ,
360+ prompt,
361+ } ) ;
362+ return { stopReason : result . stopReason } ;
363+ } catch ( err ) {
364+ if ( isAuthError ( err ) ) {
365+ log . warn ( "Auth error during prompt, recreating session" , { taskRunId } ) ;
366+ session = await this . recreateSession ( taskRunId ) ;
367+ const result = await session . connection . prompt ( {
368+ sessionId : taskRunId ,
369+ prompt,
370+ } ) ;
371+ return { stopReason : result . stopReason } ;
372+ }
373+ throw err ;
374+ }
325375 }
326376
327377 async cancelSession ( taskRunId : string ) : Promise < boolean > {
@@ -398,11 +448,12 @@ export class SessionManager {
398448 credentials : PostHogCredentials ,
399449 mockNodeDir : string ,
400450 ) : void {
451+ const token = this . getToken ( credentials . apiKey ) ;
401452 const newPath = `${ mockNodeDir } :${ process . env . PATH || "" } ` ;
402453 process . env . PATH = newPath ;
403- process . env . POSTHOG_AUTH_HEADER = `Bearer ${ credentials . apiKey } ` ;
404- process . env . ANTHROPIC_API_KEY = credentials . apiKey ;
405- process . env . ANTHROPIC_AUTH_TOKEN = credentials . apiKey ;
454+ process . env . POSTHOG_AUTH_HEADER = `Bearer ${ token } ` ;
455+ process . env . ANTHROPIC_API_KEY = token ;
456+ process . env . ANTHROPIC_AUTH_TOKEN = token ;
406457
407458 const llmGatewayUrl =
408459 process . env . LLM_GATEWAY_URL ||
@@ -508,6 +559,24 @@ export class SessionManager {
508559 // No-op: session/update notifications are captured by the stream tap
509560 // and forwarded as acp_message events to avoid duplication
510561 } ,
562+
563+ extNotification : async (
564+ method : string ,
565+ params : Record < string , unknown > ,
566+ ) : Promise < void > => {
567+ if ( method === "_posthog/sdk_session" ) {
568+ const { sessionId, sdkSessionId } = params as {
569+ sessionId : string ;
570+ sdkSessionId : string ;
571+ } ;
572+ // Store sdkSessionId in session config for recreation/reconnection
573+ const session = this . sessions . get ( sessionId ) ;
574+ if ( session ) {
575+ session . config . sdkSessionId = sdkSessionId ;
576+ log . info ( "SDK session ID captured" , { sessionId, sdkSessionId } ) ;
577+ }
578+ }
579+ } ,
511580 } ;
512581
513582 // Create client-side connection with tapped streams (bidirectional)
@@ -665,10 +734,10 @@ export function registerAgentIpc(
665734 "agent-token-refresh" ,
666735 async (
667736 _event : IpcMainInvokeEvent ,
668- taskRunId : string ,
737+ _taskRunId : string ,
669738 newToken : string ,
670739 ) : Promise < void > => {
671- sessionManager . updateSessionToken ( taskRunId , newToken ) ;
740+ sessionManager . updateToken ( newToken ) ;
672741 } ,
673742 ) ;
674743
0 commit comments