fix(time-context): validate durable zone authority

This commit is contained in:
pku-xht
2026-08-07 23:47:51 +08:00
committed by Tianyi Cui
parent 331b29d779
commit edb23439f6
10 changed files with 261 additions and 76 deletions
+151 -42
View File
@@ -1,10 +1,9 @@
// Keyless assembled-browser evidence for the opt-in Schedule overlay. A real
// root Agent receives schedule_create through the complete tool pipeline; the
// one-second owner path and a short explicit at target each queue a best-effort
// followup, commit dispatch, and render the Host's durability-gated reminder
// sidecar. No model fixture is installed: later prompt failure cannot retract
// either receipt.
import { mkdtemp, realpath, rm } from 'node:fs/promises'
// one-second owner path queues a best-effort followup, commits dispatch, and
// renders the Host's durability-gated reminder sidecar. A separate browser
// scenario drives local at through the real zone wire and model tool call.
import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -13,6 +12,7 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { AgentHandle } from '@deepseek-ai/dsh-agent'
import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ReplayEntry } from '@deepseek-ai/dsh-llm-replay'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
@@ -20,7 +20,7 @@ import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { newEnglishPage, saveFailureShot } from './support.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
import {
ScheduleId,
createAfterScheduleRecord,
@@ -177,58 +177,167 @@ describe.skipIf(MODE === 'record')('web e2e: durable after reminder receipt', ()
expect(tripwire.warnings).toEqual([])
}, 60_000)
it('renders a short explicit at reminder through the same durable Web path', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-at'))
await waitForFact(() => agentHandle.agent.status === 'idle', 10_000)
const scheduledAt = new Date(Date.now() + 3_000).toISOString()
const created = await scaffold.ctx.tools.execute({
signal: AbortSignal.timeout(10_000),
callId: CallId('schedule-at-create'),
name: 'schedule_create',
arguments: { prompt: AT_PROMPT, at: scheduledAt },
agent: agentHandle.agent,
})
expect(created.isError).toBe(false)
if (created.isError) throw new Error(created.error.message)
const value = created.value as unknown as CreatedScheduleView
expect(value).toMatchObject({
kind: 'at',
scheduledAt,
deliveryMode: 'session-local',
})
expect(value.id.length).toBeGreaterThan(0)
it('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, ['at-receipt.expected.md', 'receipt.expected.md'])
})
})
await waitForFact(() => agentHandle.agent.session.events.some(event =>
describe.skipIf(MODE === 'record')('web e2e: browser-zone local at reminder', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let tripwire: ReturnType<typeof watchConsole>
let replayDir: string
let scheduledAt: string
beforeAll(async () => {
replayDir = await mkdtemp(join(tmpdir(), 'dsh-schedule-at-wire-replay-'))
const replayOverride = join(replayDir, 'replay.override.json')
const target = Math.ceil((Date.now() + 30_000) / 1_000) * 1_000
scheduledAt = new Date(target).toISOString()
const args = JSON.stringify({
prompt: AT_PROMPT,
at: { date: scheduledAt.slice(0, 10), time: scheduledAt.slice(11, 19) },
})
const callId = CallId('schedule-at-wire-call')
const toolCall: ReplayEntry = {
kind: 'chunks',
chunks: [
{ type: 'block-start', index: 0, blockType: 'tool-call' },
{
type: 'tool-call-delta',
index: 0,
id: callId,
name: 'schedule_create',
argumentsDelta: args,
},
{
type: 'block-end',
index: 0,
block: { type: 'tool-call', id: callId, name: 'schedule_create', arguments: args },
},
{ type: 'usage', usage: { inputTokens: 256, outputTokens: 32 } },
{ type: 'finish', reason: { kind: 'tool-calls' } },
],
}
const textReply = (text: string): ReplayEntry => ({
kind: 'chunks',
chunks: [
{ type: 'block-start', index: 0, blockType: 'text' },
{ type: 'text-delta', index: 0, text },
{ type: 'block-end', index: 0, block: { type: 'text', text } },
{ type: 'usage', usage: { inputTokens: 128, outputTokens: 16 } },
{ type: 'finish', reason: { kind: 'stop' } },
],
})
await writeFile(replayOverride, JSON.stringify([
toolCall,
textReply('The zone-aware reminder is scheduled.'),
textReply('The zone-aware reminder is due.'),
] satisfies ReplayEntry[]))
scaffold = await launchWebScaffold({
extraOverlayPath: OVERLAY,
replayFixture: join(replayDir, 'override-only.jsonl'),
replayOverride,
replayContextWindow: 128_000,
})
browser = await chromium.launch()
page = await browser.newPage({
viewport: { width: 1680, height: 1000 },
locale: 'en-US',
timezoneId: SESSION_TIME_ZONE,
})
await page.addInitScript(() => { localStorage.setItem('dsh.locale', 'en') })
tripwire = watchConsole(page)
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await connectFreshWorkspace(page, scaffold.workspaceCwd, 'schedule-at-wire-e2e')
}, 120_000)
afterAll(async () => {
const failures: unknown[] = []
await browser?.close().catch((error: unknown) => failures.push(error))
await scaffold?.close().catch((error: unknown) => failures.push(error))
await rm(replayDir, { recursive: true, force: true }).catch((error: unknown) => failures.push(error))
if (failures.length === 1) throw failures[0]
if (failures.length > 1) throw new AggregateError(failures, 'Schedule at wire evidence teardown failed')
})
it('carries the browser zone through prompt context, local at, and the durable receipt', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-at-wire'))
const composer = page.locator('textarea:enabled').last()
await composer.fill('Schedule the release-window reminder in my local time.')
const settled = scaffold.whenTurnSettled(60_000)
await page.getByRole('button', { name: 'Send message', exact: true }).click()
const sessionId = await settled
const agent = scaffold.ctx.agents.get(sessionId)
if (agent === undefined) throw new Error('browser-created Schedule Session has no live Agent')
expect(agent.session.header.timeZone).toBe(SESSION_TIME_ZONE)
const request = agent.session.events.find(event =>
event.type === 'user/message'
&& event.data.source.kind === 'user'
&& event.data.content.some(block => block.type === 'text'
&& block.text === 'Schedule the release-window reminder in my local time.'))
if (request?.type !== 'user/message' || request.data.source.kind !== 'user') {
throw new Error('missing browser user-rpc message')
}
expect(request.data.source).toMatchObject({
kind: 'user',
clientTimeZone: SESSION_TIME_ZONE,
})
expect(typeof (request.data.source as { rpcId?: unknown }).rpcId).toBe('string')
const timeContextIndex = agent.session.events.findIndex(event =>
event.type === 'user/message'
&& event.data.source.kind === 'plugin'
&& event.data.source.plugin === 'time-context'
&& event.data.content.some(block => block.type === 'text'
&& block.text.includes('Session time zone: UTC.')
&& block.text.includes('Client time zone for this request: UTC.')))
const toolCallIndex = agent.session.events.findIndex(event =>
event.type === 'tool/call' && event.data.name === 'schedule_create')
expect(timeContextIndex).toBeGreaterThanOrEqual(0)
expect(toolCallIndex).toBeGreaterThan(timeContextIndex)
const created = agent.session.events.find(event =>
event.type === 'schedule/change'
&& (event.data as { operation?: unknown; id?: unknown }).operation === 'dispatch'
&& (event.data as { id?: unknown }).id === value.id), 15_000)
await expect(scaffold.ctx.sessions.flush(agentHandle.agent.session)).resolves.toBe(true)
&& event.data.operation === 'create'
&& event.data.schedule.kind === 'at'
&& event.data.schedule.scheduledAt === scheduledAt)
if (created?.type !== 'schedule/change' || created.data.operation !== 'create') {
throw new Error('local at tool call did not create its durable record')
}
const scheduleId = created.data.schedule.id
await waitForFact(() => agent.session.events.some(event =>
event.type === 'schedule/change'
&& event.data.operation === 'dispatch'
&& event.data.id === scheduleId), 45_000)
await agent.whenIdle()
await expect(scaffold.ctx.sessions.flush(agent.session)).resolves.toBe(true)
const history = await scaffold.ctx.apiProxy.sessions.history({
rpcId: RpcId('schedule-at-history'), payload: { sessionId: agentHandle.agent.id },
rpcId: RpcId('schedule-at-wire-history'),
payload: { sessionId },
})
if (!history.result.ok) throw new Error(history.result.error.message)
expect(history.result.value.events?.find(entry =>
entry.event.type === 'schedule/change'
&& (entry.event.data as { operation?: unknown; id?: unknown }).operation === 'dispatch'
&& (entry.event.data as { id?: unknown }).id === value.id)?.view).toMatchObject({
for: 'event', presentationKey: 'schedule/reminder',
&& entry.event.data.operation === 'dispatch'
&& entry.event.data.id === scheduleId)?.view).toMatchObject({
for: 'event',
view: { scheduleId, prompt: AT_PROMPT, occurrenceAt: scheduledAt },
})
const receipt = page.locator(AT_RECEIPT_SELECTOR)
await receipt.waitFor({ timeout: 15_000 })
expect(await receipt.getByText(AT_PROMPT, { exact: true }).count()).toBe(1)
expect(await receipt.getByText('Delivered in this session only', { exact: true }).count()).toBe(1)
await receipt.waitFor({ timeout: 20_000 })
const snapshot = (await captureStableAria(page, AT_RECEIPT_SELECTOR, scaffold.workspaceCwd))
.split(value.id).join('{{scheduleId}}')
.split(scheduleId).join('{{scheduleId}}')
.replace(/\d{4}-\d{2}-\d{2}T(?:\d{2}:\d{2}:\d{2}\.\d{3}|\{\{clock\}\})Z/gu, '{{occurrenceAt}}')
await compareOrRefreshGolden(AT_RECEIPT_EXPECTED, snapshot, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
it('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, ['at-receipt.expected.md', 'receipt.expected.md'])
})
})
describe.skipIf(MODE === 'record')('web e2e: Schedule restart, fork, and cold history', () => {
+1 -1
View File
@@ -1884,7 +1884,7 @@ export interface Config {
}
```
Source: [`packages/context/time-context/src/index.ts:28`](../packages/context/time-context/src/index.ts)
Source: [`packages/context/time-context/src/index.ts:29`](../packages/context/time-context/src/index.ts)
## `@deepseek-ai/dsh-tmux-context`
+3 -24
View File
@@ -14,6 +14,7 @@ import {
deriveClientTimeZoneContext,
renderTimeZoneContext,
} from './request-zone.ts'
import { createTimestampFormatter, formatTimestamp } from './timestamp.ts'
export type { ClientTimeZoneContext } from './request-zone.ts'
export { deriveClientTimeZoneContext } from './request-zone.ts'
@@ -38,17 +39,6 @@ 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. */
function formatTimestamp(now: number, formatter: Intl.DateTimeFormat, timeZone: string): string {
const parts = Object.fromEntries(
formatter.formatToParts(now).map(part => [part.type, part.value]),
) as Record<TimestampPart, string>
const offset = parts.timeZoneName.replace(/^GMT$/, 'GMT+00:00').slice(3)
return `${parts['year']}-${parts['month']}-${parts['day']}T${parts['hour']}:${parts['minute']}:${parts['second']}${offset}[${timeZone}]`
}
/** Format a non-negative elapsed millisecond count as compact whole-second units. */
function formatDuration(elapsedMs: number): string {
let seconds = Math.floor(Math.max(0, elapsedMs) / 1000)
@@ -160,20 +150,9 @@ export function apply(ctx: Context, config: Config): () => void {
const timeZone = config.timeZone
const refreshIntervalMs = config.refreshIntervalMs
validateRefreshInterval(refreshIntervalMs)
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 {
fallbackFormatter = createFormatter(timeZone)
fallbackFormatter = createTimestampFormatter(timeZone)
} catch (error: unknown) {
const message = timeZone === undefined
? 'time-context: failed to resolve the system time zone'
@@ -190,7 +169,7 @@ export function apply(ctx: Context, config: Config): () => void {
if (existing !== undefined) return existing
let created: Intl.DateTimeFormat
try {
created = createFormatter(selectedTimeZone)
created = createTimestampFormatter(selectedTimeZone)
} catch (error: unknown) {
throw new Error(`time-context: invalid Session time zone ${JSON.stringify(selectedTimeZone)}`, { cause: error })
}
@@ -4,6 +4,7 @@ import type { Context } from 'cordis'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import { deriveClientTimeZoneContext, renderTimeZoneContext } from './request-zone.ts'
import { createTimestampFormatter, formatTimestamp } from './timestamp.ts'
const PACKAGE_NAME = '@deepseek-ai/dsh-time-context'
const SOURCE_NAME = 'time-context'
@@ -140,6 +141,22 @@ function validateReading(
|| event.time < renderedTime) {
fail('time-context rendered timestamp must parse and not postdate its durable event')
}
const sessionTimeZone = session.header.timeZone
if (sessionTimeZone !== undefined) {
let expectedTimestamp: string
try {
expectedTimestamp = formatTimestamp(
renderedTime,
createTimestampFormatter(sessionTimeZone),
sessionTimeZone,
)
} catch (error: unknown) {
fail(`time-context Session time zone cannot format its durable timestamp: ${String(error)}`)
}
if (rendered !== expectedTimestamp) {
fail('time-context rendered timestamp does not match the Session time zone')
}
}
}
/* jscpd:ignore-start -- package companions share replay and dispatch plumbing */
@@ -12,6 +12,8 @@ export type ClientTimeZoneContext =
function clientTimeZone(message: UserMessage): string | undefined {
const source = message.source
return source.kind === 'user'
&& 'rpcId' in source
&& typeof source.rpcId === 'string'
&& 'clientTimeZone' in source
&& typeof source.clientTimeZone === 'string'
? source.clientTimeZone
@@ -0,0 +1,37 @@
/** ISO-shaped time-context timestamp formatting shared by production and replay validation. */
type TimestampPart = 'day' | 'hour' | 'minute' | 'month' | 'second' | 'timeZoneName' | 'year'
/**
* Create the exact formatter used by durable time-context readings.
* @param timeZone - Explicit display zone, or `undefined` for the process fallback.
* @returns A formatter with stable numeric local fields and long numeric offset.
*/
export function createTimestampFormatter(timeZone?: string): Intl.DateTimeFormat {
return 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',
})
}
/**
* Format an epoch millisecond value as an ISO-shaped timestamp with offset and IANA zone.
* @param now - Epoch milliseconds to display.
* @param formatter - Formatter created for `timeZone`.
* @param timeZone - Canonical zone label carried in brackets.
* @returns The durable timestamp text.
*/
export function formatTimestamp(now: number, formatter: Intl.DateTimeFormat, timeZone: string): string {
const parts = Object.fromEntries(
formatter.formatToParts(now).map(part => [part.type, part.value]),
) as Record<TimestampPart, string>
const offset = parts.timeZoneName.replace(/^GMT$/, 'GMT+00:00').slice(3)
return `${parts['year']}-${parts['month']}-${parts['day']}T${parts['hour']}:${parts['minute']}:${parts['second']}${offset}[${timeZone}]`
}
@@ -126,7 +126,7 @@ describe('time-context invariants', () => {
session.append('turn/start', { turn: 1 })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'travel request' }],
source: { kind: 'user', clientTimeZone: 'America/New_York' } as never,
source: { kind: 'user', rpcId: 'travel-request', clientTimeZone: 'America/New_York' } as never,
}), { surfaceOp: 'append' })
session.append('step/start', { turn: 1, step: 1 })
@@ -135,7 +135,7 @@ describe('time-context invariants', () => {
'1',
'1',
'model-visible message',
'2026-07-14T00:00:00+00:00[UTC]',
'2026-07-14T08:00:00+08:00[Asia/Shanghai]',
'Asia/Shanghai',
'America/New_York',
)))
@@ -145,11 +145,48 @@ describe('time-context invariants', () => {
'1',
'1',
'model-visible message',
'2026-07-14T00:00:00+00:00[UTC]',
'2026-07-14T08:00:00+08:00[Asia/Shanghai]',
'Asia/Shanghai',
'Asia/Shanghai',
)))
}).toThrow(/does not match the Session and current request zones/)
expect(() => {
ctx.emit('session/event', session, event(reading(
'1',
'1',
'model-visible message',
'2026-07-14T00:00:00+00:00[UTC]',
'Asia/Shanghai',
'America/New_York',
)))
}).toThrow(/rendered timestamp does not match the Session time zone/)
})
it('rejects a durable reading whose Session zone cannot format the timestamp', async () => {
const ctx = await setup()
const id = SessionId('time-invariant-invalid-zone')
const session = Session.create(id, [], {
version: 0,
id,
createdAt: SECOND,
timeZone: 'Invalid/Zone',
})
session.append('turn/start', { turn: 1 })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'invalid zone request' }],
source: { kind: 'user' },
}), { surfaceOp: 'append' })
session.append('step/start', { turn: 1, step: 1 })
expect(() => {
ctx.emit('session/event', session, event(reading(
'1',
'1',
'model-visible message',
'2026-07-14T00:00:00+00:00[UTC]',
'Invalid/Zone',
)))
}).toThrow(/Session time zone cannot format its durable timestamp/)
})
it('rejects a time-context source that duplicates request authority', async () => {
@@ -11,7 +11,7 @@ function request(clientTimeZone?: unknown) {
content: [{ type: 'text', text: 'request' }],
source: clientTimeZone === undefined
? { kind: 'user' }
: { kind: 'user', clientTimeZone } as never,
: { kind: 'user', rpcId: 'request-zone', clientTimeZone } as never,
})
}
@@ -27,6 +27,10 @@ describe('request-zone derivation', () => {
source: { kind: 'plugin', plugin: 'fixture' },
})
expect(deriveClientTimeZoneContext([plugin, request(), request(1)])).toEqual({ kind: 'missing' })
expect(deriveClientTimeZoneContext([createUserMessage({
content: [],
source: { kind: 'user', clientTimeZone: 'Asia/Shanghai' } as never,
})])).toEqual({ kind: 'missing' })
expect(deriveClientTimeZoneContext([
request('Asia/Shanghai'),
request('Asia/Shanghai'),
@@ -104,7 +104,7 @@ async function fire(
function rpcMessage(text: string, clientTimeZone: string): UserMessage {
return createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'user', clientTimeZone } as never,
source: { kind: 'user', rpcId: `rpc-${text}`, clientTimeZone } as never,
})
}
@@ -95,7 +95,7 @@ function appendRequestContext(agent: Agent, clientTimeZones: readonly string[]):
for (const [index, clientTimeZone] of clientTimeZones.entries()) {
agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: `request ${index + 1}` }],
source: { kind: 'user', clientTimeZone } as never,
source: { kind: 'user', rpcId: `request-zone-${String(index + 1)}`, clientTimeZone } as never,
}), { surfaceOp: 'append' })
}
const text = 'time context'
@@ -273,7 +273,7 @@ describe('Schedule tool protocol', () => {
unmarked.agent.session.append('step/start', { turn: 1, step: 1 })
unmarked.agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'request without time reading' }],
source: { kind: 'user', clientTimeZone: 'Asia/Shanghai' } as never,
source: { kind: 'user', rpcId: 'unmarked-request', clientTimeZone: 'Asia/Shanghai' } as never,
}), { surfaceOp: 'append' })
expect(value(await execute(unmarked, 'schedule_create', {
prompt: 'unmarked', at: { date: '2026-08-06', time: '09:00:00' },
@@ -365,7 +365,7 @@ describe('Schedule tool protocol', () => {
test.agent.session.append('step/start', { turn: 1, step: 1 })
test.agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'request' }],
source: { kind: 'user', clientTimeZone: 'Asia/Shanghai' } as never,
source: { kind: 'user', rpcId: 'array-like-request', clientTimeZone: 'Asia/Shanghai' } as never,
}), { surfaceOp: 'append' })
const text = 'time context'
test.agent.session.append('user/message', createUserMessage({
@@ -400,7 +400,7 @@ describe('Schedule tool protocol', () => {
test.agent.session.append('step/start', { turn: 1, step: 1 })
test.agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'request' }],
source: { kind: 'user', clientTimeZone: 'Asia/Shanghai' } as never,
source: { kind: 'user', rpcId: 'malformed-marker-request', clientTimeZone: 'Asia/Shanghai' } as never,
}), { surfaceOp: 'append' })
test.agent.session.append('user/message', createUserMessage({
content: [block as never],