@@ -8,10 +8,16 @@ import { redactSensitiveFieldValue, redactToolPayloadText } from "openclaw/plugi
88type CodexContextProjection = {
99 developerInstructionAddition ?: string ;
1010 promptText : string ;
11+ promptContextRange ?: CodexProjectedContextRange ;
1112 assembledMessages : AgentMessage [ ] ;
1213 prePromptMessageCount : number ;
1314} ;
1415
16+ export type CodexProjectedContextRange = {
17+ start : number ;
18+ end : number ;
19+ } ;
20+
1521const CONTEXT_HEADER = "OpenClaw assembled context for this turn:" ;
1622const CONTEXT_OPEN = "<conversation_context>" ;
1723const CONTEXT_CLOSE = "</conversation_context>" ;
@@ -23,6 +29,9 @@ const MAX_RENDERED_CONTEXT_CHARS = 1_000_000;
2329const DEFAULT_TEXT_PART_CHARS = 6_000 ;
2430const MAX_TEXT_PART_CHARS = 128_000 ;
2531const APPROX_RENDERED_CHARS_PER_TOKEN = 4 ;
32+ // Codex app-server validates the summed v2 turn/start text input against
33+ // codex-rs/protocol/src/user_input.rs::MAX_USER_INPUT_TEXT_CHARS.
34+ export const CODEX_TURN_START_TEXT_INPUT_MAX_CHARS = 1 << 20 ;
2635/** Default token reserve kept out of rendered context-engine prompt text. */
2736export const DEFAULT_CODEX_PROJECTION_RESERVE_TOKENS = 20_000 ;
2837const MIN_PROMPT_BUDGET_RATIO = 0.5 ;
@@ -44,25 +53,25 @@ export function projectContextEngineAssemblyForCodex(params: {
4453 maxTextPartChars : resolveTextPartMaxChars ( maxRenderedContextChars ) ,
4554 toolPayloadMode : params . toolPayloadMode ?? "elide" ,
4655 } ) ;
47- const promptText = renderedContext
48- ? [
49- CONTEXT_HEADER ,
50- CONTEXT_SAFETY_NOTE ,
51- "" ,
52- CONTEXT_OPEN ,
53- truncateOlderContext ( renderedContext , maxRenderedContextChars ) ,
54- CONTEXT_CLOSE ,
55- "" ,
56- REQUEST_HEADER ,
57- prompt ,
58- ] . join ( "\n" )
59- : prompt ;
56+ const boundedContext = renderedContext
57+ ? truncateOlderContext ( renderedContext , maxRenderedContextChars )
58+ : undefined ;
59+ const promptPrefix = boundedContext
60+ ? [ CONTEXT_HEADER , CONTEXT_SAFETY_NOTE , "" , CONTEXT_OPEN ] . join ( "\n" ) + "\n"
61+ : undefined ;
62+ const promptSuffix = boundedContext ? `\n${ CONTEXT_CLOSE } \n\n${ REQUEST_HEADER } \n${ prompt } ` : "" ;
63+ const promptText = boundedContext ? `${ promptPrefix } ${ boundedContext } ${ promptSuffix } ` : prompt ;
64+ const promptContextRange =
65+ promptPrefix && boundedContext
66+ ? { start : promptPrefix . length , end : promptPrefix . length + boundedContext . length }
67+ : undefined ;
6068
6169 return {
6270 ...( params . systemPromptAddition ?. trim ( )
6371 ? { developerInstructionAddition : params . systemPromptAddition . trim ( ) }
6472 : { } ) ,
6573 promptText,
74+ ...( promptContextRange ? { promptContextRange } : { } ) ,
6675 assembledMessages : params . assembledMessages ,
6776 prePromptMessageCount : params . originalHistoryMessages . length ,
6877 } ;
@@ -108,6 +117,50 @@ export function resolveCodexContextEngineProjectionReserveTokens(params: {
108117 return undefined ;
109118}
110119
120+ /** Fits projected context prompts under Codex app-server turn/start text limits. */
121+ export function fitCodexProjectedContextForTurnStart ( params : {
122+ promptText : string ;
123+ contextRange ?: CodexProjectedContextRange ;
124+ maxChars ?: number ;
125+ } ) : string {
126+ const maxChars =
127+ typeof params . maxChars === "number" && Number . isFinite ( params . maxChars )
128+ ? Math . max ( 0 , Math . floor ( params . maxChars ) )
129+ : CODEX_TURN_START_TEXT_INPUT_MAX_CHARS ;
130+ if ( params . promptText . length <= maxChars ) {
131+ return params . promptText ;
132+ }
133+ const range = normalizeProjectedContextRange ( params . contextRange , params . promptText . length ) ;
134+ if ( ! range ) {
135+ return params . promptText ;
136+ }
137+
138+ const beforeContext = params . promptText . slice ( 0 , range . start ) ;
139+ const context = params . promptText . slice ( range . start , range . end ) ;
140+ const afterContext = params . promptText . slice ( range . end ) ;
141+ const contextBudget = maxChars - beforeContext . length - afterContext . length ;
142+ const fittedContext = truncateOlderContext ( context , contextBudget ) ;
143+ return `${ beforeContext } ${ fittedContext } ${ afterContext } ` ;
144+ }
145+
146+ function normalizeProjectedContextRange (
147+ range : CodexProjectedContextRange | undefined ,
148+ textLength : number ,
149+ ) : CodexProjectedContextRange | undefined {
150+ if ( ! range ) {
151+ return undefined ;
152+ }
153+ const start = Math . floor ( range . start ) ;
154+ const end = Math . floor ( range . end ) ;
155+ if ( ! Number . isFinite ( start ) || ! Number . isFinite ( end ) || start < 0 || end < start ) {
156+ return undefined ;
157+ }
158+ if ( end > textLength ) {
159+ return undefined ;
160+ }
161+ return { start, end } ;
162+ }
163+
111164function resolveProjectionPromptBudgetTokens ( params : {
112165 contextTokenBudget : number ;
113166 reserveTokens ?: number ;
0 commit comments