@@ -20,6 +20,7 @@ import {
2020 ClientSideConnection ,
2121 ndJsonStream ,
2222 PROTOCOL_VERSION ,
23+ RequestError ,
2324 type Client ,
2425 type ContentBlock ,
2526 type RequestPermissionRequest ,
@@ -217,6 +218,39 @@ function createAcpPromptUsageEvent(threadId: string, usage: unknown): RuntimeEve
217218 ) ;
218219}
219220
221+ /**
222+ * Replace the raw JSON-RPC error from `session/load` with a message the
223+ * renderer can show verbatim. Provider-agnostic on purpose: the same code
224+ * path triggers whenever any ACP agent rejects a `session/load` call (lost,
225+ * rotated, or never-persisted sessionId).
226+ */
227+ export function rewriteLoadSessionError ( error : unknown , _sessionId : string ) : Error {
228+ const detail = extractLoadSessionDetail ( error ) ;
229+ const message = detail . notFound
230+ ? "This conversation can't be resumed — the agent no longer recognizes this session. Start a new thread to continue."
231+ : `This conversation can't be resumed: ${ detail . message ?? ( error instanceof Error ? error . message : String ( error ) ) } . Start a new thread to continue.` ;
232+ return Object . assign ( new Error ( message ) , { cause : error } ) ;
233+ }
234+
235+ function extractLoadSessionDetail ( error : unknown ) : { message ?: string ; notFound : boolean } {
236+ let message : string | undefined ;
237+ let notFound = false ;
238+ if ( error instanceof RequestError ) {
239+ message = error . message ;
240+ const data = error . data as { message ?: unknown } | undefined ;
241+ if ( data && typeof data . message === "string" ) {
242+ message = data . message ;
243+ if ( / n o t \s + f o u n d / i. test ( data . message ) ) notFound = true ;
244+ }
245+ } else if ( error instanceof Error ) {
246+ message = error . message ;
247+ if ( / s e s s i o n .* n o t \s + f o u n d / i. test ( error . message ) ) notFound = true ;
248+ }
249+ return notFound
250+ ? { ...( message ? { message } : { } ) , notFound : true }
251+ : { ...( message ? { message } : { } ) , notFound : false } ;
252+ }
253+
220254const INTERRUPT_ACK_TEXT_TAIL_LIMIT = 512 ;
221255const USER_INTERRUPT_ACK_RE = / \b o p e r a t i o n c a n c e l l e d b y u s e r \b / i;
222256
@@ -582,9 +616,21 @@ function applyAcpModeUpdateToConfig(currentConfig: ThreadConfig, modeId: string)
582616
583617// ── Session ──────────────────────────────────────────────────────
584618
619+ export interface AcpStructuredSessionOptions {
620+ /**
621+ * Hook the adapter passes in when it wants to control the message a failed
622+ * `session/load` produces. Receives the raw transport error and the
623+ * sessionId that was being loaded; must return the Error to throw.
624+ */
625+ loadSessionErrorRewriter ?: ( error : unknown , sessionId : string ) => Error ;
626+ }
627+
585628export class AcpStructuredSession implements StructuredSessionHandle {
586629 launchOptions : AgentLaunchOptions ;
587630
631+ private loadSessionErrorRewriter : ( error : unknown , sessionId : string ) => Error =
632+ rewriteLoadSessionError ;
633+
588634 private readonly child : ChildProcess ;
589635 private readonly connection : ClientSideConnection ;
590636 private readonly cwd : string ;
@@ -644,13 +690,17 @@ export class AcpStructuredSession implements StructuredSessionHandle {
644690 projectLocation : ProjectLocation ,
645691 cwd : string ,
646692 threadId : string ,
693+ options ?: AcpStructuredSessionOptions ,
647694 ) {
648695 this . child = child ;
649696 this . connection = connection ;
650697 this . projectLocation = projectLocation ;
651698 this . cwd = cwd ;
652699 this . threadId = threadId ;
653700 this . launchOptions = { suppressResumeConfigOverrides : true } ;
701+ if ( options ?. loadSessionErrorRewriter ) {
702+ this . loadSessionErrorRewriter = options . loadSessionErrorRewriter ;
703+ }
654704 }
655705
656706 /** Initialize the canonical mapper once we have a stable thread id. */
@@ -831,6 +881,7 @@ export class AcpStructuredSession implements StructuredSessionHandle {
831881 command : CommandSpec ,
832882 projectLocation : ProjectLocation ,
833883 threadId : string ,
884+ options ?: AcpStructuredSessionOptions ,
834885 ) : AcpStructuredSession {
835886 const sessionCwd = resolveSessionCwd ( projectLocation ) ;
836887 const spawnCwd = command . cwd ?? resolveSpawnCwd ( projectLocation ) ;
@@ -897,7 +948,14 @@ export class AcpStructuredSession implements StructuredSessionHandle {
897948 stream ,
898949 ) ;
899950
900- session = new AcpStructuredSession ( child , connection , projectLocation , sessionCwd , threadId ) ;
951+ session = new AcpStructuredSession (
952+ child ,
953+ connection ,
954+ projectLocation ,
955+ sessionCwd ,
956+ threadId ,
957+ options ,
958+ ) ;
901959 session . spawnReady = spawnReady ;
902960 session . stderrChunks . push ( ...stderrChunks ) ;
903961
@@ -1003,6 +1061,8 @@ export class AcpStructuredSession implements StructuredSessionHandle {
10031061 this . adoptSessionRef ( sessionRef ) ;
10041062 availableModeIds = result . modes ?. availableModes ?. map ( ( m ) => m . id ) ?? [ ] ;
10051063 configOptions = result . configOptions ?? [ ] ;
1064+ } catch ( error ) {
1065+ throw this . loadSessionErrorRewriter ( error , sessionRef . providerSessionId ) ;
10061066 } finally {
10071067 this . isReplayingHistory = false ;
10081068 this . replayHistoryUntil = Date . now ( ) + 500 ;
@@ -1483,5 +1543,9 @@ export function createAcpStructuredSession(
14831543 if ( ! shouldSpawnAcpSession ( input ) ) {
14841544 return undefined ;
14851545 }
1486- return AcpStructuredSession . create ( acpCommand , input . projectLocation , input . threadId ) ;
1546+ return AcpStructuredSession . create ( acpCommand , input . projectLocation , input . threadId , {
1547+ ...( input . loadSessionErrorRewriter
1548+ ? { loadSessionErrorRewriter : input . loadSessionErrorRewriter }
1549+ : { } ) ,
1550+ } ) ;
14871551}
0 commit comments