fix(schedule): close absolute-time review gaps
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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.')
|
||||
|
||||
Reference in New Issue
Block a user