44 */
55import { uniqueStrings } from "@openclaw/normalization-core/string-normalization" ;
66import { getRuntimeConfig } from "../../config/config.js" ;
7+ import type { CliBackendConfig } from "../../config/types.agent-defaults.js" ;
78import {
89 assertContextEngineHostSupport ,
910 buildGenericCliContextEngineHostSupport ,
@@ -110,6 +111,79 @@ const prepareDeps = {
110111 resolveApiKeyForProfile,
111112} ;
112113
114+ function resolveReusableCliSessionId ( reusableCliSession : CliReusableSession ) : string | undefined {
115+ return reusableCliSession . mode === "reuse" || reusableCliSession . mode === "reuse-with-drift"
116+ ? reusableCliSession . sessionId
117+ : undefined ;
118+ }
119+
120+ function resolveCliSessionInvalidatedReason (
121+ reusableCliSession : CliReusableSession ,
122+ ) : Extract < CliReusableSession , { mode : "invalidate" } > [ "invalidatedReason" ] | undefined {
123+ return reusableCliSession . mode === "invalidate"
124+ ? reusableCliSession . invalidatedReason
125+ : undefined ;
126+ }
127+
128+ function canApplySystemPromptOnResume ( backend : CliBackendConfig ) : boolean {
129+ return (
130+ backend . systemPromptWhen !== "never" &&
131+ Boolean (
132+ backend . systemPromptArg || backend . systemPromptFileArg || backend . systemPromptFileConfigKey ,
133+ )
134+ ) ;
135+ }
136+
137+ function buildCliSessionDriftUserContext ( params : {
138+ reusableCliSession : CliReusableSession ;
139+ extraSystemPrompt : string ;
140+ promptToolNames : string [ ] ;
141+ } ) : string | undefined {
142+ const { reusableCliSession } = params ;
143+ if ( reusableCliSession . mode !== "reuse-with-drift" ) {
144+ return undefined ;
145+ }
146+ const lines = [
147+ `OpenClaw resumed this CLI session after prompt content changed. Follow the current turn's instructions; changed=${ reusableCliSession . drift . reasons . join ( "," ) } .` ,
148+ ] ;
149+ if ( reusableCliSession . drift . reasons . includes ( "system-prompt" ) && params . extraSystemPrompt ) {
150+ lines . push ( `Current session context:\n${ params . extraSystemPrompt } ` ) ;
151+ }
152+ if ( reusableCliSession . drift . reasons . includes ( "prompt-tools" ) ) {
153+ lines . push (
154+ `Current prompt tool surface: ${
155+ params . promptToolNames . length > 0 ? params . promptToolNames . join ( ", " ) : "none"
156+ } `,
157+ ) ;
158+ }
159+ return lines . join ( "\n\n" ) ;
160+ }
161+
162+ function prependCliSessionDriftUserContext (
163+ context : RunCliAgentParams [ "currentInboundContext" ] ,
164+ reusableCliSession : CliReusableSession ,
165+ driftContext : {
166+ extraSystemPrompt : string ;
167+ promptToolNames : string [ ] ;
168+ } ,
169+ ) : RunCliAgentParams [ "currentInboundContext" ] {
170+ const note = buildCliSessionDriftUserContext ( {
171+ reusableCliSession,
172+ ...driftContext ,
173+ } ) ;
174+ if ( ! note ) {
175+ return context ;
176+ }
177+ if ( ! context ) {
178+ return { text : note } ;
179+ }
180+ return {
181+ ...context ,
182+ text : [ note , context . text ] . join ( "\n\n" ) ,
183+ ...( context . resumableText ? { resumableText : [ note , context . resumableText ] . join ( "\n\n" ) } : { } ) ,
184+ } ;
185+ }
186+
113187async function resolveCliSkillsPrompt ( params : {
114188 agentId : string ;
115189 config : RunCliAgentParams [ "config" ] ;
@@ -618,7 +692,7 @@ export async function prepareCliRunContext(
618692 ? hashCliSessionText ( JSON . stringify ( promptTools . map ( ( tool ) => tool . name ) . toSorted ( ) ) )
619693 : undefined ;
620694 const reusableCliSessionCandidate : CliReusableSession = isSideQuestion
621- ? { }
695+ ? { mode : "none" }
622696 : params . cliSessionBinding
623697 ? resolveCliSessionReuse ( {
624698 binding : params . cliSessionBinding ,
@@ -633,9 +707,15 @@ export async function prepareCliRunContext(
633707 mcpResumeHash : preparedBackendFinal . mcpResumeHash ,
634708 } )
635709 : params . cliSessionId
636- ? { sessionId : params . cliSessionId }
637- : { } ;
638- const candidateClaudeCliSessionId = reusableCliSessionCandidate . sessionId ?. trim ( ) || undefined ;
710+ ? { mode : "reuse" , sessionId : params . cliSessionId }
711+ : { mode : "none" } ;
712+ const backendReusableCliSession : CliReusableSession =
713+ reusableCliSessionCandidate . mode === "reuse-with-drift" &&
714+ ! canApplySystemPromptOnResume ( preparedBackendFinal . backend )
715+ ? { mode : "invalidate" , invalidatedReason : "system-prompt" }
716+ : reusableCliSessionCandidate ;
717+ const candidateClaudeCliSessionId =
718+ resolveReusableCliSessionId ( backendReusableCliSession ) ?. trim ( ) || undefined ;
639719 const hasClaudeCliCandidate =
640720 candidateClaudeCliSessionId !== undefined && isClaudeCliProvider ( params . provider ) ;
641721 const claudeCliTranscriptMissing =
@@ -651,18 +731,20 @@ export async function prepareCliRunContext(
651731 sessionId : candidateClaudeCliSessionId ,
652732 workspaceDir : cwd ,
653733 } ) ) ;
654- const claudeCliInvalidatedReason : CliReusableSession [ "invalidatedReason" ] | undefined =
734+ const claudeCliInvalidatedReason : "missing-transcript" | "orphaned-tool-use" | undefined =
655735 claudeCliTranscriptMissing
656736 ? "missing-transcript"
657737 : claudeCliTranscriptOrphanedToolUse
658738 ? "orphaned-tool-use"
659739 : undefined ;
660740 const reusableCliSession : CliReusableSession = claudeCliInvalidatedReason
661- ? { invalidatedReason : claudeCliInvalidatedReason }
662- : reusableCliSessionCandidate ;
663- if ( reusableCliSession . invalidatedReason ) {
741+ ? { mode : "invalidate" , invalidatedReason : claudeCliInvalidatedReason }
742+ : backendReusableCliSession ;
743+ const reusableCliSessionId = resolveReusableCliSessionId ( reusableCliSession ) ;
744+ const invalidatedReason = resolveCliSessionInvalidatedReason ( reusableCliSession ) ;
745+ if ( invalidatedReason ) {
664746 cliBackendLog . info (
665- `cli session reset: provider=${ params . provider } reason=${ reusableCliSession . invalidatedReason } ` ,
747+ `cli session reset: provider=${ params . provider } reason=${ invalidatedReason } ` ,
666748 ) ;
667749 }
668750 let openClawHistoryMessages : unknown [ ] | undefined ;
@@ -804,15 +886,23 @@ export async function prepareCliRunContext(
804886 }
805887 let historyPromptCurrentTurn = preparedPrompt ;
806888 if ( ! isSideQuestion ) {
889+ const currentInboundContext = prependCliSessionDriftUserContext (
890+ params . currentInboundContext ,
891+ reusableCliSession ,
892+ {
893+ extraSystemPrompt,
894+ promptToolNames : promptTools . map ( ( tool ) => tool . name ) . toSorted ( ) ,
895+ } ,
896+ ) ;
807897 const fullCurrentInboundPrompt = buildCurrentInboundPrompt ( {
808- context : params . currentInboundContext ,
898+ context : currentInboundContext ,
809899 prompt : preparedPrompt ,
810900 } ) ;
811901 const runCurrentInboundPrompt = buildCurrentInboundPrompt ( {
812- context : params . currentInboundContext ,
902+ context : currentInboundContext ,
813903 prompt : preparedPrompt ,
814904 preferResumableText :
815- params . currentInboundEventKind === "room_event" && Boolean ( reusableCliSession . sessionId ) ,
905+ params . currentInboundEventKind === "room_event" && Boolean ( reusableCliSessionId ) ,
816906 } ) ;
817907 historyPromptCurrentTurn = annotateInterSessionPromptText (
818908 fullCurrentInboundPrompt ,
@@ -825,11 +915,9 @@ export async function prepareCliRunContext(
825915 }
826916 const allowRawTranscriptReseed =
827917 backendResolved . config . reseedFromRawTranscriptWhenUncompacted === true ;
828- const rawTranscriptReseedReason = reusableCliSession . sessionId
829- ? "session-expired"
830- : reusableCliSession . invalidatedReason ;
918+ const rawTranscriptReseedReason = reusableCliSessionId ? "session-expired" : invalidatedReason ;
831919 const shouldPrepareOpenClawHistoryPrompt =
832- ! isSideQuestion && ( ! reusableCliSession . sessionId || allowRawTranscriptReseed ) ;
920+ ! isSideQuestion && ( ! reusableCliSessionId || allowRawTranscriptReseed ) ;
833921 const openClawHistoryPrompt = shouldPrepareOpenClawHistoryPrompt
834922 ? buildCliSessionHistoryPrompt ( {
835923 messages : await loadCliSessionReseedMessages ( {
0 commit comments