@@ -110,13 +110,54 @@ const decodeUnknownJsonStringExit = Schema.decodeUnknownExit(Schema.UnknownFromJ
110110const PROVIDER = ProviderDriverKind . make ( "claudeAgent" ) ;
111111const COMPACT_CONTEXT_COMMAND = "/compact" ;
112112const MAX_CLAUDE_FALLBACK_MODELS = 3 ;
113+ const CLAUDE_SKIP_PROMPT_HISTORY_ENV = "CLAUDE_CODE_SKIP_PROMPT_HISTORY" ;
114+ const CLAUDE_FORCE_SESSION_PERSISTENCE_ENV = "CLAUDE_CODE_FORCE_SESSION_PERSISTENCE" ;
115+ const CLAUDE_SESSION_PERSISTENCE_WARNING_KIND = "session-persistence" ;
113116type ClaudeTextStreamKind = Extract < RuntimeContentStreamKind , "assistant_text" | "reasoning_text" > ;
114117type ClaudeToolResultStreamKind = Extract <
115118 RuntimeContentStreamKind ,
116119 "command_output" | "file_change_output"
117120> ;
118121type ClaudeSdkEffort = NonNullable < ClaudeQueryOptions [ "effort" ] > ;
119122
123+ function isEnabledEnvironmentFlag ( value : string | undefined ) : boolean {
124+ switch ( value ?. trim ( ) . toLowerCase ( ) ) {
125+ case "1" :
126+ case "true" :
127+ case "yes" :
128+ case "on" :
129+ return true ;
130+ default :
131+ return false ;
132+ }
133+ }
134+
135+ function claudeSessionPersistenceEnvironmentWarning (
136+ environment : NodeJS . ProcessEnv ,
137+ ) : string | undefined {
138+ if (
139+ ! isEnabledEnvironmentFlag ( environment [ CLAUDE_SKIP_PROMPT_HISTORY_ENV ] ) ||
140+ isEnabledEnvironmentFlag ( environment [ CLAUDE_FORCE_SESSION_PERSISTENCE_ENV ] )
141+ ) {
142+ return undefined ;
143+ }
144+ return (
145+ `Claude session persistence is disabled by ${ CLAUDE_SKIP_PROMPT_HISTORY_ENV } . ` +
146+ "Threadlines will retain its activity history, but Claude cannot resume the saved model context. " +
147+ `Unset ${ CLAUDE_SKIP_PROMPT_HISTORY_ENV } or set ${ CLAUDE_FORCE_SESSION_PERSISTENCE_ENV } =1, then restart Threadlines.`
148+ ) ;
149+ }
150+
151+ function isClaudeSessionPersistenceWarning ( message : string ) : boolean {
152+ return / t r a n s c r i p t | s e s s i o n p e r s i s t e n c e | s a v e d f o r r e s u m e | - - r e s u m e w i l l n o t f i n d / i. test ( message ) ;
153+ }
154+
155+ function isClaudeDisabledSessionPersistenceWarning ( message : string ) : boolean {
156+ return / s e s s i o n p e r s i s t e n c e .* (?: d i s a b l e d | o f f ) | - - r e s u m e w i l l n o t f i n d | k e e p f u t u r e t r a n s c r i p t s / i. test (
157+ message ,
158+ ) ;
159+ }
160+
120161function encodeJsonStringForDiagnostics ( input : unknown ) : string | undefined {
121162 const result = encodeUnknownJsonStringExit ( input ) ;
122163 return Exit . isSuccess ( result ) ? result . value : undefined ;
@@ -314,6 +355,7 @@ interface ClaudeSessionContext {
314355 // turn and cleared once consumed. Set only when a session starts from a
315356 // context seed (no native resume).
316357 pendingContextSeedText : string | undefined ;
358+ sessionPersistenceDisabledWarningEmitted : boolean ;
317359 stopped : boolean ;
318360}
319361
@@ -4446,6 +4488,38 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
44464488 yield * emitRuntimeError ( context , `Claude workspace mirror error: ${ detail } ` , message ) ;
44474489 return ;
44484490 }
4491+ case "informational" : {
4492+ const content = nonEmptyString ( message . content ) ;
4493+ if ( ! content || message . level !== "warning" ) {
4494+ yield * Effect . logDebug ( "claude.sdk.informational" , {
4495+ threadId : context . session . threadId ,
4496+ level : message . level ,
4497+ content : message . content ,
4498+ } ) ;
4499+ return ;
4500+ }
4501+ const isPersistenceWarning = isClaudeSessionPersistenceWarning ( content ) ;
4502+ if (
4503+ isPersistenceWarning &&
4504+ isClaudeDisabledSessionPersistenceWarning ( content ) &&
4505+ context . sessionPersistenceDisabledWarningEmitted
4506+ ) {
4507+ yield * Effect . logInfo ( "claude.session-persistence.warning-duplicate" , {
4508+ threadId : context . session . threadId ,
4509+ content,
4510+ } ) ;
4511+ return ;
4512+ }
4513+ if ( isClaudeDisabledSessionPersistenceWarning ( content ) ) {
4514+ context . sessionPersistenceDisabledWarningEmitted = true ;
4515+ }
4516+ yield * emitRuntimeWarning ( context , content , message , {
4517+ warningKind : isPersistenceWarning
4518+ ? CLAUDE_SESSION_PERSISTENCE_WARNING_KIND
4519+ : "claude-informational" ,
4520+ } ) ;
4521+ return ;
4522+ }
44494523 case "api_retry" : {
44504524 // First-attempt retries are routine stream hiccups the SDK absorbs on
44514525 // its own; keep those in server diagnostics only. Cascades that reach
@@ -5373,6 +5447,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
53735447 includePartialMessages : true ,
53745448 enableFileCheckpointing : true ,
53755449 promptSuggestions : true ,
5450+ persistSession : true ,
53765451 // Subagent conversations arrive as messages tagged with
53775452 // parent_tool_use_id; routing keeps them out of the main transcript
53785453 // and streams them into the collab tool item instead. Travels via the
@@ -5494,11 +5569,21 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
54945569 input . contextSeed !== undefined && resumeState === undefined
54955570 ? renderThreadContextSeed ( input . contextSeed )
54965571 : undefined ,
5572+ sessionPersistenceDisabledWarningEmitted : false ,
54975573 stopped : false ,
54985574 } ;
54995575 yield * Ref . set ( contextRef , context ) ;
55005576 sessions . set ( threadId , context ) ;
55015577
5578+ const sessionPersistenceWarning =
5579+ claudeSessionPersistenceEnvironmentWarning ( claudeEnvironment ) ;
5580+ if ( sessionPersistenceWarning !== undefined ) {
5581+ context . sessionPersistenceDisabledWarningEmitted = true ;
5582+ yield * emitRuntimeWarning ( context , sessionPersistenceWarning , undefined , {
5583+ warningKind : CLAUDE_SESSION_PERSISTENCE_WARNING_KIND ,
5584+ } ) ;
5585+ }
5586+
55025587 const sessionStartedStamp = yield * makeEventStamp ( ) ;
55035588 yield * offerRuntimeEvent ( {
55045589 type : "session.started" ,
0 commit comments