feat(schedule): add absolute-time reminders

This commit is contained in:
pku-xht
2026-08-06 15:03:48 +08:00
committed by Tianyi Cui
parent 9e61b7d1b1
commit d61059364e
54 changed files with 3169 additions and 374 deletions

View File

@@ -0,0 +1,135 @@
/** Machine-readable Session and request-zone authority carried by time-context messages. */
/** Session-owned zone authority included in each time-context reading. */
export type SessionTimeZoneAuthority =
| { readonly kind: 'resolved'; readonly timeZone: string }
| { readonly kind: 'unavailable' }
/** Client-zone provenance of the messages entering one proposed step. */
export type ClientTimeZoneAuthority =
| { readonly kind: 'resolved'; readonly timeZone: string }
| { readonly kind: 'mixed'; readonly timeZones: string[] }
| { readonly kind: 'missing' }
/** Machine-readable time authority shared by model context and Schedule tools. */
export interface TimeContextAuthority {
readonly turn: number
readonly step: number
readonly session: SessionTimeZoneAuthority
readonly client: ClientTimeZoneAuthority
}
/** Source shape owned by the time-context plugin. */
export interface TimeContextMessageSource {
kind: 'plugin'
plugin: 'time-context'
authority: TimeContextAuthority
}
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'time-context': TimeContextMessageSource
}
}
/** Whether an unknown value is one ordinary JSON object. */
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/** Require one object to carry exactly the named keys. */
function hasExactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
const keys = Object.keys(value).sort()
const wanted = [...expected].sort()
return keys.length === wanted.length && keys.every((key, index) => key === wanted[index])
}
/** Decode one non-empty zone name without re-owning Host canonicalization. */
function zone(value: unknown): string {
if (typeof value !== 'string' || value.length === 0) {
throw new TypeError('time-context authority time zone must be a non-empty string')
}
return value
}
/** Decode the Session branch of one authority value. */
function sessionAuthority(value: unknown): SessionTimeZoneAuthority {
if (!isRecord(value)) throw new TypeError('time-context Session authority must be an object')
if (value['kind'] === 'unavailable' && hasExactKeys(value, ['kind'])) return { kind: 'unavailable' }
if (value['kind'] === 'resolved' && hasExactKeys(value, ['kind', 'timeZone'])) {
return { kind: 'resolved', timeZone: zone(value['timeZone']) }
}
throw new TypeError('time-context Session authority has an invalid shape')
}
/** Decode the request-client branch of one authority value. */
function clientAuthority(value: unknown): ClientTimeZoneAuthority {
if (!isRecord(value)) throw new TypeError('time-context client authority must be an object')
if (value['kind'] === 'missing' && hasExactKeys(value, ['kind'])) return { kind: 'missing' }
if (value['kind'] === 'resolved' && hasExactKeys(value, ['kind', 'timeZone'])) {
return { kind: 'resolved', timeZone: zone(value['timeZone']) }
}
if (value['kind'] === 'mixed' && hasExactKeys(value, ['kind', 'timeZones'])) {
const values = value['timeZones']
if (!Array.isArray(values)
|| !values.every((item): item is string => typeof item === 'string' && item.length > 0)
|| values.length < 2) {
throw new TypeError('time-context mixed client authority must contain at least two zones')
}
const timeZones = [...new Set(values)].sort()
if (timeZones.length !== values.length || timeZones.some((item, index) => item !== values[index])) {
throw new TypeError('time-context mixed client zones must be unique and sorted')
}
return { kind: 'mixed', timeZones }
}
throw new TypeError('time-context client authority has an invalid shape')
}
/**
* Decode the strict durable source attached to a time-context message.
* @param value - Untrusted message source.
* @returns Detached machine authority and its fixed plugin discriminator.
*/
export function decodeTimeContextSource(value: unknown): TimeContextMessageSource {
if (!isRecord(value) || !hasExactKeys(value, ['kind', 'plugin', 'authority'])
|| value['kind'] !== 'plugin' || value['plugin'] !== 'time-context') {
throw new TypeError('time-context message source has an invalid shape')
}
const authority = value['authority']
if (!isRecord(authority) || !hasExactKeys(authority, ['turn', 'step', 'session', 'client'])) {
throw new TypeError('time-context authority has an invalid shape')
}
const turn = authority['turn']
const step = authority['step']
if (!Number.isSafeInteger(turn) || (turn as number) < 1
|| !Number.isSafeInteger(step) || (step as number) < 1) {
throw new TypeError('time-context authority turn and step must be positive safe integers')
}
return {
kind: 'plugin',
plugin: 'time-context',
authority: {
turn: turn as number,
step: step as number,
session: sessionAuthority(authority['session']),
client: clientAuthority(authority['client']),
},
}
}
/**
* Render the machine authority as concise model-visible policy.
* @param authority - Session and request-zone authority for one proposed step.
* @returns The two policy lines appended to the time-context reading.
*/
export function renderTimeContextAuthority(authority: TimeContextAuthority): string {
const session = authority.session.kind === 'resolved'
? authority.session.timeZone
: 'unavailable'
const client = authority.client.kind === 'resolved'
? authority.client.timeZone
: authority.client.kind === 'mixed'
? `mixed ${JSON.stringify(authority.client.timeZones)}`
: 'missing'
return `Session time zone: ${session}.\nClient time zone for this request: ${client}.`
}

View File

@@ -9,6 +9,20 @@ import type { Context } from 'cordis'
import z from 'schemastery'
import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-llm'
import { renderTimeContextAuthority } from './authority.ts'
import type {
ClientTimeZoneAuthority,
TimeContextAuthority,
} from './authority.ts'
export type {
ClientTimeZoneAuthority,
SessionTimeZoneAuthority,
TimeContextAuthority,
TimeContextMessageSource,
} from './authority.ts'
export { decodeTimeContextSource, renderTimeContextAuthority } from './authority.ts'
/** Cordis plugin name used by loader diagnostics. */
export const name = 'time-context'
@@ -18,7 +32,7 @@ export const inject = ['agents']
/** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */
export interface Config {
/** IANA time zone used for the rendered timestamp. Omit to resolve the Node process's system zone at plugin load. */
/** Fallback display zone for headerless Sessions. Omit to use the process zone. */
timeZone?: string
/** Minimum milliseconds between durable injections in one session. Omit or set to 0 to inject at every eligible step. */
refreshIntervalMs?: number
@@ -30,6 +44,7 @@ export const Config: z<Config> = z.object({
refreshIntervalMs: z.number(),
})
type TimestampPart = 'day' | 'hour' | 'minute' | 'month' | 'second' | 'timeZoneName' | 'year'
/** Format an epoch millisecond value as an ISO-shaped timestamp with offset and IANA zone. */
@@ -99,20 +114,116 @@ function latestInjectionTime(agent: Agent): number | undefined {
return undefined
}
/** Read the Host-validated client zone from one ordinary user-rpc message. */
function clientTimeZone(message: UserMessage): string | undefined {
const source = message.source
return source.kind === 'user'
&& 'clientTimeZone' in source
&& typeof source.clientTimeZone === 'string'
? source.clientTimeZone
: undefined
}
/** Derive all distinct client zones in the current request chain. */
function requestClientTimeZones(agent: Agent, turn: number, messages: readonly UserMessage[]): string[] {
const zones = new Set<string>()
for (const event of [...agent.session.events].reverse()) {
if (event.type === 'turn/start' && event.data.turn === turn) break
if (event.type !== 'user/message') continue
const zone = clientTimeZone(event.data)
if (zone !== undefined) zones.add(zone)
}
for (const message of messages) {
const zone = clientTimeZone(message)
if (zone !== undefined) zones.add(zone)
}
return [...zones].sort()
}
/** Close the request-zone set into the machine authority union. */
function clientAuthority(timeZones: string[]): ClientTimeZoneAuthority {
const [timeZone, ...remaining] = timeZones
if (timeZone === undefined) return { kind: 'missing' }
if (remaining.length === 0) return { kind: 'resolved', timeZone }
return { kind: 'mixed', timeZones }
}
function renderText(
now: number,
turn: number,
step: number,
previous: number | undefined,
formatter: Intl.DateTimeFormat,
timeZone: string,
displayTimeZone: string,
authority: TimeContextAuthority,
): string {
const elapsed = previous === undefined ? 'unavailable' : formatDuration(now - previous)
const baseline = step === 1 ? 'model-visible message' : 'step context'
return `Time sampled while preparing turn ${turn}, step ${step}: ${formatTimestamp(now, formatter, timeZone)}\n`
return `Time sampled while preparing turn ${turn}, step ${step}: ${formatTimestamp(now, formatter, displayTimeZone)}\n`
+ `${renderTimeContextAuthority(authority)}\n`
+ `Elapsed since the preceding ${baseline}: ${elapsed}.`
}
interface PreparationPosition {
turn: number
step: number
}
interface ClaimedPreparation extends PreparationPosition {
messages: UserMessage[]
}
interface AssemblyAuthorityState extends PreparationPosition {
agent: Agent
claimed: readonly UserMessage[]
deferredIds: Set<string>
handledIds: Set<string>
accepting: boolean
lastFingerprint?: string
lastMessageId?: UserMessage['id']
readonly signal: AbortSignal
readonly onAbort: () => void
}
/** Derive the next unopened step while one turn is in pre-step preparation. */
function preparationPosition(agent: Agent): PreparationPosition | undefined {
for (const event of [...agent.session.events].reverse()) {
switch (event.type) {
case 'step/start':
case 'turn/end':
return undefined
case 'step/end':
return { turn: event.data.turn, step: event.data.step + 1 }
case 'turn/start':
return { turn: event.data.turn, step: 1 }
default:
break
}
}
return undefined
}
/** Whether two preparation coordinates identify the same unopened step. */
function samePosition<T extends PreparationPosition>(
left: T | undefined,
right: PreparationPosition,
): left is T {
return left?.turn === right.turn && left.step === right.step
}
/** Whether one message is a time-context reading for an exact preparation. */
function isAuthorityMessage(
message: UserMessage,
position: PreparationPosition,
): boolean {
const source = message.source
return source.kind === 'plugin'
&& source.plugin === name
&& 'authority' in source
&& source.authority.turn === position.turn
&& source.authority.step === position.step
}
/** Reject refresh intervals that cannot represent an exact elapsed-millisecond threshold. */
function validateRefreshInterval(refreshIntervalMs: number | undefined): void {
if (refreshIntervalMs !== undefined && (
@@ -131,37 +242,244 @@ function validateRefreshInterval(refreshIntervalMs: number | undefined): void {
* @param config - time zone and durable refresh scheduling configuration.
* @throws when the refresh interval is invalid or the configured or process time zone cannot be resolved.
*/
export function apply(ctx: Context, config: Config): void {
export function apply(ctx: Context, config: Config): () => void {
const timeZone = config.timeZone
const refreshIntervalMs = config.refreshIntervalMs
validateRefreshInterval(refreshIntervalMs)
let formatter: Intl.DateTimeFormat
const createFormatter = (selectedTimeZone?: string): Intl.DateTimeFormat => new Intl.DateTimeFormat('en-US', {
...(selectedTimeZone === undefined ? {} : { timeZone: selectedTimeZone }),
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23',
timeZoneName: 'longOffset',
})
let fallbackFormatter: Intl.DateTimeFormat
try {
formatter = new Intl.DateTimeFormat('en-US', {
...(timeZone === undefined ? {} : { timeZone }),
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23',
timeZoneName: 'longOffset',
})
fallbackFormatter = createFormatter(timeZone)
} catch (error: unknown) {
const message = timeZone === undefined
? 'time-context: failed to resolve the system time zone'
: `time-context: invalid IANA timeZone ${JSON.stringify(timeZone)}`
throw new Error(message, { cause: error })
}
const resolvedTimeZone = formatter.resolvedOptions().timeZone
const fallbackTimeZone = fallbackFormatter.resolvedOptions().timeZone
const formatters = new Map<string, Intl.DateTimeFormat>([[fallbackTimeZone, fallbackFormatter]])
const claimedPreparations = new Map<Agent, ClaimedPreparation>()
const assemblyAuthorities = new Map<Agent, AssemblyAuthorityState>()
let disposed = false
/** Resolve one Session-owned formatter without making the process zone authoritative. */
const formatterFor = (selectedTimeZone: string): Intl.DateTimeFormat => {
const existing = formatters.get(selectedTimeZone)
if (existing !== undefined) return existing
let created: Intl.DateTimeFormat
try {
created = createFormatter(selectedTimeZone)
} catch (error: unknown) {
throw new Error(`time-context: invalid Session time zone ${JSON.stringify(selectedTimeZone)}`, { cause: error })
}
formatters.set(selectedTimeZone, created)
return created
}
/** Build one current reading without placing it in the inbox or decision. */
const readingFor = (
agent: Agent,
position: PreparationPosition,
messages: readonly UserMessage[],
): { message: UserMessage; fingerprint: string } => {
const now = Date.now()
const previous = position.step === 1
? precedingMessageTime(agent)
: precedingStepContextTime(agent, position.turn)
const sessionTimeZone = agent.session.header.timeZone
const authority: TimeContextAuthority = {
turn: position.turn,
step: position.step,
session: sessionTimeZone === undefined
? { kind: 'unavailable' }
: { kind: 'resolved', timeZone: sessionTimeZone },
client: clientAuthority(requestClientTimeZones(agent, position.turn, messages)),
}
const displayTimeZone = sessionTimeZone ?? fallbackTimeZone
const formatter = sessionTimeZone === undefined
? fallbackFormatter
: formatterFor(sessionTimeZone)
return {
message: createUserMessage({
content: [{
type: 'text',
text: renderText(
now,
position.turn,
position.step,
previous,
formatter,
displayTimeZone,
authority,
),
}],
source: { kind: 'plugin', plugin: name, authority },
}),
fingerprint: JSON.stringify(authority),
}
}
/** Messages added after assembly opened, excluding deferred pre-existing work. */
const assemblyMessages = (state: AssemblyAuthorityState): UserMessage[] =>
state.agent.inbox.nextStep.filter(message => !state.deferredIds.has(message.id))
/** Stop accepting late steering while retaining the state for boundary cleanup. */
const closeAssembly = (state: AssemblyAuthorityState): void => {
state.accepting = false
}
/** Forget one preparation and detach its cancellation observer. */
const clearAssembly = (agent: Agent, state = assemblyAuthorities.get(agent)): void => {
if (state === undefined) return
state.accepting = false
state.signal.removeEventListener('abort', state.onAbort)
if (assemblyAuthorities.get(agent) === state) assemblyAuthorities.delete(agent)
}
/** Append one same-step authority after the messages that caused it. */
const stageAuthority = (state: AssemblyAuthorityState, force: boolean): void => {
if (disposed || !state.accepting) return
const reading = readingFor(
state.agent,
state,
[...state.claimed, ...assemblyMessages(state)],
)
if (!force && reading.fingerprint === state.lastFingerprint) return
state.agent.inject(reading.message)
state.lastFingerprint = reading.fingerprint
state.lastMessageId = reading.message.id
}
/**
* Capture messages claimed for the unopened step. The system-prompt
* assembly itself does not receive this batch, so the preparation listener
* preserves its request-zone provenance explicitly.
*/
ctx.on('agent/inbox/claimed', ({ agent, message, turn }) => {
if (disposed) return
const position = preparationPosition(agent)
if (position === undefined || position.turn !== turn) return
const existing = claimedPreparations.get(agent)
if (!samePosition(existing, position)) {
claimedPreparations.set(agent, { ...position, messages: [message] })
return
}
existing.messages.push(message)
})
/**
* Open the narrow assembly window before downstream prompt providers run.
* The initial authority enters the ordinary next-step outbox; AgentLoop
* drains its closed envelope only after pre-step accepts the step.
*/
ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
if (disposed) return next()
const agent = context.agent
const signal = context.signal
const position = agent === undefined ? undefined : preparationPosition(agent)
if (agent === undefined || signal === undefined || position === undefined || signal.aborted) {
return next()
}
if (samePosition(assemblyAuthorities.get(agent), position)) return next()
clearAssembly(agent)
const now = Date.now()
if (refreshIntervalMs !== undefined && refreshIntervalMs > 0) {
const lastInjection = latestInjectionTime(agent)
if (lastInjection !== undefined
&& now >= lastInjection
&& now - lastInjection < refreshIntervalMs) return next()
}
const claimed = claimedPreparations.get(agent)
const state = {
...position,
agent,
claimed: samePosition(claimed, position) ? [...claimed.messages] : [],
deferredIds: new Set(agent.inbox.nextStep.map(message => message.id)),
handledIds: new Set<string>(),
accepting: true,
signal,
onAbort: () => {},
} satisfies AssemblyAuthorityState
state.onAbort = () => { closeAssembly(state) }
assemblyAuthorities.set(agent, state)
signal.addEventListener('abort', state.onAbort, { once: true })
try {
stageAuthority(state, true)
return await next()
} finally {
closeAssembly(state)
}
}, { prepend: true })
/** A late steering message supersedes the authority synchronously behind it. */
ctx.on('agent/inbox/inserted', ({ agent, message }) => {
if (disposed) return
const state = assemblyAuthorities.get(agent)
if (state === undefined || !state.accepting
|| state.deferredIds.has(message.id)
|| !agent.inbox.nextStep.some(candidate => candidate.id === message.id)
|| message.source.kind !== 'user') return
const handledByReplacement = state.handledIds.has(message.id)
stageAuthority(state, !handledByReplacement)
state.handledIds.add(message.id)
})
/** Recompute after an edit/discard, but do not resurrect a cleared inbox. */
ctx.on('agent/inbox/discarded', ({ agent, message }) => {
if (disposed) return
const state = assemblyAuthorities.get(agent)
if (state === undefined || !state.accepting
|| state.deferredIds.has(message.id)
|| message.source.kind !== 'user') return
if (!agent.inbox.nextStep.some(candidate => isAuthorityMessage(candidate, state))) {
closeAssembly(state)
return
}
stageAuthority(state, false)
state.handledIds = new Set(
assemblyMessages(state)
.filter(candidate => candidate.source.kind === 'user')
.map(candidate => candidate.id),
)
})
ctx.on('agent/pre-step', async (
{ agent, turn, step, signal },
next,
): Promise<PreStepDecision> => {
const wasDisposed = (): boolean => disposed
if (wasDisposed()) return next()
const decision = await next()
if (decision.kind === 'reject' || signal.aborted) return decision
if (wasDisposed()) return decision
const staged = assemblyAuthorities.get(agent)
if (decision.kind === 'reject' || signal.aborted) {
if (samePosition(staged, { turn, step })) closeAssembly(staged)
return decision
}
if (samePosition(staged, { turn, step })) {
closeAssembly(staged)
const reading = readingFor(agent, { turn, step }, decision.messages)
if (reading.fingerprint !== staged.lastFingerprint) {
const replaced = staged.lastMessageId === undefined
? false
: agent.inbox.replace(staged.lastMessageId, reading.message)
if (!replaced) agent.inject(reading.message)
staged.lastFingerprint = reading.fingerprint
staged.lastMessageId = reading.message.id
}
return decision
}
if (decision.messages.length === 0) return decision
const now = Date.now()
if (refreshIntervalMs !== undefined && refreshIntervalMs > 0) {
const lastInjection = latestInjectionTime(agent)
@@ -169,19 +487,51 @@ export function apply(ctx: Context, config: Config): void {
&& now >= lastInjection
&& now - lastInjection < refreshIntervalMs) return decision
}
const previous = step === 1
? precedingMessageTime(agent)
: precedingStepContextTime(agent, turn)
const text = renderText(now, turn, step, previous, formatter, resolvedTimeZone)
const reading = readingFor(agent, { turn, step }, decision.messages)
return {
kind: 'enter',
messages: [
...decision.messages,
createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: name, form: 'snapshot', sections: [{ name, text }] },
}),
reading.message,
],
}
}, { prepend: true })
/** Step/turn/lifecycle boundaries release request-only bookkeeping. */
ctx.on('session/event', (session, event) => {
if (disposed) return
if (event.type !== 'step/start' && event.type !== 'turn/end') return
const agent = ctx.agents.get(session.id)
if (agent === undefined || agent.session !== session) return
clearAssembly(agent)
if (event.type === 'turn/end') claimedPreparations.delete(agent)
})
ctx.on('agent/status', (agent, status) => {
if (disposed) return
if (status !== 'idle') return
clearAssembly(agent)
claimedPreparations.delete(agent)
})
ctx.on('agent/disposed', (agent) => {
if (disposed) return
clearAssembly(agent)
claimedPreparations.delete(agent)
})
return () => {
disposed = true
for (const [agent, state] of assemblyAuthorities) {
closeAssembly(state)
for (const message of [...agent.inbox.nextStep]) {
if (!isAuthorityMessage(message, state)) continue
try {
agent.inbox.remove(message.id)
} catch (error: unknown) {
ctx.logger.warn(`time-context: failed to discard authority during dispose: ${String(error)}`)
}
}
clearAssembly(agent, state)
}
claimedPreparations.clear()
}
}

View File

@@ -3,12 +3,15 @@
import type { Context } from 'cordis'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import { decodeTimeContextSource, renderTimeContextAuthority } from './authority.ts'
const PACKAGE_NAME = '@deepseek-ai/dsh-time-context'
const SOURCE_NAME = 'time-context'
const READING = new RegExp(
'^Time sampled while preparing turn (\\d+), step (\\d+): '
+ '(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2})\\[[^\\]]+\\])\\n'
+ 'Session time zone: ([^.]+)\\.\\n'
+ 'Client time zone for this request: (.+)\\.\\n'
+ 'Elapsed since the preceding (model-visible message|step context): '
+ '(?:unavailable|(?:(?:\\d+d )?(?:\\d+h )?(?:\\d+m )?\\d+s))\\.$',
)
@@ -18,27 +21,43 @@ export const name = 'time-context-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/** Derive the entered step boundary at which a time-context reading may append. */
/**
* Derive the step preparation owned by a time-context reading. A normal
* reading follows `step/start`; a pre-step failure may settle context-only
* output in the still-open turn before that boundary.
*/
function preparationPosition(history: readonly SessionEvent[], fail: InvariantFailure): { turn: number; step: number } {
for (const event of history.slice().reverse()) {
let openTurn: number | undefined
let openStep: number | undefined
let nextStep = 1
for (const event of history) {
switch (event.type) {
case 'step/start':
return { turn: event.data.turn, step: event.data.step }
case 'turn/start':
case 'step/end':
case 'turn/end':
case 'request/header':
case 'assistant/chunk':
case 'assistant/message':
case 'tool/call':
case 'tool/result':
fail('time-context reading must be appended at a prompt boundary')
case 'turn/start': {
openTurn = event.data.turn
openStep = undefined
nextStep = 1
break
}
case 'step/start': {
openStep = event.data.step
break
}
case 'step/end': {
openStep = undefined
nextStep = event.data.step + 1
break
}
case 'turn/end': {
openTurn = undefined
openStep = undefined
break
}
default:
break
}
}
fail('time-context reading must be appended at a prompt boundary')
if (openTurn === undefined) fail('time-context reading must be appended inside an open turn')
return { turn: openTurn, step: openStep ?? nextStep }
}
/** Validate one plugin-attributed time reading against its session position and timestamp. */
@@ -62,7 +81,20 @@ function validateReading(
if (turn !== expected.turn || step !== expected.step) {
fail(`time-context reading names turn ${turn}/step ${step}, expected turn ${expected.turn}/step ${expected.step}`)
}
const baseline = match[4]
let source: ReturnType<typeof decodeTimeContextSource>
try {
source = decodeTimeContextSource(event.data.source)
} catch (error: unknown) {
fail(error instanceof Error ? error.message : String(error))
}
if (source.authority.turn !== turn || source.authority.step !== step) {
fail('time-context text and source authority name different positions')
}
const renderedAuthority = `Session time zone: ${match[4]}.\nClient time zone for this request: ${match[5]}.`
if (renderedAuthority !== renderTimeContextAuthority(source.authority)) {
fail('time-context text and source authority describe different zones')
}
const baseline = match[6]
if ((step === 1) !== (baseline === 'model-visible message')) {
fail(`time-context step ${step} uses the wrong elapsed-time baseline ${JSON.stringify(baseline)}`)
}