fix(schedule): close absolute-time review gaps

This commit is contained in:
Tianyi Cui
2026-08-09 20:29:37 +08:00
parent b7ec8429a9
commit 139b4f421e
23 changed files with 220 additions and 29 deletions

View File

@@ -14,6 +14,7 @@ import {
deriveBrowserTimeZoneContext,
renderBrowserTimeZoneContext,
} from './request-zone.ts'
import type { BrowserTimeZoneContext } from './request-zone.ts'
import { createTimestampFormatter, formatTimestamp } from './timestamp.ts'
/** Cordis plugin name used by loader diagnostics. */
@@ -113,13 +114,13 @@ function renderText(
previous: number | undefined,
formatter: Intl.DateTimeFormat,
timeZone: string,
messages: readonly UserMessage[],
browserContext: BrowserTimeZoneContext,
): string {
const elapsed = previous === undefined ? 'unavailable' : formatDuration(now - previous)
const baseline = step === 1 ? 'model-visible message' : 'step context'
const browserContext = renderBrowserTimeZoneContext(deriveBrowserTimeZoneContext(messages))
const browserText = renderBrowserTimeZoneContext(browserContext)
return `Time sampled while preparing turn ${turn}, step ${step}: ${formatTimestamp(now, formatter, timeZone)}\n`
+ `${browserContext}\n`
+ `${browserText}\n`
+ `Elapsed since the preceding ${baseline}: ${elapsed}.`
}
@@ -192,7 +193,7 @@ export function apply(ctx: Context, config: Config): void {
previous,
formatterFor(selectedTimeZone),
selectedTimeZone,
messages,
browser,
)
return {
kind: 'enter',

View File

@@ -3,28 +3,47 @@
import { assertNever } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-llm'
const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/
/** Browser-zone facts derived from user-rpc messages in one open turn. */
export type BrowserTimeZoneContext =
| { readonly kind: 'resolved'; readonly timeZone: string }
| { readonly kind: 'mixed'; readonly timeZones: readonly string[] }
| { readonly kind: 'missing' }
/** Read a Host-validated browser zone from one ordinary user-rpc message. */
/** Read and validate a Host-canonicalized browser zone from one ordinary user-rpc message. */
function browserTimeZone(message: UserMessage): string | undefined {
const source = message.source
return source.kind === 'user'
const value = source.kind === 'user'
&& 'rpcId' in source
&& typeof source.rpcId === 'string'
&& 'clientTimeZone' in source
&& typeof source.clientTimeZone === 'string'
? source.clientTimeZone
: undefined
if (value === undefined) return undefined
if (value !== 'UTC' && !IANA_TIME_ZONE.test(value)) {
throw new TypeError(
`browser time zone must be canonical UTC or IANA Area/Location: ${JSON.stringify(value)}`,
)
}
let canonical: string
try {
canonical = new Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions().timeZone
} catch (error: unknown) {
throw new TypeError(`browser time zone is unsupported: ${JSON.stringify(value)}`, { cause: error })
}
if (canonical !== value) {
throw new TypeError(`browser time zone must be canonical: ${JSON.stringify(value)}`)
}
return value
}
/**
* Derive the unique, mixed, or missing browser zone for one open turn.
* @param messages - Entered and proposed user messages belonging to the turn.
* @returns Sorted, duplicate-free browser-zone facts.
* @throws TypeError when a user-rpc source carries an invalid or noncanonical zone.
*/
export function deriveBrowserTimeZoneContext(
messages: readonly UserMessage[],

View File

@@ -141,7 +141,30 @@ describe('time-context invariants', () => {
`2026-07-14T00:00:00+00:00[${timeZone}]`,
policy,
)))
}).toThrow(/browser zone cannot format/)
}).toThrow(/browser time zone is unsupported/)
})
it('rejects one corrupt zone even when another zone would classify the turn as mixed', async () => {
const ctx = await setup()
const session = preparing(1, 1, 'Asia/Shanghai')
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'second browser prompt' }],
source: {
kind: 'user',
rpcId: 'turn-1-invalid',
clientTimeZone: 'Not/A_Real_Zone',
} as never,
}), { surfaceOp: 'append' })
expect(() => {
ctx.emit('session/event', session, event(reading(
'1',
'1',
'model-visible message',
'2026-07-14T00:00:00+00:00[UTC]',
'Browser time zone for this request: mixed ["Asia/Shanghai","Not/A_Real_Zone"]. '
+ 'Ask the user to clarify otherwise-unqualified dates and times.',
)))
}).toThrow(/browser time zone is unsupported/)
})
it('validates each existing reading against its preceding durable prefix', async () => {

View File

@@ -33,6 +33,19 @@ describe('browser request-zone context', () => {
})
})
it('validates every browser zone before classifying a mixed turn', () => {
expect(() => deriveBrowserTimeZoneContext([
browserMessage('+08:00'),
])).toThrow(/canonical UTC or IANA Area\/Location/)
expect(() => deriveBrowserTimeZoneContext([
browserMessage('Asia/Shanghai'),
browserMessage('Not/A_Real_Zone'),
])).toThrow(/browser time zone is unsupported/)
expect(() => deriveBrowserTimeZoneContext([
browserMessage('Etc/UTC'),
])).toThrow(/browser time zone must be canonical/)
})
it('renders one explicit model policy for every context', () => {
expect(renderBrowserTimeZoneContext({ kind: 'resolved', timeZone: 'Asia/Shanghai' }))
.toContain('Interpret otherwise-unqualified dates and times in this zone.')