@@ -10,6 +10,7 @@ import { getNativeRoteTools } from './tools';
1010import {
1111 AgentToolCallingUnavailableError ,
1212 DEFAULT_AGENT_POLICY ,
13+ type RoteAgentClientContext ,
1314 type RoteAgentClientState ,
1415 type RoteAgentContext ,
1516 type RoteAgentEmitter ,
@@ -28,6 +29,42 @@ function sourceKey(source: SemanticSearchResult): string {
2829 return `${ source . sourceType } :${ source . sourceId } ` ;
2930}
3031
32+ function asRecord ( value : unknown ) : Record < string , unknown > {
33+ return value && typeof value === 'object' && ! Array . isArray ( value )
34+ ? ( value as Record < string , unknown > )
35+ : { } ;
36+ }
37+
38+ function sanitizeString ( value : unknown , maxLength : number ) : string | undefined {
39+ if ( typeof value !== 'string' ) return undefined ;
40+ const trimmed = value . trim ( ) ;
41+ return trimmed ? trimmed . slice ( 0 , maxLength ) : undefined ;
42+ }
43+
44+ function sanitizeUtcOffsetMinutes ( value : unknown ) : number | undefined {
45+ const numeric = Number ( value ) ;
46+ if ( ! Number . isFinite ( numeric ) ) return undefined ;
47+ const minutes = Math . trunc ( numeric ) ;
48+ return minutes >= - 14 * 60 && minutes <= 14 * 60 ? minutes : undefined ;
49+ }
50+
51+ function sanitizeClientContext ( value : unknown ) : RoteAgentClientContext | null {
52+ const raw = asRecord ( value ) ;
53+ if ( ! Object . keys ( raw ) . length ) return null ;
54+
55+ const context : RoteAgentClientContext = {
56+ nowIso : sanitizeString ( raw . nowIso , 64 ) ,
57+ localDate : sanitizeString ( raw . localDate , 32 ) ,
58+ localDateTime : sanitizeString ( raw . localDateTime , 64 ) ,
59+ timeZone : sanitizeString ( raw . timeZone , 80 ) ,
60+ utcOffsetMinutes : sanitizeUtcOffsetMinutes ( raw . utcOffsetMinutes ) ,
61+ locale : sanitizeString ( raw . locale , 32 ) ,
62+ calendar : sanitizeString ( raw . calendar , 32 ) ,
63+ } ;
64+
65+ return Object . values ( context ) . some ( ( item ) => item !== undefined ) ? context : null ;
66+ }
67+
3168class SourceCollector {
3269 private sources : SemanticSearchResult [ ] = [ ] ;
3370 private indexByKey = new Map < string , number > ( ) ;
@@ -68,6 +105,8 @@ function sanitizeAgentState(request: RoteAgentRequest): RoteAgentClientState {
68105 previousPlan : state . previousPlan || request . previousPlan || null ,
69106 seenSourceIds,
70107 selectedContext : state . selectedContext || request . selectedContext || null ,
108+ clientContext :
109+ sanitizeClientContext ( state . clientContext ) || sanitizeClientContext ( request . clientContext ) ,
71110 stateVersion : Number . isFinite ( state . stateVersion ) ? state . stateVersion : 1 ,
72111 } ;
73112}
@@ -134,13 +173,51 @@ async function logChatUsage(userId: string, model: string, usage: any): Promise<
134173 } ) ;
135174}
136175
137- function buildInitialMessages ( request : RoteAgentRequest ) : ChatMessage [ ] {
176+ function buildRequestTimeContextMessage ( state : RoteAgentClientState ) : ChatMessage {
177+ const clientContext = state . clientContext ;
178+ const lines = [
179+ '## Current request time context' ,
180+ `Server now (UTC): ${ new Date ( ) . toISOString ( ) } ` ,
181+ ] ;
182+
183+ if ( clientContext ) {
184+ if ( clientContext . nowIso ) lines . push ( `Client now (UTC): ${ clientContext . nowIso } ` ) ;
185+ if ( clientContext . localDate ) lines . push ( `Client local date: ${ clientContext . localDate } ` ) ;
186+ if ( clientContext . localDateTime ) {
187+ lines . push ( `Client local date/time: ${ clientContext . localDateTime } ` ) ;
188+ }
189+ if ( clientContext . timeZone ) lines . push ( `Client time zone: ${ clientContext . timeZone } ` ) ;
190+ if ( typeof clientContext . utcOffsetMinutes === 'number' ) {
191+ lines . push ( `Client UTC offset minutes: ${ clientContext . utcOffsetMinutes } ` ) ;
192+ }
193+ if ( clientContext . locale ) lines . push ( `Client locale: ${ clientContext . locale } ` ) ;
194+ if ( clientContext . calendar ) lines . push ( `Client calendar: ${ clientContext . calendar } ` ) ;
195+ } else {
196+ lines . push (
197+ 'Client time context was not supplied; use server now and Asia/Shanghai for Rote date ranges.'
198+ ) ;
199+ }
200+
201+ lines . push (
202+ 'Resolve relative date phrases such as today, yesterday, this month, last month, 最近, 本月, and 上月 using this context.' ,
203+ 'When calling rote_search_notes for a relative date phrase, pass the phrase as timeExpression instead of inventing absolute from/to dates.' ,
204+ 'Use from/to only when the user explicitly provides absolute dates.'
205+ ) ;
206+
207+ return { role : 'system' , content : lines . join ( '\n' ) } ;
208+ }
209+
210+ function buildInitialMessages (
211+ request : RoteAgentRequest ,
212+ state : RoteAgentClientState
213+ ) : ChatMessage [ ] {
138214 const mode = request . mode || 'chat' ;
139215 const messages : ChatMessage [ ] = [
140216 {
141217 role : 'system' ,
142218 content : buildRoteAgentSystemPrompt ( mode ) ,
143219 } ,
220+ buildRequestTimeContextMessage ( state ) ,
144221 ] ;
145222
146223 if ( request . history ?. length ) {
@@ -210,7 +287,7 @@ export async function runRoteAgentStream(params: {
210287 getSources : ( ) => sourceCollector . list ( ) ,
211288 } ;
212289
213- const messages = buildInitialMessages ( request ) ;
290+ const messages = buildInitialMessages ( request , state ) ;
214291 let toolCallCount = 0 ;
215292 let hasFinalAnswer = false ;
216293
0 commit comments